Framework de back-office modulaire
Composant d'administration commun, mutualisé entre plusieurs applications d'un même socle applicatif. Il fournit une interface d'admin partagée et laisse chaque module enregistrer ses propres pages et sections de configuration via des interfaces PHP — sans toucher au layout, à la navigation, ni à un controller central.
Principe directeur : ajouter une fonctionnalité = créer une classe. Aucune modification de template ni de câblage manuel.
1. Structure de l'interface
1.1 Barre supérieure (fixe)
[ Logo ] | [ Menu des pages ] | [ Config ] | [ Sélecteur de langue ] | [ Profil / Déconnexion ]
Le menu des pages est généré dynamiquement depuis le registre de pages (§2). Rien à modifier dans le template lors de l'ajout d'une page.
1.2 Pages core (toujours présentes)
| Route | Titre | RĂ´le requis |
|---|---|---|
/admin |
Tableau de bord | ROLE_ADMIN |
/admin/config |
Configuration | ROLE_ADMIN |
/admin/profile |
Profil | ROLE_USER |
2. Registre de pages
2.1 Contrat
Chaque module déclare ses pages en implémentant une interface BoPageInterface. Les classes sont auto-découvertes par le conteneur d'injection de dépendances (tag d'autoconfiguration) et collectées par un registre BoPageRegistry, qui génère la navigation et orchestre le rendu.
Chaque page gère son propre rendu (liste, CRUD, formulaire sur mesure) via handle(Request). Pas de CRUD générique imposé : la page fait ce qu'elle veut.
2.2 Interface
#[AutoconfigureTag] interface BoPageInterface { /** Clé stable kebab-case — segment d'URL : /admin/pages/{key}. */ public function key(): string; /** Groupe dans la navigation. */ public function group(): string; /** Libellé affiché dans le menu. */ public function label(): string; /** Icône (jeu d'icônes du thème). */ public function icon(): string; /** Ordre de tri dans le groupe (croissant). */ public function priority(): int; /** * Rend le corps de la page (fragment HTML) * ou renvoie une RedirectResponse. C'est ici que vit * le CRUD, le formulaire, etc. */ public function handle(Request $request): Response; }
2.3 Exemple d'implémentation
final readonly class ArticlesPage implements BoPageInterface { public function __construct( private Environment $twig, private ArticleRepository $articles, ) {} public function key(): string { return 'articles'; } public function group(): string { return 'Contenu'; } public function label(): string { return 'Articles'; } public function icon(): string { return 'icon-file-text'; } public function priority(): int { return 10; } public function handle(Request $request): Response { // CRUD, pagination, formulaire — à la discrétion de la page. return new Response($this->twig->render('admin/bo/articles.html.twig', [ 'articles' => $this->articles->findForAdmin(), ])); } }
2.4 Registre et controller
Le registre collecte les implémentations taguées (via un itérateur autowiré), les trie par priority() et expose une vue groupée par group() pour la navigation.
#[Route('/admin/pages', name: 'admin.bo.page.')] #[IsGranted('ROLE_ADMIN')] final class BoPageController extends AbstractController { #[Route('/{key}', name: 'show', methods: ['GET', 'POST'])] public function show(string $key, Request $request, BoPageRegistry $registry): Response { $page = $registry->get($key) ?? throw $this->createNotFoundException(); $result = $page->handle($request); if ($result->isRedirection()) { return $result; } return $this->render('admin/bo/layout.html.twig', [ 'nav' => $registry->groupedByGroup(), 'current' => $key, 'body' => $result->getContent(), ]); } }
3. Registre de sections de configuration
3.1 Contrat
Chaque section de configuration est une classe qui implémente ConfigSectionInterface et s'auto-enregistre (même tag d'autoconfiguration). Un registre ConfigSectionRegistry collecte tout, génère le menu gauche (catégories → sections) et orchestre le rendu du panneau droit.
Ajouter un paramètre = créer une classe. Aucune modification du controller ni de la navigation.
3.2 Interface
#[AutoconfigureTag] interface ConfigSectionInterface { /** Clé kebab-case — segment d'URL : /admin/config/{key}. */ public function key(): string; /** Catégorie dans le menu gauche. */ public function category(): string; public function label(): string; /** Icône (jeu d'icônes du thème). */ public function icon(): string; /** Ordre de tri dans la catégorie (croissant). */ public function priority(): int; /** Rend le panneau droit ou renvoie une RedirectResponse (PRG). */ public function handle(Request $request): Response; }
3.3 Exemple — section de formulaire
final readonly class ContactFormSection implements ConfigSectionInterface { public function key(): string { return 'contact-form'; } public function category(): string { return 'Système'; } public function label(): string { return 'Formulaire de contact'; } public function icon(): string { return 'icon-envelope'; } public function priority(): int { return 35; } public function handle(Request $request): Response { $data = [ 'destinataire' => $this->settings->get('contact.destinataire', ''), 'objet' => $this->settings->get('contact.objet', '[Contact] Nouveau message'), ]; $form = $this->forms->create(ContactFormConfigType::class, $data); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { foreach ($form->getData() as $k => $v) { $this->settings->set("contact.$k", (string) $v); } return new RedirectResponse( $this->urls->generate('admin.config.section', ['key' => $this->key(), 'saved' => 1]) ); } return new Response($this->twig->render('admin/config/sections/_contact_form.html.twig', [ 'form' => $form->createView(), 'saved' => $request->query->getBoolean('saved'), ])); } }
4. Service de paramètres (AppSettings)
4.1 Contrat de lecture / écriture
// Lecture $destinataire = $this->settings->get('contact.destinataire', ''); $enabled = $this->settings->getBool('contact.honeypot.enabled', true); // Écriture (depuis une section Config uniquement) $this->settings->set('contact.destinataire', 'contact@example.org'); $this->settings->setBool('contact.honeypot.enabled', false);
Les clés sont des chaînes libres. Convention recommandée : module.sousmodule.param.
4.2 Stratégie de cache
Le service persiste ses valeurs dans le cache applicatif du framework pour éviter une requête base à chaque lecture.
| Aspect | Règle |
|---|---|
| Clé de cache | app_settings.<key> |
| TTL | permanent (les valeurs ne changent que par écriture depuis le BO) |
| Invalidation | obligatoire Ă chaque set() / delete() |
| Transparence | get(key, default) retourne la valeur du cache si chaud, de la base sinon (et met en cache) |
public function get(string $key, mixed $default = null): mixed { return $this->cache->get("app_settings.$key", function (ItemInterface $item) use ($key, $default) { $item->expiresAfter(null); // permanent — invalidation manuelle à l'écriture return $this->repository->findValueByKey($key) ?? $default; }); } public function set(string $key, mixed $value): void { $this->repository->upsert($key, $value); $this->cache->delete("app_settings.$key"); }
Sémantique cible : une configuration back-office qui écrase les valeurs de configuration statiques (fichiers), persistante et prioritaire.
5. Sécurité
- Routes
/admin/pages/*et/admin/config/*protĂ©gĂ©es parROLE_ADMIN. - Tableau de bord et profil accessibles Ă
ROLE_USER. - Chaque
BoPageInterface/ConfigSectionInterfacepeut affiner le contrôle d'accès via un Voter injecté danshandle().
6. Table de localisation (référence)
| Aspect | Localisation type |
|---|---|
BoPageInterface |
src/BO/Page/BoPageInterface.php |
BoPageRegistry |
src/BO/Page/BoPageRegistry.php |
BoPageController |
src/Controller/Admin/BoPageController.php |
ConfigSectionInterface |
src/BO/Config/ConfigSectionInterface.php |
ConfigSectionRegistry |
src/BO/Config/ConfigSectionRegistry.php |
ConfigController |
src/Controller/Admin/ConfigController.php |
AppSettings |
src/BO/Settings/AppSettings.php |
| Layout BO | templates/admin/bo/layout.html.twig |
7. Points de conception Ă retenir
- Découverte par tag DI, pas de registre manuel : une interface + un tag d'autoconfiguration suffisent.
- Registres homogènes : pages et sections partagent le même patron (collecte, tri par
priority(), regroupement). - Rendu délégué : chaque page/section renvoie un fragment ou une redirection (PRG) ; le controller n'assemble que le layout.
- Extension sans couplage : un module ajoute pages et paramètres sans modifier le cœur du back-office.