or
or
In composer.json:
{
"require": {
"twig/twig": "1.*"
}
}
base.html.twig
<html>
<body>
<div class="sidebar">
{{ block('sidebar') }}
</div>
<div class="content">
{{ block('content') }}
</div>
</body>
</html>
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 %}
form.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 %}
$env = new \Twig_Environment();
echo $env->render('myTemplateFile.html.twig', array(
'someVariable' => 'theValue'
);
// Twig: "uhm, what is this 'myTemplateFile.html.twig'?"
$stringLoader = new \Twig_Loader_String();
$env->setLoader($stringLoader);
echo $env->render('<p>Hi {{ name }}!</p>', array(
'name' => '...'
));
$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' => '...'));
$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' => '...');
$chainLoader = new \Twig_Loader_Chain(array(
$filesystemLoader,
$arrayLoader,
$stringLoader,
// ...
));
$env = new \Twig_Environment($chainLoader);
echo $env->render('do_your_best_to_find_it');
\Twig_LoaderInterface
$chainLoader->addLoader(new DatabaseLoader($connection));
Or with Symfony2, create a service with the tag "twig.loader".
interface Twig_LoaderInterface
{
public function getSource($name);
public function isFresh($name, $time);
public function getCacheKey($name);
}
class Twig_Environment
{
// ...
public function render($name, array $context = array())
{
return $this->loadTemplate($name)->render($context);
}
}
/app/cache/{environment}/twig)
/* 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";
}
// ...
}
Take this template:
{% endif %}
<ul>
{% for item in items %}
<li>{{ item|capitalize }}</li>
{% endfor %}
</ul>
"Yes, but..."
$lexer = $env->getLexer();
$template = ...;
$tokenStream = $lexer->tokenize($template);
echo $tokenStream;
BLOCK_START_TYPE() | {% |
NAME_TYPE(endif) | |
BLOCK_END_TYPE() | %} |
TEXT_TYPE(<ul>) | raw template data |
BLOCK_START_TYPE() | {% |
NAME_TYPE(for) | |
NAME_TYPE(item) | |
OPERATOR_TYPE(in) | |
NAME_TYPE(items) | |
BLOCK_END_TYPE() | %} |
TEXT_TYPE(<li>) | raw template data |
VAR_START_TYPE() | {{ |
NAME_TYPE(item) | |
PUNCTUATION_TYPE(|) | |
NAME_TYPE(capitalize) | |
VAR_END_TYPE() | }} |
TEXT_TYPE(</li>) | raw template data |
BLOCK_START_TYPE() | {% |
NAME_TYPE(endfor) | |
BLOCK_END_TYPE() | %} |
TEXT_TYPE(</ul>) | raw template data |
EOF_TYPE() | end of input |
The lexer first checks for the position of the main markers:
{%{{{#Next, the lexer
Tokens have a type, a value and a line number
// types as defined in \Twig_Token
const EOF_TYPE = -1; // end of input
const TEXT_TYPE = 0; // template text
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; // number
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; // }
{% for {% if
({[ should be closed symmetrically{{ ['a' }}
]}) can not occur first{{ ] }}
{{ \ }}
{# comment
{{ "#{variable" }}
To keep track of what the lexer is doing, it uses a stack of states.
// 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
{% endif %}
<ul>
{% for item in items %}
<li>{{ item|capitalize }}</li>
{% endfor %}
</ul>
STATE_DATA | template data |
STATE_BLOCK | block endif starts |
STATE_BLOCK | block endif ends |
STATE_DATA | <ul> |
STATE_BLOCK | block for starts |
STATE_BLOCK | name: item |
STATE_BLOCK | name: in |
STATE_BLOCK | name: items |
STATE_BLOCK | block for ends |
STATE_DATA | <li> |
STATE_VAR | variable starts, name: item |
STATE_VAR | punctuation: | |
STATE_VAR | name: capitalize |
STATE_VAR | variable ends |
STATE_DATA | </li> |
STATE_BLOCK | block endfor starts |
STATE_BLOCK | block endfor ends |
STATE_DATA | </ul> |
Push and pop: add one on top, take one from the top (last in, first out)
STATE_DATASTATE_BLOCKSTATE_DATASTATE_DATASTATE_BLOCKSTATE_DATASTATE_DATASTATE_VARSTATE_DATASTATE_DATASTATE_BLOCKSTATE_DATASTATE_DATA[EOF]
The resulting list of tokens may be semantically incorrect,
i.e. its meaning may be unclear or it may have no meaning at all.
In the Twig language, that is...
{% endif %}
<ul>
{% for item in items %}
<li>{{ item|capitalize }}</li>
{% endfor %}
</ul>
BLOCK_START_TYPE() | {% |
NAME_TYPE(endif) | |
BLOCK_END_TYPE() | %} |
| ... |
The parser will
Tokens will be
Nodes describe unequivocally the desired semantics of the original input string.
<ul>
{% for item in items %}
<li>{{ item|capitalize }}</li>
{% endfor %}
</ul>
"Hey, where is the..."
$template = ...; $tokenStream = $lexer->tokenize($template); $parser = $env->getParser(); $nodeTree = $parser->parse($tokenStream); echo $nodeTree;
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')
)
...
The parsing process results in a \Twig_Node_Module containing
The \Twig_Parser::parse() method collects nodes based on the token at the current position in the token stream.
TEXT_TYPE |
template text | create a \Twig_Node_Text with the value of the token |
VAR_START_TYPE |
variable | parse the expression that follows and expect a VAR_END_TYPE |
BLOCK_START_TYPE |
block with a tag | expect a name, which is the name of the tag (i.e. for, if, etc.) and call a subparser |
Bad name though...
It allows you to define tags including any syntax, eventually followed by a BLOCK_END_TYPE token.
{% for item in items %}
...
{% endfor %}
The parse() method of a token parser should return null or an instance of \Twig_Node
Returned nodes are inserted in the Abstract Syntax Tree
Twig contains already many token parsers and nodes for tags like
blockforifsetextendsYou can easily override the behavior of these tags.
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('AmsterdamPHP',
$token->getLine());
return new \Twig_Node_Print($expr, $token->getLine(),
$this->getTag());
}
public function getTag()
{
return 'usergroup';
}
}
{% usergroup %}
There we go, {% usergroup %}!
$template = ...;
$env = new \Twig_Environment($loader);
$env->addTokenParser(new UsergroupTokenParser());
echo $env->parse($env->tokenize($template));
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: 'AmsterdamPHP')
)
2: Twig_Node_Text(data: '!')
)
)
)
{{ 5 + age * 4 }}
$template = ...;
$moduleNode = $env->parse($env->tokenize($template));
echo $moduleNode;
Expressions are parsed by a specialized expression parser.
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')
)
)
)
)
)
)
Most operators are left associative, which means a + b + c is to be read as
((a + b) + c)
and not as
(a + (b + c))
Operators have a number indicating their precedence, so a + b * c will always be interpreted as
(a + (b * c))
since the "*" operator has a higher precedence then the "+" operator.
Just after all tokens are parsed and all the nodes are created node visitors are allowed to revisit the entire node tree and modify, add, replace or remove nodes. This allows for awesome and also dangerous things to happen.
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();
}
See for an example: Collecting Data Across Templates Using a Node Visitor
The root node of the tree of nodes is a \Twig_Node_Module which contains all the nodes generated by
The compiler just calls the compile() method of the resulting \Twig_Node_Module node.
{% for item in items %}
{{ item|capitalize }}<br />
{% endfor %}
$template = ...;
echo $env->compileSource($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));
}
}
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()
);
}
}
class Twig_Node_Print
{
public function compile(Twig_Compiler $compiler)
{
$compiler
->addDebugInfo($this)
->write('echo ')
->subcompile($this->getNode('expr'))
->raw(";\n")
;
}
}
The result can be found in the compiled template:
protected function doDisplay(array $context, array $blocks = array())
{
// line 1
echo "There we go, ";
echo "AmsterdamPHP"; // the expression was just a constant
echo "!";
}
Two important conclusions:
We only have to create our own node type and implement the compile method.
Writing parsers and nodes can be quite difficult, so
\Twig_Test_NodeTestCase)There are some very "deep" ways to modify Twig's behavior:
$env->setLoader($loader): for instance load templates from a database$env->setLexer($lexer): could be a bad idea (when you want to change basic syntax, use options)$env->setParser($parser): probably a bad idea
$options = array(
'tag_comment' => array('{#', '#}'),
'tag_block' => array('{%', '%}'),
'tag_variable' => array('{{', '}}'),
'whitespace_trim' => '-',
'interpolation' => array('#{', '}')
);
$lexer = new \Twig_Lexer($env, $options);
$env->setLexer($lexer);
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
Or when using Symfony2: create a service with a tag "twig.extension"
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);
}
}
echo $env->render('{{ myFunction("<strong>computer</strong>") }}');
Will render...
This is <b>my</b> <strong>computer</strong>.
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');
}
}
echo $env->render('{{ thing|mine(false) }}', array(
'thing' => '<strong>Life</strong>'
));
Will render...
<strong>Life</strong> (which is not mine)
public function getFilters()
{
return array(
new \Twig_SimpleFilter('*_of_*', function($type, $owner, $what) {
return sprintf('%s is a %s of %s', $what, $type, $owner);
})
);
}
echo $env->render('{{ "This game"|call_of_duty() }}');
Will render...
This game is a call of duty
class MyExtension extends \Twig_Extension
{
public function getTests()
{
return array(
new \Twig_SimpleTest('usergroup', function($name) {
return $name === 'AmsterdamPHP';
})
);
}
}
echo $env->render('{% if "AmsterdamPHP" is usergroup %}I told you so{% endif %}');
Will render...
I told you so
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)
)
)
);
}
}
echo $env->render('{% if no true maybe this %}It is true{% endif %}', array(
'this' => true
));
Will render...
It is true
class MyExtension extends \Twig_Extension
{
public function getTokenParsers()
{
return array(
new UserGroupTokenParser()
);
}
public function getNodeVisitors()
{
return array(
new UnknownNodeVisitor()
);
}
}
And...