Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

What Are Mutations? Writing a First Custom Mutation

What Are Mutations? Writing a First Custom Mutation

~8 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026

Every chapter so far has been read-only. GraphQL deliberately separates read and write operations through two distinct root types: Query and Mutation. This chapter builds the first custom mutation - marking an event as a favorite - still deliberately without access protection, to show the pure mutation mechanics. Chapters 17 and 18 harden it afterward.

Query vs. Mutation: more than just naming

Technically, a write operation could also run through a Query - GraphQL doesn't enforce the separation at the protocol level. It still exists for good reason: for multiple top-level fields in a single request, the specification guarantees sequential execution (one after another) for Mutation fields, while Query fields are allowed to execute in parallel. Anyone bundling several write operations into one request and relying on a specific order absolutely needs Mutation for that.

The input/output pattern

Magento's own mutations - generateCustomerToken, addProductsToCart, createCustomer - almost universally follow the same pattern: a single input argument as an input type (chapter 6) instead of many loose arguments, and a dedicated Output type as the return value instead of a bare scalar or entity type. The favorites mutation adopts this same pattern:

app/code/Mironsoft/Event/etc/schema.graphqls
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
}

The output type returns the updated event directly, instead of just "true"/"false" - that way, the client can both execute the mutation and query the fresh data (including the is_favorite field added in chapter 17) in a single request, instead of firing a second query afterward.

Writing the resolver (preliminary version)

app/code/Mironsoft/Event/Model/Resolver/AddEventToFavorites.php
<?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(); is insecure at this point: for a guest, getUserId() returns 0 or null, and the call would silently save the event for customer 0 instead of rejecting the request. This gap is deliberately left open in this chapter - chapter 17 first explains how authentication actually works in the resolver context, before chapter 18 concretely retrofits the missing protection.

Testing the mutation with variables

mutation AddFavorite($eventId: Int!) {
  addEventToFavorites(input: { event_id: $eventId }) {
    event {
      title
    }
  }
}

Tipp: Passing mutation arguments via variables (chapter 3) matters even more for mutations than for queries - a mutation's query string often ends up in server logs, and concatenating values directly into the string makes it harder to reuse the exact same mutation string across different calls.

With a working but still unprotected mutation in place, chapter 17 moves on to the foundation of every access control mechanism in GraphQL: customer tokens and the customer context in the resolver.