Skip to main content
Version: Next

Architecture Overview

Thelia 3 runs on Symfony and adds the pieces an e-commerce application needs on top of it.

Core components

Technology stack

LayerTechnologyPurpose
LanguagePHP 8.3+composer.json requires >= 8.3
FrameworkSymfony 7.4 LTSHTTP handling, DI, routing, security
ORMPropel ORM (not Doctrine)Database abstraction and queries
APIAPI Platform 4.3 (standalone)RESTful API generation (api-platform/symfony)
Front-OfficeTwig + Symfony UX (Flexy bundle)Reactive UI components
Back-OfficeTwig (default-twig bundle)Admin interface templating
note

Propel is not Doctrine: there is no EntityManager and no flush(). Every ->save() persists immediately. Respect the strict native Propel types (string for decimal columns, int for tinyint columns).

Back-office: Twig is the reference

The Twig back-office (default-twig bundle) is the reference admin theme. The legacy Smarty default back-office still ships, but it is no longer recommended and is expected to be dropped in a later release. Build new admin features on the default-twig bundle.

Directory structure

thelia/
├── core/
│ └── lib/Thelia/
│ ├── Api/ # API Platform integration
│ │ ├── Resource/ # API resources
│ │ ├── Bridge/Propel/ # Propel state providers
│ │ └── Service/DataAccess/ # DataAccessService
│ ├── Domain/ # Business logic facades (17 folders)
│ │ ├── Cart/ # e.g. Cart, Customer, Order, Checkout,
│ │ ├── Customer/ # Catalog, Promotion, Shipping,
│ │ ├── Order/ # Taxation, Media, CMS, Addressing,
│ │ └── Checkout/ # Admin, Localization, Marketing,
│ │ # Module, DataTransfer, Shared
│ ├── Core/ # Kernel, security, forms
│ └── Model/ # Propel models
├── templates/
│ ├── frontOffice/flexy/ # Front-office Twig theme (FlexyBundle)
│ ├── backOffice/default-twig/ # Back-office Twig theme (reference)
│ └── backOffice/default/ # Legacy Smarty back-office (deprecated)
├── vendor/thelia/
│ └── modules/ # Official modules
└── local/modules/ # Custom modules
note

The front-office theme is a Symfony bundle: templates/frontOffice/flexy/ exposes namespace FlexyBundle (class src/FlexyBundle.php). The back-office reference theme is the bundle in templates/backOffice/default-twig/ (namespace BackOfficeDefaultTwigBundle, class src/BackOfficeDefaultTwigBundle.php). Both ship their own controllers, Twig/Live components, Stimulus controllers, form themes and assets inside the bundle.

Architectural patterns

API-first design

All data access in Thelia 3 goes through the API layer:

┌─────────────────┐     ┌─────────────────┐
│ Front-Office │ │ External │
│ (Templates) │ │ Clients │
└────────┬────────┘ └────────┬────────┘
│ │
│ DataAccessService │ HTTP
│ (internal PHP) │ (JSON)
│ │
▼ ▼
┌─────────────────────────────────────────┐
│ API Platform │
│ /api/admin/ /api/front/ │
└─────────────────────────────────────────┘


┌─────────────────────────────────────────┐
│ Propel ORM │
└─────────────────────────────────────────┘

This gives you:

  • Single source of truth for data validation
  • Consistent serialization across all consumers
  • Caching at the API level
  • A direct path for integrating external systems

Domain layer (facades)

Business logic lives in facades that orchestrate services:

// CartFacade orchestrates cart operations
$cartFacade->addItem($dto); // Validates, applies rules, persists
$cartFacade->getCartFromSession(); // Retrieves current cart

See Facades for detailed documentation.

Twig everywhere

Both the front-office and the back-office reference themes are Twig bundles:

Front-Office (Flexy bundle)Back-Office (default-twig bundle)
Twig + LiveComponentsTwig + LiveComponents
DataAccessService (resources())Repositories / Services
Stimulus controllersStimulus controllers
Webpack Encore + TailwindWebpack Encore + Bootstrap 5
caution

The legacy Smarty default back-office is still shipped for backward compatibility, but it is deprecated. New back-office work should target the default-twig bundle.

See Dual Templating for details.

Module system

Modules extend Thelia. A modern Thelia 3 module is almost free of XML: routes are #[Route] PHP 8 attributes auto-scanned by ModuleAttributeLoader, services are declared in configureServices() with autowire() + autoconfigure(), and hooks and loops are auto-discovered from their base classes. The only XML the core still requires is Config/module.xml (metadata, XSD module-2_2.xsd), plus Config/schema.xml when the module creates its own database tables.

local/modules/MyModule/
├── Config/
│ ├── module.xml # REQUIRED - metadata (XSD module-2_2.xsd)
│ ├── schema.xml # REQUIRED only if the module has DB tables
│ ├── TheliaMain.sql # Generated SQL (applied by `module:schema:apply`)
│ └── config.xml # OPTIONAL - exports/imports/parameters/loop aliases only
├── Controller/ # #[Route] PHP 8 attributes (no routing.xml)
├── Api/
│ ├── Resource/ # API resources (auto-discovered)
│ └── Addon/ # Resource enrichments (ResourceAddonInterface)
├── LiveComponent/ # Front-office components (#[AsLiveComponent])
├── Hook/ # Back-office hooks (extends BaseHook, auto-tagged)
├── templates/
└── MyModule.php # extends BaseModule + static configureServices()
No more service/route/hook XML

There is no routing.xml (routes are PHP attributes), and config.xml is optional: you only need it for exports, imports, parameters or loop aliases. Services, listeners, hooks and loops are registered through configureServices() and autoconfiguration. See the modules guide for the full skeleton.

See Modules vs Bundles for the difference between Thelia modules and Symfony bundles.

Data flow

Front-office request

1. HTTP Request


2. Symfony Router


3. Twig Template

├──► resources('/api/front/products')
│ │
│ ▼
│ DataAccessService
│ │
│ ▼
│ API Platform (internal)
│ │
│ ▼
│ Propel Query
│ │
│ ▼
│ JSON Response


4. LiveComponent Rendering


5. HTML Response

API request (external)

1. HTTP Request (JSON)


2. API Platform Router


3. State Provider


4. Propel Query


5. Serialization (groups)


6. JSON-LD Response

Key concepts

Resources vs addons

ConceptPurposeUse Case
ResourceFull API entityNew data model (e.g., ProductReview)
AddonExtend existing resourceAdd fields to Product, Customer

Serialization groups

Groups control which fields are exposed:

#[Groups([self::GROUP_ADMIN_READ])]  // Admin only
#[Groups([self::GROUP_FRONT_READ])] // Public front

DataAccessService

Internal API calls without HTTP overhead:

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

Next steps