API Platform State Provider und State Processor: eigene Datenquellen
AI generated
SF
{ }
Symfony · API Platform · State Provider · PHP 8.4
API Platform State Provider und State Processor
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.

18 Min. Lesezeit ProviderInterface · ProcessorInterface · Context · Operation API Platform 4 · Symfony 7 · PHP 8.4

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.

11. FAQ: State Provider und State Processor in API Platform

1Was ist ein State Provider?
Eine Klasse mit ProviderInterface, die den Doctrine Zugriff für Lesevorgänge ersetzt und frei entscheidet, woher Ressourcendaten kommen.
2Wann brauche ich einen State Processor?
Wenn Schreiboperationen nicht direkt in eine Entity gehen, sondern an Messenger, ein externes System oder mehrere Ziele gleichzeitig.
3Eine Klasse für mehrere Operationen?
Ja, über das Operation Objekt kann der Provider erkennen, welche Operation läuft, und unterschiedlich reagieren.
4Wie gebe ich 404 zurück?
Einfach null zurückgeben bei einer Item Operation. API Platform wandelt das automatisch in einen 404 um.
5Muss der Processor selbst validieren?
Nein, die Validierung läuft vorher über die Symfony Validator Constraints, der Processor erhält bereits ein validiertes Objekt.
6Caching mit State Provider kombinieren?
Mit AsDecorator einen cachenden Provider um den eigentlichen Provider legen, der nur bei Cache Miss den inneren aufruft.
7Testen ohne HTTP Kernel?
Ja, ein State Provider ist ein normaler Service und lässt sich direkt per PHPUnit mit Test Doubles pruefen.
8N Plus 1 bei externen APIs vermeiden?
Batch Endpunkte der externen Quelle nutzen oder Ergebnisse zwischenspeichern, statt pro Element einzeln aufzurufen.
9Kombination mit Symfony Messenger?
Ja, der Processor erzeugt ein Command, dispatcht es und liefert sofort eine Bestätigung mit passendem Statuscode zurück.
10Ersetzt der Provider den Service Layer?
Nein, er bleibt eine dünne Adapterschicht, die Domain Services aufruft und deren Ergebnis in eine Ressource überführt.