Allege le contexte CLAUDE charge a chaque session, sans perdre de garantie de comportement (pur deplacement de doc, zero fichier de code touche). ## backend.md (1771 -> 702 mots) Les 5 sections deja couvertes par un test Architecture deterministe deviennent des pointeurs courts (enonce + nom du test garde-fou). Le detail (patterns, tableaux, exemples) part dans un nouveau skill `backend-entity-conventions` charge a la demande : - Messages de validation FR -> EntityConstraintsHaveFrenchMessageTest - Pagination -> CollectionsArePaginatedTest - Libelle i18n audit -> AuditableEntitiesHaveI18nLabelTest - Timestampable/Blamable -> EntitiesAreTimestampableBlamableTest - COMMENT ON COLUMN -> ColumnsHaveSqlCommentTest ## frontend.md Ajoute une reference : tout nouvel ecran de formulaire doit ressembler a l'ecran Client (structure, marges, blocs de collection, validation inline 422). ## Garanties - Aucun test modifie : les tests Architecture restent le juge, le build casse comme avant. - Chaque regle garde son pointeur (enonce + test) charge a chaque session ; le detail revient via le skill. - Reversible en un revert. --------- Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #62 Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
13 KiB
name, description
| name | description |
|---|---|
| backend-entity-conventions | Conventions détaillées des entités métier Starseed (back PHP/Symfony/API Platform) — messages de validation FR sur les contraintes, pagination API Platform et providers ORM/DBAL, libellé i18n du type d'entité auditée, Timestampable/Blamable, COMMENT ON COLUMN des migrations. Charger dès qu'on crée ou modifie une entité Domain, un ApiResource, un Provider/Processor, une contrainte de validation, ou une migration Doctrine. Le résumé court de chaque règle (+ nom du test garde-fou) reste dans .claude/rules/backend.md ; ce skill porte les patterns, tableaux et exemples complets. |
Conventions entités métier — détail
Ce skill contient le détail (patterns code, tableaux, dérivations) des 5 règles back qui ont chacune
un test Architecture déterministe. L'énoncé court de chaque règle vit dans .claude/rules/backend.md
(chargé à chaque session) ; ici on trouve le « comment » complet.
Règle d'or : le test Architecture reste le juge (il casse
make test). Ce skill aide à écrire le code juste du premier coup, il ne remplace pas le garde-fou.
1. Messages de validation (Garde-fou : EntityConstraintsHaveFrenchMessageTest)
Toute contrainte #[Assert\*] portée par une entité métier doit avoir un message FR explicite, et
Assert\Length.max doit refléter le length de la colonne ORM. C'est le pendant back du mapping
d'erreur par champ côté front (ERP-101 : useFormErrors / mapViolationsToRecord affiche sous chaque
champ le message renvoyé par le back).
Pourquoi :
- Sans
message:explicite, Symfony renvoie le défaut anglais (« This value is not a valid email address. »). La locale FR globale (default_locale: frdansframework.yaml) sert de FILET viavalidators.fr.xlf, mais les contraintes métier portent en plus leur message FR pour un contrôle total. - Une colonne string bornée sans
Assert\Lengthéchoue au niveau Postgres (500 générique, non rattachée au champ) au lieu d'une 422 propre. Lemaxdoit égaler lelengthORM (anti-dérive).
Pattern par champ scalaire :
// Email métier
#[Assert\Email(message: 'L\'adresse email n\'est pas valide.')]
// Longueur calée sur la colonne (VARCHAR(120))
#[ORM\Column(length: 120)]
#[Assert\Length(max: 120, maxMessage: 'Le nom ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
// Obligatoire (aligner nullable DB / NotBlank back / required front)
#[Assert\NotBlank(message: 'Le téléphone est obligatoire.', normalizer: 'trim')]
Cohérence à 3 niveaux pour un champ obligatoire : colonne nullable (DB) <-> Assert\NotBlank (back)
<-> :required + astérisque (front ERP-101). Les trois doivent s'accorder.
Exceptions au miroir Length : un format déjà borné par Assert\Bic / Assert\Iban (longueur
garantie) ou par un Assert\Regex borné (ex. code postal {4,5}, couleur hex #RRGGBB) — whitelister
alors la propriété dans EntityConstraintsHaveFrenchMessageTest::EXCLUDED_LENGTH_MIRROR avec justification.
Les règles inter-champs (RG métier : exclusivité distributor/broker RG-1.03, billingEmail RG-1.11, etc.)
passent par un #[Assert\Callback] qui construit la violation avec ->atPath('<champ>') — indispensable
pour que le front la mappe en inline plutôt qu'en toast.
Garde-fou architecture
tests/Architecture/EntityConstraintsHaveFrenchMessageTest scanne réflexivement les entités sous
src/Module/*/Domain/Entity/ et échoue si :
- une contrainte connue n'a pas de message FR explicite (comparé au défaut Symfony) ;
- une colonne string bornée writable n'a pas de
Assert\Length(max == ORM length)(hors whitelist).
Une contrainte non gérée par le mapping du test le fait échouer : il faut l'ajouter explicitement (anti faux positif vert).
2. Pagination (Garde-fou : CollectionsArePaginatedTest)
Règle : toute collection API DOIT être paginée. Aucun retour de collection complète côté serveur.
Standard global
Posé dans config/packages/api_platform.yaml (section defaults:) et hérité par toutes les ressources :
| Clé | Valeur | Effet |
|---|---|---|
pagination_enabled |
true |
Pagination Hydra active par défaut. |
pagination_items_per_page |
10 |
Taille de page par défaut, alignée sur l'UI MalioDataTable. |
pagination_maximum_items_per_page |
50 |
Borne dure : ?itemsPerPage=999 → ramené à 50. Anti deep-fetch. |
pagination_client_items_per_page |
true |
Le client peut envoyer ?itemsPerPage=25 (bornée par le max). |
pagination_client_enabled |
true |
Le client peut envoyer ?pagination=false pour TOUT récupérer (échappatoire selects). |
Override par ressource (rare)
Si une ressource a besoin d'un autre défaut (ex: payload lourd), utiliser les attributs sur l'opération.
JAMAIS paginationEnabled: false sans whitelist explicite dans
tests/Architecture/CollectionsArePaginatedTest::EXCLUDED.
new GetCollection(
paginationItemsPerPage: 5, // override taille par défaut
paginationMaximumItemsPerPage: 20, // override borne max
)
Selects et autocomplétions
Pour alimenter un <select> ou un drawer RBAC (Role, Permission, Site, CategoryType), le front passe :
useApi().get('/api/roles?pagination=false')
Le serveur retourne toute la collection, sans view. C'est l'échappatoire prévue par
pagination_client_enabled: true. Sur les ressources à forte volumétrie, préférer une saisie assistée
(recherche serveur via ?q=) — à planifier dans un ticket dédié.
Les tests fonctionnels qui exercent ce comportement doivent également passer ?pagination=false
(cf. CategoryListTest, PermissionApiTest).
Providers customs et pagination
Un provider custom qui retourne un array brut sur une CollectionOperationInterface
court-circuite la pagination Hydra (pas de totalItems, pas de view). Patterns supportés :
- ORM : injecter
ApiPlatform\State\Pagination\Pagination, wrap unDoctrine\ORM\Tools\Pagination\PaginatordansApiPlatform\Doctrine\Orm\Paginator. Exemple :CategoryProvider. - DBAL : implémenter un paginator local conforme à
PaginatorInterface. Exemple :DbalPaginator(Core) +AuditLogProvider.
Gérer l'échappatoire ?pagination=false :
if (!$this->pagination->isEnabled($operation, $context)) {
return $qb->getQuery()->getResult(); // tout retourner
}
Garde-fou architecture
tests/Architecture/CollectionsArePaginatedTest scanne réflexivement toutes les classes
#[ApiResource] sous src/ et échoue si une GetCollection pose paginationEnabled: false hors
whitelist EXCLUDED. Ajouter une entrée à la whitelist requiert une justification courte + un ticket
Lesstime ouvert.
3. Libellé i18n du type d'entité auditée (Garde-fou : AuditableEntitiesHaveI18nLabelTest)
Toute entité #[Auditable] doit avoir son libellé FR dans le bloc audit.entity de
frontend/i18n/locales/fr.json. C'est la contrepartie i18n de l'attribut : sans elle, le filtre
« Type d'entité » de l'audit-log affiche le type technique brut (ex: commercial.Client) au lieu d'un
libellé lisible.
Pourquoi : le filtre est dynamique (GET /audit-log-entity-types renvoie les entity_type distincts
présents en base) ; dès qu'un module audite une entité, son type y apparaît. Le front
(formatEntityType, audit-log.vue) construit la clé audit.entity.<module>_<entity> et, faute de
traduction, retombe silencieusement sur le type brut.
Dérivation de la clé (emplacement centralisé + schéma flat — décision ERP-99) :
| FQCN entité | entity_type (back) |
Clé i18n (flat) |
|---|---|---|
App\Module\Commercial\Domain\Entity\Client |
commercial.Client |
commercial_client |
App\Module\Commercial\Domain\Entity\ClientAddress |
commercial.ClientAddress |
commercial_clientaddress |
App\Module\Catalog\Domain\Entity\Category |
catalog.Category |
catalog_category |
Règle : strtolower(module) + _ + strtolower(Entity). Ajouter sa clé de libellé audit fait partie
de la définition de fini d'une entité métier auditée.
Garde-fou : tests/Architecture/AuditableEntitiesHaveI18nLabelTest scanne les entités #[Auditable]
et échoue si une seule n'a pas sa clé audit.entity.*. Conclusion : créer une entité #[Auditable]
sans son libellé i18n casse make test.
4. Timestampable + Blamable (Garde-fou : EntitiesAreTimestampableBlamableTest)
Toute nouvelle entité métier sous src/Module/*/Domain/Entity/ doit porter les 4 colonnes
created_at / updated_at / created_by / updated_by, remplies automatiquement. Trois lignes à
ajouter à l'entité :
use App\Shared\Domain\Contract\BlamableInterface;
use App\Shared\Domain\Contract\TimestampableInterface;
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
class MyEntity implements TimestampableInterface, BlamableInterface
{
use TimestampableBlamableTrait; // porte les 4 props + getters/setters
// ... reste métier
}
- Le
TimestampableBlamableSubscriber(Shared/Infrastructure/Doctrine/) remplit les colonnes auprePersist/preUpdate. Hors contexte HTTP (CLI, cron, migration), le blame restenull(libellé « Système » côté front). - La migration de l'entité doit créer les 4 colonnes (
created_at/updated_atNOT NULL,created_by/updated_bynullableON DELETE SET NULL). - Garde-fou CI :
tests/Architecture/EntitiesAreTimestampableBlamableTestéchoue si une entité oublie le pattern. Un référentiel statique justifié (ex:CategoryType) doit être explicitement whitelisté dans la constanteEXCLUDEDavec un commentaire. - Spec complète : @docs/specs/M0-categories/spec-back.md § 2.8 + § 2.8.bis
5. Migrations Doctrine — COMMENT ON COLUMN (Garde-fou : ColumnsHaveSqlCommentTest)
Toute migration qui crée ou modifie une colonne d'une table métier doit poser un COMMENT ON COLUMN
décrivant le champ. La description est stockée dans pg_description et visible dans tous les outils
d'admin BDD (DBeaver, DataGrip, pgAdmin), sans avoir à lire les annotations PHP.
Format de la description :
- En français
- ≤ 200 caractères
- Sémantique du champ — contraintes / lien RG si pertinent
- Pour les colonnes d'identifiant ou FK, mentionner la cible
Exemples :
// Migration : création d'une colonne avec son commentaire dans la même migration
$this->addSql("ALTER TABLE client ADD COLUMN siren VARCHAR(9) DEFAULT NULL");
$this->addSql("COMMENT ON COLUMN client.siren IS 'SIREN (9 chiffres) — identifiant legal entreprise. Unique parmi non-archives (RG-1.15).'");
// Cas FK : préciser la cible
$this->addSql("COMMENT ON COLUMN client.legal_form_id IS 'Reference forme juridique (SARL, SAS, SA...) — FK -> legal_form.id, ON DELETE RESTRICT.'");
// Cas booléen : préciser le sens et la valeur par défaut
$this->addSql("COMMENT ON COLUMN user.is_admin IS 'Drapeau super-administrateur — bypass complet RBAC. Faux par defaut.'");
// Bonus : décrire la table elle-même
$this->addSql("COMMENT ON TABLE client IS 'Repertoire clients (M1 Commercial) — entites archivables.'");
Helper Timestampable/Blamable
Les 4 colonnes created_at, updated_at, created_by, updated_by ajoutées par
TimestampableBlamableTrait reçoivent une description standardisée via le helper centralisé pour
éviter la duplication. Helper à créer ou appeler :
// Dans la migration, après avoir ajouté les 4 colonnes :
$this->addStandardTimestampableBlamableComments($schema, 'client');
L'implémentation du helper applique :
created_at: « Horodatage de creation de la ligne (UTC, rempli automatiquement par TimestampableBlamableSubscriber). »updated_at: « Horodatage de derniere modification de la ligne (UTC, rempli automatiquement par TimestampableBlamableSubscriber). »created_by: « ID de l'utilisateur ayant cree la ligne — null pour les creations hors HTTP (CLI, migration, fixture). FK -> user.id, ON DELETE SET NULL. »updated_by: « ID de l'utilisateur ayant modifie la ligne en dernier — null pour les modifications hors HTTP. FK -> user.id, ON DELETE SET NULL. »
Garde-fou architecture
tests/Architecture/ColumnsHaveSqlCommentTest parcourt information_schema.columns filtré sur le
schéma public et échoue si une seule colonne n'a pas de col_description. Seules les tables
système (doctrine_migration_versions) et la whitelist EXCLUDED_TABLES explicite (commentaire de
justification + ticket Lesstime ouvert pour le retrofit) sont tolérées.
Conclusion : si tu crées une colonne sans poser son COMMENT ON COLUMN, make test casse en CI.