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

DataProvider und Resolver für die Veranstaltungsliste implementieren

DataProvider und Resolver für die Veranstaltungsliste implementieren

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

Dieses Kapitel macht die events-Query aus Kapitel 12 lebendig. Erst braucht es ein EventRepositoryInterface mit einer getList()-Methode nach dem Service-Contract-Muster, dann einen DataProvider, der Repository-Aufrufe in das vom Schema erwartete Array-Format übersetzt, und schließlich einen schlanken Resolver.

Das 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;
}

Die Implementierung: 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() bekommt hier ausschließlich die Array-Form [$condition => $value] übergeben - nie einen nackten Skalar. Diese Projektkonvention ist PHPStan-Level-5-Pflicht und zahlt sich hier direkt aus: $condition kommt dynamisch aus dem SearchCriteria-Filter, ein Aufruf ohne Array-Form würde bei jedem Nicht-eq-Operator (z. B. like aus Kapitel 14) schlicht falsch filtern.

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>

Der DataProvider: SearchCriteria bauen, Repository aufrufen

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

Der Schlüssel model im jeweiligen Item-Array ist genau die Konvention aus Kapitel 10 - er transportiert das geladene EventInterface-Objekt an eventuelle Feld-Resolver auf dem Event-Typ (z. B. das is_favorite-Feld aus Kapitel 17), ohne dass diese die Datenbank ein zweites Mal abfragen müssen.

Der schlanke 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: ViewModels (ArgumentInterface) sind in diesem Projekt der bevorzugte Weg, um Block-Klassen im Frontend schlank zu halten - für GraphQL-Resolver und -DataProvider gilt das nicht: Beide implementieren feste, vom Framework vorgegebene Klassen bzw. Konventionen (ResolverInterface, das DataProvider-Namensmuster). Es gibt hier kein ArgumentInterface-Äquivalent - die saubere Trennung entsteht stattdessen durch die Resolver/DataProvider-Aufteilung selbst.

Kapitel 14 baut auf diesem DataProvider auf und ergänzt Filter- und Sortierargumente, die über SearchCriteriaBuilder in die Collection einfließen.