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