Files
Starseed/.claude/skills/backend-entity-conventions/SKILL.md
T
matthieu fcacde2a34
Auto Tag Develop / tag (push) Successful in 7s
docs(claude) : allege backend.md (pointeurs + skill) + ref ecran Client pour les formulaires (#62)
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>
2026-06-04 14:51:38 +00:00

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: 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 :

// 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 :

  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.

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 un Doctrine\ORM\Tools\Pagination\Paginator dans ApiPlatform\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 au prePersist / preUpdate. Hors contexte HTTP (CLI, cron, migration), le blame reste null (libellé « Système » côté front).
  • La migration de l'entité doit créer les 4 colonnes (created_at / updated_at NOT NULL, created_by / updated_by nullable ON 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 constante EXCLUDED avec 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.