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 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
| Aspect | Front-office (Flexy) | Back-office (default-twig) | Back-office (default, legacy) |
|---|---|---|---|
| Engine | Twig 3 | Twig 3 | Smarty |
| Data access | resources() / attr() Twig functions (DataAccessService) | Repositories + presenters (Propel) | Loops (Propel) |
| Components | TwigComponent / LiveComponent | TwigComponent / LiveComponent | Template includes |
| JavaScript | Stimulus | Stimulus | jQuery |
| Styling | Theme SCSS | Bootstrap 5.3 | Theme CSS |
| Routing | #[Route] attributes | #[Route] attributes | admin.xml route files |
| Packaging | FlexyBundle | BackOfficeDefaultTwigBundle | Thelia 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') %}
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 />
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.
Back-office: the default-twig bundle (recommended)
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.
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)
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().
Back-office (default-twig / Twig), recommended
- 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-twigconventions.
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
- Front-office development: Twig + Flexy guide
- Front-office data access:
resources()/attr() - LiveComponents: building reactive UIs
- Back-office development: admin templates and hooks
- Modules vs Bundles: how themes are packaged