From 4f19c7a04b0f2626848307c5bb6285f36b61da3f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 20 Nov 2011 09:00:09 +0100 Subject: [PATCH] added support for dynamically named filters and functions --- CHANGELOG | 1 + doc/advanced.rst | 68 ++++++++++++++++++++ lib/Twig/Environment.php | 26 ++++++++ lib/Twig/Filter.php | 11 +++ lib/Twig/FilterInterface.php | 4 + lib/Twig/Function.php | 11 +++ lib/Twig/FunctionInterface.php | 4 + lib/Twig/Node/Expression/Filter.php | 10 +++- lib/Twig/Node/Expression/Function.php | 33 ++++++--- .../Tests/Fixtures/filters/dynamic_filter.test | 10 +++ .../Tests/Fixtures/functions/dynamic_function.test | 10 +++ test/Twig/Tests/integrationTest.php | 24 ++++++-- 12 files changed, 195 insertions(+), 17 deletions(-) create mode 100644 test/Twig/Tests/Fixtures/filters/dynamic_filter.test create mode 100644 test/Twig/Tests/Fixtures/functions/dynamic_function.test diff --git a/CHANGELOG b/CHANGELOG index 47630e5..192dca8 100644 --- 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 diff --git a/doc/advanced.rst b/doc/advanced.rst index 1fa3e83..53d4202 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -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 ---- diff --git a/lib/Twig/Environment.php b/lib/Twig/Environment.php index 7d22787..ac6c914 100644 --- a/lib/Twig/Environment.php +++ b/lib/Twig/Environment.php @@ -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; diff --git a/lib/Twig/Filter.php b/lib/Twig/Filter.php index 9595a1a..f27f08e 100644 --- a/lib/Twig/Filter.php +++ b/lib/Twig/Filter.php @@ -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']; diff --git a/lib/Twig/FilterInterface.php b/lib/Twig/FilterInterface.php index 4ac19ce..866e932 100644 --- a/lib/Twig/FilterInterface.php +++ b/lib/Twig/FilterInterface.php @@ -31,4 +31,8 @@ interface Twig_FilterInterface function getSafe(Twig_Node $filterArgs); function getPreEscape(); + + function setArguments($arguments); + + function getArguments(); } diff --git a/lib/Twig/Function.php b/lib/Twig/Function.php index 1197924..cd7643f 100644 --- a/lib/Twig/Function.php +++ b/lib/Twig/Function.php @@ -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']; diff --git a/lib/Twig/FunctionInterface.php b/lib/Twig/FunctionInterface.php index ccc9fd9..d402d17 100644 --- a/lib/Twig/FunctionInterface.php +++ b/lib/Twig/FunctionInterface.php @@ -30,4 +30,8 @@ interface Twig_FunctionInterface function needsContext(); function getSafe(Twig_Node $filterArgs); + + function setArguments($arguments); + + function getArguments(); } diff --git a/lib/Twig/Node/Expression/Filter.php b/lib/Twig/Node/Expression/Filter.php index 8559e7b..8a0903a 100644 --- a/lib/Twig/Node/Expression/Filter.php +++ b/lib/Twig/Node/Expression/Filter.php @@ -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(', ') diff --git a/lib/Twig/Node/Expression/Function.php b/lib/Twig/Node/Expression/Function.php index d7bafc0..9342bb1 100644 --- a/lib/Twig/Node/Expression/Function.php +++ b/lib/Twig/Node/Expression/Function.php @@ -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 index 0000000..93c5913 --- /dev/null +++ b/test/Twig/Tests/Fixtures/filters/dynamic_filter.test @@ -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 index 0000000..913fbc9 --- /dev/null +++ b/test/Twig/Tests/Fixtures/functions/dynamic_function.test @@ -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 diff --git a/test/Twig/Tests/integrationTest.php b/test/Twig/Tests/integrationTest.php index b7ce96b..bf8cc8f 100644 --- a/test/Twig/Tests/integrationTest.php +++ b/test/Twig/Tests/integrationTest.php @@ -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); -- 1.7.2.5