documentation
Kohinos v3 Doc générée le commit 8d60b1904

Sonata Admin 4.x — patterns Kohinos, breaking changes, sécurité

Sonata Admin 4.x apporte plusieurs breaking changes vis-à-vis de la 3.x (suppression de $this->trans(), DatagridMapper rendu final, ProxyQueryInterface raboté, configuration par attributs PHP). Cette page documente les patterns Kohinos pour écrire ou maintenir un Admin sans retomber dans les pièges détectés pendant la migration SF4 → SF7.

Carte des Admins Kohinos

Tous les Admins du projet héritent de App\Admin\KohinosAdmin (lui-même extends AbstractAdmin), ce qui mutualise translator, security, max-per-page et le rendu du bouton create. Liste non exhaustive :

AdminEntitéActions custom notablesRôle requis (typique)
DonAdminDon (STI DonAdherent/DonPrestataire)create?expediteur_type=…, dropdown createROLE_TRESORIER / ROLE_SUPER_ADMIN
AdherentAdminAdherentBatch actions, vue show richeROLE_ADMIN_ADHERENT_*
PrestataireAdminPrestataireBatch (validation, mise hors-ligne), routes customROLE_ADMIN_PRESTATAIRE_*
SolidoumeParamAdminSolidoumeParameterBatch + route executeROLE_SOLIDOUME
SepaMandateAdminSepaMandateValidation trésorier (workflow), révocationROLE_TRESORIER
FluxAdmin / FluxEurosAdmin / FluxSsaAdminFlux (STI)Liste filtrée par type, lecture seule sur les écritures financièresROLE_TRESORIER
OperationAdmin & dérivésOperation*Lecture seule (intégrité comptable)ROLE_TRESORIER
RecurringMlcSubscriptionRequestAdmin, RecurringPurchaseRequestAdmin, RecurringSsaSubscriptionRequestAdminRequêtes SEPA récurrentesAnnulation atomique en cascade du mandatROLE_TRESORIER
HelloAssoAdminLogs / configurations HelloAssoAudit des webhooksROLE_SUPER_ADMIN
GlobalParameterAdminGlobalParameterParamètres globaux MLCROLE_SUPER_ADMIN
AuditLogAdmin, EmailLogAdmin, ConsentAdmin, GdprRequestAdminTraces & RGPDLecture seuleROLE_SUPER_ADMIN

Pool unique sonata.admin ; configuration centralisée dans config/packages/sonata_admin.yaml. Le security handler utilisé est app.security.handler.role (rôles fins), pas le handler ACL par défaut.

Breaking changes Sonata 4.x

⚠ À connaître par cœur
  1. $this->trans('key') n'existe plus. Remplacer par :
    $this->getTranslator()->trans('key', [], $this->getTranslationDomain());
    Symptôme historique : Call to undefined method Admin::trans().
  2. DatagridMapper est final. Donc non mockable avec PHPUnit. Conséquence pratique : configureDatagridFilters() ne se teste plus en unitaire — soit markTestIncomplete(), soit test d'intégration via WebTestCase.
  3. ProxyQueryInterface ne déclare plus getRootAlias() ni andWhere(). Ces méthodes existent toujours sur l'implémentation Doctrine ORM mais passent par __call. Les appeler directement contre l'interface lève method not found à l'analyse statique. Caster vers l'implémentation ou éviter ces accès en filtre.
  4. Configuration par attributs PHP plutôt qu'XML/YAML. Sonata 4 préfère #[AutoconfigureTag('sonata.admin')] + paramètres en constructeur. Sur Kohinos on utilise encore le tag sonata.admin en YAML pour rester rétrocompatible — voir config/services.yaml.

Pattern de mocks pour les tests

Le constructeur d'AbstractAdmin attend un code, une classe et un baseControllerName — déjà branché par le compiler-pass Sonata. Mocker un Admin sans disableOriginalConstructor() finit en spaghetti.

$admin = $this->getMockBuilder(DonAdmin::class)
    ->disableOriginalConstructor()
    ->onlyMethods(['getTranslator', 'getRequest', 'getModelManager'])
    ->getMock();

$admin->method('getTranslator')->willReturn($translator);
$admin->method('getRequest')->willReturn($request);

Méthodes configure*

MéthodeSignatureUsage Kohinos
configureFormFields FormMapper $form Ajouter les champs en réutilisant les FormTypes Symfony (TextType, EntityType, MoneyType, etc.). Toujours typer explicitement — ne pas passer null comme type de champ, Sonata 4 ne devine plus.
configureListFields ListMapper $list Colonnes de la liste + colonne _action avec les actions inline (edit, delete, custom). Toujours fermer la liste par une colonne actions explicite ; les row actions orphelines ne s'affichent pas.
configureDatagridFilters DatagridMapper $filter Filtres de la sidebar. Garder les filtres simples — les filtres complexes (multi-jointures) cassent l'ergonomie et le cache Sonata. Préférer une route custom.
configureShowFields ShowMapper $show Vue détail. Sur Kohinos, beaucoup d'Admins financiers exposent un show riche (Adhérent, Prestataire) là où la liste reste compacte.
configureRoutes RouteCollectionInterface $collection Déclarer les actions custom : $collection->add('execute', $this->getRouterIdParameter().'/execute'). Toujours associer un {id} quand l'action porte sur une instance.
configureBatchActions array $actions Ajouter / retirer des actions de masse (validate, archive…). La sécurité doit être doublée côté handler — le bouton est rendu conditionnellement, le handler doit re-vérifier le rôle.
configureActionButtons array $buttonList, string $action, ?object $object Override des boutons en haut de page. Cas Kohinos : DonAdmin remplace le bouton create par un dropdown {Don adhérent / Don prestataire}.

Sécurité Admin

⚠ Défense en profondeur
  • Au niveau Admin : déclaration security: dans le tag sonata.admin ou via getAccessMapping(). Mais ce n'est qu'un garde-fou — chaque action sensible doit revérifier le rôle.
  • Au niveau action custom : dans le contrôleur Sonata associé, $this->denyAccessUnlessGranted('ROLE_TRESORIER') avant tout side-effect. Pattern observé dans DonAdmin::configureFormFields qui adapte le formulaire selon isGranted('ROLE_TRESORIER') / 'ROLE_COMPTOIR'.
  • CSRF tokens : les formulaires Sonata standards ont un CSRF token par défaut. Mais les actions custom invoquées en GET non. Toute action qui mute des données doit être en POST avec token _token + vérification isCsrfTokenValid().
  • Batch actions : la sécurité dans configureBatchActions ne sert qu'à cacher le bouton. Le handler doit revérifier le rôle ET la propriété des objets traités (cas multi-MLC).
  • Hidden fields : ne jamais faire confiance à un champ HiddenType côté POST sans validation. Préférer 'mapped' => false + restauration explicite dans prePersist/preUpdate.

Pattern champ hidden survivant au POST

Cas DonAdmin::expediteur_type (cf. commit 530372533) — le type d'expéditeur (adhérent vs prestataire) détermine la sous-classe STI DonAdherent ou DonPrestataire. Doctrine STI exige de choisir la sous-classe dès l'instanciation, donc le formulaire doit transporter cette information de manière fiable entre createNewInstance() et le POST final.

// 1. createNewInstance() — lit la query string ?expediteur_type=...
public function createNewInstance(): object
{
    $expediteurType = $this->resolveExpediteurType();
    return $expediteurType === 'prestataire' ? new DonPrestataire() : new DonAdherent();
}

// 2. configureFormFields() — déclare le champ NON mappé sur l'entité
$formMapper->add('expediteur_type', HiddenType::class, [
    'mapped'   => false,
    'data'     => $this->resolveExpediteurType(),
    'required' => false,
]);

// 3. resolveExpediteurType() — source de vérité centralisée
//    Ordre de résolution :
//      a) query string ?expediteur_type=...  (GET initial)
//      b) payload POST {uniqid}[expediteur_type]  (resubmit Sonata)
//      c) fallback : 'adherent'
private function resolveExpediteurType(): string
{
    $request   = $this->getRequest();
    $fromQuery = (string) $request->query->get('expediteur_type', '');
    if ($fromQuery !== '') { return $fromQuery; }

    foreach ($request->request->all() as $value) {
        if (is_array($value) && isset($value['expediteur_type'])) {
            return (string) $value['expediteur_type'];
        }
    }
    return 'adherent';
}

L'astuce REFERENCE_PLACEHOLDER = '(en cours de création)' du même Admin illustre une autre contrainte : Flux::$reference porte un #[Assert\NotBlank] qui est évalué avant prePersist(). Solution : on pose une valeur sentinelle dans createNewInstance(), le prePersist l'écrase ensuite par la vraie référence dérivée de l'expéditeur.

Translations dans Admin

L'API a changé : $this->trans() n'existe plus. Le pattern Kohinos :

// ❌ Sonata 3.x — NE FONCTIONNE PLUS
$label = $this->trans('admin.don.label');

// ✅ Sonata 4.x
$label = $this->getTranslator()->trans(
    'admin.don.label',
    [],
    $this->getTranslationDomain()
);

Anti-patterns à bannir

⚠ Refusé en revue
  • $this->trans('…') dans un Admin Sonata 4.x.
  • Logique métier (calcul de solde, génération de référence financière, envoi d'email) directement dans une méthode configure* ou un lifecycle hook Admin : déléguer à un Service injecté (DonExecutionService, SepaNotificationService, etc.).
  • Modification du hash de mot de passe via Admin (interdit par les règles d'invariants — voir Invariants). Le hash est géré par UserPasswordHasherInterface uniquement.
  • Action custom sans déclaration explicite dans configureRoutes() : Sonata ne la connaîtra pas et le bouton d'action listé ne pointera nulle part.
  • Batch action sans re-vérification de rôle ni de propriété MLC dans le handler.
  • Appel direct à $proxyQuery->getRootAlias() ou andWhere() contre ProxyQueryInterface — passe via PHPStan, casse à l'analyse stricte.
  • Mock de DatagridMapper — il est final, le test échouera.
  • HiddenType mappé sur l'entité sans validation côté prePersist/preUpdate : faille de confiance triviale.

Tester un Admin

Trois niveaux selon ce qu'on veut vérifier :

NiveauQuoi testerOutils
Unitaire Méthodes pures, helpers (resolveExpediteurType, formatage, createNewInstance sans subject) PHPUnit + mocks (constructor disabled), mock du Request et RequestStack
Intégration configureFormFields/configureDatagridFilters, validation des forms, hooks prePersist/preUpdate KernelTestCase + container réel + EntityManager SQLite in-memory
Fonctionnel Routes Admin (list, create, edit, custom action), sécurité, redirections, batch WebTestCase + loginUser() + assertions sur le HTML rendu
# Suite ciblée — toujours filtrer, jamais lancer toute la suite
vendor/bin/phpunit tests/Admin/DonAdminTest.php
vendor/bin/phpunit tests/Admin/SolidoumeParamAdminTest.php
vendor/bin/phpunit --filter resolveExpediteurType

# Routes Admin multi-rôles (smoke test)
python3 scripts/test-routes.py --role super_admin --only-errors
python3 scripts/test-routes.py --role tresorier --only-errors

Filet de couverture global (SonataAdminCoverageTest)

tests/Functional/Admin/SonataAdminCoverageTest.php énumère tous les admins du pool Sonata (sonata.admin.pool, ~46 admins hors enfants) et vérifie en super_admin que chaque route list et create répond HTTP 200 (~90 routes). Tout admin cassé après un refactor (template absent, service mal câblé, query invalide, référence vers une entité supprimée) fait échouer le test avec l'URL et le code HTTP fautifs.

Pour les tests fins par admin (exports, hooks, sécurité), hériter de tests/Admin/AbstractAdminTestCase.php : implémenter adminServiceId(), utiliser $this->admin et $this->actAs(['ROLE_TRESORIER']) pour poser un token en mémoire.

Checklist de revue d'un Admin

  1. Aucune occurrence de $this->trans( — uniquement $this->getTranslator()->trans().
  2. Chaque configureFormFields a des types explicites (pas de second argument null).
  3. Chaque action custom déclarée dans configureRoutes() a une vérification de rôle dans son handler.
  4. Chaque HiddenType sensible est mapped => false et restauré explicitement en prePersist/preUpdate.
  5. Aucun appel direct à getRootAlias()/andWhere() contre ProxyQueryInterface.
  6. Pas de mock de DatagridMapper dans les tests (il est final).
  7. Pas de logique financière inline — délégation à un service domaine.
  8. CHANGELOG technique + utilisateur mis à jour si l'Admin expose une nouvelle action visible.

Pour aller plus loin

Sources
  • src/Admin/KohinosAdmin.php — base commune (translator, security, perPage).
  • src/Admin/DonAdmin.php — référence pour hidden field + STI + placeholder.
  • src/Admin/AdherentAdmin.php, PrestataireAdmin.php, SolidoumeParamAdmin.php — exemples de batch actions et routes custom.
  • config/packages/sonata_admin.yaml — pool, security handler, templates.
  • doc/TROUBLESHOOTING.md § Sonata Admin 4.x — erreurs fréquentes post-migration.
  • .claude/agents/sonata-admin-reviewer.md — agent d'audit dédié.
  • Conventions & règles dures · Tests & qualité · Invariants financiers