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
<?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;
}<?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
<?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
<?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
<?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
<?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 configquery {
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.