Skip to main content
Version: Next

Twig Basics

Thelia 3 uses Twig as the template engine for front-office development.

Official Documentation

For complete Twig documentation, see twig.symfony.com.

Template inheritance

Base layout

All pages extend a base layout:

{# base.html.twig #}
<!DOCTYPE html>
<html lang="{{ lang_code }}">
<head>
<title>{% block title %}{{ SEOnePageTitle() }}{% endblock %}</title>
{% block stylesheets %}{{ encore_entry_link_tags('app') }}{% endblock %}
{% block javascripts %}{{ encore_entry_script_tags('app') }}{% endblock %}
</head>
<body>
{% block header %}{{ include('@components/Layout/Header.html.twig') }}{% endblock %}
<main>{% block body %}{% endblock %}</main>
{% block footer %}{{ include('@components/Layout/Footer.html.twig') }}{% endblock %}
</body>
</html>

Extending the base layout

{# product.html.twig #}
{% extends 'base.html.twig' %}

{% block title %}
{{ product.i18ns.title }} - {{ parent() }}
{% endblock %}

{% block body %}
<h1>{{ product.i18ns.title }}</h1>
<p>{{ product.i18ns.description|raw }}</p>
{% endblock %}

Data access

Use the resources() function to fetch data from the API:

{# Get product by ID #}
{% set product = resources('/api/front/products/' ~ productId) %}

{# Get products in category #}
{% set products = resources('/api/front/products', {
'productCategories.category.id': categoryId,
'visible': true,
'itemsPerPage': 20
}) %}

{% for product in products %}
<div>{{ product.i18ns.title }}</div>
{% endfor %}

See Data Access for complete documentation.

Thelia-specific functions

URL attributes

Get URL parameters using attr():

{% set productId = attr('product', 'id') %}
{% set categoryId = attr('category', 'id') %}

SEO functions

<title>{{ SEOnePageTitle() }}</title>
<meta name="description" content="{{ SEOnePageDesc() }}">
<link rel="canonical" href="{{ SEOnePageCanonical() }}">
{{ SEOneBreadcrumbJsonLd(breadcrumb)|raw }}
{{ SEOneHreflang()|raw }}
note

The SEOne* functions (SEOnePageTitle, SEOnePageDesc, SEOnePageCanonical, SEOneBreadcrumbJsonLd, SEOneHreflang, SEOneBreadcrumb) are not core Twig functions. They are provided by the SEOne module (thelia/seone-module), which Flexy declares as a dependency. A theme built from scratch without that module would not have these functions available.

Translation

{{ 'Add to cart'|trans }}
{{ 'Welcome, %name%!'|trans({'%name%': customer.firstname}) }}
{{ '%count% item|%count% items'|trans({'%count%': cart.itemCount}) }}

Price formatting

{{ price|format_currency('EUR', locale: lang_code) }}

Components

Twig components

{{ component('Flexy:ProductCard', {product: product}) }}

{% component 'Flexy:Card' %}
{% block header %}Card Title{% endblock %}
{% block content %}Card content{% endblock %}
{% endcomponent %}

LiveComponents

{{ component('Flexy:CategoryFilters', {
initialCategoryId: categoryId,
initialPage: 1
}) }}

See LiveComponents for details.

Asset management

{# CSS #}
{{ encore_entry_link_tags('app') }}

{# JavaScript #}
{{ encore_entry_script_tags('app') }}

{# Single asset #}
{{ asset('build/images/logo.png') }}

Stimulus controllers

<div {{ stimulus_controller('drawer') }}>
<button {{ stimulus_action('drawer', 'toggle') }}>Toggle</button>
<div {{ stimulus_target('drawer', 'panel') }}>Content</div>
</div>

Debugging

{{ dump(product) }}
{{ dump() }} {# All variables #}
tip

dump() only works when APP_DEBUG=1.

Best practices

Use DataAccessService

{# Recommended #}
{% set products = resources('/api/front/products', {visible: true}) %}

Keep templates clean

{# Good - logic in component #}
{{ component('Flexy:ProductPrice', {product: product}) }}

{# Avoid - complex business logic in template #}
{% if someComplexCondition and anotherCondition %}
...
{% endif %}

Escape user content

{{ userComment }}  {# Auto-escaped #}
{{ product.i18ns.description|raw }} {# Trusted admin content only #}

Next steps