1Recipes
2=======
3
4.. _deprecation-notices:
5
6Displaying Deprecation Notices
7------------------------------
8
9Deprecated features generate deprecation notices (via a call to the
10``trigger_error()`` PHP function). By default, they are silenced and never
11displayed nor logged.
12
13To remove all deprecated feature usages from your templates, write and run a
14script along the lines of the following::
15
16    require_once __DIR__.'/vendor/autoload.php';
17
18    $twig = create_your_twig_env();
19
20    $deprecations = new \Twig\Util\DeprecationCollector($twig);
21
22    print_r($deprecations->collectDir(__DIR__.'/templates'));
23
24The ``collectDir()`` method compiles all templates found in a directory,
25catches deprecation notices, and return them.
26
27.. tip::
28
29    If your templates are not stored on the filesystem, use the ``collect()``
30    method instead. ``collect()`` takes a ``Traversable`` which must return
31    template names as keys and template contents as values (as done by
32    ``\Twig\Util\TemplateDirIterator``).
33
34However, this code won't find all deprecations (like using deprecated some Twig
35classes). To catch all notices, register a custom error handler like the one
36below::
37
38    $deprecations = [];
39    set_error_handler(function ($type, $msg) use (&$deprecations) {
40        if (E_USER_DEPRECATED === $type) {
41            $deprecations[] = $msg;
42        }
43    });
44
45    // run your application
46
47    print_r($deprecations);
48
49Note that most deprecation notices are triggered during **compilation**, so
50they won't be generated when templates are already cached.
51
52.. tip::
53
54    If you want to manage the deprecation notices from your PHPUnit tests, have
55    a look at the `symfony/phpunit-bridge
56    <https://github.com/symfony/phpunit-bridge>`_ package, which eases the
57    process.
58
59Making a Layout conditional
60---------------------------
61
62Working with Ajax means that the same content is sometimes displayed as is,
63and sometimes decorated with a layout. As Twig layout template names can be
64any valid expression, you can pass a variable that evaluates to ``true`` when
65the request is made via Ajax and choose the layout accordingly:
66
67.. code-block:: twig
68
69    {% extends request.ajax ? "base_ajax.html" : "base.html" %}
70
71    {% block content %}
72        This is the content to be displayed.
73    {% endblock %}
74
75Making an Include dynamic
76-------------------------
77
78When including a template, its name does not need to be a string. For
79instance, the name can depend on the value of a variable:
80
81.. code-block:: twig
82
83    {% include var ~ '_foo.html' %}
84
85If ``var`` evaluates to ``index``, the ``index_foo.html`` template will be
86rendered.
87
88As a matter of fact, the template name can be any valid expression, such as
89the following:
90
91.. code-block:: twig
92
93    {% include var|default('index') ~ '_foo.html' %}
94
95Overriding a Template that also extends itself
96----------------------------------------------
97
98A template can be customized in two different ways:
99
100* *Inheritance*: A template *extends* a parent template and overrides some
101  blocks;
102
103* *Replacement*: If you use the filesystem loader, Twig loads the first
104  template it finds in a list of configured directories; a template found in a
105  directory *replaces* another one from a directory further in the list.
106
107But how do you combine both: *replace* a template that also extends itself
108(aka a template in a directory further in the list)?
109
110Let's say that your templates are loaded from both ``.../templates/mysite``
111and ``.../templates/default`` in this order. The ``page.twig`` template,
112stored in ``.../templates/default`` reads as follows:
113
114.. code-block:: twig
115
116    {# page.twig #}
117    {% extends "layout.twig" %}
118
119    {% block content %}
120    {% endblock %}
121
122You can replace this template by putting a file with the same name in
123``.../templates/mysite``. And if you want to extend the original template, you
124might be tempted to write the following:
125
126.. code-block:: twig
127
128    {# page.twig in .../templates/mysite #}
129    {% extends "page.twig" %} {# from .../templates/default #}
130
131However, this will not work as Twig will always load the template from
132``.../templates/mysite``.
133
134It turns out it is possible to get this to work, by adding a directory right
135at the end of your template directories, which is the parent of all of the
136other directories: ``.../templates`` in our case. This has the effect of
137making every template file within our system uniquely addressable. Most of the
138time you will use the "normal" paths, but in the special case of wanting to
139extend a template with an overriding version of itself we can reference its
140parent's full, unambiguous template path in the extends tag:
141
142.. code-block:: twig
143
144    {# page.twig in .../templates/mysite #}
145    {% extends "default/page.twig" %} {# from .../templates #}
146
147.. note::
148
149    This recipe was inspired by the following Django wiki page:
150    https://code.djangoproject.com/wiki/ExtendingTemplates
151
152Customizing the Syntax
153----------------------
154
155Twig allows some syntax customization for the block delimiters. It's **not**
156recommended to use this feature as templates will be tied with your custom
157syntax. But for specific projects, it can make sense to change the defaults.
158
159To change the block delimiters, you need to create your own lexer object::
160
161    $twig = new \Twig\Environment(...);
162
163    $lexer = new \Twig\Lexer($twig, [
164        'tag_comment'   => ['{#', '#}'],
165        'tag_block'     => ['{%', '%}'],
166        'tag_variable'  => ['{{', '}}'],
167        'interpolation' => ['#{', '}'],
168    ]);
169    $twig->setLexer($lexer);
170
171Here are some configuration example that simulates some other template engines
172syntax::
173
174    // Ruby erb syntax
175    $lexer = new \Twig\Lexer($twig, [
176        'tag_comment'  => ['<%#', '%>'],
177        'tag_block'    => ['<%', '%>'],
178        'tag_variable' => ['<%=', '%>'],
179    ]);
180
181    // SGML Comment Syntax
182    $lexer = new \Twig\Lexer($twig, [
183        'tag_comment'  => ['<!--#', '-->'],
184        'tag_block'    => ['<!--', '-->'],
185        'tag_variable' => ['${', '}'],
186    ]);
187
188    // Smarty like
189    $lexer = new \Twig\Lexer($twig, [
190        'tag_comment'  => ['{*', '*}'],
191        'tag_block'    => ['{', '}'],
192        'tag_variable' => ['{$', '}'],
193    ]);
194
195Using dynamic Object Properties
196-------------------------------
197
198When Twig encounters a variable like ``article.title``, it tries to find a
199``title`` public property in the ``article`` object.
200
201It also works if the property does not exist but is rather defined dynamically
202thanks to the magic ``__get()`` method; you need to also implement the
203``__isset()`` magic method like shown in the following snippet of code::
204
205    class Article
206    {
207        public function __get($name)
208        {
209            if ('title' == $name) {
210                return 'The title';
211            }
212
213            // throw some kind of error
214        }
215
216        public function __isset($name)
217        {
218            if ('title' == $name) {
219                return true;
220            }
221
222            return false;
223        }
224    }
225
226Accessing the parent Context in Nested Loops
227--------------------------------------------
228
229Sometimes, when using nested loops, you need to access the parent context. The
230parent context is always accessible via the ``loop.parent`` variable. For
231instance, if you have the following template data::
232
233    $data = [
234        'topics' => [
235            'topic1' => ['Message 1 of topic 1', 'Message 2 of topic 1'],
236            'topic2' => ['Message 1 of topic 2', 'Message 2 of topic 2'],
237        ],
238    ];
239
240And the following template to display all messages in all topics:
241
242.. code-block:: twig
243
244    {% for topic, messages in topics %}
245        * {{ loop.index }}: {{ topic }}
246      {% for message in messages %}
247          - {{ loop.parent.loop.index }}.{{ loop.index }}: {{ message }}
248      {% endfor %}
249    {% endfor %}
250
251The output will be similar to:
252
253.. code-block:: text
254
255    * 1: topic1
256      - 1.1: The message 1 of topic 1
257      - 1.2: The message 2 of topic 1
258    * 2: topic2
259      - 2.1: The message 1 of topic 2
260      - 2.2: The message 2 of topic 2
261
262In the inner loop, the ``loop.parent`` variable is used to access the outer
263context. So, the index of the current ``topic`` defined in the outer for loop
264is accessible via the ``loop.parent.loop.index`` variable.
265
266Defining undefined Functions and Filters on the Fly
267---------------------------------------------------
268
269When a function (or a filter) is not defined, Twig defaults to throw a
270``\Twig\Error\SyntaxError`` exception. However, it can also call a `callback`_ (any
271valid PHP callable) which should return a function (or a filter).
272
273For filters, register callbacks with ``registerUndefinedFilterCallback()``.
274For functions, use ``registerUndefinedFunctionCallback()``::
275
276    // auto-register all native PHP functions as Twig functions
277    // don't try this at home as it's not secure at all!
278    $twig->registerUndefinedFunctionCallback(function ($name) {
279        if (function_exists($name)) {
280            return new \Twig\TwigFunction($name, $name);
281        }
282
283        return false;
284    });
285
286If the callable is not able to return a valid function (or filter), it must
287return ``false``.
288
289If you register more than one callback, Twig will call them in turn until one
290does not return ``false``.
291
292.. tip::
293
294    As the resolution of functions and filters is done during compilation,
295    there is no overhead when registering these callbacks.
296
297Validating the Template Syntax
298------------------------------
299
300When template code is provided by a third-party (through a web interface for
301instance), it might be interesting to validate the template syntax before
302saving it. If the template code is stored in a ``$template`` variable, here is
303how you can do it::
304
305    try {
306        $twig->parse($twig->tokenize(new \Twig\Source($template)));
307
308        // the $template is valid
309    } catch (\Twig\Error\SyntaxError $e) {
310        // $template contains one or more syntax errors
311    }
312
313If you iterate over a set of files, you can pass the filename to the
314``tokenize()`` method to get the filename in the exception message::
315
316    foreach ($files as $file) {
317        try {
318            $twig->parse($twig->tokenize(new \Twig\Source($template, $file->getFilename(), $file)));
319
320            // the $template is valid
321        } catch (\Twig\Error\SyntaxError $e) {
322            // $template contains one or more syntax errors
323        }
324    }
325
326.. note::
327
328    This method won't catch any sandbox policy violations because the policy
329    is enforced during template rendering (as Twig needs the context for some
330    checks like allowed methods on objects).
331
332Refreshing modified Templates when OPcache or APC is enabled
333------------------------------------------------------------
334
335When using OPcache with ``opcache.validate_timestamps`` set to ``0`` or APC
336with ``apc.stat`` set to ``0`` and Twig cache enabled, clearing the template
337cache won't update the cache.
338
339To get around this, force Twig to invalidate the bytecode cache::
340
341    $twig = new \Twig\Environment($loader, [
342        'cache' => new \Twig\Cache\FilesystemCache('/some/cache/path', \Twig\Cache\FilesystemCache::FORCE_BYTECODE_INVALIDATION),
343        // ...
344    ]);
345
346Reusing a stateful Node Visitor
347-------------------------------
348
349When attaching a visitor to a ``\Twig\Environment`` instance, Twig uses it to
350visit *all* templates it compiles. If you need to keep some state information
351around, you probably want to reset it when visiting a new template.
352
353This can be achieved with the following code::
354
355    protected $someTemplateState = [];
356
357    public function enterNode(\Twig\Node\Node $node, \Twig\Environment $env)
358    {
359        if ($node instanceof \Twig\Node\ModuleNode) {
360            // reset the state as we are entering a new template
361            $this->someTemplateState = [];
362        }
363
364        // ...
365
366        return $node;
367    }
368
369Using a Database to store Templates
370-----------------------------------
371
372If you are developing a CMS, templates are usually stored in a database. This
373recipe gives you a simple PDO template loader you can use as a starting point
374for your own.
375
376First, let's create a temporary in-memory SQLite3 database to work with::
377
378    $dbh = new PDO('sqlite::memory:');
379    $dbh->exec('CREATE TABLE templates (name STRING, source STRING, last_modified INTEGER)');
380    $base = '{% block content %}{% endblock %}';
381    $index = '
382    {% extends "base.twig" %}
383    {% block content %}Hello {{ name }}{% endblock %}
384    ';
385    $now = time();
386    $dbh->prepare('INSERT INTO templates (name, source, last_modified) VALUES (?, ?, ?)')->execute(['base.twig', $base, $now]);
387    $dbh->prepare('INSERT INTO templates (name, source, last_modified) VALUES (?, ?, ?)')->execute(['index.twig', $index, $now]);
388
389We have created a simple ``templates`` table that hosts two templates:
390``base.twig`` and ``index.twig``.
391
392Now, let's define a loader able to use this database::
393
394    class DatabaseTwigLoader implements \Twig\Loader\LoaderInterface
395    {
396        protected $dbh;
397
398        public function __construct(PDO $dbh)
399        {
400            $this->dbh = $dbh;
401        }
402
403        public function getSourceContext($name)
404        {
405            if (false === $source = $this->getValue('source', $name)) {
406                throw new \Twig\Error\LoaderError(sprintf('Template "%s" does not exist.', $name));
407            }
408
409            return new \Twig\Source($source, $name);
410        }
411
412        public function exists($name)
413        {
414            return $name === $this->getValue('name', $name);
415        }
416
417        public function getCacheKey($name)
418        {
419            return $name;
420        }
421
422        public function isFresh($name, $time)
423        {
424            if (false === $lastModified = $this->getValue('last_modified', $name)) {
425                return false;
426            }
427
428            return $lastModified <= $time;
429        }
430
431        protected function getValue($column, $name)
432        {
433            $sth = $this->dbh->prepare('SELECT '.$column.' FROM templates WHERE name = :name');
434            $sth->execute([':name' => (string) $name]);
435
436            return $sth->fetchColumn();
437        }
438    }
439
440Finally, here is an example on how you can use it::
441
442    $loader = new DatabaseTwigLoader($dbh);
443    $twig = new \Twig\Environment($loader);
444
445    echo $twig->render('index.twig', ['name' => 'Fabien']);
446
447Using different Template Sources
448--------------------------------
449
450This recipe is the continuation of the previous one. Even if you store the
451contributed templates in a database, you might want to keep the original/base
452templates on the filesystem. When templates can be loaded from different
453sources, you need to use the ``\Twig\Loader\ChainLoader`` loader.
454
455As you can see in the previous recipe, we reference the template in the exact
456same way as we would have done it with a regular filesystem loader. This is
457the key to be able to mix and match templates coming from the database, the
458filesystem, or any other loader for that matter: the template name should be a
459logical name, and not the path from the filesystem::
460
461    $loader1 = new DatabaseTwigLoader($dbh);
462    $loader2 = new \Twig\Loader\ArrayLoader([
463        'base.twig' => '{% block content %}{% endblock %}',
464    ]);
465    $loader = new \Twig\Loader\ChainLoader([$loader1, $loader2]);
466
467    $twig = new \Twig\Environment($loader);
468
469    echo $twig->render('index.twig', ['name' => 'Fabien']);
470
471Now that the ``base.twig`` templates is defined in an array loader, you can
472remove it from the database, and everything else will still work as before.
473
474Loading a Template from a String
475--------------------------------
476
477From a template, you can load a template stored in a string via the
478``template_from_string`` function (via the
479``\Twig\Extension\StringLoaderExtension`` extension):
480
481.. code-block:: twig
482
483    {{ include(template_from_string("Hello {{ name }}")) }}
484
485From PHP, it's also possible to load a template stored in a string via
486``\Twig\Environment::createTemplate()``::
487
488    $template = $twig->createTemplate('hello {{ name }}');
489    echo $template->render(['name' => 'Fabien']);
490
491Using Twig and AngularJS in the same Templates
492----------------------------------------------
493
494Mixing different template syntaxes in the same file is not a recommended
495practice as both AngularJS and Twig use the same delimiters in their syntax:
496``{{`` and ``}}``.
497
498Still, if you want to use AngularJS and Twig in the same template, there are
499two ways to make it work depending on the amount of AngularJS you need to
500include in your templates:
501
502* Escaping the AngularJS delimiters by wrapping AngularJS sections with the
503  ``{% verbatim %}`` tag or by escaping each delimiter via ``{{ '{{' }}`` and
504  ``{{ '}}' }}``;
505
506* Changing the delimiters of one of the template engines (depending on which
507  engine you introduced last):
508
509  * For AngularJS, change the interpolation tags using the
510    ``interpolateProvider`` service, for instance at the module initialization
511    time:
512
513    ..  code-block:: javascript
514
515        angular.module('myApp', []).config(function($interpolateProvider) {
516            $interpolateProvider.startSymbol('{[').endSymbol(']}');
517        });
518
519  * For Twig, change the delimiters via the ``tag_variable`` Lexer option::
520
521        $env->setLexer(new \Twig\Lexer($env, [
522            'tag_variable' => ['{[', ']}'],
523        ]));
524
525.. _callback: https://secure.php.net/manual/en/function.is-callable.php
526