graphql-php (Webonyx) Grundlagen: Schema-First vs. Code-First
AI generated
{ }
type
GraphQL · PHP · API-Architektur
graphql-php (Webonyx) Grundlagen
Schema-First gegen Code-First im Vergleich

graphql-php von Webonyx ist die PHP-Referenzimplementierung der GraphQL-Spezifikation und die Grundlage praktisch jeder größeren PHP-GraphQL-Lösung, von Lighthouse bis zu eigenen Standalone-APIs. Wer graphql-php direkt nutzt, muss früh entscheiden: Schema per SDL-Datei definieren oder Typen als PHP-Klassen im Code beschreiben. Diese Entscheidung prägt Wartbarkeit, IDE-Unterstützung und Teamworkflow für die gesamte Projektlaufzeit.

19 Min. Lesezeit graphql-php · Webonyx · Schema-First · Code-First PHP 8.x · DataLoader · PHPUnit

1. Was ist graphql-php und wofür eignet es sich

graphql-php ist die von Webonyx entwickelte, spezifikationstreue PHP-Portierung der offiziellen GraphQL.js-Referenzimplementierung. Anders als High-Level-Frameworks wie Lighthouse oder API Platform liefert graphql-php keine fertigen Konventionen für Eloquent-Modelle oder Doctrine-Entities mit, sondern die reinen Bausteine: Type-System, Schema-Validierung, Query-Ausführung und Introspection. Genau diese Nüchternheit macht die Bibliothek zur bevorzugten Grundlage, wenn ein Team volle Kontrolle über Resolver-Verhalten, Caching und Fehlerbehandlung braucht, ohne die Abstraktionen eines größeren Frameworks mitzuschleppen.

In der Praxis begegnet man graphql-php häufig indirekt, als Unterbau von Lighthouse in Laravel oder von API Platforms GraphQL-Modul in Symfony. Wer direkt mit graphql-php arbeitet, tut das meist aus einem von zwei Gründen: entweder existiert kein passendes Framework für den jeweiligen Anwendungsfall, etwa bei einem schlanken Microservice ohne volles Symfony- oder Laravel-Ökosystem, oder das Team braucht Low-Level-Kontrolle, die High-Level-Frameworks bewusst verstecken. Beide Szenarien führen zur ersten grundlegenden Entscheidung: Schema-First oder Code-First.

2. Schema-First: SDL-Dateien und Type-Definitions

Beim Schema-First-Ansatz beschreibt eine .graphqls-Datei in der GraphQL Schema Definition Language (SDL) sämtliche Typen, Felder und Beziehungen. graphql-php parst diese Datei zur Laufzeit mit dem BuildSchema-Helper und erzeugt daraus ein ausführbares Schema-Objekt. Der große Vorteil: Die SDL-Datei ist für jeden lesbar, auch für Frontend-Entwickler ohne PHP-Kenntnisse, und lässt sich leicht mit dem tatsächlich ausgelieferten Schema per Introspection abgleichen. Viele Teams pflegen die SDL-Datei sogar als eigenständiges, versioniertes Vertragsdokument zwischen Frontend und Backend.

Der Nachteil zeigt sich bei wachsender Größe: Reine SDL-Dateien bieten keine Typsicherheit auf PHP-Seite, Tippfehler in Feldnamen fallen erst zur Laufzeit auf, nicht beim Schreiben. Resolver werden separat als PHP-Callables definiert und über einen Type-Config-Decorator mit den SDL-Typen verknüpft, was eine zusätzliche Indirektionsebene erzeugt. Für kleinere bis mittlere Schemas mit stabiler Struktur ist Schema-First trotzdem oft die pragmatischere Wahl, weil die SDL-Datei als lebende Dokumentation fungiert.


# schema.graphqls — Schema-First definition for graphql-php
type Product {
  id: ID!
  sku: String!
  name: String!
  price: Float!
  categories: [Category!]!
}

type Category {
  id: ID!
  name: String!
  products: [Product!]!
}

type Query {
  product(sku: String!): Product
  categories: [Category!]!
}

<?php
declare(strict_types=1);

use GraphQL\GraphQL;
use GraphQL\Type\Schema;
use GraphQL\Utils\BuildSchema;
use GraphQL\Utils\SchemaExtender;

// Load SDL and attach resolvers via a type config decorator
$sdl = file_get_contents(__DIR__ . '/schema.graphqls');
$schema = BuildSchema::build($sdl, function (array $typeConfig) use ($resolvers): array {
    $typeName = $typeConfig['name'];

    if ($typeName === 'Query') {
        $typeConfig['resolveField'] = static fn ($root, array $args): mixed =>
            $resolvers[$typeConfig['name']][$args['fieldName'] ?? 'product']($root, $args);
    }

    return $typeConfig;
});

3. Code-First: PHP-Klassen als Typen

Beim Code-First-Ansatz existiert kein separates SDL-Dokument. Stattdessen werden GraphQL-Typen als PHP-Klassen definiert, die von ObjectType erben oder Objekte davon instanziieren, mit Feldern als PHP-Arrays inklusive Typangaben, Argumenten und direkt eingebetteten Resolvern. Der entscheidende Vorteil: PHP-IDEs wie PhpStorm bieten vollständige Autovervollständigung, Refactoring-Unterstützung und statische Analyse, weil das Schema tatsächlich PHP-Code ist, den PHPStan und Psalm verstehen. Tippfehler in Feldnamen werden potenziell schon durch statische Analyse gefunden, nicht erst durch einen fehlgeschlagenen Request.

Der Nachteil liegt in der Verteilung: Bei großen Schemas verteilen sich Typdefinitionen über viele Dateien, und ohne zusätzliches Tooling gibt es keine kompakte, für Nicht-PHP-Entwickler lesbare Übersicht. graphql-php bietet dafür SchemaPrinter::doPrint(), das aus dem Code-First-Schema automatisch eine SDL-Repräsentation exportiert, die dann zum Beispiel im Frontend-Repository als Referenzdokument abgelegt werden kann. Damit lässt sich der Dokumentationsnachteil von Code-First weitgehend kompensieren.


<?php
declare(strict_types=1);

namespace App\GraphQL\Type;

use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;

final class ProductType extends ObjectType
{
    public function __construct(CategoryType $categoryType)
    {
        parent::__construct([
            'name' => 'Product',
            'fields' => [
                'id' => Type::nonNull(Type::id()),
                'sku' => Type::nonNull(Type::string()),
                'name' => Type::nonNull(Type::string()),
                'price' => Type::nonNull(Type::float()),
                'categories' => [
                    'type' => Type::nonNull(Type::listOf(Type::nonNull($categoryType))),
                    'resolve' => static fn (array $product): array =>
                        $product['categories'] ?? [],
                ],
            ],
        ]);
    }
}

4. Resolver-Funktionen in graphql-php

Unabhängig vom gewählten Ansatz sind Resolver in graphql-php gewöhnliche PHP-Callables mit vier Parametern: dem Root-Wert des übergeordneten Typs, den Feld-Argumenten, dem Context-Objekt, das Request-übergreifende Abhängigkeiten wie den aktuellen Nutzer oder Repository-Instanzen transportiert, und der ResolveInfo, die Metadaten über das gerade angefragte Feld enthält, etwa welche Unterfelder selektiert wurden. Diese Signatur gilt sowohl für Schema-First-Type-Config-Decorators als auch für Code-First-Klassen.

Ein häufiges Missverständnis: Ohne expliziten resolve-Callback greift graphql-php auf einen Default-Resolver zurück, der einfach die gleichnamige Property oder Methode des Root-Werts liest. Das funktioniert gut für einfache Felder, führt aber bei komplexeren Beziehungen, etwa dem Nachladen verknüpfter Kategorien aus der Datenbank, schnell zu N+1-Problemen, wenn jeder Resolver naiv seine eigene Datenbankabfrage ausführt, statt Batching zu nutzen.

5. Fehlerbehandlung und Validierung

graphql-php unterscheidet strikt zwischen zwei Fehlerarten. Syntax- und Validierungsfehler, etwa eine unbekannte Query-Feldreferenz, werden automatisch von der Bibliothek erkannt und als GraphQL-Errors im standardisierten errors-Array der Response zurückgegeben, bevor überhaupt ein Resolver aufgerufen wird. Fachliche Fehler, etwa ein nicht gefundenes Produkt, müssen Entwickler selbst werfen, typischerweise als GraphQL\Error\Error-Instanz mit einer für Clients verständlichen Nachricht.

Für strukturierte Fehlercodes, die Frontend-Clients maschinell auswerten können, bietet graphql-php das extensions-Feld der Fehlerobjekte. Statt Fehlercodes in die Nachricht selbst zu kodieren, wie "PRODUCT_NOT_FOUND: SKU 42 unknown", gehört der Code strukturiert in extensions.code, während die Nachricht menschenlesbar bleibt. Ein zentraler errorFormatter, der beim Schema-Aufbau registriert wird, sorgt dafür, dass diese Struktur konsistent für das gesamte Schema angewendet wird, statt in jedem Resolver einzeln formatiert zu werden.


<?php
declare(strict_types=1);

use GraphQL\Error\Error;
use GraphQL\Error\DebugFlag;
use GraphQL\Error\FormattedError;

// Throw a business error with a structured extensions code
final class ProductResolver
{
    public function resolveProduct(array $root, array $args): array
    {
        $product = $this->repository->findBySku($args['sku']);

        if ($product === null) {
            throw new Error(
                message: "Product with SKU {$args['sku']} was not found",
                extensions: ['code' => 'PRODUCT_NOT_FOUND', 'sku' => $args['sku']],
            );
        }

        return $product;
    }
}

// Custom error formatter — applied once for the whole schema
$errorFormatter = static function (Error $error): array {
    $formatted = FormattedError::createFromException($error);
    $formatted['extensions']['code'] ??= 'INTERNAL_ERROR';
    return $formatted;
};

6. Performance: DataLoader gegen N+1-Probleme

Das N+1-Problem ist bei graphql-php genauso präsent wie bei jeder anderen GraphQL-Implementierung: Eine Liste von 50 Produkten mit jeweils einem verschachtelten Kategorie-Feld löst ohne Batching 50 einzelne Datenbankabfragen aus, eine pro Produkt, statt einer einzigen gesammelten Abfrage. Die Lösung ist das DataLoader-Pattern, das ursprünglich von Facebook für JavaScript entwickelt wurde und für PHP in Paketen wie overblog/dataloader-php verfügbar ist.

Ein DataLoader sammelt innerhalb eines einzelnen Event-Loop-Ticks alle angefragten IDs, bevor er sie in einer einzigen Batch-Abfrage auflöst und die Ergebnisse den ursprünglichen, individuellen Promises zuordnet. In graphql-php lässt sich das gut mit der SyncPromiseAdapter kombinieren, die auch ohne echte asynchrone Runtime wie ReactPHP funktioniert, weil sie das Batching synchron innerhalb der Query-Ausführung koordiniert. Für produktive PHP-GraphQL-APIs mit relevanter Nutzerzahl ist DataLoader kein optionales Extra, sondern eine Grundvoraussetzung für akzeptable Antwortzeiten.


<?php
declare(strict_types=1);

use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
use Overblog\DataLoader\DataLoader;

// Batch-load categories by product ID instead of one query per product
$categoryLoader = new DataLoader(
    static function (array $productIds) use ($categoryRepository): array {
        $categoriesByProduct = $categoryRepository->findByProductIds($productIds);
        return array_map(
            static fn (int $id): array => $categoriesByProduct[$id] ?? [],
            $productIds
        );
    },
    new SyncPromiseAdapter()
);

// Inside the resolve callback — accumulates, then resolves in one batch
'resolve' => static function (array $product) use ($categoryLoader) {
    return $categoryLoader->load($product['id']);
},

7. Integration in Symfony, Laravel und Standalone-Projekte

In Symfony lässt sich graphql-php entweder direkt als Service über overblog/graphql-bundle einbinden, das eigene Schema-Definitions-Formate und Symfony-typische Dependency Injection mitbringt, oder man baut den Endpunkt komplett selbst als einfachen Controller, der einen HTTP-Request entgegennimmt und an GraphQL::executeQuery() weiterreicht. Für Laravel ist der direkte Einsatz seltener, weil Lighthouse, das intern ebenfalls auf graphql-php aufbaut, bereits die meisten Integrationsaufgaben löst.

Für Standalone-Projekte ohne Framework genügt ein schlanker index.php-Einstiegspunkt, der den Request-Body parst, das Schema lädt und die Query ausführt. Diese Minimallösung eignet sich besonders für Microservices, bei denen eine volle Framework-Installation unnötigen Overhead bedeuten würde, und zeigt, wie unabhängig graphql-php vom umgebenden Ökosystem tatsächlich ist.

8. Testing von graphql-php-Schemas

Für Tests bietet graphql-php keine spezielle Testinfrastruktur, sondern setzt auf normale PHPUnit-Tests, die GraphQL::executeQuery() direkt mit einer Test-Query und Testdaten aufrufen und die zurückgegebene Datenstruktur assertieren. Das ist deutlich schneller als End-to-End-Tests über HTTP, weil kein Webserver und keine echte Netzwerkschicht involviert sind, das Schema läuft direkt im PHPUnit-Prozess.

Sinnvoll ist eine Testpyramide mit drei Ebenen: Unit-Tests für einzelne Resolver-Funktionen mit gemockten Repositories, Integrationstests, die das komplette Schema mit einer Test-Datenbank ausführen, und wenige End-to-End-Tests über den echten HTTP-Endpunkt, die vor allem Middleware, Authentifizierung und Fehlerformatierung abdecken. Snapshot-Tests, die die komplette JSON-Response gegen eine gespeicherte Referenzdatei vergleichen, helfen zusätzlich, unbeabsichtigte Schema-Änderungen frühzeitig zu erkennen.

9. Schema-First vs. Code-First im direkten Vergleich

Beide Ansätze produzieren am Ende ein funktional gleichwertiges GraphQL-Schema, die Entscheidung ist also keine Frage von Möglichkeiten, sondern von Teamgröße, Toolchain und Präferenzen bei der Fehlererkennung.

Kriterium Schema-First (SDL) Code-First (PHP-Klassen)
Lesbarkeit für Frontend-Teams Hoch, SDL direkt lesbar Nur über generierten SDL-Export
IDE-Unterstützung Eingeschränkt, kein Typ-Check der SDL Vollständig, PHPStan/Psalm-fähig
Fehlererkennung Zur Laufzeit Zum Teil schon statisch
Skalierung bei großen Schemas Eine wachsende Datei Natürlich in Klassen aufgeteilt
Vertragsdokument mit Frontend Direkt nutzbar SchemaPrinter-Export nötig

Für Teams mit klarer Frontend/Backend-Trennung und stabilem, mittelgroßem Schema ist Schema-First oft die schnellere Einstiegslösung. Für Teams, die stark auf statische Analyse setzen und ein sehr großes, häufig wachsendes Schema pflegen, ist Code-First die wartbarere Wahl. Manche Projekte kombinieren beides: Schema-First für stabile Kernbereiche, Code-First für sich schnell entwickelnde neue Module.

Mironsoft

PHP-GraphQL-APIs mit graphql-php, Symfony und Magento

Eigene GraphQL-API mit graphql-php planen?

Wir konzipieren Schema-Architektur, Resolver-Struktur und DataLoader-Strategie für eure PHP-GraphQL-API, wählen den passenden Ansatz zwischen Schema-First und Code-First und sorgen für saubere Fehlerbehandlung von Anfang an.

Schema-Architektur

Schema-First oder Code-First passend zu Teamgröße und Toolchain wählen

Performance-Tuning

DataLoader-Batching gegen N+1-Probleme in Resolvern implementieren

Framework-Integration

graphql-php sauber in Symfony, Laravel oder Standalone-Projekte einbinden

10. Zusammenfassung

graphql-php von Webonyx ist die spezifikationstreue Grundlage praktisch jeder ernstzunehmenden PHP-GraphQL-Lösung, ob direkt genutzt oder als Unterbau von Lighthouse und API Platform. Schema-First mit SDL-Dateien punktet mit Lesbarkeit und einem direkt nutzbaren Vertragsdokument für Frontend-Teams, Code-First mit PHP-Klassen punktet mit voller IDE-Unterstützung und statischer Analyse. Beide Ansätze führen zu einem funktional identischen, ausführbaren Schema.

Unabhängig vom gewählten Ansatz sind Resolver-Design, strukturierte Fehlerbehandlung über das extensions-Feld und DataLoader-basiertes Batching gegen N+1-Probleme die drei Bausteine, die über die tatsächliche Produktionstauglichkeit einer mit graphql-php gebauten API entscheiden. Wer diese Grundlagen beherrscht, kann souverän zwischen den High-Level-Frameworks wählen oder ganz bewusst direkt auf graphql-php setzen.

graphql-php (Webonyx) — Das Wichtigste auf einen Blick

Schema-First

SDL-Datei plus BuildSchema::build(). Lesbar für alle, Resolver über Type-Config-Decorator angebunden.

Code-First

PHP-Klassen mit ObjectType. Volle IDE- und Statik-Unterstützung, Export via SchemaPrinter möglich.

Fehlerbehandlung

Fachfehler als GraphQL\Error\Error mit strukturiertem extensions.code, zentraler errorFormatter.

Performance

DataLoader-Pattern mit SyncPromiseAdapter für Batching, verhindert N+1-Datenbankabfragen.

11. FAQ: graphql-php (Webonyx) Grundlagen

1Was ist graphql-php?
Die von Webonyx entwickelte PHP-Portierung der GraphQL-Referenzimplementierung, mit Type-System, Validierung und Query-Ausführung, ohne Framework-Vorgaben.
2Schema-First vs. Code-First?
Schema-First nutzt separate SDL-Dateien, Code-First PHP-Klassen mit voller IDE-Unterstützung. Beide erzeugen ein funktional identisches Schema.
3Baut Lighthouse auf graphql-php auf?
Ja, Lighthouse nutzt graphql-php intern und ergänzt es um SDL-Direktiven wie @hasMany für die direkte Eloquent-Integration.
4Wie N+1-Probleme vermeiden?
Mit dem DataLoader-Pattern: IDs werden gesammelt und in einer Batch-Abfrage aufgelöst statt in einzelnen Abfragen pro Resolver.
5Syntaxfehler vs. Fachfehler?
Syntax- und Validierungsfehler erkennt graphql-php automatisch. Fachfehler wirft man explizit als GraphQL\Error\Error mit Code im extensions-Feld.
6Braucht man ReactPHP?
Nein, die SyncPromiseAdapter macht Batching auch synchron innerhalb eines normalen PHP-FPM-Requests möglich.
7Code-First als SDL exportieren?
Mit SchemaPrinter::doPrint($schema), das erzeugt eine lesbare SDL-Datei als Referenzdokument fürs Frontend.
8Wie testet man ein Schema?
Mit PHPUnit-Tests, die GraphQL::executeQuery() direkt aufrufen. Pyramide aus Unit-, Integrations- und wenigen End-to-End-Tests empfohlen.
9Ohne Framework nutzbar?
Ja, ein schlanker index.php-Einstiegspunkt reicht, ideal für Microservices ohne volle Framework-Installation.
10Kann man beide Ansätze mischen?
Ja, ein gängiges Muster: stabile Kernbereiche Schema-First, schnell wachsende Module Code-First für bessere Refactoring-Unterstützung.