From 303b03a6a753ff790fb9a4e401560a02ba2fd167 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 4 Jun 2026 16:10:44 +0200 Subject: [PATCH 1/3] docs(claude) : skill backend-entity-conventions (detail des 5 regles entites a garde-fou CI) --- .../backend-entity-conventions/SKILL.md | 251 ++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 .claude/skills/backend-entity-conventions/SKILL.md diff --git a/.claude/skills/backend-entity-conventions/SKILL.md b/.claude/skills/backend-entity-conventions/SKILL.md new file mode 100644 index 0000000..0b8654a --- /dev/null +++ b/.claude/skills/backend-entity-conventions/SKILL.md @@ -0,0 +1,251 @@ +--- +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 `` ou un drawer RBAC (Role, Permission, Site, CategoryType), le front passe : - -```ts -useApi().get('/api/roles?pagination=false') -``` - -Le serveur retourne toute la collection, sans `view`. C'est l'echappatoire prevue par `pagination_client_enabled: true`. Sur les ressources a forte volumetrie, preferer une saisie assistee (recherche serveur via `?q=`) — a planifier dans un ticket dedie. - -Les tests fonctionnels qui exercent ce comportement doivent egalement 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 supportes : - -- **ORM** : injecter `ApiPlatform\State\Pagination\Pagination`, wrap un `Doctrine\ORM\Tools\Pagination\Paginator` dans `ApiPlatform\Doctrine\Orm\Paginator`. Exemple : `CategoryProvider`. -- **DBAL** : implementer un paginator local conforme a `PaginatorInterface`. Exemple : `DbalPaginator` (Core) + `AuditLogProvider`. - -Gerer l'echappatoire `?pagination=false` : - -```php -if (!$this->pagination->isEnabled($operation, $context)) { - return $qb->getQuery()->getResult(); // tout retourner -} -``` - -### Garde-fou architecture - -`tests/Architecture/CollectionsArePaginatedTest` scanne reflexivement toutes les classes `#[ApiResource]` sous `src/` et echoue si une `GetCollection` pose `paginationEnabled: false` hors whitelist `EXCLUDED`. Ajouter une entree a la whitelist requiert une justification courte + un ticket Lesstime ouvert. +Garde-fou : `tests/Architecture/CollectionsArePaginatedTest` (casse `make test`). +→ tableau des cles `pagination_*` + selects + providers ORM/DBAL detailles : skill `backend-entity-conventions`. ## Repositories @@ -136,42 +56,17 @@ Format obligatoire : `module.resource[.subresource].action` en snake_case. ### Libelle i18n du type d'entite (obligatoire avec `#[Auditable]`) -**Toute entite `#[Auditable]` doit avoir son libelle 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'entite » de l'audit-log affiche le type technique brut (ex: `commercial.Client`) au lieu d'un libelle lisible. +Toute entite `#[Auditable]` doit avoir sa cle `audit.entity._` dans `frontend/i18n/locales/fr.json` (cle = `strtolower(module)` + `_` + `strtolower(Entity)`, decision ERP-99). Sans elle, le filtre « Type d'entite » de l'audit-log retombe silencieusement sur le type technique brut (ex: `commercial.Client`). Fait partie de la definition de fini d'une entite auditee. -Pourquoi : le filtre est dynamique (`GET /audit-log-entity-types` renvoie les `entity_type` distincts presents en base) ; des qu'un module audite une entite, son type y apparait. Le front (`formatEntityType`, `audit-log.vue`) construit la cle `audit.entity._` et, faute de traduction, **retombe silencieusement** sur le type brut. - -Derivation de la cle (emplacement centralise + schema flat — decision ERP-99) : - -| FQCN entite | `entity_type` (back) | Cle 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` | - -Regle : `strtolower(module)` + `_` + `strtolower(Entity)`. Ajouter sa cle de libelle audit fait partie de la **definition de fini** d'une entite metier auditee. - -**Garde-fou** : `tests/Architecture/AuditableEntitiesHaveI18nLabelTest` scanne les entites `#[Auditable]` et echoue si une seule n'a pas sa cle `audit.entity.*`. Conclusion : creer une entite `#[Auditable]` sans son libelle i18n casse `make test`. +Garde-fou : `tests/Architecture/AuditableEntitiesHaveI18nLabelTest` (casse `make test`). +→ derivation detaillee + exemples : skill `backend-entity-conventions`. ## Timestampable + Blamable (obligatoire pour entites metier) -Toute **nouvelle** entite metier sous `src/Module/*/Domain/Entity/` doit porter les 4 colonnes `created_at` / `updated_at` / `created_by` / `updated_by`, remplies automatiquement. Trois lignes a ajouter a l'entite : +Toute **nouvelle** entite metier sous `src/Module/*/Domain/Entity/` : `implements TimestampableInterface, BlamableInterface` + `use TimestampableBlamableTrait` (porte les 4 colonnes `created_at` / `updated_at` / `created_by` / `updated_by`, remplies par `TimestampableBlamableSubscriber` au prePersist/preUpdate). La migration cree les 4 colonnes (`created_at`/`updated_at` NOT NULL, `created_by`/`updated_by` nullable `ON DELETE SET NULL`). Referentiel statique justifie : whitelist `EntitiesAreTimestampableBlamableTest::EXCLUDED`. -```php -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 metier -} -``` - -- Le `TimestampableBlamableSubscriber` (`Shared/Infrastructure/Doctrine/`) remplit les colonnes au `prePersist` / `preUpdate`. Hors contexte HTTP (CLI, cron, migration), le blame reste `null` (libelle « Systeme » cote front). -- La migration de l'entite doit creer les 4 colonnes (`created_at` / `updated_at` NOT NULL, `created_by` / `updated_by` nullable `ON DELETE SET NULL`). -- **Garde-fou CI** : `tests/Architecture/EntitiesAreTimestampableBlamableTest` echoue si une entite oublie le pattern. Un referentiel statique justifie (ex: `CategoryType`) doit etre explicitement whiteliste dans la constante `EXCLUDED` avec un commentaire. -- Spec complete : @docs/specs/M0-categories/spec-back.md § 2.8 + § 2.8.bis +Garde-fou : `tests/Architecture/EntitiesAreTimestampableBlamableTest` (casse `make test`). +→ snippet complet : skill `backend-entity-conventions` ; spec : @docs/specs/M0-categories/spec-back.md § 2.8 + § 2.8.bis. ## Serialization @@ -189,50 +84,7 @@ Exemple : pour qu'`User.profile` soit embarque au lieu d'un lien IRI sous le gro ## Migrations Doctrine -### Documentation SQL obligatoire (`COMMENT ON COLUMN`) +Toute migration creant/modifiant une colonne d'une table metier pose un `COMMENT ON COLUMN` (FR, ≤ 200 caracteres, semantique + contrainte/RG, cible pour les FK). Les 4 colonnes Timestampable/Blamable recoivent leur description via le helper centralise `addStandardTimestampableBlamableComments($schema, 'table')`. Bonus : `COMMENT ON TABLE` pour decrire la table. -**Toute migration qui cree ou modifie une colonne d'une table metier doit poser un `COMMENT ON COLUMN` decrivant le champ.** La description est stockee dans `pg_description` et visible dans tous les outils d'admin BDD (DBeaver, DataGrip, pgAdmin), sans avoir a lire les annotations PHP. - -**Format de la description** : -- En francais -- ≤ 200 caracteres -- Semantique du champ — contraintes / lien RG si pertinent -- Pour les colonnes d'identifiant ou FK, mentionner la cible - -Exemples : - -```php -// Migration : creation d'une colonne avec son commentaire dans la meme 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 : preciser 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 booleen : preciser le sens et la valeur par defaut -$this->addSql("COMMENT ON COLUMN user.is_admin IS 'Drapeau super-administrateur — bypass complet RBAC. Faux par defaut.'"); - -// Bonus : decrire la table elle-meme -$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` ajoutees par `TimestampableBlamableTrait` recoivent une description **standardisee** via le helper centralise pour eviter la duplication. Helper a creer ou appeler : - -```php -// Dans la migration, apres avoir ajoute les 4 colonnes : -$this->addStandardTimestampableBlamableComments($schema, 'client'); -``` - -L'implementation 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` filtre sur le schema `public` et echoue si **une seule colonne** n'a pas de `col_description`. Seules les tables system (`doctrine_migration_versions`) et la whitelist `EXCLUDED_TABLES` explicite (commentaire de justification + ticket Lesstime ouvert pour le retrofit) sont tolerees. - -Conclusion : si tu crees une colonne sans poser son `COMMENT ON COLUMN`, `make test` casse en CI. +Garde-fou : `tests/Architecture/ColumnsHaveSqlCommentTest` parcourt `information_schema.columns` (schema `public`) ; une seule colonne sans `col_description` casse `make test` (hors `EXCLUDED_TABLES`). +→ exemples SQL + textes du helper : skill `backend-entity-conventions`. -- 2.39.5 From 22ebaa6efdf97c8569b9f472bfb14e7978b3b32d Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 4 Jun 2026 16:42:10 +0200 Subject: [PATCH 3/3] docs(claude) : ref ecran Client comme standard des formulaires (frontend.md) --- .claude/rules/frontend.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.claude/rules/frontend.md b/.claude/rules/frontend.md index 426abd3..f5714c3 100644 --- a/.claude/rules/frontend.md +++ b/.claude/rules/frontend.md @@ -44,6 +44,10 @@ Tout champ de formulaire / filtre doit utiliser les composants `Malio*` plutot q Toute autre exception requiert validation avant merge. +## Standard ecran formulaire — reference : ecran Client + +**Tout nouvel ecran de formulaire doit ressembler au premier ecran Client** (`frontend/modules/commercial/pages/clients/new.vue` + `[id]/edit.vue`) : meme structure (bloc principal puis onglets), memes marges/espacements, memes blocs de collection (ajout/suppression inline), meme validation inline 422 par champ. C'est la reference visuelle et fonctionnelle des formulaires du projet — s'en inspirer avant d'en creer un nouveau. + ## Validation des formulaires — useFormErrors obligatoire (erreur par champ) **Tout formulaire qui soumet a une API DOIT afficher les erreurs de validation 422 sous le champ concerne, via `useFormErrors`** (`frontend/shared/composables/useFormErrors.ts`). C'est le pendant front de « le back renvoie TOUTES les violations d'une 422 d'un coup » : un seul aller-retour, chaque erreur affichee inline sous son champ (prop `:error` des `Malio*`), pas un toast fourre-tout. -- 2.39.5