Table des matières :
- Ce que vous remappez réellement quand vous touchez une route YAML Back Office
- « Sans changer l’URL » : ce que ça implique (et ce que ça n’implique pas)
- Remap propre via module : redéfinir la route (même nom + même path)
- Remap « sans toucher au YAML » : intercepter kernel.controller et substituer le callable
- Remap par conteneur : remplacer/décorer le service du contrôleur (route inchangée)
- Vérifications : debug router, cache Symfony, tokens/ACL et non-régression
Ce que vous remappez réellement quand vous touchez une route YAML Back Office
Sur PrestaShop 9.x (Symfony 6.4) avec PHP 8.2/8.3, une page Back Office « moderne » n’est plus pilotée par un contrôleur legacy AdminXXXController (héritage 1.6/1.7) mais par une route Symfony déclarée en YAML (ou via attributs), qui pointe vers un callable _controller (FQCN::method). Le terme « remapper une route YAML » signifie donc : changer le contrôleur exécuté pour une route donnée, sans toucher au reste (path, méthode HTTP, requirements, defaults).
Symfony résume l’enjeu sans détour : « Routing is the system that maps an HTTP request to a controller. » (Symfony Documentation – Routing). Dans PrestaShop, cette phrase est littéralement la réalité technique : à la fin, une URL BO résout un nom de route, un path, puis un contrôleur.
Dans un Back Office PrestaShop, il faut garder à l’esprit une nuance pratique : l’URL « visible » est presque toujours préfixée par le répertoire admin (ex. /admin-dev/ en environnement de dev, ou un dossier obfusqué en production). Quand vous remappez une route Symfony, vous intervenez généralement sur le chemin après ce préfixe, par exemple /sell/orders/. Autrement dit : vous ne touchez pas au front controller du BO, vous changez juste qui répond à un segment d’URL existant.
Côté PrestaShop, les routes BO sont aussi « hybridées » avec des métadonnées legacy. Vous verrez très souvent dans les defaults des clés du type _legacy_controller, _legacy_link, voire _legacy_parameters. Ce n’est pas cosmétique : ces champs servent à maintenir la compatibilité avec le système d’onglets (Tabs), la génération de liens (ex. Link::getAdminLink()), certaines permissions et la logique de token historique. Si vous remappez un contrôleur sans comprendre ces defaults, vous avez 3 problèmes possibles : 404 (route non trouvée), 403 (token/ACL), ou page BO qui s’affiche mais avec des actions qui cassent (boutons, redirections, grids).
Pour clarifier ce que vous « cassez » le plus souvent quand vous remappez un contrôleur BO, voici les points typiques :
- Fil d’Ariane / menu actif : souvent dérivés de la notion de contrôleur legacy (tabs) et de ses métadonnées.
- Génération des URLs internes : un module tiers peut continuer à appeler
Link::getAdminLink('AdminOrders'). Si_legacy_linkne correspond plus à la route, la génération (ou le token) peut diverger. - Permissions : PrestaShop s’appuie beaucoup sur la correspondance « contrôleur legacy ⇄ droits ». Si la route conserve l’URL mais perd l’identifiant legacy, vous pouvez créer un effet “tout passe en Super Admin, mais les profils restreints tombent en 403”.
L’objectif d’un remap « propre » n’est donc pas uniquement d’afficher une page : c’est de préserver l’intégration Back Office (navigation, actions, ACL, redirections), tout en changeant le point d’exécution.
« Sans changer l’URL » : ce que ça implique (et ce que ça n’implique pas)
« Ne pas changer l’URL » veut dire ne pas modifier le path: de la route, et idéalement ne pas changer non plus le nom de route si ce nom est référencé dans Twig, JS, redirections internes, ou modules tiers. Dans le BO PrestaShop, les routes sont largement consommées indirectement (générateur de liens Symfony, helpers PrestaShop, redirections post-action). Changer le nom de route peut casser des redirections sans que vous le voyiez au premier clic.
Mais « ne pas changer l’URL » n’implique pas automatiquement :
- que les paramètres attendus soient identiques (ex.
orderId,filters[...],page,sort), - que les méthodes HTTP restent cohérentes (GET/POST/DELETE),
- ni que les protections CSRF / tokens soient inchangées.
En pratique, vous devez raisonner comme si vous remplaciez une pièce dans un engrenage : l’interface (URL + paramètres + droits) doit rester stable.
Deuxième contrainte : le Back Office est un environnement authentifié avec des règles supplémentaires. Même si vous gardez l’URL identique, vous devez préserver le comportement sécurité : firewall admin, contrôles de droits, et token. Si votre remap remplace un contrôleur qui avait une vérification AdminSecurity (attribut / annotation PrestaShop), vous devez la reproduire, sinon vous ouvrez une régression d’accès (ou vous cassez l’accès légitime). Le résultat « marche chez moi » en compte super-admin ne vaut rien.
Pour donner un repère « sécurité » générique (et vérifiable) : l’OWASP Top 10 2021 classe A01: Broken Access Control comme risque n°1, ce qui reflète exactement le type de régression qu’on voit quand un remap enlève une vérification d’accès (source : OWASP Top 10 2021).
Troisième contrainte (souvent négligée) : le BO mélange routes Symfony et routes legacy derrière la façade /adminXXXX/. Beaucoup d’outils et modules continuent d’appeler Link::getAdminLink('AdminOrders'). Pour que cette génération de lien reste stable, il faut généralement conserver _legacy_link et/ou _legacy_controller sur la route remappée. Sans ça, vous « ne changez pas l’URL » en théorie, mais vous cassez la manière standard de la produire.
Checklist rapide « sans changer l’URL » (utile avant même d’écrire du code) :
- Le
path:reste strictement identique (attention aux slash finaux). - Le nom de route reste identique ou vous avez vérifié qu’il n’est pas consommé (Twig, JS, redirections).
- Les
methods:restent identiques. - Les defaults legacy (
_legacy_controller,_legacy_link) restent cohérents avec l’onglet BO visé. - Les paramètres requis (
requirements) et les noms de paramètres (ex.orderId) sont conservés. - Les permissions
AdminSecuritysont reconduites.
Pour le cadre général PrestaShop 9/Symfony, vous pouvez aussi recouper avec l’article interne sur la compatibilité Symfony des modules : PrestaShop modules : optimiser la compatibilité Symfony 6.4 et Admin API.
Remap propre via module : redéfinir la route (même nom + même path)
La méthode la plus « Symfony native » consiste à redéclarer la route (dans un module) en gardant le même path: (et souvent le même nom de route) mais en changeant le _controller. Avantage : vous ne touchez pas au cœur. Inconvénient : vous dépendez de l’ordre de chargement des routes (et donc potentiellement d’un changement en upgrade).
Dans Symfony, une RouteCollection est un dictionnaire : si vous réajoutez la même clé (le nom de route), la définition la plus récente remplace l’ancienne. C’est précisément ce qu’on exploite ici. Sur PrestaShop 9.x, les routes modules sont chargées via un loader dédié ; dans la pratique, vos routes modules peuvent surcharger des routes existantes si elles arrivent après.
Ce qui fait la différence entre un remap « qui marche aujourd’hui » et un remap « robuste » :
- vous recopiez les métadonnées utiles, pas seulement
_controller; - vous évitez les divergences silencieuses (ex. oublier
methods: [POST]sur une route de submit) ; - vous documentez le pourquoi (dans le module) pour que quelqu’un comprenne, 6 mois plus tard, pourquoi une route core pointe ailleurs.
Un mini-scenario réaliste côté BO : vous voulez remapper la liste des commandes pour ajouter un comportement local (ex. une colonne interne, un filtre supplémentaire, ou une redirection conditionnelle). Vous gardez l’URL /sell/orders/ pour ne pas perturber les utilisateurs, les bookmarks, ni les scripts internes qui ouvrent directement cette page.
Exemple minimal (module) : même route name, même URL, contrôleur remplacé
Pré-requis : PrestaShop 9.1/9.2, PHP 8.2+ ; accès SSH ou au moins accès filesystem + possibilité de vider le cache Symfony.
1) Dans modules/votre_module/config/routes.yml (ou config/routes/admin.yml selon votre structure), vous redéclarez la route cible.
# modules/votre_module/config/routes.yml
# Exemple : on remappe la route existante, sans changer l'URL.
prestashop_admin_orders_index:
path: /sell/orders/
methods: [GET]
defaults:
_controller: 'Vendor\\VotreModule\\Controller\\Admin\\Orders\\OrdersController::indexAction'
_legacy_controller: AdminOrders
_legacy_link: AdminOrders
Astuce pratique : si vous avez un doute sur la définition originale, faites d’abord un debug:router (voir section vérifications) et copiez les éléments sensibles (requirements, defaults legacy). L’erreur classique est de repartir d’un YAML « simplifié » et de perdre un détail qui n’explose qu’à la 3e action (export, pagination, tri…).
2) Vous implémentez votre contrôleur. Techniquement, vous pouvez réutiliser des services PrestaShop (GridFactory, form handlers, etc.). Le point important : gardez les attributs sécurité attendus.
<?php
// modules/votre_module/src/Controller/Admin/Orders/OrdersController.php
declare(strict_types=1);
namespace Vendor\VotreModule\Controller\Admin\Orders;
use PrestaShopBundle\Security\Annotation\AdminSecurity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class OrdersController extends AbstractController
{
#[AdminSecurity("is_granted('read', request.get('_legacy_controller'))")]
public function indexAction(): Response
{
// TODO: réutiliser la logique core ou la remplacer proprement.
return new Response('Orders index remappé');
}
}
Note importante : l’expression request.get('_legacy_controller') n’est pas un gadget : elle « raccroche » votre action au système de permissions lié au contrôleur legacy. Si vous changez _legacy_controller dans la route, vous changez implicitement la clé sur laquelle les droits sont évalués.
3) Vous déclarez le contrôleur comme service (si votre autowiring module ne le fait pas déjà), par exemple dans modules/votre_module/config/services.yml.
services:
Vendor\VotreModule\Controller\Admin\Orders\OrdersController:
autowire: true
autoconfigure: true
public: true
Points de contrôle indispensables :
debug:router prestashop_admin_orders_indexdoit pointer vers votre_controller.- le menu BO doit continuer à fonctionner (onglets, breadcrumb), ce qui dépend fortement de
_legacy_controller/_legacy_link. - les actions secondaires (exports, filtres grid, redirections après submit) doivent être testées.
- si la page initiale charge des assets spécifiques (JS/CSS), vérifiez que votre contrôleur rend bien le template attendu ou injecte les assets requis (sinon vous aurez des grilles « vides » ou des boutons inactifs).
Pour les problématiques d’overrides, et les pièges typiques en montée de version, l’article interne PrestaShop 9.2 : bonnes pratiques d’override et tests de compatibilité couvre la logique « ne pas patcher le core en prod » avec une vraie discipline de test.
Remap « sans toucher au YAML » : intercepter kernel.controller et substituer le callable
Si votre contrainte est plus stricte (ex. vous ne voulez pas dépendre du chargement des routes module, ou vous voulez conditionner le remap à un contexte), vous pouvez remplacer le contrôleur après résolution de la route, au niveau du Kernel Symfony.
Symfony documente explicitement le moment d’intervention : « The kernel.controller event is dispatched after the controller has been resolved, but before it is executed. » (Symfony Documentation – HttpKernel events). Concrètement : la route, l’URL et les paramètres sont identifiés, mais vous pouvez encore dire « exécute un autre contrôleur ».
Ce pattern est utile quand vous voulez :
- faire un remap conditionnel (par shop, par feature flag, par rôle employé, par paramètre GET, par environnement),
- éviter de dupliquer toute la définition YAML (methods, requirements, defaults),
- garder exactement la même route name, la même URL, et le même pipeline de génération de liens.
C’est aussi une bonne option quand vous voulez « injecter » une variante temporaire (ex. A/B test interne, migration progressive) sans figer dans un YAML du module une copie qui risque de diverger du core.
Exemple d’EventSubscriber
<?php
// modules/votre_module/src/EventSubscriber/AdminControllerRemapSubscriber.php
declare(strict_types=1);
namespace Vendor\VotreModule\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;
final class AdminControllerRemapSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly \Vendor\VotreModule\Controller\Admin\Orders\OrdersController $ordersController,
) {}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::CONTROLLER => 'onKernelController',
];
}
public function onKernelController(ControllerEvent $event): void
{
$request = $event->getRequest();
if ($request->attributes->get('_route') !== 'prestashop_admin_orders_index') {
return;
}
// Substitution : même route, même URL, mais autre callable.
$event->setController([$this->ordersController, 'indexAction']);
}
}
Et en services :
services:
Vendor\VotreModule\EventSubscriber\AdminControllerRemapSubscriber:
autowire: true
autoconfigure: true
Vendor\VotreModule\Controller\Admin\Orders\OrdersController:
autowire: true
autoconfigure: true
public: true
Variation très fréquente en Back Office : remap conditionnel selon le contexte multiboutique, ou selon un paramètre de bascule. Sans ajouter de sous-structure lourde, vous pouvez par exemple filtrer sur un attribut déjà présent dans la requête (ex. _legacy_controller) pour éviter de dépendre d’un nom de route qui pourrait évoluer :
- vérifier
$request->attributes->get('_legacy_controller') === 'AdminOrders', - ou vérifier que la requête est bien dans la zone admin (ex. présence de
_legacy_controller, ou préfixe de route admin si vous avez une convention interne).
Deux avertissements pratiques :
1) Sécurité : vous remplacez le contrôleur, donc les attributs AdminSecurity du contrôleur d’origine ne s’appliquent plus. Ajoutez les vôtres, ou réutilisez exactement la même logique de permission.
2) Maintenance : si PrestaShop renomme la route en upgrade, votre subscriber ne s’exécutera plus. Ajoutez un test automatique (voir section CI) et, idéalement, logguez (en dev) quand la substitution se produit pour diagnostiquer rapidement une mise à jour qui aurait déplacé la cible.
Remap par conteneur : remplacer/décorer le service du contrôleur (route inchangée)
Troisième technique : ne pas toucher à la route, mais remplacer le service Symfony du contrôleur ciblé par _controller. Ça marche uniquement si le contrôleur est instancié via le container (ce qui est généralement le cas sur PrestaShop 9.x, mais il faut vérifier).
La logique est simple : si la route pointe vers PrestaShopBundle\Controller\Admin\X\YController::action, Symfony va résoudre ce contrôleur ; si la classe est connue du container comme service (souvent service id = FQCN), vous pouvez substituer l’implémentation. C’est une technique DI classique, mais elle devient fragile dès que :
- le core change la signature du constructeur,
- le core change la classe du contrôleur,
- le core n’expose plus le contrôleur comme service public.
Un remplacement « brut » ressemble à ça :
# modules/votre_module/config/services.yml
services:
# On remplace la définition du contrôleur core par notre classe.
PrestaShopBundle\Controller\Admin\Sell\Order\OrderController:
class: Vendor\VotreModule\Controller\Admin\Sell\Order\OrderController
autowire: true
autoconfigure: true
public: true
Dans un contexte PrestaShop, cette technique est surtout acceptable si :
- vous cherchez à conserver exactement la même intégration (templates, services injectés, conventions internes),
- et vous avez un périmètre de modifications limité (ex. adapter une partie d’une action, ajouter une garde, modifier un redirect).
En revanche, c’est rarement la meilleure option pour une refonte complète : vous vous collez davantage au couplage interne du core (constructeur, dépendances, private services).
Si votre motivation initiale est « je veux faire presque pareil que le core, mais avec un petit twist », une variante plus contrôlée (quand c’est possible dans votre contexte) est la décoration de service : elle permet de garder le service original accessible et d’intercepter certains appels. Attention : cette stratégie dépend de la manière dont le contrôleur (et ses dépendances) sont définis ; elle n’est pas universelle.
Cette approche doit être traitée comme du code « à risque » : documentez-la, verrouillez-la par tests, et préparez une stratégie de rollback. Si votre substitution plante, vous retombez typiquement sur une erreur 500 au chargement d’une page admin (voir : Erreur 500 côté PrestaShop : causes fréquentes et solutions).
Vérifications : debug router, cache Symfony, tokens/ACL et non-régression
Sur PrestaShop 9.x, remapper un contrôleur Back Office « sans changer l’URL » échoue plus souvent pour des raisons d’outillage que de code. Commencez par des vérifications mécaniques, répétables.
1) Vérifier la résolution de route et le contrôleur final.
- Affichez la route :
php bin/console debug:router prestashop_admin_orders_index
Vous voulez voir le path, les methods, et surtout le _controller attendu.
- Option utile pour éviter les confusions : afficher les contrôleurs pour tout le router (ça peut être volumineux, mais très parlant quand une surcharge ne s’applique pas) :
php bin/console debug:router --show-controllers | grep prestashop_admin_orders_index
- Vérifiez aussi la correspondance « URL → route » avec :
php bin/console router:match /sell/orders/
(à exécuter dans le contexte où votre admin est configuré, sinon adaptez le chemin testé).
- Si vous utilisez un subscriber
kernel.controller, logguez explicitement la substitution (endev) pour éviter de diagnostiquer à l’aveugle. Un simplelogger->info()avec la route et le contrôleur substitué vous fait gagner beaucoup de temps lors d’un upgrade.
2) Cache : une route modifiée non prise en compte est 90% du temps un cache non vidé.
- En prod :
php bin/console cache:clear --env=prod
- En cas de doute (hébergement contraint), purge manuelle :
var/cache/prod/*etvar/cache/dev/*(avec précaution, et uniquement si vous maîtrisez vos droits FS).
Astuce “qualité” : ajoutez aussi un lint YAML quand vous touchez à des routes. Une indentation ou un caractère échappé mal géré peut provoquer des comportements contre-intuitifs :
php bin/console lint:yaml modules/votre_module/config/routes.yml
Sur des stacks « opaques » (mutualisé, surcouches), gardez en tête que les problèmes de droits/FS peuvent aussi mimer une régression routing. Si vous êtes sur OVH par exemple, la partie « cache/permissions/perf » a des spécificités (voir : Optimisations PrestaShop sur OVH : performance, sécurité, UX).
3) Sécurité et régressions fonctionnelles : testez en vrai, pas uniquement en super-admin.
Testez avec un profil employé restreint (droits lecture/écriture) pour valider que vos
AdminSecuritysont corrects.Vérifiez la génération de liens (boutons « Ajouter », « Exporter », pagination grid). Beaucoup passent par
_legacy_link.Vérifiez aussi les scénarios « secondaires » qui cassent souvent après un remap :
action de tri/pagination qui conserve des
filters[...],export CSV,
redirection après action (ex. après édition, retour à la liste),
affichage d’un message flash (succès/erreur) si votre contrôleur modifie le flux standard.
Ajoutez un test end-to-end minimal (Playwright/Cypress) ou au moins une étape de smoke test automatisée. Le bon test n’a pas besoin d’être long : il doit simplement ouvrir la route, vérifier un élément stable de la page, puis cliquer une action critique (ex. pagination). Si vous avez déjà une chaîne CI/CD, branchez ce contrôle dans votre pipeline (voir : CI/CD PrestaShop : générer des démos à la demande).
Enfin, si votre motivation initiale est de « corriger le core », gardez en tête la dette : chaque remap est un fork comportemental. Dans l’écosystème PrestaShop, c’est acceptable, mais uniquement si vous avez une discipline de suivi de versions et de tests. Le but n’est pas d’être élégant : c’est d’être prédictible en prod.




