von schema.graphqls bis zur produktionsreifen Resolver-Klasse
Eine eigene GraphQL Mutation in Magento 2 ist mehr als ein neuer Endpunkt: sie verbindet Schema-Deklaration, Resolver-Logik, Validierung, Transaktionssicherheit und Cache-Invalidierung zu einem konsistenten Ganzen. Wer diese Bausteine sauber trennt, bekommt eine Mutation, die sich wie ein natives Magento-Feature anfühlt und in produktiven Storefronts zuverlässig läuft.
Inhaltsverzeichnis
- 1. Warum eigene GraphQL Mutations nötig sind
- 2. Mutation im schema.graphqls deklarieren
- 3. Die Resolver-Klasse implementieren
- 4. Input-Validierung und GraphQL-Exceptions
- 5. Modul-Setup und DI-Konfiguration
- 6. Transaktionssicherheit und Datenkonsistenz
- 7. Cache-Invalidierung nach der Mutation
- 8. Eigene Mutations mit Integrationstests absichern
- 9. Native vs. eigene Mutations im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum eigene GraphQL Mutations nötig sind
Magento liefert bereits eine große Anzahl nativer GraphQL Mutations mit, von addProductsToCart bis createCustomer. Sobald ein Projekt jedoch fachliche Vorgänge abbildet, die über den Standard-Checkout hinausgehen, wie ein Treuepunkte-System, eine individuelle Freigabelogik im B2B-Kontext oder eine externe Zahlungsbestätigung, reicht das native Schema nicht mehr aus. Genau hier kommt die eigene GraphQL Mutation ins Spiel: sie erweitert das Schema um einen fachlich passenden Endpunkt, statt fremde Funktionalität in bestehende Mutations zu pressen.
Der zentrale Vorteil einer eigenen GraphQL Mutation gegenüber einem separaten REST-Endpunkt liegt in der Konsistenz der Storefront-API. Frontend-Teams arbeiten ohnehin mit einem GraphQL-Client, Caching-Header und Query-Batching sind bereits etabliert, und die Mutation fügt sich nahtlos in bestehende Fehlerbehandlung und Typisierung ein. Wer stattdessen einen zusätzlichen REST-Controller baut, muss Authentifizierung, Rate-Limiting und Antwortformate doppelt pflegen. Eine eigene GraphQL Mutation vermeidet diese Redundanz und bleibt im selben API-Vertrag wie der Rest der Storefront.
Bevor man mit der Implementierung beginnt, lohnt sich eine kurze Bestandsaufnahme: Welche Daten verändert die Mutation, welche Berechtigungen sind nötig, und muss die Änderung synchron oder asynchron verarbeitet werden. Diese Fragen bestimmen, wie viel Aufwand in Validierung, Transaktionslogik und Fehlerbehandlung fließt, bevor die erste Zeile Resolver-Code entsteht.
2. Mutation im schema.graphqls deklarieren
Jede GraphQL Mutation in Magento beginnt mit einer Deklaration in einer schema.graphqls-Datei im eigenen Modul. Diese Datei erweitert den bestehenden Mutation-Typ um ein neues Feld, definiert Eingabeparameter über einen Input-Typ und legt den Rückgabetyp über einen Output-Typ fest. Wichtig ist die @resolver-Direktive: sie verbindet das Schema-Feld mit der PHP-Klasse, die später die eigentliche Logik ausführt. Ohne diese Direktive weiß Magento nicht, welcher Code beim Aufruf der Mutation läuft.
Ein häufiger Anfängerfehler ist es, primitive Skalartypen wie String für strukturierte Daten zu verwenden. Für eine eigene GraphQL Mutation, die zum Beispiel eine Bewertung anlegt, sollte der Input-Typ explizite Felder für rating, title und text mit passenden Skalartypen (Int, String) enthalten, statt alles in einem JSON-String zu bündeln. Das nutzt die eingebaute GraphQL-Validierung und macht Introspection-Tools wie GraphQL Playground aussagekräftig nutzbar.
# app/code/Mironsoft/CustomerReview/etc/schema.graphqls
# Custom GraphQL mutation for submitting a product review
type Mutation {
createProductReview(input: CreateProductReviewInput!): CreateProductReviewOutput
@resolver(class: "Mironsoft\\CustomerReview\\Model\\Resolver\\CreateProductReview")
@doc(description: "Create a new review for a product")
}
input CreateProductReviewInput {
sku: String! @doc(description: "SKU of the reviewed product")
rating: Int! @doc(description: "Rating between 1 and 5")
title: String! @doc(description: "Short review headline")
text: String! @doc(description: "Full review text")
}
type CreateProductReviewOutput {
review_id: Int! @doc(description: "ID of the created review")
status: String! @doc(description: "pending or approved")
}
3. Die Resolver-Klasse implementieren
Die eigentliche Logik jeder eigenen GraphQL Mutation steckt in einer Klasse, die Magento\Framework\GraphQl\Query\ResolverInterface implementiert. Die zentrale Methode resolve() bekommt vier Parameter: das Feld-Objekt, den Kontext (inklusive Kundendaten und Store), Informationen zur Query und ein Array mit den Argumenten aus dem Input-Typ. Der Rückgabewert muss exakt der Struktur des im Schema deklarierten Output-Typs entsprechen, sonst meldet Magento einen Schema-Validierungsfehler zur Laufzeit.
Eine gut strukturierte Resolver-Klasse delegiert die eigentliche Geschäftslogik an einen Service oder ein Repository, statt sie direkt im Resolver zu implementieren. Der Resolver selbst übernimmt nur drei Aufgaben: Argumente aus dem GraphQL-Request extrahieren, Berechtigungen über den ContextInterface prüfen und das Ergebnis des Service-Aufrufs in die vom Schema geforderte Array-Struktur überführen. Diese Trennung macht die Mutation testbar, weil der Service unabhängig vom GraphQL-Layer geprüft werden kann.
<?php
declare(strict_types=1);
namespace Mironsoft\CustomerReview\Model\Resolver;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Mironsoft\CustomerReview\Api\ReviewCreatorInterface;
/**
* Resolver for the createProductReview custom GraphQL mutation.
*/
final class CreateProductReview implements ResolverInterface
{
/**
* @param ReviewCreatorInterface $reviewCreator Service that encapsulates the review creation logic
*/
public function __construct(
private readonly ReviewCreatorInterface $reviewCreator
) {
}
/**
* Resolve the createProductReview mutation.
*
* @param Field $field
* @param ContextInterface $context
* @param ResolveInfo $info
* @param array|null $value
* @param array|null $args
* @return array<string, mixed>
* @throws GraphQlAuthorizationException
* @throws GraphQlInputException
*/
public function resolve(
Field $field,
$context,
ResolveInfo $info,
array $value = null,
array $args = null
): array {
if (false === $context->getExtensionAttributes()->getIsCustomer()) {
throw new GraphQlAuthorizationException(__('Only logged-in customers can submit reviews.'));
}
$input = $args['input'] ?? [];
if (empty($input['sku']) || empty($input['rating'])) {
throw new GraphQlInputException(__('SKU and rating are required.'));
}
$review = $this->reviewCreator->create(
(string) $input['sku'],
(int) $context->getUserId(),
(int) $input['rating'],
(string) $input['title'],
(string) $input['text']
);
return [
'review_id' => $review->getId(),
'status' => $review->getStatus(),
];
}
}
4. Input-Validierung und GraphQL-Exceptions
Validierung in einer eigenen GraphQL Mutation passiert auf zwei Ebenen. Die erste Ebene ist die Typvalidierung durch das Schema selbst: ein Pflichtfeld ohne ! lässt Magento den Request schon vor dem Resolver-Aufruf ablehnen. Die zweite Ebene ist die fachliche Validierung im Resolver oder im dahinterliegenden Service, etwa ob eine Bewertung zwischen 1 und 5 liegt oder ob das referenzierte Produkt tatsächlich existiert. Für diese Ebene stellt Magento spezielle Exception-Klassen bereit, die GraphQL-konforme Fehlerantworten erzeugen.
GraphQlInputException signalisiert einen ungültigen Client-Input und wird als Fehler mit Code GRAPHQL_INPUT_ERROR in der Antwort ausgegeben, ohne den gesamten Request abzubrechen. GraphQlAuthorizationException kommuniziert fehlende Berechtigungen, GraphQlNoSuchEntityException wird verwendet, wenn eine referenzierte Entität nicht existiert. Wichtig für jede Mutation: generische \Exception-Objekte sollten niemals direkt aus dem Resolver geworfen werden, weil sie im Client als undifferenzierter Internal server error ankommen und keine Aussage über die Fehlerursache zulassen.
5. Modul-Setup und DI-Konfiguration
Damit eine eigene GraphQL Mutation überhaupt geladen wird, braucht das Modul eine vollständige Registrierung: registration.php, module.xml mit Abhängigkeit zu Magento_GraphQl, sowie die schema.graphqls-Datei im etc-Verzeichnis. Die Resolver-Klasse selbst benötigt in der Regel keinen eigenen di.xml-Eintrag, weil sie über die @resolver-Direktive im Schema referenziert und vom Object Manager automatisch instanziiert wird. Anders sieht es aus, wenn die Resolver-Klasse Abhängigkeiten hat, die per Interface injiziert werden: hier definiert di.xml die konkrete Implementierung für das Interface.
Ein Modul mit mehreren Mutations profitiert von einer klaren Namensraum-Konvention, etwa ein Resolver-Namespace mit je einer Klasse pro Mutation, und einem separaten Service-Namespace für die eigentliche Geschäftslogik. Diese Struktur verhindert, dass GraphQL-spezifischer Code, wie Exception-Handling und Kontextprüfung, sich mit fachlicher Logik vermischt, die auch von einem REST-Endpunkt oder einem Cron-Job wiederverwendet werden könnte.
<!-- app/code/Mironsoft/CustomerReview/etc/di.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="Mironsoft\CustomerReview\Api\ReviewCreatorInterface"
type="Mironsoft\CustomerReview\Model\ReviewCreator" />
</config>
6. Transaktionssicherheit und Datenkonsistenz
Sobald eine GraphQL Mutation mehr als eine Datenbank-Operation ausführt, etwa das Anlegen einer Bewertung und gleichzeitig das Aktualisieren eines Produkt-Durchschnittswerts, muss diese Operation als Transaktion laufen. Ohne explizite Transaktion kann ein Fehler zwischen den beiden Schreiboperationen inkonsistente Daten hinterlassen: die Bewertung existiert, der Durchschnittswert wurde aber nicht aktualisiert. Der Service hinter der Resolver-Klasse sollte deshalb den Ressourcen-Connection-Layer nutzen, um beide Operationen in einer Transaktion zu bündeln.
Ein zweiter wichtiger Punkt betrifft Idempotenz: GraphQL-Clients wiederholen Requests bei Netzwerkfehlern gelegentlich automatisch. Eine Mutation, die beim zweiten Aufruf mit identischen Parametern einen Duplikateintrag anlegt, produziert im Frontend doppelte Bewertungen oder doppelte Bestellungen. Eine defensive Implementierung prüft deshalb vor dem Insert, ob bereits ein passender Datensatz existiert, oder nutzt einen client-seitig generierten Idempotency-Key, der als eindeutiger Index in der Datenbank abgebildet wird.
7. Cache-Invalidierung nach der Mutation
Magentos GraphQL-Layer nutzt für Queries denselben Full-Page-Cache-Mechanismus wie die klassische Storefront, inklusive Cache-Tags. Eine eigene GraphQL Mutation, die Produktdaten verändert, etwa den Durchschnitts-Rating-Wert eines Produkts, muss deshalb die passenden Cache-Tags invalidieren, sonst liefern nachfolgende Queries veraltete Daten aus dem Cache aus. Der Resolver oder der dahinterliegende Service ruft dafür den CacheInterface beziehungsweise den entsprechenden Indexer-Trigger auf, je nachdem ob synchrone oder asynchrone Invalidierung gewünscht ist.
Für Produktbezogene Mutations ist der Tag meist cat_p_{entity_id}, für kategoriebezogene Änderungen cat_c_{entity_id}. Wer die Invalidierung vergisst, bekommt ein subtiles Bug-Muster: die Mutation meldet Erfolg, das Frontend zeigt aber für einige Minuten den alten Zustand, bis der Full-Page-Cache regulär abläuft. Solche Fehler sind in der Entwicklungsumgebung mit deaktiviertem Cache oft unsichtbar und tauchen erst in der Produktion auf.
8. Eigene Mutations mit Integrationstests absichern
Magento bringt mit Magento\TestFramework\TestCase\GraphQlAbstract eine Basisklasse mit, die GraphQL-Requests gegen eine echte Testdatenbank ausführt, inklusive Authentifizierungs-Header für eingeloggte Kunden. Ein Integrationstest für eine eigene GraphQL Mutation sollte mindestens drei Fälle abdecken: den Erfolgsfall mit gültigen Daten, den Fehlerfall mit fehlendem Pflichtfeld, und den Autorisierungsfall ohne gültigen Kunden-Token.
Diese Tests laufen deutlich schneller als End-to-End-Tests über einen echten Browser und fangen Regressionen ab, bevor sie in die Produktion gelangen, etwa wenn ein Schema-Feld versehentlich umbenannt oder eine Exception-Klasse durch eine generische ersetzt wird. Für Teams, die mehrere Mutations pro Modul pflegen, lohnt sich ein gemeinsames Test-Fixture, das Testkunden und Testprodukte über @magentoDataFixture bereitstellt, statt jeden Test die Daten einzeln anlegen zu lassen.
<?php
declare(strict_types=1);
namespace Mironsoft\CustomerReview\Test\Integration;
use Magento\TestFramework\TestCase\GraphQlAbstract;
/**
* Integration test for the createProductReview custom GraphQL mutation.
*/
class CreateProductReviewTest extends GraphQlAbstract
{
/**
* @magentoApiDataFixture Magento/Catalog/_files/product_simple.php
*/
public function testCreateReviewSucceeds(): void
{
$query = <<<MUTATION
mutation {
createProductReview(input: {
sku: "simple",
rating: 5,
title: "Great product",
text: "Works exactly as described."
}) {
review_id
status
}
}
MUTATION;
$response = $this->graphQlMutation($query, [], '', $this->getCustomerHeaders());
self::assertArrayHasKey('review_id', $response['createProductReview']);
self::assertSame('pending', $response['createProductReview']['status']);
}
}
9. Native vs. eigene Mutations im Vergleich
Bevor eine eigene GraphQL Mutation geschrieben wird, lohnt sich der Blick auf Alternativen: manchmal lässt sich das Ziel über eine Erweiterung einer nativen Mutation per Plugin erreichen, manchmal ist ein komplett neuer Endpunkt die sauberere Lösung.
| Kriterium | Native Mutation erweitern | Eigene Mutation |
|---|---|---|
| Fachlicher Bezug | Nur wenn die Domäne wirklich zur bestehenden Mutation passt | Beliebige neue Fachlogik abbildbar |
| Upgrade-Sicherheit | Plugin kann bei Core-Änderungen brechen | Eigenes Schema, unabhängig vom Core |
| Implementierungsaufwand | Gering, nur Plugin-Klasse nötig | Schema, Resolver, Service, Tests |
| API-Klarheit für Frontend | Zusatzfelder wirken angeflanscht | Eigener, klar benannter Endpunkt |
| Testbarkeit | Muss bestehende Testsuite mit abdecken | Isolierte Integrationstests |
In der Praxis überwiegt fast immer die eigene GraphQL Mutation, sobald die Fachlogik eigenständig ist. Nur bei sehr kleinen Ergänzungen, etwa einem zusätzlichen optionalen Feld in createCustomer, ist eine Plugin-Erweiterung der nativen Mutation der schnellere und wartbarere Weg.
Mironsoft
Magento 2 GraphQL-Entwicklung und API-Architektur
Eigene GraphQL Mutations für euren Magento-Shop?
Wir entwerfen und implementieren eigene GraphQL Mutations, inklusive Schema-Design, Validierung, Transaktionslogik und Testabdeckung, damit eure Storefront-API konsistent und wartbar bleibt.
Schema-Design
Saubere Input- und Output-Typen für nachhaltige GraphQL-APIs
Resolver-Entwicklung
Testbare Resolver mit klarer Trennung von GraphQL- und Fachlogik
Qualitätssicherung
Integrationstests und Cache-Invalidierung von Anfang an mitgedacht
10. Zusammenfassung
Eine eigene GraphQL Mutation in Magento 2 entsteht aus fünf zusammenhängenden Bausteinen: der Schema-Deklaration in schema.graphqls, der Resolver-Klasse mit klarer Trennung von GraphQL-Layer und Fachlogik, konsequenter Validierung über GraphQlInputException und verwandte Klassen, Transaktionssicherheit für mehrschrittige Datenänderungen sowie korrekter Cache-Invalidierung. Wer einen dieser Bausteine überspringt, riskiert entweder unklare Fehlerantworten, inkonsistente Daten oder veraltete Cache-Zustände im Frontend.
Der Aufwand für eine vollständig durchdachte Mutation ist höher als für einen schnellen Prototyp, zahlt sich aber bei jeder Erweiterung aus: neue Felder im Input-Typ, zusätzliche Validierungsregeln oder ein weiterer Cache-Tag lassen sich in der etablierten Struktur ohne Umbau ergänzen. Integrationstests machen diese Erweiterungen sicher, weil Regressionen sofort auffallen, statt erst im Live-Betrieb entdeckt zu werden.
Eigene GraphQL Mutations in Magento 2 — Das Wichtigste auf einen Blick
Schema
Mutation-Feld mit @resolver-Direktive, explizite Input- und Output-Typen statt generischer JSON-Strings.
Resolver
ResolverInterface::resolve() delegiert an einen Service, prüft Berechtigungen und liefert die exakte Output-Struktur.
Validierung & Transaktionen
GraphQlInputException für Client-Fehler, DB-Transaktionen für mehrschrittige Änderungen, Idempotenz beachten.
Cache & Tests
Cache-Tags nach Datenänderung invalidieren, Integrationstests mit GraphQlAbstract für Erfolgs- und Fehlerfälle.