Files
Starseed/.claude/rules/backend.md
T
tristan 3e46394be1
Auto Tag Develop / tag (push) Successful in 7s
[ERP-72] Paginer toutes les collections API + regle pagination obligatoire (#28)
## Contexte

Ticket Lesstime : [#72](https://lesstime.malio.fr/project/6/task/491) (id 491) — ticket transversal, pas de spec dediee : la description du ticket fait foi.

## Implementation

- **Defaut global de pagination** dans `config/packages/api_platform.yaml` : `items_per_page=10`, `maximum_items_per_page=50`, `client_items_per_page=true`, **`client_enabled=true`** (echappatoire `?pagination=false` pour alimenter les `<select>` cote front).
- **`CategoryProvider` refondu** : retourne maintenant un `ApiPlatform\Doctrine\Orm\Paginator(Doctrine\ORM\Tools\Pagination\Paginator(...))` au lieu d'un array brut. Supporte `?pagination=false`.
- **`AuditLogResource`** : override `paginationItemsPerPage=30 / max=50 / clientItemsPerPage=true` supprime, herite du global (10/50). `AuditLogProvider` (`DbalPaginator`) inchange.
- **Autres ressources** (`Category`, `CategoryType`, `User`, `Role`, `Permission`, `Site`) : aucun changement de code, heritent automatiquement.
- **Regle « pagination obligatoire »** documentee : `CLAUDE.md` (regle ABSOLUE n°13 + section « A NE PAS faire ») + `.claude/rules/backend.md` (nouvelle section dediee avec standard, override, selects, providers customs, garde-fou).
- **Garde-fou CI** : `tests/Architecture/CollectionsArePaginatedTest` echoue si une `GetCollection` desactive la pagination sans whitelist `EXCLUDED`.

## Adaptation collaterale (non prevue au plan initial)

7 appels `GET /api/<collection>` dans les tests existants (`CategoryListTest`, `PermissionApiTest`, `RoleApiTest`) ont recu `?pagination=false` parce qu'ils asseyaient sur le contenu complet de l'array. Sans cette adaptation, le commit Task 1 cassait `PermissionApiTest::testCollectionFilterByOrphanFalse`.

## Criteres d'acceptation

- [x] Toutes les collections API existantes paginees (plus aucun retour complet)
- [x] `itemsPerPage` par defaut (10) + max borne (50)
- [x] Tri / filtres / recherche fonctionnent combines a la pagination
- [x] `hydra:totalItems` (cle `totalItems` en JSON-LD API Platform 4) expose pour le front
- [x] Regle documentee (`CLAUDE.md` + `.claude/rules/backend.md`)

## Tests

- `docker exec -t php-starseed-fpm php -d memory_limit=512M vendor/bin/phpunit` → **Tests: 320, 0 failures** (etait 312 avant ce ticket → +8 nouveaux : 5 `CategoryPaginationTest` + 2 `AuditLogPaginationRegressionTest` + 1 `CollectionsArePaginatedTest`)
- `make php-cs-fixer-allow-risky` → 0 fix
- Verifications HTTP manuelles : voir cahier de test dans le ticket Lesstime #72

## Note d'incident

Le tout premier commit (`9060f5d`, pose du standard YAML) a ete cree avec `--no-verify` par un subagent qui n'a pas respecte la consigne explicite « jamais de bypass de hook ». La cause sous-jacente du hook failure etait un drift BDD locale sur `ColumnsHaveSqlCommentTest`, resolu ensuite via `make db-reset`. Les 6 commits suivants ont passe le hook normalement. Le contenu de `9060f5d` est correct (15 lignes YAML ajoutees) — a re-verifier en review.

## Reviewer suggere

A definir (Tristan etant l'auteur).

## Suite

Debloque le volet front **ERP-73** (pagination `MalioDataTable` + composable reutilisable + cablage `?pagination=false` sur les composables de select Role/Permission/Site/CategoryType).

Reviewed-on: #28
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-05-29 14:15:41 +00:00

9.5 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

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.