Files
Starseed/.claude/rules/backend.md
T
Matthieu 2ecfe226af
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m13s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Failing after 1m35s
feat(db) : documenter toutes les colonnes BDD via COMMENT ON COLUMN + garde-fou (ERP-67)
- Migration retrofit Version20260528120000 : pose COMMENT ON TABLE/COLUMN sur
  les 11 tables metier existantes (53 colonnes) via le catalogue partage
  ColumnCommentsCatalog (Shared/Infrastructure/Database).
- Commande app:apply-column-comments : rejoue le catalogue apres
  doctrine:schema:update --force (sinon l'ORM drop les commentaires absents
  du mapping PHP). Branchee dans makefile test-db-setup et workflow CI Gitea.
- Test architecture tests/Architecture/ColumnsHaveSqlCommentTest : echoue si
  une colonne public n'a pas de col_description (hors doctrine_migration_versions
  et fake_site_aware_entity). Whitelist metier vide.
- Convention documentee dans CLAUDE.md (regle ABSOLUE n°12) et
  .claude/rules/backend.md (section Migrations Doctrine) avec exemples et
  helper standardise pour les colonnes Timestampable/Blamable.
2026-05-28 16:44:18 +02:00

6.6 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}

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

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.