# API Platform Symfony REST: Vollständiges Tutorial und Interview-Fragen 2026 > REST APIs mit API Platform 4.3 und Symfony 7 entwickeln. State Providers, Processors, Serialisierungsgruppen und typische Interview-Fragen für Symfony-Entwickler. - Published: 2026-08-25 - Updated: 2026-08-25 - Author: Anthony Fillion-Maillet - Tags: api-platform, symfony, rest-api, php, tutorial - Reading time: 12 min --- API Platform 4.3 verwandelt Symfony in ein leistungsstarkes REST-API-Framework mit automatischer OpenAPI-Dokumentation, Content Negotiation und einer sauberen Architektur, die Lese- und Schreiboperationen trennt. Dieses Tutorial behandelt die Installation, fortgeschrittene Patterns und die Interview-Fragen, die Senior-Entwickler von Einsteigern unterscheiden. > **API Platform 4 Architektur** > > API Platform 4 verwendet State Providers für GET-Operationen und State Processors für POST/PUT/PATCH/DELETE. Diese Trennung entspricht CQRS-Prinzipien und vereinfacht das Testen erheblich. ## API Platform auf Symfony 7 installieren Die Installation von API Platform erfordert Symfony 7.2 oder höher. Das Bundle integriert sich standardmäßig mit Doctrine ORM, unterstützt aber auch benutzerdefinierte Datenquellen über das Provider/Processor-Pattern. ```bash # API Platform mit Symfony Flex installieren composer require api # Installation überprüfen php bin/console debug:router | grep api ``` Das `api`-Recipe installiert `api-platform/symfony` zusammen mit den Serializer-, Validator- und Property-Access-Komponenten. Symfony Flex konfiguriert die Routen automatisch unter `/api`. ```yaml # config/packages/api_platform.yaml api_platform: title: 'My API' version: '1.0.0' formats: jsonld: ['application/ld+json'] json: ['application/json'] docs_formats: jsonld: ['application/ld+json'] jsonopenapi: ['application/vnd.openapi+json'] html: ['text/html'] defaults: stateless: true cache_headers: vary: ['Content-Type', 'Authorization', 'Accept-Language'] ``` Die Einstellung `stateless: true` deaktiviert PHP-Sessions für alle API-Endpunkte, reduziert den Speicherverbrauch und ermöglicht horizontale Skalierung. ## Die erste API-Ressource mit Attributen erstellen API Platform 4 verwendet PHP 8-Attribute zur Deklaration von API-Ressourcen. Jede Entity wird durch das `#[ApiResource]`-Attribut zu einem API-Endpunkt. ```php // src/Entity/Product.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\Delete; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity] #[ApiResource( operations: [ new GetCollection(), new Get(), new Post(security: "is_granted('ROLE_ADMIN')"), new Put(security: "is_granted('ROLE_ADMIN')"), new Delete(security: "is_granted('ROLE_ADMIN')") ], paginationItemsPerPage: 30 )] class Product { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] #[Assert\NotBlank] #[Assert\Length(min: 3, max: 255)] private string $name; #[ORM\Column(type: 'decimal', precision: 10, scale: 2)] #[Assert\Positive] private string $price; #[ORM\Column] private \DateTimeImmutable $createdAt; public function __construct() { $this->createdAt = new \DateTimeImmutable(); } public function getId(): ?int { return $this->id; } public function getName(): string { return $this->name; } public function setName(string $name): static { $this->name = $name; return $this; } public function getPrice(): string { return $this->price; } public function setPrice(string $price): static { $this->price = $price; return $this; } public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; } } ``` Der `security`-Parameter jeder Operation nutzt Symfonys Expression Language. Die `is_granted()`-Funktion prüft Voter-Entscheidungen und ermöglicht rollenbasierte Zugriffskontrolle ohne benutzerdefinierte Controller. ## Serialisierungsgruppen zur Response-Gestaltung Serialisierungsgruppen steuern, welche Eigenschaften in API-Responses erscheinen. Verschiedene Operationen können unterschiedliche Felder derselben Entity anzeigen. ```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 Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Entity] #[ApiResource( operations: [ new GetCollection(normalizationContext: ['groups' => ['user:list']]), new Get(normalizationContext: ['groups' => ['user:read']]), new Post( normalizationContext: ['groups' => ['user:read']], denormalizationContext: ['groups' => ['user:write']] ) ] )] class User { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['user:list', 'user:read'])] private ?int $id = null; #[ORM\Column(length: 180)] #[Groups(['user:list', 'user:read', 'user:write'])] private string $email; #[ORM\Column] #[Groups(['user:write'])] private string $password; #[ORM\Column] #[Groups(['user:read'])] private \DateTimeImmutable $registeredAt; #[ORM\Column(type: 'json')] #[Groups(['user:read'])] private array $roles = []; } ``` Der `normalizationContext` steuert die Ausgabe (PHP zu JSON). Der `denormalizationContext` steuert die Eingabe (JSON zu PHP). Das `password`-Feld verwendet nur `user:write`, sodass es niemals in Responses erscheint. ## State Providers für benutzerdefinierte Datenquellen State Providers rufen Daten für GET-Operationen ab. Die Standard-Provider `ItemProvider` und `CollectionProvider` verwenden Doctrine, aber benutzerdefinierte Provider ermöglichen beliebige Datenquellen: Elasticsearch, externe APIs oder berechnete Werte. ```php // src/State/ProductStatsProvider.php namespace App\State; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use App\Repository\ProductRepository; use App\Dto\ProductStats; final readonly class ProductStatsProvider implements ProviderInterface { public function __construct( private ProductRepository $productRepository ) {} public function provide(Operation $operation, array $uriVariables = [], array $context = []): ProductStats { $totalProducts = $this->productRepository->count([]); $averagePrice = $this->productRepository->getAveragePrice(); $lowStockCount = $this->productRepository->countLowStock(threshold: 10); return new ProductStats( totalProducts: $totalProducts, averagePrice: $averagePrice, lowStockCount: $lowStockCount ); } } ``` ```php // src/Dto/ProductStats.php namespace App\Dto; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use App\State\ProductStatsProvider; #[ApiResource( operations: [ new Get( uriTemplate: '/products/stats', provider: ProductStatsProvider::class ) ] )] final readonly class ProductStats { public function __construct( public int $totalProducts, public float $averagePrice, public int $lowStockCount ) {} } ``` Das `ProductStats`-DTO (Data Transfer Object) ist keine Doctrine-Entity. Es repräsentiert berechnete Daten, die unter `/api/products/stats` verfügbar sind. Der Provider berechnet die Werte bei jeder Anfrage. ## State Processors für Schreiboperationen State Processors verarbeiten POST-, PUT-, PATCH- und DELETE-Operationen. Benutzerdefinierte Processors ermöglichen die Ausführung von Geschäftslogik vor oder nach der Persistierung. ```php // src/State/UserRegistrationProcessor.php namespace App\State; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use App\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; final readonly class UserRegistrationProcessor implements ProcessorInterface { public function __construct( private EntityManagerInterface $entityManager, private UserPasswordHasherInterface $passwordHasher ) {} public function process( mixed $data, Operation $operation, array $uriVariables = [], array $context = [] ): User { // $data ist die deserialisierte User-Entity aus dem Request-Body $hashedPassword = $this->passwordHasher->hashPassword( $data, $data->getPlainPassword() ); $data->setPassword($hashedPassword); $data->eraseCredentials(); $this->entityManager->persist($data); $this->entityManager->flush(); return $data; } } ``` Der Processor wird an die POST-Operation der User-Entity angehängt: ```php #[ApiResource( operations: [ new Post( processor: UserRegistrationProcessor::class, denormalizationContext: ['groups' => ['user:create']] ) ] )] ``` Der Processor hasht das Passwort, bevor Doctrine die Entity persistiert. Die `plainPassword`-Eigenschaft verwendet eine schreibgeschützte Serialisierungsgruppe und wird niemals in der Datenbank gespeichert. ## Filter für Query-Parameter API Platform bietet integrierte Filter zum Suchen, Sortieren und Filtern von Collections. Filter fügen Query-Parameter zu GET-Collection-Endpunkten hinzu. ```php // src/Entity/Article.php namespace App\Entity; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\Filter\DateFilter; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiResource; #[ORM\Entity] #[ApiResource] #[ApiFilter(SearchFilter::class, properties: [ 'title' => 'partial', 'author.name' => 'exact', 'category' => 'exact' ])] #[ApiFilter(DateFilter::class, properties: ['publishedAt'])] #[ApiFilter(OrderFilter::class, properties: ['publishedAt', 'title'])] class Article { // Entity-Eigenschaften... } ``` Die Filterkonfiguration aktiviert folgende Query-Parameter: - `GET /api/articles?title=symfony` (Teilübereinstimmung im Titel) - `GET /api/articles?author.name=John` (exakte Übereinstimmung in verknüpfter Entity) - `GET /api/articles?publishedAt[after]=2026-01-01` (Datumsbereich) - `GET /api/articles?order[publishedAt]=desc` (Sortierung) Benutzerdefinierte Filter erweitern `AbstractFilter` für komplexe Abfragelogik, die integrierte Filter nicht abdecken können. ## Fehlerbehandlung und Validierung API Platform integriert sich mit Symfony Validator. Validierungsfehler geben 422 Unprocessable Entity mit RFC 7807-Problemdetails zurück. ```php // src/Entity/Order.php use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity] #[ApiResource] class Order { #[ORM\Column] #[Assert\NotBlank(message: 'Bestellmenge ist erforderlich')] #[Assert\Positive(message: 'Menge muss größer als null sein')] #[Assert\LessThanOrEqual(value: 100, message: 'Maximal 100 Artikel pro Bestellung')] private int $quantity; } ``` Ein Validierungsfehler gibt zurück: ```json { "@type": "ConstraintViolationList", "status": 422, "violations": [ { "propertyPath": "quantity", "message": "Menge muss größer als null sein" } ] } ``` Benutzerdefinierte Validatoren mit Klassen-Constraints behandeln feldübergreifende Validierung, beispielsweise die Sicherstellung, dass das Enddatum einer Bestellung nach dem Startdatum liegt. ## Interview-Fragen: API Platform Tiefenwissen Technische Interviews prüfen das Verständnis über die Grundnutzung hinaus. Diese Fragen erscheinen häufig bei Symfony-Backend-Positionen. **F: Wie unterscheidet sich API Platform vom manuellen Schreiben von Controllern?** API Platform generiert CRUD-Operationen aus Entity-Metadaten. Ein einzelnes `#[ApiResource]`-Attribut erzeugt GET-, POST-, PUT-, PATCH- und DELETE-Endpunkte mit OpenAPI-Dokumentation, Content Negotiation, Paginierung und Validierung. Manuelle Controller erfordern die separate Implementierung jeder Funktion. API Platform reduziert Boilerplate-Code um 70-80% für Standard-REST-Operationen und bleibt durch State Providers und Processors für benutzerdefinierte Logik erweiterbar. **F: Erklären Sie den Unterschied zwischen State Providers und State Processors.** State Providers behandeln den Datenabruf (GET-Operationen). Sie implementieren `ProviderInterface::provide()` und geben Entities, DTOs oder Collections zurück. State Processors behandeln Datenänderungen (POST, PUT, PATCH, DELETE). Sie implementieren `ProcessorInterface::process()` und empfangen das deserialisierte Objekt aus dem Request-Body. Diese Trennung folgt CQRS-Prinzipien: Lese- und Schreibvorgänge haben unterschiedliche Code-Pfade. **F: Wie verhindert man die Offenlegung sensibler Felder in API-Responses?** Serialisierungsgruppen steuern die Feldsichtbarkeit. Sensible Felder (Passwörter, interne IDs, Audit-Daten) werden schreibgeschützten Gruppen zugewiesen oder vollständig ausgeschlossen. `#[Groups(['admin:read'])]` für Felder verwenden, die nur Administratoren sehen sollten, dann den operations-spezifischen `normalizationContext` so konfigurieren, dass diese Gruppe nur bei Admin-Endpunkten enthalten ist. **F: Wie funktioniert die Paginierung in API Platform?** API Platform paginiert Collections standardmäßig mit 30 Elementen pro Seite. Der `page`-Query-Parameter steuert den Offset. Hydra-Metadaten in JSON-LD-Responses enthalten `hydra:view` mit first/last/next/previous-Links. Konfiguration über `paginationItemsPerPage`, `paginationMaximumItemsPerPage` und `paginationClientItemsPerPage` (erlaubt Clients, verschiedene Seitengrößen anzufordern). **F: Wann würde man ein DTO statt der direkten Entity-Exposition verwenden?** DTOs entkoppeln den API-Vertrag vom Datenbankschema. DTOs verwenden, wenn: (1) die API-Darstellung sich wesentlich von der Entity-Struktur unterscheidet, (2) mehrere Entities zu einer Response kombiniert werden, (3) berechnete Felder in Responses erscheinen, (4) die Eingabevalidierung von Entity-Constraints abweicht, oder (5) die Entity [Doctrine Inheritance](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/inheritance-mapping.html) verwendet, die die Serialisierung kompliziert. DTOs verhindern auch die versehentliche Offenlegung neuer Entity-Felder bei Schemaänderungen. ## API Platform Endpunkte testen API Platform arbeitet mit Symfonys Test-Framework. `ApiTestCase` bietet Methoden für authentifizierte Anfragen und JSON-Response-Assertions. ```php // tests/Api/ProductTest.php namespace App\Tests\Api; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; use App\Entity\Product; class ProductTest extends ApiTestCase { public function testGetCollection(): void { $response = static::createClient()->request('GET', '/api/products'); $this->assertResponseIsSuccessful(); $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); $this->assertJsonContains(['@context' => '/api/contexts/Product']); $this->assertMatchesResourceCollectionJsonSchema(Product::class); } public function testCreateProduct(): void { $response = static::createClient()->request('POST', '/api/products', [ 'json' => [ 'name' => 'Test Product', 'price' => '29.99' ], 'headers' => [ 'Authorization' => 'Bearer ' . $this->getAdminToken() ] ]); $this->assertResponseStatusCodeSame(201); $this->assertJsonContains([ 'name' => 'Test Product', 'price' => '29.99' ]); } public function testCreateProductValidationFails(): void { static::createClient()->request('POST', '/api/products', [ 'json' => ['name' => 'AB'], // Zu kurz 'headers' => ['Authorization' => 'Bearer ' . $this->getAdminToken()] ]); $this->assertResponseStatusCodeSame(422); $this->assertJsonContains([ 'violations' => [ ['propertyPath' => 'name'] ] ]); } } ``` Die Methode `assertMatchesResourceCollectionJsonSchema()` validiert Responses gegen das automatisch generierte [JSON Schema](https://json-schema.org/) und erkennt Regressionen bei Änderungen der Entity-Struktur. ## Performance-Optimierung mit Eager Loading N+1-Queries verschlechtern die Performance von Collection-Endpunkten. API Platforms `fetchEager`-Option und Doctrines [Query Hints](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/dql-doctrine-query-language.html#query-hints) lösen dieses Problem. ```php #[ApiResource( operations: [ new GetCollection( extraProperties: ['doctrine_orm_fetch_join' => true] ) ] )] #[ORM\Entity] class Order { #[ORM\ManyToOne(fetch: 'EAGER')] #[ORM\JoinColumn(nullable: false)] private Customer $customer; #[ORM\OneToMany(mappedBy: 'order', fetch: 'EAGER')] private Collection $items; } ``` Für komplexe Abfragen übertreffen benutzerdefinierte State Providers mit optimiertem DQL das automatische Eager Loading. Die Anzahl der Queries mit dem Symfony Profiler vor und nach der Optimierung messen. ## Produktionsreife API Platform Konfiguration ```yaml # config/packages/api_platform.yaml api_platform: title: '%env(API_TITLE)%' version: '%env(API_VERSION)%' show_webby: false defaults: stateless: true cache_headers: max_age: 3600 shared_max_age: 3600 vary: ['Content-Type', 'Authorization'] extra_properties: standard_put: true exception_to_status: Symfony\Component\Security\Core\Exception\AccessDeniedException: 403 App\Exception\BusinessException: 400 ``` `show_webby` (das Maskottchen) in Produktion deaktivieren. Die `standard_put`-Eigenschaft stellt sicher, dass PUT Ressourcen vollständig ersetzt anstatt zu mergen, entsprechend der [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-4.3.4)-Semantik. ## Wichtige Erkenntnisse zu API Platform 2026 - State Providers rufen Daten für GET-Anfragen ab. State Processors verarbeiten POST/PUT/PATCH/DELETE. Diese Trennung ermöglicht sauberes Testen und benutzerdefinierte Datenquellen. - Serialisierungsgruppen steuern, welche Felder in Responses erscheinen. Unterschiedliche Gruppen für Listen- vs. Detailansichten verwenden und schreibgeschützte Gruppen für Passwörter. - Filter fügen Query-Parameter für Suche, Sortierung und Datumsbereiche hinzu. Integrierte Filter decken die meisten Fälle ab, benutzerdefinierte Filter behandeln komplexe Abfragen. - DTOs entkoppeln API-Verträge von Datenbankschemas. Sie verwenden, wenn die Response-Struktur von der Entity abweicht oder Daten aus mehreren Quellen kombiniert werden. - Der [Symfony Serializer](/technologies/symfony/interview-questions/serializer) übernimmt die JSON-zu-Entity-Konvertierung. Das Verständnis seiner Normalizer und Context-Optionen ist für fortgeschrittene API Platform-Nutzung unerlässlich. - API Platform 4.3 ist die aktuelle stabile Version. Version 5.0 (in Alpha) erfordert Symfony 7.4 oder 8.0 und beendet die Unterstützung älterer Symfony-Versionen. - Dokumentation unter [api-platform.com](https://api-platform.com/docs/) behandelt Randfälle, die hier nicht angesprochen werden. Die [Doctrine ORM Integration](/blog/symfony/doctrine-orm-mastering-relationships) erklärt die Entity-Beziehungsbehandlung im Detail. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/de/blog/symfony/api-platform-symfony-rest-tutorial