# Symfony 7 : API Platform et bonnes pratiques > Guide complet pour créer des APIs REST professionnelles avec Symfony 7 et API Platform 4. State Providers, Processors, validation et sérialisation expliqués. - Published: 2026-01-12 - Updated: 2026-03-28 - Author: SharpSkill - Tags: symfony, api platform, php, rest api, api development - Reading time: 14 min --- API Platform 4 transforme radicalement la création d'APIs REST et GraphQL avec Symfony 7. Cette nouvelle version apporte une philosophie repensée : séparation claire des responsabilités, State Providers et Processors simplifiés, et intégration native du Symfony Object Mapper. Construire une API professionnelle devient plus intuitif que jamais. > **Nouveautés API Platform 4.2** > > La version 4.2 introduit le JSON Streamer pour des gains de performance jusqu'à +32% RPS, un système de filtres repensé, et des Mutators pour personnaliser les opérations sans toucher au core. Le support Symfony 7 et 8 est natif. ## Installation et configuration initiale API Platform s'installe en quelques commandes avec Symfony Flex. La configuration par défaut couvre la majorité des cas d'usage tout en restant entièrement personnalisable. ```bash # terminal # Création d'un nouveau projet Symfony avec API Platform composer create-project symfony/skeleton my-api cd my-api # Installation d'API Platform avec Doctrine ORM composer require api # Vérification de l'installation php bin/console debug:router | grep api ``` Symfony Flex configure automatiquement les routes, la documentation OpenAPI et l'interface Swagger UI accessible à `/api`. ```yaml # config/packages/api_platform.yaml api_platform: title: 'Mon API' version: '1.0.0' # Formats de réponse supportés formats: jsonld: ['application/ld+json'] json: ['application/json'] # Documentation OpenAPI swagger: versions: [3] # Pagination par défaut defaults: pagination_items_per_page: 30 pagination_maximum_items_per_page: 100 ``` Cette configuration définit les formats de sérialisation, la pagination globale et les métadonnées de la documentation API. ## Création d'une ressource API simple L'attribut `#[ApiResource]` expose une entité Doctrine comme ressource REST. API Platform génère automatiquement les endpoints CRUD, la documentation OpenAPI et les validations de base. ```php 'DESC'], // Configuration de la pagination pour cette ressource paginationItemsPerPage: 20 )] class Book { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] #[Assert\NotBlank(message: 'Le titre est obligatoire')] #[Assert\Length(min: 2, max: 255)] private ?string $title = null; #[ORM\Column(type: 'text')] #[Assert\NotBlank] private ?string $description = null; #[ORM\Column(length: 13, unique: true)] #[Assert\Isbn] private ?string $isbn = null; #[ORM\Column] private ?\DateTimeImmutable $publishedAt = null; // Getters et setters... public function getId(): ?int { return $this->id; } public function getTitle(): ?string { return $this->title; } public function setTitle(string $title): static { $this->title = $title; return $this; } public function getDescription(): ?string { return $this->description; } public function setDescription(string $description): static { $this->description = $description; return $this; } public function getIsbn(): ?string { return $this->isbn; } public function setIsbn(string $isbn): static { $this->isbn = $isbn; return $this; } public function getPublishedAt(): ?\DateTimeImmutable { return $this->publishedAt; } public function setPublishedAt(\DateTimeImmutable $publishedAt): static { $this->publishedAt = $publishedAt; return $this; } } ``` Cette entité génère six endpoints : `GET /api/books`, `POST /api/books`, `GET /api/books/{id}`, `PUT /api/books/{id}`, `PATCH /api/books/{id}` et `DELETE /api/books/{id}`. > **UUID v7 recommandé** > > API Platform supporte nativement les UUID v7 comme identifiants. Cette approche améliore la sécurité (identifiants non prédictibles) et les performances (tri naturel par date de création). ## Groupes de sérialisation pour contrôler les données exposées Les groupes de sérialisation permettent de définir précisément quelles propriétés sont exposées en lecture (normalization) et en écriture (denormalization). Cette séparation est essentielle pour la sécurité et la flexibilité de l'API. ```php ['Default', 'user:create']]), new Get(), new Put(processor: UserPasswordHasher::class), new Patch(processor: UserPasswordHasher::class), new Delete(), ], // Propriétés exposées en lecture normalizationContext: ['groups' => ['user:read']], // Propriétés acceptées en écriture denormalizationContext: ['groups' => ['user:create', 'user:update']], )] class User implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['user:read'])] private ?int $id = null; #[ORM\Column(length: 180, unique: true)] #[Assert\NotBlank] #[Assert\Email] #[Groups(['user:read', 'user:create', 'user:update'])] private ?string $email = null; #[ORM\Column] private ?string $password = null; // Jamais exposé en lecture, uniquement en écriture #[Assert\NotBlank(groups: ['user:create'])] #[Groups(['user:create', 'user:update'])] private ?string $plainPassword = null; #[ORM\Column(length: 100)] #[Groups(['user:read', 'user:create', 'user:update'])] private ?string $fullName = null; #[ORM\Column(type: 'json')] #[Groups(['user:read'])] private array $roles = []; #[ORM\Column] #[Groups(['user:read'])] private ?\DateTimeImmutable $createdAt = null; public function __construct() { $this->createdAt = new \DateTimeImmutable(); } // Implémentation UserInterface public function getUserIdentifier(): string { return (string) $this->email; } public function getRoles(): array { $roles = $this->roles; $roles[] = 'ROLE_USER'; return array_unique($roles); } public function getPassword(): string { return $this->password; } public function eraseCredentials(): void { $this->plainPassword = null; } // Getters et setters... public function getId(): ?int { return $this->id; } public function getEmail(): ?string { return $this->email; } public function setEmail(string $email): static { $this->email = $email; return $this; } public function setPassword(string $password): static { $this->password = $password; return $this; } public function getPlainPassword(): ?string { return $this->plainPassword; } public function setPlainPassword(?string $plainPassword): static { $this->plainPassword = $plainPassword; return $this; } public function getFullName(): ?string { return $this->fullName; } public function setFullName(string $fullName): static { $this->fullName = $fullName; return $this; } public function setRoles(array $roles): static { $this->roles = $roles; return $this; } public function getCreatedAt(): ?\DateTimeImmutable { return $this->createdAt; } } ``` Avec cette configuration, `plainPassword` n'est jamais exposé en réponse mais peut être envoyé lors de la création ou mise à jour. ## State Processors pour la logique métier Les State Processors interceptent les opérations de persistance pour ajouter de la logique métier. API Platform 4 simplifie leur création grâce à l'injection de dépendances via attributs. ```php */ final class UserPasswordHasher implements ProcessorInterface { public function __construct( // Injection du processor Doctrine standard #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] private ProcessorInterface $persistProcessor, private UserPasswordHasherInterface $passwordHasher, ) { } public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User { // Hash le mot de passe si fourni if ($data->getPlainPassword()) { $hashedPassword = $this->passwordHasher->hashPassword( $data, $data->getPlainPassword() ); $data->setPassword($hashedPassword); $data->eraseCredentials(); } // Délègue la persistance au processor standard return $this->persistProcessor->process($data, $operation, $uriVariables, $context); } } ``` Ce pattern de composition permet d'ajouter n'importe quelle logique (envoi d'emails, événements, logs) tout en conservant le comportement standard de persistance. ### Processor avec logique conditionnelle Un processor peut adapter son comportement selon le type d'opération. ```php */ final class BookProcessor implements ProcessorInterface { public function __construct( #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] private ProcessorInterface $persistProcessor, #[Autowire(service: 'api_platform.doctrine.orm.state.remove_processor')] private ProcessorInterface $removeProcessor, private NotificationService $notifications, private SearchIndexer $searchIndexer, ) { } public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed { // Suppression : utilise le remove processor if ($operation instanceof DeleteOperationInterface) { $this->searchIndexer->remove($data); return $this->removeProcessor->process($data, $operation, $uriVariables, $context); } // Création : définit la date de publication if ($operation instanceof Post) { $data->setPublishedAt(new \DateTimeImmutable()); } // Persistance standard $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context); // Post-traitement : indexation et notification $this->searchIndexer->index($result); if ($operation instanceof Post) { $this->notifications->notifyNewBook($result); } return $result; } } ``` ## State Providers pour les sources de données personnalisées Les State Providers permettent de récupérer des données depuis n'importe quelle source : API externe, cache, fichiers, ou logique métier complexe. ```php */ final class PopularBooksProvider implements ProviderInterface { public function __construct( private BookRepository $bookRepository, private CacheInterface $cache, ) { } public function provide(Operation $operation, array $uriVariables = [], array $context = []): array { // Cache de 5 minutes pour les livres populaires return $this->cache->get('popular_books', function (ItemInterface $item) { $item->expiresAfter(300); return $this->bookRepository->findPopular(limit: 10); }); } } ``` Ce provider s'utilise sur une opération dédiée. ```php ['Default', 'article:create']]), // Validation plus souple pour les mises à jour new Put(validationContext: ['groups' => ['Default', 'article:update']]), ], )] class Article { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] #[Assert\NotBlank] #[Assert\Length(min: 10, max: 255)] private ?string $title = null; #[ORM\Column(type: 'text')] #[Assert\NotBlank] // Minimum 500 caractères à la création #[Assert\Length(min: 500, groups: ['article:create'])] // Minimum 100 caractères pour les mises à jour #[Assert\Length(min: 100, groups: ['article:update'])] private ?string $content = null; #[ORM\Column(length: 50)] #[Assert\NotBlank(groups: ['article:create'])] #[Assert\Choice(choices: ['draft', 'published', 'archived'])] private ?string $status = 'draft'; #[ORM\Column(nullable: true)] // Obligatoire seulement si le statut est "published" #[Assert\NotBlank(groups: ['article:publish'])] private ?\DateTimeImmutable $publishedAt = null; // Getters et setters... } ``` ### Validation dynamique avec un service Pour des règles de validation complexes, un générateur de groupes personnalisé offre une flexibilité totale. ```php security->isGranted('ROLE_ADMIN')) { $groups[] = 'admin'; return $groups; } // Validation supplémentaire si publication if ($object->getStatus() === 'published') { $groups[] = 'article:publish'; } return $groups; } } ``` > **Performance des validations** > > Les validations complexes peuvent impacter les performances. Pour les imports en masse, envisagez de désactiver temporairement certaines validations ou d'utiliser des contraintes asynchrones. ## Filtres pour des requêtes flexibles API Platform 4.2 repense complètement le système de filtres avec une séparation claire des responsabilités. Les filtres permettent aux clients de l'API de rechercher et trier les données. ```php 'partial', // LIKE %value% 'description' => 'partial', 'category.name' => 'exact', // Recherche sur relation 'sku' => 'exact', // Correspondance exacte ])] // Filtrage par plage de valeurs #[ApiFilter(RangeFilter::class, properties: ['price', 'stock'])] // Filtrage par date #[ApiFilter(DateFilter::class, properties: ['createdAt', 'updatedAt'])] // Filtrage booléen #[ApiFilter(BooleanFilter::class, properties: ['isActive', 'isFeatured'])] // Tri personnalisable #[ApiFilter(OrderFilter::class, properties: [ 'name', 'price', 'createdAt', ], arguments: ['orderParameterName' => 'sort'])] class Product { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] private ?string $name = null; #[ORM\Column(type: 'text', nullable: true)] private ?string $description = null; #[ORM\Column(length: 50, unique: true)] private ?string $sku = null; #[ORM\Column(type: 'decimal', precision: 10, scale: 2)] private ?string $price = null; #[ORM\Column] private ?int $stock = null; #[ORM\Column] private ?bool $isActive = true; #[ORM\Column] private ?bool $isFeatured = false; #[ORM\ManyToOne(targetEntity: Category::class)] private ?Category $category = null; #[ORM\Column] private ?\DateTimeImmutable $createdAt = null; #[ORM\Column(nullable: true)] private ?\DateTimeImmutable $updatedAt = null; // Getters et setters... } ``` Ces filtres génèrent automatiquement la documentation OpenAPI et permettent des requêtes comme : ``` GET /api/products?name=phone&price[gte]=100&price[lte]=500&isActive=true&sort[price]=asc ``` ## Relations et sous-ressources API Platform gère élégamment les relations entre entités avec des options de sérialisation et des sous-ressources. ```php ['author:read']], )] class Author { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['author:read', 'book:read'])] private ?int $id = null; #[ORM\Column(length: 255)] #[Groups(['author:read', 'book:read'])] private ?string $name = null; #[ORM\Column(type: 'text', nullable: true)] #[Groups(['author:read'])] private ?string $biography = null; #[ORM\OneToMany(mappedBy: 'author', targetEntity: Book::class)] #[Groups(['author:read'])] private Collection $books; public function __construct() { $this->books = new ArrayCollection(); } // Getters et setters... } ``` ```php ['book:read']], )] // Sous-ressource : GET /api/authors/{authorId}/books #[ApiResource( uriTemplate: '/authors/{authorId}/books', operations: [new GetCollection()], uriVariables: [ 'authorId' => new Link( fromProperty: 'books', fromClass: Author::class ), ], normalizationContext: ['groups' => ['book:read']], )] class Book { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['book:read', 'author:read'])] private ?int $id = null; #[ORM\Column(length: 255)] #[Groups(['book:read', 'author:read'])] private ?string $title = null; #[ORM\ManyToOne(targetEntity: Author::class, inversedBy: 'books')] #[ORM\JoinColumn(nullable: false)] #[Groups(['book:read'])] private ?Author $author = null; // Getters et setters... } ``` ## Sécurité et contrôle d'accès API Platform s'intègre parfaitement avec le système de sécurité Symfony. Les voters et expressions de sécurité contrôlent l'accès aux ressources. ```php customer; } } ``` L'expression `object.getCustomer() == user` donne accès à l'entité courante et à l'utilisateur connecté pour des vérifications fines. ## Tests automatisés de l'API API Platform fournit des traits PHPUnit pour tester facilement les endpoints. ```php request('GET', '/api/books'); // Assertions $this->assertResponseIsSuccessful(); $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); $this->assertJsonContains([ '@context' => '/api/contexts/Book', '@type' => 'Collection', 'totalItems' => 30, ]); // Vérifie la pagination (20 items par page) $this->assertCount(20, $response->toArray()['member']); } public function testCreateBook(): void { $user = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]); static::createClient()->request('POST', '/api/books', [ 'auth_bearer' => $this->getToken($user), 'json' => [ 'title' => 'Clean Code', 'description' => 'A Handbook of Agile Software Craftsmanship', 'isbn' => '9780132350884', ], ]); $this->assertResponseStatusCodeSame(201); $this->assertJsonContains([ '@type' => 'Book', 'title' => 'Clean Code', ]); } public function testCreateBookValidationFails(): void { $user = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]); static::createClient()->request('POST', '/api/books', [ 'auth_bearer' => $this->getToken($user), 'json' => [ 'title' => '', // Titre vide = erreur 'isbn' => 'invalid-isbn', ], ]); $this->assertResponseStatusCodeSame(422); $this->assertJsonContains([ '@type' => 'ConstraintViolationList', 'violations' => [ ['propertyPath' => 'title', 'message' => 'Le titre est obligatoire'], ], ]); } private function getToken(object $user): string { // Implémentation selon votre système d'authentification return 'test_token'; } } ``` ## Conclusion API Platform 4 avec Symfony 7 représente l'état de l'art pour créer des APIs REST professionnelles en PHP. La séparation claire entre State Providers (lecture) et State Processors (écriture), combinée aux groupes de sérialisation et au système de validation, permet de construire des APIs robustes et maintenables. ### Checklist pour une API de qualité - ✅ Utiliser des groupes de sérialisation distincts pour lecture et écriture - ✅ Implémenter des State Processors pour la logique métier (hash de mot de passe, notifications) - ✅ Configurer des filtres pour les recherches et le tri - ✅ Appliquer des validations par opération avec des groupes - ✅ Sécuriser les endpoints avec des expressions de sécurité - ✅ Écrire des tests fonctionnels pour chaque endpoint - ✅ Documenter l'API via les métadonnées OpenAPI La philosophie d'API Platform 4 encourage la composition plutôt que l'héritage, et la configuration plutôt que la convention. Le résultat : des APIs évolutives, testables et conformes aux standards REST/JSON-LD, prêtes pour la production. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/symfony/symfony-7-api-platform-bonnes-pratiques