HHVM & Hack available at ServerGrove

hhvm It is well known that Facebook is currently the largest site built using PHP, but not everyone knows that they have created their own new language called Hack. It’s based on PHP and contains several new features and improvements. You can start learning about Hack with their online tutorial.

To run Hack they created a new runtime platform, HHVM. HHVM can provide big performance gains, which allowed Facebook to run their site in less servers, saving lots of money. Both HHVM and Hack have been released to the public so everyone outside of Facebook can use them.

HHVM is not fully compatible with PHP, yet. The HHVM development team is working hard to increase compatibility and they already support most of the major PHP frameworks. Having said this, there is a lot of custom PHP code that may not run in HHVM, so before you go ahead and install HHVM in your production servers, test your applications and sites really well.

As we said, Hack is very similar to PHP, a simple script looks like this:

<?hh
echo "Hi, I'm a Hack script!";

There are other bigger differences, and lots of new features that are worth checking.

To run this script, you will use the hhvm command:

$ hhvm script.php

In fact, you can use composer with hhvm, which will provide a big boost in performance:

$ hhvm composer.phar update

HHVM also includes a web server and a FastCGI server, giving you the option to run a standalone HHVM server or configure your Apache or NGINX using FastCGI.

Installing HHVM & Hack on ServerGrove

We have created customized packages for our customers so they can install HHVM in their VPS following some simple steps. Currently HHVM is available for Centos 6.x and Ubuntu 12.04 VPSes. We are preparing packages for additional distributions.

For Centos 6.x:

$ yum install hhvm

For Ubuntu 12.04:

$ apt-get update && apt-get install hhvm

Once installed, you can find HHVM configuration files in /etc/hhvm. The log files will be located in /var/log/hhvm. All the HHVM commands are located in /opt/hhvm/bin which is added to your PATH configuration.

Configuring HHVM

The /etc/hhvm/server.hdf file will contain most configuration options pertaining to the web and fastcgi servers.

Once configured, you can start the HHVM server by running:

$ /etc/init.d/hhvm start

If you need more control, you can run:

# spawn daemon on the background
$ /opt/hhvm/bin/hhvm --config /etc/hhvm/server.hdf --user www-data --mode daemon
# keep server in foreground
$ /opt/hhvm/bin/hhvm --config /etc/hhvm/server.hdf --user www-data --mode server

HHVM & FastCGI

If you want to use the fastcgi mode, you will need to set this configuration in server.hdf

Server {
  Type = fastcgi
  FileSocket = /var/run/hhvm/sock
  Port = 9000
}

This will make the server available in port 9000 or /var/run/hhvm/sock. Then you need to configure Apache with mod_fastcgi support. Install and enable mod_fastcgi and add this to the Apache configuration:

 FastCgiExternalServer /var/www/html -host 127.0.0.1:9000

Our installation also includes hh_single_type_check, hh_client and hh_server which are used to run the typecheker.

Using our packages outside of ServerGrove

If you want to use our packages in your servers or VMs outside of ServerGrove, you are more than welcome. Follow the instructions on setting up our repository in your server and then install hhvm as described above:
- Centos 6.x
- Ubuntu 12.04

Conclusion

HHVM and Hack bring a new perspective into PHP and provide big performance improvements. We will keep updating our repository with new versions as they come out. At the time of publishing this post, you will install HHVM 3.0.1. And don’t forget to share with us your experience with HHVM & Hack!

abril 03 / 2014
Author Pablo
Category PHP, VPS
Comments 1 Comment
Tags ,

Deployment of Symfony2 applications with Ansible

iStock_000017954524Small

Ansible is a powerful automation engine that simplifies deploying systems and apps. Its popularity has been rising rapidly as developers and system administrators look for simpler ways to manage servers and deploy applications.

The selling points of Ansible are:

  • simplicity: the configuration is done through INI and YAML files
  • agentless: there is no agent to install, making it dead easy to use on virtual servers and shared hosting
  • extensible: thanks to roles and modules, it is very easy to extend its functionality and reuse of configuration blocks

If you follow our blog you will know that here at ServerGrove we have been promoting the use of Capifony, an extension for Capistrano that is targeted to the deployment of Symfony2 applications. However Capifony & Capistrano are based on Ruby, and every time we run into an issue, we need to dig to find out where the problem lies. In addition, with every major upgrade of Capistrano, something breaks, especially in our case where we have a fairly complex deployment process. So, we decided to give Ansible a try.

Installing and setting up Ansible

Installing Ansible is simple. The only requirement is to have Python installed in the computer where you will be launching the Ansible process. You can follow the manual instructions which recommends several ways to get Ansible running.

Once it is installed, Ansible will connect over SSH to the remote servers that it controls. The servers that you want to control need to be listed in a simple text file following the INI format, this is called the inventory:

192.168.1.50
aserver.example.org
bserver.example.org

You can group servers in sets so you can define a list of servers for testing, qa, production, etc.

[testing]
test1.example.com

[prod]
www1.example.com
www2.example.com
www[10:50].example.com

There are more complex groupings you can do, so it covers pretty much any need you can think of.

Now, you can execute simple commands with Ansible:

# send a ping command to all servers
$ ansible all -m ping
# print hostname and logged in users
$ ansible all -a "hostname -f && w"

You can also copy files:

$ ansible prod -m copy -a "src=/etc/hosts dest=/tmp/hosts"

You can manage users, OS packages (rpm, deb, etc), services (ie. stop/start/restart apache) and much more thanks to modules.

The real power comes when you start defining these commands as tasks:

---
- hosts: webservers
  vars:
    http_port: 80
    max_clients: 200
  remote_user: root
  tasks:
  - name: ensure apache is at the latest version
    yum: pkg=httpd state=latest
  - name: write the apache config file
    template: src=/srv/httpd.j2 dest=/etc/httpd.conf
    notify:
    - restart apache
  - name: ensure apache is running
    service: name=httpd state=started
  handlers:
    - name: restart apache
      service: name=httpd state=restarted

The above code tells Ansible that these tasks apply to all hosts in the group “webservers”, then we define the variables, and then create a list of tasks that will be run in sequence. If any of these tasks fail, the process will stop.

All of these can be store in files called playbooks, and these playbooks can depend on other playbooks, including external files, etc, making it really extensible and easy.

Furthermore, you can combine these playbooks into “roles” (similar to Symfony2′s bundles), so you can reuse these across projects. These roles can be shared by uploading them to Ansible Galaxy, a site where people share roles for common needs.

There is a lot more that Ansible can do, but for this article we want to focus on Symfony2, for more information we recommend that you read the excellent Ansible documentation.

Deploying Symfony2

kittens

As you may know, successfully deploying a Symfony2 application involves several steps, the most common ones are:

  • define a directory structure in the server which will allow to setup the Symfony2 application before making it live and keep multiple releases, so you can rollback to a previous release if something fails.
  • clone your git repository
  • run composer install to get all your dependencies
  • warm up the cache / dump Assetic assets
  • update the web server document root to point the site to the new release
If you used Capifony before, these are more or less the steps that are done every time you run cap deploy.

In Ansible this is easily achievable by defining a set of tasks in a playbook and then running them and you can put all of these in a role, so you can reuse it in all your Symfony2 projects.

Introducing the Ansible Symfony2 Deployment Role

We have created a role that you can start using now to easily deploy your Symfony2 apps. It is available in the Ansible Galaxy and in Github so anyone can contribute enhancements and fixes.

To use the role in your project, once you installed Ansible, run the following command to make the role available in your computer:

$ ansible-galaxy install servergrove.symfony2

The next step is to define the inventory (the list of servers where you will deploy your application). Create a file app/config/hosts.ini

[prod]
server.example.com

Next, you need to create a YAML file that will contain the variables and basic instructions for Ansible, name it app/config/deploy.yml

---
- hosts: prod
  vars:
    symfony2_project_name: your_project
    symfony2_project_root: /var/www/vhosts/example.com
    symfony2_project_repo: git@github.com:username/your_repo.git
    ansible_ssh_user: your_ssh_username

  roles:
    - servergrove.symfony2

  tasks:
    - local_action: osx_say msg="Deployment complete." voice=Zarvox

We defined a configuration for the prod group of servers. We define the variables needed for the role to deploy our project. It will run the role and when it finishes it will run the osx_say task locally that will inform us that the process has completed (this only works in Mac OSX, but you can replace it with an action that will send an email or some other form of notification).

Once you have these, you are ready to deploy your application with Ansible, do it with the following command:

$ ansible-playbook -l prod -i app/config/hosts.ini -e "symfony2_project_release=1" app/config/deploy.yml -vvv --check
  • the -l option defines which list of servers we want to run this for
  • the -i option defines the inventory file that contains the list of servers
  • the -e option allows us to define variables that will be used in the playbook. In this case we tell Ansible that we will deploy a release named “1″. You can replace this with `date +%Y%m%d%H%M%S` to name our releases with the current date/time.
  • Then we define the playbook we will use and the -vvv option allows us to see what Ansible is doing.

The servergrove.symfony2 role will perform the following tasks:

  • creates the directory structure to host your app, following a similar structure from Capifony:
  • clones the git repository
  • if composer is not installed or is outdated, it will download the phar in the project directory
  • runs composer install –no-dev –optimize-autoload
  • runs app/console assetic:dump
  • creates/updates current symlink to point to new release

This is the first version of this role, we hope to improve the functionality with new features, if you have any suggestions, please open an issue or send your pull request.

As we said before, Ansible does not need anything special in the server, it only needs to be able to connect to the server using SSH. This means that it can be used with any of our VPS plans and in all Developer and Business shared hosting plans, making the deployment of Symfony2 apps really easy and predictable.

abril 01 / 2014
Author Pablo
Category PHP, Symfony, Tutorials
Comments 7 Comments

Symfony2 components overview: Finder

This is the 9th post in this series about the Symfony2 components. Today’s post covers one of the most popular PHP packages: the Finder component. According to Packagist it is the 14th most used PHP package, and the 3rd Symfony2 component, only after the EventDispatcher and Console ones.

The Finder component gives you an excellent lens to find files and directories

Installation

Installation is really easy using Composer:

{
    "require": {
        "symfony/finder": "2.4.*"
    }
}

If you have never used Composer before, check out our Composer 101 post.

The component

The Finder component helps us to find files and directories using a fluent interface. It provides useful methods to find what we are looking for, based on name, location, size, modification date as well as many other filters.

<?php

use Symfony\Component\Finder\Finder;

include_once __DIR__. '/vendor/autoload.php';

$finder = new Finder();
$finder->in(__DIR__)->files();

foreach ($finder as $file) {
    echo $file->getRealpath() . PHP_EOL;
}

In the above example, we create a Finder instance and configure it to find only files in the current directory, as we are using the files() method. Then, as the Finder instance is a PHP iterator (implements IteratorAggregate), we can use it in a foreach loop. For each of the files found we print its real path.

Instead of iterating over the Finder with the foreach loop, we can also use some of the functions that PHP provides to deal with iterators, such as iterator_to_array and iterator_count:

$filesNumber = iterator_count($finder);
$files = iterator_to_array($finder);

var_dump($filesNumber, $files);

Assuming we only have a PHP file in the current directory, we get as output:

int(1)
array(1) {
  '/Users/raulfraile/Sites/sgposts/finder/index.php' =>
  class Symfony\Component\Finder\SplFileInfo#13 (4) {
    private $relativePath =>
    string(0) ""
    private $relativePathname =>
    string(9) "index.php"
    private $pathName =>
    string(48) "/Users/raulfraile/Sites/sgposts/finder/index.php"
    private $fileName =>
    string(9) "index.php"
  }
}

Criteria

The official documentation for this component has a huge list of available options to find files and directories and it does not make sense to repeat all of them in this article. Here is an overview of a few important ones:

  • in($dirs): The location is the only mandatory criteria. Accepts one or more directories.
  • files() / directories(): Restricts the matching to files/directories only.
  • depth(): Restrict the depth of traversing. Accepts expressions such as ‘== 0′ or ‘<3′.
  • exclude($dirs): Exclude directories.
  • ignoreVCS($ignoreVCS): Ignores version control directories such as “.git”.
  • name($pattern): Find files that matches the pattern. Patterns can be globs, strings, or regular expressions.
  • contains($pattern): Find files with content that matches the pattern. Patterns can be strings or regular expressions.
  • filter($closure) / sort($closure): These methods accept a closure as input parameter, so we can customize filtering and sorting.

Under the hood

Adapters

The component relies on the Adapter pattern to provide different ways to find files/directories depending on the user’s machine. It ships with 3 adapters: PhpAdapter, GnuFindAdapter and BsdFindAdapter.

The PhpAdapter is always available and performs the search using PHP iterators, whilst GnuFindAdapter and BsdFindAdapter are only available in Unix/BSD operating systems. Their goal is to improve the performance by using shell commands like “find, “sort”, “cut” or “grep”. For example, these are the generated commands for some common criteria using the BsdFindAdapter (e.g. MacOS):

// find  -E '/dir' -noleaf -mindepth 1 -not \( -regex '.*(^|/)\..+(/|$).*' \)
$finder->in(__DIR__);

// find  -E '/dir' -noleaf -mindepth 1 -maxdepth 1 -type f -not \( -regex '.*(^|/)\..+(/|$).*' \)
$finder->in(__DIR__)->files()->depth(0);

// find  -E '/dir' -noleaf -mindepth 1
$finder->in(__DIR__)->ignoreDotFiles(false);

// find  -E '/dir' -noleaf -mindepth 1 -type f \( -name '*.php' \) -not \( -regex '.*(^|/)\..+(/|$).*' \)
$finder->in(__DIR__)->files()->name('*.php');

// find  -E '/dir' -noleaf -mindepth 1 -type f -not \( -regex '.*(^|/)\..+(/|$).*' \) | grep -v '^$' | xargs -I{} grep -I -l -Ee 'hello' {}
$finder->in(__DIR__)->files()->contains('hello');

All the adapters must implement the AdapterInterface interface, which defines the methods to set the criteria. Two of these methods are especially important so the Finder instance can best choose an adapter: isSupported() and getName(). If the adapters extend from the generic AbstractAdapter, it forces to implement the method canBeUsed() instead of isSupported() since the generic implementation provides a cache layer.

The IteratorAggregate interface

As we saw before, the Finder instance implements two PHP interfaces: IteratorAggregate and Countable.

The Countable interface is straightforward since it only defines the count() method. This method is called when we use the count() built-in function, but not with iterator_count().

The IteratorAggregate interface defines one method as well, getIterator(), which must return an object that implements the Traversable interface. Once we use the Finder instance in a foreach loop or as a parameter in iterator_count() or iterator_to_array() functions, this method is called and a new iterator is created.

It is important to note that when searching through multiple locations (passed through the in() method), an instance of AppendIterator is created to have a list of iterators (one for each directory), so iterates one after the other.

SplFileInfo

The component does not use the standard SplFileInfo class for the results, but a class extending it. This class adds support for relative paths and for getting the contents of the files directly with methods such as getRelativePath(), getRelativePathname() and getContents().

Globs

Globs are used in Unix-like environments to perform pattern matching based on wildcard characters. For example, “*.php” to find all the files having the “php” extension. The support for globs in PHP is limited (e.g. it does not work with remote files), but the component solves these limitations converting globs into regular expressions (a tradeoff between performance and usability). So, when using the PhpAdapter with globs, we are actually not using globs internally, but regular expressions.

For example, the following criteria:

$finder->in(__DIR__)->files()->name('*.php');

Generates this regular expression:

"^(?=[^\.])[^/]*\.php$"

Who’s using it

As I said before, it’s one of the most used PHP libraries according to Packagist, so it is not easy at all to list all the projects using it. Here is a short list of them:

More info

Photo: Investigating Nana & Grampas House, by Jae Malone

marzo 26 / 2014

Introducción a Composer

logo-composer-transparent

Composer es una herramienta para la gestión de dependencias en PHP. Nos permite definir, de una manera muy simple, las librerías (paquetes a partir de ahora) que necesita nuestra aplicación y las descarga por nosotros. Sin duda, ha sido una de las mejores noticias de los últimos años en el mundo PHP, ya que ha cambiado radicalmente la forma en la que construimos los proyectos. Es posible encontrar paquetes de calidad para casi cualquier funcionalidad en Packagist.

En este tutorial mostraremos como instalarlo y empezar a utilizarlo.

Instalación

Windows

La forma más sencilla de instalar Composer en Windows es descargar y ejecutar su instalador. También se encargará de actualizar la variable %PATH% para que pueda ser llamado desde cualquier directorio en línea de comandos.

*nix

En sistemas Unix como Linux o Mac OS, la instalación se realiza descargando el instalador con cURL, que no es más que un script PHP que debe ejecutarse. Éste se asegurará que Composer puede ser usado en el sistema. Después, descargará automáticamente el fichero composer.phar, que es el ejecutable de Composer. La instrucción para realizar la instalación sería la siguiente:

$ curl -sS https://getcomposer.org/installer | php

Para instalar Composer de forma global en el sistema y poder usarlo como cualquier otro comando, es tan sencillo como copiar el archivo composer.phar en alguno de los directorios incluidos en la variable de entorno PATH, por ejemplo en /usr/local/bin:

$ mv composer.phar /usr/local/bin/composer

Como funciona

Composer es un gestor de dependencias por proyecto, por lo que los paquetes necesarios para nuestra aplicación no se instalarán globalmente como ocurre con PEAR, sino en un directorio especial llamado vendor. Después, simplemente añadiendo un require_once del archivo de autoload que genera Composer tendremos acceso a todas nuestras dependencias.

Las dependencias se definen en un archivo llamado composer.json. Un ejemplo simple sería el siguiente:

    &quot;require&quot;: {
        &quot;fabpot/goutte&quot;: &quot;v1.0.5&quot;
    }

En el ejemplo anterior definimos una única dependencia, el paquete “fabpot/goutte”, que sirve para hacer screen scrapping y web crawling en PHP.

Cuando ejecutamos “composer install”, Composer comprueba las dependencias que hemos definido y descarga los paquetes necesarios para satisfacerlas:

$ composer install
Loading composer repositories with package information
Installing dependencies (including require-dev)
  - Installing symfony/event-dispatcher (v2.4.2)
    Downloading: 100%
  - Installing guzzle/common (v3.8.1)
    Downloading: 100%
  ...
  - Installing symfony/browser-kit (v2.4.2)
    Downloading: 100%
  - Installing fabpot/goutte (v1.0.5)
    Downloading: 100%

symfony/event-dispatcher suggests installing symfony/dependency-injection ()
symfony/event-dispatcher suggests installing symfony/http-kernel ()
Writing lock file
Generating autoload files

Como puedes comprobar, Composer no solo ha descargado “fabpot/goutte”, también algunos paquetes adicionales. Esto se debe a que el paquete “fabpot/goutte” define a su vez otras dependencias – se pueden comprobar en su archivo composer.json -, pero no nos tenemos que preocupar de esto, Composer se encarga automáticamente de calcular el grafo de dependencias. Este es un proceso bastante costoso si el número de dependencias crece, por lo que Composer intenta minimizar el problema creando el archivo composer.lock, que contiene las versiones exactas de todos los paquetes a instalar. Así, la próxima vez que ejecutemos “composer install”, Composer simplemente leerá el archivo composer.lock y descargará los paquetes. Si modificamos el archivo composer.json, necesitamos ejecutar “composer update” para que el archivo composer.lock se calcule de nuevo.

“composer update” es el nuevo “compilando…” :)

Una vez que los paquetes se han descargado, Composer genera un archivo php que debemos incluir en el proyecto para poder utilizar las dependencias. Éste archivo registra un autoloader PSR-0/PSR-4, lo que facilita hacer uso de las dependencias:

&lt;?php

require_once __DIR__.'/vendor/autoload.php';

use Goutte\Client;

$client = new Client();
$crawler = $client-&gt;request('GET', 'http://www.servergrove.com');
$title = $crawler-&gt;filter('head &gt; title')-&gt;text();
echo $title;

En el ejemplo anterior, incluímos el archivo autogenerado autoload.php, y a continuación creamos un objeto de la clase Goutte\Client, que forma parte del paquete “fabpot/goutte”. Por último, utilizamos el objeto para extraer el contenido de la etiqueta title de la página web de ServerGrove.

El comando composer

Ya hemos hablado de los comandos “install” y “update”, pero Composer dispone de algunos comandos adicionales. “composer list” muestra una lista de todos los comandos disponibles:

$ composer list
   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/

Available commands:
  about            Short information about Composer
  archive          Create an archive of this composer package
  config           Set config options
  create-project   Create new project from a package into given directory.
  depends          Shows which packages depend on the given package
  diagnose         Diagnoses the system to identify common errors.
  dump-autoload    Dumps the autoloader
  dumpautoload     Dumps the autoloader
  global           Allows running commands in the global composer dir ($COMPOSER_HOME).
  help             Displays help for a command
  init             Creates a basic composer.json file in current directory.
  install          Installs the project dependencies from the composer.lock file if present, or falls back on the composer.json.
  licenses         Show information about licenses of dependencies
  list             Lists commands
  require          Adds required packages to your composer.json and installs them
  run-script       Run the scripts defined in composer.json.
  search           Search for packages
  self-update      Updates composer.phar to the latest version.
  selfupdate       Updates composer.phar to the latest version.
  show             Show information about packages
  status           Show a list of locally modified packages
  update           Updates your dependencies to the latest version according to composer.json, and updates the composer.lock file.
  validate         Validates a composer.json

Algunos comandos interesantes:

  • init: Crea un archivo composer.json de forma interactiva.
  • validate: Comprueba si el archivo composer.json es válido.
  • selfupdate: Actualiza Composer a la última versión disponible. Si la versión de Composer es demasiado antigua – 30 días o más -, se muestra un mensaje de alerta cuando se ejecuta cualquier comando.
  • search: Busca paquetes en Packagist – por ejemplo, “composer search yaml” -.
  • show: Muestra información sobre los paquetes. Por ejemplo, “composer show –installed” muestra un listado de los paquetes instalados.
  • create-project: Crea un nuevo proyecto a partir de un paquete. Es bastante útil para frameworks – por ejemplo, para crear un nuevo proyecto Symfony2: “php composer.phar create-project symfony/framework-standard-edition path/ 2.4.2″.

Versiones de los paquetes

Hay varias formas de definir qué versión o versiones aceptamos en nuestro proyecto:

  • Versión exacta: “1.4.2″, “v1.4.2″.
  • Rangos: “>=1.0,<2.0", ">=1.0,<1.1 | >=1.2″.
  • Comodines: “1.0.*”, “1.*”.
  • Next Significant Release: “~1.2″ es equivalente a “>=1.2,<2.0".

Estabilidad

Cada una de las versiones de los paquetes dispone de un stability flag, que indica si la versión es “final” o si en cambio se trata de una versión previa (p.ej. beta). Por defecto, solo se tienen en cuenta las versiones finales, por lo que si necesitamos alguna versión no stable, debemos cambiar el parámetro minimum-stability. Los valores disponibles son “dev”, “alpha”, “beta”, “RC” y “stable”. También es posible definir el minimum stability por dependencia, añadiendo “@flag” a la versión del paquete – por ejemplo “”fabpot/goutte”: “1.0.*@dev””.

El directorio “vendor”

Composer crea automáticamente el directorio vendor, que debemos ignorar en el sistema de control de versiones (VCS) – por ejemplo, añadiéndolo al .gitignore en GIT-. Es recomendable incluir el archivo composer.lock en el VCS para que todo el equipo de desarrollo utilice exactamente las mismas versiones de los paquetes. Se acabó la excusa de “funciona en mi ordenador…”!

Más información

Éste artículo es solamente una introducción a Composer y como empezar a utilizarlo en nuestros proyectos para gestionar sus dependencias. Si deseas profundizar más, Composer dispone de una excelente documentación, tanto para creadores de paquetes como para los consumidores.

marzo 19 / 2014
Author Raul Fraile
Category PHP
Comments No Comments

Symfony2 components overview: Translation

This post covers the Symfony Translation component. The component  provides tools to internationalize our applications.

In today’s globalized world internationalizing our applications is a must

The Translation component

Modern applications need to be internationalized and localized to be able to reach people from all over the world. Internationalization – aka i18n – is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes. Localization – aka l10n – is the process of adapting internationalized software for a specific region or language by adding locale-specific components such as dates or currency. The Translation component provides different tools to get your application internationalized, but not localized.

The component is split in three main parts:

  • Catalogues: These are key-value collections of messages. The translator class looks in catalogues to find the required matching key.
  • Loaders: Loaders convert translation resources such as YAML, XLIFF or JSON files into PHP array.
  • Dumpers: Dumpers export a catalogue into a given format.

The component ships with several loaders and dumpers for the most common formats: PHP, XLIFF, JSON, YAML, PO, MO, INI and CSV. It it can also load PHP arrays.

Simple example

Let’s start with a very simple example. What we want to do here is translate a few strings into Spanish and Portuguese. Take a look at the code:

<?php

use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\Translator;

include_once __DIR__. '/vendor/autoload.php';

$translator = new Translator('es_ES');

$translator->addLoader('array', new ArrayLoader());

$translator->addResource('array', array(
    'hello world!' => '¡hola mundo!',
    'hello %name%!' => '¡hola %name%!'
), 'es_ES');

$translator->addResource('array', array(
    'hello world!' => 'Olá mundo!',
    'hello %name%!' => 'Olá %name%!'
), 'pt');

var_dump($translator->trans('hello world!'));
var_dump($translator->trans('hello %name%!', array('%name%' => 'Raul')));
var_dump($translator->trans('hello %name%!', array('%name%' => 'Raul'), null, 'pt_PT'));

We created the $translator object, which is an instance of Translator, and set “es_ES” as default locale, which is the combination of an ISO 639-1 code for the language and ISO 3166-1 alpha-2 for the country. This will be the default locale.

Then, we add the ArrayLoader loader which allows us to pass the PHP code for the translation messages (instead of messing around with translation files in this basic example). Using this loader, we register translation messages for “es_ES” and “pt”.

Finally, we call the trans() method of the translator object to get the translated strings. The first string will output “¡hola mundo!”, as the default locale is “es_ES”. The second string will call “¡hola Raul!” as we use placeholders, while the last call will display “Olá Raul!”, in portuguese, as the translator first checks for “pt_PT” and then “pt”:

string(13) "¡hola mundo!"
string(12) "¡hola Raul!"
string(10) "Olá Raul!"

Instead of full strings, using keys are recommended:

$translator->addResource('array', array(
    'hello_world' => '¡hola mundo!',
    'hello_name!' => '¡hola %name%!'
), 'es_ES');

Load translations from translation files

Normally, we will have translation files instead of plain PHP arrays. Loading these files is as easy as registering the loader we want to use – specific for the file format – and adding the resource. For example, to load the following YAML file:

messages:
    good_morning: "Buenos días"

errors:
    invalid_email: "Email no válido"

We register a YamlFileLoader instance with the key yaml and then we add the full path of the YAML:

<?php

use Symfony\Component\Translation\Loader\YamlFileLoader;
use Symfony\Component\Translation\Translator;

include_once __DIR__. '/vendor/autoload.php';

$translator = new Translator('es_ES');
$translator->addLoader('yaml', new YamlFileLoader());

$translator->addResource('yaml', __DIR__ . '/translations/es_ES.yml' , 'es_ES');

var_dump($translator->trans('messages.good_morning'));
var_dump($translator->trans('errors.invalid_email'));

The output, as expected, is:

string(12) "Buenos días"
string(16) "Email no válido"

Keep in mind that all provided loaders are dependent on the Config component, and YamlFileLoader has a dependency on the Yaml component too.

Translation domains

In the previous example, we used the same YAML file to define “normal” and error messages, and as nested keys are flatten, we used “messages.good_morning” and “errors.invalid_email” as keys. While this works, it is usually a good idea to have different translation domains, so we can separate translation messages in different files by their purpose. It is common to have domains for normal messages, errors, form messages or validations.

In our example, we could split the YAML file into two files and load them separately indicating the domain. If we don’t provide a domain name, “messages” is the default one.

<?php

use Symfony\Component\Translation\Loader\YamlFileLoader;
use Symfony\Component\Translation\Translator;

include_once __DIR__. '/vendor/autoload.php';

$translator = new Translator('es_ES');
$translator->addLoader('yaml', new YamlFileLoader());

$translator->addResource('yaml', __DIR__ . '/translations/messages.es_ES.yml' , 'es_ES');
$translator->addResource('yaml', __DIR__ . '/translations/errors.es_ES.yml' , 'es_ES', 'errors');

var_dump($translator->trans('good_morning'));
var_dump($translator->trans('invalid_email', array(), 'errors'));

Pluralization

Pluralization is a really hard task to achieve in some languages such as Russian or Polish, as rules can be quite complex. Anyway, the component provides the mechanisms to define these rules and be able to pluralize messages.

For example, to show the message “There is 1 cat” if there is actually only 1 cat, and “There are X cats” if there are two or more, it is defined using options and placeholders:

cats: "There is 1 cat|There are %number% cats"

Then, the transChoice() method of the translator instance takes care of it. It gets the translation key, the number of cats in this example and optionally the placeholder values:

var_dump($translator->transChoice('cats', 1));
var_dump($translator->transChoice('cats', 2, array('%number%' => 2)));

This code will output:

string(14) "There is 1 cat"
string(16) "There are 2 cats"

You can find more examples of pluralization in the Symfony documentation.

Dump messages

The component is also shipped with a few dumpers that allow us to export catalogues into different formats. This can be useful for using third-party translation files or if you just to have them in an easier to read format for a human.

In the following example, we load a catalogue from a YAML file and export it to JSON, using a JsonFileDumper instance:

<?php

use Symfony\Component\Translation\Loader\YamlFileLoader;
use Symfony\Component\Translation\Dumper\JsonFileDumper;

include_once __DIR__. '/vendor/autoload.php';

$loader = new YamlFileLoader();
$catalogue = $loader->load(__DIR__ . '/translations/messages.es_ES.yml' , 'es_ES');

$dumper = new JsonFileDumper();
$dumper->dump($catalogue, array('path' => __DIR__.'/dumps'));

This script creates the file ./dumps/messages.es_ES.yml, with the messages in JSON format:

{
    "good_morning": "Buenos d\u00edas",
    "good_night": "Buenas noches",
    "welcome": "Bienvenido"
}

Custom loaders

So, this is really cool, but what happens when we are trying to modernize a legacy project using Symfony2 components and it uses a weird custom format for translation files? Could we still use the Translation component? Absolutely! We just have to create a loader for that format.

Imagine we have a custom format where translation messages are defined using one line for each translation and parenthesis to wrap the key and the message. A translation file would look like this:

(welcome)(Bienvenido)
(goodbye)(Adios)
(hello)(Hola)

To define a custom loader able to read this kind of files, we must implement the LoaderInterface interface, which defines a load() method. In our loader, this method will get a filename and parse it to create an array. Then, it will create the catalog that will be returned.

<?php

namespace RaulFraile\Loader;

use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Loader\LoaderInterface;

class CustomLoader implements LoaderInterface
{

    public function load($resource, $locale, $domain = 'messages')
    {
        $messages = array();
        $lines = file($resource);

        foreach ($lines as $line) {
            if (preg_match('/\(([^\)]+)\)\(([^\)]+)\)/', $line, $matches)) {
                $messages[$matches[1]] = $matches[2];
            }
        }

        $catalogue = new MessageCatalogue($locale);
        $catalogue->add($messages, $domain);

        return $catalogue;
    }

}

As you see, adding support for new formats is fast and painless, and once you have it, it can be used as any other loader:

<?php

use Symfony\Component\Translation\Translator;
use RaulFraile\Loader\CustomLoader;

include_once __DIR__. '/vendor/autoload.php';

$translator = new Translator('es_ES');
$translator->addLoader('custom', new CustomLoader());

$translator->addResource('custom', __DIR__.'/translations/messages.txt', 'es_ES');

var_dump($translator->trans('hello'));

It outputs:

string(4) "Hola"

That’s the magic of well designed software, extending it is like child’s play.

It is also possible to create a custom dumper for any made-up format invent. To achieve this, a new class implementing the DumperInterface interface must be created. To write the dump contents into a file, extending the FileDumper class is going to save us a few lines.

<?php

namespace RaulFraile\Dumper;

use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Dumper\FileDumper;

class CustomDumper extends FileDumper
{

    public function format(MessageCatalogue $messages, $domain = 'messages')
    {
        $output = '';

        foreach ($messages->all($domain) as $source => $target) {
            $output .= sprintf("(%s)(%s)\n", $source, $target);
        }

        return $output;
    }

    protected function getExtension()
    {
        return 'txt';
    }
}

The format method creates the output string, that will be used by the dump() method of the FileDumper class to create the file. The dumper can be used like any other built-in dumper. In this example, we export the translation messages defined in the YAML file into a text file with our custom format:

<?php

use Symfony\Component\Translation\Loader\YamlFileLoader;
use RaulFraile\Dumper\CustomDumper;

include_once __DIR__. '/vendor/autoload.php';

$loader = new YamlFileLoader();
$catalogue = $loader->load(__DIR__ . '/translations/messages.es_ES.yml' , 'es_ES');

$dumper = new CustomDumper();
$dumper->dump($catalogue, array('path' => __DIR__.'/dumps'));

Conclusion

We have seen that the Translation component makes the arduous task of internazionalizating our application a bit easier, providing all the needed tools to do so: simple translations, placeholders, pluralization, loaders, and dumpers, as well as options to to extend it.

Who’s using it?

More info

Photo: World-map, by greyweed

marzo 18 / 2014