Files
Inventory/docs/BACKEND.md
2026-03-08 13:47:46 +01:00

29 KiB

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
  2. Comment fonctionne une API REST
  3. Symfony + API Platform — les bases
  4. Les Entités (les modèles de données)
  5. Les Controllers (les endpoints personnalisés)
  6. Le système d'audit
  7. L'authentification par session
  8. Les services
  9. Les migrations de base de données
  10. Les tests
  11. Flux complet d'une requête
  12. 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 :

{
  "@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 :
{
  "@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 :

// 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
// src/Entity/Machine.php

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Delete;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;

#[ORM\Entity(repositoryClass: MachineRepository::class)]    // ← Lié à une table en BDD
#[ORM\HasLifecycleCallbacks]                                  // ← Active les hooks PrePersist/PreUpdate
#[ApiResource(                                                // ← Génère les endpoints API
    operations: [
        new GetCollection(security: "is_granted('ROLE_VIEWER')"),     // GET /api/machines
        new Get(security: "is_granted('ROLE_VIEWER')"),               // GET /api/machines/{id}
        new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),        // POST /api/machines
        new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),       // PATCH /api/machines/{id}
        new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),      // DELETE /api/machines/{id}
    ],
    normalizationContext: ['groups' => ['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 :

// 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
// src/Controller/CommentController.php

namespace App\Controller;

use App\Entity\Comment;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;

class CommentController extends AbstractController
{
    #[Route('/api/comments', methods: ['POST'])]    // ← Définit l'URL et le verbe HTTP
    public function create(
        Request $request,                            // ← La requête HTTP entrante
        EntityManagerInterface $em,                  // ← Pour écrire en BDD (injecté automatiquement)
    ): JsonResponse {
        $this->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é :

{
  "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 :

{
  "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 :

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é :

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 :

$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 :

// 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 :

$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 :

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

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

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

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