0

I have the following class method for creating a Twig environment object.

public function getView($filename, 
                        array $customFunctions = null, 
                        array $customFunctionArgs = null, 
                        $debug = false) {

  $loader = new \Twig_Loader_Filesystem('/App/Views/Templates/Main');
  $twig = new \Twig_Environment($loader);
  if (isset($customFunctions)) {
    foreach ($customFunctions as $customFunction) {
      $customFunction['name'] = new \Twig_SimpleFunction($customFunction['name'], 
      function ($customFunctionArgs) {
        return $customFunction['method']($customFunctionArgs);
      });
      $twig->addFunction($customFunction['name']);
    }
  }
   // Check debugging option
   if ($debug == true && !$twig->isDebug()) {
     $twig->enableDebug();
     $twig->addExtension(new \Twig_Extension_Debug());
   } elseif (!$debug && $twig->isDebug()) {
        $twig->disableDebug();
   }

   $template = $twig->load($filename);

   return $template;
}

Problem is, I don't understand how to pass values in order to make this work dynamically and keep all the objects in context and scope. For instance, here is how I'm trying to use it but can't pass the variables as a reference I guess?

$customFunctions = ['name' => 'customFunctionName', 
                    'method' => $Class->method($arg)];
$customFunctionArgs = [$arg];
$template = $View->getView('template.html.twig', $customFunctions, $customFunctionArgs, true);

My environment is PHP 5.6 & Twig 1.35.0. I suppose this is not a Twig specific question per se, but more of how to use class objects within other classes/methods.

1
  • what happens? what is the problem? I think $customFunctions should be an array of arrays, based from your implementation. Commented Mar 14, 2018 at 5:04

2 Answers 2

1

Félix Gagnon-Grenier's answer helped me find a solution to this problem. However, I feel the need to post an answer with all the missing pieces to the puzzle for anyone that needs a solution for this.

I believe it will make more sense if I start at the end and explain to the beginning. When creating your array, there are several things to consider.

  1. Any class objects that are needed for the function have to be declared inside a use() with the closure.
  2. Any arguments for the custom function must be declared as a function parameter for the closure. This will allow you to declare them later.
  3. I ended up adding a sub-array with the arguments I needed for each custom function, that way I don't need to iterate over them separately.

    $customFunctions = [
        [
           'name' => 'customFunction',
           'method' => function($arg1, $arg2) use($Class) { 
                       return $Class->customFunction($arg1, $arg2); 
                                                          },
           'arguments' =>
                 [
                     'arg1', 'arg2'
                 ]
        ]
    ];
    $template = $View->getView(
                               'template.html.twig', 
                                true, 
                                $customFunctions
                              );
    echo $View->renderView($template);
    

Based on this code (reflective of question above), I had to make some notable modifications.

if (isset($customFunctions)) {
    foreach ($customFunctions as $index => $customFunction) {
        if (isset($customFunctions['arguments'])) {
            $arguments = $customFunctions['arguments'];
        } else {
            $arguments = [];
        }

        $twigFunction = new \Twig_SimpleFunction(
            $customFunction['name'], 
            function (...$arguments) use ($customFunction) {
                return $customFunction['method'](...$arguments);
            });
        $twig->addFunction($twigFunction);
    }
}

You can do this whatever way works for you, but there are important things to consider which I struggled with. Once again, your arguments MUST go into the function parameters. function (...$arguments) use ($customFunction). Your custom function will be passed in the use(). In order to actually pass the arguments in the closure, you must use ... to unpack them (as an array). This applies to PHP 5.6+. It allows the arguments to be dynamically expanded to the correct amount, otherwise you will get missing argument errors.

Sign up to request clarification or add additional context in comments.

Comments

0

There are slight flaws in how you construct the custom functions data array and the loop that injects them into the template.

  • The custom functions should be a three dimensional array

    $customFunctions = [
        [ // notice the extra level, allowing you to access the name
            'name' => 'customFunctionName',
            'method' => function() { return 'wat'; }
            // you need to pass a callable, not the result of a call
        ]
    ];
    

  • The scope is not inherited like you seem to think it is, you need to use() variables you intend to access. I personnally would not overwrite the 'name' value of the array, but that's uncanny paranoia of internal side effects, it seems to work in practice.

    if (isset($customFunctions)) {
        foreach ($customFunctions as $customFunction) {
            $customFunction['name'] = new \Twig_SimpleFunction(
                $customFunction['name'],
                function () use ($customFunctionArgs, $customFunction) {
                    return $customFunction['method']($customFunctionArgs);
                });
            $twig->addFunction($customFunction['name']);
        }
    }
    

  • You might need to add looping over $args so that the correct args are sent to the correct function (send $args[0] to $customFunctions[0] etc.).

    Note that this prevents you from sending a parameter into your custom function unless you add it in the loop:

    function ($templateArg) use ($customFunctionArgs, $customFunction) {
        return $customFunction['method']($customFunctionArgs, $templateArg);
    }
    

Here is a gist with tests if you're interested.

4 Comments

Thanks for your answer! Had a hunch I would need to use a callable, but most of these advanced OOP concepts are new to me. I'm not at liberty to test it at the moment but will follow up on this tomorrow and let you know how it went.
You really helped me out here, I was able to get it working! Although it wasn't clear that I needed 'method' => function() use($blah) { return 'wat'; } in the closure.
I got the functions working inside the twig template. But I am not able to understand how to pass the arguments properly. I do 'method' => function($arg1, $arg2) use($Class) { return $Class->classMethod($arg1, $arg2);. In the loop I do $arguments = implode(', ', $customFunctionArgs[$index]); then just use them. How do I pass the correct amount of params from the array? It doesn't recognize $arg2 because it only sees one argument passed.
Looks like I figured it out, I had to use function (...$customFunctionArgs) use ($customFunction) { return $customFunction['method'](...$customFunctionArgs); } to unpack.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.