Skip to content
intermediate Phase 62 · GraphQL Advanced

GraphQL Resolvers

Understanding Magento 2 GraphQL resolvers: resolver classes, data loading, N+1 prevention, and batching

45m
0 problems
Topic Progress 0%

Resolver Basics

Resolver Interface

<?php
namespace Magento\Framework\GraphQl\Resolver;

use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;

interface ResolverInterface
{
    /**
     * @param Field $field
     * @param mixed $context
     * @param ResolveInfo $info
     * @param array|null $value
     * @param array|null $args
     * @return mixed
     */
    public function resolve(
        Field $field,
        $context,
        ResolveInfo $info,
        array $value = null,
        array $args = null
    );
}

Basic Resolver

<?php
namespace Vendor\Module\GraphQl\Resolver\Query;

use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\GraphQl\Resolver\ResolverInterface;

class ProductResolver implements ResolverInterface
{
    public function __construct(
        private \Magento\Catalog\Api\ProductRepositoryInterface $productRepository,
    ) {
    }
    
    public function resolve(
        Field $field,
        $context,
        ResolveInfo $info,
        array $value = null,
        array $args = null
    ) {
        $sku = $args['sku'];
        
        $product = $this->productRepository->get($sku);
        
        return [
            'id' => $product->getId(),
            'name' => $product->getName(),
            'sku' => $product->getSku(),
            'price' => $product->getPrice()
        ];
    }
}

Register Resolver

<!-- etc/schema.graphqls -->
<config>
    <type name="Query">
        <field name="product"
               resolver="Vendor\Module\GraphQl\Resolver\Query\ProductResolver"
               type="Product"/>
    </type>
</config>

Data Loading

Collection Loading

public function resolve(
    Field $field,
    $context,
    ResolveInfo $info,
    array $value = null,
    array $args = null
) {
    $collection = $this->collectionFactory->create();
    
    // Apply filters
    if (isset($args['filter'])) {
        $this->filterApplier->applyFilters($collection, $args['filter']);
    }
    
    // Apply sorting
    if (isset($args['sort'])) {
        $this->sortApplier->applySorting($collection, $args['sort']);
    }
    
    // Apply pagination
    $pageSize = $args['pageSize'] ?? 20;
    $currentPage = $args['currentPage'] ?? 1;
    $collection->setPageSize($pageSize);
    $collection->setCurPage($currentPage);
    
    $items = [];
    foreach ($collection as $item) {
        $items[] = $this->formatItem($item);
    }
    
    return [
        'items' => $items,
        'total_count' => $collection->getSize(),
        'page_info' => [
            'page_size' => $pageSize,
            'current_page' => $currentPage
        ]
    ];
}

Repository Loading

public function resolve(
    Field $field,
    $context,
    ResolveInfo $info,
    array $value = null,
    array $args = null
) {
    try {
        $product = $this->productRepository->getById($args['id']);
        
        return [
            'id' => $product->getId(),
            'name' => $product->getName(),
            'sku' => $product->getSku()
        ];
    } catch (\Exception $e) {
        throw new \GraphQL\Error\GraphQLError(
            __('Product not found'),
            null,
            null,
            null,
            ['id' => $args['id']]
        );
    }
}

N+1 Prevention

The N+1 Problem

query {
    products {
        items {
            name
            category {
                name  # N+1: Separate query for each product's category
            }
        }
    }
}

Solution: DataLoader

<?php
namespace Vendor\Module\GraphQl\DataLoader;

use GraphQL\Utils\Utils;
use Magento\Framework\GraphQl\Model\DataLoader\BatchResolverInterface;

class CategoryBatchResolver implements BatchResolverInterface
{
    public function __construct(
        private \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory $collectionFactory,
    ) {
    }
    
    public function resolve(array $keys): array
    {
        $collection = $this->collectionFactory->create()
            ->addFieldToFilter('entity_id', ['in' => $keys])
            ->addFieldToSelect(['entity_id', 'name']);
        
        $categories = [];
        foreach ($collection as $category) {
            $categories[$category->getId()] = [
                'id' => $category->getId(),
                'name' => $category->getName()
            ];
        }
        
        // Return in same order as keys
        $result = [];
        foreach ($keys as $key) {
            $result[] = $categories[$key] ?? null;
        }
        
        return $result;
    }
}

Use DataLoader in Resolver

public function resolve(
    Field $field,
    $context,
    ResolveInfo $info,
    array $value = null,
    array $args = null
) {
    // Use DataLoader for related data
    return $context->categoryLoader->load($value['category_id']);
}

Benefits

  • Single query for all categories
  • Reduces database queries
  • Improves performance

Batching and Performance

Batch Loading

<?php
namespace Vendor\Module\GraphQl\Resolver\Product;

use Magento\Framework\GraphQl\Model\DataLoader\BatchResolverInterface;

class CategoryBatchResolver implements BatchResolverInterface
{
    public function resolve(array $ids): array
    {
        // Single query for all IDs
        $collection = $this->collectionFactory->create()
            ->addFieldToFilter('entity_id', ['in' => $ids])
            ->addFieldToSelect(['entity_id', 'name', 'url_path']);
        
        $categories = [];
        foreach ($collection as $category) {
            $categories[$category->getId()] = $this->formatCategory($category);
        }
        
        // Return results in order
        return array_map(fn($id) => $categories[$id] ?? null, $ids);
    }
}

Query Complexity Analysis

<?php
namespace Vendor\Module\GraphQl\Model\Query\Complexity;

use Magento\Framework\GraphQl\Model\Query\Complexity\AbstractComplexity;

class ProductComplexity extends AbstractComplexity
{
    public function getComplexity(
        Field $field,
        $childComplexity
    ): int {
        return 10 + $childComplexity;
    }
}

Query Depth Limiting

<!-- etc/graphQl.xml -->
<config>
    <type name="Magento\Framework\GraphQl\Model\Query\Validator">
        <arguments>
            <argument name="validators" xsi:type="array">
                <item name="depth_limit" xsi:type="object">
                    Magento\Framework\GraphQl\Model\Query\Validator\DepthLimit
                </item>
            </argument>
        </arguments>
    </type>
</config>

Performance Tips

  1. Use DataLoader for related entities
  2. Batch database queries instead of individual
  3. Limit query depth to prevent abuse
  4. Implement caching for frequently accessed data
  5. Use indexed fields in filters

Quiz

1. What is the N+1 problem?

Question 1 options

2. How do you prevent N+1 queries?

Question 2 options

3. What interface must resolvers implement?

Question 3 options

Flashcards

Question

What is the N+1 problem?

Answer

Too many individual queries for related data

Question

How to prevent N+1?

Answer

Use DataLoader to batch related queries

Question

What interface do resolvers implement?

Answer

ResolverInterface

Question

How do you register a resolver?

Answer

In schema.graphqls with resolver attribute

Question

What is query complexity?

Answer

A measure of how expensive a query is

Revision Notes

Key Takeaways

  • 1. Resolvers provide data for GraphQL fields
  • 2. N+1 problem occurs with individual related queries
  • 3. DataLoader batches queries for efficiency
  • 4. Query complexity limits prevent abuse
  • 5. Cache frequently accessed data

Interview Tips

  • Explain the N+1 problem and solutions
  • Know how to create resolvers
  • Discuss performance optimization
  • Be ready to implement DataLoader

Cheat Sheet

Resolver:
  implements ResolverInterface
  resolve(Field, context, info, value, args)

N+1 Prevention:
  Use DataLoader for batch loading
  Single query for all related IDs

Registration:
  <field resolver="Vendor\Resolver"/>

Performance:
  - Limit query depth
  - Use batch loading
  - Cache results