# 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 : ```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 ## 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 : ```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.