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

Implementing the DataProvider and Resolver for the Events List

Implementing the DataProvider and Resolver for the Events List

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

This chapter brings the events query from chapter 12 to life. First it needs an EventRepositoryInterface with a getList() method following the service-contract pattern, then a DataProvider that translates repository calls into the array format expected by the schema, and finally a thin resolver.

The repository interface

app/code/Mironsoft/Event/Api/EventRepositoryInterface.php
<?php

declare(strict_types=1);

namespace Mironsoft\Event\Api;

use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Mironsoft\Event\Api\Data\EventInterface;
use Mironsoft\Event\Api\Data\EventSearchResultsInterface;

/**
 * Service contract for reading and writing events.
 */
interface EventRepositoryInterface
{
    /**
     * Loads an event by its numeric ID.
     *
     * @param int $eventId
     * @return EventInterface
     * @throws NoSuchEntityException
     */
    public function getById(int $eventId): EventInterface;

    /**
     * Loads an event by its URL-safe identifier.
     *
     * @param string $identifier
     * @return EventInterface
     * @throws NoSuchEntityException
     */
    public function getByIdentifier(string $identifier): EventInterface;

    /**
     * Returns a filtered, sorted, paginated list of events.
     *
     * @param SearchCriteriaInterface $searchCriteria
     * @return EventSearchResultsInterface
     */
    public function getList(SearchCriteriaInterface $searchCriteria): EventSearchResultsInterface;
}
app/code/Mironsoft/Event/Api/Data/EventSearchResultsInterface.php
<?php

declare(strict_types=1);

namespace Mironsoft\Event\Api\Data;

use Magento\Framework\Api\SearchResultsInterface;

/**
 * Search results wrapper for event collections, provides typed items.
 */
interface EventSearchResultsInterface extends SearchResultsInterface
{
    /**
     * Returns the matched events.
     *
     * @return EventInterface[]
     */
    public function getItems(): array;

    /**
     * Sets the matched events.
     *
     * @param EventInterface[] $items
     * @return $this
     */
    public function setItems(array $items): self;
}

The implementation: EventRepository

app/code/Mironsoft/Event/Model/EventRepository.php
<?php

declare(strict_types=1);

namespace Mironsoft\Event\Model;

use Magento\Framework\Api\SearchCriteriaInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Mironsoft\Event\Api\Data\EventInterface;
use Mironsoft\Event\Api\Data\EventSearchResultsInterface;
use Mironsoft\Event\Api\Data\EventSearchResultsInterfaceFactory;
use Mironsoft\Event\Api\EventRepositoryInterface;
use Mironsoft\Event\Model\ResourceModel\Event as EventResource;
use Mironsoft\Event\Model\ResourceModel\Event\CollectionFactory;

/**
 * Reads and writes events via the resource model and collection.
 */
class EventRepository implements EventRepositoryInterface
{
    /**
     * @param EventResource $eventResource Resource model for load/save/delete
     * @param EventFactory $eventFactory Factory for empty Event models
     * @param CollectionFactory $collectionFactory Factory for event collections
     * @param EventSearchResultsInterfaceFactory $searchResultsFactory Factory for the search results wrapper
     */
    public function __construct(
        private readonly EventResource $eventResource,
        private readonly EventFactory $eventFactory,
        private readonly CollectionFactory $collectionFactory,
        private readonly EventSearchResultsInterfaceFactory $searchResultsFactory,
    ) {
    }

    /**
     * @inheritDoc
     */
    public function getById(int $eventId): EventInterface
    {
        $event = $this->eventFactory->create();
        $this->eventResource->load($event, $eventId);

        if ($event->getEventId() === null) {
            throw new NoSuchEntityException(
                __('The event with ID "%1" doesn\'t exist.', $eventId)
            );
        }

        return $event;
    }

    /**
     * @inheritDoc
     */
    public function getByIdentifier(string $identifier): EventInterface
    {
        $event = $this->eventFactory->create();
        $this->eventResource->load($event, $identifier, EventInterface::IDENTIFIER);

        if ($event->getEventId() === null) {
            throw new NoSuchEntityException(
                __('The event with identifier "%1" doesn\'t exist.', $identifier)
            );
        }

        return $event;
    }

    /**
     * @inheritDoc
     */
    public function getList(SearchCriteriaInterface $searchCriteria): EventSearchResultsInterface
    {
        $collection = $this->collectionFactory->create();

        foreach ($searchCriteria->getFilterGroups() as $filterGroup) {
            foreach ($filterGroup->getFilters() as $filter) {
                $condition = $filter->getConditionType() ?: 'eq';
                $collection->addFieldToFilter(
                    $filter->getField(),
                    [$condition => $filter->getValue()]
                );
            }
        }

        foreach ($searchCriteria->getSortOrders() ?? [] as $sortOrder) {
            $collection->addOrder(
                (string) $sortOrder->getField(),
                (string) $sortOrder->getDirection()
            );
        }

        $collection->setCurPage($searchCriteria->getCurrentPage());
        $collection->setPageSize($searchCriteria->getPageSize());

        /** @var EventSearchResultsInterface $searchResults */
        $searchResults = $this->searchResultsFactory->create();
        $searchResults->setSearchCriteria($searchCriteria);
        $searchResults->setItems($collection->getItems());
        $searchResults->setTotalCount($collection->getSize());

        return $searchResults;
    }
}

addFieldToFilter() is only ever passed the array form [$condition => $value] here - never a bare scalar. This project convention is a PHPStan-level-5 requirement, and it pays off directly here: $condition comes dynamically from the search criteria filter; a call without the array form would simply filter incorrectly for any non-eq operator (e.g. like from chapter 14).

Preference in di.xml

app/code/Mironsoft/Event/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\Event\Api\EventRepositoryInterface"
                type="Mironsoft\Event\Model\EventRepository"/>
    <preference for="Mironsoft\Event\Api\Data\EventInterface"
                type="Mironsoft\Event\Model\Event"/>
    <preference for="Mironsoft\Event\Api\Data\EventSearchResultsInterface"
                type="Mironsoft\Event\Model\EventSearchResults"/>
</config>

The DataProvider: building SearchCriteria, calling the repository

app/code/Mironsoft/Event/Model/Resolver/DataProvider/Events.php
<?php

declare(strict_types=1);

namespace Mironsoft\Event\Model\Resolver\DataProvider;

use Magento\Framework\Api\SearchCriteriaBuilder;
use Mironsoft\Event\Api\Data\EventInterface;
use Mironsoft\Event\Api\EventRepositoryInterface;

/**
 * Loads events for the GraphQL events query and shapes them into the
 * array format expected by the Events/Event schema types.
 */
class Events
{
    /**
     * @param EventRepositoryInterface $eventRepository Service contract for event access
     * @param SearchCriteriaBuilder $searchCriteriaBuilder Builder for repository search criteria
     */
    public function __construct(
        private readonly EventRepositoryInterface $eventRepository,
        private readonly SearchCriteriaBuilder $searchCriteriaBuilder,
    ) {
    }

    /**
     * Fetches active events for the given page and shapes the Events payload.
     *
     * @param int $pageSize Number of events per page
     * @param int $currentPage Requested page, 1-based
     * @return array{items: array<int, array<string, mixed>>, total_count: int, page_info: array<string, int>}
     */
    public function getList(int $pageSize, int $currentPage): array
    {
        $this->searchCriteriaBuilder->addFilter(EventInterface::IS_ACTIVE, 1);
        $this->searchCriteriaBuilder->setCurrentPage($currentPage);
        $this->searchCriteriaBuilder->setPageSize($pageSize);
        $searchCriteria = $this->searchCriteriaBuilder->create();

        $searchResults = $this->eventRepository->getList($searchCriteria);
        $totalCount = $searchResults->getTotalCount();

        $items = [];
        foreach ($searchResults->getItems() as $event) {
            $items[] = [
                '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,
            ];
        }

        $totalPages = $pageSize > 0 ? (int) ceil($totalCount / $pageSize) : 0;

        return [
            'items' => $items,
            'total_count' => $totalCount,
            'page_info' => [
                'page_size' => $pageSize,
                'current_page' => $currentPage,
                'total_pages' => $totalPages,
            ],
        ];
    }
}

The model key in each item array is exactly the convention from chapter 10 - it carries the loaded EventInterface object along to any field resolvers on the Event type (e.g. the is_favorite field from chapter 17), without them having to query the database a second time.

The thin resolver

app/code/Mironsoft/Event/Model/Resolver/Events.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\Model\Resolver\DataProvider\Events as EventsDataProvider;

/**
 * Resolves the events query field.
 */
class Events implements ResolverInterface
{
    /**
     * @param EventsDataProvider $eventsDataProvider Loads and shapes event list data
     */
    public function __construct(
        private readonly EventsDataProvider $eventsDataProvider,
    ) {
    }

    /**
     * Delegates to the DataProvider using the pageSize/currentPage arguments.
     *
     * @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 events field
     * @return array<string, mixed>
     */
    public function resolve(
        Field $field,
        $context,
        ResolveInfo $info,
        ?array $value = null,
        ?array $args = null
    ): array {
        $pageSize = (int) ($args['pageSize'] ?? 20);
        $currentPage = (int) ($args['currentPage'] ?? 1);

        return $this->eventsDataProvider->getList($pageSize, $currentPage);
    }
}
bin/cache-clean config
query {
  events(pageSize: 2) {
    items { identifier title start_at }
    total_count
  }
}

Tipp: In this project, ViewModels (ArgumentInterface) are the preferred way to keep frontend block classes thin - that doesn't apply to GraphQL resolvers and DataProviders: both implement fixed classes or conventions mandated by the framework (ResolverInterface, the DataProvider naming pattern). There's no ArgumentInterface equivalent here - clean separation instead comes from the resolver/DataProvider split itself.

Chapter 14 builds on this DataProvider and adds filter and sort arguments that flow into the collection via SearchCriteriaBuilder.