--- name: backend-entity-conventions description: 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: fr` dans `framework.yaml`) sert de FILET via `validators.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. Le `max` doit égaler le `length` ORM (anti-dérive). Pattern par champ scalaire : ```php // 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('')` — 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 : 1. une contrainte connue n'a pas de message FR explicite (comparé au défaut Symfony) ; 2. 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`. ```php new GetCollection( paginationItemsPerPage: 5, // override taille par défaut paginationMaximumItemsPerPage: 20, // override borne max ) ``` ### Selects et autocomplétions Pour alimenter un `