reference/back-office-symfony.md

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 par ROLE_ADMIN.
  • Tableau de bord et profil accessibles Ă  ROLE_USER.
  • Chaque BoPageInterface / ConfigSectionInterface peut affiner le contrĂ´le d'accès via un Voter injectĂ© dans handle().

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

  1. Découverte par tag DI, pas de registre manuel : une interface + un tag d'autoconfiguration suffisent.
  2. Registres homogènes : pages et sections partagent le même patron (collecte, tri par priority(), regroupement).
  3. Rendu délégué : chaque page/section renvoie un fragment ou une redirection (PRG) ; le controller n'assemble que le layout.
  4. Extension sans couplage : un module ajoute pages et paramètres sans modifier le cœur du back-office.

Assistant documentaire

Posez une question sur la documentation. Les réponses citent leurs sources — un clic ouvre le document à gauche.