Controllers
Controllers handle HTTP requests in your module. Thelia provides two base controller types, one for the front office and one for the back office.
Controller types
| Type | Base Class | Template engine | Authorization |
|---|---|---|---|
| Front | BaseFrontController | Active front-office template (Flexy / Twig) | Manual: call checkAuth() when a page requires a logged-in customer |
| Admin | BaseAdminController | Active back-office template | Manual: call checkAuth($resources, $modules, $accesses) per action |
Admin controllers render through the active back-office template, resolved by TheliaTemplateHelper (via getActiveAdminTemplate()). The reference back-office template in Thelia 3 is the default-twig bundle (Twig). The legacy Smarty default back-office theme is no longer recommended and is expected to be dropped in Thelia 3.1, so target Twig templates for new modules.
Extending BaseAdminController does not secure your routes by itself. You must call checkAuth() at the start of each action that needs protection (see Authorization Checks).
Front controllers
Front controllers render public-facing pages with Twig templates.
Basic front controller
<?php
declare(strict_types=1);
namespace MyProject\Controller\Front;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Thelia\Controller\Front\BaseFrontController;
final class PageController extends BaseFrontController
{
#[Route('/my-feature', name: 'myproject.front.index')]
public function indexAction(): Response
{
return $this->render('my-page');
}
#[Route('/my-feature/{id}', name: 'myproject.front.show', requirements: ['id' => '\d+'])]
public function showAction(int $id): Response
{
return $this->render('my-page-detail', [
'item_id' => $id,
]);
}
}
Template location
Templates are resolved from:
templates/frontOffice/{active_template}/modules/MyProject/local/modules/MyProject/templates/frontOffice/default/
Injecting services
<?php
declare(strict_types=1);
namespace MyProject\Controller\Front;
use MyProject\Service\ProductService;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Thelia\Controller\Front\BaseFrontController;
final class ProductController extends BaseFrontController
{
public function __construct(
private readonly ProductService $productService,
) {}
#[Route('/featured-products', name: 'myproject.front.featured')]
public function featuredAction(): Response
{
$products = $this->productService->getFeaturedProducts();
return $this->render('featured-products', [
'products' => $products,
]);
}
}
Accessing request data
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
#[Route('/search', name: 'myproject.front.search')]
public function searchAction(Request $request): Response
{
$query = $request->query->get('q', '');
$page = $request->query->getInt('page', 1);
$results = $this->searchService->search($query, $page);
return $this->render('search-results', [
'query' => $query,
'results' => $results,
'page' => $page,
]);
}
JSON responses
use Symfony\Component\HttpFoundation\JsonResponse;
#[Route('/api/check-availability/{productId}', name: 'myproject.front.check_availability')]
public function checkAvailabilityAction(int $productId): JsonResponse
{
$stock = $this->stockService->getAvailableStock($productId);
return new JsonResponse([
'available' => $stock > 0,
'quantity' => $stock,
]);
}
Requiring a logged-in customer
On the front office, checkAuth() takes no arguments. It throws a RedirectException to the login page when no customer is authenticated:
// core/lib/Thelia/Controller/Front/BaseFrontController.php
public function checkAuth(): void
Call it at the start of an action that must only be reachable by a logged-in customer:
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Thelia\Controller\Front\BaseFrontController;
final class AccountController extends BaseFrontController
{
#[Route('/my-account/orders', name: 'myproject.front.account_orders')]
public function ordersAction(): Response
{
// Redirects to the login page if no customer is logged in
$this->checkAuth();
$customer = $this->getSecurityContext()->getCustomerUser();
return $this->render('account-orders', [
'customer' => $customer,
]);
}
}
The argument-based form checkAuth($resources, $modules, $accesses) exists only on BaseAdminController. On BaseFrontController, checkAuth() is argument-less. Do not pass AccessManager constants to a front controller's checkAuth().
Coming back to the page after signing in
Since Thelia 3.1, a visitor bounced to the login page comes back to where they were instead of landing on their account. checkAuth() appends the page it is leaving as a redirect query parameter, and Thelia\Domain\Customer\Service\AuthenticationReturnUrl is what a theme uses on the other end:
use Thelia\Domain\Customer\Service\AuthenticationReturnUrl;
public function __construct(
private readonly AuthenticationReturnUrl $returnUrl,
) {
}
// On the sign-in and registration screens: remember where the visitor came from
$this->returnUrl->capture();
// Once the customer is signed in, or registered
return $this->generateRedirect($this->returnUrl->consume('/account'));
of($request)gives the value a sign-in link should carry for the page it sits on.capture()stores the parameter of the current request in the session, underthelia.authentication_return_url, so it survives the POST that signs the customer in and the detour through the registration form. A request without the parameter leaves what is remembered untouched.consume()reads the destination back and clears it, the parameter of the current request winning over the remembered one, and falls back to the URL you pass when there is nothing to come back to.forget()drops it, for a flow that decides its destination itself.
Every URL that goes through the service is checked by Thelia\Tools\RedirectUrl::isSafe(): an empty value, a protocol-relative //host target, a backslash, a scheme other than http or https, or an absolute URL on another host is refused, and the fallback applies. An open redirect on the login form is the classic phishing lever, so do not bypass the check by reading the parameter yourself.
Redirects
use Symfony\Component\HttpFoundation\RedirectResponse;
#[Route('/old-page', name: 'myproject.front.old_page')]
public function oldPageAction(): RedirectResponse
{
// getRoute() turns a route id into a URL string, generateRedirect() wraps it in a RedirectResponse
return $this->generateRedirect(
$this->getRoute('myproject.front.new_page')
);
}
generateRedirectFromRoute() combines both steps. Pass URL parameters as the second argument:
return $this->generateRedirectFromRoute(
'myproject.front.show',
[], // extra query parameters appended to the URL
['id' => 42], // route parameters (placeholders)
);