Twig Syntax
The three Twig delimiters, conditionals and loops, and exactly what the dot in article.title resolves to.
{{ print_this }} {% do this %} {# ignore this #}Twig has three delimiters and no others. Once you know them you can read any template.
{{ ... }}evaluates an expression and prints the result, escaped.{% ... %}runs a statement: a condition, a loop, an assignment.{# ... #}is a comment. It is removed when the template is compiled, so it never reaches the browser.
What the dot does
For {{ article.title }} Twig tries each of these in order and stops at the first that works: $article['title'], $article->title, $article->title(), $article->getTitle(), $article->isTitle(), $article->hasTitle(). If none works the expression is null, or an error when strict_variables is enabled. So one dot covers arrays, public properties and getters, and you do not change the template when a property becomes a method.
The long form is {{ attribute(article, 'title') }}. It does the same lookup and is the only option when the name is itself in a variable, as in {{ attribute(article, field) }}.
Conditions and loops
{% if %}, {% elseif %}, {% else %}, {% endif %} behave as expected. Inside {% for %} a loop variable exists, with loop.index counting from 1, loop.index0 from 0, and loop.first and loop.last as booleans.
Two shorthands worth recognising: {{ a ?: b }} is {{ a ? a : b }}, and {{ name|default('Guest') }} is {{ name is defined and name is not empty ? name : 'Guest' }}.
Example
// src/Model/Article.php
declare(strict_types=1);
namespace App\Model;
final class Article
{
public function __construct(
// {{ article.title }} finds this public property
public readonly string $title,
private readonly array $tags,
private readonly bool $published,
private readonly int $wordCount,
) {
}
// {{ article.tags }} finds this getter
public function getTags(): array
{
return $this->tags;
}
// {{ article.published }} finds this isser
public function isPublished(): bool
{
return $this->published;
}
// {{ article.readingMinutes }} calls it with no arguments;
// {{ article.readingMinutes(150) }} passes one
public function readingMinutes(int $wordsPerMinute = 200): int
{
return (int) ceil($this->wordCount / $wordsPerMinute);
}
}
// $article = new Article('Routing', ['http'], true, 1200);
// echo $article->readingMinutes();
// Output: 6When to use it
- Print an entity's fields without knowing whether each is a property or a getter.
- Render a list and mark the first or last row differently.
- Build a table whose columns are chosen at runtime from a list of field names.
More examples
All three delimiters in one template
Each dot in this template resolves to something different in the PHP class: a property, an isser, a getter and a method. The template does not need to know which. Use loop.last for separators rather than tracking a counter yourself.
{# templates/article/show.html.twig #}
{# This comment is compiled away and never sent to the browser. #}
<h1>{{ article.title }}</h1>
{% if article.published %}
<p>{{ article.readingMinutes }} minute read</p>
{% else %}
<p>Draft - not visible to readers.</p>
{% endif %}
<ul>
{% for tag in article.tags %}
<li>{{ loop.index }}. {{ tag }}{% if not loop.last %},{% endif %}</li>
{% endfor %}
</ul>attribute(), default() and the shorthand ternary
attribute() is the long form of the dot and the only form that accepts a dynamic name, which is what a generic table or an admin list needs. Showing both ternary forms matters because you will meet the short one in other people's templates and it is not obvious that it repeats the left-hand side.
{# templates/admin/_row.html.twig #}
{# The field name lives in a variable, so the dot cannot be used. #}
{% for field in columns %}
<td>{{ attribute(article, field) }}</td>
{% endfor %}
{# These two lines are the same thing. #}
<p>{{ author_name|default('Unknown author') }}</p>
<p>{{ author_name is defined and author_name is not empty ? author_name : 'Unknown author' }}</p>
{# And these two. #}
<p>{{ subtitle ?: 'No subtitle' }}</p>
<p>{{ subtitle ? subtitle : 'No subtitle' }}</p>Local variables with {% set %}
{% set %} keeps a template readable when the same expression appears several times, and its block form captures rendered markup into a variable you can place later. Reach for it when a value is used more than once, not as a way to move logic into the view.
{# templates/article/_summary.html.twig #}
{# Compute once, use twice, instead of repeating the expression. #}
{% set minutes = article.readingMinutes %}
<p title="{{ minutes }} minutes">{{ minutes }} min</p>
{# A set block captures markup rather than a value. #}
{% set badge %}
<span>{{ article.published ? 'Live' : 'Draft' }}</span>
{% endset %}
{{ badge }}
Discussion