Skip to main content
Version: Thelia 3

Front-Office Development

Thelia 3 builds its front office on Twig templates and Symfony UX components. Components are reactive, and most of the UI logic stays on the server.

Technology stack​

TechnologyPurpose
TwigTemplate engine
Symfony UX LiveComponentsReactive UI components
Symfony UX TwigComponentStatic reusable components
StimulusJavaScript controllers
AssetMapper + Tailwind CSSAsset management, no bundler and no Node build
DataAccessServiceAPI data fetching

The theme also carries the front-office routes, including the catch-all that renders category, product, content and folder pages. No Thelia module is required to serve a page. See Serving the pages.

Architecture overview​

Front-Office Request Flow
┌─────────────────────────────────────────────────────────────────┐
│ Browser Request │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Symfony Controller │
│ (renders Twig template) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Twig Template │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ resources('/api/front/products') │ │
│ │ → DataAccessService → API Platform → Propel │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ <twig:Organisms:ProductCard:Base :product=... /> │ │
│ │ → LiveComponent (reactive) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ HTML Response │
│ (with Stimulus controllers) │
└─────────────────────────────────────────────────────────────────┘

Key concepts​

Data fetching with DataAccessService​

Instead of running loops directly in templates, Thelia 3 uses DataAccessService to call API endpoints internally, without HTTP overhead:

{# Get current product ID from URL #}
{% set productId = attr('product', 'id') %}

{# Fetch product data via API #}
{% set product = resources('/api/front/products/' ~ productId) %}

{# Display product title #}
<h1>{{ product.i18ns.title }}</h1>

Reactive components with LiveComponents​

LiveComponents give you reactive UI without writing JavaScript:

{# Render a reactive product component #}
<twig:Layouts:ProductDetails:Base :product="product" />

The component automatically handles:

  • Add to cart functionality
  • Product variant selection
  • Quantity updates
  • Real-time price changes

JavaScript behavior with Stimulus​

For custom JavaScript behavior, Stimulus controllers use a declarative approach:

<div data-controller="drawer">
<button data-action="drawer#toggle">Open Menu</button>
<div data-drawer-target="panel">...</div>
</div>

Theme structure​

The front-office theme is a Symfony bundle (FlexyBundle). Its services are autoconfigured, and its TwigComponents/LiveComponents, Stimulus controllers and form theme all live inside the bundle, so there is no XML wiring to maintain.

The default Flexy theme shows the recommended structure:

templates/frontOffice/flexy/
├── base.html.twig # Base layout
├── index.html.twig # Homepage page template
├── category.html.twig # Category page template
├── product.html.twig # Product page template
├── checkout-*.html.twig # Checkout page templates
├── account*.html.twig # Customer account page templates
├── components/ # Every component, namespace @Flexy
│ ├── Atoms/ # Smallest UI primitives (Icon, Font, ...)
│ ├── Fields/ # Form field markup, rendered by the form theme
│ ├── Forms/ # Form components (LiveComponents)
│ ├── Layouts/ # Header, Footer, listings, ...
│ ├── Molecules/ # Reusable UI elements
│ ├── Organisms/ # Complex components
│ └── Toolkit/ # Component showcase pages
├── src/ # PHP code (autoconfigured by FlexyBundle)
│ ├── Controller/ # Front-office controllers
│ ├── DTO/ # Data Transfer Objects
│ ├── Event/ # Front-office events
│ ├── EventListener/ # Event listeners (ViewListener)
│ ├── Form/ # Symfony form types
│ ├── Service/ # Services (DeliveryService, FormService, ...)
│ ├── Twig/ # Twig extensions (DataAccessExtension)
│ └── FlexyBundle.php # Bundle class (loadExtension / prependExtension)
├── form/ # Form theme (flexy_form_theme.html.twig), namespace @FlexyForm
├── assets/ # styles, icons, images
│ └── controllers/ # Stimulus controllers
├── config/
│ ├── views.yaml # root templates that are not pages of their own
│ └── packages/ # framework config the theme ships
├── template.xml # Theme manifest
└── importmap.php # AssetMapper entrypoints and JavaScript dependencies
Components: anonymous vs. PHP-backed

Both kinds live in components/, side by side. An anonymous component is a .html.twig file on its own; a PHP-backed one adds a class next to its template, in the same folder. FlexyBundle\Components\ maps to that directory, and a component's name is its path under it: components/Organisms/ProductCard/Base.php is Organisms:ProductCard:Base.

Section contents​

SectionDescription
Twig BasicsTwig templating in Thelia
Data AccessFetching data with resources()
Flexy ThemeDefault theme structure
LiveComponentsCreating reactive components
StimulusJavaScript controllers
Theme HooksTheme extension points for modules
FormsFront-office forms

Quick example​

A minimal category page that uses all of these concepts together:

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

{# Get category ID from URL parameters #}
{% set categoryId = attr('category', 'id') %}

{# Fetch category and products via API #}
{% set category = resources('/api/front/categories/' ~ categoryId) %}
{% set products = resources('/api/front/products', {
'productCategories.category.id': categoryId,
'visible': true,
'itemsPerPage': 30
}) %}

{% block body %}
<h1>{{ category.i18ns.title }}</h1>

{# Use a LiveComponent for filtering #}
<twig:Layouts:ProductListing:Base :categoryId="categoryId" :page="1" />

{# Or render products directly #}
<div class="product-grid">
{% for product in products %}
<twig:Organisms:ProductCard:Base :product="product" />
{% endfor %}
</div>
{% endblock %}

{% block stimulus_controller %}
{{ stimulus_controller('drawer') }}
{% endblock %}

Comparison with the back office​

AspectFront officeBack office
Template engineTwigTwig (default-twig bundle)
Data fetchingDataAccessServicePropel Repository classes
ComponentsLiveComponentsTwig and Live components
HooksMinimal useExtensively used
JavaScriptStimulusStimulus

Next steps​

  1. Twig Basics for the Twig templating fundamentals
  2. Data Access for the resources() function
  3. Flexy Theme for the default theme