API Platform 3 — API REST en minutes
API Platform 3 génère automatiquement une API REST complète (GET/POST/PUT/PATCH/DELETE) + documentation OpenAPI + client GraphQL à partir d'un seul attribut sur votre entité.
composer require api
# Installe api-platform/core, serializer, doctrine…
Auto-CRUD
GET/POST/PUT/PATCH/DELETE générés automatiquement
OpenAPI
Swagger UI intégrée — /api/docs
Hydra/JSON-LD
Hypermedia par défaut, JSON:API en option
Extensible
StateProcessors, StateProviders, Voters
#[ApiResource] — déclaration
<?php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Delete;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ApiResource(
operations: [
new GetCollection(normalizationContext: ['groups' => ['article:list']]),
new Post(
normalizationContext: ['groups' => ['article:read']],
denormalizationContext: ['groups' => ['article:write']],
security: "is_granted('ROLE_USER')",
),
new Get(normalizationContext: ['groups' => ['article:read']]),
new Put(
denormalizationContext: ['groups' => ['article:write']],
security: "is_granted('edit', object)",
),
new Patch(
denormalizationContext: ['groups' => ['article:write']],
security: "is_granted('edit', object)",
),
new Delete(security: "is_granted('delete', object)"),
],
paginationItemsPerPage: 20,
)]
class Article
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
#[Groups(['article:list', 'article:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['article:list', 'article:read', 'article:write'])]
#[Assert\NotBlank]
#[Assert\Length(min: 5, max: 255)]
private string $title = '';
#[ORM\Column(type: 'text', nullable: true)]
#[Groups(['article:read', 'article:write'])]
private ?string $body = null;
#[ORM\Column(type: 'boolean')]
#[Groups(['article:read'])]
private bool $published = false;
}
Opérations personnalisées
<?php
use ApiPlatform\Metadata\Post as ApiPost;
// Opération custom : publier un article
#[ApiPost(
uriTemplate: '/articles/{id}/publish',
name: 'article_publish',
input: false,
output: Article::class,
processor: PublishArticleProcessor::class,
security: "is_granted('ROLE_EDITOR')",
description: 'Publier un article',
)]
class Article { /* … */ }
Routes générées (exemple)
GET /api/articles → liste paginée
POST /api/articles → créer un article
GET /api/articles/{id} → détail
PUT /api/articles/{id} → remplacer
PATCH /api/articles/{id} → modifier (partiel)
DELETE /api/articles/{id} → supprimer
POST /api/articles/{id}/publish → opération custom
Groupes de sérialisation
<?php
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
class Article
{
#[Groups(['article:list', 'article:read'])]
private ?int $id = null;
// SerializedName — renommer la clé JSON
#[Groups(['article:list', 'article:read', 'article:write'])]
#[SerializedName('headline')]
private string $title = '';
// Relation imbriquée — exposer seulement certains champs
#[Groups(['article:read'])]
private ?User $author = null;
}
class User
{
#[Groups(['article:read', 'user:read'])]
private ?int $id = null;
#[Groups(['article:read', 'user:read'])]
private string $name = '';
// email non exposé dans le contexte article:read
#[Groups(['user:read'])]
private string $email = '';
}
Propriétés calculées (ApiProperty)
<?php
use ApiPlatform\Metadata\ApiProperty;
class Article
{
#[ApiProperty(readable: true, writable: false)]
#[Groups(['article:read'])]
public function getExcerpt(): string
{
return mb_substr($this->body ?? '', 0, 200) . '…';
}
}
Filtres
<?php
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
use ApiPlatform\Metadata\ApiFilter;
#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: [
'title' => 'partial', // LIKE %title%
'author.email' => 'exact',
'category.name' => 'ipartial', // insensible à la casse
])]
#[ApiFilter(OrderFilter::class, properties: ['createdAt', 'title'])]
#[ApiFilter(BooleanFilter::class, properties: ['published'])]
#[ApiFilter(DateFilter::class, properties: ['createdAt'])]
class Article { /* … */ }
# Exemples de requêtes avec filtres
GET /api/articles?title=symfony
GET /api/articles?published=true
GET /api/articles?order[createdAt]=desc
GET /api/articles?createdAt[after]=2026-01-01
GET /api/articles?author.email=user@example.com
Pagination
<?php
#[ApiResource(
paginationEnabled: true,
paginationItemsPerPage: 20,
paginationMaximumItemsPerPage: 100,
paginationClientItemsPerPage: true, // le client peut choisir
paginationPartial: false, // vrai si partial pagination
)]
class Article { /* … */ }
# Réponse paginée (JSON-LD)
GET /api/articles?page=2
{
"@context": "/api/contexts/Article",
"@type": "hydra:Collection",
"hydra:member": [ /* …20 articles… */ ],
"hydra:totalItems": 157,
"hydra:view": {
"@id": "/api/articles?page=2",
"hydra:first": "/api/articles?page=1",
"hydra:last": "/api/articles?page=8",
"hydra:next": "/api/articles?page=3"
}
}
StateProcessor & StateProvider
StateProcessor — modifier les données avant persistance
<?php
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Article;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
class ArticleProcessor implements ProcessorInterface
{
public function __construct(
private readonly ProcessorInterface $innerProcessor,
private readonly SluggerInterface $slugger,
) {}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
{
if ($data instanceof Article && empty($data->getSlug())) {
$data->setSlug($this->slugger->slug($data->getTitle())->lower()->toString());
}
return $this->innerProcessor->process($data, $operation, $uriVariables, $context);
}
}
StateProvider — source de données custom
<?php
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Dto\ArticleStatsDto;
class ArticleStatsProvider implements ProviderInterface
{
public function __construct(private readonly ArticleRepository $repo) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
return new ArticleStatsDto(
total: $this->repo->count([]),
published: $this->repo->countPublished(),
);
}
}
Documentation OpenAPI / Swagger
<?php
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\OpenApi\Model;
#[ApiResource(
openapi: new Model\Operation(
summary: 'Créer un article',
description: 'Crée un nouvel article et retourne la ressource créée.',
tags: ['Articles'],
requestBody: new Model\RequestBody(
description: 'Données de l\'article',
content: new \ArrayObject([
'application/json' => [
'schema' => [
'type' => 'object',
'properties' => [
'title' => ['type' => 'string', 'example' => 'Mon article Symfony'],
'body' => ['type' => 'string'],
],
],
],
]),
),
)
)]
class Article { /* … */ }
Swagger UI disponible sur
/api/docs — interface interactive pour tester tous les endpoints. JSON à /api/docs.json, YAML à /api/docs.yaml.