added support for dynamically named filters and functions
authorFabien Potencier <fabien.potencier@gmail.com>
Sun, 20 Nov 2011 08:00:09 +0000 (09:00 +0100)
committerFabien Potencier <fabien.potencier@gmail.com>
Sun, 18 Dec 2011 19:20:35 +0000 (20:20 +0100)
12 files changed:
CHANGELOG
doc/advanced.rst
lib/Twig/Environment.php
lib/Twig/Filter.php
lib/Twig/FilterInterface.php
lib/Twig/Function.php
lib/Twig/FunctionInterface.php
lib/Twig/Node/Expression/Filter.php
lib/Twig/Node/Expression/Function.php
test/Twig/Tests/Fixtures/filters/dynamic_filter.test [new file with mode: 0644]
test/Twig/Tests/Fixtures/functions/dynamic_function.test [new file with mode: 0644]
test/Twig/Tests/integrationTest.php

index 47630e5..192dca8 100644 (file)
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,6 @@
 * 1.5.0
 
+ * added support for dynamically named filters and functions
  * added a dump function to help debugging templates
  * added a nl2br filter
  * added a random function
index 1fa3e83..53d4202 100644 (file)
@@ -240,6 +240,40 @@ case, set the ``pre_escape`` option::
 
     $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
 ---------
 
@@ -273,6 +307,40 @@ You can also expose extension methods as functions in your templates::
 
 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
 ----
 
index 7d22787..ac6c914 100644 (file)
@@ -771,6 +771,19 @@ class Twig_Environment
             return $this->filters[$name];
         }
 
+        foreach ($this->filters as $pattern => $filter) {
+            $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
+
+            if ($count) {
+                if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
+                    array_shift($matches);
+                    $filter->setArguments($matches);
+
+                    return $filter;
+                }
+            }
+        }
+
         foreach ($this->filterCallbacks as $callback) {
             if (false !== $filter = call_user_func($callback, $name)) {
                 return $filter;
@@ -865,6 +878,19 @@ class Twig_Environment
             return $this->functions[$name];
         }
 
+        foreach ($this->functions as $pattern => $function) {
+            $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
+
+            if ($count) {
+                if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
+                    array_shift($matches);
+                    $function->setArguments($matches);
+
+                    return $function;
+                }
+            }
+        }
+
         foreach ($this->functionCallbacks as $callback) {
             if (false !== $function = call_user_func($callback, $name)) {
                 return $function;
index 9595a1a..f27f08e 100644 (file)
@@ -18,6 +18,7 @@
 abstract class Twig_Filter implements Twig_FilterInterface
 {
     protected $options;
+    protected $arguments = array();
 
     public function __construct(array $options = array())
     {
@@ -28,6 +29,16 @@ abstract class Twig_Filter implements Twig_FilterInterface
         ), $options);
     }
 
+    public function setArguments($arguments)
+    {
+        $this->arguments = $arguments;
+    }
+
+    public function getArguments()
+    {
+        return $this->arguments;
+    }
+
     public function needsEnvironment()
     {
         return $this->options['needs_environment'];
index 4ac19ce..866e932 100644 (file)
@@ -31,4 +31,8 @@ interface Twig_FilterInterface
     function getSafe(Twig_Node $filterArgs);
 
     function getPreEscape();
+
+    function setArguments($arguments);
+
+    function getArguments();
 }
index 1197924..cd7643f 100644 (file)
@@ -18,6 +18,7 @@
 abstract class Twig_Function implements Twig_FunctionInterface
 {
     protected $options;
+    protected $arguments = array();
 
     public function __construct(array $options = array())
     {
@@ -27,6 +28,16 @@ abstract class Twig_Function implements Twig_FunctionInterface
         ), $options);
     }
 
+    public function setArguments($arguments)
+    {
+        $this->arguments = $arguments;
+    }
+
+    public function getArguments()
+    {
+        return $this->arguments;
+    }
+
     public function needsEnvironment()
     {
         return $this->options['needs_environment'];
index ccc9fd9..d402d17 100644 (file)
@@ -30,4 +30,8 @@ interface Twig_FunctionInterface
     function needsContext();
 
     function getSafe(Twig_Node $filterArgs);
+
+    function setArguments($arguments);
+
+    function getArguments();
 }
index 8559e7b..8a0903a 100644 (file)
@@ -38,9 +38,17 @@ class Twig_Node_Expression_Filter extends Twig_Node_Expression
             ->raw($filter->compile().'(')
             ->raw($filter->needsEnvironment() ? '$this->env, ' : '')
             ->raw($filter->needsContext() ? '$context, ' : '')
-            ->subcompile($this->getNode('node'))
         ;
 
+        foreach ($filter->getArguments() as $argument) {
+            $compiler
+                ->string($argument)
+                ->raw(', ')
+            ;
+        }
+
+        $compiler->subcompile($this->getNode('node'));
+
         foreach ($this->getNode('arguments') as $node) {
             $compiler
                 ->raw(', ')
index d7bafc0..9342bb1 100644 (file)
@@ -28,26 +28,37 @@ class Twig_Node_Expression_Function extends Twig_Node_Expression
             throw new Twig_Error_Syntax($message, $this->getLine());
         }
 
-        $compiler
-            ->raw($function->compile().'(')
-            ->raw($function->needsEnvironment() ? '$this->env' : '')
-        ;
+        $compiler->raw($function->compile().'(');
+
+        $first = true;
+
+        if ($function->needsEnvironment()) {
+            $compiler->raw('$this->env');
+            $first = false;
+        }
 
         if ($function->needsContext()) {
-            $compiler->raw($function->needsEnvironment() ? ', $context' : '$context');
+            if (!$first) {
+                $compiler->raw(', ');
+            }
+            $compiler->raw('$context');
+            $first = false;
+        }
+
+        foreach ($function->getArguments() as $argument) {
+            if (!$first) {
+                $compiler->raw(', ');
+            }
+            $compiler->string($argument);
+            $first = false;
         }
 
-        $first = true;
         foreach ($this->getNode('arguments') as $node) {
             if (!$first) {
                 $compiler->raw(', ');
-            } else {
-                if ($function->needsEnvironment() || $function->needsContext()) {
-                    $compiler->raw(', ');
-                }
-                $first = false;
             }
             $compiler->subcompile($node);
+            $first = false;
         }
 
         $compiler->raw(')');
diff --git a/test/Twig/Tests/Fixtures/filters/dynamic_filter.test b/test/Twig/Tests/Fixtures/filters/dynamic_filter.test
new file mode 100644 (file)
index 0000000..93c5913
--- /dev/null
@@ -0,0 +1,10 @@
+--TEST--
+dynamic filter
+--TEMPLATE--
+{{ 'bar'|foo_path }}
+{{ 'bar'|a_foo_b_bar }}
+--DATA--
+return array()
+--EXPECT--
+foo/bar
+a/b/bar
diff --git a/test/Twig/Tests/Fixtures/functions/dynamic_function.test b/test/Twig/Tests/Fixtures/functions/dynamic_function.test
new file mode 100644 (file)
index 0000000..913fbc9
--- /dev/null
@@ -0,0 +1,10 @@
+--TEST--
+dynamic function
+--TEMPLATE--
+{{ foo_path('bar') }}
+{{ a_foo_b_bar('bar') }}
+--DATA--
+return array()
+--EXPECT--
+foo/bar
+a/b/bar
index b7ce96b..bf8cc8f 100644 (file)
@@ -181,19 +181,23 @@ class TestExtension extends Twig_Extension
     public function getFilters()
     {
         return array(
-            '☃' => new Twig_Filter_Method($this, '☃Filter'),
+            '☃'                => new Twig_Filter_Method($this, '☃Filter'),
             'escape_and_nl2br' => new Twig_Filter_Method($this, 'escape_and_nl2br', array('needs_environment' => true, 'is_safe' => array('html'))),
-            'nl2br' => new Twig_Filter_Method($this, 'nl2br', array('pre_escape' => 'html', 'is_safe' => array('html'))),
+            'nl2br'            => new Twig_Filter_Method($this, 'nl2br', array('pre_escape' => 'html', 'is_safe' => array('html'))),
             'escape_something' => new Twig_Filter_Method($this, 'escape_something', array('is_safe' => array('something'))),
+            '*_path'           => new Twig_Filter_Method($this, 'dynamic_path'),
+            '*_foo_*_bar'      => new Twig_Filter_Method($this, 'dynamic_foo'),
         );
     }
 
     public function getFunctions()
     {
         return array(
-            '☃' => new Twig_Function_Method($this, '☃Function'),
-            'safe_br' => new Twig_Function_Method($this, 'br', array('is_safe' => array('html'))),
-            'unsafe_br' => new Twig_Function_Method($this, 'br'),
+            '☃'           => new Twig_Function_Method($this, '☃Function'),
+            'safe_br'     => new Twig_Function_Method($this, 'br', array('is_safe' => array('html'))),
+            'unsafe_br'   => new Twig_Function_Method($this, 'br'),
+            '*_path'      => new Twig_Function_Method($this, 'dynamic_path'),
+            '*_foo_*_bar' => new Twig_Function_Method($this, 'dynamic_foo'),
         );
     }
 
@@ -225,6 +229,16 @@ class TestExtension extends Twig_Extension
         return str_replace("\n", "$sep\n", $value);
     }
 
+    public function dynamic_path($element, $item)
+    {
+        return $element.'/'.$item;
+    }
+
+    public function dynamic_foo($foo, $bar, $item)
+    {
+        return $foo.'/'.$bar.'/'.$item;
+    }
+
     public function escape_something($value)
     {
         return strtoupper($value);