Search code examples
phpsymfonytemplatestwiglegacy

Is it possible to get a twig variable value in an extension


I'm modernizing a legacy app using Symfony2 components. I've been trying (and mostly failing) to replace the old php templates with twig ones.

The part I'm struggling with is : each subtemplate has its own class containing its own logic (Told you it's all about legacy).

So, I created a twig extension that calls the template class and then includes the sub template passing it the class defined variables (Here's the extension code).

e.g:

{% template "NavBlockTemplate" %}
  • Creates a new NavBlockTemplate instance.
    • calls getTemplateName to get the twig template file to include
    • calls getVariables to get the vars needed by the template
    • creates a Twig_Node_Include of said template with given vars

The sad part here is : each template can pass variables to it's subtemplate class constructor ...

So, what I'd need, but not sure it is even possible is something like :

{% template "NavBlockTemplate" with { 'varName': value, 'var_id': otherVar.id } 
  • Compiles with vars from Twig_Expression objects to php vars
  • Creates a new NavBlockTemplate instance with php compiles vars
    • calls getTemplateName to get the twig template file to include
    • calls getVariables to get the vars needed by the template
    • creates a Twig_Node_Include of said template with given vars

So, Is that possible ? Any tips on how to achieve that ?


Solution

  • Variable values cannot be accessed during the compilation of the template. They are not available yet. Twig has 2 distinct phases when you call render($name, $context):

    • first it compiles the template (if not yet available in the cache)
    • second it renders it.

    The 2 steps are easily seen by the implementation of Twig_Environment::render():

    public function render($name, array $context = array())
    {
        return $this->loadTemplate($name)->render($context);
    }
    

    Your custom tag needs to account for that. It will need to create a special node class which will get compiled into the logic you need. you can look at the way existing Twig tags are implemented.
    Even the class name you include can be accessed at compile time like you did. $expr->getAttribute('value') will work only if the expression is a constant expression, and you don't enforce it in your parser.

    On the other hand, using a tag in this case is probably not the best solution (while it is the most complex one). According to the semantic of Twig, a function would be better. This is precisely why Twig also introduced a include() function as it fits better. this is how it would look like.

    in the template:

    {{ include_legacy("NavBlockTemplate", { 'varName': value, 'var_id': otherVar.id }) }}
    

    in the extension:

    class LegacyIncludeExtension extends \TwigExtension
    {
    
        public function getFunctions()
        {
            return array(
                new \Twig_SimpleFunction(
                    'include_legacy',
                    array($this, 'includeLegacy'),
                    array('is_safe' => array('all'), 'needs_environment' => true, 'needs_context' => true)
                ),
            );
        }
    
        public function includeLegacy(\Twig_Environment $env, array $context, $name, array $variables = array())
        {
            $fqcn = // determine the class name
    
            $instance = new fqcn();
    
            $template = $instance->getTemplateName();
            $variables = array_merge($instance->getVariables(), $variables);
    
            return $env->resolveTemplate($template)->render(array_merge($context, $variables));
        }
    }
    

    The last line of the method performs the main work of twig_include. If you need support for isolating the context, it is quite easy (make the array merge of the template conditional). Support for ignore_missing is more work, and you would be better at calling twig_include directly in such case:

        public function includeLegacy(\Twig_Environment $env, array $context, $name, array $variables = array(), $withContext = true, $ignoreMissing = false)
        {
            $fqcn = // determine the class name
    
            $instance = new fqcn();
    
            $template = $instance->getTemplateName();
            $variables = array_merge($instance->getVariables(), $variables)
    
            return twig_include($env, $context, $template, $variables, $withContext, $ignoreMissing);
        }