* 1.12.0 (2012-XX-XX)
+ * added the ability to use any PHP callable to define filters, functions, and tests
* added the ability to set default values for macro arguments
* added support for named arguments for filters, tests, and functions
* moved filters/functions/tests syntax errors to the parser
Extending Twig
==============
+.. caution::
+
+ This section describes how to extend Twig as of **Twig 1.12**. If you are
+ using an older version, read the :doc:`legacy<advanced_legacy>` chapter
+ instead.
+
Twig can be extended in many ways; you can add extra tags, filters, tests,
operators, global variables, and functions. You can even extend the parser
itself with node visitors.
Filters
-------
-A filter is a regular PHP function or an object method that takes the left
-side of the filter (before the pipe ``|``) as first argument and the extra
-arguments passed to the filter (within parentheses ``()``) as extra arguments.
-
-Defining a filter is as easy as associating the filter name with a PHP
-callable. For instance, let's say you have the following code in a template:
-
-.. code-block:: jinja
-
- {{ 'TWIG'|lower }}
-
-When compiling this template to PHP, Twig looks for the PHP callable
-associated with the ``lower`` filter. The ``lower`` filter is a built-in Twig
-filter, and it is simply mapped to the PHP ``strtolower()`` function. After
-compilation, the generated PHP code is roughly equivalent to:
-
-.. code-block:: html+php
-
- <?php echo strtolower('TWIG') ?>
-
-As you can see, the ``'TWIG'`` string is passed as a first argument to the PHP
-function.
+Creating a filter is as simple as associating a name with a PHP callable::
-A filter can also take extra arguments like in the following example:
-
-.. code-block:: jinja
-
- {{ now|date('d/m/Y') }}
-
-In this case, the extra arguments are passed to the function after the main
-argument, and the compiled code is equivalent to:
-
-.. code-block:: html+php
-
- <?php echo twig_date_format_filter($now, 'd/m/Y') ?>
-
-Let's see how to create a new filter.
+ // an anonymous function
+ $filter = new Twig_SimpleFilter('rot13', function ($string) {
+ return str_rot13($string);
+ });
-In this section, we will create a ``rot13`` filter, which should return the
-`rot13`_ transformation of a string. Here is an example of its usage and the
-expected output:
+ // or a simple PHP function
+ $filter = new Twig_SimpleFilter('rot13', 'str_rot13');
-.. code-block:: jinja
+ // or a class method
+ $filter = new Twig_SimpleFilter('rot13', array('SomeClass', 'rot13Filter'));
- {{ "Twig"|rot13 }}
+The first argument passed to the ``Twig_SimpleFilter`` constructor is the name
+of the filter you will use in templates and the second one is the PHP callable
+to associate with it.
- {# should displays Gjvt #}
-
-Adding a filter is as simple as calling the ``addFilter()`` method on the
-``Twig_Environment`` instance::
+Then, add the filter to your Twig environment::
$twig = new Twig_Environment($loader);
- $twig->addFilter('rot13', new Twig_Filter_Function('str_rot13'));
-
-The second argument of ``addFilter()`` is an instance of ``Twig_Filter``.
-Here, we use ``Twig_Filter_Function`` as the filter is a PHP function. The
-first argument passed to the ``Twig_Filter_Function`` constructor is the name
-of the PHP function to call, here ``str_rot13``, a native PHP function.
+ $twig->addFilter($filter);
-Let's say I now want to be able to add a prefix before the converted string:
+And here is how to use it in a template:
.. code-block:: jinja
- {{ "Twig"|rot13('prefix_') }}
-
- {# should displays prefix_Gjvt #}
-
-As the PHP ``str_rot13()`` function does not support this requirement, let's
-create a new PHP function::
-
- function project_compute_rot13($string, $prefix = '')
- {
- return $prefix.str_rot13($string);
- }
+ {{ 'Twig'|rot13 }}
-As you can see, the ``prefix`` argument of the filter is passed as an extra
-argument to the ``project_compute_rot13()`` function.
+ {# will output giwT #}
-Adding this filter is as easy as before::
+When called by Twig, the PHP callable receives the left side of the filter
+(before the pipe ``|``) as the first argument and the extra arguments passed
+to the filter (within parentheses ``()``) as extra arguments.
- $twig->addFilter('rot13', new Twig_Filter_Function('project_compute_rot13'));
+For instance, the following code:
-For better encapsulation, a filter can also be defined as a static method of a
-class. The ``Twig_Filter_Function`` class can also be used to register such
-static methods as filters::
+.. code-block:: jinja
- $twig->addFilter('rot13', new Twig_Filter_Function('SomeClass::rot13Filter'));
+ {{ 'TWIG'|lower }}
+ {{ now|date('d/m/Y') }}
-.. tip::
+is compiled to something like the following::
- In an extension, you can also define a filter as a static method of the
- extension class.
+ <?php echo strtolower('TWIG') ?>
+ <?php echo twig_date_format_filter($now, 'd/m/Y') ?>
-The filter class constructors take an array of options as their last
+The ``Twig_SimpleFilter`` class takes an array of options as its last
argument::
- $filter = new Twig_Filter_Function('str_rot13', $options);
+ $filter = new Twig_SimpleFilter('rot13', 'str_rot13', $options);
Environment aware Filters
~~~~~~~~~~~~~~~~~~~~~~~~~
-If you want access to the current environment instance in your filter, set the
-``needs_environment`` option to ``true``::
-
- $filter = new Twig_Filter_Function('str_rot13', array('needs_environment' => true));
-
-Twig will then pass the current environment as the first argument to the
-filter call::
+If you want to access the current environment instance in your filter, set the
+``needs_environment`` option to ``true``; Twig will pass the current
+environment as the first argument to the filter call::
- function twig_compute_rot13(Twig_Environment $env, $string)
- {
+ $filter = new Twig_SimpleFilter('rot13', function (Twig_Environment $env, $string) {
// get the current charset for instance
$charset = $env->getCharset();
return str_rot13($string);
- }
+ }, array('needs_environment' => true));
Context aware Filters
~~~~~~~~~~~~~~~~~~~~~
the first argument to the filter call (or the second one if
``needs_environment`` is also set to ``true``)::
- $filter = new Twig_Filter_Function('str_rot13', array('needs_context' => true));
+ $filter = new Twig_SimpleFilter('rot13', function ($context, $string) {
+ // ...
+ }, array('needs_context' => true));
+
+ $filter = new Twig_SimpleFilter('rot13', function (Twig_Environment $env, $context, $string) {
+ // ...
+ }, array('needs_context' => true, 'needs_environment' => true));
Automatic Escaping
~~~~~~~~~~~~~~~~~~
If automatic escaping is enabled, the output of the filter may be escaped
before printing. If your filter acts as an escaper (or explicitly outputs html
-or javascript code), you will want the raw output to be printed. In such a
+or JavaScript code), you will want the raw output to be printed. In such a
case, set the ``is_safe`` option::
- $filter = new Twig_Filter_Function('nl2br', array('is_safe' => array('html')));
+ $filter = new Twig_SimpleFilter('nl2br', 'nl2br', array('is_safe' => array('html')));
Some filters may need to work on input that is already escaped or safe, for
example when adding (safe) html tags to originally unsafe output. In such a
case, set the ``pre_escape`` option to escape the input data before it is run
through your filter::
- $filter = new Twig_Filter_Function('somefilter', array('pre_escape' => 'html', 'is_safe' => array('html')));
+ $filter = new Twig_SimpleFilter('somefilter', 'somefilter', array('pre_escape' => 'html', 'is_safe' => array('html')));
Dynamic Filters
~~~~~~~~~~~~~~~
-.. versionadded:: 1.5
- Dynamic filters support was added in Twig 1.5.
-
A filter name containing the special ``*`` character is a dynamic filter as
the ``*`` can be any string::
- $twig->addFilter('*_path', new Twig_Filter_Function('twig_path'));
-
- function twig_path($name, $arguments)
- {
+ $filter = new Twig_SimpleFilter('*_path', function ($name, $arguments) {
// ...
- }
+ });
The following filters will be matched by the above defined dynamic filter:
A dynamic filter can define more than one dynamic parts::
- $twig->addFilter('*_path_*', new Twig_Filter_Function('twig_path'));
-
- function twig_path($name, $suffix, $arguments)
- {
+ $filter = new Twig_SimpleFilter('*_path', function ($name, $suffix, $arguments) {
// ...
- }
+ });
The filter will receive all dynamic part values before the normal filters
-arguments. For instance, a call to ``'foo'|a_path_b()`` will result in the
-following PHP call: ``twig_path('a', 'b', 'foo')``.
+arguments, but after the environment and the context. For instance, a call to
+``'foo'|a_path_b()`` will result in the following arguments to be passed to
+the filter: ``('a', 'b', 'foo')``.
Functions
---------
-A function is a regular PHP function or an object method that can be called from
-templates.
-
-.. code-block:: jinja
-
- {{ constant("DATE_W3C") }}
-
-When compiling this template to PHP, Twig looks for the PHP callable
-associated with the ``constant`` function. The ``constant`` function is a built-in Twig
-function, and it is simply mapped to the PHP ``constant()`` function. After
-compilation, the generated PHP code is roughly equivalent to:
-
-.. code-block:: html+php
-
- <?php echo constant('DATE_W3C') ?>
-
-Adding a function is similar to adding a filter. This can be done by calling the
-``addFunction()`` method on the ``Twig_Environment`` instance::
-
- $twig = new Twig_Environment($loader);
- $twig->addFunction('functionName', new Twig_Function_Function('someFunction'));
-
-You can also expose extension methods as functions in your templates::
+Functions are defined in the exact same way as filters, but you need to create
+an instance of ``Twig_SimpleFunction``::
- // $this is an object that implements Twig_ExtensionInterface.
$twig = new Twig_Environment($loader);
- $twig->addFunction('otherFunction', new Twig_Function_Method($this, 'someMethod'));
-
-Functions also support ``needs_environment`` and ``is_safe`` parameters.
-
-Dynamic Functions
-~~~~~~~~~~~~~~~~~
-
-.. versionadded:: 1.5
- Dynamic functions support was added in Twig 1.5.
-
-A function name containing the special ``*`` character is a dynamic function
-as the ``*`` can be any string::
-
- $twig->addFunction('*_path', new Twig_Function_Function('twig_path'));
-
- function twig_path($name, $arguments)
- {
+ $function = new Twig_SimpleFunction('function_name', function () {
// ...
- }
-
-The following functions will be matched by the above defined dynamic function:
+ });
+ $twig->addFunction($function);
-* ``product_path``
-* ``category_path``
+Functions support the same features as filters, except for the ``pre_escape``
+and ``preserves_safety`` options.
-A dynamic function can define more than one dynamic parts::
+Tests
+-----
- $twig->addFilter('*_path_*', new Twig_Filter_Function('twig_path'));
+Tests are defined in the exact same way as filters and functions, but you need
+to create an instance of ``Twig_SimpleTest``::
- function twig_path($name, $suffix, $arguments)
- {
+ $twig = new Twig_Environment($loader);
+ $test = new Twig_SimpleTest('test_name', function () {
// ...
- }
+ });
+ $twig->addTest($test);
-The function will receive all dynamic part values before the normal functions
-arguments. For instance, a call to ``a_path_b('foo')`` will result in the
-following PHP call: ``twig_path('a', 'b', 'foo')``.
+Tests do not support any options.
Tags
----
.. tip::
When packaging your code into an extension, Twig is smart enough to
- recompile your templates whenever you make a change to it (when the
+ recompile your templates whenever you make a change to it (when
``auto_reload`` is enabled).
.. note::
public function getFunctions()
{
return array(
- 'lipsum' => new Twig_Function_Function('generate_lipsum'),
+ new Twig_SimpleFunction('lipsum', 'generate_lipsum'),
);
}
public function getFilters()
{
return array(
- 'rot13' => new Twig_Filter_Function('str_rot13'),
- );
- }
-
- // ...
- }
-
-As you can see in the above code, the ``getFilters()`` method returns an array
-where keys are the name of the filters (``rot13``) and the values the
-definition of the filter (``new Twig_Filter_Function('str_rot13')``).
-
-As seen in the previous chapter, you can also define filters as static methods
-on the extension class::
-
-$twig->addFilter('rot13', new Twig_Filter_Function('Project_Twig_Extension::rot13Filter'));
-
-You can also use ``Twig_Filter_Method`` instead of ``Twig_Filter_Function``
-when defining a filter to use a method::
-
- class Project_Twig_Extension extends Twig_Extension
- {
- public function getFilters()
- {
- return array(
- 'rot13' => new Twig_Filter_Method($this, 'rot13Filter'),
+ new Twig_SimpleFilter('rot13', 'str_rot13'),
);
}
- public function rot13Filter($string)
- {
- return str_rot13($string);
- }
-
// ...
}
-The first argument of the ``Twig_Filter_Method`` constructor is always
-``$this``, the current extension object. The second one is the name of the
-method to call.
-
-Using methods for filters is a great way to package your filter without
-polluting the global namespace. This also gives the developer more flexibility
-at the cost of a small overhead.
-
Tags
~~~~
public function getTests()
{
return array(
- 'even' => new Twig_Test_Function('twig_test_even'),
+ new Twig_SimpleTest('even', 'twig_test_even'),
);
}
function, define it again **as late as possible**::
$twig = new Twig_Environment($loader);
- $twig->addFilter('date', new Twig_Filter_Function('my_date'));
+ $twig->addFilter(new Twig_SimpleFilter('date', function ($timestamp, $format = 'F j, Y H:i') {
+ // do something different from the built-in date filter
+ }));
Here, we have overloaded the built-in ``date`` filter with a custom one.
public function getFilters()
{
return array(
- 'date' => new Twig_Filter_Method($this, 'dateFilter'),
+ new Twig_SimpleFilter('date', array($this, 'dateFilter')),
);
}
Testing an Extension
--------------------
-.. versionadded:: 1.10
- Support for functional tests was added in Twig 1.10.
-
Functional Tests
~~~~~~~~~~~~~~~~
--- /dev/null
+Extending Twig
+==============
+
+.. caution::
+
+ This section describes how to extends Twig for versions **older than
+ 1.12**. If you are using a newer version, read the :doc:`newer<advanced>`
+ chapter instead.
+
+Twig can be extended in many ways; you can add extra tags, filters, tests,
+operators, global variables, and functions. You can even extend the parser
+itself with node visitors.
+
+.. note::
+
+ The first section of this chapter describes how to extend Twig easily. If
+ you want to reuse your changes in different projects or if you want to
+ share them with others, you should then create an extension as described
+ in the following section.
+
+.. caution::
+
+ When extending Twig by calling methods on the Twig environment instance,
+ Twig won't be able to recompile your templates when the PHP code is
+ updated. To see your changes in real-time, either disable template caching
+ or package your code into an extension (see the next section of this
+ chapter).
+
+Before extending Twig, you must understand the differences between all the
+different possible extension points and when to use them.
+
+First, remember that Twig has two main language constructs:
+
+* ``{{ }}``: used to print the result of an expression evaluation;
+
+* ``{% %}``: used to execute statements.
+
+To understand why Twig exposes so many extension points, let's see how to
+implement a *Lorem ipsum* generator (it needs to know the number of words to
+generate).
+
+You can use a ``lipsum`` *tag*:
+
+.. code-block:: jinja
+
+ {% lipsum 40 %}
+
+That works, but using a tag for ``lipsum`` is not a good idea for at least
+three main reasons:
+
+* ``lipsum`` is not a language construct;
+* The tag outputs something;
+* The tag is not flexible as you cannot use it in an expression:
+
+ .. code-block:: jinja
+
+ {{ 'some text' ~ {% lipsum 40 %} ~ 'some more text' }}
+
+In fact, you rarely need to create tags; and that's good news because tags are
+the most complex extension point of Twig.
+
+Now, let's use a ``lipsum`` *filter*:
+
+.. code-block:: jinja
+
+ {{ 40|lipsum }}
+
+Again, it works, but it looks weird. A filter transforms the passed value to
+something else but here we use the value to indicate the number of words to
+generate (so, ``40`` is an argument of the filter, not the value we want to
+transform).
+
+Next, let's use a ``lipsum`` *function*:
+
+.. code-block:: jinja
+
+ {{ lipsum(40) }}
+
+Here we go. For this specific example, the creation of a function is the
+extension point to use. And you can use it anywhere an expression is accepted:
+
+.. code-block:: jinja
+
+ {{ 'some text' ~ ipsum(40) ~ 'some more text' }}
+
+ {% set ipsum = ipsum(40) %}
+
+Last but not the least, you can also use a *global* object with a method able
+to generate lorem ipsum text:
+
+.. code-block:: jinja
+
+ {{ text.lipsum(40) }}
+
+As a rule of thumb, use functions for frequently used features and global
+objects for everything else.
+
+Keep in mind the following when you want to extend Twig:
+
+========== ========================== ========== =========================
+What? Implementation difficulty? How often? When?
+========== ========================== ========== =========================
+*macro* trivial frequent Content generation
+*global* trivial frequent Helper object
+*function* trivial frequent Content generation
+*filter* trivial frequent Value transformation
+*tag* complex rare DSL language construct
+*test* trivial rare Boolean decision
+*operator* trivial rare Values transformation
+========== ========================== ========== =========================
+
+Globals
+-------
+
+A global variable is like any other template variable, except that it's
+available in all templates and macros::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addGlobal('text', new Text());
+
+You can then use the ``text`` variable anywhere in a template:
+
+.. code-block:: jinja
+
+ {{ text.lipsum(40) }}
+
+Filters
+-------
+
+A filter is a regular PHP function or an object method that takes the left
+side of the filter (before the pipe ``|``) as first argument and the extra
+arguments passed to the filter (within parentheses ``()``) as extra arguments.
+
+Defining a filter is as easy as associating the filter name with a PHP
+callable. For instance, let's say you have the following code in a template:
+
+.. code-block:: jinja
+
+ {{ 'TWIG'|lower }}
+
+When compiling this template to PHP, Twig looks for the PHP callable
+associated with the ``lower`` filter. The ``lower`` filter is a built-in Twig
+filter, and it is simply mapped to the PHP ``strtolower()`` function. After
+compilation, the generated PHP code is roughly equivalent to:
+
+.. code-block:: html+php
+
+ <?php echo strtolower('TWIG') ?>
+
+As you can see, the ``'TWIG'`` string is passed as a first argument to the PHP
+function.
+
+A filter can also take extra arguments like in the following example:
+
+.. code-block:: jinja
+
+ {{ now|date('d/m/Y') }}
+
+In this case, the extra arguments are passed to the function after the main
+argument, and the compiled code is equivalent to:
+
+.. code-block:: html+php
+
+ <?php echo twig_date_format_filter($now, 'd/m/Y') ?>
+
+Let's see how to create a new filter.
+
+In this section, we will create a ``rot13`` filter, which should return the
+`rot13`_ transformation of a string. Here is an example of its usage and the
+expected output:
+
+.. code-block:: jinja
+
+ {{ "Twig"|rot13 }}
+
+ {# should displays Gjvt #}
+
+Adding a filter is as simple as calling the ``addFilter()`` method on the
+``Twig_Environment`` instance::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addFilter('rot13', new Twig_Filter_Function('str_rot13'));
+
+The second argument of ``addFilter()`` is an instance of ``Twig_Filter``.
+Here, we use ``Twig_Filter_Function`` as the filter is a PHP function. The
+first argument passed to the ``Twig_Filter_Function`` constructor is the name
+of the PHP function to call, here ``str_rot13``, a native PHP function.
+
+Let's say I now want to be able to add a prefix before the converted string:
+
+.. code-block:: jinja
+
+ {{ "Twig"|rot13('prefix_') }}
+
+ {# should displays prefix_Gjvt #}
+
+As the PHP ``str_rot13()`` function does not support this requirement, let's
+create a new PHP function::
+
+ function project_compute_rot13($string, $prefix = '')
+ {
+ return $prefix.str_rot13($string);
+ }
+
+As you can see, the ``prefix`` argument of the filter is passed as an extra
+argument to the ``project_compute_rot13()`` function.
+
+Adding this filter is as easy as before::
+
+ $twig->addFilter('rot13', new Twig_Filter_Function('project_compute_rot13'));
+
+For better encapsulation, a filter can also be defined as a static method of a
+class. The ``Twig_Filter_Function`` class can also be used to register such
+static methods as filters::
+
+ $twig->addFilter('rot13', new Twig_Filter_Function('SomeClass::rot13Filter'));
+
+.. tip::
+
+ In an extension, you can also define a filter as a static method of the
+ extension class.
+
+Environment aware Filters
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``Twig_Filter`` classes take options as their last argument. For instance,
+if you want access to the current environment instance in your filter, set the
+``needs_environment`` option to ``true``::
+
+ $filter = new Twig_Filter_Function('str_rot13', array('needs_environment' => true));
+
+Twig will then pass the current environment as the first argument to the
+filter call::
+
+ function twig_compute_rot13(Twig_Environment $env, $string)
+ {
+ // get the current charset for instance
+ $charset = $env->getCharset();
+
+ return str_rot13($string);
+ }
+
+Automatic Escaping
+~~~~~~~~~~~~~~~~~~
+
+If automatic escaping is enabled, the output of the filter may be escaped
+before printing. If your filter acts as an escaper (or explicitly outputs html
+or javascript code), you will want the raw output to be printed. In such a
+case, set the ``is_safe`` option::
+
+ $filter = new Twig_Filter_Function('nl2br', array('is_safe' => array('html')));
+
+Some filters may need to work on input that is already escaped or safe, for
+example when adding (safe) html tags to originally unsafe output. In such a
+case, set the ``pre_escape`` option to escape the input data before it is run
+through your filter::
+
+ $filter = new Twig_Filter_Function('somefilter', array('pre_escape' => 'html', 'is_safe' => array('html')));
+
+Dynamic Filters
+~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.5
+ Dynamic filters support was added in Twig 1.5.
+
+A filter name containing the special ``*`` character is a dynamic filter as
+the ``*`` can be any string::
+
+ $twig->addFilter('*_path', new Twig_Filter_Function('twig_path'));
+
+ function twig_path($name, $arguments)
+ {
+ // ...
+ }
+
+The following filters will be matched by the above defined dynamic filter:
+
+* ``product_path``
+* ``category_path``
+
+A dynamic filter can define more than one dynamic parts::
+
+ $twig->addFilter('*_path_*', new Twig_Filter_Function('twig_path'));
+
+ function twig_path($name, $suffix, $arguments)
+ {
+ // ...
+ }
+
+The filter will receive all dynamic part values before the normal filters
+arguments. For instance, a call to ``'foo'|a_path_b()`` will result in the
+following PHP call: ``twig_path('a', 'b', 'foo')``.
+
+Functions
+---------
+
+A function is a regular PHP function or an object method that can be called from
+templates.
+
+.. code-block:: jinja
+
+ {{ constant("DATE_W3C") }}
+
+When compiling this template to PHP, Twig looks for the PHP callable
+associated with the ``constant`` function. The ``constant`` function is a built-in Twig
+function, and it is simply mapped to the PHP ``constant()`` function. After
+compilation, the generated PHP code is roughly equivalent to:
+
+.. code-block:: html+php
+
+ <?php echo constant('DATE_W3C') ?>
+
+Adding a function is similar to adding a filter. This can be done by calling the
+``addFunction()`` method on the ``Twig_Environment`` instance::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addFunction('functionName', new Twig_Function_Function('someFunction'));
+
+You can also expose extension methods as functions in your templates::
+
+ // $this is an object that implements Twig_ExtensionInterface.
+ $twig = new Twig_Environment($loader);
+ $twig->addFunction('otherFunction', new Twig_Function_Method($this, 'someMethod'));
+
+Functions also support ``needs_environment`` and ``is_safe`` parameters.
+
+Dynamic Functions
+~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.5
+ Dynamic functions support was added in Twig 1.5.
+
+A function name containing the special ``*`` character is a dynamic function
+as the ``*`` can be any string::
+
+ $twig->addFunction('*_path', new Twig_Function_Function('twig_path'));
+
+ function twig_path($name, $arguments)
+ {
+ // ...
+ }
+
+The following functions will be matched by the above defined dynamic function:
+
+* ``product_path``
+* ``category_path``
+
+A dynamic function can define more than one dynamic parts::
+
+ $twig->addFilter('*_path_*', new Twig_Filter_Function('twig_path'));
+
+ function twig_path($name, $suffix, $arguments)
+ {
+ // ...
+ }
+
+The function will receive all dynamic part values before the normal functions
+arguments. For instance, a call to ``a_path_b('foo')`` will result in the
+following PHP call: ``twig_path('a', 'b', 'foo')``.
+
+Tags
+----
+
+One of the most exciting feature of a template engine like Twig is the
+possibility to define new language constructs. This is also the most complex
+feature as you need to understand how Twig's internals work.
+
+Let's create a simple ``set`` tag that allows the definition of simple
+variables from within a template. The tag can be used like follows:
+
+.. code-block:: jinja
+
+ {% set name = "value" %}
+
+ {{ name }}
+
+ {# should output value #}
+
+.. note::
+
+ The ``set`` tag is part of the Core extension and as such is always
+ available. The built-in version is slightly more powerful and supports
+ multiple assignments by default (cf. the template designers chapter for
+ more information).
+
+Three steps are needed to define a new tag:
+
+* Defining a Token Parser class (responsible for parsing the template code);
+
+* Defining a Node class (responsible for converting the parsed code to PHP);
+
+* Registering the tag.
+
+Registering a new tag
+~~~~~~~~~~~~~~~~~~~~~
+
+Adding a tag is as simple as calling the ``addTokenParser`` method on the
+``Twig_Environment`` instance::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addTokenParser(new Project_Set_TokenParser());
+
+Defining a Token Parser
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Now, let's see the actual code of this class::
+
+ class Project_Set_TokenParser extends Twig_TokenParser
+ {
+ public function parse(Twig_Token $token)
+ {
+ $lineno = $token->getLine();
+ $name = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE)->getValue();
+ $this->parser->getStream()->expect(Twig_Token::OPERATOR_TYPE, '=');
+ $value = $this->parser->getExpressionParser()->parseExpression();
+
+ $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE);
+
+ return new Project_Set_Node($name, $value, $lineno, $this->getTag());
+ }
+
+ public function getTag()
+ {
+ return 'set';
+ }
+ }
+
+The ``getTag()`` method must return the tag we want to parse, here ``set``.
+
+The ``parse()`` method is invoked whenever the parser encounters a ``set``
+tag. It should return a ``Twig_Node`` instance that represents the node (the
+``Project_Set_Node`` calls creating is explained in the next section).
+
+The parsing process is simplified thanks to a bunch of methods you can call
+from the token stream (``$this->parser->getStream()``):
+
+* ``getCurrent()``: Gets the current token in the stream.
+
+* ``next()``: Moves to the next token in the stream, *but returns the old one*.
+
+* ``test($type)``, ``test($value)`` or ``test($type, $value)``: Determines whether
+ the current token is of a particular type or value (or both). The value may be an
+ array of several possible values.
+
+* ``expect($type[, $value[, $message]])``: If the current token isn't of the given
+ type/value a syntax error is thrown. Otherwise, if the type and value are correct,
+ the token is returned and the stream moves to the next token.
+
+* ``look()``: Looks a the next token without consuming it.
+
+Parsing expressions is done by calling the ``parseExpression()`` like we did for
+the ``set`` tag.
+
+.. tip::
+
+ Reading the existing ``TokenParser`` classes is the best way to learn all
+ the nitty-gritty details of the parsing process.
+
+Defining a Node
+~~~~~~~~~~~~~~~
+
+The ``Project_Set_Node`` class itself is rather simple::
+
+ class Project_Set_Node extends Twig_Node
+ {
+ public function __construct($name, Twig_Node_Expression $value, $lineno, $tag = null)
+ {
+ parent::__construct(array('value' => $value), array('name' => $name), $lineno, $tag);
+ }
+
+ public function compile(Twig_Compiler $compiler)
+ {
+ $compiler
+ ->addDebugInfo($this)
+ ->write('$context[\''.$this->getAttribute('name').'\'] = ')
+ ->subcompile($this->getNode('value'))
+ ->raw(";\n")
+ ;
+ }
+ }
+
+The compiler implements a fluid interface and provides methods that helps the
+developer generate beautiful and readable PHP code:
+
+* ``subcompile()``: Compiles a node.
+
+* ``raw()``: Writes the given string as is.
+
+* ``write()``: Writes the given string by adding indentation at the beginning
+ of each line.
+
+* ``string()``: Writes a quoted string.
+
+* ``repr()``: Writes a PHP representation of a given value (see
+ ``Twig_Node_For`` for a usage example).
+
+* ``addDebugInfo()``: Adds the line of the original template file related to
+ the current node as a comment.
+
+* ``indent()``: Indents the generated code (see ``Twig_Node_Block`` for a
+ usage example).
+
+* ``outdent()``: Outdents the generated code (see ``Twig_Node_Block`` for a
+ usage example).
+
+.. _creating_extensions:
+
+Creating an Extension
+---------------------
+
+The main motivation for writing an extension is to move often used code into a
+reusable class like adding support for internationalization. An extension can
+define tags, filters, tests, operators, global variables, functions, and node
+visitors.
+
+Creating an extension also makes for a better separation of code that is
+executed at compilation time and code needed at runtime. As such, it makes
+your code faster.
+
+Most of the time, it is useful to create a single extension for your project,
+to host all the specific tags and filters you want to add to Twig.
+
+.. tip::
+
+ When packaging your code into an extension, Twig is smart enough to
+ recompile your templates whenever you make a change to it (when the
+ ``auto_reload`` is enabled).
+
+.. note::
+
+ Before writing your own extensions, have a look at the Twig official
+ extension repository: http://github.com/fabpot/Twig-extensions.
+
+An extension is a class that implements the following interface::
+
+ interface Twig_ExtensionInterface
+ {
+ /**
+ * Initializes the runtime environment.
+ *
+ * This is where you can load some file that contains filter functions for instance.
+ *
+ * @param Twig_Environment $environment The current Twig_Environment instance
+ */
+ function initRuntime(Twig_Environment $environment);
+
+ /**
+ * Returns the token parser instances to add to the existing list.
+ *
+ * @return array An array of Twig_TokenParserInterface or Twig_TokenParserBrokerInterface instances
+ */
+ function getTokenParsers();
+
+ /**
+ * Returns the node visitor instances to add to the existing list.
+ *
+ * @return array An array of Twig_NodeVisitorInterface instances
+ */
+ function getNodeVisitors();
+
+ /**
+ * Returns a list of filters to add to the existing list.
+ *
+ * @return array An array of filters
+ */
+ function getFilters();
+
+ /**
+ * Returns a list of tests to add to the existing list.
+ *
+ * @return array An array of tests
+ */
+ function getTests();
+
+ /**
+ * Returns a list of functions to add to the existing list.
+ *
+ * @return array An array of functions
+ */
+ function getFunctions();
+
+ /**
+ * Returns a list of operators to add to the existing list.
+ *
+ * @return array An array of operators
+ */
+ function getOperators();
+
+ /**
+ * Returns a list of global variables to add to the existing list.
+ *
+ * @return array An array of global variables
+ */
+ function getGlobals();
+
+ /**
+ * Returns the name of the extension.
+ *
+ * @return string The extension name
+ */
+ function getName();
+ }
+
+To keep your extension class clean and lean, it can inherit from the built-in
+``Twig_Extension`` class instead of implementing the whole interface. That
+way, you just need to implement the ``getName()`` method as the
+``Twig_Extension`` provides empty implementations for all other methods.
+
+The ``getName()`` method must return a unique identifier for your extension.
+
+Now, with this information in mind, let's create the most basic extension
+possible::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getName()
+ {
+ return 'project';
+ }
+ }
+
+.. note::
+
+ Of course, this extension does nothing for now. We will customize it in
+ the next sections.
+
+Twig does not care where you save your extension on the filesystem, as all
+extensions must be registered explicitly to be available in your templates.
+
+You can register an extension by using the ``addExtension()`` method on your
+main ``Environment`` object::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addExtension(new Project_Twig_Extension());
+
+Of course, you need to first load the extension file by either using
+``require_once()`` or by using an autoloader (see `spl_autoload_register()`_).
+
+.. tip::
+
+ The bundled extensions are great examples of how extensions work.
+
+Globals
+~~~~~~~
+
+Global variables can be registered in an extension via the ``getGlobals()``
+method::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getGlobals()
+ {
+ return array(
+ 'text' => new Text(),
+ );
+ }
+
+ // ...
+ }
+
+Functions
+~~~~~~~~~
+
+Functions can be registered in an extension via the ``getFunctions()``
+method::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getFunctions()
+ {
+ return array(
+ 'lipsum' => new Twig_Function_Function('generate_lipsum'),
+ );
+ }
+
+ // ...
+ }
+
+Filters
+~~~~~~~
+
+To add a filter to an extension, you need to override the ``getFilters()``
+method. This method must return an array of filters to add to the Twig
+environment::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getFilters()
+ {
+ return array(
+ 'rot13' => new Twig_Filter_Function('str_rot13'),
+ );
+ }
+
+ // ...
+ }
+
+As you can see in the above code, the ``getFilters()`` method returns an array
+where keys are the name of the filters (``rot13``) and the values the
+definition of the filter (``new Twig_Filter_Function('str_rot13')``).
+
+As seen in the previous chapter, you can also define filters as static methods
+on the extension class::
+
+$twig->addFilter('rot13', new Twig_Filter_Function('Project_Twig_Extension::rot13Filter'));
+
+You can also use ``Twig_Filter_Method`` instead of ``Twig_Filter_Function``
+when defining a filter to use a method::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getFilters()
+ {
+ return array(
+ 'rot13' => new Twig_Filter_Method($this, 'rot13Filter'),
+ );
+ }
+
+ public function rot13Filter($string)
+ {
+ return str_rot13($string);
+ }
+
+ // ...
+ }
+
+The first argument of the ``Twig_Filter_Method`` constructor is always
+``$this``, the current extension object. The second one is the name of the
+method to call.
+
+Using methods for filters is a great way to package your filter without
+polluting the global namespace. This also gives the developer more flexibility
+at the cost of a small overhead.
+
+Overriding default Filters
+..........................
+
+If some default core filters do not suit your needs, you can easily override
+them by creating your own extension. Just use the same names as the one you
+want to override::
+
+ class MyCoreExtension extends Twig_Extension
+ {
+ public function getFilters()
+ {
+ return array(
+ 'date' => new Twig_Filter_Method($this, 'dateFilter'),
+ // ...
+ );
+ }
+
+ public function dateFilter($timestamp, $format = 'F j, Y H:i')
+ {
+ return '...'.twig_date_format_filter($timestamp, $format);
+ }
+
+ public function getName()
+ {
+ return 'project';
+ }
+ }
+
+Here, we override the ``date`` filter with a custom one. Using this extension
+is as simple as registering the ``MyCoreExtension`` extension by calling the
+``addExtension()`` method on the environment instance::
+
+ $twig = new Twig_Environment($loader);
+ $twig->addExtension(new MyCoreExtension());
+
+Tags
+~~~~
+
+Adding a tag in an extension can be done by overriding the
+``getTokenParsers()`` method. This method must return an array of tags to add
+to the Twig environment::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getTokenParsers()
+ {
+ return array(new Project_Set_TokenParser());
+ }
+
+ // ...
+ }
+
+In the above code, we have added a single new tag, defined by the
+``Project_Set_TokenParser`` class. The ``Project_Set_TokenParser`` class is
+responsible for parsing the tag and compiling it to PHP.
+
+Operators
+~~~~~~~~~
+
+The ``getOperators()`` methods allows to add new operators. Here is how to add
+``!``, ``||``, and ``&&`` operators::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getOperators()
+ {
+ return array(
+ array(
+ '!' => array('precedence' => 50, 'class' => 'Twig_Node_Expression_Unary_Not'),
+ ),
+ array(
+ '||' => array('precedence' => 10, 'class' => 'Twig_Node_Expression_Binary_Or', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
+ '&&' => array('precedence' => 15, 'class' => 'Twig_Node_Expression_Binary_And', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
+ ),
+ );
+ }
+
+ // ...
+ }
+
+Tests
+~~~~~
+
+The ``getTests()`` methods allows to add new test functions::
+
+ class Project_Twig_Extension extends Twig_Extension
+ {
+ public function getTests()
+ {
+ return array(
+ 'even' => new Twig_Test_Function('twig_test_even'),
+ );
+ }
+
+ // ...
+ }
+
+Testing an Extension
+--------------------
+
+.. versionadded:: 1.10
+ Support for functional tests was added in Twig 1.10.
+
+Functional Tests
+~~~~~~~~~~~~~~~~
+
+You can create functional tests for extensions simply by creating the
+following file structure in your test directory::
+
+ Fixtures/
+ filters/
+ foo.test
+ bar.test
+ functions/
+ foo.test
+ bar.test
+ tags/
+ foo.test
+ bar.test
+ IntegrationTest.php
+
+The ``IntegrationTest.php`` file should look like this::
+
+ class Project_Tests_IntegrationTest extends Twig_Test_IntegrationTestCase
+ {
+ public function getExtensions()
+ {
+ return array(
+ new Project_Twig_Extension1(),
+ new Project_Twig_Extension2(),
+ );
+ }
+
+ public function getFixturesDir()
+ {
+ return dirname(__FILE__).'/Fixtures/';
+ }
+ }
+
+Fixtures examples can be found within the Twig repository
+`tests/Twig/Fixtures`_ directory.
+
+Node Tests
+~~~~~~~~~~
+
+Testing the node visitors can be complex, so extend your test cases from
+``Twig_Test_NodeTestCase``. Examples can be found in the Twig repository
+`tests/Twig/Node`_ directory.
+
+.. _`spl_autoload_register()`: http://www.php.net/spl_autoload_register
+.. _`rot13`: http://www.php.net/manual/en/function.str-rot13.php
+.. _`tests/Twig/Fixtures`: https://github.com/fabpot/Twig/tree/master/test/Twig/Tests/Fixtures
+.. _`tests/Twig/Node`: https://github.com/fabpot/Twig/tree/master/test/Twig/Tests/Node
PEAR support will be discontinued in Twig 2.0, and no PEAR packages will be
provided. Use Composer instead.
+Filters
+-------
+
+* As of Twig 1.x, use ``Twig_SimpleFilter`` to add a filter. The following
+ classes and interfaces will be removed in 2.0:
+
+ * ``Twig_FilterInterface``
+ * ``Twig_FilterCallableInterface``
+ * ``Twig_Filter``
+ * ``Twig_Filter_Function``
+ * ``Twig_Filter_Method``
+ * ``Twig_Filter_Node``
+
+* As of Twig 2.x, the ``Twig_SimpleFilter`` class is deprecated and will be
+ removed in Twig 3.x (use ``Twig_Filter`` instead). In Twig 2.x,
+ ``Twig_SimpleFilter`` is just an alias for ``Twig_Filter``.
+
+Functions
+---------
+
+* As of Twig 1.x, use ``Twig_SimpleFunction`` to add a function. The following
+ classes and interfaces will be removed in 2.0:
+
+ * ``Twig_FunctionInterface``
+ * ``Twig_FunctionCallableInterface``
+ * ``Twig_Function``
+ * ``Twig_Function_Function``
+ * ``Twig_Function_Method``
+ * ``Twig_Function_Node``
+
+* As of Twig 2.x, the ``Twig_SimpleFunction`` class is deprecated and will be
+ removed in Twig 3.x (use ``Twig_Function`` instead). In Twig 2.x,
+ ``Twig_SimpleFunction`` is just an alias for ``Twig_Function``.
+
+Tests
+-----
+
+* As of Twig 1.x, use ``Twig_SimpleTest`` to add a test. The following classes
+ and interfaces will be removed in 2.0:
+
+ * ``Twig_TestInterface``
+ * ``Twig_TestCallableInterface``
+ * ``Twig_Test``
+ * ``Twig_Test_Function``
+ * ``Twig_Test_Method``
+ * ``Twig_Test_Node``
+
+* As of Twig 2.x, the ``Twig_SimpleTest`` class is deprecated and will be
+ removed in Twig 3.x (use ``Twig_Test`` instead). In Twig 2.x,
+ ``Twig_SimpleTest`` is just an alias for ``Twig_Test``.
+
Interfaces
----------
/**
* Registers a Filter.
*
- * @param string $name The filter name
- * @param Twig_FilterInterface $filter A Twig_FilterInterface instance
+ * @param string|Twig_SimpleFilter $name The filter name or a Twig_SimpleFilter instance
+ * @param Twig_FilterInterface|Twig_SimpleFilter $filter A Twig_FilterInterface instance or a Twig_SimpleFilter instance
*/
- public function addFilter($name, Twig_FilterInterface $filter)
+ public function addFilter($name, $filter = null)
{
if ($this->extensionInitialized) {
throw new LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $name));
}
+ if (!$name instanceof Twig_SimpleFilter && !($filter instanceof Twig_SimpleFilter || $filter instanceof Twig_FilterInterface)) {
+ throw new LogicException('A filter must be an instance of Twig_FilterInterface or Twig_SimpleFilter');
+ }
+
$this->staging->addFilter($name, $filter);
}
/**
* Registers a Test.
*
- * @param string $name The test name
- * @param Twig_TestInterface $test A Twig_TestInterface instance
+ * @param string|Twig_SimpleTest $name The test name or a Twig_SimpleTest instance
+ * @param Twig_TestInterface|Twig_SimpleTest $test A Twig_TestInterface instance or a Twig_SimpleTest instance
*/
- public function addTest($name, Twig_TestInterface $test)
+ public function addTest($name, $test = null)
{
if ($this->extensionInitialized) {
throw new LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $name));
}
+ if (!$name instanceof Twig_SimpleTest && !($test instanceof Twig_SimpleTest || $test instanceof Twig_TestInterface)) {
+ throw new LogicException('A test must be an instance of Twig_TestInterface or Twig_SimpleTest');
+ }
+
$this->staging->addTest($name, $test);
}
/**
* Registers a Function.
*
- * @param string $name The function name
- * @param Twig_FunctionInterface $function A Twig_FunctionInterface instance
+ * @param string|Twig_SimpleFunction $name The function name or a Twig_SimpleFunction instance
+ * @param Twig_FunctionInterface|Twig_SimpleFunction $function A Twig_FunctionInterface instance or a Twig_SimpleFunction instance
*/
- public function addFunction($name, Twig_FunctionInterface $function)
+ public function addFunction($name, $function = null)
{
if ($this->extensionInitialized) {
throw new LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $name));
}
+ if (!$name instanceof Twig_SimpleFunction && !($function instanceof Twig_SimpleFunction || $function instanceof Twig_FunctionInterface)) {
+ throw new LogicException('A function must be an instance of Twig_FunctionInterface or Twig_SimpleFunction');
+ }
+
$this->staging->addFunction($name, $function);
}
{
// filters
foreach ($extension->getFilters() as $name => $filter) {
+ if ($name instanceof Twig_SimpleFilter) {
+ $filter = $name;
+ $name = $filter->getName();
+ } elseif ($filter instanceof Twig_SimpleFilter) {
+ $name = $filter->getName();
+ }
+
$this->filters[$name] = $filter;
}
// functions
foreach ($extension->getFunctions() as $name => $function) {
+ if ($name instanceof Twig_SimpleFunction) {
+ $function = $name;
+ $name = $function->getName();
+ } elseif ($function instanceof Twig_SimpleFunction) {
+ $name = $function->getName();
+ }
+
$this->functions[$name] = $function;
}
// tests
foreach ($extension->getTests() as $name => $test) {
+ if ($name instanceof Twig_SimpleTest) {
+ $test = $name;
+ $name = $test->getName();
+ } elseif ($test instanceof Twig_SimpleTest) {
+ $name = $test->getName();
+ }
+
$this->tests[$name] = $test;
}
throw new Twig_Error_Syntax($message, $line, $this->parser->getFilename());
}
+ if ($function instanceof Twig_SimpleFunction) {
+ return $function->getNodeClass();
+ }
+
return $function instanceof Twig_Function_Node ? $function->getClass() : 'Twig_Node_Expression_Function';
}
throw new Twig_Error_Syntax($message, $line, $this->parser->getFilename());
}
+ if ($filter instanceof Twig_SimpleFilter) {
+ return $filter->getNodeClass();
+ }
+
return $filter instanceof Twig_Filter_Node ? $filter->getClass() : 'Twig_Node_Expression_Filter';
}
{
$filters = array(
// formatting filters
- 'date' => new Twig_Filter_Function('twig_date_format_filter', array('needs_environment' => true)),
- 'date_modify' => new Twig_Filter_Function('twig_date_modify_filter', array('needs_environment' => true)),
- 'format' => new Twig_Filter_Function('sprintf'),
- 'replace' => new Twig_Filter_Function('strtr'),
- 'number_format' => new Twig_Filter_Function('twig_number_format_filter', array('needs_environment' => true)),
- 'abs' => new Twig_Filter_Function('abs'),
+ new Twig_SimpleFilter('date', 'twig_date_format_filter', array('needs_environment' => true)),
+ new Twig_SimpleFilter('date_modify', 'twig_date_modify_filter', array('needs_environment' => true)),
+ new Twig_SimpleFilter('format', 'sprintf'),
+ new Twig_SimpleFilter('replace', 'strtr'),
+ new Twig_SimpleFilter('number_format', 'twig_number_format_filter', array('needs_environment' => true)),
+ new Twig_SimpleFilter('abs', 'abs'),
// encoding
- 'url_encode' => new Twig_Filter_Function('twig_urlencode_filter'),
- 'json_encode' => new Twig_Filter_Function('twig_jsonencode_filter'),
- 'convert_encoding' => new Twig_Filter_Function('twig_convert_encoding'),
+ new Twig_SimpleFilter('url_encode', 'twig_urlencode_filter'),
+ new Twig_SimpleFilter('json_encode', 'twig_jsonencode_filter'),
+ new Twig_SimpleFilter('convert_encoding', 'twig_convert_encoding'),
// string filters
- 'title' => new Twig_Filter_Function('twig_title_string_filter', array('needs_environment' => true)),
- 'capitalize' => new Twig_Filter_Function('twig_capitalize_string_filter', array('needs_environment' => true)),
- 'upper' => new Twig_Filter_Function('strtoupper'),
- 'lower' => new Twig_Filter_Function('strtolower'),
- 'striptags' => new Twig_Filter_Function('strip_tags'),
- 'trim' => new Twig_Filter_Function('trim'),
- 'nl2br' => new Twig_Filter_Function('nl2br', array('pre_escape' => 'html', 'is_safe' => array('html'))),
+ new Twig_SimpleFilter('title', 'twig_title_string_filter', array('needs_environment' => true)),
+ new Twig_SimpleFilter('capitalize', 'twig_capitalize_string_filter', array('needs_environment' => true)),
+ new Twig_SimpleFilter('upper', 'strtoupper'),
+ new Twig_SimpleFilter('lower', 'strtolower'),
+ new Twig_SimpleFilter('striptags', 'strip_tags'),
+ new Twig_SimpleFilter('trim', 'trim'),
+ new Twig_SimpleFilter('nl2br', 'nl2br', array('pre_escape' => 'html', 'is_safe' => array('html'))),
// array helpers
- 'join' => new Twig_Filter_Function('twig_join_filter'),
- 'split' => new Twig_Filter_Function('twig_split_filter'),
- 'sort' => new Twig_Filter_Function('twig_sort_filter'),
- 'merge' => new Twig_Filter_Function('twig_array_merge'),
+ new Twig_SimpleFilter('join', 'twig_join_filter'),
+ new Twig_SimpleFilter('split', 'twig_split_filter'),
+ new Twig_SimpleFilter('sort', 'twig_sort_filter'),
+ new Twig_SimpleFilter('merge', 'twig_array_merge'),
// string/array filters
- 'reverse' => new Twig_Filter_Function('twig_reverse_filter', array('needs_environment' => true)),
- 'length' => new Twig_Filter_Function('twig_length_filter', array('needs_environment' => true)),
- 'slice' => new Twig_Filter_Function('twig_slice', array('needs_environment' => true)),
+ new Twig_SimpleFilter('reverse', 'twig_reverse_filter', array('needs_environment' => true)),
+ new Twig_SimpleFilter('length', 'twig_length_filter', array('needs_environment' => true)),
+ new Twig_SimpleFilter('slice', 'twig_slice', array('needs_environment' => true)),
// iteration and runtime
- 'default' => new Twig_Filter_Node('Twig_Node_Expression_Filter_Default'),
- '_default' => new Twig_Filter_Function('_twig_default_filter'),
-
- 'keys' => new Twig_Filter_Function('twig_get_array_keys_filter'),
+ new Twig_SimpleFilter('default', '_twig_default_filter', array('node_class' => 'Twig_Node_Expression_Filter_Default')),
+ new Twig_SimpleFilter('keys', 'twig_get_array_keys_filter'),
// escaping
- 'escape' => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
- 'e' => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
+ new Twig_SimpleFilter('escape', 'twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
+ new Twig_SimpleFilter('e', 'twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
);
if (function_exists('mb_get_info')) {
public function getFunctions()
{
return array(
- 'range' => new Twig_Function_Function('range'),
- 'constant' => new Twig_Function_Function('constant'),
- 'cycle' => new Twig_Function_Function('twig_cycle'),
- 'random' => new Twig_Function_Function('twig_random', array('needs_environment' => true)),
- 'date' => new Twig_Function_Function('twig_date_converter', array('needs_environment' => true)),
+ new Twig_SimpleFunction('range', 'range'),
+ new Twig_SimpleFunction('constant', 'constant'),
+ new Twig_SimpleFunction('cycle', 'twig_cycle'),
+ new Twig_SimpleFunction('random', 'twig_random', array('needs_environment' => true)),
+ new Twig_SimpleFunction('date', 'twig_date_converter', array('needs_environment' => true)),
);
}
public function getTests()
{
return array(
- 'even' => new Twig_Test_Node('Twig_Node_Expression_Test_Even'),
- 'odd' => new Twig_Test_Node('Twig_Node_Expression_Test_Odd'),
- 'defined' => new Twig_Test_Node('Twig_Node_Expression_Test_Defined'),
- 'sameas' => new Twig_Test_Node('Twig_Node_Expression_Test_Sameas'),
- 'none' => new Twig_Test_Node('Twig_Node_Expression_Test_Null'),
- 'null' => new Twig_Test_Node('Twig_Node_Expression_Test_Null'),
- 'divisibleby' => new Twig_Test_Node('Twig_Node_Expression_Test_Divisibleby'),
- 'constant' => new Twig_Test_Node('Twig_Node_Expression_Test_Constant'),
- 'empty' => new Twig_Test_Function('twig_test_empty'),
- 'iterable' => new Twig_Test_Function('twig_test_iterable'),
+ new Twig_SimpleTest('even', null, array('node_class' => 'Twig_Node_Expression_Test_Even')),
+ new Twig_SimpleTest('odd', null, array('node_class' => 'Twig_Node_Expression_Test_Odd')),
+ new Twig_SimpleTest('defined', null, array('node_class' => 'Twig_Node_Expression_Test_Defined')),
+ new Twig_SimpleTest('sameas', null, array('node_class' => 'Twig_Node_Expression_Test_Sameas')),
+ new Twig_SimpleTest('none', null, array('node_class' => 'Twig_Node_Expression_Test_Null')),
+ new Twig_SimpleTest('null', null, array('node_class' => 'Twig_Node_Expression_Test_Null')),
+ new Twig_SimpleTest('divisibleby', null, array('node_class' => 'Twig_Node_Expression_Test_Divisibleby')),
+ new Twig_SimpleTest('constant', null, array('node_class' => 'Twig_Node_Expression_Test_Constant')),
+ new Twig_SimpleTest('empty', 'twig_test_empty'),
+ new Twig_SimpleTest('iterable', 'twig_test_iterable'),
);
}
throw new Twig_Error_Syntax($message, $line, $parser->getFilename());
}
+ if ($testMap[$name] instanceof Twig_SimpleTest) {
+ return $testMap[$name]->getNodeClass();
+ }
+
return $testMap[$name] instanceof Twig_Test_Node ? $testMap[$name]->getClass() : 'Twig_Node_Expression_Test';
}
;
return array(
- 'dump' => new Twig_Function_Function('twig_var_dump', array('is_safe' => $isDumpOutputHtmlSafe ? array('html') : array(), 'needs_context' => true, 'needs_environment' => true)),
+ new Twig_SimpleFunction('dump', 'twig_var_dump', array('is_safe' => $isDumpOutputHtmlSafe ? array('html') : array(), 'needs_context' => true, 'needs_environment' => true)),
);
}
public function getFilters()
{
return array(
- 'raw' => new Twig_Filter_Function('twig_raw_filter', array('is_safe' => array('all'))),
+ new Twig_SimpleFilter('raw', 'twig_raw_filter', array('is_safe' => array('all'))),
);
}
protected $globals = array();
protected $tests = array();
- public function addFunction($name, Twig_FunctionInterface $function)
+ public function addFunction($name, $function)
{
$this->functions[$name] = $function;
}
return $this->functions;
}
- public function addFilter($name, Twig_FilterInterface $filter)
+ public function addFilter($name, $filter)
{
$this->filters[$name] = $filter;
}
return $this->globals;
}
- public function addTest($name, Twig_TestInterface $test)
+ public function addTest($name, $test)
{
$this->tests[$name] = $test;
}
public function getFunctions()
{
return array(
- 'template_from_string' => new Twig_Function_Function('twig_template_from_string', array('needs_environment' => true)),
+ new Twig_SimpleFunction('template_from_string', 'twig_template_from_string', array('needs_environment' => true)),
);
}
/**
* Represents a template filter.
*
+ * Use Twig_SimpleFilter instead.
+ *
* @package twig
* @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
abstract class Twig_Filter implements Twig_FilterInterface, Twig_FilterCallableInterface
{
/**
* Represents a function template filter.
*
+ * Use Twig_SimpleFilter instead.
+ *
* @package twig
* @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
class Twig_Filter_Function extends Twig_Filter
{
/**
* Represents a method template filter.
*
+ * Use Twig_SimpleFilter instead.
+ *
* @package twig
* @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
class Twig_Filter_Method extends Twig_Filter
{
/**
* Represents a template filter as a node.
*
+ * Use Twig_SimpleFilter instead.
+ *
* @package twig
* @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
class Twig_Filter_Node extends Twig_Filter
{
/**
* Represents a callable template filter.
*
+ * Use Twig_SimpleFilter instead.
+ *
* @package twig
* @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
interface Twig_FilterCallableInterface
{
/**
* Represents a template filter.
*
+ * Use Twig_SimpleFilter instead.
+ *
* @package twig
* @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
interface Twig_FilterInterface
{
/**
* Represents a template function.
*
+ * Use Twig_SimpleFunction instead.
+ *
* @package twig
* @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
abstract class Twig_Function implements Twig_FunctionInterface, Twig_FunctionCallableInterface
{
/**
* Represents a function template function.
*
+ * Use Twig_SimpleFunction instead.
+ *
* @package twig
* @author Arnaud Le Blanc <arnaud.lb@gmail.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
class Twig_Function_Function extends Twig_Function
{
/**
* Represents a method template function.
*
+ * Use Twig_SimpleFunction instead.
+ *
* @package twig
* @author Arnaud Le Blanc <arnaud.lb@gmail.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
class Twig_Function_Method extends Twig_Function
{
/**
* Represents a template function as a node.
*
+ * Use Twig_SimpleFunction instead.
+ *
* @package twig
* @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
class Twig_Function_Node extends Twig_Function
{
/**
* Represents a callable template function.
*
+ * Use Twig_SimpleFunction instead.
+ *
* @package twig
* @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
interface Twig_FunctionCallableInterface
{
/**
* Represents a template function.
*
+ * Use Twig_SimpleFunction instead.
+ *
* @package twig
* @author Arnaud Le Blanc <arnaud.lb@gmail.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
interface Twig_FunctionInterface
{
*/
abstract class Twig_Node_Expression_Call extends Twig_Node_Expression
{
+ protected function compileCallable(Twig_Compiler $compiler)
+ {
+ $callable = $this->getAttribute('callable');
+
+ $closingParenthesis = false;
+ if ($callable) {
+ if (is_string($callable)) {
+ $compiler->raw($callable);
+ } elseif (is_array($callable) && $callable[0] instanceof Twig_ExtensionInterface) {
+ $compiler->raw(sprintf('$this->env->getExtension(\'%s\')->%s', $callable[0]->getName(), $callable[1]));
+ } else {
+ $compiler->raw(sprintf('call_user_func($this->env->getFilter(\'%s\')->getCallable(), ', $this->getAttribute('name')));
+ $closingParenthesis = true;
+ }
+ } else {
+ $compiler->raw($this->getAttribute('thing')->compile());
+ }
+
+ $this->compileArguments($compiler);
+
+ if ($closingParenthesis) {
+ $compiler->raw(')');
+ }
+ }
+
protected function compileArguments(Twig_Compiler $compiler)
{
$compiler->raw('(');
$name = $this->getNode('filter')->getAttribute('value');
$filter = $compiler->getEnvironment()->getFilter($name);
- $compiler->raw($filter->compile());
-
$this->setAttribute('name', $name);
$this->setAttribute('type', 'filter');
+ $this->setAttribute('thing', $filter);
$this->setAttribute('needs_environment', $filter->needsEnvironment());
$this->setAttribute('needs_context', $filter->needsContext());
$this->setAttribute('arguments', $filter->getArguments());
- if ($filter instanceof Twig_FilterCallableInterface) {
+ if ($filter instanceof Twig_FilterCallableInterface || $filter instanceof Twig_SimpleFilter) {
$this->setAttribute('callable', $filter->getCallable());
}
- $this->compileArguments($compiler);
+ $this->compileCallable($compiler);
}
}
{
public function __construct(Twig_NodeInterface $node, Twig_Node_Expression_Constant $filterName, Twig_NodeInterface $arguments, $lineno, $tag = null)
{
- $default = new Twig_Node_Expression_Filter($node, new Twig_Node_Expression_Constant('_default', $node->getLine()), $arguments, $node->getLine());
+ $default = new Twig_Node_Expression_Filter($node, new Twig_Node_Expression_Constant('default', $node->getLine()), $arguments, $node->getLine());
if ('default' === $filterName->getAttribute('value') && ($node instanceof Twig_Node_Expression_Name || $node instanceof Twig_Node_Expression_GetAttr)) {
$test = new Twig_Node_Expression_Test_Defined(clone $node, 'defined', new Twig_Node(), $node->getLine());
$name = $this->getAttribute('name');
$function = $compiler->getEnvironment()->getFunction($name);
- $compiler->raw($function->compile());
-
$this->setAttribute('name', $name);
$this->setAttribute('type', 'function');
+ $this->setAttribute('thing', $function);
$this->setAttribute('needs_environment', $function->needsEnvironment());
$this->setAttribute('needs_context', $function->needsContext());
$this->setAttribute('arguments', $function->getArguments());
- if ($function instanceof Twig_FunctionCallableInterface) {
+ if ($function instanceof Twig_FunctionCallableInterface || $function instanceof Twig_SimpleFunction) {
$this->setAttribute('callable', $function->getCallable());
}
- $this->compileArguments($compiler);
+ $this->compileCallable($compiler);
}
}
$this->setAttribute('name', $name);
$this->setAttribute('type', 'test');
- if ($test instanceof Twig_TestCallableInterface) {
+ $this->setAttribute('thing', $test);
+ if ($test instanceof Twig_TestCallableInterface || $test instanceof Twig_SimpleTest) {
$this->setAttribute('callable', $test->getCallable());
}
- $compiler->raw($test->compile());
-
- $this->compileArguments($compiler);
+ $this->compileCallable($compiler);
}
}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009-2012 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a template filter.
+ *
+ * @package twig
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Twig_SimpleFilter
+{
+ protected $name;
+ protected $callable;
+ protected $options;
+ protected $arguments = array();
+
+ public function __construct($name, $callable, array $options = array())
+ {
+ $this->name = $name;
+ $this->callable = $callable;
+ $this->options = array_merge(array(
+ 'needs_environment' => false,
+ 'needs_context' => false,
+ 'is_safe' => null,
+ 'is_safe_callback' => null,
+ 'pre_escape' => null,
+ 'preserves_safety' => null,
+ 'node_class' => 'Twig_Node_Expression_Filter',
+ ), $options);
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getCallable()
+ {
+ return $this->callable;
+ }
+
+ public function getNodeClass()
+ {
+ return $this->options['node_class'];
+ }
+
+ public function setArguments($arguments)
+ {
+ $this->arguments = $arguments;
+ }
+
+ public function getArguments()
+ {
+ return $this->arguments;
+ }
+
+ public function needsEnvironment()
+ {
+ return $this->options['needs_environment'];
+ }
+
+ public function needsContext()
+ {
+ return $this->options['needs_context'];
+ }
+
+ public function getSafe(Twig_Node $filterArgs)
+ {
+ if (null !== $this->options['is_safe']) {
+ return $this->options['is_safe'];
+ }
+
+ if (null !== $this->options['is_safe_callback']) {
+ return call_user_func($this->options['is_safe_callback'], $filterArgs);
+ }
+
+ return null;
+ }
+
+ public function getPreservesSafety()
+ {
+ return $this->options['preserves_safety'];
+ }
+
+ public function getPreEscape()
+ {
+ return $this->options['pre_escape'];
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010-2012 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a template function.
+ *
+ * @package twig
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Twig_SimpleFunction
+{
+ protected $name;
+ protected $callable;
+ protected $options;
+ protected $arguments = array();
+
+ public function __construct($name, $callable, array $options = array())
+ {
+ $this->name = $name;
+ $this->callable = $callable;
+ $this->options = array_merge(array(
+ 'needs_environment' => false,
+ 'needs_context' => false,
+ 'is_safe' => null,
+ 'is_safe_callback' => null,
+ 'node_class' => 'Twig_Node_Expression_Function',
+ ), $options);
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getCallable()
+ {
+ return $this->callable;
+ }
+
+ public function getNodeClass()
+ {
+ return $this->options['node_class'];
+ }
+
+ public function setArguments($arguments)
+ {
+ $this->arguments = $arguments;
+ }
+
+ public function getArguments()
+ {
+ return $this->arguments;
+ }
+
+ public function needsEnvironment()
+ {
+ return $this->options['needs_environment'];
+ }
+
+ public function needsContext()
+ {
+ return $this->options['needs_context'];
+ }
+
+ public function getSafe(Twig_Node $functionArgs)
+ {
+ if (null !== $this->options['is_safe']) {
+ return $this->options['is_safe'];
+ }
+
+ if (null !== $this->options['is_safe_callback']) {
+ return call_user_func($this->options['is_safe_callback'], $functionArgs);
+ }
+
+ return array();
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010-2012 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a template test.
+ *
+ * @package twig
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class Twig_SimpleTest
+{
+ protected $name;
+ protected $callable;
+ protected $options;
+
+ public function __construct($name, $callable, array $options = array())
+ {
+ $this->name = $name;
+ $this->callable = $callable;
+ $this->options = array_merge(array(
+ 'node_class' => 'Twig_Node_Expression_Test',
+ ), $options);
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getCallable()
+ {
+ return $this->callable;
+ }
+
+ public function getNodeClass()
+ {
+ return $this->options['node_class'];
+ }
+}
*
* @package twig
* @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
abstract class Twig_Test implements Twig_TestInterface, Twig_TestCallableInterface
{
*
* @package twig
* @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
class Twig_Test_Function extends Twig_Test
{
*
* @package twig
* @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
class Twig_Test_Method extends Twig_Test
{
*
* @package twig
* @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
class Twig_Test_Node extends Twig_Test
{
*
* @package twig
* @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
interface Twig_TestCallableInterface
{
*
* @package twig
* @author Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
*/
interface Twig_TestInterface
{