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

Building a Protected Mutation: Marking an Event as a Favorite (Logged-In Customers Only)

Building a Protected Mutation: Marking an Event as a Favorite (Logged-In Customers Only)

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

Using the knowledge from chapter 17, this chapter upgrades the naive AddEventToFavorites mutation from chapter 16 into a genuinely protected mutation - the last missing piece for the events project's actual goal.

The resource class for favorites

Before the resolver can be secured, the concrete database access to mironsoft_event_customer_favorite (chapter 11) is still missing, which IsFavorite (chapter 17) already referenced:

app/code/Mironsoft/Event/Model/ResourceModel/EventFavorite.php
<?php

declare(strict_types=1);

namespace Mironsoft\Event\Model\ResourceModel;

use Magento\Framework\App\ResourceConnection;

/**
 * Direct low-level access to the mironsoft_event_customer_favorite table.
 * A dedicated model/collection pair is intentionally skipped here - the
 * linkage table has no identity of its own beyond the composite key.
 */
class EventFavorite
{
    private const TABLE_NAME = 'mironsoft_event_customer_favorite';

    /**
     * @param ResourceConnection $resourceConnection Database connection provider
     */
    public function __construct(
        private readonly ResourceConnection $resourceConnection,
    ) {
    }

    /**
     * Marks the given event as a favorite for the given customer (idempotent).
     *
     * @param int $customerId
     * @param int $eventId
     * @return void
     */
    public function addFavorite(int $customerId, int $eventId): void
    {
        $connection = $this->resourceConnection->getConnection();
        $connection->insertOnDuplicate(
            $this->resourceConnection->getTableName(self::TABLE_NAME),
            ['event_id' => $eventId, 'customer_id' => $customerId]
        );
    }

    /**
     * Checks whether the given event is a favorite of the given customer.
     *
     * @param int $customerId
     * @param int $eventId
     * @return bool
     */
    public function isFavorite(int $customerId, int $eventId): bool
    {
        $connection = $this->resourceConnection->getConnection();
        $select = $connection->select()
            ->from($this->resourceConnection->getTableName(self::TABLE_NAME), ['event_id'])
            ->where('event_id = ?', $eventId)
            ->where('customer_id = ?', $customerId);

        return $connection->fetchOne($select) !== false;
    }
}

Securing the resolver

app/code/Mironsoft/Event/Model/Resolver/AddEventToFavorites.php (updated resolve() method)
<?php

declare(strict_types=1);

namespace Mironsoft\Event\Model\Resolver;

use Magento\Authorization\Model\UserContextInterface;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException;
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, restricted to logged-in
 * customers.
 */
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
     * @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>
     * @throws GraphQlAuthorizationException
     */
    public function resolve(
        Field $field,
        $context,
        ResolveInfo $info,
        ?array $value = null,
        ?array $args = null
    ): array {
        if ($context->getUserType() !== UserContextInterface::USER_TYPE_CUSTOMER
            || !$context->getExtensionAttributes()->getIsCustomer()
        ) {
            throw new GraphQlAuthorizationException(
                __('You must be logged in as a customer to favorite an event.')
            );
        }

        $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,
            ],
        ];
    }
}

The double check - getUserType() AND getIsCustomer() - is deliberately redundant: USER_TYPE_CUSTOMER alone already excludes guests and admins, but getIsCustomer() is the officially documented check used by Magento's own customer resolvers. Both together stay on the safe side, even if the behavior of either method were to change in a future Magento minor release.

Testing the full flow

mutation {
  addEventToFavorites(input: { event_id: 3 }) {
    event {
      title
      is_favorite
    }
  }
}

With a valid Authorization: Bearer header, the response returns is_favorite: true right in the same response - the value comes from the IsFavorite resolver from chapter 17, which builds on the same model key that AddEventToFavorites supplies here in its return array.

Achtung: Without an Authorization header, the same mutation now returns an error with "category": "graphql-authorization" instead of silently filling in customer 0 - the gap from chapter 16 is now closed. Chapter 19 covers, in depth, how to cleanly distinguish this and other error kinds.

With a real, protected mutation in place, the project's write path is functionally complete. Chapter 19 wraps up block 5 with systematic error handling across both resolvers.