Handle functions like filters
authorArnaud Le Blanc <arnaud.lb@gmail.com>
Fri, 24 Dec 2010 16:21:10 +0000 (17:21 +0100)
committerFabien Potencier <fabien.potencier@gmail.com>
Thu, 30 Dec 2010 08:31:08 +0000 (09:31 +0100)
Functions are not global variables anymore. They are resolved at
compile time, and Twig_Function objects are instanciated only when
compiling.

14 files changed:
lib/Twig/Environment.php
lib/Twig/Extension.php
lib/Twig/Extension/Core.php
lib/Twig/ExtensionInterface.php
lib/Twig/Function.php
lib/Twig/Function/Function.php [new file with mode: 0644]
lib/Twig/Function/Method.php [new file with mode: 0644]
lib/Twig/FunctionInterface.php [new file with mode: 0644]
lib/Twig/Node/Expression/Function.php
test/Twig/Tests/Fixtures/expressions/function.test [deleted file]
test/Twig/Tests/Fixtures/globals.test [deleted file]
test/Twig/Tests/Fixtures/tags/from.test [new file with mode: 0644]
test/Twig/Tests/Fixtures/tags/include/only.test
test/Twig/Tests/integrationTest.php

index d561ee5..79ccfe1 100644 (file)
@@ -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) {
index 41832e2..7690984 100644 (file)
@@ -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.
index 393e417..b933344 100644 (file)
@@ -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'),
         );
     }
 
index d430b95..19df6e9 100644 (file)
@@ -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
index dd29601..9fcb655 100644 (file)
  */
 
 /**
- * Defines a new Twig function.
+ * Represents a template function.
  *
  * @package    twig
  * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
  */
-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 (file)
index 0000000..3237d8c
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2010 Arnaud Le Blanc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a function template function.
+ *
+ * @package    twig
+ * @author     Arnaud Le Blanc <arnaud.lb@gmail.com>
+ */
+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 (file)
index 0000000..9cecb51
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2009 Fabien Potencier
+ * (c) 2010 Arnaud Le Blanc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Represents a method template function.
+ *
+ * @package    twig
+ * @author     Arnaud Le Blanc <arnaud.lb@gmail.com>
+ */
+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 (file)
index 0000000..ee4f489
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ * (c) 2010 Arnaud Le Blanc
+ *
+ * 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     Arnaud Le Blanc <arnaud.lb@gmail.com>
+ */
+interface Twig_FunctionInterface
+{
+    public function compile();
+    public function needsEnvironment();
+    public function getSafe(Twig_Node $filterArgs);
+}
index 6b5c131..e773d08 100644 (file)
@@ -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 (file)
index 4175f9c..0000000
+++ /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 (file)
index e816ca6..0000000
+++ /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 (file)
index 0000000..5f5da0e
--- /dev/null
@@ -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
index c6e6b23..22e3d0f 100644 (file)
@@ -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,
index f2cb36a..63d49ea 100644 (file)
@@ -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 '<br />';
+    }
+
     public function getName()
     {
         return 'test';