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
<?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;
}The implementation: 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() 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
<?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
<?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
<?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: 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.