I'm trying to execute a closure that resides inside an array on a Twig template. Below you could find a simplified snippet of which I'm trying:
//Symfony controller
...
$funcs = array(
"conditional" => function($obj){
return $obj->getFoo() === $obj::TRUE_FOO
}
);
$this->render('template_name', array('funcs' => $funcs));
{# Twig template #}
{# obj var is set #}
...
{% if funcs.conditional(obj)%}
<p>Got it</p>
{% endif %}
When Twig renders the template, throws an exception complaining about an Array to string conversion
An exception has been thrown during the rendering of a template ("Notice: Array to string conversion") in "template_name.html.twig".
500 Internal Server Error - Twig_Error_Runtime
1 linked Exception: ContextErrorException »
I will appreciate your help.
Thanks!
You cannot directly execute a closure inside your Twig template. However, if you need to call some PHP inside your template, you should use create a Twig Extension and include your logic inside.
Twig does not allow to do this directly. You can either add a simple function to Twig to handle the execution of closures or wrap your closure in a class to be able to use the attribute function of Twig (since directly calling attribute(_context, 'myclosure', args)
will trigger a Fatal Error as Twig will return the closure directly and ignore the given arguments since _context
is an array).
A simple Twig extension that achieves this purpose would look like this for Symfony 2.8+. (For Symfony 4, see the new documentation)
// src/AppBundle/Twig/Extension/CoreExtensions.php
namespace AppBundle\Twig\Extension;
class CoreExtensions extends \Twig_Extension
{
public function getFunctions()
{
return [
new \Twig_SimpleFunction('execute', [$this, 'executeClosure'])
];
}
public function executeClosure(\Closure $closure, $arguments)
{
return $closure(...$arguments);
}
public function getName()
{
return 'core_extensions_twig_extension';
}
}
Then, in your templates, you simply have to call execute:
{{ execute(closure, [argument1, argument2]) }}
Without extending Twig, one way to get around this problem is to use a class that acts as a wrapper for your closure and to use the attribute
function of Twig as it can be used to call a method of an object.
// src/AppBundle/Twig/ClosureWrapper.php
namespace AppBundle\Twig;
/**
* Wrapper to get around the issue of not being able to use closures in Twig
* Since it is possible to call a method of a given object in Twig via "attribute",
* the only purpose of this class is to store the closure and give a method to execute it
*/
class ClosureWrapper
{
private $closure;
public function __construct($closure)
{
$this->closure = $closure;
}
public function execute()
{
return ($this->closure)(...func_get_args());
}
}
Then, you simply need to give a ClosureWrapper instance to your template when rendering instead of the closure itself:
use AppBundle\Twig\ClosureWrapper;
class MyController extends Controller
{
public function myAction()
{
$localValue = 2;
$closure = new ClosureWrapper(function($param1, $param2) use ($localValue) {
return $localValue + $param1 + $param2;
});
return $this->render('mytemplate.html.twig', ['closure' => $closure]);
}
...
Eventually, in your template, you need to use attribute
to execute the closure you defined in your controller:
// Displays 12
{{ attribute(closure, 'execute', [4, 6]) }}
However, this is a bit redundant as, internally, the attribute
function of Twig unpacks the given arguments as well. By using the above code, for each call, arguments are successively unpacked, packed and unpacked again.
If you are using a closure you can use the the call method of the closure
http://php.net/manual/en/closure.call.php
You will end up with something like this
{{ funcs.conditional.call(obj, obj) }}
Since the first parameter must be the object that 'this' will refer too I am passing the same object as the first parameter.
No twig extension and no extra PHP code to do ;)
来源:https://stackoverflow.com/questions/34944887/executing-closure-on-twig