Resolvers
A resolver implements ResolverInterface, receives the field, context, info, parent value and arguments, and returns an array.
public function resolve(Field $field, $context, ResolveInfo $info, ?array $value = null, ?array $args = null)A resolver is the PHP class that produces the value of one GraphQL field. You point a field at one with @resolver in schema.graphqls, and the class implements Magento\Framework\GraphQl\Query\ResolverInterface. It has one method.
The five parameters
$field— aFieldconfig element describing the field being resolved.$context— the request context:getUserId(),getUserType(), and the store undergetExtensionAttributes()->getStore().$info— aResolveInfoholding the parsed query, so you can see which sub-fields were requested.$value— the array the parent resolver returned, andnullat the top level.$args— the field's arguments, andnullwhen it has none.
Return an array whose keys match the fields of the declared type. Child resolvers receive that same array as their $value, which is why core resolvers add 'model' => $entity: the children need the loaded object, not just its scalars.
Throwing an error properly
Do not let an ordinary exception escape. It reaches the client as an internal error with the message stripped. Throw one from Magento\Framework\GraphQl\Exception instead: GraphQlInputException for a bad argument, GraphQlNoSuchEntityException for a missing record, GraphQlAuthenticationException and GraphQlAuthorizationException for identity and permissions, GraphQlAlreadyExistsException for a duplicate. Each is client-aware, so the message survives, and each adds a category to the error's extensions.
A new resolver class needs bin/magento cache:clean config for the schema, plus bin/magento setup:di:compile in production mode.
Example
<?php
// app/code/Learning/Faq/Model/Resolver/FaqEntry.php
declare(strict_types=1);
namespace Learning\Faq\Model\Resolver;
use Learning\Faq\Api\FaqRepositoryInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
class FaqEntry implements ResolverInterface
{
public function __construct(private readonly FaqRepositoryInterface $faqRepository)
{
}
/**
* @inheritDoc
* @param \Magento\GraphQl\Model\Query\ContextInterface $context
*/
public function resolve(
Field $field,
$context,
ResolveInfo $info,
?array $value = null,
?array $args = null
): array {
$uid = trim($args['uid'] ?? '');
// Longhand of the line above; the two behave identically:
// $uid = trim(isset($args['uid']) ? $args['uid'] : '');
if ($uid === '') {
throw new GraphQlInputException(__('A value for "uid" is required.'));
}
try {
$entry = $this->faqRepository->getByUid(
$uid,
(int) $context->getExtensionAttributes()->getStore()->getId()
);
} catch (NoSuchEntityException $e) {
throw new GraphQlNoSuchEntityException(__('No FAQ entry "%1" exists.', $uid), $e);
}
return [
'uid' => $entry->getUid(),
'question' => $entry->getQuestion(),
'answer' => $entry->getAnswer(),
'model' => $entry,
];
}
}When to use it
- Return a custom entity from a new GraphQL query.
- Turn a repository's NoSuchEntityException into an error a storefront can display.
- Read the current store or logged-in customer id inside a resolver.
- Give a nested field access to the model its parent already loaded.
More examples
Passing the loaded object down to child resolvers
The parent resolver put 'model' in its return array, so this child can work with the entity instead of reloading it. Every catalog resolver in core relies on the same convention and throws when the key is absent. Reach for this whenever a nested field needs more than the scalars the parent already returned.
<?php
// app/code/Learning/Faq/Model/Resolver/FaqEntry/RelatedCount.php
declare(strict_types=1);
namespace Learning\Faq\Model\Resolver\FaqEntry;
use Learning\Faq\Api\Data\FaqEntryInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
class RelatedCount implements ResolverInterface
{
/**
* @inheritDoc
*/
public function resolve(
Field $field,
$context,
ResolveInfo $info,
?array $value = null,
?array $args = null
): int {
if (empty($value['model'])) {
throw new LocalizedException(__('"model" value should be specified'));
}
/** @var FaqEntryInterface $entry */
$entry = $value['model'];
return count($entry->getRelatedUids());
}
}Constructor property promotion, long and short
You will meet both forms in the same codebase: core Magento classes predate promotion, while recent third-party modules use it. They compile to the same thing. Recognising the short form matters because it hides the dependency list in the signature, which is still where you look to see what a resolver injects.
<?php
// Both forms declare the same private property and assign the same argument.
// Longhand, which is what most core classes still use:
class LonghandResolver
{
/**
* @var FaqRepositoryInterface
*/
private FaqRepositoryInterface $faqRepository;
public function __construct(FaqRepositoryInterface $faqRepository)
{
$this->faqRepository = $faqRepository;
}
}
// Shorthand, available since PHP 8.0 and valid on this course's PHP 8.4:
class ShorthandResolver
{
public function __construct(private readonly FaqRepositoryInterface $faqRepository)
{
}
}What a thrown GraphQL exception looks like to the client
GraphQlInputException sets the category to graphql-input; GraphQlNoSuchEntityException sets graphql-no-such-entity. A client can branch on that category instead of parsing the message. If you see category graphql-internal or a bare "Internal server error", an ordinary exception escaped your resolver and the real message is in var/log/exception.log.
{
"errors": [
{
"message": "A value for \"uid\" is required.",
"locations": [ { "line": 2, "column": 3 } ],
"path": [ "faqEntry" ],
"extensions": { "category": "graphql-input" }
}
],
"data": { "faqEntry": null }
}
Discussion