eigene Datenquellen jenseits von Doctrine anbinden
Wer glaubt, API Platform funktioniere nur mit Doctrine Entities, kennt den State Provider und den State Processor noch nicht. Diese beiden Interfaces entkoppeln Lesen und Schreiben komplett von der Persistenzschicht und erlauben es, REST Endpunkte, Legacy Systeme oder reine Rechenwerte als vollwertige API Ressourcen bereitzustellen.
Inhaltsverzeichnis
- 1. Warum ein State Provider und ein State Processor
- 2. Das ProviderInterface: eigene Lesezugriffe
- 3. Das ProcessorInterface: Schreiblogik nach der Validierung
- 4. Non Doctrine Datenquellen anbinden
- 5. Context und Operation Objekt richtig auslesen
- 6. Mehrere Provider mit dem Decorator Pattern kombinieren
- 7. Testing von State Providern und Processoren
- 8. Fehlerbehandlung und Performance
- 9. State Provider im Vergleich zum Doctrine Standardpfad
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein State Provider und ein State Processor
API Platform bindet standardmäßig Doctrine ORM Entities an REST und GraphQL Endpunkte. Das funktioniert gut, solange die Ressource wirklich einer Datenbanktabelle entspricht. Sobald aber ein Endpunkt Daten aus mehreren Quellen zusammenführt, einen externen Dienst abfragt oder rein berechnete Werte liefert, reicht der Standardpfad nicht mehr aus. Genau hier setzt der State Provider an: Er ersetzt den impliziten Doctrine Zugriff durch eine eigene Klasse, die entscheidet, wie eine Ressource geladen wird.
Der State Processor übernimmt spiegelbildlich die Schreibseite. Nach der Deserialisierung und Validierung eines Requests übergibt API Platform das Objekt an den State Processor, der frei entscheidet, wohin die Daten geschrieben werden: in eine Datenbank, an eine Warteschlange, an ein externes System oder in mehrere Ziele gleichzeitig. Diese Trennung von Transportformat und Persistenz ist der Kern, warum ein State Provider und ein State Processor in fortgeschrittenen API Platform Projekten praktisch unumgänglich werden.
Wichtig ist dabei: Ein State Provider ist kein Ersatz für einen Service, sondern eine dünne Adapterschicht. Die eigentliche Geschäftslogik bleibt in Domain Services, der State Provider ruft sie nur auf und formt das Ergebnis in ein API Ressourcen Objekt um. Wer diese Trennung konsequent einhält, bekommt testbare, austauschbare Bausteine statt eines monolithischen Controllers mit API Platform Anmerkungen.
2. Das ProviderInterface: eigene Lesezugriffe
Ein State Provider implementiert ProviderInterface mit genau einer Methode: provide(Operation $operation, array $uriVariables = [], array $context = []). Für eine Collection Operation gibt die Methode ein iterierbares Array oder ein Paginator Objekt zurück, für eine Item Operation ein einzelnes Objekt oder null, wenn die Ressource nicht gefunden wurde. API Platform kümmert sich danach automatisch um Serialisierung, Statuscode und Content Negotiation, der State Provider muss sich darum nicht kümmern.
Die Registrierung erfolgt über das provider Attribut direkt auf der Ressourcenklasse oder pro Operation, was feingranulare Kontrolle erlaubt: Eine Collection Operation kann einen anderen State Provider nutzen als die zugehörige Item Operation derselben Ressource. Das ist besonders praktisch, wenn Listen aus einem Cache kommen sollen, Einzelabrufe aber immer frisch von einer externen API geladen werden müssen.
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\ApiResource\WeatherStation;
use App\Service\WeatherFeedClient;
/**
* Custom state provider that loads weather station data
* from an external feed instead of Doctrine.
*/
final readonly class WeatherStationProvider implements ProviderInterface
{
public function __construct(
private WeatherFeedClient $client,
) {
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable|object|null
{
// Item operation: single station requested by id
if (isset($uriVariables['id'])) {
$data = $this->client->fetchStation($uriVariables['id']);
return $data === null ? null : WeatherStation::fromFeed($data);
}
// Collection operation: return all active stations
return array_map(
static fn (array $row): WeatherStation => WeatherStation::fromFeed($row),
$this->client->fetchAllStations(),
);
}
}
3. Das ProcessorInterface: Schreiblogik nach der Validierung
Der State Processor implementiert ProcessorInterface mit der Methode process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []). Der Parameter $data enthält bereits das deserialisierte und validierte Objekt, die eigentliche Validierung ist zu diesem Zeitpunkt abgeschlossen. Der State Processor entscheidet nur noch, was mit den Daten passiert: persistieren, an einen Message Bus weiterreichen oder in ein anderes Format transformieren, bevor es an ein Drittsystem geht.
Ein häufiges Muster ist der kombinierte Einsatz aus State Processor und Symfony Messenger: Der Processor validiert nichts mehr selbst, sondern erzeugt lediglich ein Command, das asynchron verarbeitet wird, und gibt sofort eine Bestätigung mit Statuscode 202 zurück. So bleibt der API Aufruf schnell, während die eigentliche Verarbeitung im Hintergrund läuft, ohne dass der Client warten muss.
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\ApiResource\OrderExport;
use Symfony\Component\Messenger\MessageBusInterface;
/**
* State processor that dispatches order exports asynchronously
* instead of writing directly to Doctrine.
*/
final readonly class OrderExportProcessor implements ProcessorInterface
{
public function __construct(
private MessageBusInterface $bus,
) {
}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): OrderExport
{
// Validated DTO is dispatched as a message, not persisted directly
$this->bus->dispatch(new RequestOrderExport(
orderId: $data->orderId,
format: $data->format,
));
$data->status = 'queued';
return $data;
}
}
4. Non Doctrine Datenquellen anbinden
Der praktische Nutzen eines State Providers zeigt sich am deutlichsten bei Non Doctrine Datenquellen. Klassische Beispiele sind ein Legacy SOAP Dienst, eine In Memory Konfiguration, ein Redis Hash oder eine externe REST API, die als eigene API Platform Ressource nach außen gespiegelt werden soll. In allen Fällen bleibt die API Ressource ein simples PHP Objekt mit Attributen, während der State Provider die eigentliche Beschaffung übernimmt.
Wichtig ist, dass die API Ressource selbst keine Doctrine Annotationen mehr braucht. Sie wird als reines PHP Objekt mit dem Attribut #[ApiResource(provider: WeatherStationProvider::class)] deklariert. Das entkoppelt die öffentliche API Struktur vollständig vom internen Speicherformat, sodass sich die Datenquelle später austauschen lässt, ohne dass sich am veröffentlichten Vertrag etwas ändert.
Gerade bei Migrationen ist das Gold wert: Ein Team kann eine alte Datenbanktabelle nach und nach durch einen neuen Microservice ersetzen, ohne dass API Konsumenten etwas davon bemerken, solange der State Provider die neue Quelle transparent hinter derselben Ressource versteckt.
5. Context und Operation Objekt richtig auslesen
Das $context Array, das an jeden State Provider und State Processor übergeben wird, enthält wertvolle Metadaten: den aktuellen Request, gesetzte Filter, den Sicherheitskontext und bei GraphQL zusätzlich Informationen über angeforderte Felder. Ein State Provider kann so beispielsweise nur die Felder aus einer externen API laden, die tatsächlich abgefragt wurden, und unnötige Netzwerkaufrufe vermeiden.
Das Operation Objekt liefert unter anderem den Namen der Operation, konfigurierte Extra Properties und die zugehörige Ressourcenklasse. Diese Informationen erlauben es, einen einzigen generischen State Provider für mehrere Ressourcen wiederzuverwenden, solange die relevanten Unterschiede über Extra Properties im #[ApiResource] Attribut konfiguriert werden, statt für jede Ressource eine eigene Klasse zu schreiben.
6. Mehrere Provider mit dem Decorator Pattern kombinieren
In größeren Projekten müssen mehrere Zuständigkeiten kombiniert werden: Caching, Zugriffsprüfung und die eigentliche Datenbeschaffung. Statt all das in einer Klasse zu bündeln, lässt sich ein State Provider dekorieren. Ein cachender State Provider ruft im Cache Miss Fall den inneren, eigentlichen State Provider auf und speichert dessen Ergebnis zwischen. Symfonys Dependency Injection unterstützt das über #[AsDecorator] direkt auf der PHP Klasse.
Dieses Muster hält jede einzelne State Provider Klasse klein und einzeln testbar, während sich Querschnittsbelange wie Caching oder Logging beliebig hinzufügen oder entfernen lassen, ohne die eigentliche Datenbeschaffungslogik anzufassen. Es ist dasselbe Prinzip wie bei Middleware, nur auf der Ebene von API Platform Providern angewendet.
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Contracts\Cache\CacheInterface;
/**
* Decorates the inner provider with a short lived cache layer.
*/
#[AsDecorator(decorates: WeatherStationProvider::class)]
final readonly class CachedWeatherStationProvider implements ProviderInterface
{
public function __construct(
private ProviderInterface $inner,
private CacheInterface $cache,
) {
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable|object|null
{
$key = 'weather_station_' . ($uriVariables['id'] ?? 'collection');
return $this->cache->get($key, function () use ($operation, $uriVariables, $context) {
return $this->inner->provide($operation, $uriVariables, $context);
});
}
}
7. Testing von State Providern und Processoren
Weil ein State Provider ein normaler Symfony Service ist, lässt er sich vollständig ohne HTTP Kernel testen. Ein reiner PHPUnit Test instanziiert die Klasse mit einem Test Double für die abhängige Datenquelle und ruft provide() direkt auf. Das ist deutlich schneller als ein funktionaler Test über den Kernel und deckt die eigentliche Logik isoliert ab.
Für den kompletten Pfad inklusive Serialisierung und Statuscodes bleibt trotzdem ein funktionaler Test mit ApiTestCase sinnvoll, der prüft, dass der registrierte State Provider tatsächlich aufgerufen wird und die HTTP Antwort korrekt aussieht. Beide Testebenen ergänzen sich: der Unit Test für die Logik im State Provider, der funktionale Test für die Integration in API Platform.
8. Fehlerbehandlung und Performance
Ein State Provider sollte niemals stillschweigend null zurückgeben, wenn eigentlich ein Fehler in der Datenquelle vorliegt. API Platform interpretiert null bei einer Item Operation als 404, das ist bei einem Netzwerkfehler zur externen Quelle irreführend. Stattdessen sollte eine eigene Exception geworfen werden, die über einen Symfony Exception Listener in einen passenden 502 oder 503 Statuscode übersetzt wird, damit Monitoring den Unterschied zwischen fehlender Ressource und defekter Anbindung erkennen kann.
Bei Performance lohnt sich ein Blick auf N Plus 1 Probleme auch außerhalb von Doctrine: Ein State Provider, der für jedes Element einer Collection einen eigenen externen Aufruf macht, skaliert schlecht. Batch Endpunkte der externen Quelle oder ein zwischengeschalteter Cache im Decorator lösen dieses Problem, bevor es in Produktion zum Engpass wird.
9. State Provider im Vergleich zum Doctrine Standardpfad
Die Entscheidung zwischen dem impliziten Doctrine Zugriff und einem eigenen State Provider hängt von der Herkunft der Daten und den Anforderungen an Testbarkeit ab. Die folgende Tabelle fasst die wichtigsten Unterschiede zusammen.
| Kriterium | Doctrine Standardpfad | Eigener State Provider | Empfehlung |
|---|---|---|---|
| Datenquelle | Doctrine Entity Manager | Beliebig: API, Cache, Legacy System | State Provider bei Nicht Doctrine Quellen |
| Testbarkeit | Benötigt Datenbank Fixtures | Reiner Unit Test möglich | State Provider für schnelle Testsuiten |
| Kopplung | Ressource an Entity gebunden | Ressource frei modellierbar | State Provider für stabile öffentliche Verträge |
| Implementierungsaufwand | Keine eigene Klasse nötig | Eigene Klasse pro Ressource | Doctrine Standardpfad für CRUD ohne Sonderfälle |
| Caching und Dekoration | Schwer nachrüstbar | Über AsDecorator beliebig erweiterbar | State Provider für Querschnittsbelange |
In der Praxis mischen die meisten Projekte beide Ansätze: einfache CRUD Ressourcen bleiben beim Doctrine Standardpfad, während komplexe Aggregationen, externe Anbindungen und asynchrone Schreiboperationen über einen eigenen State Provider oder State Processor laufen. Diese Mischung ist kein Kompromiss, sondern die eigentliche Stärke von API Platform: Der Standardpfad bleibt für den einfachen Fall erhalten, während der State Provider für den komplexen Fall die volle Kontrolle zurückgibt.
Mironsoft
Symfony und API Platform Architektur für anspruchsvolle APIs
Eigene State Provider und Processor für euer API Platform Projekt?
Wir entwerfen API Ressourcen, die von der Persistenz entkoppelt sind, binden Legacy Systeme und externe APIs sauber an und sichern eure Schreibpfade mit getesteten State Processoren ab.
API Architektur
State Provider und Processor Design für komplexe Ressourcen
Legacy Integration
Externe Systeme über eigene State Provider transparent anbinden
Testautomatisierung
Unit und funktionale Tests für State Provider und Processor aufbauen
10. Zusammenfassung
Ein State Provider ersetzt den impliziten Doctrine Zugriff durch eine eigene Klasse, die frei entscheidet, woher die Daten einer API Ressource kommen. Ein State Processor übernimmt spiegelbildlich die Schreibseite und entkoppelt die Zielspeicherung vollständig von Validierung und Serialisierung. Beide Interfaces sind der Schlüssel, um API Platform auch für Non Doctrine Datenquellen, asynchrone Verarbeitung und Legacy Anbindungen produktiv einzusetzen.
Wer State Provider und State Processor konsequent als dünne Adapterschicht über Domain Services baut, bekommt kleine, unit testbare Klassen statt aufgeblähter Controller. Das Decorator Pattern erlaubt zusätzlich, Caching und andere Querschnittsbelange ohne Eingriff in die eigentliche Beschaffungslogik nachzurüsten, sodass ein einmal geschriebener State Provider über die gesamte Projektlaufzeit wartbar bleibt.
State Provider und State Processor in API Platform: Das Wichtigste auf einen Blick
ProviderInterface
Ersetzt den Doctrine Zugriff für Lesevorgänge, liefert Objekte oder Arrays für beliebige Datenquellen zurück.
ProcessorInterface
Übernimmt nach Validierung die Schreiblogik, kann persistieren, an Messenger weiterreichen oder transformieren.
Decorator Pattern
Mit AsDecorator lassen sich Caching, Logging und Zugriffsprüfung um einen State Provider herumlegen.
Testbarkeit
Reine PHP Klassen ohne Kernel Abhängigkeit, direkt per PHPUnit gegen Test Doubles prüfbar.