NexusCS

Twig

PHP
Quick reference for Twig - a modern, fast, and secure template engine for PHP used by Symfony, Drupal, and many PHP applications.
featured

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