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