Créer une extension pour Twig

Thibaut Cuvelier

On peut vouloir créer une extension Twig pour de nombreuses raisons, comme la volonté de factoriser du code ou de réaliser des choses qui ne sont pas possibles de base (surligner du texte alors que le filtre n'existe pas de base dans Twig, définir un nouveau format pour l'affichage des dates, etc.). C'est ce que nous allons faire ici.

Avant toute chose, il faut vérifier qu'une extension qui fait ce que vous voulez n'existe pas déjà : Twig-extensions. C'est inutile de passer son temps à réinventer la roue.

Commentez

- Titre : Créer une extension pour Twig

- Auteur : Thibaut Cuvelier

- Parution : 25 mai 2011

- Mise à jour : 29 mai 2011

- Licence : Copyright ® 2011 Thibaut Cuvelier. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. L'interface

- III. Première méthode et enregistrement

- III-A. Intégration à Twig

- III-B. Intégration à Symfony2

- IV. Implémentation

- IV-A. Fonctions

- IV-B. Filtres

- V. Conclusion

I - Introduction

Twig est un moteur de templating, utilisé par Symfony2 en parallèle avec des templates en PHP pur. L'avantage principal d'un moteur de templating est de séparer sans ambiguïté la présentation des données et leur traitement.

Cet article se destine à des utilisateurs ayant déjà une certaine expérience de Twig, souhaitant l'étendre par la création d'extensions.

II - L'interface

Une extension Twig est une classe qui implémente cette interface (code repris directement du framework) :

interface Twig_ExtensionInterface
{
/**
* Initializes the runtime environment.
*
* This is where you can load some file that contains filter functions for instance.
*
* @param Twig_Environment $environment The current Twig_Environment instance
*/
public function initRuntime(Twig_Environment $environment);

/**
* Returns the token parser instances to add to the existing list.
*
* @return array An array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances
*/
public function getTokenParsers();

/**
* Returns the node visitor instances to add to the existing list.
*
* @return array An array of Twig_NodeVisitorInterface instances
*/
public function getNodeVisitors();

/**
* Returns a list of filters to add to the existing list.
*
* @return array An array of filters
*/
public function getFilters();

/**
* Returns a list of tests to add to the existing list.
*
* @return array An array of tests
*/
public function getTests();

/**
* Returns a list of operators to add to the existing list.
*
* @return array An array of operators
*/
public function getOperators();

/**
* Returns a list of global functions to add to the existing list.
*
* @return array An array of global functions
*/
public function getGlobals();

/**
* Returns the name of the extension.
*
* @return string The extension name
*/
public function getName();
}

Tout n'est pas à implémenter, bon nombre de ces fonctions le sont déjà dans Twig_Extension, il suffit d'hériter de cette dernière pour simplifier le code.

III - Première méthode et enregistrement

La première méthode que nous implémenterons rendra simplement le nom de l'extension, celui-ci doit être unique. Votre classe devrait ressembler à ceci :

<?php

namespace App\AppBundle\Twig\Extension;

use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Bundle\TwigBundle\Loader\FilesystemLoader;

class AppExtension extends \Twig_Extension
{
public function getName()
{
return 'twigext';
}
}

Peu importe où vous mettez ce fichier, il suffit de le renseigner à Twig pour que l'extension soit disponible. Par exemple, vous pouvez lui consacrer un dossier Twig/Extension dans un de vos bundles ou créer un dossier Twigext (du nom de l'extension) dans votre bundle d'extensions Twig, tout dépend de la taille de votre application, du besoin en réutilisation des extensions et d'autres facteurs propres à votre application.

III-A - Intégration à Twig

Pour enregistrer votre extension, deux cas se présentent. Le premier est celui où vous utilisez Twig en standalone :

$twig = new Twig_Environment($loader);
$twig->addExtension(new AppExtension());

III-B - Intégration à Symfony2

Pour une intégration à Symfony2, c'est plus compliqué, il faut utiliser le principe DependencyInjection, très puissant mais pas forcément simple à manipuler au début.

Pour cela, le mieux est de placer tout ce qui concerne l'injection de dépendances dans un répertoire spécifique, DependencyInjection à la racine de votre bundle par exemple. Créons-y un fichier du nom du bundle qui contient l'extension, soit AppExtension dans notre cas. Notez que l'on définit alors le bundle courant comme une extension de Symfony2. Par exemple, si votre bundle s'appelle AppBundle, ce fichier devra s'appeler AppExtension. AppBundle correspond au fichier de bundle créé à la racine dudit bundle (le fichier créé par la ligne de commande lors de l'initialisation d'un bundle).

Tout se déroule dans la fonction load(). Nous allons faire au plus simple : simplement donner un fichier de configuration dans lequel tout sera mis. C'est aussi la méthode la plus propre, l'autre consistant à définir chaque extension directement dans ce fichier PHP. Nous verrons aussi comment faire juste après.

<?php

namespace App\AppBundle\DependencyInjection;

use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;

class AppExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
}

Maintenant, il va falloir mettre ce qu'il faut dans ce fichier de configuration Resources/config/services.yml à la racine de votre bundle :

services:
twig.extension.twigext:
class: App\AppBundle\Twig\Extension\AppExtension
tags:
- { name: twig.extension }

La méthode sans fichier de configuration se résume à ce genre de fichier d'injection de dépendance :

<?php

namespace Bundle\AppBundle\DependencyInjection;

use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;

class AppExtension extends Extension
{
public function load(array $config, ContainerBuilder $container)
{
$definition = new Definition('App\AppBundle\Extension\AppExtension');
$definition->addTag('twig.extension');
$container->setDefinition('app_twig_extension', $definition); // le nom de l'extension renseigné dans "getName" de l'extension Twig
}
}

IV - Implémentation

Dernière étape : l'implémentation de votre extension. Ces snippets sont à mettre dans votre fichier d'extension Twig, soit dans le fichier du dossier Twig/Extension si vous avez suivi mes conseils.

IV-A - Fonctions

Cette extension Twig peut comporter des fonctions, qui sont enregistrées comme suit :

public function getFunctions()
{
return array(
'fc' => new \Twig_Function_Method($this, 'fc', array('is_safe' => array('html'))),
'fc2' => new \Twig_Function_Method('fc2', array('is_safe' => array('html'))),
);
}

public function fc($txt)
{
// votre code
}

Dans le dernier cas, il s'agira de la fonction fc2, elle doit être accessible dans ce fichier (définie à la suite ou include).

La fonction appelée doit renvoyer directement ce qui doit être mis dans le contenu envoyé au navigateur.

IV-B - Filtres

Un filtre est une fonction qui prend en paramètre un certain texte et le renvoie transformé. Par exemple, une date, qui est rendue dans un autre format. Un filtre prend donc toujours au moins un paramètre, ce texte à transformer. Il peut en prendre d'autres (reprenons l'exemple de la date : un second paramètre peut être le format).

public function getFilters()
{
return array(
'date' => new Twig_Filter_Function('date'),
'datebis' => new Twig_Filter_Function($this, 'date'),
'fluo' => new Twig_Filter_Function($this, 'fluo'),
);
}

public function date($timestamp, $format)
{
return '42' . date($format, $timestamp);
}

public function fluo($text, $what)
{
return preg_replace('/(' . $what . ')/', '<span style="background-color: #FFFF00; color: #0000FF;">\1</span>', $text);
}

Remarquez qu'on a ici surchargé le filtre standard de Twig date. On peut évidemment brancher derrière ce filtre une implémentation beaucoup plus spécifique en remplacement du fonctionnement standard. Un exemple d'implémentation de filtre tout à fait personnalisé est aussi fourni ci-dessus pour le filtre datebis, qui ne surcharge aucun filtre préexistant. Une implémentation de filtre doit renvoyer, au même titre qu'une fonction, ce qui doit être écrit dans le fichier renvoyé au navigateur. La dernière méthode, fluo, permet de mettre en évidence ce qui est passé en argument. Maintenant, comment appeler ces filtres ?

{{ '1305238362'|datebis(l jS \of F Y h:i:s A')}}

{{ 'Un peu de texte'|fluo('t')|raw }}

Remarquez l'utilisation du filtre raw après fluo : ce dernier renvoie du code HTML à écrire tel quel, il faut donc l'écrire sans aucun échappement.

V - Conclusion

Il n'est pas très compliqué d'écrire une extension pour Twig, ce n'est cependant pas une raison pour implémenter une extension pour tout besoin, n'oubliez surtout pas de regarder du côté des extensions déjà existantes (les officielles).

Il est évidemment possible d'aller beaucoup plus loin en utilisant ces quelques principes de base : on peut vouloir utiliser des dates dans une version plus verbeuse comme « il y a deux jours » au lieu de « 10/05/2011 ». On peut aussi, suite à une recherche, vouloir mettre les mots correspondant à la requête en emphase forte et en rouge. Ou bien la traduction : non, c'est un mauvais exemple, c'est déjà géré par une extension, même si elle est beaucoup plus compliquée que ce que l'on a fait ici, puisqu'elle gère de nouveaux jetons, une grammaire Twig étendue.

Merci à creativecwx, winzou, nathieb et djayp pour leur aide lors de la rédaction de cet article ! Merci à Claude Leloup pour sa précieuse relecture orthographique !