| Recipes |
| ======= |
| |
| .. _deprecation-notices: |
| |
| Displaying Deprecation Notices |
| ------------------------------ |
| |
| Deprecated features generate deprecation notices (via a call to the |
| ``trigger_error()`` PHP function). By default, they are silenced and never |
| displayed nor logged. |
| |
| To remove all deprecated feature usages from your templates, write and run a |
| script along the lines of the following:: |
| |
| require_once __DIR__.'/vendor/autoload.php'; |
| |
| $twig = create_your_twig_env(); |
| |
| $deprecations = new \Twig\Util\DeprecationCollector($twig); |
| |
| print_r($deprecations->collectDir(__DIR__.'/templates')); |
| |
| The ``collectDir()`` method compiles all templates found in a directory, |
| catches deprecation notices, and return them. |
| |
| .. tip:: |
| |
| If your templates are not stored on the filesystem, use the ``collect()`` |
| method instead. ``collect()`` takes a ``Traversable`` which must return |
| template names as keys and template contents as values (as done by |
| ``\Twig\Util\TemplateDirIterator``). |
| |
| However, this code won't find all deprecations (like using deprecated some Twig |
| classes). To catch all notices, register a custom error handler like the one |
| below:: |
| |
| $deprecations = []; |
| set_error_handler(function ($type, $msg) use (&$deprecations) { |
| if (E_USER_DEPRECATED === $type) { |
| $deprecations[] = $msg; |
| } |
| }); |
| |
| // run your application |
| |
| print_r($deprecations); |
| |
| Note that most deprecation notices are triggered during **compilation**, so |
| they won't be generated when templates are already cached. |
| |
| .. tip:: |
| |
| If you want to manage the deprecation notices from your PHPUnit tests, have |
| a look at the `symfony/phpunit-bridge |
| <https://github.com/symfony/phpunit-bridge>`_ package, which eases the |
| process. |
| |
| Making a Layout conditional |
| --------------------------- |
| |
| Working with Ajax means that the same content is sometimes displayed as is, |
| and sometimes decorated with a layout. As Twig layout template names can be |
| any valid expression, you can pass a variable that evaluates to ``true`` when |
| the request is made via Ajax and choose the layout accordingly: |
| |
| .. code-block:: twig |
| |
| {% extends request.ajax ? "base_ajax.html" : "base.html" %} |
| |
| {% block content %} |
| This is the content to be displayed. |
| {% endblock %} |
| |
| Making an Include dynamic |
| ------------------------- |
| |
| When including a template, its name does not need to be a string. For |
| instance, the name can depend on the value of a variable: |
| |
| .. code-block:: twig |
| |
| {% include var ~ '_foo.html' %} |
| |
| If ``var`` evaluates to ``index``, the ``index_foo.html`` template will be |
| rendered. |
| |
| As a matter of fact, the template name can be any valid expression, such as |
| the following: |
| |
| .. code-block:: twig |
| |
| {% include var|default('index') ~ '_foo.html' %} |
| |
| Overriding a Template that also extends itself |
| ---------------------------------------------- |
| |
| A template can be customized in two different ways: |
| |
| * *Inheritance*: A template *extends* a parent template and overrides some |
| blocks; |
| |
| * *Replacement*: If you use the filesystem loader, Twig loads the first |
| template it finds in a list of configured directories; a template found in a |
| directory *replaces* another one from a directory further in the list. |
| |
| But how do you combine both: *replace* a template that also extends itself |
| (aka a template in a directory further in the list)? |
| |
| Let's say that your templates are loaded from both ``.../templates/mysite`` |
| and ``.../templates/default`` in this order. The ``page.twig`` template, |
| stored in ``.../templates/default`` reads as follows: |
| |
| .. code-block:: twig |
| |
| {# page.twig #} |
| {% extends "layout.twig" %} |
| |
| {% block content %} |
| {% endblock %} |
| |
| You can replace this template by putting a file with the same name in |
| ``.../templates/mysite``. And if you want to extend the original template, you |
| might be tempted to write the following: |
| |
| .. code-block:: twig |
| |
| {# page.twig in .../templates/mysite #} |
| {% extends "page.twig" %} {# from .../templates/default #} |
| |
| However, this will not work as Twig will always load the template from |
| ``.../templates/mysite``. |
| |
| It turns out it is possible to get this to work, by adding a directory right |
| at the end of your template directories, which is the parent of all of the |
| other directories: ``.../templates`` in our case. This has the effect of |
| making every template file within our system uniquely addressable. Most of the |
| time you will use the "normal" paths, but in the special case of wanting to |
| extend a template with an overriding version of itself we can reference its |
| parent's full, unambiguous template path in the extends tag: |
| |
| .. code-block:: twig |
| |
| {# page.twig in .../templates/mysite #} |
| {% extends "default/page.twig" %} {# from .../templates #} |
| |
| .. note:: |
| |
| This recipe was inspired by the following Django wiki page: |
| https://code.djangoproject.com/wiki/ExtendingTemplates |
| |
| Customizing the Syntax |
| ---------------------- |
| |
| Twig allows some syntax customization for the block delimiters. It's **not** |
| recommended to use this feature as templates will be tied with your custom |
| syntax. But for specific projects, it can make sense to change the defaults. |
| |
| To change the block delimiters, you need to create your own lexer object:: |
| |
| $twig = new \Twig\Environment(...); |
| |
| $lexer = new \Twig\Lexer($twig, [ |
| 'tag_comment' => ['{#', '#}'], |
| 'tag_block' => ['{%', '%}'], |
| 'tag_variable' => ['{{', '}}'], |
| 'interpolation' => ['#{', '}'], |
| ]); |
| $twig->setLexer($lexer); |
| |
| Here are some configuration example that simulates some other template engines |
| syntax:: |
| |
| // Ruby erb syntax |
| $lexer = new \Twig\Lexer($twig, [ |
| 'tag_comment' => ['<%#', '%>'], |
| 'tag_block' => ['<%', '%>'], |
| 'tag_variable' => ['<%=', '%>'], |
| ]); |
| |
| // SGML Comment Syntax |
| $lexer = new \Twig\Lexer($twig, [ |
| 'tag_comment' => ['<!--#', '-->'], |
| 'tag_block' => ['<!--', '-->'], |
| 'tag_variable' => ['${', '}'], |
| ]); |
| |
| // Smarty like |
| $lexer = new \Twig\Lexer($twig, [ |
| 'tag_comment' => ['{*', '*}'], |
| 'tag_block' => ['{', '}'], |
| 'tag_variable' => ['{$', '}'], |
| ]); |
| |
| Using dynamic Object Properties |
| ------------------------------- |
| |
| When Twig encounters a variable like ``article.title``, it tries to find a |
| ``title`` public property in the ``article`` object. |
| |
| It also works if the property does not exist but is rather defined dynamically |
| thanks to the magic ``__get()`` method; you need to also implement the |
| ``__isset()`` magic method like shown in the following snippet of code:: |
| |
| class Article |
| { |
| public function __get($name) |
| { |
| if ('title' == $name) { |
| return 'The title'; |
| } |
| |
| // throw some kind of error |
| } |
| |
| public function __isset($name) |
| { |
| if ('title' == $name) { |
| return true; |
| } |
| |
| return false; |
| } |
| } |
| |
| Accessing the parent Context in Nested Loops |
| -------------------------------------------- |
| |
| Sometimes, when using nested loops, you need to access the parent context. The |
| parent context is always accessible via the ``loop.parent`` variable. For |
| instance, if you have the following template data:: |
| |
| $data = [ |
| 'topics' => [ |
| 'topic1' => ['Message 1 of topic 1', 'Message 2 of topic 1'], |
| 'topic2' => ['Message 1 of topic 2', 'Message 2 of topic 2'], |
| ], |
| ]; |
| |
| And the following template to display all messages in all topics: |
| |
| .. code-block:: twig |
| |
| {% for topic, messages in topics %} |
| * {{ loop.index }}: {{ topic }} |
| {% for message in messages %} |
| - {{ loop.parent.loop.index }}.{{ loop.index }}: {{ message }} |
| {% endfor %} |
| {% endfor %} |
| |
| The output will be similar to: |
| |
| .. code-block:: text |
| |
| * 1: topic1 |
| - 1.1: The message 1 of topic 1 |
| - 1.2: The message 2 of topic 1 |
| * 2: topic2 |
| - 2.1: The message 1 of topic 2 |
| - 2.2: The message 2 of topic 2 |
| |
| In the inner loop, the ``loop.parent`` variable is used to access the outer |
| context. So, the index of the current ``topic`` defined in the outer for loop |
| is accessible via the ``loop.parent.loop.index`` variable. |
| |
| Defining undefined Functions, Filters, and Tags on the Fly |
| ---------------------------------------------------------- |
| |
| .. versionadded:: 3.2 |
| |
| The ``registerUndefinedTokenParserCallback()`` method was added in Twig |
| 3.2. |
| |
| When a function/filter/tag is not defined, Twig defaults to throw a |
| ``\Twig\Error\SyntaxError`` exception. However, it can also call a `callback`_ |
| (any valid PHP callable) which should return a function/filter/tag. |
| |
| For tags, register callbacks with ``registerUndefinedTokenParserCallback()``. |
| For filters, register callbacks with ``registerUndefinedFilterCallback()``. |
| For functions, use ``registerUndefinedFunctionCallback()``:: |
| |
| // auto-register all native PHP functions as Twig functions |
| // NEVER do this in a project as it's NOT secure |
| $twig->registerUndefinedFunctionCallback(function ($name) { |
| if (function_exists($name)) { |
| return new \Twig\TwigFunction($name, $name); |
| } |
| |
| return false; |
| }); |
| |
| If the callable is not able to return a valid function/filter/tag, it must |
| return ``false``. |
| |
| If you register more than one callback, Twig will call them in turn until one |
| does not return ``false``. |
| |
| .. tip:: |
| |
| As the resolution of functions/filters/tags is done during compilation, |
| there is no overhead when registering these callbacks. |
| |
| Validating the Template Syntax |
| ------------------------------ |
| |
| When template code is provided by a third-party (through a web interface for |
| instance), it might be interesting to validate the template syntax before |
| saving it. If the template code is stored in a ``$template`` variable, here is |
| how you can do it:: |
| |
| try { |
| $twig->parse($twig->tokenize(new \Twig\Source($template))); |
| |
| // the $template is valid |
| } catch (\Twig\Error\SyntaxError $e) { |
| // $template contains one or more syntax errors |
| } |
| |
| If you iterate over a set of files, you can pass the filename to the |
| ``tokenize()`` method to get the filename in the exception message:: |
| |
| foreach ($files as $file) { |
| try { |
| $twig->parse($twig->tokenize(new \Twig\Source($template, $file->getFilename(), $file))); |
| |
| // the $template is valid |
| } catch (\Twig\Error\SyntaxError $e) { |
| // $template contains one or more syntax errors |
| } |
| } |
| |
| .. note:: |
| |
| This method won't catch any sandbox policy violations because the policy |
| is enforced during template rendering (as Twig needs the context for some |
| checks like allowed methods on objects). |
| |
| Refreshing modified Templates when OPcache or APC is enabled |
| ------------------------------------------------------------ |
| |
| When using OPcache with ``opcache.validate_timestamps`` set to ``0`` or APC |
| with ``apc.stat`` set to ``0`` and Twig cache enabled, clearing the template |
| cache won't update the cache. |
| |
| To get around this, force Twig to invalidate the bytecode cache:: |
| |
| $twig = new \Twig\Environment($loader, [ |
| 'cache' => new \Twig\Cache\FilesystemCache('/some/cache/path', \Twig\Cache\FilesystemCache::FORCE_BYTECODE_INVALIDATION), |
| // ... |
| ]); |
| |
| Reusing a stateful Node Visitor |
| ------------------------------- |
| |
| When attaching a visitor to a ``\Twig\Environment`` instance, Twig uses it to |
| visit *all* templates it compiles. If you need to keep some state information |
| around, you probably want to reset it when visiting a new template. |
| |
| This can be achieved with the following code:: |
| |
| protected $someTemplateState = []; |
| |
| public function enterNode(\Twig\Node\Node $node, \Twig\Environment $env) |
| { |
| if ($node instanceof \Twig\Node\ModuleNode) { |
| // reset the state as we are entering a new template |
| $this->someTemplateState = []; |
| } |
| |
| // ... |
| |
| return $node; |
| } |
| |
| Using a Database to store Templates |
| ----------------------------------- |
| |
| If you are developing a CMS, templates are usually stored in a database. This |
| recipe gives you a simple PDO template loader you can use as a starting point |
| for your own. |
| |
| First, let's create a temporary in-memory SQLite3 database to work with:: |
| |
| $dbh = new PDO('sqlite::memory:'); |
| $dbh->exec('CREATE TABLE templates (name STRING, source STRING, last_modified INTEGER)'); |
| $base = '{% block content %}{% endblock %}'; |
| $index = ' |
| {% extends "base.twig" %} |
| {% block content %}Hello {{ name }}{% endblock %} |
| '; |
| $now = time(); |
| $dbh->prepare('INSERT INTO templates (name, source, last_modified) VALUES (?, ?, ?)')->execute(['base.twig', $base, $now]); |
| $dbh->prepare('INSERT INTO templates (name, source, last_modified) VALUES (?, ?, ?)')->execute(['index.twig', $index, $now]); |
| |
| We have created a simple ``templates`` table that hosts two templates: |
| ``base.twig`` and ``index.twig``. |
| |
| Now, let's define a loader able to use this database:: |
| |
| class DatabaseTwigLoader implements \Twig\Loader\LoaderInterface |
| { |
| protected $dbh; |
| |
| public function __construct(PDO $dbh) |
| { |
| $this->dbh = $dbh; |
| } |
| |
| public function getSourceContext(string $name): Source |
| { |
| if (false === $source = $this->getValue('source', $name)) { |
| throw new \Twig\Error\LoaderError(sprintf('Template "%s" does not exist.', $name)); |
| } |
| |
| return new \Twig\Source($source, $name); |
| } |
| |
| public function exists(string $name) |
| { |
| return $name === $this->getValue('name', $name); |
| } |
| |
| public function getCacheKey(string $name): string |
| { |
| return $name; |
| } |
| |
| public function isFresh(string $name, int $time): bool |
| { |
| if (false === $lastModified = $this->getValue('last_modified', $name)) { |
| return false; |
| } |
| |
| return $lastModified <= $time; |
| } |
| |
| protected function getValue($column, $name) |
| { |
| $sth = $this->dbh->prepare('SELECT '.$column.' FROM templates WHERE name = :name'); |
| $sth->execute([':name' => (string) $name]); |
| |
| return $sth->fetchColumn(); |
| } |
| } |
| |
| Finally, here is an example on how you can use it:: |
| |
| $loader = new DatabaseTwigLoader($dbh); |
| $twig = new \Twig\Environment($loader); |
| |
| echo $twig->render('index.twig', ['name' => 'Fabien']); |
| |
| Using different Template Sources |
| -------------------------------- |
| |
| This recipe is the continuation of the previous one. Even if you store the |
| contributed templates in a database, you might want to keep the original/base |
| templates on the filesystem. When templates can be loaded from different |
| sources, you need to use the ``\Twig\Loader\ChainLoader`` loader. |
| |
| As you can see in the previous recipe, we reference the template in the exact |
| same way as we would have done it with a regular filesystem loader. This is |
| the key to be able to mix and match templates coming from the database, the |
| filesystem, or any other loader for that matter: the template name should be a |
| logical name, and not the path from the filesystem:: |
| |
| $loader1 = new DatabaseTwigLoader($dbh); |
| $loader2 = new \Twig\Loader\ArrayLoader([ |
| 'base.twig' => '{% block content %}{% endblock %}', |
| ]); |
| $loader = new \Twig\Loader\ChainLoader([$loader1, $loader2]); |
| |
| $twig = new \Twig\Environment($loader); |
| |
| echo $twig->render('index.twig', ['name' => 'Fabien']); |
| |
| Now that the ``base.twig`` templates is defined in an array loader, you can |
| remove it from the database, and everything else will still work as before. |
| |
| Loading a Template from a String |
| -------------------------------- |
| |
| From a template, you can load a template stored in a string via the |
| ``template_from_string`` function (via the |
| ``\Twig\Extension\StringLoaderExtension`` extension): |
| |
| .. code-block:: twig |
| |
| {{ include(template_from_string("Hello {{ name }}")) }} |
| |
| From PHP, it's also possible to load a template stored in a string via |
| ``\Twig\Environment::createTemplate()``:: |
| |
| $template = $twig->createTemplate('hello {{ name }}'); |
| echo $template->render(['name' => 'Fabien']); |
| |
| Using Twig and AngularJS in the same Templates |
| ---------------------------------------------- |
| |
| Mixing different template syntaxes in the same file is not a recommended |
| practice as both AngularJS and Twig use the same delimiters in their syntax: |
| ``{{`` and ``}}``. |
| |
| Still, if you want to use AngularJS and Twig in the same template, there are |
| two ways to make it work depending on the amount of AngularJS you need to |
| include in your templates: |
| |
| * Escaping the AngularJS delimiters by wrapping AngularJS sections with the |
| ``{% verbatim %}`` tag or by escaping each delimiter via ``{{ '{{' }}`` and |
| ``{{ '}}' }}``; |
| |
| * Changing the delimiters of one of the template engines (depending on which |
| engine you introduced last): |
| |
| * For AngularJS, change the interpolation tags using the |
| ``interpolateProvider`` service, for instance at the module initialization |
| time: |
| |
| .. code-block:: javascript |
| |
| angular.module('myApp', []).config(function($interpolateProvider) { |
| $interpolateProvider.startSymbol('{[').endSymbol(']}'); |
| }); |
| |
| * For Twig, change the delimiters via the ``tag_variable`` Lexer option:: |
| |
| $env->setLexer(new \Twig\Lexer($env, [ |
| 'tag_variable' => ['{[', ']}'], |
| ])); |
| |
| .. _callback: https://secure.php.net/manual/en/function.is-callable.php |