Matthias Andreas Benkard | 12a5735 | 2021-12-28 18:02:04 +0100 | [diff] [blame^] | 1 | Recipes |
| 2 | ======= |
| 3 | |
| 4 | .. _deprecation-notices: |
| 5 | |
| 6 | Displaying Deprecation Notices |
| 7 | ------------------------------ |
| 8 | |
| 9 | Deprecated features generate deprecation notices (via a call to the |
| 10 | ``trigger_error()`` PHP function). By default, they are silenced and never |
| 11 | displayed nor logged. |
| 12 | |
| 13 | To remove all deprecated feature usages from your templates, write and run a |
| 14 | script 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 | |
| 24 | The ``collectDir()`` method compiles all templates found in a directory, |
| 25 | catches 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 | |
| 34 | However, this code won't find all deprecations (like using deprecated some Twig |
| 35 | classes). To catch all notices, register a custom error handler like the one |
| 36 | below:: |
| 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 | |
| 49 | Note that most deprecation notices are triggered during **compilation**, so |
| 50 | they 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 | |
| 59 | Making a Layout conditional |
| 60 | --------------------------- |
| 61 | |
| 62 | Working with Ajax means that the same content is sometimes displayed as is, |
| 63 | and sometimes decorated with a layout. As Twig layout template names can be |
| 64 | any valid expression, you can pass a variable that evaluates to ``true`` when |
| 65 | the 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 | |
| 75 | Making an Include dynamic |
| 76 | ------------------------- |
| 77 | |
| 78 | When including a template, its name does not need to be a string. For |
| 79 | instance, the name can depend on the value of a variable: |
| 80 | |
| 81 | .. code-block:: twig |
| 82 | |
| 83 | {% include var ~ '_foo.html' %} |
| 84 | |
| 85 | If ``var`` evaluates to ``index``, the ``index_foo.html`` template will be |
| 86 | rendered. |
| 87 | |
| 88 | As a matter of fact, the template name can be any valid expression, such as |
| 89 | the following: |
| 90 | |
| 91 | .. code-block:: twig |
| 92 | |
| 93 | {% include var|default('index') ~ '_foo.html' %} |
| 94 | |
| 95 | Overriding a Template that also extends itself |
| 96 | ---------------------------------------------- |
| 97 | |
| 98 | A 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 | |
| 107 | But how do you combine both: *replace* a template that also extends itself |
| 108 | (aka a template in a directory further in the list)? |
| 109 | |
| 110 | Let's say that your templates are loaded from both ``.../templates/mysite`` |
| 111 | and ``.../templates/default`` in this order. The ``page.twig`` template, |
| 112 | stored 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 | |
| 122 | You 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 |
| 124 | might 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 | |
| 131 | However, this will not work as Twig will always load the template from |
| 132 | ``.../templates/mysite``. |
| 133 | |
| 134 | It turns out it is possible to get this to work, by adding a directory right |
| 135 | at the end of your template directories, which is the parent of all of the |
| 136 | other directories: ``.../templates`` in our case. This has the effect of |
| 137 | making every template file within our system uniquely addressable. Most of the |
| 138 | time you will use the "normal" paths, but in the special case of wanting to |
| 139 | extend a template with an overriding version of itself we can reference its |
| 140 | parent'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 | |
| 152 | Customizing the Syntax |
| 153 | ---------------------- |
| 154 | |
| 155 | Twig allows some syntax customization for the block delimiters. It's **not** |
| 156 | recommended to use this feature as templates will be tied with your custom |
| 157 | syntax. But for specific projects, it can make sense to change the defaults. |
| 158 | |
| 159 | To 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 | |
| 171 | Here are some configuration example that simulates some other template engines |
| 172 | syntax:: |
| 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 | |
| 195 | Using dynamic Object Properties |
| 196 | ------------------------------- |
| 197 | |
| 198 | When Twig encounters a variable like ``article.title``, it tries to find a |
| 199 | ``title`` public property in the ``article`` object. |
| 200 | |
| 201 | It also works if the property does not exist but is rather defined dynamically |
| 202 | thanks 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 | |
| 226 | Accessing the parent Context in Nested Loops |
| 227 | -------------------------------------------- |
| 228 | |
| 229 | Sometimes, when using nested loops, you need to access the parent context. The |
| 230 | parent context is always accessible via the ``loop.parent`` variable. For |
| 231 | instance, 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 | |
| 240 | And 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 | |
| 251 | The 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 | |
| 262 | In the inner loop, the ``loop.parent`` variable is used to access the outer |
| 263 | context. So, the index of the current ``topic`` defined in the outer for loop |
| 264 | is accessible via the ``loop.parent.loop.index`` variable. |
| 265 | |
| 266 | Defining 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 | |
| 274 | When 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 | |
| 278 | For tags, register callbacks with ``registerUndefinedTokenParserCallback()``. |
| 279 | For filters, register callbacks with ``registerUndefinedFilterCallback()``. |
| 280 | For 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 | |
| 292 | If the callable is not able to return a valid function/filter/tag, it must |
| 293 | return ``false``. |
| 294 | |
| 295 | If you register more than one callback, Twig will call them in turn until one |
| 296 | does 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 | |
| 303 | Validating the Template Syntax |
| 304 | ------------------------------ |
| 305 | |
| 306 | When template code is provided by a third-party (through a web interface for |
| 307 | instance), it might be interesting to validate the template syntax before |
| 308 | saving it. If the template code is stored in a ``$template`` variable, here is |
| 309 | how 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 | |
| 319 | If 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 | |
| 338 | Refreshing modified Templates when OPcache or APC is enabled |
| 339 | ------------------------------------------------------------ |
| 340 | |
| 341 | When using OPcache with ``opcache.validate_timestamps`` set to ``0`` or APC |
| 342 | with ``apc.stat`` set to ``0`` and Twig cache enabled, clearing the template |
| 343 | cache won't update the cache. |
| 344 | |
| 345 | To 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 | |
| 352 | Reusing a stateful Node Visitor |
| 353 | ------------------------------- |
| 354 | |
| 355 | When attaching a visitor to a ``\Twig\Environment`` instance, Twig uses it to |
| 356 | visit *all* templates it compiles. If you need to keep some state information |
| 357 | around, you probably want to reset it when visiting a new template. |
| 358 | |
| 359 | This 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 | |
| 375 | Using a Database to store Templates |
| 376 | ----------------------------------- |
| 377 | |
| 378 | If you are developing a CMS, templates are usually stored in a database. This |
| 379 | recipe gives you a simple PDO template loader you can use as a starting point |
| 380 | for your own. |
| 381 | |
| 382 | First, 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 | |
| 395 | We have created a simple ``templates`` table that hosts two templates: |
| 396 | ``base.twig`` and ``index.twig``. |
| 397 | |
| 398 | Now, 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 | |
| 446 | Finally, 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 | |
| 453 | Using different Template Sources |
| 454 | -------------------------------- |
| 455 | |
| 456 | This recipe is the continuation of the previous one. Even if you store the |
| 457 | contributed templates in a database, you might want to keep the original/base |
| 458 | templates on the filesystem. When templates can be loaded from different |
| 459 | sources, you need to use the ``\Twig\Loader\ChainLoader`` loader. |
| 460 | |
| 461 | As you can see in the previous recipe, we reference the template in the exact |
| 462 | same way as we would have done it with a regular filesystem loader. This is |
| 463 | the key to be able to mix and match templates coming from the database, the |
| 464 | filesystem, or any other loader for that matter: the template name should be a |
| 465 | logical 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 | |
| 477 | Now that the ``base.twig`` templates is defined in an array loader, you can |
| 478 | remove it from the database, and everything else will still work as before. |
| 479 | |
| 480 | Loading a Template from a String |
| 481 | -------------------------------- |
| 482 | |
| 483 | From 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 | |
| 491 | From 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 | |
| 497 | Using Twig and AngularJS in the same Templates |
| 498 | ---------------------------------------------- |
| 499 | |
| 500 | Mixing different template syntaxes in the same file is not a recommended |
| 501 | practice as both AngularJS and Twig use the same delimiters in their syntax: |
| 502 | ``{{`` and ``}}``. |
| 503 | |
| 504 | Still, if you want to use AngularJS and Twig in the same template, there are |
| 505 | two ways to make it work depending on the amount of AngularJS you need to |
| 506 | include 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 |