blob: f801348d115993b6c0511036365ae32fc7c9f585 [file] [log] [blame]
Matthias Andreas Benkard12a57352021-12-28 18:02:04 +01001Extending Twig
2==============
3
4Twig can be extended in many ways; you can add extra tags, filters, tests,
5operators, global variables, and functions. You can even extend the parser
6itself with node visitors.
7
8.. note::
9
10 The first section of this chapter describes how to extend Twig. If you want
11 to reuse your changes in different projects or if you want to share them
12 with others, you should then create an extension as described in the
13 following section.
14
15.. caution::
16
17 When extending Twig without creating an extension, Twig won't be able to
18 recompile your templates when the PHP code is updated. To see your changes
19 in real-time, either disable template caching or package your code into an
20 extension (see the next section of this chapter).
21
22Before extending Twig, you must understand the differences between all the
23different possible extension points and when to use them.
24
25First, remember that Twig has two main language constructs:
26
27* ``{{ }}``: used to print the result of an expression evaluation;
28
29* ``{% %}``: used to execute statements.
30
31To understand why Twig exposes so many extension points, let's see how to
32implement a *Lorem ipsum* generator (it needs to know the number of words to
33generate).
34
35You can use a ``lipsum`` *tag*:
36
37.. code-block:: twig
38
39 {% lipsum 40 %}
40
41That works, but using a tag for ``lipsum`` is not a good idea for at least
42three main reasons:
43
44* ``lipsum`` is not a language construct;
45* The tag outputs something;
46* The tag is not flexible as you cannot use it in an expression:
47
48 .. code-block:: twig
49
50 {{ 'some text' ~ {% lipsum 40 %} ~ 'some more text' }}
51
52In fact, you rarely need to create tags; and that's good news because tags are
53the most complex extension point.
54
55Now, let's use a ``lipsum`` *filter*:
56
57.. code-block:: twig
58
59 {{ 40|lipsum }}
60
61Again, it works. But a filter should transform the passed value to something
62else. Here, we use the value to indicate the number of words to generate (so,
63``40`` is an argument of the filter, not the value we want to transform).
64
65Next, let's use a ``lipsum`` *function*:
66
67.. code-block:: twig
68
69 {{ lipsum(40) }}
70
71Here we go. For this specific example, the creation of a function is the
72extension point to use. And you can use it anywhere an expression is accepted:
73
74.. code-block:: twig
75
76 {{ 'some text' ~ lipsum(40) ~ 'some more text' }}
77
78 {% set lipsum = lipsum(40) %}
79
80Lastly, you can also use a *global* object with a method able to generate lorem
81ipsum text:
82
83.. code-block:: twig
84
85 {{ text.lipsum(40) }}
86
87As a rule of thumb, use functions for frequently used features and global
88objects for everything else.
89
90Keep in mind the following when you want to extend Twig:
91
92========== ========================== ========== =========================
93What? Implementation difficulty? How often? When?
94========== ========================== ========== =========================
95*macro* simple frequent Content generation
96*global* simple frequent Helper object
97*function* simple frequent Content generation
98*filter* simple frequent Value transformation
99*tag* complex rare DSL language construct
100*test* simple rare Boolean decision
101*operator* simple rare Values transformation
102========== ========================== ========== =========================
103
104Globals
105-------
106
107A global variable is like any other template variable, except that it's
108available in all templates and macros::
109
110 $twig = new \Twig\Environment($loader);
111 $twig->addGlobal('text', new Text());
112
113You can then use the ``text`` variable anywhere in a template:
114
115.. code-block:: twig
116
117 {{ text.lipsum(40) }}
118
119Filters
120-------
121
122Creating a filter consists of associating a name with a PHP callable::
123
124 // an anonymous function
125 $filter = new \Twig\TwigFilter('rot13', function ($string) {
126 return str_rot13($string);
127 });
128
129 // or a simple PHP function
130 $filter = new \Twig\TwigFilter('rot13', 'str_rot13');
131
132 // or a class static method
133 $filter = new \Twig\TwigFilter('rot13', ['SomeClass', 'rot13Filter']);
134 $filter = new \Twig\TwigFilter('rot13', 'SomeClass::rot13Filter');
135
136 // or a class method
137 $filter = new \Twig\TwigFilter('rot13', [$this, 'rot13Filter']);
138 // the one below needs a runtime implementation (see below for more information)
139 $filter = new \Twig\TwigFilter('rot13', ['SomeClass', 'rot13Filter']);
140
141The first argument passed to the ``\Twig\TwigFilter`` constructor is the name of the
142filter you will use in templates and the second one is the PHP callable to
143associate with it.
144
145Then, add the filter to the Twig environment::
146
147 $twig = new \Twig\Environment($loader);
148 $twig->addFilter($filter);
149
150And here is how to use it in a template:
151
152.. code-block:: twig
153
154 {{ 'Twig'|rot13 }}
155
156 {# will output Gjvt #}
157
158When called by Twig, the PHP callable receives the left side of the filter
159(before the pipe ``|``) as the first argument and the extra arguments passed
160to the filter (within parentheses ``()``) as extra arguments.
161
162For instance, the following code:
163
164.. code-block:: twig
165
166 {{ 'TWIG'|lower }}
167 {{ now|date('d/m/Y') }}
168
169is compiled to something like the following::
170
171 <?php echo strtolower('TWIG') ?>
172 <?php echo twig_date_format_filter($now, 'd/m/Y') ?>
173
174The ``\Twig\TwigFilter`` class takes an array of options as its last argument::
175
176 $filter = new \Twig\TwigFilter('rot13', 'str_rot13', $options);
177
178Environment-aware Filters
179~~~~~~~~~~~~~~~~~~~~~~~~~
180
181If you want to access the current environment instance in your filter, set the
182``needs_environment`` option to ``true``; Twig will pass the current
183environment as the first argument to the filter call::
184
185 $filter = new \Twig\TwigFilter('rot13', function (\Twig\Environment $env, $string) {
186 // get the current charset for instance
187 $charset = $env->getCharset();
188
189 return str_rot13($string);
190 }, ['needs_environment' => true]);
191
192Context-aware Filters
193~~~~~~~~~~~~~~~~~~~~~
194
195If you want to access the current context in your filter, set the
196``needs_context`` option to ``true``; Twig will pass the current context as
197the first argument to the filter call (or the second one if
198``needs_environment`` is also set to ``true``)::
199
200 $filter = new \Twig\TwigFilter('rot13', function ($context, $string) {
201 // ...
202 }, ['needs_context' => true]);
203
204 $filter = new \Twig\TwigFilter('rot13', function (\Twig\Environment $env, $context, $string) {
205 // ...
206 }, ['needs_context' => true, 'needs_environment' => true]);
207
208Automatic Escaping
209~~~~~~~~~~~~~~~~~~
210
211If automatic escaping is enabled, the output of the filter may be escaped
212before printing. If your filter acts as an escaper (or explicitly outputs HTML
213or JavaScript code), you will want the raw output to be printed. In such a
214case, set the ``is_safe`` option::
215
216 $filter = new \Twig\TwigFilter('nl2br', 'nl2br', ['is_safe' => ['html']]);
217
218Some filters may need to work on input that is already escaped or safe, for
219example when adding (safe) HTML tags to originally unsafe output. In such a
220case, set the ``pre_escape`` option to escape the input data before it is run
221through your filter::
222
223 $filter = new \Twig\TwigFilter('somefilter', 'somefilter', ['pre_escape' => 'html', 'is_safe' => ['html']]);
224
225Variadic Filters
226~~~~~~~~~~~~~~~~
227
228When a filter should accept an arbitrary number of arguments, set the
229``is_variadic`` option to ``true``; Twig will pass the extra arguments as the
230last argument to the filter call as an array::
231
232 $filter = new \Twig\TwigFilter('thumbnail', function ($file, array $options = []) {
233 // ...
234 }, ['is_variadic' => true]);
235
236Be warned that :ref:`named arguments <named-arguments>` passed to a variadic
237filter cannot be checked for validity as they will automatically end up in the
238option array.
239
240Dynamic Filters
241~~~~~~~~~~~~~~~
242
243A filter name containing the special ``*`` character is a dynamic filter and
244the ``*`` part will match any string::
245
246 $filter = new \Twig\TwigFilter('*_path', function ($name, $arguments) {
247 // ...
248 });
249
250The following filters are matched by the above defined dynamic filter:
251
252* ``product_path``
253* ``category_path``
254
255A dynamic filter can define more than one dynamic parts::
256
257 $filter = new \Twig\TwigFilter('*_path_*', function ($name, $suffix, $arguments) {
258 // ...
259 });
260
261The filter receives all dynamic part values before the normal filter arguments,
262but after the environment and the context. For instance, a call to
263``'foo'|a_path_b()`` will result in the following arguments to be passed to the
264filter: ``('a', 'b', 'foo')``.
265
266Deprecated Filters
267~~~~~~~~~~~~~~~~~~
268
269You can mark a filter as being deprecated by setting the ``deprecated`` option
270to ``true``. You can also give an alternative filter that replaces the
271deprecated one when that makes sense::
272
273 $filter = new \Twig\TwigFilter('obsolete', function () {
274 // ...
275 }, ['deprecated' => true, 'alternative' => 'new_one']);
276
277When a filter is deprecated, Twig emits a deprecation notice when compiling a
278template using it. See :ref:`deprecation-notices` for more information.
279
280Functions
281---------
282
283Functions are defined in the exact same way as filters, but you need to create
284an instance of ``\Twig\TwigFunction``::
285
286 $twig = new \Twig\Environment($loader);
287 $function = new \Twig\TwigFunction('function_name', function () {
288 // ...
289 });
290 $twig->addFunction($function);
291
292Functions support the same features as filters, except for the ``pre_escape``
293and ``preserves_safety`` options.
294
295Tests
296-----
297
298Tests are defined in the exact same way as filters and functions, but you need
299to create an instance of ``\Twig\TwigTest``::
300
301 $twig = new \Twig\Environment($loader);
302 $test = new \Twig\TwigTest('test_name', function () {
303 // ...
304 });
305 $twig->addTest($test);
306
307Tests allow you to create custom application specific logic for evaluating
308boolean conditions. As a simple example, let's create a Twig test that checks if
309objects are 'red'::
310
311 $twig = new \Twig\Environment($loader);
312 $test = new \Twig\TwigTest('red', function ($value) {
313 if (isset($value->color) && $value->color == 'red') {
314 return true;
315 }
316 if (isset($value->paint) && $value->paint == 'red') {
317 return true;
318 }
319 return false;
320 });
321 $twig->addTest($test);
322
323Test functions must always return ``true``/``false``.
324
325When creating tests you can use the ``node_class`` option to provide custom test
326compilation. This is useful if your test can be compiled into PHP primitives.
327This is used by many of the tests built into Twig::
328
329 namespace App;
330
331 use Twig\Environment;
332 use Twig\Node\Expression\TestExpression;
333 use Twig\TwigTest;
334
335 $twig = new Environment($loader);
336 $test = new TwigTest(
337 'odd',
338 null,
339 ['node_class' => OddTestExpression::class]);
340 $twig->addTest($test);
341
342 class OddTestExpression extends TestExpression
343 {
344 public function compile(\Twig\Compiler $compiler)
345 {
346 $compiler
347 ->raw('(')
348 ->subcompile($this->getNode('node'))
349 ->raw(' % 2 != 0')
350 ->raw(')')
351 ;
352 }
353 }
354
355The above example shows how you can create tests that use a node class. The node
356class has access to one sub-node called ``node``. This sub-node contains the
357value that is being tested. When the ``odd`` filter is used in code such as:
358
359.. code-block:: twig
360
361 {% if my_value is odd %}
362
363The ``node`` sub-node will contain an expression of ``my_value``. Node-based
364tests also have access to the ``arguments`` node. This node will contain the
365various other arguments that have been provided to your test.
366
367If you want to pass a variable number of positional or named arguments to the
368test, set the ``is_variadic`` option to ``true``. Tests support dynamic
369names (see dynamic filters for the syntax).
370
371Tags
372----
373
374One of the most exciting features of a template engine like Twig is the
375possibility to define new **language constructs**. This is also the most complex
376feature as you need to understand how Twig's internals work.
377
378Most of the time though, a tag is not needed:
379
380* If your tag generates some output, use a **function** instead.
381
382* If your tag modifies some content and returns it, use a **filter** instead.
383
384 For instance, if you want to create a tag that converts a Markdown formatted
385 text to HTML, create a ``markdown`` filter instead:
386
387 .. code-block:: twig
388
389 {{ '**markdown** text'|markdown }}
390
391 If you want use this filter on large amounts of text, wrap it with the
392 :doc:`apply <tags/apply>` tag:
393
394 .. code-block:: twig
395
396 {% apply markdown %}
397 Title
398 =====
399
400 Much better than creating a tag as you can **compose** filters.
401 {% endapply %}
402
403* If your tag does not output anything, but only exists because of a side
404 effect, create a **function** that returns nothing and call it via the
405 :doc:`filter <tags/do>` tag.
406
407 For instance, if you want to create a tag that logs text, create a ``log``
408 function instead and call it via the :doc:`do <tags/do>` tag:
409
410 .. code-block:: twig
411
412 {% do log('Log some things') %}
413
414If you still want to create a tag for a new language construct, great!
415
416Let's create a ``set`` tag that allows the definition of simple variables from
417within a template. The tag can be used like follows:
418
419.. code-block:: twig
420
421 {% set name = "value" %}
422
423 {{ name }}
424
425 {# should output value #}
426
427.. note::
428
429 The ``set`` tag is part of the Core extension and as such is always
430 available. The built-in version is slightly more powerful and supports
431 multiple assignments by default.
432
433Three steps are needed to define a new tag:
434
435* Defining a Token Parser class (responsible for parsing the template code);
436
437* Defining a Node class (responsible for converting the parsed code to PHP);
438
439* Registering the tag.
440
441Registering a new tag
442~~~~~~~~~~~~~~~~~~~~~
443
444Add a tag by calling the ``addTokenParser`` method on the ``\Twig\Environment``
445instance::
446
447 $twig = new \Twig\Environment($loader);
448 $twig->addTokenParser(new Project_Set_TokenParser());
449
450Defining a Token Parser
451~~~~~~~~~~~~~~~~~~~~~~~
452
453Now, let's see the actual code of this class::
454
455 class Project_Set_TokenParser extends \Twig\TokenParser\AbstractTokenParser
456 {
457 public function parse(\Twig\Token $token)
458 {
459 $parser = $this->parser;
460 $stream = $parser->getStream();
461
462 $name = $stream->expect(\Twig\Token::NAME_TYPE)->getValue();
463 $stream->expect(\Twig\Token::OPERATOR_TYPE, '=');
464 $value = $parser->getExpressionParser()->parseExpression();
465 $stream->expect(\Twig\Token::BLOCK_END_TYPE);
466
467 return new Project_Set_Node($name, $value, $token->getLine(), $this->getTag());
468 }
469
470 public function getTag()
471 {
472 return 'set';
473 }
474 }
475
476The ``getTag()`` method must return the tag we want to parse, here ``set``.
477
478The ``parse()`` method is invoked whenever the parser encounters a ``set``
479tag. It should return a ``\Twig\Node\Node`` instance that represents the node (the
480``Project_Set_Node`` calls creating is explained in the next section).
481
482The parsing process is simplified thanks to a bunch of methods you can call
483from the token stream (``$this->parser->getStream()``):
484
485* ``getCurrent()``: Gets the current token in the stream.
486
487* ``next()``: Moves to the next token in the stream, *but returns the old one*.
488
489* ``test($type)``, ``test($value)`` or ``test($type, $value)``: Determines whether
490 the current token is of a particular type or value (or both). The value may be an
491 array of several possible values.
492
493* ``expect($type[, $value[, $message]])``: If the current token isn't of the given
494 type/value a syntax error is thrown. Otherwise, if the type and value are correct,
495 the token is returned and the stream moves to the next token.
496
497* ``look()``: Looks at the next token without consuming it.
498
499Parsing expressions is done by calling the ``parseExpression()`` like we did for
500the ``set`` tag.
501
502.. tip::
503
504 Reading the existing ``TokenParser`` classes is the best way to learn all
505 the nitty-gritty details of the parsing process.
506
507Defining a Node
508~~~~~~~~~~~~~~~
509
510The ``Project_Set_Node`` class itself is quite short::
511
512 class Project_Set_Node extends \Twig\Node\Node
513 {
514 public function __construct($name, \Twig\Node\Expression\AbstractExpression $value, $line, $tag = null)
515 {
516 parent::__construct(['value' => $value], ['name' => $name], $line, $tag);
517 }
518
519 public function compile(\Twig\Compiler $compiler)
520 {
521 $compiler
522 ->addDebugInfo($this)
523 ->write('$context[\''.$this->getAttribute('name').'\'] = ')
524 ->subcompile($this->getNode('value'))
525 ->raw(";\n")
526 ;
527 }
528 }
529
530The compiler implements a fluid interface and provides methods that helps the
531developer generate beautiful and readable PHP code:
532
533* ``subcompile()``: Compiles a node.
534
535* ``raw()``: Writes the given string as is.
536
537* ``write()``: Writes the given string by adding indentation at the beginning
538 of each line.
539
540* ``string()``: Writes a quoted string.
541
542* ``repr()``: Writes a PHP representation of a given value (see
543 ``\Twig\Node\ForNode`` for a usage example).
544
545* ``addDebugInfo()``: Adds the line of the original template file related to
546 the current node as a comment.
547
548* ``indent()``: Indents the generated code (see ``\Twig\Node\BlockNode`` for a
549 usage example).
550
551* ``outdent()``: Outdents the generated code (see ``\Twig\Node\BlockNode`` for a
552 usage example).
553
554.. _creating_extensions:
555
556Creating an Extension
557---------------------
558
559The main motivation for writing an extension is to move often used code into a
560reusable class like adding support for internationalization. An extension can
561define tags, filters, tests, operators, functions, and node visitors.
562
563Most of the time, it is useful to create a single extension for your project,
564to host all the specific tags and filters you want to add to Twig.
565
566.. tip::
567
568 When packaging your code into an extension, Twig is smart enough to
569 recompile your templates whenever you make a change to it (when
570 ``auto_reload`` is enabled).
571
572An extension is a class that implements the following interface::
573
574 interface \Twig\Extension\ExtensionInterface
575 {
576 /**
577 * Returns the token parser instances to add to the existing list.
578 *
579 * @return \Twig\TokenParser\TokenParserInterface[]
580 */
581 public function getTokenParsers();
582
583 /**
584 * Returns the node visitor instances to add to the existing list.
585 *
586 * @return \Twig\NodeVisitor\NodeVisitorInterface[]
587 */
588 public function getNodeVisitors();
589
590 /**
591 * Returns a list of filters to add to the existing list.
592 *
593 * @return \Twig\TwigFilter[]
594 */
595 public function getFilters();
596
597 /**
598 * Returns a list of tests to add to the existing list.
599 *
600 * @return \Twig\TwigTest[]
601 */
602 public function getTests();
603
604 /**
605 * Returns a list of functions to add to the existing list.
606 *
607 * @return \Twig\TwigFunction[]
608 */
609 public function getFunctions();
610
611 /**
612 * Returns a list of operators to add to the existing list.
613 *
614 * @return array<array> First array of unary operators, second array of binary operators
615 */
616 public function getOperators();
617 }
618
619To keep your extension class clean and lean, inherit from the built-in
620``\Twig\Extension\AbstractExtension`` class instead of implementing the interface as it provides
621empty implementations for all methods::
622
623 class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
624 {
625 }
626
627This extension does nothing for now. We will customize it in the next sections.
628
629You can save your extension anywhere on the filesystem, as all extensions must
630be registered explicitly to be available in your templates.
631
632You can register an extension by using the ``addExtension()`` method on your
633main ``Environment`` object::
634
635 $twig = new \Twig\Environment($loader);
636 $twig->addExtension(new Project_Twig_Extension());
637
638.. tip::
639
640 The Twig core extensions are great examples of how extensions work.
641
642Globals
643~~~~~~~
644
645Global variables can be registered in an extension via the ``getGlobals()``
646method::
647
648 class Project_Twig_Extension extends \Twig\Extension\AbstractExtension implements \Twig\Extension\GlobalsInterface
649 {
650 public function getGlobals(): array
651 {
652 return [
653 'text' => new Text(),
654 ];
655 }
656
657 // ...
658 }
659
660Functions
661~~~~~~~~~
662
663Functions can be registered in an extension via the ``getFunctions()``
664method::
665
666 class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
667 {
668 public function getFunctions()
669 {
670 return [
671 new \Twig\TwigFunction('lipsum', 'generate_lipsum'),
672 ];
673 }
674
675 // ...
676 }
677
678Filters
679~~~~~~~
680
681To add a filter to an extension, you need to override the ``getFilters()``
682method. This method must return an array of filters to add to the Twig
683environment::
684
685 class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
686 {
687 public function getFilters()
688 {
689 return [
690 new \Twig\TwigFilter('rot13', 'str_rot13'),
691 ];
692 }
693
694 // ...
695 }
696
697Tags
698~~~~
699
700Adding a tag in an extension can be done by overriding the
701``getTokenParsers()`` method. This method must return an array of tags to add
702to the Twig environment::
703
704 class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
705 {
706 public function getTokenParsers()
707 {
708 return [new Project_Set_TokenParser()];
709 }
710
711 // ...
712 }
713
714In the above code, we have added a single new tag, defined by the
715``Project_Set_TokenParser`` class. The ``Project_Set_TokenParser`` class is
716responsible for parsing the tag and compiling it to PHP.
717
718Operators
719~~~~~~~~~
720
721The ``getOperators()`` methods lets you add new operators. Here is how to add
722the ``!``, ``||``, and ``&&`` operators::
723
724 class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
725 {
726 public function getOperators()
727 {
728 return [
729 [
730 '!' => ['precedence' => 50, 'class' => \Twig\Node\Expression\Unary\NotUnary::class],
731 ],
732 [
733 '||' => ['precedence' => 10, 'class' => \Twig\Node\Expression\Binary\OrBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT],
734 '&&' => ['precedence' => 15, 'class' => \Twig\Node\Expression\Binary\AndBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT],
735 ],
736 ];
737 }
738
739 // ...
740 }
741
742Tests
743~~~~~
744
745The ``getTests()`` method lets you add new test functions::
746
747 class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
748 {
749 public function getTests()
750 {
751 return [
752 new \Twig\TwigTest('even', 'twig_test_even'),
753 ];
754 }
755
756 // ...
757 }
758
759Definition vs Runtime
760~~~~~~~~~~~~~~~~~~~~~
761
762Twig filters, functions, and tests runtime implementations can be defined as
763any valid PHP callable:
764
765* **functions/static methods**: Simple to implement and fast (used by all Twig
766 core extensions); but it is hard for the runtime to depend on external
767 objects;
768
769* **closures**: Simple to implement;
770
771* **object methods**: More flexible and required if your runtime code depends
772 on external objects.
773
774The simplest way to use methods is to define them on the extension itself::
775
776 class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
777 {
778 private $rot13Provider;
779
780 public function __construct($rot13Provider)
781 {
782 $this->rot13Provider = $rot13Provider;
783 }
784
785 public function getFunctions()
786 {
787 return [
788 new \Twig\TwigFunction('rot13', [$this, 'rot13']),
789 ];
790 }
791
792 public function rot13($value)
793 {
794 return $this->rot13Provider->rot13($value);
795 }
796 }
797
798This is very convenient but not recommended as it makes template compilation
799depend on runtime dependencies even if they are not needed (think for instance
800as a dependency that connects to a database engine).
801
802You can decouple the extension definitions from their runtime implementations by
803registering a ``\Twig\RuntimeLoader\RuntimeLoaderInterface`` instance on the
804environment that knows how to instantiate such runtime classes (runtime classes
805must be autoload-able)::
806
807 class RuntimeLoader implements \Twig\RuntimeLoader\RuntimeLoaderInterface
808 {
809 public function load($class)
810 {
811 // implement the logic to create an instance of $class
812 // and inject its dependencies
813 // most of the time, it means using your dependency injection container
814 if ('Project_Twig_RuntimeExtension' === $class) {
815 return new $class(new Rot13Provider());
816 } else {
817 // ...
818 }
819 }
820 }
821
822 $twig->addRuntimeLoader(new RuntimeLoader());
823
824.. note::
825
826 Twig comes with a PSR-11 compatible runtime loader
827 (``\Twig\RuntimeLoader\ContainerRuntimeLoader``).
828
829It is now possible to move the runtime logic to a new
830``Project_Twig_RuntimeExtension`` class and use it directly in the extension::
831
832 class Project_Twig_RuntimeExtension
833 {
834 private $rot13Provider;
835
836 public function __construct($rot13Provider)
837 {
838 $this->rot13Provider = $rot13Provider;
839 }
840
841 public function rot13($value)
842 {
843 return $this->rot13Provider->rot13($value);
844 }
845 }
846
847 class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
848 {
849 public function getFunctions()
850 {
851 return [
852 new \Twig\TwigFunction('rot13', ['Project_Twig_RuntimeExtension', 'rot13']),
853 // or
854 new \Twig\TwigFunction('rot13', 'Project_Twig_RuntimeExtension::rot13'),
855 ];
856 }
857 }
858
859Testing an Extension
860--------------------
861
862Functional Tests
863~~~~~~~~~~~~~~~~
864
865You can create functional tests for extensions by creating the following file
866structure in your test directory::
867
868 Fixtures/
869 filters/
870 foo.test
871 bar.test
872 functions/
873 foo.test
874 bar.test
875 tags/
876 foo.test
877 bar.test
878 IntegrationTest.php
879
880The ``IntegrationTest.php`` file should look like this::
881
882 use Twig\Test\IntegrationTestCase;
883
884 class Project_Tests_IntegrationTest extends IntegrationTestCase
885 {
886 public function getExtensions()
887 {
888 return [
889 new Project_Twig_Extension1(),
890 new Project_Twig_Extension2(),
891 ];
892 }
893
894 public function getFixturesDir()
895 {
896 return __DIR__.'/Fixtures/';
897 }
898 }
899
900Fixtures examples can be found within the Twig repository
901`tests/Twig/Fixtures`_ directory.
902
903Node Tests
904~~~~~~~~~~
905
906Testing the node visitors can be complex, so extend your test cases from
907``\Twig\Test\NodeTestCase``. Examples can be found in the Twig repository
908`tests/Twig/Node`_ directory.
909
910.. _`tests/Twig/Fixtures`: https://github.com/twigphp/Twig/tree/3.x/tests/Fixtures
911.. _`tests/Twig/Node`: https://github.com/twigphp/Twig/tree/3.x/tests/Node