diff --git a/.claude/rules/backend.md b/.claude/rules/backend.md index 4a3d3d6..d0b33f3 100644 --- a/.claude/rules/backend.md +++ b/.claude/rules/backend.md @@ -8,39 +8,10 @@ ## Messages de validation (obligatoire) -**Toute contrainte `#[Assert\*]` portee par une entite metier doit avoir un message FR explicite**, et **`Assert\Length.max` doit refleter le `length` de la colonne ORM**. Pendant logique back de la regle de mapping d'erreur par champ cote front (ERP-101 : `useFormErrors` / `mapViolationsToRecord` affiche sous chaque champ le `message` renvoye par le back). +Toute contrainte `#[Assert\*]` d'une entite metier : **message FR explicite**, et `Assert\Length.max` = `length` de la colonne ORM (coherence 3 niveaux nullable DB <-> NotBlank back <-> required front, ERP-101). RG inter-champs via `#[Assert\Callback]->atPath('')` (mapping inline front, pas toast). Exceptions miroir Length (Bic/Iban/Regex borne) : whitelist `EntityConstraintsHaveFrenchMessageTest::EXCLUDED_LENGTH_MIRROR`. -Pourquoi : -- Sans `message:` explicite, Symfony renvoie le defaut **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 metier portent en plus leur message FR pour un controle total. -- Une colonne string bornee **sans `Assert\Length`** echoue au niveau Postgres (500 generique, non rattachee au champ) au lieu d'une 422 propre. Le `max` doit egaler le `length` ORM (anti-derive). - -Pattern par champ scalaire : - -```php -// Email metier -#[Assert\Email(message: 'L\'adresse email n\'est pas valide.')] - -// Longueur calee 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')] -``` - -Coherence a 3 niveaux pour un champ obligatoire : colonne `nullable` (DB) <-> `Assert\NotBlank` (back) <-> `:required` + asterisque (front ERP-101). Les trois doivent s'accorder. - -Exceptions au miroir `Length` : un format deja borne par `Assert\Bic` / `Assert\Iban` (longueur garantie) ou par un `Assert\Regex` borne (ex. code postal `{4,5}`, couleur hex `#RRGGBB`) — whitelister alors la propriete dans `EntityConstraintsHaveFrenchMessageTest::EXCLUDED_LENGTH_MIRROR` avec justification. - -Les regles inter-champs (RG metier : exclusivite 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 plutot qu'en toast. - -### Garde-fou architecture - -`tests/Architecture/EntityConstraintsHaveFrenchMessageTest` scanne reflexivement les entites sous `src/Module/*/Domain/Entity/` et echoue si : -1. une contrainte connue n'a pas de message FR explicite (compare au defaut Symfony) ; -2. une colonne string bornee writable n'a pas de `Assert\Length(max == ORM length)` (hors whitelist). - -Une contrainte non geree par le mapping du test le fait echouer : il faut l'ajouter explicitement (anti faux positif vert). +Garde-fou : `tests/Architecture/EntityConstraintsHaveFrenchMessageTest` (casse `make test`). +→ patterns code + exemples + justification complete : skill `backend-entity-conventions`. ## API Platform (pas de controllers) @@ -51,61 +22,10 @@ Une contrainte non geree par le mapping du test le fait echouer : il faut l'ajou ## Pagination (obligatoire) -**Regle** : toute collection API DOIT etre paginee. Aucun retour de collection complete cote serveur. +Toute collection API est paginee (defaut 10, max 50 ; `?pagination=false` = echappatoire selects, `?itemsPerPage=25` borne par le max). Standard global dans `config/packages/api_platform.yaml`. Jamais `paginationEnabled: false` hors whitelist `CollectionsArePaginatedTest::EXCLUDED`. Provider custom : ne jamais retourner un `array` brut sur une `CollectionOperationInterface` (court-circuite Hydra) — wrapper un Paginator (ORM : `ApiPlatform\Doctrine\Orm\Paginator` ; DBAL : `DbalPaginator`) et gerer `?pagination=false` via `$this->pagination->isEnabled(...)`. -### Standard global - -Pose dans `config/packages/api_platform.yaml` (section `defaults:`) et heritee par toutes les ressources : - -| Cle | Valeur | Effet | -|---|---|---| -| `pagination_enabled` | `true` | Pagination Hydra active par defaut. | -| `pagination_items_per_page` | `10` | Taille de page par defaut, aligne sur l'UI `MalioDataTable`. | -| `pagination_maximum_items_per_page` | `50` | Borne dure : `?itemsPerPage=999` → ramene a 50. Anti deep-fetch. | -| `pagination_client_items_per_page` | `true` | Le client peut envoyer `?itemsPerPage=25` (bornee par le max). | -| `pagination_client_enabled` | `true` | Le client peut envoyer `?pagination=false` pour TOUT recuperer (echappatoire selects). | - -### Override par ressource (rare) - -Si une ressource a besoin d'un autre defaut (ex: payload lourd), utiliser les attributs sur l'operation. **JAMAIS `paginationEnabled: false`** sans whitelist explicite dans `tests/Architecture/CollectionsArePaginatedTest::EXCLUDED`. - -```php -new GetCollection( - paginationItemsPerPage: 5, // override taille par defaut - paginationMaximumItemsPerPage: 20, // override borne max -) -``` - -### Selects et autocompletions - -Pour alimenter un `