Was sind Mutations? Eine erste eigene Mutation schreiben
Was sind Mutations? Eine erste eigene Mutation schreiben
~8 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026
Alle bisherigen Kapitel haben ausschließlich gelesen. GraphQL trennt lesende und schreibende Operationen bewusst über zwei getrennte Basistypen: Query und Mutation. Dieses Kapitel baut die erste eigene Mutation - eine Veranstaltung als Favorit markieren - noch bewusst ohne Zugriffsschutz, um die reine Mutation-Mechanik zu zeigen. Die Kapitel 17 und 18 härten sie danach ab.
Query vs. Mutation: mehr als nur Benennung
Technisch könnte ein schreibender Vorgang auch über eine Query laufen - GraphQL erzwingt die Trennung nicht auf Protokollebene. Sie existiert trotzdem aus gutem Grund: Bei mehreren Top-Level-Feldern in einer einzigen Anfrage garantiert die Spezifikation für Mutation-Felder eine sequenzielle Ausführung (eine nach der anderen), während Query-Felder parallel ausgeführt werden dürfen. Wer mehrere Schreibvorgänge in einer Anfrage bündelt und sich auf eine bestimmte Reihenfolge verlässt, braucht dafür zwingend Mutation.
Das Input/Output-Muster
Magentos eigene Mutations - generateCustomerToken, addProductsToCart, createCustomer - folgen fast durchgängig demselben Muster: ein einziges input-Argument als Input-Typ (Kapitel 6) statt vieler loser Argumente, und ein eigener Output-Typ als Rückgabewert statt eines nackten Skalars oder Entity-Typs. Dieses Muster übernimmt auch die Favoriten-Mutation:
type Mutation {
addEventToFavorites(
input: AddEventToFavoritesInput!
): AddEventToFavoritesOutput
@resolver(class: "Mironsoft\\Event\\Model\\Resolver\\AddEventToFavorites")
@doc(description: "Adds an event to the current customer's favorites")
}
input AddEventToFavoritesInput @doc(description: "Input for addEventToFavorites") {
event_id: Int!
}
type AddEventToFavoritesOutput @doc(description: "Result of addEventToFavorites") {
event: Event
}Der Output-Typ gibt die aktualisierte Veranstaltung direkt zurück, statt nur "true"/"false" - so kann der Client in einer einzigen Anfrage sowohl die Mutation ausführen als auch die neuen Daten (inklusive des in Kapitel 17 ergänzten is_favorite-Feldes) abfragen, statt danach eine zweite Query nachzureichen.
Den Resolver schreiben (vorläufige Fassung)
<?php
declare(strict_types=1);
namespace Mironsoft\Event\Model\Resolver;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Mironsoft\Event\Api\EventRepositoryInterface;
use Mironsoft\Event\Model\ResourceModel\EventFavorite;
/**
* Resolves the addEventToFavorites mutation field.
*
* NOTE: this first version blindly trusts the caller's customer ID - it is
* hardened with a proper authentication check in chapter 18.
*/
class AddEventToFavorites implements ResolverInterface
{
/**
* @param EventRepositoryInterface $eventRepository Service contract for event access
* @param EventFavorite $eventFavoriteResource Resource model for the favorites linkage table
*/
public function __construct(
private readonly EventRepositoryInterface $eventRepository,
private readonly EventFavorite $eventFavoriteResource,
) {
}
/**
* Adds the given event to the current customer's favorites.
*
* @param Field $field Resolved GraphQL field configuration
* @param mixed $context Resolver context, carries the customer ID (chapter 17)
* @param ResolveInfo $info GraphQL resolve tree info
* @param array|null $value Parent resolver's value, unused for a top-level field
* @param array|null $args Arguments passed to the addEventToFavorites field
* @return array<string, mixed>
*/
public function resolve(
Field $field,
$context,
ResolveInfo $info,
?array $value = null,
?array $args = null
): array {
$eventId = (int) ($args['input']['event_id'] ?? 0);
$customerId = (int) $context->getUserId();
$event = $this->eventRepository->getById($eventId);
$this->eventFavoriteResource->addFavorite($customerId, $eventId);
return [
'event' => [
'event_id' => $event->getEventId(),
'identifier' => $event->getIdentifier(),
'title' => $event->getTitle(),
'description' => $event->getDescription(),
'location' => $event->getLocation(),
'start_at' => $event->getStartAt(),
'end_at' => $event->getEndAt(),
'capacity' => $event->getCapacity(),
'model' => $event,
],
];
}
}Achtung: $customerId = (int) $context->getUserId(); ist an dieser Stelle unsicher: Für einen Gast liefert getUserId() 0 oder null, und der Aufruf würde die Veranstaltung stillschweigend für Kunde 0 speichern, statt die Anfrage abzulehnen. Diese Lücke ist in diesem Kapitel bewusst noch offen - Kapitel 17 erklärt zunächst, wie Authentifizierung im Resolver-Context überhaupt funktioniert, bevor Kapitel 18 den fehlenden Schutz konkret nachrüstet.
Die Mutation mit Variablen testen
mutation AddFavorite($eventId: Int!) {
addEventToFavorites(input: { event_id: $eventId }) {
event {
title
}
}
}Tipp: Mutation-Argumente per Variablen zu übergeben (Kapitel 3) ist bei Mutations noch wichtiger als bei Queries - der Query-String einer Mutation landet oft in Server-Logs, und Werte direkt im String zu verketten erschwert die Wiederverwendung des immer gleichen Mutation-Strings über verschiedene Aufrufe hinweg.
Mit einer funktionierenden, aber noch ungeschützten Mutation geht es in Kapitel 17 an das Fundament jeder Zugriffskontrolle in GraphQL: Kunden-Token und der Customer-Context im Resolver.