Twig

Twig internals

of

Alles over Twig

Twig

Armin Ronacher

Fabien Potencier

Over mij

Matthias Noback

Overzicht

Twig installeren

In composer.json:

{
    "require": {
        "twig/twig": "1.*"
    }
}
    

Demo

Voorbeeld: base.html.twig

<html>
    <body>

        <div class="sidebar">

            {{ block('sidebar') }}

        </div>

        <div class="content">

            {{ block('content') }}

        </div>

    </body>
</html>
    

Demo

Voorbeeld: users.html.twig

{% extends "base.html.twig" %}

{% block sidebar %}
    <p>Nothing to see here</p>
{% endblock %}

{% block content %}

    {% for user in users %}

        <h2>{{ user.name|capitalize }}</h2>

        {% include "profile.html.twig" with { 'profile' : user.profile }

    {% else %}

        <p>No items</p>

    {% endfor %}

{% endblock %}
    

Demo

Voorbeeld: dashboard.html.twig

{% extends "base.html.twig" %}

{# use the block "sidebar" from another template #}

{% use "sidebar.html.twig" %}

{% block content %}

    {% if logged_in_user is defined %}

        <p>Hi there, {{ logged_in_user }}</p>

    {% else %}

        <p>Log in please</p>

    {% endif %}

{% endblock %}
    

Twig Environment

$env = new \Twig_Environment();

echo $env->render('myTemplateFile.html.twig', array(
    'someVariable' => 'theValue'
);

// Twig: "uhm, wat is 'myTemplateFile.html.twig'?"

Een template laden

String loader

$stringLoader = new \Twig_Loader_String();

$env->setLoader($stringLoader);

echo $env->render('<p>Hi {{ name }}!</p>', array(
    'name' => '...'
));
    

Een template laden

Array loader

$arrayLoader = new \Twig_Loader_Array(array(
    'just_a_name' => '{{ name }} is in an array',
));

$env->setLoader($arrayLoader);

echo $env->render('just_a_name', array('name' => '...'));
    

Een template laden

Filesystem loader

$filesystemLoader = new \Twig_Loader_Filesystem(array(
    __DIR__.'/Resources/views',
    __DIR__.'/../Matthias/Demo/Resources/views',
    // ...
));

$env->setLoader($filesystemLoader);

echo $env->render('index.html.twig', array('name' => '...');
    

Een template laden

Chain loader

$chainLoader = new \Twig_Loader_Chain(array(
    $filesystemLoader,
    $arrayLoader,
    $stringLoader,
    // ...
));

$env = new \Twig_Environment($chainLoader);

echo $env->render('do_your_best_to_find_it');
    

Een template laden

Eigen loader

$chainLoader->addLoader(new DatabaseLoader($connection));
    

Met Symfony2: maak een service met de tag "twig.loader".

interface Twig_LoaderInterface
{
    public function getSource($name);

    public function isFresh($name, $time);

    public function getCacheKey($name);
}
    

Een template laden

class Twig_Environment
{
    // ...

    public function render($name, array $context = array())
    {
        return $this->loadTemplate($name)->render($context);
    }
}
    
  1. Uit een array met eerder geladen templates (binnen dezelfde request)
  2. Uit een cache directory (Symfony2: /app/cache/{environment}/twig)
  3. Of
    1. Haal de template code op met behulp van de template loader
    2. Compileer de template code
    3. Bewaar de gecompileerde template class in de cache directory
    4. Laad dit bestand in
    5. Bewaar een instantie van de template class in de array cache

Een gecompileerde template

/* index.html.twig */
class __TwigTemplate_d1d2705938bfae31fd9839ce0fe15e96 extends Twig_Template
{
    // ...

    protected function doDisplay(array $context, array $blocks = array())
    {
        // line 1
        if (isset($context["name"])) { $_name_ = $context["name"]; } else { $_name_ = null; }
        echo twig_escape_filter($this->env, $_name_, "html", null, true);
        echo " is in a file
";
    }

    public function getTemplateName()
    {
        return "index.html.twig";
    }

    // ...
}
    

Template compileren

Lexer

  1. Zoekt naar bekende patronen in de input string ("lexemes")
  2. Bepaalt token types voor de herkende delen
  3. Geeft een stream van tokens terug

Template compileren

Lexer

Template compileren

Lexer

{% endif %}
<ul>
    {% for item in items %}
        <li>{{ item|capitalize }}</li>
    {% endfor %}
</ul>
    

"Ja maar..."

$lexer = $env->getLexer();

$template = ...;

$tokenStream = $lexer->tokenize($template);
    

Template compileren

Lexer

Token stream

echo $tokenStream;
    
BLOCK_START_TYPE(){%
NAME_TYPE(endif)
BLOCK_END_TYPE()%}
TEXT_TYPE(<ul>)template data
BLOCK_START_TYPE(){%
NAME_TYPE(for)
NAME_TYPE(item)
OPERATOR_TYPE(in)
NAME_TYPE(items)
BLOCK_END_TYPE()%}
TEXT_TYPE(<li>)template data
VAR_START_TYPE(){{
NAME_TYPE(item)
PUNCTUATION_TYPE(|)
NAME_TYPE(capitalize)
VAR_END_TYPE()}}
TEXT_TYPE(</li>)template data
BLOCK_START_TYPE(){%
NAME_TYPE(endfor)
BLOCK_END_TYPE()%}
TEXT_TYPE(</ul>)template data
EOF_TYPE()end of input

Template compileren

Lexer

Zoeken naar Twig blokken

De lexer zoekt naar de positie van deze markers:

Totdat het einde van de input string bereikt is

  1. loopt de lexer langs de gevonden posities, terwijl hij
  2. een aantal algemene syntax regels valideert en
  3. tokens verzamelt

Template compileren

Lexer

Token types

Tokens hebben een bepaald type, een waarde en een regelnummer

// token types zoals gedefinieerd in \Twig_Token
const EOF_TYPE                  = -1; // einde van de input string
const TEXT_TYPE                 = 0;  // template data, bv. HTML
const BLOCK_START_TYPE          = 1;  // {%
const VAR_START_TYPE            = 2;  // {{
const BLOCK_END_TYPE            = 3;  // %}
const VAR_END_TYPE              = 4;  // }}
const NAME_TYPE                 = 5;  // for, if, etc.
const NUMBER_TYPE               = 6;  // getal
const STRING_TYPE               = 7;  // '...' or "..."
const OPERATOR_TYPE             = 8;  // +, -, ~, etc.
const PUNCTUATION_TYPE          = 9;  // |, [, {, etc.
const INTERPOLATION_START_TYPE  = 10; // #{
const INTERPOLATION_END_TYPE    = 11; // }
    

Template compileren

Lexer

Syntax validatie

Template compileren

Lexer

Syntax validatie (vervolg)

Template compileren

Lexer

States

Om bij te houden in welke toestand hij verkeert, houdt de lexer een stack bij.

// defined in \Twig_Lexer
const STATE_DATA           = 0; // lexing data (start state)
const STATE_BLOCK          = 1; // lexing a block
const STATE_VAR            = 2; // lexing a variable
const STATE_STRING         = 3; // lexing a string
const STATE_INTERPOLATION  = 4; // lexing a string interpolation
    

Template compileren

Lexer

{% endif %}
<ul>
    {% for item in items %}
        <li>{{ item|capitalize }}</li>
    {% endfor %}
</ul>
    

Opeenvolgende states

STATE_DATAtemplate data
STATE_BLOCKbegin van endif blok
STATE_BLOCKeinde van endif blok
STATE_DATA<ul>
STATE_BLOCKbegin van for blok
STATE_BLOCKnaam: item
STATE_BLOCKnaam: in
STATE_BLOCKnaam: items
STATE_BLOCKeinde van for block
STATE_DATA<li>
STATE_VARbegin van variabele, naam: item
STATE_VARinterpunctie: |
STATE_VARnaam: capitalize
STATE_VAReinde van variabele
STATE_DATA</li>
STATE_BLOCKbegin van endfor blok
STATE_BLOCKeinde van endfor blok
STATE_DATA</ul>

Template compileren

Lexer

States in een stack

Push en pop: last in, first out

    • STATE_DATA
    • STATE_BLOCK
    • STATE_DATA
    • STATE_DATA
    • STATE_BLOCK
    • STATE_DATA
    • STATE_DATA
    • STATE_VAR
    • STATE_DATA
    • STATE_DATA
    • STATE_BLOCK
    • STATE_DATA
    • STATE_DATA
  1. [EOF]

Template compileren

Lexer

De verzameling tokens kan semantisch incorrect zijn,
d.w.z. de betekenis van de tokens kan onduidelijk zijn, of er klopt zelfs helemaal niets van.

{% endif %}
<ul>
    {% for item in items %}
        <li>{{ item|capitalize }}</li>
    {% endfor %}
</ul>
    
BLOCK_START_TYPE(){%
NAME_TYPE(endif)
BLOCK_END_TYPE()%}
...

De token stream parsen

De parser

Tokens worden daarbij

Nodes beschrijven ondubbelzinnig de betekenis van de originele input string.

De token stream parsen

Samenstellen van de Abstract Syntax Tree

<ul>
    {% for item in items %}
        <li>{{ item|capitalize }}</li>
    {% endfor %}
</ul>
    

"Hé, waar is..."

$template = ...;

$tokenStream = $lexer->tokenize($template);

$parser = $env->getParser();

$nodeTree = $parser->parse($tokenStream);

echo $nodeTree;

De token stream parsen

Stukje van de Abstract Syntax Tree

Twig_Node_Module(
  body: Twig_Node_Body(
    0: Twig_Node(
      0: Twig_Node_Text(data: '<ul>    ')
      1: Twig_Node(
        0: Twig_Node_SetTemp(name: 'items')
        1: Twig_Node_For(
          value_target: Twig_Node_Expression_AssignName(name: 'item')
          seq: Twig_Node_Expression_TempName(name: 'items')
          body: Twig_Node(
            0: Twig_Node(
              0: Twig_Node_Text(data: '        <li>')
              1: Twig_Node(
                0: Twig_Node_SetTemp(name: 'item')
                1: Twig_Node_Print(
                  expr: Twig_Node_Expression_Filter(
                    node: Twig_Node_Expression_Filter(
                      node: Twig_Node_Expression_TempName(name: 'item')
                      filter: Twig_Node_Expression_Constant(value: 'capitalize')
                    )
                    filter: Twig_Node_Expression_Constant(value: 'escape')
                  )
...
    

De token stream parsen

De module node

Het parsen resulteert in een \Twig_Node_Module. Die bevat

De token stream parsen

Token types

De \Twig_Parser::parse() method verzamelt nodes op basis van de token op de huidige positie in de token stream.

TEXT_TYPE template tekst Maak een \Twig_Node_Text met de waarde van de token
VAR_START_TYPE variabele parse de expressie die volgt en verwacht daarna een VAR_END_TYPE
BLOCK_START_TYPE block met een tag verwacht een naam (de naam van de tag, b.v. for, if, etc.) en roep een subparser aan

De token stream parsen

Een subparser is ook wel bekend als een "token parser"

Beetje verwarrende naam...

Je kunt daarmee zelf tags definiëren, met een eigen syntax, uiteindelijk afsloten met een BLOCK_END_TYPE token.

{% for item in items %}
   ...
{% endfor %}
    

De token stream parsen

Nodes teruggeven

De parse() method van een token parser moet of null of een instantie van \Twig_Node teruggeven

De nodes die worden teruggegeven worden toegevoegd aan de Abstract Syntax Tree

Twig bevat al een heleboel token parsers en nodes voor tags als

Je kunt het gedrag van deze ingebouwde tags eenvoudig aanpassen.

De token stream parsen

Een eigen token parser

class UsergroupTokenParser extends \Twig_TokenParser
{
    public function parse(\Twig_Token $token)
    {
        // immediately expect "end of block", no arguments
        $this->parser->getStream()->expect(\Twig_Token::BLOCK_END_TYPE);

        $expr = new \Twig_Node_Expression_Constant('Symfony User Group',
            $token->getLine());

        return new \Twig_Node_Print($expr, $token->getLine(),
            $this->getTag());
    }

    public function getTag()
    {
        return 'usergroup';
    }
}
    
{% usergroup %}
    

De token stream parsen

Een eigen token parser

There we go, {% usergroup %}!
    
$template = ...;

$env = new \Twig_Environment($loader);

$env->addTokenParser(new UsergroupTokenParser());

echo $env->parse($env->tokenize($template));
    

De token stream parsen

Stukje van de Abstract Syntax Tree

Twig_Node_Module(
  body: Twig_Node_Body(
    0: Twig_Node(
      0: Twig_Node_Text(data: 'There we go, ')
      1: Twig_Node_Print(
        expr: Twig_Node_Expression_Constant(value: 'Symfony User Group')
      )
      2: Twig_Node_Text(data: '!')
    )
  )
)

De token stream parsen

Expressies

{{ 5 + age * 4 }}
    
$template = ...;

$moduleNode = $env->parse($env->tokenize($template));

echo $moduleNode;
    

Voor expressies is er een expression parser.

De token stream parsen

Expressies

Twig_Node_Module(
  body: Twig_Node_Body(
    0: Twig_Node(
      0: Twig_Node_SetTemp(name: 'age')
      1: Twig_Node_Print(
        expr: Twig_Node_Expression_Filter(
          node: Twig_Node_Expression_Binary_Add(
            left: Twig_Node_Expression_Constant(value: 5)
            right: Twig_Node_Expression_Binary_Mul(
              left: Twig_Node_Expression_TempName(name: 'age')
              right: Twig_Node_Expression_Constant(value: 4)
            )
          )
          filter: Twig_Node_Expression_Constant(value: 'escape')
          arguments: Twig_Node(
            0: Twig_Node_Expression_Constant(value: 'html')
          )
        )
      )
    )
  )
)

De token stream parsen

Expressies

Associativiteit en prioriteit

De meeste operators zijn links-associatief, wat betekent dat a + b + c gelezen moet worden als

((a + b) + c)

en niet als

(a + (b + c))

Operators hebben een getal dat hun prioriteit aanduidt zodat a + b * c altijd wordt gezien als

(a + (b * c))

aangezien de "*" operator een hogere prioriteit heeft dan de "+" operator.

De token stream parsen

Node visitors

Nadat alle tokens zijn verwerkt en de nodes zijn gemaakt, lopen node visitors langs de Abstract Syntax Tree. Deze kunnen nodes toevoegen, bestaande nodes vervangen of verwijderen.

Fantastisch, maar ook gevaarlijk...

interface Twig_NodeVisitorInterface
{
    // called before child nodes are visited.
    public function enterNode(Twig_NodeInterface $node, Twig_Environment $env);

    // called after child nodes are visited.
    public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env);

    // returns the priority for this visitor.
    public function getPriority();
}
    

Zie voor een voorbeeld: Collecting Data Across Templates Using a Node Visitor

Template compileren

De hoofd-node is een \Twig_Node_Module. Deze node bevat alle nodes afkomstig van

De compiler hoeft alleen nog de compile() method aan te roepen van \Twig_Node_Module.

{% for item in items %}
    {{ item|capitalize }}<br />
{% endfor %}
    
$template = ...;

echo $env->compileSource($template);
    

Template compileren

Stukje uit een gecompileerde template

class __TwigTemplate_d41d8cd98f00b204e9800998ecf8427e extends Twig_Template
{
    protected function doDisplay(array $context, array $blocks = array())
    {
        // line 1
        if (isset($context["items"])) { $_items_ = $context["items"]; } else { $_items_ = null; }
        $context['_parent'] = (array) $context;
        $context['_seq'] = twig_ensure_traversable($_items_);
        foreach ($context['_seq'] as $context["_key"] => $context["item"]) {
            // line 2
            echo "    ";
            if (isset($context["item"])) { $_item_ = $context["item"]; } else { $_item_ = null; }
            echo twig_escape_filter($this->env, twig_capitalize_string_filter($this->env, $_item_), "html", null, true);
            echo "<br />
";
        }
        $_parent = $context['_parent'];
        unset($context['_seq'], $context['_iterated'], $context['_key'], $context['item'], $context['_parent'], $context['loop']);
        $context = array_merge($_parent, array_intersect_key($context, $_parent));
    }
}
    

Template compileren

UsergroupTokenParser

class UsergroupTokenParser extends \Twig_TokenParser
{
    public function parse(Twig_Token $token)
    {
        // create an instance of \Twig_Node_Expression
        $expr  = ...

        return new \Twig_Node_Print(
            $expr,
            $token->getLine(),
            $this->getTag()
        );
    }
}
    

Template compileren

Een print node compileren

class Twig_Node_Print
{
    public function compile(Twig_Compiler $compiler)
    {
        $compiler
            ->addDebugInfo($this)
            ->write('echo ')
            ->subcompile($this->getNode('expr'))
            ->raw(";\n")
        ;
    }
}
    

In de gecompileerde template:

protected function doDisplay(array $context, array $blocks = array())
{
    // line 1
    echo "There we go, ";
    echo "Symfony User Group"; // the expression was just a constant
    echo "!";
}
    

Template compileren

Twee conclusies:

  1. We kunnen willekeurig welke PHP code in een template stoppen
  2. We kunnen de zware berekeningen bij het compileren van de template doen

Het enige wat we nodig hebben is: een eigen node type met een compile() method.

Token parsers en nodes maken kan moeilijk zijn dus:

  1. bekijk de gecompileerde templates in de cache directory en
  2. schrijf unit tests voor de token parser en node types (extend van \Twig_Test_NodeTestCase)

Twig uitbreiden

Er zijn een aantal dramatische manieren om Twig aan te passen:

$options = array(
    'tag_comment'     => array('{#', '#}'),
    'tag_block'       => array('{%', '%}'),
    'tag_variable'    => array('{{', '}}'),
    'whitespace_trim' => '-',
    'interpolation'   => array('#{', '}')
);

$lexer = new \Twig_Lexer($env, $options);

$env->setLexer($lexer);
    

Twig uitbreiden

Extensies

interface Twig_ExtensionInterface
{
    public function initRuntime(Twig_Environment $environment);

    public function getTokenParsers();

    public function getNodeVisitors();

    public function getFilters();

    public function getTests();

    public function getFunctions();

    public function getOperators();

    public function getGlobals();

    public function getName();
}
    
$env->addExtension($extension) // is the way
    
Symfony2: maak een service met een tag "twig.extension"

Twig uitbreiden

Functies

class MyExtension extends \Twig_Extension
{
    public function getFunctions()
    {
        return array(
            new \Twig_SimpleFunction('myFunction',
                array($this, 'myFunctionMethod'),
                array(
                    // 'needs_environment' => true,
                    // 'needs_context' => true,
                    'is_safe' => array('html')
                )
            )
        );
    }

    public function myFunctionMethod($thing)
    {
        return sprintf('This is <b>my</b> %s.', $thing);
    }
}
    

Twig uitbreiden

Functies

echo $env->render('{{ myFunction("<strong>computer</strong>") }}');
    

Toont...

This is <b>my</b> <strong>computer</strong>.
    

Twig uitbreiden

Filters

class MyExtension extends \Twig_Extension
{
    public function getFilters()
    {
        return array(
            new \Twig_SimpleFilter('mine',
                array($this, 'myFilterMethod'),
                array(
                    // 'needs_environment' => true,
                    // 'needs_context' => true,
                    'is_safe' => array('html'),
                    'pre_escape' => 'html'
                )
            )
        );
    }

    public function myFilterMethod($what, $mine = true)
    {
        return sprintf('%s (which %s mine)', $what, $mine ? 'is':'is not');
    }
}
    

Twig uitbreiden

Filters

echo $env->render('{{ thing|mine(false) }}', array(
    'thing' => '<strong>Life</strong>'
));
    

Toont...

&lt;strong&gt;Life&lt;/strong&gt; (which is not mine)
    

Twig uitbreiden

Dynamische filters

public function getFilters()
{
    return array(
        new \Twig_SimpleFilter('*_of_*', function($type, $owner, $what) {
            return sprintf('%s is a %s of %s', $what, $type, $owner);
        })
    );
}
    

Twig uitbreiden

Dynamische filters

echo $env->render('{{ "This game"|call_of_duty() }}');
    

Toont...

This game is a call of duty
    

Twig uitbreiden

Tests

class MyExtension extends \Twig_Extension
{
    public function getTests()
    {
        return array(
            new \Twig_SimpleTest('usergroup', function($name) {
                return $name === 'Symfony User Group';
            })
        );
    }
}
    

Twig uitbreiden

Tests

echo $env->render('{% if "Symfony User Group" is usergroup %}I told you so{% endif %}');
    

Toont...

I told you so
    

Twig uitbreiden

Operators

class MyExtension extends \Twig_Extension
{
    public function getOperators()
    {
        return array(
            array(
                'no' => array(
                    'precedence' => 50,
                    'class' => 'Twig_Node_Expression_Unary_Not'
                )
            ),
            array(
                'maybe' => array(
                    'precedence' => 10,
                    'class' => 'Twig_Node_Expression_Binary_Or',
                    'associativity' => \Twig_ExpressionParser::OPERATOR_LEFT)
                )
            )
        );
    }
}
    

Twig uitbreiden

Operators

echo $env->render('{% if no true maybe this %}It is true{% endif %}', array(
    'this' => true
));
    

Toont...

It is true
    

Twig uitbreiden

Token parsers en node visitors

class MyExtension extends \Twig_Extension
{
    public function getTokenParsers()
    {
        return array(
            new UserGroupTokenParser()
        );
    }

    public function getNodeVisitors()
    {
        return array(
            new UnknownNodeVisitor()
        );
    }
}
    

En...

Dat was 't

Bedankt en
tot ziens

@matthiasnoback

php-and-symfony.matthiasnoback.nl

Dutch PHP Conference

Dutch PHP Conference: Dependency Injection Smells