added the ability to use any PHP callable to define filters, functions, and tests
authorFabien Potencier <fabien.potencier@gmail.com>
Fri, 16 Nov 2012 14:22:28 +0000 (15:22 +0100)
committerFabien Potencier <fabien.potencier@gmail.com>
Mon, 10 Dec 2012 14:09:47 +0000 (15:09 +0100)
37 files changed:
CHANGELOG
doc/advanced.rst
doc/advanced_legacy.rst [new file with mode: 0644]
doc/deprecated.rst
lib/Twig/Environment.php
lib/Twig/ExpressionParser.php
lib/Twig/Extension/Core.php
lib/Twig/Extension/Debug.php
lib/Twig/Extension/Escaper.php
lib/Twig/Extension/Staging.php
lib/Twig/Extension/StringLoader.php
lib/Twig/Filter.php
lib/Twig/Filter/Function.php
lib/Twig/Filter/Method.php
lib/Twig/Filter/Node.php
lib/Twig/FilterCallableInterface.php
lib/Twig/FilterInterface.php
lib/Twig/Function.php
lib/Twig/Function/Function.php
lib/Twig/Function/Method.php
lib/Twig/Function/Node.php
lib/Twig/FunctionCallableInterface.php
lib/Twig/FunctionInterface.php
lib/Twig/Node/Expression/Call.php
lib/Twig/Node/Expression/Filter.php
lib/Twig/Node/Expression/Filter/Default.php
lib/Twig/Node/Expression/Function.php
lib/Twig/Node/Expression/Test.php
lib/Twig/SimpleFilter.php [new file with mode: 0644]
lib/Twig/SimpleFunction.php [new file with mode: 0644]
lib/Twig/SimpleTest.php [new file with mode: 0644]
lib/Twig/Test.php
lib/Twig/Test/Function.php
lib/Twig/Test/Method.php
lib/Twig/Test/Node.php
lib/Twig/TestCallableInterface.php
lib/Twig/TestInterface.php

index fffef7e..0f3b7fd 100644 (file)
--- 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
index efe31ac..869c112 100644 (file)
@@ -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<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.
@@ -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
-
-    <?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
 ~~~~~~~~~~~~~~~~~~~~~
@@ -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
-
-    <?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
 ----
@@ -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 (file)
index 0000000..8dbc525
--- /dev/null
@@ -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<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
index 1933d36..3b2df29 100644 (file)
@@ -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
 ----------
 
index 95eb2fe..7aa427a 100644 (file)
@@ -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;
         }
 
index bc8a8fd..a330246 100644 (file)
@@ -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';
     }
 
index 3727f5f..81b4644 100644 (file)
@@ -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';
     }
 
index 3dc4d2d..97007fb 100644 (file)
@@ -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)),
         );
     }
 
index db1a50e..c9a7f68 100644 (file)
@@ -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'))),
         );
     }
 
index ca7847c..f67fc65 100644 (file)
@@ -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;
     }
index 90caf28..d5b881b 100644 (file)
@@ -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)),
         );
     }
 
index 90a62d3..879ef67 100644 (file)
 /**
  * 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
 {
index 59af50d..ae1f961 100644 (file)
 /**
  * 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
 {
index 0f5b27e..074371a 100644 (file)
 /**
  * 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
 {
index 7481c05..4d27c93 100644 (file)
 /**
  * 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
 {
index 86f0419..97c7610 100644 (file)
 /**
  * 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
 {
index 0a07c7c..0c7f0a4 100644 (file)
 /**
  * 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
 {
index 0a13441..b30c30e 100644 (file)
 /**
  * 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
 {
index e102479..7e5c9c2 100644 (file)
 /**
  * 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
 {
index d0f296d..a13741e 100644 (file)
 /**
  * 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
 {
index df937e5..068c5fd 100644 (file)
 /**
  * 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
 {
index fc54308..dfd6f75 100644 (file)
 /**
  * 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
 {
index d652ac3..1c03cbd 100644 (file)
 /**
  * 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
 {
index 47f92f6..e014955 100644 (file)
  */
 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('(');
index ea7f4a6..207b062 100644 (file)
@@ -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);
     }
 }
index 1cb3342..fccd39a 100644 (file)
@@ -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());
index 8f2be6e..3e1f6b5 100644 (file)
@@ -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);
     }
 }
index 45bb45b..9ec54f3 100644 (file)
@@ -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 (file)
index 0000000..7089a9d
--- /dev/null
@@ -0,0 +1,97 @@
+<?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'];
+    }
+}
diff --git a/lib/Twig/SimpleFunction.php b/lib/Twig/SimpleFunction.php
new file mode 100644 (file)
index 0000000..9924154
--- /dev/null
@@ -0,0 +1,85 @@
+<?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();
+    }
+}
diff --git a/lib/Twig/SimpleTest.php b/lib/Twig/SimpleTest.php
new file mode 100644 (file)
index 0000000..7502c79
--- /dev/null
@@ -0,0 +1,47 @@
+<?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'];
+    }
+}
index 06d4f12..7fef1b4 100644 (file)
@@ -14,6 +14,7 @@
  *
  * @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
 {
index 50f561f..d0ff490 100644 (file)
@@ -14,6 +14,7 @@
  *
  * @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
 {
index 20dbf82..5a5f37f 100644 (file)
@@ -14,6 +14,7 @@
  *
  * @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
 {
index d9d47eb..eee48f9 100644 (file)
@@ -14,6 +14,7 @@
  *
  * @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
 {
index 3f12290..18a9ca2 100644 (file)
@@ -14,6 +14,7 @@
  *
  * @package    twig
  * @author     Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
  */
 interface Twig_TestCallableInterface
 {
index 96db428..c1a2118 100644 (file)
@@ -14,6 +14,7 @@
  *
  * @package    twig
  * @author     Fabien Potencier <fabien@symfony.com>
+ * @deprecated since 1.12 (to be removed in 2.0)
  */
 interface Twig_TestInterface
 {