GraphQL mit webonyx/graphql-php direkt integrieren
API Platform liefert GraphQL-Unterstützung praktisch out of the box, bringt dafür aber einen kompletten Ressourcen-, Filter- und Serialisierungs-Stack mit, der nicht jedes Projekt braucht. Wer nur eine schlanke GraphQL-Schnittstelle über ein bestehendes Symfony-Projekt legen will, kommt mit der Basisbibliothek webonyx/graphql-php oft schneller und mit weniger Overhead ans Ziel, weil Schema und Resolver dort direkt und explizit definiert werden.
Inhaltsverzeichnis
- 1. Warum überhaupt GraphQL ohne API Platform
- 2. Schema-Definition mit Typen, Queries und Mutations
- 3. Resolver als Symfony-Services per Dependency Injection binden
- 4. Query-Ausführung über einen dedizierten Controller
- 5. Abgrenzung zum eingebauten GraphQL-Support von API Platform
- 6. Performance und das N+1-Problem bei verschachtelten Resolvern
- 7. Fehlerbehandlung und Eingabevalidierung in Mutations
- 8. Schema-Caching und Persisted Queries für den Produktivbetrieb
- 9. Wann sich der Verzicht auf API Platform tatsächlich lohnt
- 10. Zusammenfassung
- 11. FAQ
1. Warum überhaupt GraphQL ohne API Platform
API Platform ist die naheliegende Wahl, wenn ein Projekt von Anfang an sowohl REST als auch GraphQL anbieten soll und die automatische Ressourcen-Erkennung über Doctrine-Entities gewünscht ist. Der Preis dafür ist ein zusätzlicher Abstraktionslayer aus Ressourcen-Metadaten, Filtern, Paginatoren und einer eigenen Serialisierungs-Pipeline, die in kleineren oder sehr spezifischen Projekten mehr Konfigurationsaufwand erzeugt als sie einspart. Wer bereits eine gewachsene Symfony-Anwendung mit eigenen Services, DTOs und Repositories betreibt, müsste diese Struktur teilweise an die Erwartungen von API Platform anpassen, statt sie einfach wiederzuverwenden.
Die Bibliothek webonyx/graphql-php ist die Referenzimplementierung der GraphQL-Spezifikation für PHP und arbeitet vollständig unabhängig von Symfony oder API Platform. Man definiert ein Schema aus Typen, Queries und Mutations, bindet dafür eigene Resolver-Funktionen ein und entscheidet selbst, wie diese an bestehende Services angebunden werden. In einem Symfony-Projekt bedeutet das, dass Resolver ganz normale Symfony-Services mit Constructor Injection sein können, was die Integration in bestehende Repository- und Service-Schichten deutlich direkter macht als der Weg über API-Platform-Ressourcen.
2. Schema-Definition mit Typen, Queries und Mutations
Ein GraphQL-Schema besteht aus einem Query-Root-Typ, optional einem Mutation-Root-Typ und den dazugehörigen Objekttypen. Mit webonyx/graphql-php lassen sich Typen entweder deklarativ als PHP-Klassen definieren, die von ObjectType erben, oder programmatisch als Instanzen mit einem fields-Callback. Für mittelgroße Schemas ist die klassenbasierte Variante meist übersichtlicher, weil jeder Typ in einer eigenen Datei liegt und sich wie ein regulärer Symfony-Service registrieren lässt, statt in einer einzigen großen Schema-Datei zu verschwinden.
Wichtig ist die klare Trennung zwischen der Typdefinition, die nur beschreibt, welche Felder mit welchem Typ existieren, und dem Resolver, der beschreibt, wie ein Feldwert tatsächlich ermittelt wird. Diese Trennung erlaubt es, dieselbe Typdefinition mit unterschiedlichen Resolvern zu kombinieren, etwa für Tests mit In-Memory-Daten oder für produktive Auflosung über Doctrine-Repositories, ohne das Schema selbst anzufassen.
<?php
declare(strict_types=1);
namespace App\GraphQL\Type;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use App\GraphQL\Resolver\ProductResolver;
/**
* GraphQL-Typdefinition für ein Produkt.
*/
final class ProductType extends ObjectType
{
public function __construct(private readonly ProductResolver $resolver)
{
parent::__construct([
'name' => 'Product',
'fields' => [
'id' => Type::nonNull(Type::id()),
'name' => Type::nonNull(Type::string()),
'price' => Type::nonNull(Type::float()),
'inStock' => [
'type' => Type::nonNull(Type::boolean()),
'resolve' => fn (array $root): bool =>
$this->resolver->isInStock((int) $root['id']),
],
],
]);
}
}
3. Resolver als Symfony-Services per Dependency Injection binden
Der große Vorteil einer manuellen GraphQL-Integration liegt darin, dass Resolver ganz normale Symfony-Services sind. Ein ProductResolver kann per Constructor Property Promotion ein ProductRepositoryInterface und einen LoggerInterface injiziert bekommen, genau wie jeder andere Service auch. Dadurch entfällt jede Sonderbehandlung für GraphQL-Code, und bestehende Business-Logik aus REST-Controllern oder Console-Commands lässt sich direkt wiederverwenden, statt sie für ein API-Platform-spezifisches Ressourcen-Modell zu duplizieren.
Damit die Typen selbst als Services mit injizierten Resolvern funktionieren, müssen sie in der services.yaml registriert und beim Aufbau des Schemas über den Container aufgelöst werden, statt sie mit new direkt zu instanziieren. Der TypeRegistry-Service kapselt diesen Auflösungsschritt zentral, sodass beim Hinzufügen eines neuen Typs nur die Klasse geschrieben und als Service getaggt werden muss, ohne den Schema-Aufbau-Code selbst zu ändern.
4. Query-Ausführung über einen dedizierten Controller
Anders als API Platform, das den GraphQL-Endpunkt automatisch registriert, wird bei einer manuellen Integration ein eigener Controller für /graphql angelegt. Dieser Controller liest Query, Variablen und Operation-Name aus dem Request-Body, ruft GraphQL::executeQuery mit dem zusammengesetzten Schema auf und gibt das Ergebnis als JSON zurück. Fehlerbehandlung, etwa für ungültige Queries oder Resolver-Exceptions, kann hier zentral an das GraphQL-eigene Error-Handling angebunden werden, das formatierte Fehlerobjekte im GraphQL-typischen errors-Array liefert.
Für Introspection, also die Fähigkeit von Tools wie GraphiQL oder Apollo Studio, das Schema automatisch zu erkunden, ist keine zusätzliche Konfiguration nötig, da webonyx/graphql-php die Introspection-Queries bereits nativ unterstützt. In produktiven Umgebungen ist es dennoch sinnvoll, Introspection über ein Feature-Flag abschaltbar zu machen, um interne Schema-Details nicht unnötig offenzulegen.
5. Abgrenzung zum eingebauten GraphQL-Support von API Platform
API Platforms GraphQL-Support generiert Queries und Mutations automatisch aus den vorhandenen API-Ressourcen inklusive Filterung, Sortierung und Paginierung nach dem Relay-Cursor-Muster. Das spart viel Boilerplate, sobald die Ressourcen bereits als API-Platform-Entities modelliert sind, bindet die GraphQL-Schicht aber eng an dieses Ressourcen-Modell. Änderungen am automatisch generierten Schema, etwa ein Feld, das im GraphQL-Typ anders heißen soll als in der Entity, erfordern zusätzliche Attribute und Konfiguration auf der Ressource selbst.
Bei einer manuellen Integration mit webonyx/graphql-php gibt es keine automatische Ableitung, dafür aber volle Kontrolle über jedes Detail des Schemas, unabhängig davon, wie die zugrunde liegenden Daten intern modelliert sind. Das lohnt sich besonders, wenn das GraphQL-Schema bewusst anders aussehen soll als das interne Datenmodell, etwa weil es eine öffentliche API für Drittanbieter beschreibt, die sich unabhängig von internen Refactorings stabil verhalten muss.
6. Performance und das N+1-Problem bei verschachtelten Resolvern
Sobald ein Resolver für ein Listenfeld pro Element wieder eigene Datenbankabfragen auslöst, entsteht das klassische N+1-Problem, bei dem eine Query mit zehn Produkten und deren Kategorien schnell elf statt zwei Datenbankzugriffe erzeugt. Die Lösung ist ein DataLoader-Pattern, bei dem einzelne Resolver-Aufrufe innerhalb eines Request-Zyklus gesammelt und anschließend gebündelt als eine einzige IN-Query ausgeführt werden, statt jede Anfrage sofort einzeln zu beantworten.
In webonyx/graphql-php lässt sich das über SyncPromiseAdapter und ein eigenes DataLoader-Objekt umsetzen, das Ladeanfragen zwischenspeichert und erst beim tatsächlichen Auflösen der Promise bündelt. Wer diese Optimierung von Anfang an einplant, vermeidet später schwer nachvollziehbare Performance-Probleme, die erst bei tief verschachtelten Queries mit vielen Objekten sichtbar werden und im Entwicklungsbetrieb mit kleinen Testdatenmengen oft unbemerkt bleiben.
7. Fehlerbehandlung und Eingabevalidierung in Mutations
Mutations sind der Ort, an dem Eingabevalidierung besonders wichtig ist, weil sie im Gegensatz zu Queries Zustand verändern. Der Symfony-Validator-Component lässt sich problemlos in einen Resolver injizieren, sodass Eingabe-Arrays zunächst in ein DTO gemapped und anschließend über die bekannten Constraints validiert werden, bevor die eigentliche Business-Logik aufgerufen wird. Bei Validierungsfehlern gibt webonyx/graphql-php die Möglichkeit, über eine eigene Error-Klasse strukturierte Fehlerdetails im errors-Array der Antwort mitzugeben, statt nur eine generische Fehlermeldung zurückzuliefern.
Für technische Fehler, etwa eine nicht erreichbare Datenbank, sollte zwischen erwarteten Business-Fehlern und unerwarteten Exceptions unterschieden werden. Ein zentraler ErrorFormatter kann unerwartete Exceptions loggen und dem Client nur eine generische Meldung liefern, während erwartete Fehler wie eine Validierungsverletzung mit konkreten, für den Client nutzbaren Details durchgereicht werden.
8. Schema-Caching und Persisted Queries für den Produktivbetrieb
Der Aufbau eines großen Schemas mit vielen Typen und Feldern kostet bei jedem Request etwas Zeit, wenn er komplett dynamisch aus Services zusammengesetzt wird. In produktiven Umgebungen lohnt sich daher ein Cache für die aufgelöste Schema-Struktur, sodass der teure Aufbau nur einmal pro Deployment statt bei jedem Request erfolgt. Symfonys eigener Cache-Component eignet sich dafür gut, weil sich die aufgebauten Type-Objekte darin unter einem festen Schlüssel ablegen lassen.
Persisted Queries sind eine zusätzliche Optimierung, bei der der Client statt der vollständigen Query nur noch einen Hash sendet, den der Server zuvor bereits gespeichert hat. Das reduziert die Request-Größe und erlaubt es, unbekannte oder nicht freigegebene Queries serverseitig abzulehnen, was besonders bei öffentlich erreichbaren GraphQL-Endpunkten die Angriffsfläche für beliebig komplexe, potenziell teure Queries deutlich verringert.
9. Wann sich der Verzicht auf API Platform tatsächlich lohnt
Der manuelle Weg mit webonyx/graphql-php lohnt sich vor allem dann, wenn bereits eine gewachsene Service- und Repository-Struktur existiert, die man nicht extra an API-Platform-Konventionen anpassen möchte, oder wenn das GraphQL-Schema bewusst nicht eins zu eins dem internen Datenmodell entsprechen soll. Auch Projekte, die ausschließlich GraphQL und kein REST anbieten, sparen sich durch den Verzicht auf API Platform die Konfiguration eines Frameworks, dessen REST-Fähigkeiten dann ohnehin ungenutzt blieben.
Umgekehrt ist API Platform die bessere Wahl, wenn REST und GraphQL parallel aus denselben Ressourcen bedient werden sollen und die automatische Ableitung von Filtern, Paginierung und OpenAPI-Dokumentation mehr Zeit spart, als sie an Flexibilität kostet. Die Entscheidung ist also weniger eine Frage von richtig oder falsch als eine Abwägung zwischen maximaler Kontrolle und maximalem Konventions-Komfort, die je nach Projektgröße und vorhandener Code-Basis unterschiedlich ausfällt.
| Aspekt | webonyx/graphql-php pur | API Platform GraphQL |
|---|---|---|
| Schema-Erstellung | manuell, volle Kontrolle | automatisch aus Ressourcen |
| Anbindung an Services | direkte Dependency Injection | über Ressourcen-Metadaten |
| REST parallel | nicht enthalten | automatisch mitgeliefert |
| Lernkurve | GraphQL-Spezifikation direkt | API-Platform-Konventionen |
| Passend für | gewachsene, individuelle Services | neue, ressourcenorientierte Projekte |
Mironsoft
Symfony-Architektur, saubere Domänenlogik und Legacy-Modernisierung
Symfony-Anwendungen, die nach zwei Jahren noch wartbar sind?
Wir prüfen bestehende Symfony-Projekte auf aufgeblähte Controller, fehlende Serviceabstraktionen und ungetestete Kernlogik und bauen daraus eine Architektur, die neue Features aufnimmt, ohne bei jedem Release brüchiger zu werden.
Architektur-Review
Bundle-Struktur, Dependency Injection und Serviceabstraktionen auf Wartbarkeit prüfen.
Legacy-Modernisierung
Schrittweise Migration veralteter Symfony-Versionen ohne kompletten Neuschrieb.
Test- und Qualitätssicherung
PHPUnit, PHPStan und CI-Pipelines für nachhaltige Codequalität aufbauen.
10. Zusammenfassung
GraphQL ohne API Platform: Das Wichtigste auf einen Blick
Bibliothek
webonyx/graphql-php als Referenzimplementierung der GraphQL-Spezifikation für PHP
Integration
Resolver als normale Symfony-Services mit Constructor Injection
Kontrolle
volle Kontrolle über Schema, unabhängig vom internen Datenmodell
Abgrenzung
kein automatischer REST-Support, dafür kein API-Platform-Overhead