Skip to main content
Version: Thelia 3

Domain Facades

Facades in Thelia 3 are the main entry point for business logic. They coordinate several services and expose a single API for common operations.

Purpose

A facade does four things:

  1. Combines several service calls into a single method, so a controller does not have to wire them together.
  2. Holds the business rules for its domain: validation, authorization, and side effects.
  3. Gives easy access to session state, such as the current cart or customer.
  4. Acts as the single source of truth for its domain operations.

Available facades

These are the four facades you will use most often in a front office.

FacadeNamespacePurpose
CartFacadeThelia\Domain\CartCart operations, items, addresses
CustomerFacadeThelia\Domain\CustomerAuthentication, registration, profile
OrderFacadeThelia\Domain\OrderOrder creation
CheckoutFacadeThelia\Domain\CheckoutCheckout process orchestration
note

These four are the most common front-office facades, but they are not the only ones. The core ships around sixteen domain facades. The others are organized by domain under Thelia\Domain\:

  • AddressFacade in Thelia\Domain\Addressing
  • ShippingFacade in Thelia\Domain\Shipping
  • ProductFacade and PSEFacade in Thelia\Domain\Catalog\Product
  • CategoryFacade in Thelia\Domain\Catalog\Category
  • BrandFacade in Thelia\Domain\Catalog\Brand
  • CurrencyFacade in Thelia\Domain\Catalog\Currency
  • TaxFacade in Thelia\Domain\Catalog\Tax
  • MediaFacade in Thelia\Domain\Media
  • ContentFacade in Thelia\Domain\CMS\Content
  • LocalizationFacade in Thelia\Domain\Localization

Some catalog facades are nested deeper than the top-level domain, such as Thelia\Domain\Catalog\Product, so confirm the exact namespace against the class file before importing it.

CartFacade

Manages shopping cart operations.

Location: core/lib/Thelia/Domain/Cart/CartFacade.php

Usage

<?php

declare(strict_types=1);

use Thelia\Domain\Cart\CartFacade;
use Thelia\Domain\Cart\DTO\CartItemAddDTO;
use Thelia\Domain\Cart\DTO\CartItemDeleteDTO;
use Thelia\Domain\Cart\DTO\CartItemUpdateQuantityDTO;

final readonly class CartController
{
public function __construct(
private CartFacade $cartFacade,
) {}

public function addToCart(int $productId, int $productSaleElementId, int $quantity): void
{
$cart = $this->cartFacade->getOrCreateFromSession();

$dto = new CartItemAddDTO(
cart: $cart,
productId: $productId,
productSaleElementId: $productSaleElementId,
quantity: $quantity,
);

$cartItem = $this->cartFacade->addItem($dto);
}

public function removeFromCart(Cart $cart, int $cartItemId): void
{
$dto = new CartItemDeleteDTO(cart: $cart, cartItemId: $cartItemId);
$this->cartFacade->removeItem($dto);
}

public function updateQuantity(Cart $cart, int $cartItemId, int $newQuantity): void
{
$dto = new CartItemUpdateQuantityDTO(
cart: $cart,
cartItemId: $cartItemId,
quantity: $newQuantity,
);

$cartItem = $this->cartFacade->updateItemQuantity($dto);
}
}

Methods

MethodDescription
addItem(CartItemAddDTO)Add product to cart
removeItem(CartItemDeleteDTO)Remove item from cart
updateItemQuantity(CartItemUpdateQuantityDTO)Update item quantity
setDeliveryAddress(CheckoutDTO)Set delivery address
setInvoiceAddress(CheckoutDTO)Set invoice address
setDeliveryModule(CheckoutDTO)Select shipping method
setPaymentModule(CheckoutDTO)Select payment method
recalculatePostage(Cart)Force shipping recalculation
reset(bool)Reset cart data
getCartFromSession()Get current cart (nullable)
getOrCreateForCustomer(Customer)Get or create a cart for a given customer
getOrCreateFromSession()Get or create cart from the current session
getDeliveryAddressId()Get selected delivery address
getInvoiceAddressId()Get selected invoice address
getDeliveryModuleId()Get selected shipping module
getPaymentModuleId()Get selected payment module

CustomerFacade

Manages customer authentication and account operations.

Location: core/lib/Thelia/Domain/Customer/CustomerFacade.php

Usage

<?php

declare(strict_types=1);

use Thelia\Domain\Customer\CustomerFacade;
use Thelia\Domain\Customer\DTO\CustomerRegisterDTO;

final readonly class AccountController
{
public function __construct(
private CustomerFacade $customerFacade,
) {}

public function getCurrentUser(): ?Customer
{
return $this->customerFacade->getCurrentCustomer();
}

public function isAuthenticated(): bool
{
return $this->customerFacade->isLoggedIn();
}

public function register(CustomerRegisterDTO $dto): Customer
{
return $this->customerFacade->register($dto);
}

public function logout(): void
{
$this->customerFacade->logout();
}
}

Methods

MethodDescription
login(CustomerLogin): voidAuthenticate with credentials, set the session and remember-me cookie
logout(): voidEnd customer session
getCurrentCustomer(): ?CustomerGet authenticated customer (nullable)
isLoggedIn(): boolCheck if customer is authenticated
register(CustomerRegisterDTO): CustomerCreate new account, returns the created customer
update(CustomerRegisterDTO, Customer): voidUpdate an existing customer profile
sendCode(Customer): voidResend an account code email
caution

update() returns void, not Customer. The updated state is applied in place on the Customer model you pass in. Only register() returns a Customer.

CheckoutFacade

Orchestrates the checkout flow: selecting addresses and modules, validating the cart, paying, and cancelling.

Location: core/lib/Thelia/Domain/Checkout/CheckoutFacade.php

Usage

<?php

declare(strict_types=1);

use Symfony\Component\HttpFoundation\Response;
use Thelia\Domain\Checkout\CheckoutFacade;
use Thelia\Domain\Checkout\DTO\CheckoutDTO;
use Thelia\Model\Cart;

final readonly class CheckoutController
{
public function __construct(
private CheckoutFacade $checkoutFacade,
) {}

public function placeOrder(Cart $cart): ?Response
{
$dto = new CheckoutDTO(
cart: $cart,
deliveryModuleId: 1,
deliveryAddressId: 42,
invoiceAddressId: 42,
paymentModuleId: 2,
);

// Validates the cart, then runs the payment module.
return $this->checkoutFacade->pay($dto);
}
}

Methods

MethodDescription
selectDeliveryAddress(CheckoutDTO): voidSelect the delivery address and refresh shipping
selectInvoiceAddress(CheckoutDTO): voidSelect the invoice address and refresh shipping
selectDeliveryModule(CheckoutDTO): voidSelect the delivery module and refresh shipping
selectPaymentModule(CheckoutDTO): voidSelect the payment module and refresh shipping
validateForOrder(Cart): voidCheck the cart is ready for order placement (items, addresses, payment)
pay(CheckoutDTO): ?ResponseValidate then run the payment module, returns the payment Response if any
cancelOrder(int): OrderCancel an order by its identifier
resetCheckout(): voidReset checkout selections on the cart and clear postage
note

CheckoutDTO is constructed from a Cart. Any identifier you leave null (delivery module, delivery/invoice address, payment module) is back-filled from the cart's current selection in the constructor. Pass only what you want to override.

OrderFacade

Creates an Order from a session order and a cart, within a single Propel transaction (stock decrement, taxes, addresses, virtual products).

Location: core/lib/Thelia/Domain/Order/OrderFacade.php

Usage

<?php

declare(strict_types=1);

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Thelia\Domain\Order\OrderFacade;
use Thelia\Core\Security\User\UserInterface;
use Thelia\Model\Cart;
use Thelia\Model\Currency;
use Thelia\Model\Lang;
use Thelia\Model\Order;

final readonly class PlaceOrderHandler
{
public function __construct(
private OrderFacade $orderFacade,
private EventDispatcherInterface $dispatcher,
) {}

public function place(
Order $sessionOrder,
Currency $currency,
Lang $lang,
Cart $cart,
UserInterface $customer,
): Order {
return $this->orderFacade->createOrder(
dispatcher: $this->dispatcher,
sessionOrder: $sessionOrder,
currency: $currency,
lang: $lang,
cart: $cart,
customer: $customer,
);
}
}

Methods

MethodDescription
createOrder(EventDispatcherInterface, Order, Currency, Lang, Cart, UserInterface, bool): OrderPersist a placed order from the session order and cart, in one transaction
note

createOrder() throws TheliaProcessException if the customer, currency, language, or cart has no identifier. The optional last argument bool $useOrderDefinedAddresses = false: when true, the existing OrderAddress rows are reused instead of creating new ones from the chosen addresses.

Using facades in LiveComponents

<?php

declare(strict_types=1);

namespace App\LiveComponent;

use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Thelia\Domain\Cart\CartFacade;
use Thelia\Domain\Cart\DTO\CartItemAddDTO;

#[AsLiveComponent(name: 'AddToCart')]
final class AddToCart
{
use DefaultActionTrait;

#[LiveProp]
public int $productSaleElementId;

#[LiveProp(writable: true)]
public int $quantity = 1;

public function __construct(
private readonly CartFacade $cartFacade,
) {}

#[LiveProp]
public int $productId;

#[LiveAction]
public function add(): void
{
$cart = $this->cartFacade->getOrCreateFromSession();

$this->cartFacade->addItem(new CartItemAddDTO(
cart: $cart,
productId: $this->productId,
productSaleElementId: $this->productSaleElementId,
quantity: $this->quantity,
));

$this->emit('cart:updated');
}
}

Using facades in Twig

Facades are mostly used in PHP, but you can also reach them in templates through services:

{# In a LiveComponent template #}
{% if this.customerFacade.isLoggedIn %}
<p>Welcome, {{ this.customerFacade.currentCustomer.firstname }}!</p>
{% endif %}

DTOs

Facades take Data Transfer Objects as method parameters:

CartItemAddDTO

readonly class CartItemAddDTO
{
public function __construct(
private Cart $cart,
private int $productId,
private int $productSaleElementId,
private int $quantity = 1,
private bool $append = true,
private bool $newness = true,
) {}
}

CartItemDeleteDTO

readonly class CartItemDeleteDTO
{
public function __construct(
private Cart $cart,
private int $cartItemId,
) {}
}

CartItemUpdateQuantityDTO

readonly class CartItemUpdateQuantityDTO
{
public function __construct(
private Cart $cart,
private int $cartItemId,
private int $quantity,
) {}
}

CustomerRegisterDTO

readonly class CustomerRegisterDTO
{
public function __construct(
private ?int $id = null,
private ?string $firstname = null,
private ?string $lastname = null,
private ?string $email = null,
private ?string $password = null,
private ?int $title = null,
private ?int $langId = null,
private ?string $sponsor = null,
private ?string $ref = null,
private ?float $discount = null,
private bool $forceEmailUpdate = false,
private bool $enabled = false,
private bool $reseller = false,
) {}
}

Creating custom facades

For module-specific business logic, write your own facades:

<?php

declare(strict_types=1);

namespace MyModule\Domain;

use MyModule\Service\WishlistService;
use MyModule\Service\WishlistNotificationService;

final readonly class WishlistFacade
{
public function __construct(
private WishlistService $wishlistService,
private WishlistNotificationService $notificationService,
) {}

public function addProduct(int $customerId, int $productId): void
{
$this->wishlistService->add($customerId, $productId);
$this->notificationService->notifyProductAdded($customerId, $productId);
}

public function removeProduct(int $customerId, int $productId): void
{
$this->wishlistService->remove($customerId, $productId);
}

public function getWishlist(int $customerId): array
{
return $this->wishlistService->getByCustomer($customerId);
}
}

A module facade placed under src/ needs no XML. As long as your module declares configureServices() with autowire() and autoconfigure(), the class is registered and its dependencies are injected automatically:

// local/modules/MyModule/MyModule.php
use Symfony\Component\DependencyInjection\Loader\Configurator\ServicesConfigurator;
use Thelia\Module\BaseModule;

final class MyModule extends BaseModule
{
public static function configureServices(ServicesConfigurator $services): void
{
$services->load(self::getModuleCode().'\\', __DIR__)
->autowire()
->autoconfigure();
}
}
note

configureServices() is mandatory: without it, no class in the module is scanned at all (auto-registration is reverted). Once it is present, you do not declare individual services such as a facade in config.xml. That file is optional in Thelia 3 and only needed for things configureServices() cannot express, such as <exports>, <imports>, <parameters>, or a <loop> alias.

Best practices

  1. Pass DTOs instead of long lists of primitive parameters.
  2. Keep facades thin: delegate to services rather than implementing logic in the facade.
  3. Give each facade a single domain to cover.
  4. Inject dependencies through the constructor.
  5. Mark facades readonly so they stay immutable.
  6. Type every method, parameter, and return value.

Next steps