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
- Vue d'ensemble
- Comment fonctionne une API REST
- Symfony + API Platform — les bases
- Les Entités (les modèles de données)
- Les Controllers (les endpoints personnalisés)
- Le système d'audit
- L'authentification par session
- Les services
- Les migrations de base de données
- Les tests
- Flux complet d'une requête
- 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 :
@idest 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
- 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
- Les attributs PHP 8 (
#[...]) remplacent les annotations et configurent tout : BDD, API, sérialisation, sécurité - Les groupes de sérialisation (
machine:read,machine:write) contrôlent quels champs sont visibles/modifiables - L'audit est automatique : chaque modification est tracée sans rien avoir à faire manuellement
- L'authentification est par session (cookies), pas par tokens JWT
- Les IDs sont des CUID (chaînes aléatoires), pas des auto-increment
- PostgreSQL stocke les noms en minuscules : attention dans le SQL brut
- Les tests utilisent des transactions : chaque test est isolé et la BDD est nettoyée automatiquement