Bienvenue dans ce tutoriel où je vous propose de découvrir comment créer un tag personnalisé sur Twig (3.X).
Objectif
Notre objectif est de créer un tag qui permettra la mise en cache d'une partie de notre template. Ceci afin d'optimiser les performances de certaines parties de notre application (conversion markdown par exemple)
{% cache post %}
<li>Post {{ post.id }} {{ slow() }}</li>
{% endcache %}
Le cache sera capable de déterminer une clef à partir de l'objet reçu en paramètre {id}-Post-{updatedAt}
. Pour que cela fonctionne on créera une interface qui permettra de définir ce qui est attendu comme type.
<?php
namespace App\Twig\Cache;
interface CacheableInterface
{
public function getId(): int;
public function getUpdatedAt(): \DateTimeInterface;
}
Extension twig
La première étape est assez classique et consiste à créer une nouvelle extension Twig qui va permettre de définir le tag à ajouter.
<?php
namespace App\Twig;
use App\Twig\Cache\CacheableInterface;
use App\Twig\Cache\CacheTokenParser;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\CacheItem;
use Twig\Extension\AbstractExtension;
use Twig\TokenParser\TokenParserInterface;
class TwigCacheExtension extends AbstractExtension
{
private AdapterInterface $cache;
public function __construct(AdapterInterface $cache)
{
$this->cache = $cache;
}
/**
* @return array<TokenParserInterface>
*/
public function getTokenParsers(): array
{
return [
new CacheTokenParser()
];
}
public function getCachedValue(CacheableInterface $key): ?string
{
return $this->getItem($key)->get();
}
public function setCachedValue(CacheableInterface $key, string $value): void
{
$item = $this->getItem($key);
$item->set($value);
$this->cache->save($item);
}
private function getItem(CacheableInterface $entity): CacheItem
{
$className = get_class($entity);
$className = substr($className, strrpos($className, '\\') + 1);
$key = $entity->getId() . $className . $entity->getUpdatedAt()->getTimestamp();
return $this->cache->getItem($key);
}
}
Pour que twig comprenne notre nouveau tag nous allons définir un nouveau parser gràce à la méthode getTokenParsers()
. On ajoutera aussi au sein de cette extension différentes méthodes (qui seront utilisables depuis le code généré) afin d'interagir avec le cache.
Token parser
Cette classe va permettre d'indiquer à twig comment traiter notre tag et son contenu. On aura à l'intérieur plusieurs méthodes importantes :
getTag()
, renvoie le nom de votre tag personnalisé.parse()
, permet de détailler le processus de parsing et devra renvoyer un noeud (qui sera ensuite traité par Twig).
Pour notre logique nous allons conserver tout le contenu entre notre {% cache %}
et {% endcache %}
dans un noeud body
. Nous allons ensuite créer un nouveau type de noeud CacheNode
.
<?php
namespace App\Twig\Cache;
use Twig\Node\Node;
use Twig\Token;
use Twig\TokenParser\AbstractTokenParser;
class CacheTokenParser extends AbstractTokenParser
{
public function parse(Token $token) {
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$key = $this->parser->getExpressionParser()->parseExpression();
$key->setAttribute('always_defined', true);
$stream->expect(Token::BLOCK_END_TYPE);
$body = $this->parser->subparse([$this, 'decideCacheEnd'], true);
$stream->expect(Token::BLOCK_END_TYPE);
return new CacheNode($key, $body, $lineno, $this->getTag());
}
public function getTag(): string
{
return 'cache';
}
public function decideCacheEnd(Token $token): bool
{
return $token->test('endcache');
}
}
Le noeud
Le noeud est le dernier point (mais aussi le plus important) de notre système. C'est lui, au travers de sa méthode compile()
, qui va être responsable de générer le code PHP correspondant à la fonctionnalité de notre tag.
Il sera possible de récupérer l'instance de notre extension au travers de $this->env->getExtension('Namepsace\De\Notre\Extension')
.
<?php
namespace App\Twig\Cache;
use App\Twig\TwigCacheExtension;
use Twig\Compiler;
use Twig\Node\Node;
class CacheNode extends Node
{
private static int $cacheCount = 1;
public function __construct(Node $key, Node $body, int $lineno = 0, string $tag = null)
{
parent::__construct(['key' => $key, 'body' => $body], [], $lineno, $tag);
}
public function compile(Compiler $compiler)
{
$i = self::$cacheCount++;
$extension = TwigCacheExtension::class;
$compiler
->addDebugInfo($this)
->write("\$twigCacheExtension = \$this->env->getExtension('{$extension}');\n")
->write("\$twigCacheBody{$i} = \$twigCacheExtension->getCachedValue(")
->subcompile($this->getNode('key'))
->raw(");\n")
->write("if(\$twigCacheBody{$i} !== null) {\n")
->indent()
->write("echo \$twigCacheBody{$i};\n")
->outdent()
->write("} else { \n")
->indent()
->write("ob_start();\n")
->subcompile($this->getNode('body'))
->write("\$twigCacheBody{$i} = ob_get_clean();\n")
->write("echo \$twigCacheBody{$i};\n")
->write("\$twigCacheExtension->setCachedValue(")
->subcompile($this->getNode('key'))
->raw(", \$twigCacheBody{$i});\n")
->outdent()
->write("}\n");
;
}
}
Et voila !
Pour aller plus loin
Si vous voulez pousser les choses plus loin vous pouvez mettre en place le support des tableaux dans la génération de la clef.
{% cache ['card', post] %}
<li>Post {{ post.id }} {{ slow() }}</li>
{% endcache %}