blob: 01c937daddad506db6631d9de95ec13c5361d8e8 [file] [log] [blame]
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +01001Recipes
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, Filters, and Tags on the Fly
267----------------------------------------------------------
268
269.. versionadded:: 3.2
270
271 The ``registerUndefinedTokenParserCallback()`` method was added in Twig
272 3.2.
273
274When a function/filter/tag is not defined, Twig defaults to throw a
275``\Twig\Error\SyntaxError`` exception. However, it can also call a `callback`_
276(any valid PHP callable) which should return a function/filter/tag.
277
278For tags, register callbacks with ``registerUndefinedTokenParserCallback()``.
279For filters, register callbacks with ``registerUndefinedFilterCallback()``.
280For functions, use ``registerUndefinedFunctionCallback()``::
281
282 // auto-register all native PHP functions as Twig functions
283 // NEVER do this in a project as it's NOT secure
284 $twig->registerUndefinedFunctionCallback(function ($name) {
285 if (function_exists($name)) {
286 return new \Twig\TwigFunction($name, $name);
287 }
288
289 return false;
290 });
291
292If the callable is not able to return a valid function/filter/tag, it must
293return ``false``.
294
295If you register more than one callback, Twig will call them in turn until one
296does not return ``false``.
297
298.. tip::
299
300 As the resolution of functions/filters/tags is done during compilation,
301 there is no overhead when registering these callbacks.
302
303Validating the Template Syntax
304------------------------------
305
306When template code is provided by a third-party (through a web interface for
307instance), it might be interesting to validate the template syntax before
308saving it. If the template code is stored in a ``$template`` variable, here is
309how you can do it::
310
311 try {
312 $twig->parse($twig->tokenize(new \Twig\Source($template)));
313
314 // the $template is valid
315 } catch (\Twig\Error\SyntaxError $e) {
316 // $template contains one or more syntax errors
317 }
318
319If you iterate over a set of files, you can pass the filename to the
320``tokenize()`` method to get the filename in the exception message::
321
322 foreach ($files as $file) {
323 try {
324 $twig->parse($twig->tokenize(new \Twig\Source($template, $file->getFilename(), $file)));
325
326 // the $template is valid
327 } catch (\Twig\Error\SyntaxError $e) {
328 // $template contains one or more syntax errors
329 }
330 }
331
332.. note::
333
334 This method won't catch any sandbox policy violations because the policy
335 is enforced during template rendering (as Twig needs the context for some
336 checks like allowed methods on objects).
337
338Refreshing modified Templates when OPcache or APC is enabled
339------------------------------------------------------------
340
341When using OPcache with ``opcache.validate_timestamps`` set to ``0`` or APC
342with ``apc.stat`` set to ``0`` and Twig cache enabled, clearing the template
343cache won't update the cache.
344
345To get around this, force Twig to invalidate the bytecode cache::
346
347 $twig = new \Twig\Environment($loader, [
348 'cache' => new \Twig\Cache\FilesystemCache('/some/cache/path', \Twig\Cache\FilesystemCache::FORCE_BYTECODE_INVALIDATION),
349 // ...
350 ]);
351
352Reusing a stateful Node Visitor
353-------------------------------
354
355When attaching a visitor to a ``\Twig\Environment`` instance, Twig uses it to
356visit *all* templates it compiles. If you need to keep some state information
357around, you probably want to reset it when visiting a new template.
358
359This can be achieved with the following code::
360
361 protected $someTemplateState = [];
362
363 public function enterNode(\Twig\Node\Node $node, \Twig\Environment $env)
364 {
365 if ($node instanceof \Twig\Node\ModuleNode) {
366 // reset the state as we are entering a new template
367 $this->someTemplateState = [];
368 }
369
370 // ...
371
372 return $node;
373 }
374
375Using a Database to store Templates
376-----------------------------------
377
378If you are developing a CMS, templates are usually stored in a database. This
379recipe gives you a simple PDO template loader you can use as a starting point
380for your own.
381
382First, let's create a temporary in-memory SQLite3 database to work with::
383
384 $dbh = new PDO('sqlite::memory:');
385 $dbh->exec('CREATE TABLE templates (name STRING, source STRING, last_modified INTEGER)');
386 $base = '{% block content %}{% endblock %}';
387 $index = '
388 {% extends "base.twig" %}
389 {% block content %}Hello {{ name }}{% endblock %}
390 ';
391 $now = time();
392 $dbh->prepare('INSERT INTO templates (name, source, last_modified) VALUES (?, ?, ?)')->execute(['base.twig', $base, $now]);
393 $dbh->prepare('INSERT INTO templates (name, source, last_modified) VALUES (?, ?, ?)')->execute(['index.twig', $index, $now]);
394
395We have created a simple ``templates`` table that hosts two templates:
396``base.twig`` and ``index.twig``.
397
398Now, let's define a loader able to use this database::
399
400 class DatabaseTwigLoader implements \Twig\Loader\LoaderInterface
401 {
402 protected $dbh;
403
404 public function __construct(PDO $dbh)
405 {
406 $this->dbh = $dbh;
407 }
408
409 public function getSourceContext(string $name): Source
410 {
411 if (false === $source = $this->getValue('source', $name)) {
412 throw new \Twig\Error\LoaderError(sprintf('Template "%s" does not exist.', $name));
413 }
414
415 return new \Twig\Source($source, $name);
416 }
417
418 public function exists(string $name)
419 {
420 return $name === $this->getValue('name', $name);
421 }
422
423 public function getCacheKey(string $name): string
424 {
425 return $name;
426 }
427
428 public function isFresh(string $name, int $time): bool
429 {
430 if (false === $lastModified = $this->getValue('last_modified', $name)) {
431 return false;
432 }
433
434 return $lastModified <= $time;
435 }
436
437 protected function getValue($column, $name)
438 {
439 $sth = $this->dbh->prepare('SELECT '.$column.' FROM templates WHERE name = :name');
440 $sth->execute([':name' => (string) $name]);
441
442 return $sth->fetchColumn();
443 }
444 }
445
446Finally, here is an example on how you can use it::
447
448 $loader = new DatabaseTwigLoader($dbh);
449 $twig = new \Twig\Environment($loader);
450
451 echo $twig->render('index.twig', ['name' => 'Fabien']);
452
453Using different Template Sources
454--------------------------------
455
456This recipe is the continuation of the previous one. Even if you store the
457contributed templates in a database, you might want to keep the original/base
458templates on the filesystem. When templates can be loaded from different
459sources, you need to use the ``\Twig\Loader\ChainLoader`` loader.
460
461As you can see in the previous recipe, we reference the template in the exact
462same way as we would have done it with a regular filesystem loader. This is
463the key to be able to mix and match templates coming from the database, the
464filesystem, or any other loader for that matter: the template name should be a
465logical name, and not the path from the filesystem::
466
467 $loader1 = new DatabaseTwigLoader($dbh);
468 $loader2 = new \Twig\Loader\ArrayLoader([
469 'base.twig' => '{% block content %}{% endblock %}',
470 ]);
471 $loader = new \Twig\Loader\ChainLoader([$loader1, $loader2]);
472
473 $twig = new \Twig\Environment($loader);
474
475 echo $twig->render('index.twig', ['name' => 'Fabien']);
476
477Now that the ``base.twig`` templates is defined in an array loader, you can
478remove it from the database, and everything else will still work as before.
479
480Loading a Template from a String
481--------------------------------
482
483From a template, you can load a template stored in a string via the
484``template_from_string`` function (via the
485``\Twig\Extension\StringLoaderExtension`` extension):
486
487.. code-block:: twig
488
489 {{ include(template_from_string("Hello {{ name }}")) }}
490
491From PHP, it's also possible to load a template stored in a string via
492``\Twig\Environment::createTemplate()``::
493
494 $template = $twig->createTemplate('hello {{ name }}');
495 echo $template->render(['name' => 'Fabien']);
496
497Using Twig and AngularJS in the same Templates
498----------------------------------------------
499
500Mixing different template syntaxes in the same file is not a recommended
501practice as both AngularJS and Twig use the same delimiters in their syntax:
502``{{`` and ``}}``.
503
504Still, if you want to use AngularJS and Twig in the same template, there are
505two ways to make it work depending on the amount of AngularJS you need to
506include in your templates:
507
508* Escaping the AngularJS delimiters by wrapping AngularJS sections with the
509 ``{% verbatim %}`` tag or by escaping each delimiter via ``{{ '{{' }}`` and
510 ``{{ '}}' }}``;
511
512* Changing the delimiters of one of the template engines (depending on which
513 engine you introduced last):
514
515 * For AngularJS, change the interpolation tags using the
516 ``interpolateProvider`` service, for instance at the module initialization
517 time:
518
519 .. code-block:: javascript
520
521 angular.module('myApp', []).config(function($interpolateProvider) {
522 $interpolateProvider.startSymbol('{[').endSymbol(']}');
523 });
524
525 * For Twig, change the delimiters via the ``tag_variable`` Lexer option::
526
527 $env->setLexer(new \Twig\Lexer($env, [
528 'tag_variable' => ['{[', ']}'],
529 ]));
530
531.. _callback: https://secure.php.net/manual/en/function.is-callable.php