Updated June 2026. Tested on Symfony 7.x and Twig 3. Part of the Techalyst Symfony series.
Twig is the template engine Symfony renders HTML with. It keeps your templates clean by giving them just enough logic, no raw PHP, and it escapes output by default so you do not accidentally open an XSS hole. The whole language is small, and once you know output, control flow, and inheritance, you can build any page.
Outputting values
You print a value with double braces. Property access uses a dot, and Twig figures out whether to read a property, call a getter, or hit an array key:
<h1>{{ product.name }}</h1> {# calls $product->getName() #}
<p>{{ items|length }} items</p>
Filters transform a value with a pipe, and you can chain them:
{{ name|upper }}
{{ price|number_format(2) }}
{{ post.publishedAt|date('d M Y') }}
Control flow
Twig has the tags you expect, wrapped in {% %}:
{% if product.inStock %}
<span>Available</span>
{% else %}
<span>Sold out</span>
{% endif %}
{% for product in products %}
<li>{{ product.name }}</li>
{% else %}
<li>No products yet.</li>
{% endfor %}
The {% else %} inside a for is a nice touch: it renders when the collection is empty, so you handle the empty case without a separate if.
Template inheritance
This is the feature that makes Twig productive. You write a base layout once with named blocks, and each page extends it and fills the blocks in. A base.html.twig:
<!DOCTYPE html>
<html>
<head><title>{% block title %}Techalyst{% endblock %}</title></head>
<body>
{% block body %}{% endblock %}
</body>
</html>
And a page that extends it:
{% extends 'base.html.twig' %}
{% block title %}Products{% endblock %}
{% block body %}
<h1>Our products</h1>
{% endblock %}
The page only declares what is different. Change the layout once and every page that extends it updates. For reusable fragments smaller than a page, a nav bar, a card, {{ include('partials/_card.html.twig', { product: product }) }} pulls one template into another.
Symfony functions
Inside Symfony, Twig gets functions that tie into the framework. You build URLs from route names, reference assets, and render forms:
<a href="{{ path('product_show', { id: product.id }) }}">View</a>
<img src="{{ asset('images/logo.png') }}">
{{ form(form) }}
Auto-escaping keeps you safe
Everything you output with {{ }} is HTML-escaped automatically, so a user's name containing <script> renders as harmless text, not a running script. This is on by default and it is why Twig is safer than echoing PHP. On the rare occasion you genuinely need to output trusted HTML, you opt out with the raw filter, {{ content|raw }}, but only for content you control.
Rendering from a controller
You return a rendered template from a controller with render, passing the variables the template needs:
return $this->render('product/show.html.twig', ['product' => $product]);
Wrapping up
Twig gives Symfony a small, safe templating language: output values and pipe them through filters, branch and loop with {% if %} and {% for %}, and above all use inheritance, a base layout with {% block %} that pages extend and override. Lean on Symfony's path, asset, and form functions, and trust auto-escaping to protect your output, reaching for raw only on HTML you own. With those pieces, templates stay clean and your layout lives in one place.
All comments ()
No comments yet
Be the first to leave a comment on this post.