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.
Inhaltsverzeichnis
- 1. Warum ein eigener Resolver kein Hexenwerk ist
- 2. Die schema.graphqls-Datei: Typen, Queries und Argumente
- 3. Das ResolverInterface implementieren
- 4. DI-Konfiguration: Resolver registrieren
- 5. Query-Argumente verarbeiten und validieren
- 6. Fehlerbehandlung im Resolver: Exceptions vs. Errors
- 7. Mutations bauen: Input, Service, Response
- 8. Integrationstests für den eigenen Resolver
- 9. Zusammenfassung
- 10. Vergleich: gute vs. problematische Resolver-Muster
- 11. FAQ
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 |