Skip to main content
Version: Thelia 3

Templating Engines

Thelia 3 renders its interfaces with Twig. The front-office (Flexy theme) and the modern back-office (default-twig theme) are Twig bundles built on Symfony UX (Stimulus, TwigComponent, LiveComponent) and Bootstrap 5, and the transactional emails and order PDF are Twig themes too.

Smarty, the engine of Thelia 2, is no longer part of a default install: it only served the legacy default back-office theme, which is retired.

The Smarty back-office is retired

The Smarty default back-office theme (thelia/backoffice-default-template) is no longer installed or maintained, and neither is thelia/smarty-module. Build admin screens and module admin extensions against the default-twig bundle conventions. The Smarty examples below are kept to help you read and port Thelia 2 code.

Overview​

AspectFront-office (Flexy)Back-office (default-twig)Back-office (default, legacy)
EngineTwig 3Twig 3Smarty
Data accessresources() / attr() Twig functions (DataAccessService)Repositories + presenters (Propel)Loops (Propel)
ComponentsTwigComponent / LiveComponentTwigComponent / LiveComponentTemplate includes
JavaScriptStimulusStimulusjQuery
StylingTheme SCSSBootstrap 5.3Theme CSS
Routing#[Route] attributes#[Route] attributesadmin.xml route files
PackagingFlexyBundleBackOfficeDefaultTwigBundleThelia templates

Both Twig themes are Symfony bundles: their templates, components, Stimulus controllers, form themes and assets all live inside the bundle. See Modules vs Bundles.

Front-office: Twig + Symfony UX​

The front-office ships as the FlexyBundle. Page templates live at the theme root, components under components/, and the rest of the PHP under src/.

Template location​

templates/frontOffice/flexy/
├── base.html.twig # base layout
├── index.html.twig # homepage
├── product.html.twig # product page
├── category.html.twig # category page
├── checkout-cart.html.twig # cart step
├── components/ # TwigComponent / LiveComponent (namespace @Flexy)
└── src/
└── Twig/
└── DataAccessExtension.php # registers resources() and attr()

Data retrieval​

Fetch data from the API layer with the resources() Twig function. It is registered by FlexyBundle\Twig\DataAccessExtension and delegates to Thelia\Api\Service\DataAccess\DataAccessService:

{# Fetch a single product #}
{% set product = resources('/api/front/products/' ~ productId) %}

{# Fetch a collection with filters #}
{% set products = resources('/api/front/products', {
'productCategories.category.id': categoryId,
'visible': true,
'order[position]': 'asc',
'itemsPerPage': 20
}) %}

{# Display products #}
{% for product in products %}
<h2>{{ product.i18ns.title }}</h2>
{# Prices live on the product's sale elements (PSE), not on a flat property #}
<p>{{ product.productSaleElements[0].productPrices[0].price }} EUR</p>
{% endfor %}

See Front-office data access for the full reference.

Route attributes​

Read a value derived from the current route with the attr() Twig function (also registered by DataAccessExtension, backed by AttributeAccessService):

{# On /product/42 page #}
{% set productId = attr('product', 'id') %}

{# On /category/5 page #}
{% set categoryId = attr('category', 'id') %}
note

attr(type, name) resolves a method attribute<Type>() on AttributeAccessService. Calling it with a type that has no matching method throws a RuntimeException, so only documented attribute types are valid.

Components​

Flexy ships TwigComponents (server-rendered) and LiveComponents (reactive, re-render on the server without a page reload). Both live under components/, and both are rendered with the <twig:...> tag syntax:

{# Product details LiveComponent - receives the product array #}
<twig:Layouts:ProductDetails:Base :product="product" />

{# Category listing with filters (LiveComponent) #}
<twig:Layouts:ProductListing:Base :categoryId="categoryId" :page="page" />

{# Cart step (LiveComponent) #}
<twig:Organisms:Cart:Base />

{# Order summary (LiveComponent) #}
<twig:Organisms:Summary:Checkout />
tip

A component is named after its class path under FlexyBundle\Components\, with \ written as : and no prefix: components/Organisms/Cart/Base.php is Organisms:Cart:Base. The #[AsTwigComponent] and #[AsLiveComponent] attributes carry no name: argument. Browse components/ to discover what is available and read each class for its props.

See LiveComponents for building your own.

Stimulus controllers​

For custom JavaScript behavior, attach a Stimulus controller. Controllers live in assets/controllers/ inside the bundle:

<div data-controller="product-gallery">
<img data-product-gallery-target="main" src="{{ mainImage }}">

{% for image in images %}
<img
data-action="click->product-gallery#select"
data-product-gallery-url-param="{{ image.url }}"
src="{{ image.thumbnail }}"
>
{% endfor %}
</div>

See Stimulus.

The reference back-office is the default-twig theme, packaged as BackOfficeDefaultTwigBundle. It is a Bootstrap 5.3 / Twig / Stimulus port of the legacy Smarty admin, and it is autonomous: its routes, hooks, forms and assets all live inside the bundle.

Activation​

# fresh install
ddev exec php bin/install \
--frontoffice_theme=flexy --backoffice_theme=default-twig \
--pdf_theme=default --email_theme=default \
--with-demo --with-admin

# already installed: switch active back-office template
ddev exec php Thelia template:set backOffice default-twig
ddev exec php Thelia cache:warmup -e dev

# build the stylesheet (bin/install runs it on a fresh install)
ddev exec php bin/console sass:build

Bundle layout​

templates/backOffice/default-twig/
├── base.html.twig # base layout
├── home.html.twig # dashboard
├── <domain>/ # one folder per domain (catalog, customer, ...)
│ ├── list.html.twig
│ ├── edit.html.twig
│ └── _create_modal.html.twig
├── components/ # reusable Twig components (BoDataTable, BoDashboard, ...)
├── form/
│ └── bo_form_theme.html.twig # custom Bootstrap 5 form theme
├── assets/ # SCSS + JS + img
│ └── controllers/ # Stimulus controllers
└── src/
├── BackOfficeDefaultTwigBundle.php
├── Controller/ # thin controllers, #[Route] attributes
├── DTO/ # immutable data transfer objects
├── Form/ # Symfony forms
├── Twig/ # HookExtension, BackOfficeUrlExtension, DataTableExtension, ...
└── UiComponents/ # AsTwigComponent / AsLiveComponent

Routing with attributes​

Admin screens declare their routes with PHP 8 #[Route] attributes on thin controllers, with no admin.xml:

// templates/backOffice/default-twig/src/Controller/Catalog/BrandController.php
#[Route('/admin/brand', name: 'admin.brand.')]
final class BrandController
{
#[Route('', name: 'default', methods: ['GET'])]
public function list(/* ... */) { /* ... */ }

#[Route('/create', name: 'create', methods: ['POST'])]
public function create(/* ... */) { /* ... */ }

#[Route('/update/{brand_id}', name: 'update', methods: ['GET'], requirements: ['brand_id' => '\d+'])]
public function updateView(int $brand_id) { /* ... */ }
}

Components​

The bundle exposes its own TwigComponents, prefixed Bo. Render them with component():

{# Reusable list table #}
{{ component('BoDataTable', { /* ... */ }) }}

{# Confirmation modal #}
{{ component('BoConfirmDialog', { /* ... */ }) }}

Discover them by grepping the bundle for AsTwigComponent under src/UiComponents/ (BoDataTable, BoDashboard, BoCreateDialog, BoConfirmDialog, BoPagination, ...).

Hooks​

Modules extend admin screens through hooks, exposed as Twig functions by BackOfficeDefaultTwigBundle\Twig\HookExtension:

{{ safe_hook('main.head-css') }}

{% for block in hook_block('home.block', { foo: bar }) %}
<h2>{{ block.title }}</h2>
{{ block.content|raw }}
{% endfor %}

{% if has_hook('product.tab') %}{# ... #}{% endif %}

Most hook names are kept identical to the legacy Smarty template; a few renamed hooks are bridged to their legacy name so existing third-party modules keep working unchanged. See Back-office hooks.

Access control

ACL resources live in core/lib/Thelia/Core/Security/Resource/AdminResources.php. Check access with is_granted('VIEW', 'admin.brand') in templates, or via the bundle's AdminAccessChecker in controllers.

Emails and PDF: Twig themes​

Transactional emails and order documents (invoice, delivery slip) are Twig themes as well, installed under templates/email/ and templates/pdf/. They render through the same TwigParser as the rest of the site, selected by the ParserResolver.

The ParserResolver picks a parser by file extension, not by a global setting: TwigParser claims .html.twig and .txt.twig, the SmartyParser of Thelia 2 claimed .html and .tpl. An email message is a .html.twig (HTML body) plus a .txt.twig (text body); a PDF document is a single .html.twig. Because the choice is per file, the parser follows the template it is given, not a global setting.

Since a mail or a PDF is often rendered with no HTTP request behind it (from a worker, or the mail:render / pdf:render console commands), these themes use the CLI-safe helpers the TwigEngine module provides (loop, format_money, format_date, format_address, config, thelia_url, media_url, hook and hook_block) rather than request-bound functions. The PDF theme renders HTML that is then converted to PDF by dompdf.

See Emails and PDF for the full reference.

Legacy back-office: Smarty (retired)​

caution

This section documents the legacy default (Smarty) back-office, which is no longer installed or maintained. It stays here so you can read Thelia 2 code and port it. Do not target it for new work. Use the default-twig bundle conventions above.

The legacy admin uses Smarty templates with Thelia's loop system for data retrieval.

Data retrieval with loops​

{* List products in a category *}
{loop name="products" type="product" category=$category_id visible=1}
<tr>
<td>{$ID}</td>
<td>{$TITLE}</td>
<td>{$PRICE} {$CURRENCY}</td>
<td>
<a href="{url path="/admin/product/update/$ID"}">Edit</a>
</td>
</tr>
{/loop}

Hooks​

{hook name="product.additional-info"}

Form handling​

{form name="product_modification"}
<input type="text" name="{field field='title'}" value="{$form.title.value}">
{if $form.title.hasError}
<span class="error">{$form.title.error}</span>
{/if}

<button type="submit">Save</button>
{/form}

When to use which​

Front-office (Flexy / Twig)​

  • Customer-facing pages and storefront themes.
  • Reactive UI with LiveComponents and Stimulus.
  • Data fetched through resources() / attr().
  • New admin screens and admin extensions in modules.
  • Thin controllers with #[Route] attributes.
  • Reusable Bo* TwigComponents, Bootstrap 5 form theme, Stimulus.
  • Extending admin screens through the bundle's hook functions.

Back-office (Smarty), legacy only​

  • No longer supported. A module that still renders Smarty admin templates has to be ported to the default-twig conventions.

Comparison: displaying a product list​

Front-office (Twig):

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

<div class="products">
{% for product in products %}
<article class="product-card">
<h2>{{ product.i18ns.title }}</h2>
<p class="price">{{ product.productSaleElements[0].productPrices[0].price|number_format(2) }} EUR</p>
<a href="{{ product.publicUrl }}">View</a>
</article>
{% endfor %}
</div>

Legacy back-office (Smarty):

{loop name="products" type="product" category=$category_id visible=1}
<tr>
<td>{$TITLE}</td>
<td>{$PRICE|number_format:2} {$CURRENCY}</td>
<td><a href="{url path="/admin/product/update/$ID"}">Edit</a></td>
</tr>
{/loop}

Learn more​