# Symfony 7: API Platform und Best Practices > Vollstaendiger Leitfaden zu API Platform 4 mit Symfony 7. State Processors, State Providers, Serialisierungsgruppen und erweiterte Validierung fuer professionelle REST-APIs. - Published: 2026-01-12 - Updated: 2026-04-07 - Author: SharpSkill - Tags: symfony, api platform, php, rest api, api development - Reading time: 14 min --- API Platform 4, kombiniert mit Symfony 7, hat sich als der leistungsfaehigste Ansatz fuer die Entwicklung moderner REST- und GraphQL-APIs in der PHP-Oekosystem etabliert. Dieses Framework bietet automatische OpenAPI-Dokumentation, integrierte Paginierung, Filter, Validierung und Serialisierung — alles aus einer einzigen, deklarativen Konfiguration heraus. Mit den Neuerungen von API Platform 4.2 und Symfony 7.2 stehen Entwicklern noch maechtiger Werkzeuge zur Verfuegung, um produktionsreife APIs zu bauen. > **Neuerungen in API Platform 4.2** > > API Platform 4.2 bringt State Processors und State Providers als vollstaendigen Ersatz fuer DataTransformers und DataProviders der alten Versionen. Diese neuen Konzepte trennen Lese- und Schreiboperationen klar voneinander und machen den Code testbarer und wartbarer. Ausserdem unterstuetzt API Platform 4.2 nativ UUID v7 fuer Ressourcen-IDs. ## Installation und Erstkonfiguration Ein neues Symfony-Projekt mit API Platform wird ueber Composer erstellt. Der API Platform Installer richtet alle notwendigen Abhaengigkeiten ein. ```bash # Installation via Composer composer create-project symfony/skeleton my-api cd my-api composer require api # Alternatively with the API Platform distribution composer create-project api-platform/api-platform my-api # Install development tools composer require --dev symfony/maker-bundle doctrine/doctrine-fixtures-bundle # Configure the database (PostgreSQL recommended) composer require doctrine/doctrine-bundle doctrine/orm # Start the development server symfony serve -d ``` Die Grundkonfiguration von API Platform erfolgt in `config/packages/api_platform.yaml`. ```yaml # config/packages/api_platform.yaml api_platform: title: 'My API' version: '1.0.0' description: 'API built with API Platform 4 and Symfony 7' # Default formats formats: jsonld: mime_types: ['application/ld+json'] json: mime_types: ['application/json'] html: mime_types: ['text/html'] # Default pagination defaults: pagination_enabled: true pagination_items_per_page: 20 pagination_maximum_items_per_page: 100 # OpenAPI documentation openapi: contact: name: 'API Support' url: 'https://example.com/support' email: 'support@example.com' ``` ## Erstellen einer einfachen API-Ressource Mit dem `#[ApiResource]`-Attribut wird jede Doctrine-Entitaet zu einer vollstaendigen REST-API-Ressource, inklusive automatischer CRUD-Operationen, OpenAPI-Dokumentation und Validierung. ```php ['book:read']], denormalizationContext: ['groups' => ['book:write']], paginationItemsPerPage: 10, )] class Book { #[ORM\Id] #[ORM\Column(type: 'uuid', unique: true)] #[ORM\GeneratedValue(strategy: 'CUSTOM')] #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')] private ?Uuid $id = null; #[ORM\Column(length: 255)] #[Assert\NotBlank] #[Assert\Length(min: 2, max: 255)] private ?string $title = null; #[ORM\Column(length: 13, unique: true)] #[Assert\NotBlank] #[Assert\Isbn] private ?string $isbn = null; #[ORM\Column(type: 'text', nullable: true)] private ?string $description = null; #[ORM\Column] #[Assert\NotNull] #[Assert\Positive] private ?float $price = null; #[ORM\Column] private \DateTimeImmutable $publishedAt; public function getId(): ?Uuid { return $this->id; } public function getTitle(): ?string { return $this->title; } public function setTitle(string $title): static { $this->title = $title; return $this; } public function getIsbn(): ?string { return $this->isbn; } public function setIsbn(string $isbn): static { $this->isbn = $isbn; return $this; } public function getDescription(): ?string { return $this->description; } public function setDescription(?string $description): static { $this->description = $description; return $this; } public function getPrice(): ?float { return $this->price; } public function setPrice(float $price): static { $this->price = $price; return $this; } public function getPublishedAt(): \DateTimeImmutable { return $this->publishedAt; } public function setPublishedAt(\DateTimeImmutable $publishedAt): static { $this->publishedAt = $publishedAt; return $this; } } ``` Nach der Migration (`php bin/console doctrine:migrations:migrate`) stellt API Platform automatisch die Endpunkte `GET /books`, `POST /books`, `GET /books/{id}`, `PUT /books/{id}`, `PATCH /books/{id}` und `DELETE /books/{id}` bereit — vollstaendig dokumentiert in Swagger UI unter `/api`. > **UUID v7 empfohlen** > > Seit API Platform 4.1 wird UUID v7 fuer Ressourcen-IDs empfohlen. Im Gegensatz zu UUID v4 ist UUID v7 zeitbasiert und daher besser fuer Datenbankindizes geeignet. Die Konfiguration erfolgt ueber `#[ORM\CustomIdGenerator(class: UuidV7Generator::class)]`. Sequentielle IDs aus Sicherheitsgruenden vermeiden. ## Serialisierungsgruppen zur Kontrolle exponierter Daten Serialisierungsgruppen ermoelichen die praezise Steuerung, welche Felder bei Lese- oder Schreiboperationen zugelassen werden. Dies verhindert Massenassignment-Sicherheitsluecken und reduziert die uebertragene Datenmenge. ```php ['user:list']] ), new Get( normalizationContext: ['groups' => ['user:read']] ), new Post( denormalizationContext: ['groups' => ['user:create']], normalizationContext: ['groups' => ['user:read']] ), new Put( denormalizationContext: ['groups' => ['user:update']], normalizationContext: ['groups' => ['user:read']] ), ] )] class User { #[ORM\Id, ORM\GeneratedValue, ORM\Column] #[Groups(['user:list', 'user:read'])] private ?int $id = null; #[ORM\Column(length: 180, unique: true)] #[Groups(['user:list', 'user:read', 'user:create'])] #[Assert\NotBlank, Assert\Email] private ?string $email = null; #[ORM\Column(length: 100)] #[Groups(['user:list', 'user:read', 'user:create', 'user:update'])] #[Assert\NotBlank] private ?string $displayName = null; // Only visible in the detailed read, never in the list #[ORM\Column(type: 'text', nullable: true)] #[Groups(['user:read', 'user:update'])] private ?string $biography = null; // Only writable, never readable (password hash) #[Groups(['user:create', 'user:update'])] #[Assert\NotBlank(groups: ['user:create'])] #[Assert\Length(min: 8)] private ?string $plainPassword = null; // Internal: not exposed in any group #[ORM\Column] private string $password = ''; #[ORM\Column] #[Groups(['user:read'])] private \DateTimeImmutable $createdAt; // Getters and setters... } ``` Diese Konfiguration stellt sicher, dass Passwoerter niemals in API-Antworten erscheinen, die E-Mail-Adresse nach der Erstellung nicht geaendert werden kann und die Biografie nur in der Detailansicht sichtbar ist. ## State Processors fuer Geschaeftslogik State Processors ersetzen in API Platform 4 die alten DataTransformers und PersistedDataProviders. Sie werden ausgefuehrt, wenn eine Ressource erstellt, aktualisiert oder geloescht wird, und ermoelichen die Einbindung von Geschaeftslogik vor oder nach der Datenbankoperation. ```php getPlainPassword()) { return $this->processor->process($data, $operation, $uriVariables, $context); } $hashedPassword = $this->passwordHasher->hashPassword( $data, $data->getPlainPassword() ); $data->setPassword($hashedPassword); $data->eraseCredentials(); return $this->processor->process($data, $operation, $uriVariables, $context); } } ``` Fuer komplexere Geschaeftslogik kann ein dediziierter Processor mehrere Services einbinden. ```php processor->process($data, $operation, $uriVariables, $context); } if ($operation instanceof Delete) { $this->searchIndex->remove($data->getId()); $this->logger->info('Book removed from search index', ['id' => $data->getId()]); return $this->processor->process($data, $operation, $uriVariables, $context); } $result = $this->processor->process($data, $operation, $uriVariables, $context); // Post-persistence actions $this->searchIndex->index($result); if ($operation instanceof Post) { $this->notifications->sendNewBookNotification($result); } $this->logger->info('Book processed', [ 'id' => $result->getId(), 'operation' => $operation::class, ]); return $result; } } ``` Die Registrierung des Processors erfolgt per Attribut direkt in der Entitaet oder in der `services.yaml`. ## State Providers fuer benutzerdefinierte Datenquellen State Providers kontrollieren, wie Daten vor der Serialisierung abgerufen werden. Sie sind besonders nuetzlich fuer komplexe Abfragen, externe Datenquellen oder berechnete Ressourcen. ```php cache->get($cacheKey, function (ItemInterface $item) use ($period, $page, $limit) { $item->expiresAfter(3600); // 1 hour cache return $this->bookRepository->findPopular( period: $period, page: $page, limit: $limit ); }); } } ``` Der Provider wird direkt im `#[ApiResource]`-Attribut referenziert. ```php ArticleGroupsGenerator::class] ), new Put( validationContext: ['groups' => ArticleGroupsGenerator::class] ), ] )] class Article { #[ORM\Id, ORM\GeneratedValue, ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] #[Assert\NotBlank(groups: ['Default', 'article:draft', 'article:publish'])] #[Assert\Length(min: 5, max: 255, groups: ['Default', 'article:draft', 'article:publish'])] private ?string $title = null; #[ORM\Column(type: 'text', nullable: true)] #[Assert\NotBlank(groups: ['article:publish'])] #[Assert\Length(min: 100, groups: ['article:publish'])] private ?string $content = null; #[ORM\Column(length: 20)] #[Assert\Choice(choices: ['draft', 'review', 'published'])] private string $status = 'draft'; #[ORM\Column(nullable: true)] #[Assert\NotNull(groups: ['article:publish'])] private ?\DateTimeImmutable $publishAt = null; #[AppAssert\UniqueSlug(groups: ['Default', 'article:publish'])] #[ORM\Column(length: 255, unique: true, nullable: true)] private ?string $slug = null; // Getters and setters... } ``` Der Groups Generator bestimmt dynamisch, welche Validierungsgruppen in Abhaengigkeit des Anwendungsstatus verwendet werden. ```php getStatus() === 'published') { $groups[] = 'article:publish'; } elseif ($object->getStatus() === 'draft') { $groups[] = 'article:draft'; } // Additional group if a publication date is set if ($object->getPublishAt() !== null) { $groups[] = 'article:scheduled'; } return $groups; } } ``` > **Validierungsperformance** > > Bei grossen Datensaetzen koennen Constraints wie `#[Assert\UniqueEntity]` zu einem zusaetzlichen Datenbankquery pro Validierung fuehren. Fuer massenhafte Importe den Validator direkt mit gezielten Gruppen aufrufen oder die Validierung asynchron per Messenger-Job durchfuehren. ## Filter fuer flexible Abfragen API Platform bietet eine grosse Anzahl vorgefertigter Filter fuer Suche, Sortierung und Bereichsabfragen. Die Konfiguration erfolgt ausschliesslich ueber PHP-Attribute. ```php 'partial', // LIKE %name% 'category' => 'exact', // Exact match 'brand' => 'start', // LIKE brand% 'description' => 'word_start', // Full-text word search ])] #[ApiFilter(RangeFilter::class, properties: ['price', 'stock'])] #[ApiFilter(BooleanFilter::class, properties: ['inStock', 'featured'])] #[ApiFilter(DateFilter::class, properties: ['createdAt', 'updatedAt'])] #[ApiFilter(NumericFilter::class, properties: ['weight', 'rating'])] #[ApiFilter(OrderFilter::class, properties: ['name', 'price', 'createdAt', 'rating'], 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(length: 100)] private ?string $category = null; #[ORM\Column(length: 100)] private ?string $brand = null; #[ORM\Column] private float $price = 0.0; #[ORM\Column] private int $stock = 0; #[ORM\Column] private bool $inStock = true; #[ORM\Column] private bool $featured = false; #[ORM\Column] private \DateTimeImmutable $createdAt; // Getters and setters... } ``` Nach dieser Konfiguration stehen folgende Abfragen automatisch zur Verfuegung: ``` GET /products?name=wireless&category=electronics&inStock=true&price[gte]=50&price[lte]=500&sort[price]=asc ``` API Platform generiert die entsprechenden Swagger-Parameter automatisch und validiert Eingaben vor der Ausfuehrung. ## Beziehungen und Subressourcen API Platform verarbeitet Doctrine-Beziehungen nativ und stellt sie als IRIs (Internationalized Resource Identifiers) in JSON-LD-Antworten dar. Subressourcen ermoelichen verschachtelte Endpunkte. ```php ['author:read']] )] class Author { #[ORM\Id, ORM\GeneratedValue, ORM\Column] #[Groups(['author:read', 'book:read'])] private ?int $id = null; #[ORM\Column(length: 100)] #[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 new Link( fromClass: Author::class, fromProperty: 'books' ) ], operations: [new GetCollection()], normalizationContext: ['groups' => ['book:read']] )] #[ApiResource( operations: [ new GetCollection(), new Post(), new Get(), new Put(), new Patch(), new Delete(), ], normalizationContext: ['groups' => ['book:read']], denormalizationContext: ['groups' => ['book:write']], )] class Book { // ... #[ORM\ManyToOne(inversedBy: 'books')] #[ORM\JoinColumn(nullable: false)] #[Groups(['book:read', 'book:write'])] private ?Author $author = null; } ``` Dadurch wird der Endpunkt `GET /authors/{authorId}/books` automatisch erstellt, der nur die Buecher des angegebenen Autors zurueckgibt. ## Sicherheit und Zugriffskontrolle API Platform integriert Symfonys Security-Komponente ueber `security`- und `securityPostDenormalize`-Attribute auf Ressourcen- und Operationsebene. ```php ['order:read']], denormalizationContext: ['groups' => ['order:write']], )] class Order { #[ORM\Id, ORM\GeneratedValue, ORM\Column] #[Groups(['order:read'])] private ?int $id = null; #[ORM\ManyToOne] #[Groups(['order:read', 'admin:read'])] private ?User $customer = null; #[ORM\Column(length: 20)] #[Groups(['order:read'])] private string $status = 'pending'; #[ORM\Column] #[Groups(['order:read'])] private float $total = 0.0; public function getCustomer(): ?User { return $this->customer; } public function getStatus(): string { return $this->status; } // Other getters and setters... } ``` Fuer komplexe Berechtigungslogik empfiehlt sich die Verwendung von Symfony Voters, die aus den Security-Expressions heraus aufgerufen werden koennen. ## Automatisierte API-Tests API Platform liefert eine Testinfrastruktur, die auf Symfonys HttpKernel aufbaut und einen vollstaendigen API-Testzyklus mit JSON-Assertions ermoeglicht. ```php request('GET', '/api/books'); $this->assertResponseIsSuccessful(); $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); $this->assertJsonContains([ '@context' => '/api/contexts/Book', '@type' => 'hydra:Collection', 'hydra:totalItems' => 5, ]); $this->assertCount(5, $response->toArray()['hydra:member']); } public function testCreateBook(): void { $user = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]); $token = $this->getToken(['username' => $user->getEmail(), 'password' => 'password']); static::createClient()->request('POST', '/api/books', [ 'auth_bearer' => $token, 'json' => [ 'title' => 'Clean Code', 'isbn' => '9780132350884', 'price' => 35.99, 'publishedAt' => '2008-08-01T00:00:00+00:00', ], ]); $this->assertResponseStatusCodeSame(201); $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); $this->assertJsonContains([ '@type' => 'Book', 'title' => 'Clean Code', 'isbn' => '9780132350884', 'price' => 35.99, ]); } public function testUpdateBook(): void { $book = BookFactory::createOne(['price' => 25.00]); $user = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]); $token = $this->getToken(['username' => $user->getEmail(), 'password' => 'password']); static::createClient()->request('PATCH', '/api/books/' . $book->getId(), [ 'auth_bearer' => $token, 'headers' => ['Content-Type' => 'application/merge-patch+json'], 'json' => ['price' => 29.99], ]); $this->assertResponseIsSuccessful(); $this->assertJsonContains(['price' => 29.99]); } public function testDeleteBookForbiddenForUser(): void { $book = BookFactory::createOne(); $user = UserFactory::createOne(['roles' => ['ROLE_USER']]); $token = $this->getToken(['username' => $user->getEmail(), 'password' => 'password']); static::createClient()->request('DELETE', '/api/books/' . $book->getId(), [ 'auth_bearer' => $token, ]); $this->assertResponseStatusCodeSame(403); } private function getToken(array $credentials): string { $response = static::createClient()->request('POST', '/api/auth', ['json' => $credentials]); return $response->toArray()['token']; } } ``` ## Fazit API Platform 4 mit Symfony 7 bietet ein vollstaendiges Oekosystem fuer den Aufbau professioneller APIs. Die Kombination aus deklarativer Konfiguration, leistungsstarken State Processors und Providers sowie der nahtlosen Integration mit dem Symfony-Ecosystem macht es zur ersten Wahl fuer skalierbare PHP-APIs. ### Checkliste fuer eine produktionsreife API Platform API - `#[ApiResource]` mit expliziten Operationen statt Default-Konfiguration verwenden - Serialisierungsgruppen fuer jede Lese-/Schreiboperation definieren - Passwoerter und sensible Daten aus allen Serialisierungsgruppen ausschliessen - State Processors fuer Geschaeftslogik einsetzen, niemals direkt im Controller - State Providers fuer benutzerdefinierte oder gecachte Datenquellen verwenden - Dynamische Validierungsgruppen fuer statusabhaengige Regeln konfigurieren - Filter ueber Attribute definieren und nie manuell in Providers implementieren - Sicherheitsregeln mit `security` auf jeder Operation festlegen - API-Tests mit `ApiTestCase` und Foundry Factories abdecken - UUID v7 fuer Ressourcen-IDs anstelle sequenzieller Integer verwenden Die konsequente Anwendung dieser Patterns resultiert in APIs, die sowohl sicher als auch wartbar sind und den automatisch generierten OpenAPI-Dokumentationsvorteil vollstaendig ausnutzen. API Platform nimmt Entwicklern die repetitive Arbeit ab und laesst Raum fuer die Implementierung eigentlicher Geschaeftslogik. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/de/blog/symfony/symfony-7-api-platform-best-practices