Files
Starseed/.claude/rules/backend.md
T
matthieu 052a39092b
Auto Tag Develop / tag (push) Successful in 8s
fix(audit) : libellés i18n des types d'entité + garde-fou (ERP-99) (#48)
## Contexte
Le filtre « Type d'entité » de l'audit-log est dynamique (`GET /audit-log-entity-types`). Toute entité `#[Auditable]` dont la clé i18n manquait s'affichait en **type technique brut** (ex: `commercial.Client`), le rendu retombant **silencieusement** sur le fallback.

## Décisions (cœur du ticket ERP-99)
- **Schéma de clé** : flat `audit.entity.<module>_<entity>` (inchangé, zéro régression).
- **Emplacement** : centralisé dans `frontend/i18n/locales/fr.json` (migration per-module = ticket infra i18n dédié).
- **Source de vérité** : `entity_type` = `strtolower(module).Entity` (confirmé dans `AuditListener::formatEntityType`).

## Changements
- **Complétude** : ajout des clés `audit.entity.*` manquantes (catalog + commercial) → 9 entités `#[Auditable]` couvertes.
- **Convention** : `.claude/rules/backend.md` § Audit — ajouter sa clé de libellé audit fait partie de la définition de fini d'une entité auditée.
- **Garde-fou** : `tests/Architecture/AuditableEntitiesHaveI18nLabelTest` scanne les entités `#[Auditable]` et échoue si une clé `audit.entity.*` manque ou est vide (rend le manque bloquant en CI).

## Vérifications
- Suite PHPUnit complète : **465 tests OK** (1604 assertions).
- Garde-fou : vert (9 entités) + test négatif confirmé rouge (clé retirée → échec actionnable).
- JSON `fr.json` valide, php-cs-fixer OK.

---------

Co-authored-by: admin malio <malio@yuno.malio.fr>
Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #48
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
2026-06-03 09:09:37 +00:00

11 KiB

Backend — Regles PHP / Symfony / API Platform

Structure de fichier

  • Toujours declare(strict_types=1); en tete de tout fichier PHP
  • PHP CS Fixer : regles Symfony + PSR-12 + strict types (commande : make php-cs-fixer-allow-risky)
  • Commentaires (docblock, inline, bloc) en francais ; code (classes, methodes, variables) en anglais

API Platform (pas de controllers)

  • Toujours utiliser #[ApiResource] + Providers + Processors — pas de controllers Symfony classiques
  • Routes prefixees /api (via config/routes/api_platform.yaml)
  • Le login /login_check est hors prefix /api (nginx reecrit REQUEST_URI vers /login_check)
  • Exception : si tu dois creer un controller custom sous /api/, mettre priority: 1 sur #[Route] pour eviter le conflit avec API Platform {id}

Pagination (obligatoire)

Regle : toute collection API DOIT etre paginee. Aucun retour de collection complete cote serveur.

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.

new GetCollection(
    paginationItemsPerPage: 5,           // override taille par defaut
    paginationMaximumItemsPerPage: 20,   // override borne max
)

Selects et autocompletions

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

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.

Repositories

  • Interface : *RepositoryInterface dans Domain/Repository/
  • Implementation Doctrine : Doctrine*Repository dans Infrastructure/Doctrine/
  • Le domaine garde les attributs ORM (approche pragmatique)

RBAC (permissions)

Format obligatoire : module.resource[.subresource].action en snake_case.

  • Exemples : core.users.view, commercial.clients.contacts.edit, core.audit_log.view
  • Declarees via la methode statique permissions() des *Module.php
  • Synchronisation : app:sync-permissions
  • Verification API Platform : is_granted('module.resource.action')
  • Verification front : usePermissions()

Roles

  • Hierarchie dans config/packages/security.yaml : ROLE_ADMIN, ROLE_USER
  • Le role ne remplace pas la permission RBAC — deux niveaux complementaires

Audit (obligatoire)

  • Toute entite metier (nouvelle ou existante) : #[Auditable] (de Shared/Domain/Attribute/)
  • Champs sensibles (password, token, secret) : #[AuditIgnore]
  • Audit ManyToMany : trace automatiquement {fieldName: {added: [ids], removed: [ids]}} — aucune action supplementaire
  • Spec complete : @doc/audit-log.md

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.

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.<module>_<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.

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 :

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

Serialization

Pour embarquer une relation dans le JSON (au lieu d'un IRI Hydra), ajouter le groupe du parent sur les proprietes de l'entite cible.

Exemple : pour qu'User.profile soit embarque au lieu d'un lien IRI sous le groupe user:read, annoter Profile.$firstName avec #[Groups(['user:read'])].

Upload de fichiers

  • Valider cote serveur avec $file->getMimeType()jamais getClientMimeType() (spoofable par le client)

PostgreSQL

  • Noms de colonnes toujours en minuscules dans le SQL brut (commun a tous les projets MALIO)

Migrations Doctrine

Documentation SQL obligatoire (COMMENT ON COLUMN)

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 :

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

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