From c225a5bed92616e69b13888e47ca2f99fe26731e Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 24 Dec 2010 17:21:10 +0100 Subject: [PATCH] Handle functions like filters Functions are not global variables anymore. They are resolved at compile time, and Twig_Function objects are instanciated only when compiling. --- lib/Twig/Environment.php | 38 ++++++++++++++++++++ lib/Twig/Extension.php | 10 +++++ lib/Twig/Extension/Core.php | 8 ++-- lib/Twig/ExtensionInterface.php | 7 ++++ lib/Twig/Function.php | 30 ++++++++++------ lib/Twig/Function/Function.php | 34 +++++++++++++++++ lib/Twig/Function/Method.php | 35 ++++++++++++++++++ lib/Twig/FunctionInterface.php | 24 ++++++++++++ lib/Twig/Node/Expression/Function.php | 25 +++++++------ test/Twig/Tests/Fixtures/expressions/function.test | 13 ------- test/Twig/Tests/Fixtures/globals.test | 25 ------------- test/Twig/Tests/Fixtures/tags/from.test | 14 +++++++ test/Twig/Tests/Fixtures/tags/include/only.test | 8 ++-- test/Twig/Tests/integrationTest.php | 13 +++++++ 14 files changed, 215 insertions(+), 69 deletions(-) create mode 100644 lib/Twig/Function/Function.php create mode 100644 lib/Twig/Function/Method.php create mode 100644 lib/Twig/FunctionInterface.php delete mode 100644 test/Twig/Tests/Fixtures/expressions/function.test delete mode 100644 test/Twig/Tests/Fixtures/globals.test create mode 100644 test/Twig/Tests/Fixtures/tags/from.test diff --git a/lib/Twig/Environment.php b/lib/Twig/Environment.php index d561ee5..79ccfe1 100644 --- a/lib/Twig/Environment.php +++ b/lib/Twig/Environment.php @@ -27,6 +27,7 @@ class Twig_Environment protected $visitors; protected $filters; protected $tests; + protected $functions; protected $globals; protected $runtimeInitialized; protected $loadedTemplates; @@ -455,6 +456,43 @@ class Twig_Environment return $this->tests; } + public function addFunction($name, Twig_Function $function) + { + if (null === $this->functions) { + $this->loadFunctions(); + } + $this->functions[$name] = $function; + } + + /** + * Get a function by name + * + * Subclasses may override getFunction($name) and load functions differently; + * so no list of functions is available. + * + * @param string $name function name + * @return Twig_Function|null A Twig_Function instance or null if the function does not exists + */ + public function getFunction($name) + { + if (null === $this->functions) { + $this->loadFunctions(); + } + + if (isset($this->functions[$name])) { + return $this->functions[$name]; + } + + return null; + } + + protected function loadFunctions() { + $this->functions = array(); + foreach ($this->getExtensions() as $extension) { + $this->functions = array_merge($this->functions, $extension->getFunctions()); + } + } + public function addGlobal($name, $value) { if (null === $this->globals) { diff --git a/lib/Twig/Extension.php b/lib/Twig/Extension.php index 41832e2..7690984 100644 --- a/lib/Twig/Extension.php +++ b/lib/Twig/Extension.php @@ -60,6 +60,16 @@ abstract class Twig_Extension implements Twig_ExtensionInterface { return array(); } + + /** + * Returns a list of functions to add to the existing list. + * + * @return array An array of functions + */ + public function getFunctions() + { + return array(); + } /** * Returns a list of operators to add to the existing list. diff --git a/lib/Twig/Extension/Core.php b/lib/Twig/Extension/Core.php index 393e417..b933344 100644 --- a/lib/Twig/Extension/Core.php +++ b/lib/Twig/Extension/Core.php @@ -87,12 +87,12 @@ class Twig_Extension_Core extends Twig_Extension * * @return array An array of global functions */ - public function getGlobals() + public function getFunctions() { return array( - 'fn_range' => new Twig_Function($this, 'getRange'), - 'fn_constant' => new Twig_Function($this, 'getConstant'), - 'fn_cycle' => new Twig_Function($this, 'getCycle'), + 'range' => new Twig_Function_Method($this, 'getRange'), + 'constant' => new Twig_Function_Method($this, 'getConstant'), + 'cycle' => new Twig_Function_Method($this, 'getCycle'), ); } diff --git a/lib/Twig/ExtensionInterface.php b/lib/Twig/ExtensionInterface.php index d430b95..19df6e9 100644 --- a/lib/Twig/ExtensionInterface.php +++ b/lib/Twig/ExtensionInterface.php @@ -55,6 +55,13 @@ interface Twig_ExtensionInterface public function getTests(); /** + * Returns a list of functions to add to the existing list. + * + * @return array An array of functions + */ + public function getFunctions(); + + /** * Returns a list of operators to add to the existing list. * * @return array An array of operators diff --git a/lib/Twig/Function.php b/lib/Twig/Function.php index dd29601..9fcb655 100644 --- a/lib/Twig/Function.php +++ b/lib/Twig/Function.php @@ -10,29 +10,37 @@ */ /** - * Defines a new Twig function. + * Represents a template function. * * @package twig * @author Fabien Potencier */ -class Twig_Function extends Exception +abstract class Twig_Function implements Twig_FunctionInterface { - protected $object; - protected $method; + protected $options; - public function __construct($object, $method) + public function __construct(array $options = array()) { - $this->object = $object; - $this->method = $method; + $this->options = array_merge(array( + 'needs_environment' => false, + ), $options); } - public function getObject() + public function needsEnvironment() { - return $this->object; + return $this->options['needs_environment']; } - public function getMethod() + public function getSafe(Twig_Node $functionArgs) { - return $this->method; + if (isset($this->options['is_safe'])) { + return $this->options['is_safe']; + } + + if (isset($this->options['is_safe_callback'])) { + return call_user_func($this->options['is_safe_callback'], $functionArgs); + } + + return array(); } } diff --git a/lib/Twig/Function/Function.php b/lib/Twig/Function/Function.php new file mode 100644 index 0000000..3237d8c --- /dev/null +++ b/lib/Twig/Function/Function.php @@ -0,0 +1,34 @@ + + */ +class Twig_Function_Function extends Twig_Function +{ + protected $function; + + public function __construct($function, array $options = array()) + { + parent::__construct($options); + + $this->function = $function; + } + + public function compile() + { + return $this->function; + } +} diff --git a/lib/Twig/Function/Method.php b/lib/Twig/Function/Method.php new file mode 100644 index 0000000..9cecb51 --- /dev/null +++ b/lib/Twig/Function/Method.php @@ -0,0 +1,35 @@ + + */ +class Twig_Function_Method extends Twig_Filter +{ + protected $extension, $method; + + public function __construct(Twig_ExtensionInterface $extension, $method, array $options = array()) + { + parent::__construct($options); + + $this->extension = $extension; + $this->method = $method; + } + + public function compile() + { + return sprintf('$this->env->getExtension(\'%s\')->%s', $this->extension->getName(), $this->method); + } +} diff --git a/lib/Twig/FunctionInterface.php b/lib/Twig/FunctionInterface.php new file mode 100644 index 0000000..ee4f489 --- /dev/null +++ b/lib/Twig/FunctionInterface.php @@ -0,0 +1,24 @@ + + */ +interface Twig_FunctionInterface +{ + public function compile(); + public function needsEnvironment(); + public function getSafe(Twig_Node $filterArgs); +} diff --git a/lib/Twig/Node/Expression/Function.php b/lib/Twig/Node/Expression/Function.php index 6b5c131..e773d08 100644 --- a/lib/Twig/Node/Expression/Function.php +++ b/lib/Twig/Node/Expression/Function.php @@ -17,22 +17,23 @@ class Twig_Node_Expression_Function extends Twig_Node_Expression public function compile($compiler) { - // functions must be prefixed with fn_ - $this->getNode('name')->setAttribute('name', 'fn_'.$this->getNode('name')->getAttribute('name')); + $function = $compiler->getEnvironment()->getFunction($this->getNode('name')->getAttribute('name')); + if (!$function) { + throw new Twig_Error_Syntax(sprintf('The function "%s" does not exist', $this->getNode('name')->getAttribute('name')), $this->getLine()); + } - $compiler - ->raw('$this->callFunction($context, ') - ->subcompile($this->getNode('name')) - ->raw(', array(') - ; + $compiler->raw($function->compile().($function->needsEnvironment() ? '($this->env, ' : '(')); + $first = true; foreach ($this->getNode('arguments') as $node) { - $compiler - ->subcompile($node) - ->raw(', ') - ; + if (!$first) { + $compiler->raw(', '); + } else { + $first = false; + } + $compiler->subcompile($node); } - $compiler->raw('))'); + $compiler->raw(')'); } } diff --git a/test/Twig/Tests/Fixtures/expressions/function.test b/test/Twig/Tests/Fixtures/expressions/function.test deleted file mode 100644 index 4175f9c..0000000 --- a/test/Twig/Tests/Fixtures/expressions/function.test +++ /dev/null @@ -1,13 +0,0 @@ ---TEST-- -Twig supports calling variables ---TEMPLATE-- -{{ lower('FOO') }} -{{ lower1('FOO') }} ---DATA-- -return array( - 'foo' => new Foo(), - 'fn_lower' => new Twig_Function('foo', 'strToLower'), - 'fn_lower1' => new Twig_Function(new Foo(), 'strToLower')) ---EXPECT-- -foo -foo diff --git a/test/Twig/Tests/Fixtures/globals.test b/test/Twig/Tests/Fixtures/globals.test deleted file mode 100644 index e816ca6..0000000 --- a/test/Twig/Tests/Fixtures/globals.test +++ /dev/null @@ -1,25 +0,0 @@ ---TEST-- -global variables ---TEMPLATE-- -{{ foo() }} -{{ bar() }} -{% include "included.twig" %} -{% from "included.twig" import foobar %} -{{ foobar() }} ---TEMPLATE(included.twig)-- -{% macro foobar() %} -{{ foo() }} - -{% endmacro %} -{{ foo() }} - ---DATA-- -$twig->addGlobal('fn_foo', new Twig_Function(new Foo(), 'getFoo')); -$twig->addGlobal('fn_bar', new Twig_Function('barObj', 'getFoo')); -return array('barObj' => new Foo()); ---EXPECT-- -foo -foo - -foo -foo diff --git a/test/Twig/Tests/Fixtures/tags/from.test b/test/Twig/Tests/Fixtures/tags/from.test new file mode 100644 index 0000000..5f5da0e --- /dev/null +++ b/test/Twig/Tests/Fixtures/tags/from.test @@ -0,0 +1,14 @@ +--TEST-- +global variables +--TEMPLATE-- +{% include "included.twig" %} +{% from "included.twig" import foobar %} +{{ foobar() }} +--TEMPLATE(included.twig)-- +{% macro foobar() %} +called foobar +{% endmacro %} +--DATA-- +return array(); +--EXPECT-- +called foobar diff --git a/test/Twig/Tests/Fixtures/tags/include/only.test b/test/Twig/Tests/Fixtures/tags/include/only.test index c6e6b23..22e3d0f 100644 --- a/test/Twig/Tests/Fixtures/tags/include/only.test +++ b/test/Twig/Tests/Fixtures/tags/include/only.test @@ -10,7 +10,7 @@ --DATA-- return array('foo' => 'bar') --EXPECT-- -fn_range,fn_constant,fn_cycle,foo,_parent, -fn_range,fn_constant,fn_cycle,_parent, -fn_range,fn_constant,fn_cycle,foo,foo1,_parent, -fn_range,fn_constant,fn_cycle,foo1,_parent, +foo,_parent, +_parent, +foo,foo1,_parent, +foo1,_parent, diff --git a/test/Twig/Tests/integrationTest.php b/test/Twig/Tests/integrationTest.php index f2cb36a..63d49ea 100644 --- a/test/Twig/Tests/integrationTest.php +++ b/test/Twig/Tests/integrationTest.php @@ -135,6 +135,14 @@ class TestExtension extends Twig_Extension ); } + public function getFunctions() + { + return array( + 'safe_br' => new Twig_Function_Method($this, 'br', array('is_safe' => array('html'))), + 'unsafe_br' => new Twig_Function_Method($this, 'br'), + ); + } + /** * nl2br which also escapes, for testing escaper filters */ @@ -158,6 +166,11 @@ class TestExtension extends Twig_Extension return strtoupper($value); } + public function br() + { + return '
'; + } + public function getName() { return 'test'; -- 1.7.2.5