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
  • 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

Upcoming Conferences

WeCamp

PHP New Zealand

PHP Summer Camp Croatia

PHPNE

MadisonPHP

brnoPHP

SymfonyLiveLondon

ZgPHP

PHPSouthAfrica

PHPNWUK

SymfonyLiveNYC

PHPForumParis

PHPARG

PHPWorld

TechMeetupUY

SymfonyConMadrid

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

Upcoming Conferences

WeCamp

PHP New Zealand

PHP Summer Camp Croatia

PHPNE

MadisonPHP

brnoPHP

SymfonyLiveLondon

ZgPHP

PHPSouthAfrica

PHPNWUK

SymfonyLiveNYC

PHPForumParis

PHPARG

PHPWorld

TechMeetupUY

SymfonyConMadrid

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

Upcoming Conferences

WeCamp

PHP New Zealand

PHP Summer Camp Croatia

PHPNE

MadisonPHP

brnoPHP

SymfonyLiveLondon

ZgPHP

PHPSouthAfrica

PHPNWUK

SymfonyLiveNYC

PHPForumParis

PHPARG

PHPWorld

TechMeetupUY

SymfonyConMadrid

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

Upcoming Conferences

WeCamp

PHP New Zealand

PHP Summer Camp Croatia

PHPNE

MadisonPHP

brnoPHP

SymfonyLiveLondon

ZgPHP

PHPSouthAfrica

PHPNWUK

SymfonyLiveNYC

PHPForumParis

PHPARG

PHPWorld

TechMeetupUY

SymfonyConMadrid

Symfony2 components overview: Templating

In the 7th post of this series we are going to talk about a very important component for modern PHP frameworks. This component is usually hidden under a few abstractions to make the life of framework users easier, we are talking about the Templating component.

Templating engines allow to create representations of data

The Templating component

The Templating component provides an engine-agnostic infrastructure to load template files and render them using the input variables. Although the component provides methods for creating template systems, we are going to focus on how to integrate third-party template engines like Twig and Smarty, and how to use the right engine based on the templates file extension.

Templates using PHP

Using PHP to generate templates has been common practice in the PHP community and although this approach has a few advantages, such as not having to learn another language and that templates can be executed without having to compile them, there are also some drawbacks. Some of the drawbacks are:

  • PHP templates are usually more verbose, especially when dealing with output escaping:
    <?php echo htmlspecialchars($var, ENT_QUOTES, 'UTF-8') ?>
    
  • Missing features such as template inheritance, layouts, filters, and extensions.
  • Difficult for web designers who don’t know PHP.

The component provides a default engine to use PHP for templates improving some of these limitations and providing ways to escape output easily, include subtemplates, and common helpers.

Let’s create the simplest template possible, one that says hello using the variable $name, which is passed by the controller:

Hello <?php echo $name ?>! From a humble PHP template.

Then, to render the template:

<?php

use Symfony\Component\Templating\PhpEngine;
use Symfony\Component\Templating\TemplateNameParser;
use Symfony\Component\Templating\Loader\FilesystemLoader;

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

$loader = new FilesystemLoader(__DIR__.'/views/%name%');
$templating = new PhpEngine(new TemplateNameParser(), $loader);

echo $templating->render('hello.php', array('name' => 'Raul'));

It outputs:

Hello Raul! From a humble PHP template.

Here’s an overview of what happened under the hood. First, we create the PhpEngine instance with the required parameters, a couple of objects implementing the TemplateNameParserInterface and LoaderInterface interfaces. Don’t be afraid by all this code, it’s much simpler than it seems.

$loader is an instance of FilesystemLoader, it defines where the templates are – in our example, templates are in the view directory. The TemplateNameParser object converts a string containing the template filename to a TemplateReference object.

Finally, we execute the render method, which renders the template ‘views/hello.php’ with the ‘name’ variable set to ‘Raul’.

This complex setup is the price we pay when working with Symfony components, but as we will see in the next section, this design makes adding new templating engines really easy.

Adding other template engines

When adding new template engines, we create bridges that connect the engine itself with the component. This is done implementing the three methods defined in the EngineInterface interface:

  • render($name, array $parameters = array()): It’s the method in charge of actually rendering the template with the name $name passing the array $parameters as variables.
  • exists($name): Checks if the template with the name $name exists.
  • supports($name): Returns true if the engine bridge is able to process the template with the name $name. Usually, this is done checking the file extension. When there are several engine bridges this method is called to determine which one to use.

So, if we want to add support for Twig templates we just have to implement these methods.

<?php

namespace RaulFraile\Templating;

use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Templating\TemplateReferenceInterface;
use Symfony\Component\Templating\TemplateNameParserInterface;

use Twig_Loader_Filesystem;
use Twig_Environment;

class TwigEngine implements EngineInterface
{

    protected $environment;
    protected $parser;

    /**
     * Constructor.
     *
     * @param \Twig_Environment           $environment A \Twig_Environment instance
     * @param TemplateNameParserInterface $parser      A TemplateNameParserInterface instance
     * @param string                      $dir         Templates directory
     */
    public function __construct(\Twig_Environment $environment, TemplateNameParserInterface $parser, $dir)
    {
        $loader = new Twig_Loader_Filesystem($dir);

        $this->environment = $environment;
        $this->environment->setLoader($loader);

        $this->parser = $parser;
    }

    /**
     * Renders a template.
     *
     * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance
     * @param array $parameters An array of parameters to pass to the template
     *
     * @return string The evaluated template as a string
     *
     * @throws \RuntimeException if the template cannot be rendered
     *
     * @api
     */
    public function render($name, array $parameters = array())
    {
        return $this->environment->loadTemplate((string) $name)->render($parameters);
    }

    /**
     * Returns true if the template exists.
     *
     * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance
     *
     * @return Boolean true if the template exists, false otherwise
     *
     * @throws \RuntimeException if the engine cannot handle the template name
     *
     * @api
     */
    public function exists($name)
    {
        $loader = $this->environment->getLoader();

        return $loader->exists($name);
    }

    /**
     * Returns true if this class is able to render the given template.
     *
     * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance
     *
     * @return Boolean true if this class supports the given template, false otherwise
     *
     * @api
     */
    public function supports($name)
    {
        if ($name instanceof \Twig_Template) {
            return true;
        }

        $template = $this->parser->parse($name);

        return 'twig' === $template->get('engine');
    }

}

To be able to use dependency injection later, the newly created TwigEngine needs an instance of the Twig_Environment and an object that implements TemplateNameParserInterface, which is used to get the engine name – based on the extension. It also accepts a directory where the templates can be found.

The same could be applied to any engine we want to support. For example, a simple engine bridge for Smarty templates would look like this:

<?php

namespace RaulFraile\Templating;

use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Templating\TemplateReferenceInterface;
use Symfony\Component\Templating\TemplateNameParserInterface;
use Smarty;

class SmartyEngine implements EngineInterface
{

    protected $smarty;
    protected $parser;

    /**
     * Constructor.
     *
     * @param Smarty                      $environment A Smarty instance
     * @param TemplateNameParserInterface $parser      A TemplateNameParserInterface instance
     * @param string                      $dir         Templates directory
     */
    public function __construct(Smarty $smarty, TemplateNameParserInterface $parser, $dir)
    {
        $this->smarty = $smarty;
        $this->smarty->setTemplateDir($dir);

        $this->parser = $parser;
    }

    /**
     * Renders a template.
     *
     * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance
     * @param array $parameters An array of parameters to pass to the template
     *
     * @return string The evaluated template as a string
     *
     * @throws \RuntimeException if the template cannot be rendered
     *
     * @api
     */
    public function render($name, array $parameters = array())
    {
        $this->smarty->assign($parameters);

        return $this->smarty->fetch($name);
    }

    /**
     * Returns true if the template exists.
     *
     * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance
     *
     * @return Boolean true if the template exists, false otherwise
     *
     * @throws \RuntimeException if the engine cannot handle the template name
     *
     * @api
     */
    public function exists($name)
    {
        return $this->smarty->templateExists($name);
    }

    /**
     * Returns true if this class is able to render the given template.
     *
     * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance
     *
     * @return Boolean true if this class supports the given template, false otherwise
     *
     * @api
     */
    public function supports($name)
    {
        $template = $this->parser->parse($name);

        return 'tpl' === $template->get('engine');
    }

}

To use them in a controller:

<?php

use Symfony\Component\Templating\TemplateNameParser;
use RaulFraile\Templating\TwigEngine;
use RaulFraile\Templating\SmartyEngine;

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

$templatesDir = __DIR__.'/views/';
$templateNameParser = new TemplateNameParser();

$templating = new TwigEngine(new Twig_Environment(), $templateNameParser, $templatesDir),
// or $templating = new SmartyEngine(new \Smarty(), $templateNameParser, $templatesDir);

echo $templating->render('hello.php', array('name' => 'Raul'));
// or echo $templating->render('hello.tpl', array('name' => 'Raul'));

Choosing the right engine

We have a few engine bridges, but how to choose the right one? Frameworks usually allow us to use the template engine of our choice. For example, in Symfony2 full-stack framework, we can use both Twig and PHP templates simply by changing the filename:

$this->render('AcmeBundle:Hello:index.html.twig', array('name' => $name));

// or

$this->render('AcmeBundle:Hello:index.html.php', array('name' => $name));

Symfony knows which engine to use based on the extension. The component provides a way to follow this approach using the DelegatingEngine class and the supports() method of the engine bridges that we just implemented.

<?php

use Symfony\Component\Templating\PhpEngine;
use Symfony\Component\Templating\TemplateNameParser;
use Symfony\Component\Templating\Loader\FilesystemLoader;
use Symfony\Component\Templating\DelegatingEngine;

use RaulFraile\Templating\TwigEngine;
use RaulFraile\Templating\SmartyEngine;

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

$templatesDir = __DIR__.'/views/';
$templateNameParser = new TemplateNameParser();

$engines = new DelegatingEngine(array(
    new PhpEngine($templateNameParser, new FilesystemLoader($templatesDir . '%name%')),
    new TwigEngine(new Twig_Environment(), $templateNameParser, $templatesDir),
    new SmartyEngine(new \Smarty(), $templateNameParser, $templatesDir)
));

echo $engines->render('hello.php', array('name' => 'Raul')) . PHP_EOL;

The render() method of DelegatingEngine will iterate over all the defined engine bridges and call to their supports() method. As soon as it finds an engine capable of handling that template, executes the render() method of that engine.

An alternative for PHP templates: Plates

If you prefer to use native PHP templates but miss some features of compiled ones such as template inheritance, layouts or plugins/extensions, it is worth taking a look at Plates, which is a PHP templating system inspired by Twig.

If we want to provide Plates as the default engine for PHP templates, all we have to do is create a new engine that implements the EngineInterface interface and accepts ‘*.php’ templates.

<?php

namespace RaulFraile\Templating;

use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Templating\TemplateReferenceInterface;
use Symfony\Component\Templating\TemplateNameParserInterface;
use League\Plates;

class PlatesEngine implements EngineInterface
{

    protected $engine;
    protected $parser;

    public function __construct(Plates\Engine $engine, TemplateNameParserInterface $parser, $dir)
    {
        $this->engine = $engine;
        $this->engine->setDirectory($dir);
        $this->engine->setFileExtension(null);

        $this->parser = $parser;
    }

    public function render($name, array $parameters = array())
    {
        $template = new Plates\Template($this->engine);
        $template->data($parameters);

        return $template->render($name);
    }

    public function exists($name)
    {
        return $this->engine->pathExists($name);
    }

    public function supports($name)
    {
        $template = $this->parser->parse($name);

        return 'php' === $template->get('engine');
    }
}

Calling the setFileExtension() method with null is necessary in our example as we are already using the extension to choose the template engine, so we want Plates to use hello.php instead of hello.php.php.

Then, we just replace the PhpEngine object by the PlatesEngine one.

<?php

use Symfony\Component\Templating\TemplateNameParser;
use Symfony\Component\Templating\DelegatingEngine;

use RaulFraile\Templating\TwigEngine;
use RaulFraile\Templating\SmartyEngine;
use RaulFraile\Templating\PlatesEngine;

use League\Plates;

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

$templatesDir = __DIR__.'/views/';
$templateNameParser = new TemplateNameParser();

$engines = new DelegatingEngine(array(
    new PlatesEngine(new Plates\Engine(), $templateNameParser, $templatesDir),
    new TwigEngine(new Twig_Environment(), $templateNameParser, $templatesDir),
    new SmartyEngine(new \Smarty(), $templateNameParser, $templatesDir)
));

echo $engines->render('hello.php', array('name' => 'Raul')) . PHP_EOL;

Even though the template is the same, we can now use all the features of Plates.

Who’s using it?

Conclusion

The Templating component comes very handy when we need to provide different template engines, especially in frameworks and projects that can be extended somehow with templates, such as static blog generators or plugin systems.

More info

Photo: Matt Cornock

marzo 11 / 2014

Upcoming Conferences

WeCamp

PHP New Zealand

PHP Summer Camp Croatia

PHPNE

MadisonPHP

brnoPHP

SymfonyLiveLondon

ZgPHP

PHPSouthAfrica

PHPNWUK

SymfonyLiveNYC

PHPForumParis

PHPARG

PHPWorld

TechMeetupUY

SymfonyConMadrid