Available for Contract Work

Hire me to come and work for you.

Project Enquiries

Send us an enquiry now.

Symfony: Twig Tags – Under-the-hood Part 2

Twig Logo

In the last part of this tutorial we dove into the basics of Twig tags. We learned the basics of Twig templates and how template inheritance using the ‘extends’ and ‘include’ tags work, as well as the types of classes which control and expose the various functionalities of each tag – the TokenParser.

This time we’ll be looking at the process flow at the point when a Twig template is rendered to try and understand how tags are loaded and processed.

Whilst this is a sort of deep-dive, we by no means explore every aspect of the process – it’s really, really complex! We touch on the important aspects and highlight where and how tags are processed.

First of all let’s do a basic summarisation of where Twig sit’s in the Symfony request flow.

We know that when a request comes in from the client (your browser) it first hits our front controller (index.php) where the Symfony Kernel is booted and a Request object is created from the incoming HTTP request info. From there Symfony tries to match the incoming request with a Controller action. If successful, it will trigger a Controller action within our app which will return a Response object so that Symfony can finish the request and send an actual response back to the client. In most cases, in our controller we typically use Twig to render content we want to send to the client, which is used to set the content of a ‘Response’ and returned. It looks something like this:

Request flow

Twig template rendering sits between the controller action and the response being handed back to Symfony to send to the client, which considering the entire request flow is a somewhat minor part of it. However if you look into what happens, you’ll start to see that it is a pretty detailed and impressive underlying subsystem. Let’s look at it as a list of steps taken during the processing of a template as Symfony prepares the Response.

Step #1 Rendering a Template from the Controller:

A call to function AbstractController::renderView() is responsible for returning the final HTML content. During this call the Twig\Environment::render() is called and is passed two arguments, 1) The $view – relative path template i.e. index/index.html.twig and 2) An array of $parameters i.e. $context.

Step #2 Identifying and Preparing the Template:

Inside the render() function, Symfony uses the $view string to load or create a TemplateWrapper object (holds a Template object within) and calls render() on that. This function takes one argument, the array of $parameters from before. This argument is named $context in that functions definition.

Step #3 Loading the Template:

This is technically part of Step #2 as it happens during that step, however to highlight what happens and it’s importance I’ve put it here. This is where the magic happens. Inside the render() function of Twig\Environment line 280, our TemplateWrapper (referred to above) object is loaded via to a function call `$this->load($name)`. Inside this function what happens is that first the system checks if the given name parameter is already of type TemplateWrapper and if so returns it. If is it not (i.e it’s a path to our template) then a new TemplateWrapper is instantiated and returned. The constructor of TemplateWrapper takes two arguments, first $env of type Twig\Environment and second $template of type Twig\Template. It is here, at the point of creating the TemplateWrapper object that Symfony creates the Template object required as parameter #2. On line 312 (Environment.php) we see the following code:

				
					return new TemplateWrapper($this, $this->loadTemplate($this->getTemplateClass($name), $name));

				
			

The Environment::loadTemplate() method takes three parameters,1) a generated Template class name, 2) the template’s relative path/name in our file system and 3) an integer representing the current template if it is an embedded template. 

Basically the loadTemplate() method checks to see if the generated Template class has already been loaded returns it. if not, then after trying to load the template from cache without success, it  rebuilds the template and then caches it (Environment.php line 350-353). 

Step #4 Compiling a Template and Caching It:

Inside the Twig\Environment class between line 350 – 353 Symfony compiles the template, fetches it’s rendered content and caches it for quicker retrieval next time. The code which does that is:

				
					# $name i.e. index/index.html.twig

$source = $this->getLoader()->getSourceContext($name);
$content = $this->compileSource($source);
$this->cache->write($key, $content);
$this->cache->load($key);
				
			

In the above code snippet, the line which we want to pay special attention to is:

$content = $this->compileSource($source);

This one line is responsible for building and writing a unique Template class that represents the template rendered in our controller and, in doing so, processes all of the Twig tags and functions contained in our template. Let’s see how:

  1. Firstly Twig tries to load an object oriented representation of our template which is represented by the class Twig\Template. If it doesn’t exist the Symfony creates one. These are special classes which extend Twig\Template created at runtime and look something like this:
    class __TwigTemplate_a99848ca117b5a20ccadec5b2379e049 extends Template {...}
  2. The method Twig\Environment::compileSource() takes one argument: $source which is of type Twig\Source – a class that holds information about a non-compiled Twig template. With this, the first thing that the method does is call $this->tokenize($source) which is used to break the template Source code into ‘tokens’ and it returns this information in the form of a TokenStream object. During this operation a special class ExtensionSet is instantiated and method ExtensionSet::initExtensions() is called. Halfway down on line 453 this then iterates through all required/available TokenParsers and stores them in a variable for later use.
    foreach ($extension->getTokenParsers() as $parser) {
    if (!$parser instanceof TokenParserInterface) {
    throw new \LogicException(
                'getTokenParsers() must return an array of \Twig\TokenParser\TokenParserInterface.'
    );
    }

             $this->parsers[$parser->getTag()] = $parser;
    }

    The tokenize() method finally returns a TokenStream object.

  3. Next the TokenStream object is then passed into the method Environment::parse() which hands that off to Twig\Parser::parse() method. On a high level this method converts a token stream to a  Twig\Node\ModuleNode object and then instantiates a new NodeTraverser which Symfony then calls its traverse($node) method, passing the ModuleNode to it. The traversal here traverses each node and calls the registered Twig\NodeVisitor\NodeVisitorInterface visitors (see this for more info).
  4. Environment::compile() is called which after ensuring that a Twig\Compiler is available, calls the Compiler::parse($node) method on that class.

    Here we see nodes being iterated over and compiled (the compile() method of each Node class is called). For each Node the parse() method is called. There are several types of Node. For example TextNode and ForLoopNode.

    Note: These class extend Twig\Node\Node and/or implements \Countable.

  5. Finally, once a Template is available, the method display() is called upon that template. Since templates support inheritance and once template extends or includes other templates, this process is repeated for however many templates are in the hierarchy. The ultimate job of the method display() is to process content and send it to the output buffer.

    Once the template has been fully processed, the output is returned into the AbstractController::renderView() method which then sets the Response->content using the rendered content and returns it.

Step #5 Finalising the Response

The processed content is set on the Response and returned to the router. Symfony does several house-keeping jobs before sending the final content and response headers back to the client.