# Guide Backend — Inventory Guide complet du backend Symfony pour comprendre comment tout fonctionne, même si tu débutes. ## Table des matières 1. [Vue d'ensemble](#vue-densemble) 2. [Comment fonctionne une API REST](#comment-fonctionne-une-api-rest) 3. [Symfony + API Platform — les bases](#symfony--api-platform--les-bases) 4. [Les Entités (les modèles de données)](#les-entités) 5. [Les Controllers (les endpoints personnalisés)](#les-controllers) 6. [Le système d'audit](#le-système-daudit) 7. [L'authentification par session](#lauthentification-par-session) 8. [Les services](#les-services) 9. [Les migrations de base de données](#les-migrations) 10. [Les tests](#les-tests) 11. [Flux complet d'une requête](#flux-complet-dune-requête) 12. [Commandes Symfony utiles](#commandes-symfony-utiles) --- ## Vue d'ensemble Le backend est une **API REST** construite avec : - **Symfony 8** : le framework PHP (gère le routing, la sécurité, la config, etc.) - **API Platform 4.2** : une surcouche qui génère automatiquement les endpoints CRUD à partir des entités - **Doctrine ORM** : fait le lien entre les objets PHP et les tables PostgreSQL - **PostgreSQL 16** : la base de données relationnelle ### Le principe Au lieu d'écrire manuellement chaque endpoint (GET /machines, POST /machines, etc.), **API Platform** les génère automatiquement à partir des entités PHP. Tu déclares tes champs, tes relations, tes règles de sécurité directement sur la classe PHP, et API Platform fait le reste. --- ## Comment fonctionne une API REST ### C'est quoi une API REST ? Une API REST c'est un serveur qui répond à des requêtes HTTP (comme un site web, mais au lieu de renvoyer du HTML, il renvoie du JSON). ### Les verbes HTTP | Verbe | Action | Exemple | |-------|--------|---------| | `GET` | Lire des données | `GET /api/machines` → liste toutes les machines | | `POST` | Créer une donnée | `POST /api/machines` + body JSON → crée une machine | | `PUT` | Remplacer une donnée | `PUT /api/machines/123` + body JSON → remplace la machine 123 | | `PATCH` | Modifier partiellement | `PATCH /api/machines/123` + body JSON → modifie certains champs | | `DELETE` | Supprimer | `DELETE /api/machines/123` → supprime la machine 123 | ### Les codes de réponse HTTP | Code | Signification | Quand | |------|---------------|-------| | `200` | OK | Requête réussie | | `201` | Created | Ressource créée avec succès (POST) | | `204` | No Content | Suppression réussie (DELETE) | | `400` | Bad Request | Données invalides envoyées | | `401` | Unauthorized | Pas connecté / session expirée | | `403` | Forbidden | Connecté mais pas les permissions | | `404` | Not Found | La ressource n'existe pas | | `409` | Conflict | Doublon (ex: nom déjà pris) | | `500` | Server Error | Bug côté serveur | ### Le format JSON-LD L'API utilise **JSON-LD** (JSON Linked Data), une extension de JSON qui ajoute des métadonnées : ```json { "@context": "/api/contexts/Machine", "@id": "/api/machines/cl1a2b3c4d5e6f7g8h9i0j1k", "@type": "Machine", "id": "cl1a2b3c4d5e6f7g8h9i0j1k", "name": "CNC Mazak 01", "reference": "CNM-001", "prix": "50000.00", "site": "/api/sites/cl9z8y7x6w5v4u3t2s1r0q", "createdAt": "2026-01-15T10:30:00+00:00" } ``` Points importants : - `@id` est l'**IRI** (Internationalized Resource Identifier) : c'est l'identifiant unique de la ressource dans l'API - Les relations utilisent des IRIs : `"site": "/api/sites/cl9z8..."` au lieu d'un simple ID - Les collections retournent un format hydra avec pagination : ```json { "@context": "/api/contexts/Machine", "@id": "/api/machines", "@type": "hydra:Collection", "hydra:totalItems": 42, "hydra:member": [ { "@id": "/api/machines/cl...", "name": "CNC 01", ... }, { "@id": "/api/machines/cl...", "name": "Tour 02", ... } ] } ``` --- ## Symfony + API Platform — les bases ### La structure des fichiers ``` src/ ├── Entity/ # Les classes PHP qui représentent les tables de la BDD ├── Controller/ # Les endpoints HTTP personnalisés (quand API Platform ne suffit pas) ├── EventSubscriber/ # Du code qui s'exécute automatiquement quand quelque chose se passe ├── Repository/ # Les requêtes SQL personnalisées ├── Service/ # La logique métier réutilisable ├── State/ # Les processeurs API Platform (interceptent le flux CRUD) ├── Security/ # L'authentification ├── Serializer/ # Personnalisation de la conversion entité ↔ JSON ├── Command/ # Commandes CLI (php bin/console app:xxx) ├── Enum/ # Les énumérations PHP (ex: catégories) └── OpenApi/ # Personnalisation de la doc Swagger ``` ### Comment Symfony traite une requête ``` Requête HTTP ↓ Symfony Router (quel code doit répondre ?) ↓ Sécurité (l'utilisateur a-t-il le droit ?) ↓ Controller ou API Platform (traitement) ↓ Doctrine ORM (lecture/écriture en BDD) ↓ Serializer (conversion entité → JSON) ↓ Réponse HTTP (JSON envoyé au frontend) ``` ### Les attributs PHP 8 Le projet utilise les **attributs PHP 8** (les `#[...]`) au lieu des annotations (les `@...`). C'est la syntaxe moderne de PHP : ```php // Attribut PHP 8 (ce qu'on utilise) ✅ #[ORM\Column(type: 'string', length: 255)] private string $name; // Annotation (ancien style, on ne l'utilise pas) ❌ /** @ORM\Column(type="string", length=255) */ ``` --- ## Les Entités Les entités sont les classes PHP qui représentent les tables de la base de données. Chaque propriété de la classe correspond à une colonne. ### Anatomie d'une entité Prenons un exemple simplifié : ```php ['machine:read']], // ← Quels champs exposer en lecture denormalizationContext: ['groups' => ['machine:write']], // ← Quels champs accepter en écriture paginationItemsPerPage: 30, // ← 30 résultats par page )] class Machine { #[ORM\Id] #[ORM\Column(type: 'string', length: 36)] #[Groups(['machine:read'])] // ← Exposé en lecture uniquement private string $id; #[ORM\Column(type: 'string', length: 255, unique: true)] #[Groups(['machine:read', 'machine:write'])] // ← Exposé en lecture ET écriture private string $name; #[ORM\Column(type: 'decimal', precision: 10, scale: 2, nullable: true)] #[Groups(['machine:read', 'machine:write'])] private ?string $prix = null; #[ORM\ManyToOne(targetEntity: Site::class)] // ← Relation : chaque machine appartient à un site #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] // ← Obligatoire, supprimé en cascade #[Groups(['machine:read', 'machine:write'])] private Site $site; #[ORM\Column(type: 'datetime_immutable')] #[Groups(['machine:read'])] // ← Lecture seule (pas dans machine:write) private \DateTimeImmutable $createdAt; // ... getters et setters } ``` ### Décryptage des attributs importants | Attribut | Signification | |----------|--------------| | `#[ORM\Entity]` | Cette classe est stockée en BDD | | `#[ORM\Column]` | Cette propriété est une colonne | | `#[ORM\Id]` | C'est la clé primaire | | `#[ORM\ManyToOne]` | Relation N→1 (plusieurs machines → un site) | | `#[ORM\OneToMany]` | Relation 1→N (un site → plusieurs machines) | | `#[ORM\ManyToMany]` | Relation N→N (machines ↔ constructeurs) | | `#[ApiResource]` | API Platform génère les endpoints CRUD | | `#[Groups]` | Contrôle quels champs sont visibles/modifiables | | `security: "is_granted('ROLE_X')"` | Qui a le droit d'utiliser cet endpoint | ### Le trait CuidEntityTrait Toutes les entités utilisent un trait partagé qui génère les IDs et gère les timestamps : ```php // src/Entity/Trait/CuidEntityTrait.php trait CuidEntityTrait { #[ORM\PrePersist] // ← S'exécute automatiquement AVANT l'insertion en BDD public function generateId(): void { if (!isset($this->id)) { $this->id = 'cl' . bin2hex(random_bytes(12)); // ← Génère un ID unique de 26 chars } $this->createdAt = new \DateTimeImmutable(); $this->updatedAt = new \DateTimeImmutable(); } #[ORM\PreUpdate] // ← S'exécute automatiquement AVANT une mise à jour public function updateTimestamp(): void { $this->updatedAt = new \DateTimeImmutable(); } } ``` ### Les entités du projet #### Entités "catalogue" (les éléments qu'on gère) | Entité | Table | Champs clés | Relations | |--------|-------|-------------|-----------| | **Machine** | `machine` | name, reference, prix | → Site, ↔ Constructeur, → Documents | | **Composant** | `composant` | name, reference, description, prix, structure (JSON) | → ModelType, → Product, ↔ Constructeur | | **Piece** | `piece` | name, reference, description, prix, productIds (JSON) | → ModelType, → Product, ↔ Constructeur | | **Product** | `product` | name, reference, supplierPrice | → ModelType, ↔ Constructeur | #### Entités de classification | Entité | Table | Champs clés | Rôle | |--------|-------|-------------|------| | **Site** | `site` | name, contactName, contactPhone, contactAddress | Regrouper les machines par lieu | | **Constructeur** | `constructeur` | name, email, phone | Fournisseurs/fabricants partagés | | **ModelType** | `model_type` | name, code, category (enum), skeletons (JSON) | Catégoriser composants/pièces/produits | #### Entités de liaison hiérarchique (structure machine) | Entité | Rôle | Relations | |--------|------|-----------| | **MachineComponentLink** | Lie un composant à une machine | → Machine, → Composant, → parent (self) | | **MachinePieceLink** | Lie une pièce à une machine | → Machine, → Piece, → parent composant | | **MachineProductLink** | Lie un produit à une machine | → Machine, → Product, → parent (flexible) | Ces entités permettent la **structure arborescente** : un composant peut contenir des pièces, qui contiennent des produits. #### Entités de métadonnées | Entité | Rôle | |--------|------| | **CustomField** | Définition d'un champ personnalisé (nom, type, options) | | **CustomFieldValue** | Valeur d'un champ personnalisé pour une entité donnée | | **Document** | Fichier uploadé (PDF, image) rattaché à une entité | | **AuditLog** | Entrée du journal d'audit (diff + snapshot) | | **Comment** | Commentaire/ticket sur une fiche | | **Profile** | Compte utilisateur (email, rôle, mot de passe hashé) | ### Les relations entre entités (schéma simplifié) ``` Site ──1:N──► Machine ──1:N──► MachineComponentLink ──► Composant │ │ │ └──1:N──► MachinePieceLink ──► Piece │ │ │ └──1:N──► MachineProductLink ──► Product │ └──N:N──► Constructeur (via table de jointure) ModelType ──1:N──► Composant / Piece / Product │ └──► CustomField ──1:N──► CustomFieldValue Machine / Composant / Piece / Product ──1:N──► Document ──1:N──► CustomFieldValue ``` --- ## Les Controllers API Platform génère automatiquement les endpoints CRUD standard. Les controllers personnalisés gèrent les cas plus complexes. ### Liste des controllers #### Authentification (3 controllers) **SessionProfileController** (`/api/session/profile`) — Login/Logout ``` POST /api/session/profile → Se connecter (payload: { profileId, password }) GET /api/session/profile → Récupérer le profil connecté DELETE /api/session/profile → Se déconnecter ``` **SessionProfilesController** (`/api/session/profiles`) — Liste des profils ``` GET /api/session/profiles → Liste tous les profils actifs (page de login) ``` **AdminProfileController** (`/api/admin/profiles`) — Administration des utilisateurs ``` GET /api/admin/profiles → Liste tous les profils (ADMIN only) POST /api/admin/profiles → Créer un profil PUT /api/admin/profiles/{id}/role → Changer le rôle d'un profil PUT /api/admin/profiles/{id}/password → Réinitialiser un mot de passe PUT /api/admin/profiles/{id}/deactivate → Désactiver un profil ``` #### Données et logique métier **MachineStructureController** — Structure hiérarchique des machines ``` GET /api/machines/{id}/structure → Récupérer l'arborescence complète PATCH /api/machines/{id}/structure → Modifier l'arborescence POST /api/machines/{id}/clone → Cloner une machine avec toute sa structure ``` **MachineCustomFieldsController** — Champs personnalisés machines ``` POST /api/machines/{id}/custom-fields/init → Initialiser les champs personnalisés manquants ``` **EntityHistoryController** — Historique d'audit par entité ``` GET /api/{entityType}/{id}/history → 200 derniers événements d'audit ``` **ActivityLogController** — Journal d'activité global ``` GET /api/activity-log → Liste paginée avec filtres (entityType, action) ``` **CommentController** — Commentaires/tickets ``` POST /api/comments → Créer un commentaire PATCH /api/comments/{id}/resolve → Résoudre un commentaire GET /api/comments/unresolved-count → Nombre de commentaires non résolus ``` **CustomFieldValueController** — Valeurs de champs personnalisés ``` POST /api/custom-field-values → Créer/mettre à jour une valeur (upsert) DELETE /api/custom-field-values/{id} → Supprimer une valeur ``` #### Fichiers **DocumentQueryController** — Requêter les documents par entité ``` GET /api/documents/by-site/{id} → Documents d'un site GET /api/documents/by-machine/{id} → Documents d'une machine GET /api/documents/by-composant/{id} → Documents d'un composant GET /api/documents/by-piece/{id} → Documents d'une pièce GET /api/documents/by-product/{id} → Documents d'un produit ``` **DocumentServeController** — Servir les fichiers ``` GET /api/documents/{id}/file → Afficher le fichier (inline) GET /api/documents/{id}/download → Télécharger le fichier (attachment) ``` #### Monitoring **HealthCheckController** — Vérification de santé ``` GET /api/health → Version, latence BDD, mémoire, version PHP ``` ### Exemple de controller commenté ```php denyAccessUnlessGranted('ROLE_VIEWER'); // ← Vérifie que l'utilisateur est connecté $data = json_decode($request->getContent(), true); // ← Parse le body JSON $comment = new Comment(); $comment->setContent($data['content']); $comment->setEntityType($data['entityType']); $comment->setEntityId($data['entityId']); // ... autres champs $em->persist($comment); // ← Dit à Doctrine "je veux sauvegarder ça" $em->flush(); // ← Exécute réellement le INSERT SQL return $this->json($comment, 201); // ← Renvoie le commentaire créé avec le code 201 } } ``` --- ## Le système d'audit Chaque modification sur les entités principales est automatiquement enregistrée dans un journal d'audit. C'est un des points forts de l'application. ### Comment ça marche ? Les **Event Subscribers** de Doctrine interceptent les opérations de base de données **avant** qu'elles soient exécutées (événement `onFlush`). ``` L'utilisateur modifie une machine ↓ Doctrine détecte le changement ↓ onFlush se déclenche ↓ Le subscriber calcule le diff (ancien → nouveau) ↓ Le subscriber crée un AuditLog avec : - entityType : "machine" - entityId : "cl1a2b3c..." - action : "update" - diff : { "name": { "from": "CNC 01", "to": "CNC 02" } } - snapshot : { état complet de la machine } - actorProfileId : "cl9z8y7x..." (qui a fait la modif) ↓ Les deux (machine + audit log) sont sauvegardés en même temps ``` ### Le diff Le diff capture exactement ce qui a changé : ```json { "name": { "from": "CNC Mazak 01", "to": "CNC Mazak 02" }, "prix": { "from": "45000.00", "to": "50000.00" }, "constructeurIds": { "from": ["cl111...", "cl222..."], "to": ["cl111...", "cl333..."] } } ``` ### Le snapshot Le snapshot capture l'état complet de l'entité au moment de la modification : ```json { "id": "cl1a2b3c...", "name": "CNC Mazak 02", "reference": "CNM-001", "prix": "50000.00", "siteId": "cl9z8y7x...", "constructeurIds": ["cl111...", "cl333..."] } ``` ### Les subscribers d'audit | Subscriber | Entité | Type | |------------|--------|------| | MachineAuditSubscriber | Machine | Complex (avec constructeurs + custom fields) | | ComposantAuditSubscriber | Composant | Complex | | PieceAuditSubscriber | Piece | Complex | | ProductAuditSubscriber | Product | Complex | | ConstructeurAuditSubscriber | Constructeur | Simple | | DocumentAuditSubscriber | Document | Simple | | ModelTypeAuditSubscriber | ModelType | Simple | **Simple** = suit seulement les champs de l'entité **Complex** = suit aussi les relations ManyToMany (constructeurs) et les champs personnalisés ### AbstractAuditSubscriber La classe de base qui contient toute la logique partagée : ```php abstract class AbstractAuditSubscriber implements EventSubscriber { // Méthode à implémenter par chaque subscriber abstract protected function getEntityClass(): string; // Ex: Machine::class abstract protected function getEntityType(): string; // Ex: 'machine' abstract protected function buildSnapshot($entity): array; // Construit le snapshot // Deux chemins d'exécution : // 1. onFlushSimple() : pour les entités sans collections ManyToMany // 2. onFlushComplex() : pour les entités avec constructeurs (détecte les ajouts/suppressions) } ``` ### Autres subscribers | Subscriber | Rôle | |------------|------| | **PieceProductSyncSubscriber** | Synchronise le champ `productIds` sur Piece quand un Product est lié/délié | | **UniqueConstraintSubscriber** | Capture les erreurs de doublon PostgreSQL et renvoie un message clair | --- ## L'authentification par session ### Le flux complet ``` 1. GET /api/session/profiles → Retourne la liste des profils actifs (nom, prénom, email, hasPassword) → Le frontend affiche la page de login avec les profils disponibles 2. POST /api/session/profile Body: { "profileId": "cl...", "password": "secret" } → Le backend vérifie le mot de passe → Si OK : stocke profileId dans la session PHP, retourne le profil → Si KO : retourne 401 3. GET /api/session/profile (à chaque chargement de page) → Le navigateur envoie le cookie de session automatiquement → Le backend retrouve le profil via la session → Retourne le profil connecté ou 401 4. DELETE /api/session/profile → Supprime le profileId de la session → L'utilisateur est déconnecté ``` ### La sécurité sur les endpoints Chaque endpoint API Platform a une règle de sécurité : ```php new GetCollection(security: "is_granted('ROLE_VIEWER')") // Lecture → minimum ROLE_VIEWER new Post(security: "is_granted('ROLE_GESTIONNAIRE')") // Création → minimum ROLE_GESTIONNAIRE ``` Les controllers personnalisés utilisent : ```php $this->denyAccessUnlessGranted('ROLE_VIEWER'); ``` ### La hiérarchie des rôles Grâce à la hiérarchie, un ADMIN a automatiquement tous les rôles inférieurs : ``` ROLE_ADMIN ─── a aussi ──► ROLE_GESTIONNAIRE ──► ROLE_VIEWER ──► ROLE_USER ``` Donc `is_granted('ROLE_VIEWER')` accepte aussi les GESTIONNAIRES et les ADMINS. --- ## Les services ### DocumentStorageService Gère le stockage des fichiers sur le système de fichiers : ```php // Stocker un fichier uploadé $path = $storageService->store($uploadedFile, $entityType, $entityId); // Supprimer un fichier $storageService->delete($path); ``` Les fichiers sont stockés dans `var/documents/{entityType}/{entityId}/{filename}`. ### PdfCompressorService Compresse les fichiers PDF via Ghostscript pour réduire leur taille : ```php $compressorService->compress($filePath); ``` ### ModelTypeCategoryConversionService Permet de convertir la catégorie d'un ModelType (ex: transformer un type "composant" en type "pièce"). --- ## Les migrations Les migrations sont des scripts SQL qui modifient la structure de la base de données. Elles sont dans le dossier `migrations/`. ### Principe Quand tu ajoutes un champ à une entité, il faut créer une migration pour mettre à jour la BDD : ```bash # Générer une migration à partir des changements détectés make shell php bin/console doctrine:migrations:diff # Appliquer les migrations php bin/console doctrine:migrations:migrate ``` ### Particularités PostgreSQL Les migrations utilisent du **SQL brut** avec des gardes pour l'idempotence : ```sql -- On peut relancer la migration sans erreur ALTER TABLE machine ADD COLUMN IF NOT EXISTS description TEXT; DROP INDEX IF EXISTS idx_machine_name; CREATE UNIQUE INDEX IF NOT EXISTS idx_machine_name ON machine (name); ``` **Attention aux noms de colonnes** : PostgreSQL stocke tout en **minuscules**. Donc `typePieceId` en PHP devient `typepieceid` en SQL. Toujours utiliser des noms lowercase dans le SQL brut. --- ## Les tests ### Stack de test - **PHPUnit 12** : framework de test PHP - **API Platform Test** : utilitaires pour tester des endpoints API - **DAMA DoctrineTestBundle** : wrappe chaque test dans une transaction avec rollback automatique (pas besoin de nettoyer la BDD entre les tests) ### Structure ``` tests/ ├── AbstractApiTestCase.php # Classe de base avec helpers └── Api/ └── Entity/ ├── MachineTest.php # Tests des endpoints machine ├── SiteTest.php # Tests des endpoints site └── ... ``` ### Exemple de test ```php class MachineTest extends AbstractApiTestCase { public function testCreateMachine(): void { // Créer un client HTTP connecté en tant que gestionnaire $client = $this->createGestionnaireClient(); // Créer un site (prérequis) $site = $this->createSite(); // Envoyer une requête POST pour créer une machine $client->request('POST', '/api/machines', [ 'json' => [ 'name' => 'Machine Test', 'reference' => 'MT-001', 'site' => '/api/sites/' . $site->getId(), ], ]); // Vérifier que la réponse est 201 Created $this->assertResponseStatusCodeSame(201); // Vérifier le contenu de la réponse $this->assertJsonContains([ 'name' => 'Machine Test', 'reference' => 'MT-001', ]); } } ``` ### Helpers disponibles dans AbstractApiTestCase | Méthode | Description | |---------|-------------| | `createViewerClient()` | Client HTTP connecté avec ROLE_VIEWER | | `createGestionnaireClient()` | Client HTTP connecté avec ROLE_GESTIONNAIRE | | `createAdminClient()` | Client HTTP connecté avec ROLE_ADMIN | | `createProfile()` | Crée un profil utilisateur en BDD | | `createSite()` | Crée un site en BDD | | `createMachine()` | Crée une machine en BDD | ### Lancer les tests ```bash make test # Tous les tests make test FILES=tests/Api/Entity/MachineTest.php # Un fichier make test-setup # (Re)créer la BDD de test ``` --- ## Flux complet d'une requête ### Exemple : créer une machine ``` 1. Le frontend envoie : POST /api/machines Content-Type: application/ld+json Cookie: PHPSESSID=abc123 { "name": "CNC Mazak 01", "reference": "CNM-001", "prix": "50000.00", "site": "/api/sites/cl9z8y7x..." } 2. Symfony reçoit la requête → Le routeur identifie : c'est un endpoint API Platform (POST sur Machine) 3. Sécurité → Vérifie le cookie de session → retrouve le profil connecté → Vérifie is_granted('ROLE_GESTIONNAIRE') → OK 4. Désérialisation (JSON → objet PHP) → API Platform convertit le JSON en objet Machine → Le champ "site" (IRI) est résolu en objet Site → Seuls les champs du groupe 'machine:write' sont acceptés 5. Validation → Vérifie les contraintes (name non vide, site existe, etc.) 6. Persistence (objet PHP → BDD) → Doctrine déclenche PrePersist (CuidEntityTrait) → Génère l'ID : "cl" + 24 hex chars aléatoires → Set createdAt et updatedAt → Doctrine détecte l'INSERT à faire 7. Audit (onFlush) → MachineAuditSubscriber détecte la nouvelle machine → Crée un AuditLog avec action='create', diff, snapshot → L'AuditLog est aussi ajouté à la transaction 8. Flush → Doctrine exécute les requêtes SQL : INSERT INTO machine (id, name, reference, ...) VALUES (...) INSERT INTO audit_log (id, entity_type, entity_id, action, diff, snapshot, ...) VALUES (...) 9. Sérialisation (objet PHP → JSON) → API Platform convertit la Machine en JSON-LD → Seuls les champs du groupe 'machine:read' sont inclus 10. Réponse HTTP/1.1 201 Created { "@context": "/api/contexts/Machine", "@id": "/api/machines/cl1a2b3c...", "@type": "Machine", "id": "cl1a2b3c...", "name": "CNC Mazak 01", ... } ``` --- ## Commandes Symfony utiles Lancer ces commandes dans le conteneur Docker (`make shell` pour y entrer) : | Commande | Description | |----------|-------------| | `php bin/console debug:router` | Voir toutes les routes disponibles | | `php bin/console debug:config api_platform` | Voir la config API Platform | | `php bin/console doctrine:schema:validate` | Vérifier que les entités sont synchronisées avec la BDD | | `php bin/console doctrine:migrations:diff` | Générer une migration à partir des changements | | `php bin/console doctrine:migrations:migrate` | Appliquer les migrations | | `php bin/console cache:clear` | Vider le cache (résout beaucoup de problèmes) | | `php bin/console app:compress-pdf` | Compresser les PDFs existants | | `php bin/console app:create-profile` | Créer un profil utilisateur | --- ## Résumé des points clés pour un débutant 1. **API Platform génère les endpoints CRUD automatiquement** à partir des entités — tu n'as pas besoin d'écrire de controllers pour les opérations standard 2. **Les attributs PHP 8** (`#[...]`) remplacent les annotations et configurent tout : BDD, API, sérialisation, sécurité 3. **Les groupes de sérialisation** (`machine:read`, `machine:write`) contrôlent quels champs sont visibles/modifiables 4. **L'audit est automatique** : chaque modification est tracée sans rien avoir à faire manuellement 5. **L'authentification est par session (cookies)**, pas par tokens JWT 6. **Les IDs sont des CUID** (chaînes aléatoires), pas des auto-increment 7. **PostgreSQL stocke les noms en minuscules** : attention dans le SQL brut 8. **Les tests utilisent des transactions** : chaque test est isolé et la BDD est nettoyée automatiquement