Getting started
Introduction
Twig is a modern, fast, and secure template engine for PHP. Used by Symfony, Drupal, and many PHP applications.
{# Comment #}
{{ variable }} {# Output #}
{% set name = "value" %} {# Logic #}
Installation
composer require "twig/twig:^3.0"
Basic example
<?php
require_once '/path/to/vendor/autoload.php';
$loader = new \Twig\Loader\FilesystemLoader('/path/to/templates');
$twig = new \Twig\Environment($loader, [
'cache' => '/path/to/compilation_cache',
'debug' => true,
]);
echo $twig->render('index.html.twig', ['name' => 'World']);
Template syntax
{# Comments #}
{{ variable }} {# Output #}
{{ 1 + 2 }} {# Expression #}
{{ 'hello'|upper }} {# Filter #}
{% set foo = 'bar' %} {# Set variable #}
{% if condition %} {# Control structure #}
{% for item in items %} {# Loop #}
{% block content %} {# Block #}
Output & Variables
Printing variables
{{ user.name }} {# Attribute access #}
{{ user['name'] }} {# Array access #}
{{ user.getName() }} {# Method call #}
{{ user.name|default('Guest') }} {# With default #}
Variable assignment
{% set name = 'value' %}
{% set foo, bar = 'foo', 'bar' %}
{% set array = [1, 2, 3] %}
{% set hash = {'key': 'value'} %}
Escaping
{{ user.bio|raw }} {# Don't escape #}
{{ user.bio|e }} {# Escape (default) #}
{{ user.bio|e('html') }} {# HTML escape #}
{{ user.bio|e('js') }} {# JavaScript escape #}
{{ user.bio|e('css') }} {# CSS escape #}
{{ user.bio|e('url') }} {# URL encode #}
String interpolation
{{ "Hello #{name}!" }}
{{ "Total: #{price * quantity}" }}
{{ "Path: #{path|replace('\\', '/')}" }}
Filters
String filters
| Filter | Description | Example |
|---|---|---|
upper |
Uppercase | {{ name|upper }} |
lower |
Lowercase | {{ name|lower }} |
title |
Title case | {{ title|title }} |
capitalize |
Capitalize first | {{ text|capitalize }} |
trim |
Remove whitespace | {{ text|trim }} |
striptags |
Remove HTML tags | {{ html|striptags }} |
nl2br |
Newlines to <br> |
{{ text|nl2br }} |
replace |
Replace strings | {{ text|replace({'a': 'b'}) }} |
format |
sprintf format | {{ "Hello %s"|format(name) }} |
spaceless |
Remove HTML whitespace | {{ html|spaceless }} |
Array filters
| Filter | Description | Example |
|---|---|---|
length |
Count items | {{ items|length }} |
first |
First element | {{ items|first }} |
last |
Last element | {{ items|last }} |
join |
Join with string | {{ items|join(', ') }} |
sort |
Sort array | {{ items|sort }} |
reverse |
Reverse order | {{ items|reverse }} |
keys |
Get keys | {{ hash|keys }} |
merge |
Merge arrays | {{ arr1|merge(arr2) }} |
slice |
Extract slice | {{ items|slice(1, 3) }} |
batch |
Split into batches | {{ items|batch(3) }} |
Number filters
{{ 42.123|round }} {# 42 #}
{{ 42.123|round(1) }} {# 42.1 #}
{{ 42.123|round(0, 'ceil') }} {# 43 #}
{{ 42.123|round(0, 'floor') }} {# 42 #}
{{ 1234.56|number_format }} {# 1,234.56 #}
{{ 1234.56|number_format(0) }} {# 1,235 #}
{{ 1234.56|number_format(2, ',', ' ') }} {# 1 234,56 #}
{{ -15|abs }} {# 15 #}
Date filters
{{ post.published_at|date }}
{{ post.published_at|date("Y-m-d") }}
{{ post.published_at|date("F j, Y") }}
{{ "now"|date("Y-m-d H:i:s") }}
{{ "+1 day"|date("Y-m-d") }}
{{ "2024-01-01"|date_modify("+1 month")|date("Y-m-d") }}
Encoding filters
{{ data|json_encode }}
{{ data|json_encode(constant('JSON_PRETTY_PRINT')) }}
{{ text|url_encode }}
{{ html|raw }} {# Disable escaping #}
{{ text|escape }} {# HTML escape #}
{{ text|escape('js') }}
Other useful filters
{{ value|default('N/A') }}
{{ text|truncate(100) }} {# ⚠️ Requires extension #}
{{ items|map(v => v.name) }}
{{ items|filter(v => v.active) }}
{{ items|reduce((c, v) => c + v.price, 0) }}
{{ [1, 2, 3]|column('name') }}
Control Structures
If statements
{% if user.active %}
Active user
{% elseif user.pending %}
Pending approval
{% else %}
Inactive
{% endif %}
{# Inline #}
{{ user.active ? 'Active' : 'Inactive' }}
For loops
{% for item in items %}
{{ loop.index }}: {{ item.name }}
{% endfor %}
{% for key, value in hash %}
{{ key }}: {{ value }}
{% endfor %}
{% for i in 0..10 %}
{{ i }}
{% endfor %}
{% for letter in 'a'..'z' %}
{{ letter }}
{% endfor %}
{% for i in 0..10 step 2 %}
{{ i }}
{% endfor %}
Loop variable
{% for item in items %}
{{ loop.index }} {# 1, 2, 3... #}
{{ loop.index0 }} {# 0, 1, 2... #}
{{ loop.revindex }} {# ..., 3, 2, 1 #}
{{ loop.revindex0 }} {# ..., 2, 1, 0 #}
{{ loop.first }} {# true on first #}
{{ loop.last }} {# true on last #}
{{ loop.length }} {# Total items #}
{{ loop.parent }} {# Parent context #}
{% endfor %}
Else clause
{% for user in users %}
<li>{{ user.name }}</li>
{% else %}
<li>No users found</li>
{% endfor %}
Template Inheritance
Base template
{# base.html.twig #}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Default Title{% endblock %}</title>
{% block stylesheets %}{% endblock %}
</head>
<body>
<header>{% block header %}{% endblock %}</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>{% block footer %}{% endblock %}</footer>
{% block javascripts %}{% endblock %}
</body>
</html>
Child template
{# page.html.twig #}
{% extends "base.html.twig" %}
{% block title %}My Page{% endblock %}
{% block content %}
<h1>Welcome</h1>
<p>Content here</p>
{% endblock %}
{% block javascripts %}
{{ parent() }} {# Include parent content #}
<script src="app.js"></script>
{% endblock %}
Block shortcuts
{% block title page.title %} {# Short form #}
{# Equivalent to: #}
{% block title %}{{ page.title }}{% endblock %}
Named block end tags
{% block content %}
{# ... long content ... #}
{% endblock content %} {# Improves readability #}
Including Templates
Include
{% include 'header.html.twig' %}
{% include 'sidebar.html.twig' with {'user': user} %}
{% include 'card.html.twig' with {'item': item} only %}
{# Conditional include #}
{% include 'footer.html.twig' ignore missing %}
Include with variables
{% include 'product.html.twig' with {
'product': item,
'showPrice': true
} %}
Dynamic includes
{% include template_path %}
{% include ['page_' ~ type ~ '.html.twig', 'page.html.twig'] %}
Embed
{% embed "card.html.twig" %}
{% block title %}Custom Title{% endblock %}
{% block content %}Custom Content{% endblock %}
{% endembed %}
Embed allows you to override blocks while including.
Macros
Defining macros
{# macros.html.twig #}
{% macro input(name, value, type = "text") %}
<input type="{{ type }}"
name="{{ name }}"
value="{{ value }}">
{% endmacro %}
{% macro button(text, type = "button") %}
<button type="{{ type }}">{{ text }}</button>
{% endmacro %}
Importing macros
{% import "macros.html.twig" as forms %}
{{ forms.input('email', user.email, 'email') }}
{{ forms.button('Submit', 'submit') }}
Importing specific macros
{% from "macros.html.twig" import input, button %}
{{ input('username', user.name) }}
{{ button('Save') }}
Macro with named arguments
{{ forms.input(name='email', type='email', value='') }}
Tests
Common tests
{% if user is defined %}
{% if user is null %}
{% if user is empty %} {# null, false, 0, '', [] #}
{% if user is even %}
{% if user is odd %}
{% if user is iterable %}
{% if name is same as("John") %}
Type tests
{% if value is constant('CLASS::CONST') %}
{% if var is divisible by(3) %}
{% if items is iterable %}
{% if var is null %}
{% if var is same as(false) %}
Negation
{% if user is not null %}
{% if items is not empty %}
{% if value is not divisible by(2) %}
Custom tests
<?php
$twig->addTest(new \Twig\TwigTest('red', function ($value) {
return $value === 'red';
}));
{% if color is red %}
This is red!
{% endif %}
Functions
Common functions
{{ range(0, 10) }} {# [0, 1, ..., 10] #}
{{ range(0, 10, 2) }} {# [0, 2, 4, ...] #}
{{ cycle(['odd', 'even'], i) }}
{{ random(10) }} {# 0 to 10 #}
{{ random(['a', 'b', 'c']) }}
{{ max(1, 3, 2) }} {# 3 #}
{{ min(1, 3, 2) }} {# 1 #}
Array functions
{{ attribute(object, 'property') }}
{{ attribute(array, 'key') }}
{% set items = [1, 2, 3] %}
{{ include('item.html.twig', {item: 1}) }}
{% for i in range(1, 10) %}
{{ cycle(['odd', 'even'], loop.index0) }}
{% endfor %}
String functions
{{ block('content') }} {# Get block content #}
{{ parent() }} {# Parent block content #}
{{ source('template.html.twig') }} {# Template source #}
{{ template_from_string("Hello {{ name }}") }}
Date functions
{{ date() }} {# Current date #}
{{ date(timestamp) }} {# From timestamp #}
{{ date("-1 day") }} {# Relative #}
Debug functions
{{ dump(variable) }} {# Requires debug mode #}
{{ dump() }} {# Dump all variables #}
Operators
Math operators
{{ 5 + 3 }} {# 8 #}
{{ 5 - 3 }} {# 2 #}
{{ 5 * 3 }} {# 15 #}
{{ 10 / 3 }} {# 3.333... #}
{{ 10 // 3 }} {# 3 (floor division) #}
{{ 10 % 3 }} {# 1 (modulo) #}
{{ 2 ** 3 }} {# 8 (power) #}
Comparison operators
{{ 5 == 5 }} {# true #}
{{ 5 != 3 }} {# true #}
{{ 5 < 10 }} {# true #}
{{ 5 > 3 }} {# true #}
{{ 5 >= 5 }} {# true #}
{{ 5 <= 10 }} {# true #}
{{ 5 === 5 }} {# Strict equality #}
{{ 5 !== '5' }} {# Strict inequality #}
Logic operators
{{ true and false }} {# false #}
{{ true or false }} {# true #}
{{ not true }} {# false #}
{{ (true and false) or true }} {# true #}
Other operators
{{ 1..5 }} {# [1, 2, 3, 4, 5] #}
{{ 'a'..'z' }} {# Array of letters #}
{{ 'foo' ~ 'bar' }} {# 'foobar' (concat) #}
{{ 5 in [1, 5, 10] }} {# true #}
{{ 'key' in {'key': 'val'} }} {# true #}
{{ foo ? 'yes' : 'no' }} {# Ternary #}
{{ foo ?: 'default' }} {# Elvis operator #}
{{ foo ?? 'default' }} {# Null coalescing #}
Regex matching
{{ 'hello' matches '/^h/' }} {# true #}
{{ phone matches '/^\\d+$/' }} {# Numbers only #}
Comments
Single line
{# This is a comment #}
{# TODO: Fix this later #}
Multi-line
{#
This is a
multi-line comment
#}
Inline comments
{{ user.name }} {# Display username #}
Comment out code
{#
{% if user.admin %}
<button>Delete</button>
{% endif %}
#}
Comments are removed during compilation and don't appear in output.
Whitespace Control
Trim whitespace
{% spaceless %}
<div>
<strong>Hello</strong>
</div>
{% endspaceless %}
{# Output: <div><strong>Hello</strong></div> #}
Whitespace modifiers
{{- value }} {# Trim before #}
{{ value -}} {# Trim after #}
{{- value -}} {# Trim both #}
{%- if condition %} {# Trim before #}
{% endif -%} {# Trim after #}
{%- if condition -%} {# Trim both #}
Example usage
<ul>
{%- for item in items %}
<li>{{ item }}</li>
{%- endfor %}
</ul>
{# No extra whitespace between tags #}
Auto-escaping
Escaping strategy
<?php
$twig = new \Twig\Environment($loader, [
'autoescape' => 'html', // html, js, css, url, false, name
]);
Override per template
{% autoescape 'html' %}
{{ user.bio }} {# Escaped #}
{% endautoescape %}
{% autoescape 'js' %}
var name = {{ user.name }};
{% endautoescape %}
{% autoescape false %}
{{ html_content }} {# Not escaped #}
{% endautoescape %}
Raw output
{{ user.bio|raw }} {# Bypass escaping #}
{% set safe_html %}
<strong>Bold</strong>
{% endset %}
{{ safe_html|raw }}
Escape manually
{{ user.input|escape }}
{{ user.input|e }} {# Shorthand #}
{{ user.input|e('html') }}
{{ user.input|e('js') }}
{{ user.input|e('css') }}
{{ user.input|e('url') }}
{{ user.input|e('html_attr') }}
Configuration
Environment options
<?php
$twig = new \Twig\Environment($loader, [
'debug' => false,
'charset' => 'UTF-8',
'cache' => '/path/to/cache',
'auto_reload' => true,
'strict_variables' => false,
'autoescape' => 'html',
'optimizations' => -1,
]);
Loader options
<?php
// Filesystem loader
$loader = new \Twig\Loader\FilesystemLoader('/path/to/templates');
$loader->addPath('/path/to/more', 'admin');
// Array loader (in-memory)
$loader = new \Twig\Loader\ArrayLoader([
'index.html.twig' => 'Hello {{ name }}!',
]);
// Chain loader
$loader = new \Twig\Loader\ChainLoader([
new \Twig\Loader\ArrayLoader([...]),
new \Twig\Loader\FilesystemLoader([...]),
]);
Namespaced templates
<?php
$loader->addPath('/path/to/admin', 'admin');
{% include '@admin/header.html.twig' %}
Extensions
Built-in extensions
<?php
$twig->addExtension(new \Twig\Extension\DebugExtension());
$twig->addExtension(new \Twig\Extension\StringLoaderExtension());
Custom filter
<?php
$filter = new \Twig\TwigFilter('price', function ($number, $currency = 'USD') {
return $currency . ' ' . number_format($number, 2);
});
$twig->addFilter($filter);
{{ product.price|price }} {# USD 19.99 #}
{{ product.price|price('EUR') }} {# EUR 19.99 #}
Custom function
<?php
$function = new \Twig\TwigFunction('asset', function ($path) {
return '/assets/' . $path;
});
$twig->addFunction($function);
<script src="{{ asset('app.js') }}"></script>
Custom test
<?php
$test = new \Twig\TwigTest('admin', function ($user) {
return $user->hasRole('admin');
});
$twig->addTest($test);
{% if user is admin %}
<a href="/admin">Admin Panel</a>
{% endif %}
Advanced Features
Use statement
{% use "blocks.html.twig" %}
{% use "sidebar.html.twig" with sidebar as base_sidebar %}
Apply tag
{% apply upper %}
This text will be uppercase
{% endapply %}
{% apply spaceless %}
<div>
<strong>Hello</strong>
</div>
{% endapply %}
Deprecated tag
{% deprecated 'The "old_template" template is deprecated, use "new_template" instead.' %}
With tag
{% with {foo: 42} %}
{{ foo }} {# 42 #}
{% endwith %}
{% with %}
{% set foo = 42 %}
{{ foo }}
{% endwith %}
Sandbox
Enable sandbox
<?php
$tags = ['if', 'for'];
$filters = ['upper'];
$methods = [
'Article' => ['getTitle', 'getBody'],
];
$properties = [
'Article' => ['title', 'body'],
];
$functions = ['range'];
$policy = new \Twig\Sandbox\SecurityPolicy($tags, $filters, $methods, $properties, $functions);
$sandbox = new \Twig\Extension\SandboxExtension($policy);
$twig->addExtension($sandbox);
Sandbox per template
{% sandbox %}
{% include 'user_template.html.twig' %}
{% endsandbox %}
Gotchas
Strict variables
<?php
$twig = new \Twig\Environment($loader, [
'strict_variables' => true, // ⚠️ Error on undefined vars
]);
{# Without strict_variables #}
{{ undefined_var }} {# Outputs empty string #}
{# With strict_variables #}
{{ undefined_var }} {# Throws error #}
{# Safe access #}
{{ undefined_var|default('N/A') }} {# Always safe #}
Array vs object access
{# Arrays/objects can use dot notation #}
{{ user.name }} {# Tries multiple strategies #}
{# Twig tries in order: #}
{# 1. $user['name'] #}
{# 2. $user->name #}
{# 3. $user->name() #}
{# 4. $user->getName() #}
{# 5. $user->isName() #}
{# 6. $user->hasName() #}
Loop variable scope
{% for item in items %}
{% set x = item.value %}
{% endfor %}
{{ x }} {# ⚠️ Undefined! #}
{# Variables in loops don't leak to parent scope #}
Spaceless vs whitespace control
{# spaceless removes ALL whitespace between HTML tags #}
{% spaceless %}<div> <p>Hello</p> </div>{% endspaceless %}
{# Output: <div><p>Hello</p></div> #}
{# Whitespace control removes whitespace around Twig tags #}
{%- if true -%} Hello {%- endif -%}
{# Output: Hello (no spaces around "Hello") #}
Template caching
<?php
// ⚠️ Cache is keyed by template name, not content
$twig = new \Twig\Environment($loader, [
'cache' => '/tmp/twig',
'auto_reload' => true, // Check if template changed
]);
// Disable cache in development
'cache' => false,
Macro limitations
{# ⚠️ Macros can't access template variables #}
{% set global_var = "hello" %}
{% macro test() %}
{{ global_var }} {# Undefined! #}
{% endmacro %}
{# Pass variables as parameters #}
{% macro test(var) %}
{{ var }} {# Works! #}
{% endmacro %}
Comparison quirks
{{ '5' == 5 }} {# true (loose comparison) #}
{{ '5' === 5 }} {# false (strict comparison) #}
{# ⚠️ String to number coercion #}
{{ '10' > 5 }} {# true #}
{{ '10' > '5' }} {# false (string comparison) #}
Filter chain order
{{ "HELLO"|lower|capitalize }} {# "Hello" #}
{{ "HELLO"|capitalize|lower }} {# "hello" #}
{# Filters execute left-to-right #}
Dynamic property access
{% set property = 'name' %}
{{ user[property] }} {# user.name #}
{{ user.property }} {# ⚠️ Literal 'property', not variable #}
{# Use [] for dynamic access #}
Twig 3.x Changes
Breaking changes
{# ⚠️ Removed in Twig 3 #}
{% spaceless %} {# Use {% apply spaceless %} #}
{% filter upper %} {# Use {% apply upper %} #}
{# ✅ Twig 3 syntax #}
{% apply spaceless %}...{% endapply %}
{% apply upper %}...{% endapply %}
New features
{# Arrow functions (3.0+) #}
{{ items|map(v => v.name) }}
{{ items|filter(v => v.active) }}
{{ items|reduce((carry, v) => carry + v, 0) }}
{# Named arguments (3.0+) #}
{{ date(timestamp, timezone='UTC') }}
{{ number_format(value, decimals=2, dec_point=',') }}
Deprecated features
<?php
// ⚠️ Deprecated in Twig 3, removed in 4
$twig->addFilter('filter_name', function() { ... });
// ✅ Use TwigFilter class
$twig->addFilter(new \Twig\TwigFilter('filter_name', function() { ... }));
Performance improvements
Twig 3.x includes:
- Faster template compilation
- Improved caching
- Reduced memory usage
- Better error messages
Also see
- Official Twig Documentation (twig.symfony.com)
- Twig for Template Designers (twig.symfony.com)
- Twig Filters Reference (twig.symfony.com)
- Twig Functions Reference (twig.symfony.com)
- Twig Tests Reference (twig.symfony.com)
- Twig on GitHub (github.com)
- Symfony Twig Documentation (symfony.com)