Table des matières :
- Comprendre le « CRUD backoffice » PrestaShop : Legacy AdminController vs pages Symfony (Grid + Form)
- Pré-requis, versions et squelette minimal d’un module CRUD exploitable
- Personnaliser un listing (Grid) : colonnes, filtres, tri… et éviter les listings « lents par design »
- Ajouter des actions (ligne et masse) : pattern, routing, tokens et contrôle d’accès
- Personnaliser la vue détail : contrôleur, Twig, et formulaires admin sans « template spaghetti »
- Débogage, qualité, CI et déploiement : rendre votre CRUD maintenable (pas juste fonctionnel)
Comprendre le « CRUD backoffice » PrestaShop : Legacy AdminController vs pages Symfony (Grid + Form)
Dans PrestaShop 8/9, « faire un CRUD » en module ne veut pas dire la même chose selon que vous restez sur le socle legacy (AdminController) ou que vous basculez sur les pages Symfony (Controller + Twig + Grid + Form). Le legacy reste omniprésent (et parfois incontournable pour certaines pages), mais la personnalisation fine des listings, des actions et des vues détail est beaucoup plus maîtrisable côté Symfony, parce que l’UI est structurée autour de composants réutilisables (Grid, Form, CQRS, services).
Concrètement, un CRUD backoffice bien conçu répond à quatre besoins très terre-à-terre côté exploitation :
- Lister (avec filtres et tri fiables) sans mettre la base à genoux.
- Agir (sur une ligne ou en masse) avec contrôle d’accès, traçabilité et sécurité HTTP.
- Consulter une vue détail utile (diagnostic, données liées, historique).
- Éditer en respectant les règles métier (validation, contraintes d’unicité, multi-boutique si nécessaire).
Le composant Grid du backoffice est le cœur technique des listings modernes : définition (colonnes, filtres, actions), construction de requêtes (QueryBuilder), puis hydratation/formatage des lignes. La séparation Definition / Query / Data évite le pattern « tout dans le contrôleur » qu’on a connu sur certaines pages legacy. Pour comprendre l’intention et la structure attendue, la documentation officielle est le point de départ le plus sûr :
Le point à retenir pour un module CRUD : vous n’êtes pas obligé de « cloner » les écrans du core. Vous pouvez réutiliser l’architecture (Grid + FormHandler) et ne surcharger que ce qui est nécessaire : colonnes supplémentaires, actions custom, vues détail métier, contraintes de sécurité (ACL), et surtout performances SQL (indexes, pagination, tri).
Legacy vs Symfony : ce qui change vraiment (et ce qui ne change pas)
Sans entrer dans une guerre de chapelles, voici une lecture pragmatique des différences qui impactent directement un CRUD de module :
| Sujet | Legacy (AdminController) |
Pages Symfony (Grid + Form) |
|---|---|---|
| Listing | fields_list, overrides et hooks historiques |
GridDefinition + QueryBuilder + DataFactory |
| Actions | liens legacy + tokens legacy | routes Symfony + contrôles explicites + CSRF |
| Templates | Smarty, souvent très couplé au contrôleur | Twig, composition (includes), UI plus standardisée |
| Testabilité | faible (beaucoup de logique implicite) | meilleure (services injectés, séparation des responsabilités) |
| Rétrocompatibilité menu/tokens | nativement legacy | nécessite _legacy_controller / _legacy_link bien renseignés |
Si votre besoin est uniquement d’améliorer un listing existant (ex. : produits), un hook dédié suffit (voir par exemple le hook actionProductGridDataModifier détaillé ici : Hook actionProductGridDataModifier PrestaShop : emplacement, appel et usage développeur). À l’inverse, dès que vous introduisez une entité métier (ex. Vendors, importeurs, transporteurs “internes”, synchronisations ERP), une page Symfony dédiée devient vite plus rentable : structure plus claire, UX plus cohérente, et maintenance plus prévisible.
Pré-requis, versions et squelette minimal d’un module CRUD exploitable
Contexte technique recommandé (au moment d’écrire ces lignes) : PrestaShop 9.1+ avec Symfony 6.4 et PHP 8.1–8.5 (selon la version mineure de votre 9.1.x). Pour éviter les surprises en prod, alignez votre matrice de compatibilité sur la politique annoncée par le core : PrestaShop 9.1 : compatibilité PHP 8.1–8.5, CLI et nouveautés développeurs.
Côté dev, prévoyez un environnement reproductible (Docker/DDEV) et un debugger, parce que vous allez manipuler routing admin, services, QueryBuilder et templates. Pour l’outillage, gardez Composer dans vos conteneurs si vous travaillez en DDEV : DDEV Composer : exécuter et configurer Composer dans les conteneurs.
Structure : partez d’un module « moderne » (namespace, services, contrôleurs Symfony, templates Twig). Si vous n’avez pas de base propre, reprenez une structure validée : Module PrestaShop 9 : structure, services et bonnes pratiques Symfony.
Deux points qui font gagner (ou perdre) des heures
1) Routes admin + rétrocompatibilité menu
Le point non négociable : exposez vos pages Symfony via des routes admin avec _legacy_controller / _legacy_link cohérents, sinon vous allez vous battre avec le menu, les tokens et la rétrocompatibilité.
Un exemple typique (simplifié) de route admin correctement “reliée” au legacy :
# modules/myvendors/config/routes.yml
admin_myvendors_vendor_index:
path: /myvendors/vendors
methods: [GET]
defaults:
_controller: 'Myvendors\Controller\Admin\VendorController::index'
_legacy_controller: AdminMyvendorsVendor
_legacy_link: AdminMyvendorsVendor
admin_myvendors_vendor_show:
path: /myvendors/vendors/{vendorId}
methods: [GET]
defaults:
_controller: 'Myvendors\Controller\Admin\VendorController::show'
_legacy_controller: AdminMyvendorsVendor
requirements:
vendorId: '\d+'
2) Base de données : conventions + multi-boutique (si concerné)
Un CRUD backoffice finit presque toujours par toucher la base. Même si vous démarrez “simple”, anticipez :
- nommage de table (
ps_myvendors_vendor,ps_myvendors_vendor_shopsi multi-shop), - indexes (sur champs filtrés/triés),
- champs de traçabilité (
date_add,date_upd, éventuellementlast_sync_at), - et règles d’unicité (ex.
external_idunique, ou unique par shop).
Risques et garde-fous : un CRUD backoffice touche au routing (backoffice = contexte auth + tokens), aux ACL et souvent à la base. Faites une sauvegarde avant toute migration SQL (même en dev, ça évite de déboguer « à l’aveugle »). En production, n’injectez pas de logique métier lourde dans les templates Twig : vous payez ça en temps CPU et en complexité de debug. Pour le debug PHP local, Xdebug + VS Code reste le chemin le plus court pour tracer un QueryBuilder ou un FormHandler : Xdebug VS Code : configurer le débogage PHP en local.
Exemple de squelette (extraits) pour un CRUD « Vendors » (entité métier du module) :
modules/myvendors/
myvendors.php
config/services.yml
config/routes.yml
src/Controller/Admin/VendorController.php
src/Grid/Definition/Factory/VendorGridDefinitionFactory.php
src/Grid/Query/VendorQueryBuilder.php
src/Grid/Filters/VendorFilters.php
views/templates/admin/vendor/index.html.twig
views/templates/admin/vendor/show.html.twig
Personnaliser un listing (Grid) : colonnes, filtres, tri… et éviter les listings « lents par design »
La personnalisation d’un listing Symfony se joue à trois étages : GridDefinitionFactory (structure), QueryBuilder (SQL), puis DataFactory/DataModifier (formatage). Dans un module CRUD, le plus propre est de déclarer votre propre Grid plutôt que de « bricoler » un AdminController avec une fields_list à l’ancienne. Le legacy marche, mais vous perdez : filtres typés, actions standardisées, et surtout une architecture testable.
Côté définition, vous ajoutez colonnes et filtres en restant strict sur le typage (DateTime, bool, string) et en pensant dès le début à l’exploitabilité : colonne « statut », colonne « dernière synchro », colonne « environnement », etc. Exemple (simplifié) :
// src/Grid/Definition/Factory/VendorGridDefinitionFactory.php
use PrestaShop\PrestaShop\Core\Grid\Column\DataColumn;
use PrestaShop\PrestaShop\Core\Grid\Filter\Filter;
use PrestaShop\PrestaShop\Core\Grid\Definition\Factory\AbstractGridDefinitionFactory;
final class VendorGridDefinitionFactory extends AbstractGridDefinitionFactory
{
protected function getId(): string { return 'myvendors_vendor'; }
protected function getName(): string { return $this->trans('Vendors', [], 'Modules.Myvendors.Admin'); }
protected function getColumns()
{
$columns = parent::getColumns();
$columns
->add((new DataColumn('id_vendor'))
->setName('ID')
->setOptions(['field' => 'id_vendor']))
->add((new DataColumn('name'))
->setName($this->trans('Nom', [], 'Admin.Global'))
->setOptions(['field' => 'name']))
->add((new DataColumn('last_sync_at'))
->setName($this->trans('Dernière synchro', [], 'Modules.Myvendors.Admin'))
->setOptions(['field' => 'last_sync_at']));
return $columns;
}
protected function getFilters()
{
return (new Filter('name', 'text'))
->setAssociatedColumn('name');
}
}
Le vrai sujet : la requête (et l’anticipation des volumes)
Le piège classique n’est pas « comment ajouter une colonne », c’est comment garder un listing rapide une fois que vous ajoutez des champs calculés, des jointures et des tris. Deux règles : (1) indexez ce que vous filtrez/ordonnez, (2) bannissez les sous-requêtes corrélées par ligne.
Un bon réflexe consiste à formaliser, dès le début, les colonnes qui seront triables et filtrables. Par exemple, si vous filtrez souvent par active et triez par last_sync_at, il est fréquent qu’un index composite soit plus efficace qu’une collection d’index “au hasard”.
Exemple d’aide-mémoire (à adapter à votre schéma) :
| Usage backoffice | Champs typiques | Index suggéré |
|---|---|---|
| Filtrer “actif/inactif” | active |
KEY active (active) |
| Tri “dernière synchro” | last_sync_at |
KEY last_sync_at (last_sync_at) |
| Filtre texte “nom” | name |
selon usage : KEY name (name) (attention au LIKE %...%) |
| Unicité métier | external_id |
UNIQUE external_id (external_id) |
Pour vérifier, activez un slow query log et mesurez (temps, rows examined). Si vous ne l’avez jamais fait sur une boutique réelle, ce guide vous évite les erreurs de base : Requêtes MySQL lentes PrestaShop : activer slow query log. Et pour aller plus loin, travaillez vos indexes avec EXPLAIN : Index MySQL : optimiser WHERE, JOIN et ORDER BY avec EXPLAIN.
Mini-scenario “terrain” (catalogue volumineux)
Sur une boutique B2B (souvent hébergée en France/UE pour des raisons contractuelles et de conformité), le support passe la journée dans le backoffice. Si votre grid ajoute une colonne “statut ERP” obtenue via un JOIN non indexé, l’écran Vendors peut devenir plus lent que les pages front, et vous créez un goulot d’étranglement opérationnel : lenteur ressentie, timeouts PHP-FPM, et parfois contention MySQL si plusieurs employés filtrent en même temps.
Dans des catalogues >100k produits, on voit des grids passer de ~150–300 ms à plusieurs secondes juste pour un tri sur une colonne calculée. Si le besoin est récurrent, externalisez l’info dans une table de materialized data et mettez-la à jour via cron/queue, plutôt que de recalculer à chaque affichage.
Enfin, si vous personnalisez un grid du core (ex. Product grid) via hook, traitez ça comme une extension « à coût marginal ». Ajouter une colonne qui fait un JOIN sur une table volumineuse sans index, c’est transformer un écran backoffice en générateur de charge.
Ajouter des actions (ligne et masse) : pattern, routing, tokens et contrôle d’accès
Une fois le listing propre, la valeur d’un module CRUD vient souvent des actions : bouton « voir », « éditer », mais aussi « synchroniser », « exporter CSV », « invalider cache », « relancer un traitement ». Sur Grid, vous avez typiquement deux familles : Row actions (par ligne) et Bulk actions (multi-sélection). L’enjeu n’est pas esthétique : c’est de rendre l’action auditable, sécurisée et idempotente.
Côté implémentation, privilégiez les actions qui pointent vers une route Symfony (LinkRowAction) plutôt que des endpoints legacy ad hoc. Ça vous donne : génération d’URL fiable, contrôle d’accès centralisé et gestion des exceptions.
Exemple : une action « Détail » (show) et une action « Synchroniser » (sync) qui déclenche un job (ou au minimum une commande) côté serveur.
// Dans votre GridDefinitionFactory : ajout d'actions de ligne
use PrestaShop\PrestaShop\Core\Grid\Action\Row\Type\LinkRowAction;
$rowActions = $this->actionCollectionFactory->getRowActionCollection();
$rowActions
->add((new LinkRowAction('show'))
->setName($this->trans('Voir', [], 'Admin.Actions'))
->setIcon('visibility')
->setOptions([
'route' => 'admin_myvendors_vendor_show',
'route_param_name' => 'vendorId',
'route_param_field' => 'id_vendor',
]))
->add((new LinkRowAction('sync'))
->setName($this->trans('Sync', [], 'Modules.Myvendors.Admin'))
->setIcon('sync')
->setOptions([
'route' => 'admin_myvendors_vendor_sync',
'route_param_name' => 'vendorId',
'route_param_field' => 'id_vendor',
]));
Bulk actions : utiles, mais à cadrer
Les actions de masse sont celles qui causent le plus d’incidents si elles ne sont pas cadrées : elles peuvent toucher beaucoup d’enregistrements d’un coup, et elles sont souvent déclenchées par des profils “catalogue” non techniques.
Deux bonnes pratiques :
- Limiter : ajoutez une confirmation + un seuil (ex. refuser >1000 IDs sans mode asynchrone).
- Tracer : loggez l’employé, l’heure, le nombre d’éléments, et le résultat (succès/échec). En contexte RGPD, restez proportionné : tracez l’opération, pas des données personnelles inutiles.
Dans l’UI, une bulk action sert typiquement à :
- resynchroniser une sélection,
- activer/désactiver,
- exporter une sélection (CSV),
- recalculer un champ dérivé.
Sécurité : permissions + CSRF + méthode HTTP
Les actions admin ne doivent pas « faire confiance » au fait qu’on est en backoffice. Vérifiez systématiquement les permissions (ACL/roles) et protégez les actions mutables par un token CSRF.
Référence utile (principe et implémentation) : Symfony — CSRF documentation
En pratique : pour une action « sync », évitez le GET qui modifie l’état. Faites un POST avec formulaire minimal + token, puis appliquez le pattern POST → Redirect → GET (pour éviter les doubles soumissions au refresh). Et si l’action peut durer (API externe, recalcul), sortez-la du cycle HTTP : commande CLI, queue, ou au moins un traitement asynchrone.
Pour industrialiser les actions sensibles, le pattern « commande CLI + audit » est plus robuste (et observable via logs). Si vous avez déjà des commandes, gardez une doc interne sur vos scripts : Commandes CLI PrestaShop : liste, catégories et options d’aide.
Personnaliser la vue détail : contrôleur, Twig, et formulaires admin sans « template spaghetti »
Le listing est le point d’entrée, mais la vue détail est là où le module CRUD devient réellement utile : diagnostics, timeline, données liées, logs de synchro, erreurs, et actions contextualisées.
Sur PrestaShop 9, vous pouvez rester purement Symfony : un contrôleur admin, une route, puis un template Twig. Si vous avez besoin d’édition, utilisez les briques PrestaShop (FormBuilder + FormHandler) plutôt que du Symfony Form brut « isolé », car vous allez gagner la cohérence UI et l’intégration avec le contexte backoffice (traductions, layout, messages flash, validations).
Pour la partie Twig, restez simple : layout standard backoffice, blocs clairement séparés, et aucune requête SQL dans la vue. Si vous devez afficher des sous-objets (ex. derniers jobs, derniers retours API), préparez un DTO côté contrôleur.
Un pattern qui marche bien sur des vues détail “métier” :
- un résumé (statut, dates, flags),
- un bloc actions (sync, export, etc.),
- une section historique (dernières opérations, erreurs),
- une section données liées (liens vers produits, commandes, etc., si pertinent).
Pour vous remettre dans le bain Twig rapidement, ce rappel est utile : Twig PHP : syntaxe essentielle, héritage de templates et includes.
Exemple minimal de contrôleur « show » :
// src/Controller/Admin/VendorController.php
namespace Myvendors\Controller\Admin;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
final class VendorController extends AbstractController
{
#[Route(
path: '/myvendors/vendors/{vendorId}',
name: 'admin_myvendors_vendor_show',
requirements: ['vendorId' => '\\d+']
)]
public function show(int $vendorId): Response
{
// Ici : récupérer vendor + stats via repository/service (pas dans Twig)
return $this->render('@Modules/myvendors/views/templates/admin/vendor/show.html.twig', [
'vendorId' => $vendorId,
'vendor' => [/* ... */],
'stats' => [/* ... */],
]);
}
}
Et côté Twig, gardez l’HTML lisible en découpant. Exemple d’approche (extrait) :
{# views/templates/admin/vendor/show.html.twig #}
{% extends '@PrestaShop/Admin/layout.html.twig' %}
{% block content %}
{% include '@Modules/myvendors/views/templates/admin/vendor/_summary.html.twig' with { vendor: vendor, stats: stats } %}
{% include '@Modules/myvendors/views/templates/admin/vendor/_history.html.twig' with { vendorId: vendorId } %}
{% endblock %}
Édition : validation métier, unicité, transactions
Pour l’édition (le « U » du CRUD), ne sous-estimez pas les détails : validation métier, contraintes d’unicité, et gestion transactionnelle. Un cas fréquent : un champ “code fournisseur” doit être unique, mais seulement dans un shop en multi-boutique. Sans règle claire, vous introduisez des incohérences difficiles à rattraper.
Si vous utilisez Doctrine dans votre module, faites-le proprement (Entity, repository, migrations, transactions) et isolez la couche d’accès aux données. Vous pouvez vous appuyer sur une approche Symfony/Doctrine éprouvée dans l’écosystème : Symfony PrestaShop : développer des modules robustes avec Doctrine. Attention cependant : mélanger Doctrine (module) et Db (legacy) sans discipline mène vite à des incohérences de transaction et à du « double mapping » implicite.
Débogage, qualité, CI et déploiement : rendre votre CRUD maintenable (pas juste fonctionnel)
Un module CRUD backoffice « marche » en dev dès que les routes répondent et que le listing s’affiche. En prod, ce qui casse en premier, c’est : permissions, performance, et régressions lors des updates PrestaShop. Le socle Symfony aide, mais uniquement si vous outillez correctement.
Pour les bugs de Grid (filtres qui ne passent plus, tri erratique, paramètre manquant), Xdebug est souvent plus rentable que des dump() dispersés : Xdebug VS Code : configurer le débogage PHP en local. Complétez par une config d’erreurs propre dev/prod, sinon vous allez masquer les exceptions utiles : Gestion d’erreur PHP : bonnes pratiques et configuration développement/production.
Checklist “qualité minimale” pour un CRUD admin
Sans sur-industrialiser, une base saine ressemble souvent à ça :
- Contrôle d’accès : chaque route admin vérifie un droit (profil/roles), pas seulement “être connecté”.
- CSRF : toute action mutante (sync, delete, bulk) passe en POST + token.
- Performance : EXPLAIN sur les requêtes de listing + indexes sur filtres/tri.
- Logs : une trace exploitable (action, employé, résultat) sans sur-collecte.
- Ergonomie : messages flash clairs (“X éléments traités, Y en erreur”), pas juste “OK”.
- Compatibilité : test rapide sur un volume réaliste (copie anonymisée ou jeu de données).
Qualité statique : un CRUD touche à beaucoup de surface (controllers, services, templates, SQL). Sans garde-fous, vous allez accumuler du « code admin » qui ne se refactorise pas. En 2026, l’outillage standard côté PHP reste PHPStan + Rector (typage, règles, refactorings) : PHPStan et Rector : industrialiser la qualité du code PHP.
Pour les modules distribués à des clients, ajoutez une CI minimale (lint, static analysis, packaging) et des contrôles de provenance/dépendances : CI PrestaShop : provenance, SBOM et validation automatique des modules.
Déploiement et exploitation : le “runbook” qui évite les retours
Le CRUD backoffice est souvent exécuté par des équipes (support, catalogue, admin). Une régression sur un listing peut bloquer l’exploitation. Avant livraison, faites un « runbook » simple :
- temps de chargement cible (ex. < 500 ms sur page 1, < 1 s avec filtres),
- test sur gros volumes (pagination, tri, filtres combinés),
- comportement des filtres (valeurs vides, caractères spéciaux, accents),
- test de permissions (profil employé restreint),
- test d’actions mutantes (POST + token, confirmation UI),
- stratégie d’échec (message clair + logs + possibilité de relancer).
Sur les boutiques sous charge, l’impact d’un backoffice lent est réel (verrouillage de sessions admin, timeouts PHP-FPM, saturation DB). Si vous suspectez déjà des limites d’infra, commencez par objectiver : monitoring, slow queries, cache serveur. Deux lectures utiles selon votre contexte : Audit performance PrestaShop : méthode en 6 étapes reproductibles et, si vous industrialisez Redis côté cache, Redis PrestaShop : configurer le cache sur VPS ou serveur dédié.
Le dernier point, souvent oublié : si votre objectif est uniquement d’enrichir le catalogue (colonnes et actions) sans développer un CRUD complet, évaluez le coût de maintenance. Un module « listing-only » s’accroche à un hook stable et a moins de surface de régression qu’un ensemble de routes + templates + formulaires.
À l’inverse, si vous avez une vraie entité métier avec cycle de vie, permissions et audit, alors une page Symfony complète (Grid + show + edit) est la base saine — et c’est précisément là que « PrestaShop module CRUD : personnaliser les listings, actions et vues détail » prend tout son sens : vous maîtrisez l’UX admin et les contraintes d’exploitation, sans dépendre des limites du legacy.
