## 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>
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(viaconfig/routes/api_platform.yaml) - Le login
/login_checkest hors prefix/api(nginx reecritREQUEST_URIvers/login_check) - Exception : si tu dois creer un controller custom sous
/api/, mettrepriority: 1sur#[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 unDoctrine\ORM\Tools\Pagination\PaginatordansApiPlatform\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 :
*RepositoryInterfacedansDomain/Repository/ - Implementation Doctrine :
Doctrine*RepositorydansInfrastructure/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](deShared/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 auprePersist/preUpdate. Hors contexte HTTP (CLI, cron, migration), le blame restenull(libelle « Systeme » cote front). - La migration de l'entite doit creer les 4 colonnes (
created_at/updated_atNOT NULL,created_by/updated_bynullableON DELETE SET NULL). - Garde-fou CI :
tests/Architecture/EntitiesAreTimestampableBlamableTestechoue si une entite oublie le pattern. Un referentiel statique justifie (ex:CategoryType) doit etre explicitement whiteliste dans la constanteEXCLUDEDavec 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()— jamaisgetClientMimeType()(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.