Symfony 7: API Platform en Best Practices
Volledige gids voor het bouwen van professionele REST API's met Symfony 7 en API Platform 4. State Providers, Processors, validatie en serialisatie uitgelegd met praktische voorbeelden.

API Platform 4 transformeert de creatie van REST- en GraphQL-API's met Symfony 7 radicaal. Deze nieuwe versie introduceert een herwerkte filosofie: duidelijke scheiding van verantwoordelijkheden, vereenvoudigde State Providers en Processors, en native integratie van de Symfony Object Mapper. Het bouwen van een professionele API is nog nooit zo intuïtief geweest.
Versie 4.2 introduceert de JSON Streamer voor prestatiewinst tot +32% RPS, een volledig vernieuwd filtersysteem en Mutators om operaties aan te passen zonder de kern te raken. Ondersteuning voor Symfony 7 en 8 is native aanwezig.
Installatie en Initiele Configuratie
API Platform installeert in enkele commando's met Symfony Flex. De standaardconfiguratie dekt de meeste gebruiksscenario's en blijft volledig aanpasbaar.
# terminal
# Create a new Symfony project with API Platform
composer create-project symfony/skeleton my-api
cd my-api
# Install API Platform with Doctrine ORM
composer require api
# Verify installation
php bin/console debug:router | grep apiSymfony Flex configureert automatisch routes, OpenAPI-documentatie en de Swagger UI-interface die toegankelijk is via /api.
# config/packages/api_platform.yaml
api_platform:
title: 'My API'
version: '1.0.0'
# Supported response formats
formats:
jsonld: ['application/ld+json']
json: ['application/json']
# OpenAPI documentation
swagger:
versions: [3]
# Default pagination
defaults:
pagination_items_per_page: 30
pagination_maximum_items_per_page: 100Deze configuratie definieert serialisatieformaten, globale paginering en API-documentatiemetadata.
Een Eenvoudige API Resource Aanmaken
Het #[ApiResource]-attribuut stelt een Doctrine-entiteit bloot als REST-resource. API Platform genereert automatisch CRUD-endpoints, OpenAPI-documentatie en basisvalidaties.
<?php
// src/Entity/Book.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\Validator\Constraints as Assert;
#[ORM\Entity]
#[ApiResource(
// Explicit definition of available operations
operations: [
new GetCollection(),
new Post(),
new Get(),
new Put(),
new Patch(),
new Delete(),
],
// Default ordering for collections
order: ['publishedAt' => 'DESC'],
// Pagination configuration for this resource
paginationItemsPerPage: 20
)]
class Book
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank(message: 'Title is required')]
#[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 and 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;
}
}Deze entiteit genereert zes endpoints: GET /api/books, POST /api/books, GET /api/books/{id}, PUT /api/books/{id}, PATCH /api/books/{id} en DELETE /api/books/{id}.
API Platform ondersteunt UUID v7 als identifier van nature. Deze aanpak verbetert de beveiliging (niet-voorspelbare identifiers) en prestaties (natuurlijke sortering op aanmaafdatum).
Serialisatiegroepen voor Controle over Blootgestelde Data
Serialisatiegroepen definiëren nauwkeurig welke eigenschappen worden blootgesteld voor lezen (normalisatie) en schrijven (denormalisatie). Deze scheiding is essentieel voor de beveiliging en flexibiliteit van de API.
<?php
// src/Entity/User.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;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use App\State\UserPasswordHasher;
#[ORM\Entity]
#[ORM\Table(name: '`user`')]
#[UniqueEntity('email')]
#[ApiResource(
operations: [
new GetCollection(),
// Custom processor for password hashing
new Post(processor: UserPasswordHasher::class,
validationContext: ['groups' => ['Default', 'user:create']]),
new Get(),
new Put(processor: UserPasswordHasher::class),
new Patch(processor: UserPasswordHasher::class),
new Delete(),
],
// Properties exposed when reading
normalizationContext: ['groups' => ['user:read']],
// Properties accepted when writing
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;
// Never exposed when reading, only when writing
#[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();
}
// UserInterface implementation
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 and 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;
}
}Met deze configuratie wordt plainPassword nooit blootgesteld in antwoorden, maar kan het worden meegegeven bij aanmaak of updates.
Klaar om je Symfony gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
State Processors voor Bedrijfslogica
State Processors onderscheppen persistentieoperaties om bedrijfslogica toe te voegen. API Platform 4 vereenvoudigt hun creatie via dependency injection op basis van attributen.
<?php
// src/State/UserPasswordHasher.php
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\User;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
/**
* Processor that hashes password before persistence
* @implements ProcessorInterface<User, User>
*/
final class UserPasswordHasher implements ProcessorInterface
{
public function __construct(
// Injection of standard Doctrine processor
#[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 password if provided
if ($data->getPlainPassword()) {
$hashedPassword = $this->passwordHasher->hashPassword(
$data,
$data->getPlainPassword()
);
$data->setPassword($hashedPassword);
$data->eraseCredentials();
}
// Delegate persistence to standard processor
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
}
}Dit compositiepatroon maakt het mogelijk om willekeurige logica toe te voegen (verzenden van e-mails, events, logging) met behoud van het standaard persistentiegedrag.
Processor met Conditionele Logica
Een processor kan zijn gedrag aanpassen op basis van het type operatie.
<?php
// src/State/BookProcessor.php
namespace App\State;
use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Post;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Book;
use App\Service\NotificationService;
use App\Service\SearchIndexer;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
/**
* @implements ProcessorInterface<Book, Book|void>
*/
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
{
// Deletion: use remove processor
if ($operation instanceof DeleteOperationInterface) {
$this->searchIndexer->remove($data);
return $this->removeProcessor->process($data, $operation, $uriVariables, $context);
}
// Creation: set publication date
if ($operation instanceof Post) {
$data->setPublishedAt(new \DateTimeImmutable());
}
// Standard persistence
$result = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
// Post-processing: indexing and notification
$this->searchIndexer->index($result);
if ($operation instanceof Post) {
$this->notifications->notifyNewBook($result);
}
return $result;
}
}State Providers voor Aangepaste Databronnen
State Providers halen gegevens op uit elke bron: externe API's, cache, bestanden of complexe bedrijfslogica.
<?php
// src/State/PopularBooksProvider.php
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Entity\Book;
use App\Repository\BookRepository;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
/**
* Provider that returns popular books with caching
* @implements ProviderInterface<Book>
*/
final class PopularBooksProvider implements ProviderInterface
{
public function __construct(
private BookRepository $bookRepository,
private CacheInterface $cache,
) {
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
{
// 5-minute cache for popular books
return $this->cache->get('popular_books', function (ItemInterface $item) {
$item->expiresAfter(300);
return $this->bookRepository->findPopular(limit: 10);
});
}
}Deze provider wordt gebruikt op een toegewijde operatie.
<?php
// src/Entity/Book.php (excerpt)
use App\State\PopularBooksProvider;
#[ApiResource(
operations: [
// ... other operations
new GetCollection(
uriTemplate: '/books/popular',
provider: PopularBooksProvider::class,
paginationEnabled: false,
),
],
)]
class Book
{
// ...
}Geavanceerde Validatie met Dynamische Groepen
API Platform integreert van nature de Symfony Validator-component. Validatiegroepen kunnen variëren op basis van de operatie of context.
<?php
// src/Entity/Article.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 Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ApiResource(
operations: [
new GetCollection(),
new Get(),
// Strict validation on creation
new Post(validationContext: ['groups' => ['Default', 'article:create']]),
// More lenient validation for updates
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 characters on creation
#[Assert\Length(min: 500, groups: ['article:create'])]
// Minimum 100 characters for updates
#[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)]
// Required only if status is "published"
#[Assert\NotBlank(groups: ['article:publish'])]
private ?\DateTimeImmutable $publishedAt = null;
// Getters and setters...
}Dynamische Validatie met een Service
Voor complexe validatieregels biedt een aangepaste groepsgenerator maximale flexibiliteit.
<?php
// src/Validator/ArticleGroupsGenerator.php
namespace App\Validator;
use ApiPlatform\Symfony\Validator\ValidationGroupsGeneratorInterface;
use App\Entity\Article;
use Symfony\Bundle\SecurityBundle\Security;
final class ArticleGroupsGenerator implements ValidationGroupsGeneratorInterface
{
public function __construct(
private Security $security,
) {
}
public function __invoke(object $object): array
{
assert($object instanceof Article);
$groups = ['Default'];
// Admins have fewer restrictions
if ($this->security->isGranted('ROLE_ADMIN')) {
$groups[] = 'admin';
return $groups;
}
// Additional validation if publishing
if ($object->getStatus() === 'published') {
$groups[] = 'article:publish';
}
return $groups;
}
}Complexe validaties kunnen de prestaties beïnvloeden. Voor bulkimports verdient het aanbeveling bepaalde validaties tijdelijk uit te schakelen of asynchrone constraints te gebruiken.
Filters voor Flexibele Queries
API Platform 4.2 hertekent het filtersysteem volledig met een duidelijke scheiding van verantwoordelijkheden. Filters stellen API-clients in staat om gegevens te doorzoeken en te sorteren.
<?php
// src/Entity/Product.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ApiResource]
// Text search on name and description
#[ApiFilter(SearchFilter::class, properties: [
'name' => 'partial', // LIKE %value%
'description' => 'partial',
'category.name' => 'exact', // Search on relation
'sku' => 'exact', // Exact match
])]
// Range filtering
#[ApiFilter(RangeFilter::class, properties: ['price', 'stock'])]
// Date filtering
#[ApiFilter(DateFilter::class, properties: ['createdAt', 'updatedAt'])]
// Boolean filtering
#[ApiFilter(BooleanFilter::class, properties: ['isActive', 'isFeatured'])]
// Customizable sorting
#[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 and setters...
}Deze filters genereren automatisch OpenAPI-documentatie en maken queries mogelijk zoals:
GET /api/products?name=phone&price[gte]=100&price[lte]=500&isActive=true&sort[price]=ascRelaties en Subresources
API Platform behandelt entiteitsrelaties elegant met serialisatieopties en subresources.
<?php
// src/Entity/Author.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity]
#[ApiResource(
normalizationContext: ['groups' => ['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 and setters...
}<?php
// src/Entity/Book.php (with relation)
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity]
#[ApiResource(
normalizationContext: ['groups' => ['book:read']],
)]
// Subresource: 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 and setters...
}Beveiliging en Toegangscontrole
API Platform integreert naadloos met het beveiligingssysteem van Symfony. Voters en beveiligingsexpressies regelen de toegang tot resources.
<?php
// src/Entity/Order.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;
#[ORM\Entity]
#[ApiResource(
operations: [
// List: authenticated users see their orders
new GetCollection(
security: "is_granted('ROLE_USER')",
// Automatic filter by connected user
provider: UserOrdersProvider::class,
),
// Creation: any authenticated user
new Post(
security: "is_granted('ROLE_USER')",
processor: CreateOrderProcessor::class,
),
// Read: owner or admin
new Get(
security: "is_granted('ROLE_ADMIN') or object.getCustomer() == user",
securityMessage: "Access denied to this order.",
),
// Modification: admin only
new Patch(
security: "is_granted('ROLE_ADMIN')",
),
// Deletion: admin only
new Delete(
security: "is_granted('ROLE_ADMIN')",
),
],
)]
class Order
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false)]
private ?User $customer = null;
#[ORM\Column(length: 50)]
private ?string $status = 'pending';
#[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
private ?string $total = null;
#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;
// Getters...
public function getCustomer(): ?User
{
return $this->customer;
}
}De expressie object.getCustomer() == user geeft toegang tot de huidige entiteit en de aangemelde gebruiker voor fijnmazige controles.
Geautomatiseerde API Tests
API Platform levert PHPUnit-traits voor het eenvoudig testen van endpoints.
<?php
// tests/Api/BookTest.php
namespace App\Tests\Api;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Entity\Book;
use App\Factory\BookFactory;
use App\Factory\UserFactory;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
class BookTest extends ApiTestCase
{
use ResetDatabase;
use Factories;
public function testGetCollection(): void
{
// Create test data
BookFactory::createMany(30);
// GET request on collection
$response = static::createClient()->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,
]);
// Verify pagination (20 items per 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' => '', // Empty title = error
'isbn' => 'invalid-isbn',
],
]);
$this->assertResponseStatusCodeSame(422);
$this->assertJsonContains([
'@type' => 'ConstraintViolationList',
'violations' => [
['propertyPath' => 'title', 'message' => 'Title is required'],
],
]);
}
private function getToken(object $user): string
{
// Implementation depends on your authentication system
return 'test_token';
}
}Conclusie
API Platform 4 met Symfony 7 vertegenwoordigt de stand van de techniek voor het maken van professionele REST API's in PHP. De duidelijke scheiding tussen State Providers (lezen) en State Processors (schrijven), gecombineerd met serialisatiegroepen en het validatiesysteem, maakt het mogelijk om robuuste en onderhoudbare API's te bouwen.
Checklist voor Kwaliteits-API's
- Gebruik afzonderlijke serialisatiegroepen voor lezen en schrijven
- Implementeer State Processors voor bedrijfslogica (wachtwoordhashing, notificaties)
- Configureer filters voor zoeken en sorteren
- Pas validaties per operatie toe met groepen
- Beveilig endpoints met beveiligingsexpressies
- Schrijf functionele tests voor elk endpoint
- Documenteer de API via OpenAPI-metadata
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
De filosofie van API Platform 4 moedigt compositie boven overerving aan, en configuratie boven conventie. Het resultaat: schaalbare, testbare API's die voldoen aan REST/JSON-LD-standaarden en vanaf dag één productieklaar zijn.
Tags
Delen
