From: Fabien Potencier Date: Fri, 16 Nov 2012 14:22:28 +0000 (+0100) Subject: added the ability to use any PHP callable to define filters, functions, and tests X-Git-Url: http://git.silmor.de/gitweb/?a=commitdiff_plain;h=1918edefa937b81233087aa79297e54ea0462f9b;p=web%2Fkonrad%2Ftwig.git added the ability to use any PHP callable to define filters, functions, and tests --- diff --git a/CHANGELOG b/CHANGELOG index fffef7e..0f3b7fd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ * 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 diff --git a/doc/advanced.rst b/doc/advanced.rst index efe31ac..869c112 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -1,6 +1,12 @@ 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` 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. @@ -120,122 +126,70 @@ You can then use the ``text`` variable anywhere in a template: 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 - - - -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 - - - -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. + + -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 ~~~~~~~~~~~~~~~~~~~~~ @@ -245,40 +199,40 @@ If you want to access the current context in your filter, set the 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: @@ -287,83 +241,43 @@ 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 - - - -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 ---- @@ -530,7 +444,7 @@ 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 + recompile your templates whenever you make a change to it (when ``auto_reload`` is enabled). .. note:: @@ -676,7 +590,7 @@ method:: public function getFunctions() { return array( - 'lipsum' => new Twig_Function_Function('generate_lipsum'), + new Twig_SimpleFunction('lipsum', 'generate_lipsum'), ); } @@ -695,50 +609,13 @@ environment:: 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 ~~~~ @@ -794,7 +671,7 @@ The ``getTests()`` methods allows to add new test functions:: public function getTests() { return array( - 'even' => new Twig_Test_Function('twig_test_even'), + new Twig_SimpleTest('even', 'twig_test_even'), ); } @@ -808,7 +685,9 @@ To overload an already defined filter, test, operator, global variable, or 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. @@ -819,7 +698,7 @@ That also works with an extension:: public function getFilters() { return array( - 'date' => new Twig_Filter_Method($this, 'dateFilter'), + new Twig_SimpleFilter('date', array($this, 'dateFilter')), ); } @@ -845,9 +724,6 @@ That also works with an extension:: Testing an Extension -------------------- -.. versionadded:: 1.10 - Support for functional tests was added in Twig 1.10. - Functional Tests ~~~~~~~~~~~~~~~~ diff --git a/doc/advanced_legacy.rst b/doc/advanced_legacy.rst new file mode 100644 index 0000000..8dbc525 --- /dev/null +++ b/doc/advanced_legacy.rst @@ -0,0 +1,887 @@ +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` + 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 + + + +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 + + + +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 + + + +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 diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 1933d36..3b2df29 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -26,6 +26,57 @@ PEAR 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 ---------- diff --git a/lib/Twig/Environment.php b/lib/Twig/Environment.php index 95eb2fe..7aa427a 100644 --- a/lib/Twig/Environment.php +++ b/lib/Twig/Environment.php @@ -748,15 +748,19 @@ class Twig_Environment /** * 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); } @@ -828,15 +832,19 @@ class Twig_Environment /** * 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); } @@ -857,15 +865,19 @@ class Twig_Environment /** * 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); } @@ -1051,16 +1063,37 @@ class Twig_Environment { // 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; } diff --git a/lib/Twig/ExpressionParser.php b/lib/Twig/ExpressionParser.php index bc8a8fd..a330246 100644 --- a/lib/Twig/ExpressionParser.php +++ b/lib/Twig/ExpressionParser.php @@ -547,6 +547,10 @@ class Twig_ExpressionParser 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'; } @@ -563,6 +567,10 @@ class Twig_ExpressionParser 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'; } diff --git a/lib/Twig/Extension/Core.php b/lib/Twig/Extension/Core.php index 3727f5f..81b4644 100644 --- a/lib/Twig/Extension/Core.php +++ b/lib/Twig/Extension/Core.php @@ -126,47 +126,45 @@ class Twig_Extension_Core extends Twig_Extension { $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')) { @@ -185,11 +183,11 @@ class Twig_Extension_Core extends Twig_Extension 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)), ); } @@ -201,16 +199,16 @@ class Twig_Extension_Core extends Twig_Extension 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'), ); } @@ -288,6 +286,10 @@ class Twig_Extension_Core extends Twig_Extension 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'; } diff --git a/lib/Twig/Extension/Debug.php b/lib/Twig/Extension/Debug.php index 3dc4d2d..97007fb 100644 --- a/lib/Twig/Extension/Debug.php +++ b/lib/Twig/Extension/Debug.php @@ -27,7 +27,7 @@ class Twig_Extension_Debug extends Twig_Extension ; 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)), ); } diff --git a/lib/Twig/Extension/Escaper.php b/lib/Twig/Extension/Escaper.php index db1a50e..c9a7f68 100644 --- a/lib/Twig/Extension/Escaper.php +++ b/lib/Twig/Extension/Escaper.php @@ -45,7 +45,7 @@ class Twig_Extension_Escaper extends Twig_Extension 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'))), ); } diff --git a/lib/Twig/Extension/Staging.php b/lib/Twig/Extension/Staging.php index ca7847c..f67fc65 100644 --- a/lib/Twig/Extension/Staging.php +++ b/lib/Twig/Extension/Staging.php @@ -26,7 +26,7 @@ class Twig_Extension_Staging extends Twig_Extension protected $globals = array(); protected $tests = array(); - public function addFunction($name, Twig_FunctionInterface $function) + public function addFunction($name, $function) { $this->functions[$name] = $function; } @@ -39,7 +39,7 @@ class Twig_Extension_Staging extends Twig_Extension return $this->functions; } - public function addFilter($name, Twig_FilterInterface $filter) + public function addFilter($name, $filter) { $this->filters[$name] = $filter; } @@ -91,7 +91,7 @@ class Twig_Extension_Staging extends Twig_Extension return $this->globals; } - public function addTest($name, Twig_TestInterface $test) + public function addTest($name, $test) { $this->tests[$name] = $test; } diff --git a/lib/Twig/Extension/StringLoader.php b/lib/Twig/Extension/StringLoader.php index 90caf28..d5b881b 100644 --- a/lib/Twig/Extension/StringLoader.php +++ b/lib/Twig/Extension/StringLoader.php @@ -16,7 +16,7 @@ class Twig_Extension_StringLoader extends Twig_Extension 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)), ); } diff --git a/lib/Twig/Filter.php b/lib/Twig/Filter.php index 90a62d3..879ef67 100644 --- a/lib/Twig/Filter.php +++ b/lib/Twig/Filter.php @@ -12,8 +12,11 @@ /** * Represents a template filter. * + * Use Twig_SimpleFilter instead. + * * @package twig * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ abstract class Twig_Filter implements Twig_FilterInterface, Twig_FilterCallableInterface { diff --git a/lib/Twig/Filter/Function.php b/lib/Twig/Filter/Function.php index 59af50d..ae1f961 100644 --- a/lib/Twig/Filter/Function.php +++ b/lib/Twig/Filter/Function.php @@ -12,8 +12,11 @@ /** * Represents a function template filter. * + * Use Twig_SimpleFilter instead. + * * @package twig * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ class Twig_Filter_Function extends Twig_Filter { diff --git a/lib/Twig/Filter/Method.php b/lib/Twig/Filter/Method.php index 0f5b27e..074371a 100644 --- a/lib/Twig/Filter/Method.php +++ b/lib/Twig/Filter/Method.php @@ -12,8 +12,11 @@ /** * Represents a method template filter. * + * Use Twig_SimpleFilter instead. + * * @package twig * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ class Twig_Filter_Method extends Twig_Filter { diff --git a/lib/Twig/Filter/Node.php b/lib/Twig/Filter/Node.php index 7481c05..4d27c93 100644 --- a/lib/Twig/Filter/Node.php +++ b/lib/Twig/Filter/Node.php @@ -12,8 +12,11 @@ /** * Represents a template filter as a node. * + * Use Twig_SimpleFilter instead. + * * @package twig * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ class Twig_Filter_Node extends Twig_Filter { diff --git a/lib/Twig/FilterCallableInterface.php b/lib/Twig/FilterCallableInterface.php index 86f0419..97c7610 100644 --- a/lib/Twig/FilterCallableInterface.php +++ b/lib/Twig/FilterCallableInterface.php @@ -12,8 +12,11 @@ /** * Represents a callable template filter. * + * Use Twig_SimpleFilter instead. + * * @package twig * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ interface Twig_FilterCallableInterface { diff --git a/lib/Twig/FilterInterface.php b/lib/Twig/FilterInterface.php index 0a07c7c..0c7f0a4 100644 --- a/lib/Twig/FilterInterface.php +++ b/lib/Twig/FilterInterface.php @@ -12,8 +12,11 @@ /** * Represents a template filter. * + * Use Twig_SimpleFilter instead. + * * @package twig * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ interface Twig_FilterInterface { diff --git a/lib/Twig/Function.php b/lib/Twig/Function.php index 0a13441..b30c30e 100644 --- a/lib/Twig/Function.php +++ b/lib/Twig/Function.php @@ -12,8 +12,11 @@ /** * Represents a template function. * + * Use Twig_SimpleFunction instead. + * * @package twig * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ abstract class Twig_Function implements Twig_FunctionInterface, Twig_FunctionCallableInterface { diff --git a/lib/Twig/Function/Function.php b/lib/Twig/Function/Function.php index e102479..7e5c9c2 100644 --- a/lib/Twig/Function/Function.php +++ b/lib/Twig/Function/Function.php @@ -13,8 +13,11 @@ /** * Represents a function template function. * + * Use Twig_SimpleFunction instead. + * * @package twig * @author Arnaud Le Blanc + * @deprecated since 1.12 (to be removed in 2.0) */ class Twig_Function_Function extends Twig_Function { diff --git a/lib/Twig/Function/Method.php b/lib/Twig/Function/Method.php index d0f296d..a13741e 100644 --- a/lib/Twig/Function/Method.php +++ b/lib/Twig/Function/Method.php @@ -13,8 +13,11 @@ /** * Represents a method template function. * + * Use Twig_SimpleFunction instead. + * * @package twig * @author Arnaud Le Blanc + * @deprecated since 1.12 (to be removed in 2.0) */ class Twig_Function_Method extends Twig_Function { diff --git a/lib/Twig/Function/Node.php b/lib/Twig/Function/Node.php index df937e5..068c5fd 100644 --- a/lib/Twig/Function/Node.php +++ b/lib/Twig/Function/Node.php @@ -12,8 +12,11 @@ /** * Represents a template function as a node. * + * Use Twig_SimpleFunction instead. + * * @package twig * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ class Twig_Function_Node extends Twig_Function { diff --git a/lib/Twig/FunctionCallableInterface.php b/lib/Twig/FunctionCallableInterface.php index fc54308..dfd6f75 100644 --- a/lib/Twig/FunctionCallableInterface.php +++ b/lib/Twig/FunctionCallableInterface.php @@ -12,8 +12,11 @@ /** * Represents a callable template function. * + * Use Twig_SimpleFunction instead. + * * @package twig * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ interface Twig_FunctionCallableInterface { diff --git a/lib/Twig/FunctionInterface.php b/lib/Twig/FunctionInterface.php index d652ac3..1c03cbd 100644 --- a/lib/Twig/FunctionInterface.php +++ b/lib/Twig/FunctionInterface.php @@ -13,8 +13,11 @@ /** * Represents a template function. * + * Use Twig_SimpleFunction instead. + * * @package twig * @author Arnaud Le Blanc + * @deprecated since 1.12 (to be removed in 2.0) */ interface Twig_FunctionInterface { diff --git a/lib/Twig/Node/Expression/Call.php b/lib/Twig/Node/Expression/Call.php index 47f92f6..e014955 100644 --- a/lib/Twig/Node/Expression/Call.php +++ b/lib/Twig/Node/Expression/Call.php @@ -10,6 +10,31 @@ */ 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('('); diff --git a/lib/Twig/Node/Expression/Filter.php b/lib/Twig/Node/Expression/Filter.php index ea7f4a6..207b062 100644 --- a/lib/Twig/Node/Expression/Filter.php +++ b/lib/Twig/Node/Expression/Filter.php @@ -21,17 +21,16 @@ class Twig_Node_Expression_Filter extends Twig_Node_Expression_Call $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); } } diff --git a/lib/Twig/Node/Expression/Filter/Default.php b/lib/Twig/Node/Expression/Filter/Default.php index 1cb3342..fccd39a 100644 --- a/lib/Twig/Node/Expression/Filter/Default.php +++ b/lib/Twig/Node/Expression/Filter/Default.php @@ -23,7 +23,7 @@ class Twig_Node_Expression_Filter_Default extends Twig_Node_Expression_Filter { 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()); diff --git a/lib/Twig/Node/Expression/Function.php b/lib/Twig/Node/Expression/Function.php index 8f2be6e..3e1f6b5 100644 --- a/lib/Twig/Node/Expression/Function.php +++ b/lib/Twig/Node/Expression/Function.php @@ -20,17 +20,16 @@ class Twig_Node_Expression_Function extends Twig_Node_Expression_Call $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); } } diff --git a/lib/Twig/Node/Expression/Test.php b/lib/Twig/Node/Expression/Test.php index 45bb45b..9ec54f3 100644 --- a/lib/Twig/Node/Expression/Test.php +++ b/lib/Twig/Node/Expression/Test.php @@ -23,12 +23,11 @@ class Twig_Node_Expression_Test extends Twig_Node_Expression_Call $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); } } diff --git a/lib/Twig/SimpleFilter.php b/lib/Twig/SimpleFilter.php new file mode 100644 index 0000000..7089a9d --- /dev/null +++ b/lib/Twig/SimpleFilter.php @@ -0,0 +1,97 @@ + + */ +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']; + } +} diff --git a/lib/Twig/SimpleFunction.php b/lib/Twig/SimpleFunction.php new file mode 100644 index 0000000..9924154 --- /dev/null +++ b/lib/Twig/SimpleFunction.php @@ -0,0 +1,85 @@ + + */ +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(); + } +} diff --git a/lib/Twig/SimpleTest.php b/lib/Twig/SimpleTest.php new file mode 100644 index 0000000..7502c79 --- /dev/null +++ b/lib/Twig/SimpleTest.php @@ -0,0 +1,47 @@ + + */ +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']; + } +} diff --git a/lib/Twig/Test.php b/lib/Twig/Test.php index 06d4f12..7fef1b4 100644 --- a/lib/Twig/Test.php +++ b/lib/Twig/Test.php @@ -14,6 +14,7 @@ * * @package twig * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ abstract class Twig_Test implements Twig_TestInterface, Twig_TestCallableInterface { diff --git a/lib/Twig/Test/Function.php b/lib/Twig/Test/Function.php index 50f561f..d0ff490 100644 --- a/lib/Twig/Test/Function.php +++ b/lib/Twig/Test/Function.php @@ -14,6 +14,7 @@ * * @package twig * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ class Twig_Test_Function extends Twig_Test { diff --git a/lib/Twig/Test/Method.php b/lib/Twig/Test/Method.php index 20dbf82..5a5f37f 100644 --- a/lib/Twig/Test/Method.php +++ b/lib/Twig/Test/Method.php @@ -14,6 +14,7 @@ * * @package twig * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ class Twig_Test_Method extends Twig_Test { diff --git a/lib/Twig/Test/Node.php b/lib/Twig/Test/Node.php index d9d47eb..eee48f9 100644 --- a/lib/Twig/Test/Node.php +++ b/lib/Twig/Test/Node.php @@ -14,6 +14,7 @@ * * @package twig * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ class Twig_Test_Node extends Twig_Test { diff --git a/lib/Twig/TestCallableInterface.php b/lib/Twig/TestCallableInterface.php index 3f12290..18a9ca2 100644 --- a/lib/Twig/TestCallableInterface.php +++ b/lib/Twig/TestCallableInterface.php @@ -14,6 +14,7 @@ * * @package twig * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ interface Twig_TestCallableInterface { diff --git a/lib/Twig/TestInterface.php b/lib/Twig/TestInterface.php index 96db428..c1a2118 100644 --- a/lib/Twig/TestInterface.php +++ b/lib/Twig/TestInterface.php @@ -14,6 +14,7 @@ * * @package twig * @author Fabien Potencier + * @deprecated since 1.12 (to be removed in 2.0) */ interface Twig_TestInterface {