{ }
type
GraphQL · Magento · Modul-Struktur · Schema · Resolver
Magento GraphQL für eigene Module vorbereiten
sinnvolle Struktur und Dateiaufteilung

Ein eigenes Magento-Modul GraphQL-fähig zu machen klingt einfach, scheitert aber häufig an fehlender Struktur: falsch platzierte Schema-Dateien, Resolver ohne klare Delegation, di.xml-Einträge, die nie greifen. Dieser Artikel zeigt, wie die Dateiaufteilung aussehen sollte und warum Konvention hier wichtiger ist als Kreativität.

15 Min. Lesezeit etc/schema.graphqls · di.xml · ResolverInterface · Model-Delegation Magento 2.4 · PHP 8.x

1. Warum Struktur bei GraphQL-Modulen so wichtig ist

Magento lädt GraphQL-Schemata aus allen aktiven Modulen zusammen und fügt sie zu einem gemeinsamen Schema zusammen. Das bedeutet: Fehler in der eigenen schema.graphqls-Datei können das gesamte GraphQL-Endpoint lahmlegen, nicht nur das eigene Modul. Gleichzeitig sind schlecht platzierte Resolver-Klassen schwer zu testen, schwer zu debuggen und neigen dazu, Geschäftslogik zu akkumulieren, die dort nicht hingehört. Eine klare Dateiaufteilung ist deshalb keine Formalie, sondern eine direkte Investition in Wartbarkeit und Fehlervermeidung.

In der Praxis sieht man häufig, dass Entwickler das GraphQL-Schema als Anhängsel behandeln und Resolver-Klassen irgendwo im Modul ablegen, ohne einen klaren Namensraum zu verwenden. Das funktioniert anfangs, wird aber spätestens bei mehreren Queries und Mutationen zur Blackbox. Wer von Anfang an eine konventionelle Verzeichnisstruktur verwendet, die Magento selbst in seinen Core-Modulen vorlebt, hat einen deutlich einfacheren Einstieg in Debugging, Testing und Schema-Erweiterung.

2. Welche Dateien ein GraphQL-fähiges Modul braucht

Ein minimales GraphQL-fähiges Magento-Modul besteht aus drei Dateien zusätzlich zur normalen Modulstruktur: der Schema-Datei unter etc/schema.graphqls, mindestens einer Resolver-Klasse unter Model/Resolver/ und einem Eintrag in etc/di.xml, der den Resolver mit dem Schema-Typ verknüpft. Optional kommt ein DataProvider oder ein Service-Interface dazu, wenn der Resolver komplexere Datenzugriffe benötigt. Diese Trennung von Schema-Definition, Resolver-Logik und Datenzugriff ist der entscheidende Unterschied zwischen einem wartbaren Modul und einem Modul, das nach drei Monaten niemand mehr anfassen möchte.

Die Verzeichnisstruktur folgt dabei der Konvention der Magento-Core-Module: Schema-Dateien in etc/, Resolver in Model/Resolver/, Interfaces in Api/ und DataProvider in Model/ oder dediziert in Model/DataProvider/. Wer diese Trennung von Anfang an einhält, kann Resolver einzeln testen, DataProvider durch Mock-Implementierungen ersetzen und Schema-Änderungen sicher durchführen, ohne Resolver-Logik anfassen zu müssen.

3. Das Schema: etc/schema.graphqls richtig aufbauen

Die Datei etc/schema.graphqls ist der einzige Ort, an dem neue Typen, Queries und Mutationen für das eigene Modul definiert werden. Magento mergt alle Schema-Dateien aller aktiven Module beim Start zusammen. Dabei gelten strikte Regeln: Felder dürfen nicht ohne weiteres überschrieben werden, Typen müssen eindeutige Namen haben und Interfaces müssen vollständig implementiert werden. Ein häufiger Fehler ist die Definition eines Typs mit demselben Namen wie ein Core-Typ – das führt zu Merge-Konflikten, die erst beim nächsten Cache-Flush sichtbar werden.

Gut strukturierte Schemata verwenden sprechende Typnamen mit Vendor-Präfix, um Kollisionen zu vermeiden. Statt type Product zu erweitern, sollte man das @doc-Annotation-System von Magento nutzen und neue Felder über den Interface-Erweiterungsmechanismus hinzufügen. Für eigene Queries empfiehlt sich ein klarer Namensraum: mironCustomerBadge statt generischem customerData, um Konflikte mit Drittmodulen zu vermeiden.


# etc/schema.graphqls — Define a custom query with proper namespacing
type Query {
    mironCustomerBadge(customer_id: Int @doc(description: "Customer entity ID")): MironCustomerBadgeOutput
        @resolver(class: "Mironsoft\\CustomerBadge\\Model\\Resolver\\CustomerBadge")
        @doc(description: "Returns badge information for a specific customer")
        @cache(cacheIdentity: "Mironsoft\\CustomerBadge\\Model\\Resolver\\Identity\\CustomerBadgeIdentity")
}

type MironCustomerBadgeOutput @doc(description: "Output type for customer badge data") {
    badge_level: String @doc(description: "Current badge level, e.g. silver, gold, platinum")
    points: Int @doc(description: "Total accumulated reward points")
    next_level_threshold: Int @doc(description: "Points required to reach next badge level")
    expires_at: String @doc(description: "Expiration date in ISO 8601 format")
}

4. Resolver: Klassen, Interfaces und Delegation

Jede Resolver-Klasse implementiert Magento\Framework\GraphQl\Query\ResolverInterface mit der einzigen Methode resolve(). Diese Methode empfängt Argumente aus der Query, den aktuellen Kontext (Store, Kundengruppe, Authorization) und die ResolveInfo-Instanz, die beschreibt, welche Felder angefragt wurden. Der häufigste Anfängerfehler ist, in dieser Methode direkt Datenbankabfragen zu machen, Daten zu transformieren und Fehlerbehandlung zu implementieren – alles in einem. Stattdessen sollte der Resolver ausschließlich delegieren: Eingaben validieren, einen Service oder ein Repository aufrufen und das Ergebnis in das erwartete Array-Format umwandeln.

Die Resolver-Klasse sollte über Constructor Property Promotion nur die benötigten Services injizieren, nicht das gesamte Object Manager oder generische Repositories. Eine Resolver-Klasse mit mehr als 50 Zeilen ist meistens ein Zeichen dafür, dass Logik falsch platziert ist. Gute Resolver sind kurz, testbar und haben keine eigene Geschäftslogik. Die eigentliche Arbeit – Berechtigungsprüfung, Datenabruf, Transformation – gehört in einen Service, der unabhängig von GraphQL testbar ist.


# Query to test the custom resolver — send this via Altair or curl
query GetCustomerBadge {
  mironCustomerBadge(customer_id: 42) {
    badge_level
    points
    next_level_threshold
    expires_at
  }
}

# Introspection query to verify the type was registered correctly
query InspectCustomType {
  __type(name: "MironCustomerBadgeOutput") {
    name
    fields {
      name
      type {
        name
        kind
      }
    }
  }
}

5. di.xml: Resolver registrieren und Typen verbinden

In Magento 2 wird der Resolver nicht nur über den @resolver-Annotation im Schema registriert. Für komplexere Setups – etwa wenn ein Resolver eine abstrakte Klasse oder ein Interface zurückgeben soll, das zur Laufzeit aufgelöst werden muss – kommt die etc/di.xml ins Spiel. Hier werden auch Union-Type-Resolver registriert, die für Interfaces wie ProductInterface notwendig sind: Magento muss zur Laufzeit entscheiden, welcher konkrete Typ zurückgegeben wird.

Ein weiterer wichtiger di.xml-Einsatz ist die Definition von virtualType-Instanzen für DataProvider-Klassen, die leicht unterschiedliche Konfigurationen für verschiedene Resolver benötigen. Statt für jede Variante eine eigene Klasse zu schreiben, kann man über virtualType verschiedene Instanzen mit unterschiedlichen Konstruktor-Argumenten erzeugen. Diese Technik ist in Magento-Core-Modulen weit verbreitet und sollte auch in eigenen GraphQL-Modulen konsequent genutzt werden.

6. Modelle und Services nicht im Resolver vermischen

Die sauberste Architektur für Magento-GraphQL-Module trennt vier Schichten: das Schema definiert die Struktur, der Resolver nimmt die Anfrage entgegen und delegiert, ein Service implementiert die Geschäftslogik und ein Repository oder DataProvider greift auf die Datenbank zu. Diese Trennung ist nicht akademisch, sondern hat direkte praktische Konsequenzen: Wenn die Geschäftslogik im Service statt im Resolver liegt, kann sie in Unit-Tests ohne GraphQL-Kontext getestet werden. Wenn der Datenzugriff im Repository liegt, kann er gecacht und optimiert werden, ohne den Resolver anzufassen.

In der Praxis scheitert diese Trennung oft an Zeitdruck: Ein Resolver wird schnell mit einer Repository-Abhängigkeit ausgestattet und fertig. Das funktioniert für eine Query. Wenn aber drei Monate später Performance-Probleme auftauchen oder ein zweiter Einstiegspunkt – etwa eine REST-API – dieselbe Logik benötigt, steht man vor dem Problem, Logik aus dem Resolver zu extrahieren, ohne den laufenden Betrieb zu stören. Wer die Trennung von Anfang an konsequent umsetzt, spart sich genau diese Refactoring-Situation.

7. Dateistruktur im Vergleich: falsch gegen richtig

Die Wahl der Verzeichnisstruktur ist keine reine Geschmacksfrage, sondern hat direkte Auswirkungen auf die Wartbarkeit, Testbarkeit und die Erweiterbarkeit des Moduls. Die folgende Tabelle zeigt die häufigsten Abweichungen von der Magento-Konvention und welche Konsequenzen sie haben.

Aspekt Falsch / problematisch Richtig / empfohlen Konsequenz
Schema-Pfad Model/schema.graphqls etc/schema.graphqls Magento lädt Schema nur aus etc/
Resolver-Ort Block/GraphQl/Resolver.php Model/Resolver/QueryName.php Konvention, Auffindbarkeit, Testbarkeit
Logik im Resolver DB-Abfragen direkt in resolve() Delegation an Service/Repository Unit-Testbarkeit, Wiederverwendung
Typname type Product (Konflikt) type MironBadgeOutput Kein Schema-Merge-Konflikt
Fehlerbehandlung Exception direkt in resolve() GraphQlInputException / GraphQlNoSuchEntityException Korrekte HTTP-Status-Codes, strukturierte Fehler

8. Typische Fehler beim ersten GraphQL-Modul

Der häufigste Fehler beim Einstieg in Magento-GraphQL-Module ist das Vergessen des bin/magento setup:upgrade-Aufrufs nach dem Hinzufügen der Schema-Datei. Magento registriert neue Schema-Dateien erst nach einem vollständigen Setup-Upgrade und Cache-Flush. Ohne diesen Schritt ist das neue Schema schlicht nicht vorhanden, was zu kryptischen Fehlermeldungen führt, die nach Schema-Syntaxfehlern aussehen, aber eigentlich Registrierungsprobleme sind.

Ein weiterer verbreiteter Fehler betrifft den Resolver-Namespace: Wenn der in @resolver(class: "...") angegebene FQCN nicht exakt mit dem tatsächlichen Klassennamen und Dateiort übereinstimmt, wirft Magento zur Laufzeit eine Exception, die nicht immer klar auf das Problem hinweist. Besonders kritisch: Magento lädt Resolver lazy, sodass der Fehler erst bei der ersten Query-Ausführung auftaucht, nicht beim Server-Start. Das macht das Debugging zeitaufwändig, wenn man nicht weiß, wo man suchen muss.


# Minimal smoke test — paste into Altair or GraphiQL after setup:upgrade
# Verifies the endpoint responds and the custom type is registered
query SmokeTest {
  __schema {
    queryType {
      fields {
        name
        description
      }
    }
  }
}

# Check if custom query field appears in schema
query VerifyCustomField {
  __type(name: "Query") {
    fields {
      name
    }
  }
}

9. Checkliste vor dem ersten Query-Test

Bevor ein neues GraphQL-Modul das erste Mal getestet wird, sollte eine feste Checkliste abgearbeitet werden. Erstens: Ist die Schema-Datei unter etc/schema.graphqls platziert und syntaktisch korrekt? Eine schnelle Syntaxprüfung ist mit bin/magento graphql:schema oder durch Introspection möglich. Zweitens: Sind alle referenzierten Resolver-Klassen vorhanden, haben den korrekten FQCN und implementieren ResolverInterface? Drittens: Wurde bin/magento setup:upgrade ausgeführt und der Cache mit bin/magento cache:flush geleert?

Viertens: Gibt es Konflikte mit bestehenden Typnamen? Ein Blick in die Core-Schemata und die Schemata anderer installierter Module hilft, Merge-Konflikte frühzeitig zu erkennen. Fünftens: Ist die Fehlerbehandlung im Resolver sauber implementiert? Unkontrollierte PHP-Exceptions werden von Magento GraphQL in generische Fehlermeldungen umgewandelt, die wenig Debugging-Information liefern. Gezielt eingesetzte GraphQL-Exceptions wie GraphQlInputException oder GraphQlAuthorizationException geben dem Client strukturierte, verwertbare Fehlerinformationen.


# Integration test query — verify resolver returns expected structure
query CustomerBadgeIntegrationTest {
  mironCustomerBadge(customer_id: 1) {
    badge_level
    points
    next_level_threshold
    expires_at
  }
}

# Expected response shape:
# {
#   "data": {
#     "mironCustomerBadge": {
#       "badge_level": "gold",
#       "points": 1250,
#       "next_level_threshold": 2000,
#       "expires_at": "2027-01-01T00:00:00Z"
#     }
#   }
# }

10. Zusammenfassung

Ein Magento-Modul GraphQL-fähig zu machen erfordert keine komplexe Architektur, aber eine konsequente Einhaltung der Magento-Konventionen. Das Schema gehört in etc/schema.graphqls, Resolver in Model/Resolver/, Geschäftslogik in einen separaten Service. Typnamen mit Vendor-Präfix vermeiden Merge-Konflikte, und Resolver sollten so kurz wie möglich gehalten werden – delegieren statt implementieren ist die wichtigste Designregel.

Die häufigsten Probleme beim Einstieg – vergessener setup:upgrade, falsche FQCN-Angaben, Logik direkt im Resolver – lassen sich durch eine klare Checkliste und konsequente Trennung der Verantwortlichkeiten vermeiden. Wer diese Grundstruktur von Anfang an einhält, hat bei späteren Erweiterungen und Performance-Optimierungen einen deutlich einfacheren Ausgangspunkt.

Magento GraphQL Modulstruktur — Das Wichtigste auf einen Blick

Schema-Datei

Immer unter etc/schema.graphqls – Magento sucht Schema-Dateien ausschließlich dort. Typnamen mit Vendor-Präfix verwenden, um Konflikte zu vermeiden.

Resolver-Klassen

In Model/Resolver/ ablegen, implementieren ResolverInterface, delegieren ausschließlich an Services oder Repositories – keine eigene Datenbanklogik.

Deploy-Reihenfolge

setup:upgrade dann cache:flush nach jeder Schema-Änderung. Ohne diese Schritte ist das neue Schema nicht sichtbar – auch nicht für Introspection.

Fehlerbehandlung

Gezielte GraphQL-Exceptions (GraphQlInputException, GraphQlAuthorizationException) statt generischer PHP-Exceptions – gibt dem Client verwertbare Fehlermeldungen.

11. FAQ: Magento GraphQL für eigene Module vorbereiten

1Wo muss die schema.graphqls-Datei liegen?
Ausschließlich unter etc/schema.graphqls. Magento lädt Schema-Dateien nur aus diesem Pfad – Dateien an anderen Orten werden ignoriert.
2Nach jeder Schema-Änderung setup:upgrade nötig?
Ja. Schema-Änderungen werden erst nach setup:upgrade und cache:flush aktiv – ohne diese Schritte ist das neue Schema für Magento unsichtbar.
3Wie Konflikte mit Core-Typnamen vermeiden?
Vendor-Präfixe verwenden: MironBadgeOutput statt Product. Magento mergt alle Schemata und wirft bei Namenskonflikten einen Fehler.
4Darf ein Resolver direkt auf die Datenbank zugreifen?
Technisch ja, praktisch nein. Resolver sollten ausschließlich an Services oder Repositories delegieren, damit Logik testbar und wiederverwendbar bleibt.
5Unterschied @resolver vs. di.xml-Registrierung?
@resolver im Schema reicht für einfache Fälle. di.xml wird zusätzlich für Union-Types, Interface-Implementierungen und virtualType-Konfigurationen gebraucht.
6Wie prüfen, ob der Resolver registriert ist?
Introspection: __type(name: "Query") { fields { name } } listet alle registrierten Query-Felder. Fehlt das eigene Feld, stimmt FQCN oder Setup-Status nicht.
7Welche Exception-Typen soll ein Resolver verwenden?
GraphQlInputException, GraphQlNoSuchEntityException, GraphQlAuthorizationException – je nach Fehlerart. Generische PHP-Exceptions liefern dem Client kaum verwertbare Infos.
8Kann derselbe Service aus Resolver und REST-API genutzt werden?
Ja – das ist sogar der empfohlene Weg. Service-Klassen ohne GraphQL-Abhängigkeit können gleichzeitig von REST-Controllern und GraphQL-Resolvern genutzt werden.
9Was bedeutet @cache in der Schema-Datei?
Verknüpft eine Identity-Klasse mit dem Resolver, die bestimmt, welche Cache-Tags gesetzt werden. Magento kann so den Response-Cache bei relevanten Änderungen gezielt invalidieren.
10Wie Resolver debuggen, der keine Fehlermeldung zeigt?
var/log/debug.log und exception.log sind der erste Anlaufpunkt. Den Resolver isoliert in einem Integrationstest testen hilft, den HTTP-Layer als Fehlerquelle auszuschließen.