{ }
type
GraphQL · Magento · Resolver · Schema · PHP 8.4
Eigenen Magento GraphQL Resolver bauen
Query von A bis Z

Von der Schema-Definition über das ResolverInterface bis zur DI-Konfiguration, Fehlerbehandlung und Integrationstests – der vollständige Weg zu einem produktionstauglichen Magento GraphQL Resolver ohne Abkürzungen.

18 Min. Lesezeit schema.graphqls · ResolverInterface · DI · Fehlerbehandlung · Tests Magento 2.4 · PHP 8.4 · GraphQL

1. Warum ein eigener Resolver kein Hexenwerk ist

Ein eigener GraphQL-Resolver in Magento besteht aus vier Dateien: der Schema-Definition, der Resolver-Klasse, der DI-Konfiguration und einem Integrationstest. Wer diese vier Bausteine kennt, kann jeden beliebigen Datenzugriff in eine GraphQL-Query übersetzen. Das Schwierige ist nicht das Konzept, sondern die Kenntnis der Magento-internen Konventionen: wie der $value-Array aufgebaut ist, wie der Resolver-Kontext die Auth-Informationen trägt und wie man Exceptions so wirft, dass der GraphQL-Client sie als strukturierten Error erhält.

In Magento 2.4 ist das ResolverInterface in Magento\Framework\GraphQl\Query\Resolver\ResolverInterface definiert. Die einzige Methode dieses Interfaces, resolve(), erhält vier Parameter: das Field-Objekt mit Metadaten zum angeforderten Feld, den $context mit Authentifizierungsinformationen, das ResolveInfo-Objekt mit der vollständigen Query-Struktur und die optionalen $value- und $args-Arrays. Wer diese fünf Eingaben versteht, kann nahezu jeden Resolver korrekt implementieren.

2. Die schema.graphqls-Datei: Typen, Queries und Argumente

Die schema.graphqls-Datei liegt im Verzeichnis app/code/Vendor/Module/etc/. Magento sammelt alle Schema-Dateien aus allen Modulen beim ersten Request zusammen und baut daraus das vollständige Schema. Namenskollisionen zwischen Modulen führen zu Fehlern – deshalb sollten eigene Typen immer mit einem Vendor-Prefix benannt werden. Queries werden durch Erweiterung des Root-Query-Typs definiert, Mutations durch Erweiterung von Mutation.

Argumente in Queries sind typisiert und können als Pflichtargument (!) oder optional (ohne !, mit Default-Wert) deklariert werden. Input-Typen bündeln mehrere Argumente in einem strukturierten Objekt und sind für Mutations der Standard. Das @resolver-Direktiv verknüpft ein Feld im Schema mit der konkreten PHP-Resolver-Klasse – ohne diese Verknüpfung bleibt das Feld im Schema sichtbar, aber der Wert ist immer null.


# app/code/Vendor/BlogModule/etc/schema.graphqls
# Define custom types with vendor prefix to avoid collisions
type VendorBlogPost {
    id: Int!
    slug: String!
    title: String!
    content: String!
    published_at: String
    author_name: String
}

type VendorBlogPostList {
    items: [VendorBlogPost!]!
    total_count: Int!
}

input VendorBlogPostFilterInput {
    slug: FilterEqualTypeInput
    author_id: FilterEqualTypeInput
}

extend type Query {
    vendorBlogPost(slug: String!): VendorBlogPost
        @resolver(class: "Vendor\\BlogModule\\Model\\Resolver\\BlogPost")
        @doc(description: "Fetch a single blog post by slug")
    vendorBlogPosts(
        filter: VendorBlogPostFilterInput
        pageSize: Int = 20
        currentPage: Int = 1
    ): VendorBlogPostList
        @resolver(class: "Vendor\\BlogModule\\Model\\Resolver\\BlogPosts")
        @doc(description: "Fetch a paginated list of blog posts")
}

3. Das ResolverInterface implementieren

Die Resolver-Klasse ist eine final-Klasse, die ResolverInterface implementiert. In PHP 8.4 nutzt man Constructor Property Promotion für die Dependency Injection, was die Klasse deutlich kompakter macht. Der Resolver selbst enthält keine Geschäftslogik – er entnimmt die nötigen Argumente aus $args, delegiert den Datenzugriff an ein Repository oder einen Service und gibt das Ergebnis als Array zurück. Magento mappt dieses Array auf die deklarierten Felder des GraphQL-Typs.

Der Rückgabewert des Resolvers muss ein Array sein, dessen Schlüssel den Feldnamen des GraphQL-Typs entsprechen – oder null, wenn das Feld optional ist und kein Wert gefunden wurde. Für nicht-nullable Felder (mit !) muss der Resolver immer einen Wert liefern oder eine Exception werfen. Wirft er eine GraphQlNoSuchEntityException, antwortet die API mit einem strukturierten Fehler im errors-Array der GraphQL-Antwort, ohne die gesamte Response zu invalidieren.

4. DI-Konfiguration: Resolver registrieren

In Magento muss kein Resolver explizit registriert werden – die Verknüpfung erfolgt über das @resolver-Direktiv im Schema. Was in der di.xml konfiguriert werden muss, sind die Abhängigkeiten des Resolvers: Repositories, Services und andere Klassen, die Magento per Dependency Injection bereitstellt. Da der Resolver final ist, gibt es keine Vererbungsprobleme. Eine saubere di.xml enthält nur die Konfiguration, die tatsächlich von der Standardinstanziierung abweicht.


# Example query against the custom resolver
query GetBlogPost {
  vendorBlogPost(slug: "magento-graphql-best-practices") {
    id
    title
    content
    published_at
    author_name
  }
}

# Paginated list with filter
query GetBlogPosts {
  vendorBlogPosts(
    filter: { author_id: { eq: "5" } }
    pageSize: 10
    currentPage: 1
  ) {
    total_count
    items {
      slug
      title
      published_at
    }
  }
}

5. Query-Argumente verarbeiten und validieren

Argumente aus der GraphQL-Query stehen im $args-Array zur Verfügung. Magento validiert nur den deklarierten Typ (String, Int, Boolean) – semantische Validierung muss der Resolver selbst übernehmen. Ein leerer String für ein Pflichtargument wie slug ist typvalidiert aber semantisch falsch. Der Resolver sollte solche Fälle mit einer GraphQlInputException abfangen, bevor der Datenbankaufruf erfolgt. Das verhindert unnötige Datenbankzugriffe und gibt dem Client einen klar verständlichen Fehler zurück.

Für Integer-Argumente wie pageSize und currentPage empfiehlt es sich, Grenzen zu prüfen: eine pageSize von 0 oder negativen Werten führt zu undefiniertem Verhalten im Repository. Eine maximale pageSize – üblicherweise 100 oder 200 – verhindert, dass ein Client die gesamte Datenbanktabelle in einem einzigen Request abruft. Diese Grenzen sind Teil der API-Sicherheitsschicht und sollten in der Konfiguration, nicht hartkodiert im Resolver, definiert sein.

6. Fehlerbehandlung im Resolver: Exceptions vs. Errors

GraphQL unterscheidet zwischen Fehlern, die die gesamte Response ungültig machen, und solchen, die nur das betroffene Feld betreffen. In Magento wird dieses Verhalten durch den Exception-Typ gesteuert. GraphQlNoSuchEntityException und GraphQlInputException landen im errors-Array der Antwort, der Rest der Response bleibt gültig. Eine nicht gefangene PHP-Exception hingegen führt zu einem internen Serverfehler und einer leeren Response. Resolver sollten immer explizit mit Magento-spezifischen GraphQL-Exceptions arbeiten, niemals generische Exceptions unkontrolliert nach oben durchlassen.


# GraphQL error response structure — partial success is possible
{
  "data": {
    "vendorBlogPost": null
  },
  "errors": [
    {
      "message": "Blog post with slug \"not-found\" does not exist.",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["vendorBlogPost"],
      "extensions": {
        "category": "graphql-no-such-entity"
      }
    }
  ]
}

# Compare: without proper exception handling, the entire response fails
# { "errors": [{ "message": "Internal server error" }], "data": null }

7. Mutations bauen: Input, Service, Response

Mutations folgen in Magento einem festen Dreischritt-Muster: Input validieren, Service aufrufen, Response zusammenstellen. Der Input kommt als $args['input']-Array, der die Felder des deklarierten Input-Typs enthält. Der Service ist eine dedizierte PHP-Klasse, die die eigentliche Fachlogik ausführt und nichts von GraphQL weiß. Die Response ist ein Array, das auf die deklarierten Felder des Mutation-Response-Typs gemappt wird. Dieses saubere Drei-Layer-Muster macht Mutations testbar und wartbar.

Ein häufiger Fehler ist, die gesamte Mutation-Logik im Resolver zu implementieren: Datenbankzugriffe, E-Mail-Versand, Event-Dispatch – alles direkt im Resolver. Das macht den Resolver schwer testbar, eng gekoppelt und schwer wiederverwendbar. Der Resolver muss dünn bleiben: Argument aus $args entnehmen, an den Service weitergeben, das Ergebnis als Array zurückgeben. Alles, was komplexer ist, gehört in den Service.

8. Integrationstests für den eigenen Resolver

Integrationstests für Magento GraphQL Resolver verwenden die GraphQlQueryTest-Basisklasse, die echte HTTP-Requests gegen den GraphQL-Endpunkt absetzt. Ein Basistest für einen Blog-Post-Resolver prüft: Resolver gibt korrekten Wert zurück, Resolver wirft GraphQlNoSuchEntityException bei unbekanntem Slug, und die Response-Struktur entspricht dem deklarierten Schema-Typ. Für Mutations kommt ein vierter Test dazu: idempotente Wiederholung liefert dasselbe Ergebnis und erzeugt keine Duplikate in der Datenbank.


# Integration test: valid slug returns post data
query BlogPostValid {
  vendorBlogPost(slug: "test-post-fixture") {
    id
    title
    content
  }
}
# Expected: data.vendorBlogPost.title = "Test Post Fixture"

# Integration test: unknown slug returns structured error
query BlogPostNotFound {
  vendorBlogPost(slug: "does-not-exist-xyz") {
    id
    title
  }
}
# Expected: data.vendorBlogPost = null
# Expected: errors[0].extensions.category = "graphql-no-such-entity"

# Integration test: page size limit enforced
query BlogPostsOverLimit {
  vendorBlogPosts(pageSize: 999) {
    total_count
    items { slug }
  }
}
# Expected: error with category "graphql-input"

9. Zusammenfassung

Einen eigenen Magento GraphQL Resolver von A bis Z zu bauen ist ein klar strukturierter Prozess: Schema definieren, Resolver-Klasse implementieren, in der DI registrieren – oder vielmehr das @resolver-Direktiv im Schema korrekt setzen –, Argumente validieren und Exceptions sauber verwenden. Wer diesen Prozess einmal vollständig durchlaufen hat, kann jeden weiteren Resolver deutlich schneller entwickeln. Die Zeitinvestition steckt nicht in der Implementierung des Resolvers selbst, sondern in der Kenntnis der Magento-Konventionen rund um den Resolver-Kontext, den Wertauflösungsmechanismus und die Exception-Hierarchie.

Der wichtigste einzelne Ratschlag: Resolver dünn halten. Jede Logik, die über das Entnehmen von Argumenten, das Aufrufen eines Services und das Zurückgeben eines Arrays hinausgeht, gehört nicht in den Resolver. Ein dünner Resolver ist testbar, wartbar und verständlich – auch für Entwickler, die den Resolver später warten müssen, ohne die Entstehungsgeschichte zu kennen.

Eigenen Magento GraphQL Resolver bauen — Das Wichtigste auf einen Blick

Schema

Typen mit Vendor-Prefix, @resolver-Direktiv für jedes Feld, Input-Typen für Mutations. Schema-Datei in etc/schema.graphqls.

Resolver

final-Klasse, ResolverInterface, Constructor Property Promotion, keine Logik im Resolver – nur Delegation an Services.

Fehlerbehandlung

Magento-spezifische GraphQL-Exceptions verwenden. GraphQlNoSuchEntityException und GraphQlInputException landen im errors-Array, nicht als 500.

Testing

GraphQlQueryTest-Basisklasse für echte HTTP-Integrationstests. Valide Daten, fehlende Entities und ungültige Argumente jeweils separat testen.

10. Vergleich: gute vs. problematische Resolver-Muster

Der Unterschied zwischen einem produktionstauglichen und einem fragilen Resolver liegt meist nicht in der Funktionalität, sondern in der Struktur. Beide lösen das Problem – aber einer ist wartbar, testbar und sicher, der andere wird zur Blackbox in der Produktion.

Kriterium Problematisch Produktionstauglich Auswirkung
Logik im Resolver SQL direkt im resolve() Delegation an Repository Testbarkeit, Wiederverwendbarkeit
Exception-Handling Generische Exception unbehandelt GraphQlNoSuchEntityException Strukturierter Fehler statt 500
Argument-Validierung Argumente direkt an DB weitergeben GraphQlInputException bei Regelverletzung Sicherheit, klare Fehlermeldungen
Klassen-Design Extends einer anderen Resolver-Klasse final + ResolverInterface Keine unerwarteten Plugin-Eingriffe
Tests Kein Test GraphQlQueryTest mit 3+ Szenarien Regressionssicherheit bei Schema-Änderungen

11. FAQ: Eigenen Magento GraphQL Resolver bauen

1Muss ich den Resolver in di.xml registrieren?
Nein. Die Verknüpfung erfolgt über das @resolver-Direktiv im Schema. In der di.xml werden nur abweichende Abhängigkeiten konfiguriert.
2Was enthält der $value-Array?
Daten des Parent-Objekts. Bei Root-Queries (extend type Query) ist $value null. Bei verschachtelten Feldern enthält er das aufgelöste Parent-Objekt.
3Warum den Resolver final deklarieren?
Verhindert unerwartete Vererbung. Plugins müssen explizit über di.xml konfiguriert werden. Macht die Klasse berechenbarer und schützt vor Override-Effekten.
4Exception bei nicht gefundenem Objekt?
GraphQlNoSuchEntityException. Landet im errors-Array ohne die gesamte Response zu invalidieren. Der Rest der Antwort bleibt gültig.
5Unbegrenzte Datensätze verhindern?
Maximale pageSize in der Argument-Validierung erzwingen, bei Überschreitung GraphQlInputException werfen. Konfigurierbar, nicht hartkodiert.
6Kann ein Resolver mehrere Felder auflösen?
Nein. Jedes Feld hat seinen eigenen Resolver. Für N+1-Vermeidung den DataLoader-Ansatz (Batch-Loader) verwenden.
7Resolver testen ohne Datenbank?
Services mit gemockten Repositories im Unit-Test testen. Der Resolver selbst ist so dünn, dass er keinen Unit-Test braucht – Integrationstests übernehmen das.
8Unterschied @resolver und @doc?
@resolver verknüpft ein Feld mit der PHP-Klasse und ist Pflicht. @doc ist eine optionale Beschreibung für Introspection und Dokumentationstools.
9Liste als Resolver-Rückgabe?
Als Array von Arrays. Jedes innere Array repräsentiert ein Objekt und muss die deklarierten Felder als Schlüssel enthalten. Magento mappt automatisch.
10null bei Non-nullable Feld zurückgeben?
Führt zu einem internen Fehler im errors-Array. Entweder Exception werfen oder das Feld im Schema als nullable (ohne !) deklarieren.