allowed filters/functions/tests implementation to use a different class than the extension they belong to

This commit is contained in:
Fabien Potencier
2015-11-09 22:37:11 +01:00
parent c8e96e8c89
commit 9cb6860e53
6 changed files with 253 additions and 24 deletions
+1
View File
@@ -1,5 +1,6 @@
* 1.26.0 (2016-XX-XX)
* allowed filters/functions/tests implementation to use a different class than the extension they belong to
* deprecated Twig_ExtensionInterface::getName()
* 1.25.0 (2016-09-21)
+103 -6
View File
@@ -136,7 +136,13 @@ Creating a filter is as simple as associating a name with a PHP callable::
// or a simple PHP function
$filter = new Twig_SimpleFilter('rot13', 'str_rot13');
// or a class static method
$filter = new Twig_SimpleFilter('rot13', array('SomeClass', 'rot13Filter'));
$filter = new Twig_SimpleFilter('rot13', 'SomeClass::rot13Filter');
// or a class method
$filter = new Twig_SimpleFilter('rot13', array($this, 'rot13Filter'));
// the one below needs a runtime implementation (see below for more information)
$filter = new Twig_SimpleFilter('rot13', array('SomeClass', 'rot13Filter'));
The first argument passed to the ``Twig_SimpleFilter`` constructor is the name
@@ -525,10 +531,6 @@ 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.
@@ -646,7 +648,7 @@ main ``Environment`` object::
.. tip::
The bundled extensions are great examples of how extensions work.
The Twig core extensions are great examples of how extensions work.
Globals
~~~~~~~
@@ -765,6 +767,101 @@ The ``getTests()`` method lets you add new test functions::
// ...
}
Definition vs Runtime
~~~~~~~~~~~~~~~~~~~~~
Twig filters, functions, and tests runtime implementations can be defined as
any valid PHP callable:
* **functions/static methods**: Simple to implement and fast (used by all Twig
core extensions); but it is hard for the runtime to depend on external
objects;
* **closures**: Simple to implement;
* **object methods**: More flexible and required if your runtime code depends
on external objects.
The simplest way to use methods is to define them on the extension itself::
class Project_Twig_Extension extends Twig_Extension
{
private $rot13Provider;
public function __construct($rot13Provider)
{
$this->rot13Provider = $rot13Provider;
}
public function getFunctions()
{
return array(
new Twig_SimpleFunction('rot13', array($this, 'rot13')),
);
}
public function rot13($value)
{
return $rot13Provider->rot13($value);
}
}
This is very convenient but not recommended as it makes template compilation
depend on runtime dependencies even if they are not needed (think for instance
as a dependency that connects to a database engine).
As of Twig 1.26, you can easily decouple the extension definitions from their
runtime implementations by registering a ``Twig_RuntimeLoaderInterface``
instance on the environment that knows how to instantiate such runtime classes
(runtime classes must be autoload-able)::
class RuntimeLoader implements Twig_RuntimeLoaderInterface
{
public function load($class)
{
// implement the logic to create an instance of $class
// and inject its dependencies
// most of the time, it means using your dependency injection container
if ('Project_Twig_RuntimeExtension' === $class) {
return new $class(new Rot13Provider());
} else {
// ...
}
}
}
$twig->addRuntimeLoader(new RuntimeLoader());
It is now possible to move the runtime logic to a new
``Project_Twig_RuntimeExtension`` class and use it directly in the extension::
class Project_Twig_RuntimeExtension extends Twig_Extension
{
private $rot13Provider;
public function __construct($rot13Provider)
{
$this->rot13Provider = $rot13Provider;
}
public function rot13($value)
{
return $rot13Provider->rot13($value);
}
}
class Project_Twig_Extension extends Twig_Extension
{
public function getFunctions()
{
return array(
new Twig_SimpleFunction('rot13', array('Project_Twig_RuntimeExtension', 'rot13')),
// or
new Twig_SimpleFunction('rot13', 'Project_Twig_RuntimeExtension::rot13'),
);
}
}
Overloading
-----------
@@ -792,7 +889,7 @@ possible** (order matters)::
Here, we have overloaded the built-in ``date`` filter with a custom one.
If you do the same on the Twig_Environment itself, beware that it takes
If you do the same on the ``Twig_Environment`` itself, beware that it takes
precedence over any other registered extensions::
$twig = new Twig_Environment($loader);
+32 -1
View File
@@ -49,7 +49,8 @@ class Twig_Environment
private $bcWriteCacheFile = false;
private $bcGetCacheFilename = false;
private $lastModifiedExtension = 0;
private $legacyExtensionNames = array();
private $runtimeLoaders = array();
private $runtimes = array();
/**
* Constructor.
@@ -786,6 +787,14 @@ class Twig_Environment
return isset($this->extensions[ltrim($class, '\\')]);
}
/**
* Adds a runtime loader.
*/
public function addRuntimeLoader(Twig_RuntimeLoaderInterface $loader)
{
$this->runtimeLoaders[] = $loader;
}
/**
* Gets an extension by class name.
*
@@ -809,6 +818,28 @@ class Twig_Environment
return $this->extensions[$class];
}
/**
* Returns the runtime implementation of a Twig element (filter/function/test).
*
* @param string $class A runtime class name
*
* @return object The runtime implementation
*/
public function getRuntime($class)
{
if (isset($this->runtimes[$class])) {
return $this->runtimes[$class];
}
foreach ($this->runtimeLoaders as $loader) {
if (null !== $runtime = $loader->load($class)) {
return $this->runtimes[$class] = $runtime;
}
}
throw new \Exception(sprintf('Unable to load the "%s" runtime.', $class));
}
/**
* Registers an extension.
*
+40 -17
View File
@@ -10,18 +10,29 @@
*/
abstract class Twig_Node_Expression_Call extends Twig_Node_Expression
{
private $reflector;
protected function compileCallable(Twig_Compiler $compiler)
{
$closingParenthesis = false;
if ($this->hasAttribute('callable') && $callable = $this->getAttribute('callable')) {
if (is_string($callable)) {
if (is_string($callable) && false === strpos($callable, '::')) {
$compiler->raw($callable);
} elseif (is_array($callable) && $callable[0] instanceof Twig_ExtensionInterface) {
$compiler->raw(sprintf('$this->env->getExtension(\'%s\')->%s', get_class($callable[0]), $callable[1]));
} else {
$type = ucfirst($this->getAttribute('type'));
$compiler->raw(sprintf('call_user_func_array($this->env->get%s(\'%s\')->getCallable(), array', $type, $this->getAttribute('name')));
$closingParenthesis = true;
list($r, $callable) = $this->reflectCallable($callable);
if ($r instanceof ReflectionMethod && is_string($callable[0])) {
if ($r->isStatic()) {
$compiler->raw(sprintf('%s::%s', $callable[0], $callable[1]));
} else {
$compiler->raw(sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1]));
}
} elseif ($r instanceof ReflectionMethod && $callable[0] instanceof Twig_ExtensionInterface) {
$compiler->raw(sprintf('$this->env->getExtension(\'%s\')->%s', get_class($callable[0]), $callable[1]));
} else {
$type = ucfirst($this->getAttribute('type'));
$compiler->raw(sprintf('call_user_func_array($this->env->get%s(\'%s\')->getCallable(), array', $type, $this->getAttribute('name')));
$closingParenthesis = true;
}
}
} else {
$compiler->raw($this->getAttribute('thing')->compile());
@@ -121,7 +132,6 @@ abstract class Twig_Node_Expression_Call extends Twig_Node_Expression
throw new LogicException($message);
}
// manage named arguments
$callableParameters = $this->getCallableParameters($callable, $isVariadic);
$arguments = array();
$names = array();
@@ -208,16 +218,7 @@ abstract class Twig_Node_Expression_Call extends Twig_Node_Expression
private function getCallableParameters($callable, $isVariadic)
{
if (is_array($callable)) {
$r = new ReflectionMethod($callable[0], $callable[1]);
} elseif (is_object($callable) && !$callable instanceof Closure) {
$r = new ReflectionObject($callable);
$r = $r->getMethod('__invoke');
} elseif (is_string($callable) && false !== strpos($callable, '::')) {
$r = new ReflectionMethod($callable);
} else {
$r = new ReflectionFunction($callable);
}
list($r, $_) = $this->reflectCallable($callable);
$parameters = $r->getParameters();
if ($this->hasNode('node')) {
@@ -250,4 +251,26 @@ abstract class Twig_Node_Expression_Call extends Twig_Node_Expression
return $parameters;
}
private function reflectCallable($callable)
{
if (null !== $this->reflector) {
return $this->reflector;
}
if (is_array($callable)) {
$r = new ReflectionMethod($callable[0], $callable[1]);
} elseif (is_object($callable) && !$callable instanceof Closure) {
$r = new ReflectionObject($callable);
$r = $r->getMethod('__invoke');
$callable = array($callable, '__invoke');
} elseif (is_string($callable) && false !== $pos = strpos($callable, '::')) {
$r = new ReflectionMethod($callable);
$callable = array(substr($callable, 0, $pos), substr($callable, $pos + 2));
} else {
$r = new ReflectionFunction($callable);
}
return $this->reflector = array($r, $callable);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Creates runtime implementations for Twig elements (filters/functions/tests).
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface Twig_RuntimeLoaderInterface
{
/**
* Creates the runtime implementation of a Twig element (filter/function/test).
*
* @param string $class A runtime class
*
* @return object|null The runtime instance or null if the loader does not know how to create the runtime for this class
*/
public function load($class);
}
+50
View File
@@ -396,6 +396,32 @@ EOF
restore_error_handler();
}
public function testAddRuntimeLoader()
{
$runtimeLoader = $this->getMockBuilder('Twig_RuntimeLoaderInterface')->getMock();
$runtimeLoader->expects($this->any())->method('load')->will($this->returnValue(new Twig_Tests_EnvironmentTest_Runtime()));
$loader = new Twig_Loader_Array(array(
'func_array' => '{{ from_runtime_array("foo") }}',
'func_array_default' => '{{ from_runtime_array() }}',
'func_array_named_args' => '{{ from_runtime_array(name="foo") }}',
'func_string' => '{{ from_runtime_string("foo") }}',
'func_string_default' => '{{ from_runtime_string() }}',
'func_string_named_args' => '{{ from_runtime_string(name="foo") }}',
));
$twig = new Twig_Environment($loader);
$twig->addExtension(new Twig_Tests_EnvironmentTest_ExtensionWithoutRuntime());
$twig->addRuntimeLoader($runtimeLoader);
$this->assertEquals('foo', $twig->render('func_array'));
$this->assertEquals('bar', $twig->render('func_array_default'));
$this->assertEquals('foo', $twig->render('func_array_named_args'));
$this->assertEquals('foo', $twig->render('func_string'));
$this->assertEquals('bar', $twig->render('func_string_default'));
$this->assertEquals('foo', $twig->render('func_string_named_args'));
}
protected function getMockLoader($templateName, $templateContent)
{
$loader = $this->getMockBuilder('Twig_LoaderInterface')->getMock();
@@ -526,3 +552,27 @@ class Twig_Tests_EnvironmentTest_ExtensionWithoutDeprecationInitRuntime extends
{
}
}
class Twig_Tests_EnvironmentTest_ExtensionWithoutRuntime extends Twig_Extension
{
public function getFunctions()
{
return array(
new Twig_SimpleFunction('from_runtime_array', array('Twig_Tests_EnvironmentTest_Runtime', 'fromRuntime')),
new Twig_SimpleFunction('from_runtime_string', 'Twig_Tests_EnvironmentTest_Runtime::fromRuntime'),
);
}
public function getName()
{
return 'from_runtime';
}
}
class Twig_Tests_EnvironmentTest_Runtime
{
public function fromRuntime($name = 'bar')
{
return $name;
}
}