1Extending 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
367.. versionadded:: 2.6
368
369    Dynamic tests support was added in Twig 2.6.
370
371If you want to pass a variable number of positional or named arguments to the
372test, set the ``is_variadic`` option to ``true``. Tests support dynamic
373names (see dynamic filters for the syntax).
374
375Tags
376----
377
378One of the most exciting features of a template engine like Twig is the
379possibility to define new **language constructs**. This is also the most complex
380feature as you need to understand how Twig's internals work.
381
382Most of the time though, a tag is not needed:
383
384* If your tag generates some output, use a **function** instead.
385
386* If your tag modifies some content and returns it, use a **filter** instead.
387
388  For instance, if you want to create a tag that converts a Markdown formatted
389  text to HTML, create a ``markdown`` filter instead:
390
391  .. code-block:: twig
392
393      {{ '**markdown** text'|markdown }}
394
395  If you want use this filter on large amounts of text, wrap it with the
396  :doc:`apply <tags/apply>` tag:
397
398  .. code-block:: twig
399
400      {% apply markdown %}
401      Title
402      =====
403
404      Much better than creating a tag as you can **compose** filters.
405      {% endapply %}
406
407 .. note::
408
409      The ``apply`` tag was introduced in Twig 2.9; use the ``filter`` tag with
410      previous versions.
411
412* If your tag does not output anything, but only exists because of a side
413  effect, create a **function** that returns nothing and call it via the
414  :doc:`filter <tags/do>` tag.
415
416  For instance, if you want to create a tag that logs text, create a ``log``
417  function instead and call it via the :doc:`do <tags/do>` tag:
418
419  .. code-block:: twig
420
421      {% do log('Log some things') %}
422
423If you still want to create a tag for a new language construct, great!
424
425Let's create a ``set`` tag that allows the definition of simple variables from
426within a template. The tag can be used like follows:
427
428.. code-block:: twig
429
430    {% set name = "value" %}
431
432    {{ name }}
433
434    {# should output value #}
435
436.. note::
437
438    The ``set`` tag is part of the Core extension and as such is always
439    available. The built-in version is slightly more powerful and supports
440    multiple assignments by default.
441
442Three steps are needed to define a new tag:
443
444* Defining a Token Parser class (responsible for parsing the template code);
445
446* Defining a Node class (responsible for converting the parsed code to PHP);
447
448* Registering the tag.
449
450Registering a new tag
451~~~~~~~~~~~~~~~~~~~~~
452
453Add a tag by calling the ``addTokenParser`` method on the ``\Twig\Environment``
454instance::
455
456    $twig = new \Twig\Environment($loader);
457    $twig->addTokenParser(new Project_Set_TokenParser());
458
459Defining a Token Parser
460~~~~~~~~~~~~~~~~~~~~~~~
461
462Now, let's see the actual code of this class::
463
464    class Project_Set_TokenParser extends \Twig\TokenParser\AbstractTokenParser
465    {
466        public function parse(\Twig\Token $token)
467        {
468            $parser = $this->parser;
469            $stream = $parser->getStream();
470
471            $name = $stream->expect(\Twig\Token::NAME_TYPE)->getValue();
472            $stream->expect(\Twig\Token::OPERATOR_TYPE, '=');
473            $value = $parser->getExpressionParser()->parseExpression();
474            $stream->expect(\Twig\Token::BLOCK_END_TYPE);
475
476            return new Project_Set_Node($name, $value, $token->getLine(), $this->getTag());
477        }
478
479        public function getTag()
480        {
481            return 'set';
482        }
483    }
484
485The ``getTag()`` method must return the tag we want to parse, here ``set``.
486
487The ``parse()`` method is invoked whenever the parser encounters a ``set``
488tag. It should return a ``\Twig\Node\Node`` instance that represents the node (the
489``Project_Set_Node`` calls creating is explained in the next section).
490
491The parsing process is simplified thanks to a bunch of methods you can call
492from the token stream (``$this->parser->getStream()``):
493
494* ``getCurrent()``: Gets the current token in the stream.
495
496* ``next()``: Moves to the next token in the stream, *but returns the old one*.
497
498* ``test($type)``, ``test($value)`` or ``test($type, $value)``: Determines whether
499  the current token is of a particular type or value (or both). The value may be an
500  array of several possible values.
501
502* ``expect($type[, $value[, $message]])``: If the current token isn't of the given
503  type/value a syntax error is thrown. Otherwise, if the type and value are correct,
504  the token is returned and the stream moves to the next token.
505
506* ``look()``: Looks at the next token without consuming it.
507
508Parsing expressions is done by calling the ``parseExpression()`` like we did for
509the ``set`` tag.
510
511.. tip::
512
513    Reading the existing ``TokenParser`` classes is the best way to learn all
514    the nitty-gritty details of the parsing process.
515
516Defining a Node
517~~~~~~~~~~~~~~~
518
519The ``Project_Set_Node`` class itself is quite short::
520
521    class Project_Set_Node extends \Twig\Node\Node
522    {
523        public function __construct($name, \Twig\Node\Expression\AbstractExpression $value, $line, $tag = null)
524        {
525            parent::__construct(['value' => $value], ['name' => $name], $line, $tag);
526        }
527
528        public function compile(\Twig\Compiler $compiler)
529        {
530            $compiler
531                ->addDebugInfo($this)
532                ->write('$context[\''.$this->getAttribute('name').'\'] = ')
533                ->subcompile($this->getNode('value'))
534                ->raw(";\n")
535            ;
536        }
537    }
538
539The compiler implements a fluid interface and provides methods that helps the
540developer generate beautiful and readable PHP code:
541
542* ``subcompile()``: Compiles a node.
543
544* ``raw()``: Writes the given string as is.
545
546* ``write()``: Writes the given string by adding indentation at the beginning
547  of each line.
548
549* ``string()``: Writes a quoted string.
550
551* ``repr()``: Writes a PHP representation of a given value (see
552  ``\Twig\Node\ForNode`` for a usage example).
553
554* ``addDebugInfo()``: Adds the line of the original template file related to
555  the current node as a comment.
556
557* ``indent()``: Indents the generated code (see ``\Twig\Node\BlockNode`` for a
558  usage example).
559
560* ``outdent()``: Outdents the generated code (see ``\Twig\Node\BlockNode`` for a
561  usage example).
562
563.. _creating_extensions:
564
565Creating an Extension
566---------------------
567
568The main motivation for writing an extension is to move often used code into a
569reusable class like adding support for internationalization. An extension can
570define tags, filters, tests, operators, functions, and node visitors.
571
572Most of the time, it is useful to create a single extension for your project,
573to host all the specific tags and filters you want to add to Twig.
574
575.. tip::
576
577    When packaging your code into an extension, Twig is smart enough to
578    recompile your templates whenever you make a change to it (when
579    ``auto_reload`` is enabled).
580
581An extension is a class that implements the following interface::
582
583    interface \Twig\Extension\ExtensionInterface
584    {
585        /**
586         * Returns the token parser instances to add to the existing list.
587         *
588         * @return \Twig\TokenParser\TokenParserInterface[]
589         */
590        public function getTokenParsers();
591
592        /**
593         * Returns the node visitor instances to add to the existing list.
594         *
595         * @return \Twig\NodeVisitor\NodeVisitorInterface[]
596         */
597        public function getNodeVisitors();
598
599        /**
600         * Returns a list of filters to add to the existing list.
601         *
602         * @return \Twig\TwigFilter[]
603         */
604        public function getFilters();
605
606        /**
607         * Returns a list of tests to add to the existing list.
608         *
609         * @return \Twig\TwigTest[]
610         */
611        public function getTests();
612
613        /**
614         * Returns a list of functions to add to the existing list.
615         *
616         * @return \Twig\TwigFunction[]
617         */
618        public function getFunctions();
619
620        /**
621         * Returns a list of operators to add to the existing list.
622         *
623         * @return array<array> First array of unary operators, second array of binary operators
624         */
625        public function getOperators();
626    }
627
628To keep your extension class clean and lean, inherit from the built-in
629``\Twig\Extension\AbstractExtension`` class instead of implementing the interface as it provides
630empty implementations for all methods::
631
632    class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
633    {
634    }
635
636This extension does nothing for now. We will customize it in the next sections.
637
638You can save your extension anywhere on the filesystem, as all extensions must
639be registered explicitly to be available in your templates.
640
641You can register an extension by using the ``addExtension()`` method on your
642main ``Environment`` object::
643
644    $twig = new \Twig\Environment($loader);
645    $twig->addExtension(new Project_Twig_Extension());
646
647.. tip::
648
649    The Twig core extensions are great examples of how extensions work.
650
651Globals
652~~~~~~~
653
654Global variables can be registered in an extension via the ``getGlobals()``
655method::
656
657    class Project_Twig_Extension extends \Twig\Extension\AbstractExtension implements \Twig\Extension\GlobalsInterface
658    {
659        public function getGlobals()
660        {
661            return [
662                'text' => new Text(),
663            ];
664        }
665
666        // ...
667    }
668
669Functions
670~~~~~~~~~
671
672Functions can be registered in an extension via the ``getFunctions()``
673method::
674
675    class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
676    {
677        public function getFunctions()
678        {
679            return [
680                new \Twig\TwigFunction('lipsum', 'generate_lipsum'),
681            ];
682        }
683
684        // ...
685    }
686
687Filters
688~~~~~~~
689
690To add a filter to an extension, you need to override the ``getFilters()``
691method. This method must return an array of filters to add to the Twig
692environment::
693
694    class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
695    {
696        public function getFilters()
697        {
698            return [
699                new \Twig\TwigFilter('rot13', 'str_rot13'),
700            ];
701        }
702
703        // ...
704    }
705
706Tags
707~~~~
708
709Adding a tag in an extension can be done by overriding the
710``getTokenParsers()`` method. This method must return an array of tags to add
711to the Twig environment::
712
713    class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
714    {
715        public function getTokenParsers()
716        {
717            return [new Project_Set_TokenParser()];
718        }
719
720        // ...
721    }
722
723In the above code, we have added a single new tag, defined by the
724``Project_Set_TokenParser`` class. The ``Project_Set_TokenParser`` class is
725responsible for parsing the tag and compiling it to PHP.
726
727Operators
728~~~~~~~~~
729
730The ``getOperators()`` methods lets you add new operators. Here is how to add
731the ``!``, ``||``, and ``&&`` operators::
732
733    class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
734    {
735        public function getOperators()
736        {
737            return [
738                [
739                    '!' => ['precedence' => 50, 'class' => \Twig\Node\Expression\Unary\NotUnary::class],
740                ],
741                [
742                    '||' => ['precedence' => 10, 'class' => \Twig\Node\Expression\Binary\OrBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT],
743                    '&&' => ['precedence' => 15, 'class' => \Twig\Node\Expression\Binary\AndBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT],
744                ],
745            ];
746        }
747
748        // ...
749    }
750
751Tests
752~~~~~
753
754The ``getTests()`` method lets you add new test functions::
755
756    class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
757    {
758        public function getTests()
759        {
760            return [
761                new \Twig\TwigTest('even', 'twig_test_even'),
762            ];
763        }
764
765        // ...
766    }
767
768Definition vs Runtime
769~~~~~~~~~~~~~~~~~~~~~
770
771Twig filters, functions, and tests runtime implementations can be defined as
772any valid PHP callable:
773
774* **functions/static methods**: Simple to implement and fast (used by all Twig
775  core extensions); but it is hard for the runtime to depend on external
776  objects;
777
778* **closures**: Simple to implement;
779
780* **object methods**: More flexible and required if your runtime code depends
781  on external objects.
782
783The simplest way to use methods is to define them on the extension itself::
784
785    class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
786    {
787        private $rot13Provider;
788
789        public function __construct($rot13Provider)
790        {
791            $this->rot13Provider = $rot13Provider;
792        }
793
794        public function getFunctions()
795        {
796            return [
797                new \Twig\TwigFunction('rot13', [$this, 'rot13']),
798            ];
799        }
800
801        public function rot13($value)
802        {
803            return $this->rot13Provider->rot13($value);
804        }
805    }
806
807This is very convenient but not recommended as it makes template compilation
808depend on runtime dependencies even if they are not needed (think for instance
809as a dependency that connects to a database engine).
810
811You can decouple the extension definitions from their runtime implementations by
812registering a ``\Twig\RuntimeLoader\RuntimeLoaderInterface`` instance on the
813environment that knows how to instantiate such runtime classes (runtime classes
814must be autoload-able)::
815
816    class RuntimeLoader implements \Twig\RuntimeLoader\RuntimeLoaderInterface
817    {
818        public function load($class)
819        {
820            // implement the logic to create an instance of $class
821            // and inject its dependencies
822            // most of the time, it means using your dependency injection container
823            if ('Project_Twig_RuntimeExtension' === $class) {
824                return new $class(new Rot13Provider());
825            } else {
826                // ...
827            }
828        }
829    }
830
831    $twig->addRuntimeLoader(new RuntimeLoader());
832
833.. note::
834
835    Twig comes with a PSR-11 compatible runtime loader
836    (``\Twig\RuntimeLoader\ContainerRuntimeLoader``).
837
838It is now possible to move the runtime logic to a new
839``Project_Twig_RuntimeExtension`` class and use it directly in the extension::
840
841    class Project_Twig_RuntimeExtension
842    {
843        private $rot13Provider;
844
845        public function __construct($rot13Provider)
846        {
847            $this->rot13Provider = $rot13Provider;
848        }
849
850        public function rot13($value)
851        {
852            return $this->rot13Provider->rot13($value);
853        }
854    }
855
856    class Project_Twig_Extension extends \Twig\Extension\AbstractExtension
857    {
858        public function getFunctions()
859        {
860            return [
861                new \Twig\TwigFunction('rot13', ['Project_Twig_RuntimeExtension', 'rot13']),
862                // or
863                new \Twig\TwigFunction('rot13', 'Project_Twig_RuntimeExtension::rot13'),
864            ];
865        }
866    }
867
868Testing an Extension
869--------------------
870
871Functional Tests
872~~~~~~~~~~~~~~~~
873
874You can create functional tests for extensions by creating the following file
875structure in your test directory::
876
877    Fixtures/
878        filters/
879            foo.test
880            bar.test
881        functions/
882            foo.test
883            bar.test
884        tags/
885            foo.test
886            bar.test
887    IntegrationTest.php
888
889The ``IntegrationTest.php`` file should look like this::
890
891    class Project_Tests_IntegrationTest extends \Twig\Test\IntegrationTestCase
892    {
893        public function getExtensions()
894        {
895            return [
896                new Project_Twig_Extension1(),
897                new Project_Twig_Extension2(),
898            ];
899        }
900
901        public function getFixturesDir()
902        {
903            return __DIR__.'/Fixtures/';
904        }
905    }
906
907Fixtures examples can be found within the Twig repository
908`tests/Twig/Fixtures`_ directory.
909
910Node Tests
911~~~~~~~~~~
912
913Testing the node visitors can be complex, so extend your test cases from
914``\Twig\Test\NodeTestCase``. Examples can be found in the Twig repository
915`tests/Twig/Node`_ directory.
916
917.. _`tests/Twig/Fixtures`: https://github.com/twigphp/Twig/tree/2.x/tests/Fixtures
918.. _`tests/Twig/Node`:     https://github.com/twigphp/Twig/tree/2.x/tests/Node
919