Magento 2.3 wprowadziło obsługę GraphQL jako alternatywę dla REST API. O ile REST wymaga osobnych endpointów dla każdego zasobu, GraphQL pozwala klientowi samemu określić jakich danych potrzebuje w jednym zapytaniu. Pokazuję jak napisać własny resolver i udostępnić dane przez GraphQL w module Magento 2.
GraphQL vs REST w kontekście Magento 2
REST API Magento zwraca z góry określoną strukturę danych. Gdy frontend potrzebuje tylko nazwy i ceny produktu, i tak dostaje kilkadziesiąt pól. GraphQL pozwala zapytać dokładnie o to, co jest potrzebne:
# Zapytanie GraphQL - klient decyduje co dostaje
query {
products(search: "buty", pageSize: 5) {
items {
name
sku
price_range {
minimum_price {
final_price {
value
currency
}
}
}
}
}
}
Mniej danych przez sieć, mniej parsowania na frontendzie, jeden request zamiast kilku. To szczególnie istotne przy PWA Studio i headless commerce, gdzie frontend komunikuje się z Magento wyłącznie przez API.
Struktura modułu z własnym GraphQL
Vendor/Module/
etc/
module.xml
schema.graphqls <- definicja schematu
Model/
Resolver/
Subscription.php <- resolver
Subscriptions.php <- resolver kolekcji
registration.php
Definicja schematu – schema.graphqls
type Query {
newsletterSubscription(email: String!): SubscriptionOutput @resolver(class: "Vendor\\Module\\Model\\Resolver\\Subscription")
newsletterSubscriptions(pageSize: Int = 20, currentPage: Int = 1): SubscriptionsOutput @resolver(class: "Vendor\\Module\\Model\\Resolver\\Subscriptions")
}
type SubscriptionOutput {
id: Int
email: String
status: String
created_at: String
}
type SubscriptionsOutput {
items: [SubscriptionOutput]
total_count: Int
page_info: SearchResultPageInfo
}
input SubscribeInput {
email: String!
store_id: Int
}
type Mutation {
subscribeNewsletter(input: SubscribeInput!): SubscriptionOutput @resolver(class: "Vendor\\Module\\Model\\Resolver\\Subscribe")
}
Resolver – implementacja w PHP
<?php
namespace Vendor\Module\Model\Resolver;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\Builder;
use Vendor\Module\Api\SubscriptionRepositoryInterface;
class Subscription implements ResolverInterface
{
public function __construct(
private SubscriptionRepositoryInterface $subscriptionRepository
) {}
public function resolve(
Field $field,
mixed $context,
ResolveInfo $info,
?array $value = null,
?array $args = null
): array {
$email = $args['email'] ?? null;
if (!$email) {
throw new \Magento\Framework\GraphQl\Exception\GraphQlInputException(
__('Email is required')
);
}
try {
$subscription = $this->subscriptionRepository->getByEmail($email);
} catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
throw new \Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException(
__('Subscription not found for email: %1', $email)
);
}
return [
'id' => $subscription->getId(),
'email' => $subscription->getEmail(),
'status' => $subscription->getStatus(),
'created_at' => $subscription->getCreatedAt(),
];
}
}
Resolver kolekcji z paginacją
<?php
namespace Vendor\Module\Model\Resolver;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Vendor\Module\Api\SubscriptionRepositoryInterface;
class Subscriptions implements ResolverInterface
{
public function __construct(
private SubscriptionRepositoryInterface $subscriptionRepository,
private SearchCriteriaBuilder $searchCriteriaBuilder
) {}
public function resolve(
Field $field,
mixed $context,
ResolveInfo $info,
?array $value = null,
?array $args = null
): array {
$pageSize = $args['pageSize'] ?? 20;
$currentPage = $args['currentPage'] ?? 1;
$searchCriteria = $this->searchCriteriaBuilder
->setPageSize($pageSize)
->setCurrentPage($currentPage)
->create();
$result = $this->subscriptionRepository->getList($searchCriteria);
$items = array_map(
fn($sub) => [
'id' => $sub->getId(),
'email' => $sub->getEmail(),
'status' => $sub->getStatus(),
'created_at' => $sub->getCreatedAt(),
],
$result->getItems()
);
return [
'items' => $items,
'total_count' => $result->getTotalCount(),
'page_info' => [
'page_size' => $pageSize,
'current_page' => $currentPage,
'total_pages' => (int) ceil($result->getTotalCount() / $pageSize),
],
];
}
}
Autoryzacja w resolverach
Jeśli endpoint wymaga zalogowanego użytkownika, sprawdzasz to przez kontekst:
<?php
public function resolve(Field $field, mixed $context, ResolveInfo $info, ?array $value = null, ?array $args = null): array
{
// Sprawdzenie czy użytkownik jest zalogowany
if (false === $context->getExtensionAttributes()->getIsCustomer()) {
throw new \Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException(
__('The current customer isn\'t authorized.')
);
}
$customerId = $context->getUserId();
// ... dalsza logika
}
Testowanie GraphQL w DDEV
# Zapytanie przez curl
curl -X POST \
https://magento2-dev.ddev.site/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{ products(search: \"test\", pageSize: 3) { items { name sku } } }"}'
Do wygodnego testowania GraphQL polecam Altair GraphQL Client lub wbudowany playground dostępny pod /graphql w trybie developer.
Podsumowanie
GraphQL w Magento 2 to solidna alternatywa dla REST szczególnie przy projektach headless i PWA. Schemat .graphqls jasno dokumentuje co API udostępnia, resolwery implementują dokładnie jeden kontrakt, a klient pobiera tylko potrzebne dane. Jeśli planujesz projekt z Vue Storefront lub Magento PWA Studio - GraphQL to jedyna rozsądna droga.
