Skip to content
intermediate Phase 117 · Intermediate Projects

Project - Custom GraphQL Module

Build a custom GraphQL module with custom queries, mutations, schema types, and resolver implementations

1h 30m
0 problems
Topic Progress 0%

Module Setup and Schema Definition

Module Structure

app/code/Vendor/RmaGraphQl/
├── registration.php
├── etc/
│   ├── module.xml
│   ├── schema.graphqls
│   └── di.xml
├── GraphQl/
│   └── Resolver/
│       ├── Query/
│       │   ├── RmaResolver.php
│       │   └── RmaListResolver.php
│       └── Mutation/
│           ├── CreateRmaResolver.php
│           ├── UpdateRmaResolver.php
│           └── CancelRmaResolver.php
├── Api/
│   ├── RmaRepositoryInterface.php
│   └── Data/
│       └── RmaInterface.php
├── Model/
│   ├── RmaRepository.php
│   ├── Rma.php
│   └── ResourceModel/
│       ├── Rma.php
│       └── Collection.php
└── Model/Data/
    └── Rma.php

registration.php and module.xml

<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
    ComponentRegistrar::MODULE,
    'Vendor_RmaGraphQl',
    __DIR__
);
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Vendor_RmaGraphQl" setup_version="1.0.0">
        <sequence>
            <module name="Magento_GraphQl"/>
            <module name="Magento_Customer"/>
        </sequence>
    </module>
</config>

schema.graphqls

type Query {
    # Get single RMA by ID
    rma(id: ID!): Rma @resolver(class: "Vendor\\RmaGraphQl\\GraphQl\\Resolver\\Query\\RmaResolver")

    # Get list of RMAs
    rmaList(
        filter: RmaFilterInput
        sort: RmaSortInput
        pageSize: Int
        currentPage: Int
    ): RmaSearchResult! @resolver(class: "Vendor\\RmaGraphQl\\GraphQl\\Resolver\\Query\\RmaListResolver")
}

type Mutation {
    # Create new RMA request
    createRma(input: CreateRmaInput!): Rma! @resolver(class: "Vendor\\RmaGraphQl\\GraphQl\\Resolver\\Mutation\\CreateRmaResolver")

    # Update RMA
    updateRma(input: UpdateRmaInput!): Rma! @resolver(class: "Vendor\\RmaGraphQl\\GraphQl\\Resolver\\Mutation\\UpdateRmaResolver")

    # Cancel RMA
    cancelRma(input: CancelRmaInput!): Rma! @resolver(class: "Vendor\\RmaGraphQl\\GraphQl\\Resolver\\Mutation\\CancelRmaResolver")
}

type Rma {
    id: ID!
    order_increment_id: String!
    status: RmaStatus!
    reason: String!
    resolution: String
    items: [RmaItem!]!
    customer_note: String
    admin_note: String
    created_at: DateTime
    updated_at: DateTime
}

type RmaStatus {
    code: String!
    label: String!
}

type RmaItem {
    sku: String!
    product_name: String!
    quantity: Int!
    reason: String!
}

type RmaSearchResult {
    items: [Rma!]!
    total_count: Int
    page_info: PageInfo
}

type PageInfo {
    page_size: Int
    current_page: Int
    total_pages: Int
}

input CreateRmaInput {
    order_increment_id: String!
    items: [RmaItemInput!]!
    reason: String!
    resolution: String
    customer_note: String
}

input UpdateRmaInput {
    id: ID!
    status: String
    admin_note: String
    resolution: String
}

input CancelRmaInput {
    id: ID!
    reason: String!
}

input RmaItemInput {
    sku: String!
    quantity: Int!
    reason: String!
}

input RmaFilterInput {
    order_increment_id: FilterEqualTypeInput
    status: FilterEqualTypeInput
    created_at: FilterRangeTypeInput
}

input RmaSortInput {
    created_at: SortOrder
    status: SortOrder
}

enum SortOrder {
    ASC
    DESC
}

Query Resolvers

Single Item Resolver

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

use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\GraphQl\Resolver\ResolverInterface;
use Vendor\RmaGraphQl\Api\RmaRepositoryInterface;
use Magento\Framework\Exception\NoSuchEntityException;

class RmaResolver implements ResolverInterface
{
    public function __construct(
        private RmaRepositoryInterface $rmaRepository,
    ) {
    }

    public function resolve(
        Field $field,
        $context,
        ResolveInfo $info,
        array $value = null,
        array $args = null
    ) {
        $customerId = $context->getExtensionAttributes()->getCustomerGraphQlContext()->getCustomerId();

        if (!$customerId) {
            throw new \Magento\Framework\GraphQl\Exception\GraphQlInputException(
                __('Customer authentication required')
            );
        }

        $id = (int) $args['id'];
        $rma = $this->rmaRepository->getById($id);

        if ($rma->getCustomerId() !== $customerId) {
            throw new \Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException(
                __('You are not authorized to view this RMA')
            );
        }

        return $this->formatRma($rma);
    }

    private function formatRma($rma): array
    {
        return [
            'id' => $rma->getId(),
            'order_increment_id' => $rma->getOrderIncrementId(),
            'status' => [
                'code' => $rma->getStatus(),
                'label' => $this->getStatusLabel($rma->getStatus()),
            ],
            'reason' => $rma->getReason(),
            'resolution' => $rma->getResolution(),
            'items' => $this->formatItems($rma->getItems()),
            'customer_note' => $rma->getCustomerNote(),
            'admin_note' => $rma->getAdminNote(),
            'created_at' => $rma->getCreatedAt(),
            'updated_at' => $rma->getUpdatedAt(),
        ];
    }

    private function getStatusLabel(string $code): string
    {
        return match($code) {
            'pending' => 'Pending Review',
            'approved' => 'Approved',
            'processing' => 'Processing',
            'completed' => 'Completed',
            'cancelled' => 'Cancelled',
            default => $code,
        };
    }

    private function formatItems(array $items): array
    {
        return array_map(function ($item) {
            return [
                'sku' => $item->getSku(),
                'product_name' => $item->getProductName(),
                'quantity' => $item->getQuantity(),
                'reason' => $item->getReason(),
            ];
        }, $items);
    }
}

List Resolver with Pagination

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

use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\GraphQl\Resolver\ResolverInterface;
use Vendor\RmaGraphQl\Api\RmaRepositoryInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;

class RmaListResolver implements ResolverInterface
{
    public function __construct(
        private RmaRepositoryInterface $rmaRepository,
        private SearchCriteriaBuilder $searchCriteriaBuilder,
    ) {
    }

    public function resolve(
        Field $field,
        $context,
        ResolveInfo $info,
        array $value = null,
        array $args = null
    ) {
        $customerId = $context->getExtensionAttributes()->getCustomerGraphQlContext()->getCustomerId();

        if (!$customerId) {
            throw new \Magento\Framework\GraphQl\Exception\GraphQlInputException(
                __('Customer authentication required')
            );
        }

        $searchCriteriaBuilder = $this->searchCriteriaBuilder
            ->addFilter('customer_id', $customerId);

        if (isset($args['filter'])) {
            $this->applyFilters($searchCriteriaBuilder, $args['filter']);
        }

        if (isset($args['sort'])) {
            $this->applySortOrders($searchCriteriaBuilder, $args['sort']);
        }

        $pageSize = $args['pageSize'] ?? 20;
        $currentPage = $args['currentPage'] ?? 1;

        $searchCriteriaBuilder
            ->setPageSize($pageSize)
            ->setCurrentPage($currentPage);

        $searchCriteria = $searchCriteriaBuilder->create();
        $result = $this->rmaRepository->getList($searchCriteria);

        $items = array_map([$this, 'formatRma'], $result->getItems());
        $totalPages = (int) ceil($result->getTotalCount() / $pageSize);

        return [
            'items' => $items,
            'total_count' => $result->getTotalCount(),
            'page_info' => [
                'page_size' => $pageSize,
                'current_page' => $currentPage,
                'total_pages' => $totalPages,
            ],
        ];
    }

    private function applyFilters(SearchCriteriaBuilder $builder, array $filters): void
    {
        foreach ($filters as $field => $filter) {
            if (isset($filter['eq'])) {
                $builder->addFilter($field, $filter['eq']);
            }
            if (isset($filter['like'])) {
                $builder->addFilter($field, '%' . $filter['like'] . '%', 'like');
            }
            if (isset($filter['from']) && isset($filter['to'])) {
                $builder->addFilter($field, [$filter['from'], $filter['to']], 'range');
            }
        }
    }

    private function applySortOrders(SearchCriteriaBuilder $builder, array $sortOrders): void
    {
        foreach ($sortOrders as $field => $direction) {
            $builder->addSortOrder($field, $direction);
        }
    }
}

Mutation Resolvers

Create RMA Mutation

<?php
namespace Vendor\RmaGraphQl\GraphQl\Resolver\Mutation;

use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\GraphQl\Resolver\ResolverInterface;
use Vendor\RmaGraphQl\Api\RmaRepositoryInterface;
use Vendor\RmaGraphQl\Api\Data\RmaInterfaceFactory;
use Vendor\RmaGraphQl\Model\Data\RmaItemFactory;

class CreateRmaResolver implements ResolverInterface
{
    public function __construct(
        private RmaRepositoryInterface $rmaRepository,
        private RmaInterfaceFactory $rmaFactory,
        private RmaItemFactory $rmaItemFactory,
    ) {
    }

    public function resolve(
        Field $field,
        $context,
        ResolveInfo $info,
        array $value = null,
        array $args = null
    ) {
        $customerId = $context->getExtensionAttributes()->getCustomerGraphQlContext()->getCustomerId();

        if (!$customerId) {
            throw new \Magento\Framework\GraphQl\Exception\GraphQlInputException(
                __('Customer authentication required')
            );
        }

        $input = $args['input'];

        // Validate input
        $this->validateInput($input);

        // Verify order belongs to customer
        $this->verifyOrderOwnership($input['order_increment_id'], $customerId);

        // Create RMA
        $rma = $this->rmaFactory->create();
        $rma->setCustomerId($customerId);
        $rma->setOrderIncrementId($input['order_increment_id']);
        $rma->setReason($input['reason']);
        $rma->setResolution($input['resolution'] ?? null);
        $rma->setCustomerNote($input['customer_note'] ?? null);
        $rma->setStatus('pending');

        // Add items
        $items = [];
        foreach ($input['items'] as $itemData) {
            $item = $this->rmaItemFactory->create();
            $item->setSku($itemData['sku']);
            $item->setProductName($this->getProductName($itemData['sku']));
            $item->setQuantity($itemData['quantity']);
            $item->setReason($itemData['reason']);
            $items[] = $item;
        }
        $rma->setItems($items);

        $savedRma = $this->rmaRepository->save($rma);

        return $this->formatRma($savedRma);
    }

    private function validateInput(array $input): void
    {
        if (empty($input['order_increment_id'])) {
            throw new \Magento\Framework\GraphQl\Exception\GraphQlInputException(
                __('Order increment ID is required')
            );
        }

        if (empty($input['items'])) {
            throw new \Magento\Framework\GraphQl\Exception\GraphQlInputException(
                __('At least one item is required')
            );
        }

        foreach ($input['items'] as $item) {
            if ($item['quantity'] <= 0) {
                throw new \Magento\Framework\GraphQl\Exception\GraphQlInputException(
                    __('Quantity must be greater than 0 for SKU: %1', $item['sku'])
                );
            }
        }
    }

    private function formatRma($rma): array
    {
        return [
            'id' => $rma->getId(),
            'order_increment_id' => $rma->getOrderIncrementId(),
            'status' => ['code' => $rma->getStatus(), 'label' => ucfirst($rma->getStatus())],
            'reason' => $rma->getReason(),
            'items' => array_map(fn($item) => [
                'sku' => $item->getSku(),
                'product_name' => $item->getProductName(),
                'quantity' => $item->getQuantity(),
                'reason' => $item->getReason(),
            ], $rma->getItems()),
            'created_at' => $rma->getCreatedAt(),
        ];
    }
}

Update and Cancel Mutations

// UpdateRmaResolver.php
public function resolve(
    Field $field,
    $context,
    ResolveInfo $info,
    array $value = null,
    array $args = null
) {
    $customerId = $context->getExtensionAttributes()->getCustomerGraphQlContext()->getCustomerId();
    $input = $args['input'];
    $id = (int) $input['id'];

    $rma = $this->rmaRepository->getById($id);

    if ($rma->getCustomerId() !== $customerId) {
        throw new \Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException(
            __('You are not authorized to update this RMA')
        );
    }

    if ($rma->getStatus() === 'completed' || $rma->getStatus() === 'cancelled') {
        throw new \Magento\Framework\GraphQl\Exception\GraphQlInputException(
            __('Cannot update completed or cancelled RMA')
        );
    }

    if (isset($input['admin_note'])) {
        $rma->setAdminNote($input['admin_note']);
    }

    $savedRma = $this->rmaRepository->save($rma);
    return $this->formatRma($savedRma);
}

// CancelRmaResolver.php
public function resolve(
    Field $field,
    $context,
    ResolveInfo $info,
    array $value = null,
    array $args = null
) {
    $customerId = $context->getExtensionAttributes()->getCustomerGraphQlContext()->getCustomerId();
    $input = $args['input'];
    $id = (int) $input['id'];

    $rma = $this->rmaRepository->getById($id);

    if ($rma->getCustomerId() !== $customerId) {
        throw new \Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException(
            __('You are not authorized to cancel this RMA')
        );
    }

    $rma->setStatus('cancelled');
    $rma->setAdminNote('Cancelled by customer: ' . $input['reason']);

    $savedRma = $this->rmaRepository->save($rma);
    return $this->formatRma($savedRma);
}

Testing and Error Handling

GraphQL Query Testing

# Query single RMA
query {
  rma(id: 1) {
    id
    order_increment_id
    status {
      code
      label
    }
    reason
    items {
      sku
      product_name
      quantity
      reason
    }
    created_at
  }
}

# Query RMA list with filters
query {
  rmaList(
    filter: { status: { eq: "pending" } }
    sort: { created_at: DESC }
    pageSize: 10
    currentPage: 1
  ) {
    items {
      id
      order_increment_id
      status { code label }
      created_at
    }
    total_count
    page_info {
      current_page
      total_pages
    }
  }
}

# Create RMA mutation
mutation {
  createRma(input: {
    order_increment_id: "000000100"
    items: [
      { sku: "TSHIRT-001", quantity: 2, reason: "Wrong size" }
    ]
    reason: "Received wrong size"
    customer_note: "Please exchange for size M"
  }) {
    id
    status { code label }
    items { sku quantity }
    created_at
  }
}

# Cancel RMA mutation
mutation {
  cancelRma(input: {
    id: 1
    reason: "Changed my mind"
  }) {
    id
    status { code label }
  }
}

Error Response Handling

// GraphQl errors are returned in the errors array:
{
  "data": null,
  "errors": [
    {
      "message": "Customer authentication required",
      "locations": [{ "line": 2, "column": 3 }],n      "path": ["rma"],
      "extensions": {
        "category": "graphql-input"
      }
    }
  ]
}

Integration Test

<?php
namespace Vendor\RmaGraphQl\Test\GraphQl;

use Magento\GraphQl\Module\Manager as GraphQlManager;
use PHPUnit\Framework\TestCase;

class RmaGraphQlTest extends TestCase
{
    public function testRmaQueryWithoutAuthThrowsError(): void
    {
        $query = '{ rma(id: 1) { id status { code } } }';
        // Should return authorization error
    }

    public function testCreateRmaMutation(): void
    {
        $mutation = 'mutation {
            createRma(input: {
                order_increment_id: "000000100"
                items: [{ sku: "TSHIRT-001", quantity: 1, reason: "Defective" }]
                reason: "Product defective"
            }) {
                id
                status { code }
            }
        }';
        // Should create RMA and return data
    }
}

Quiz

1. Where is the GraphQL schema defined for a module?

Question 1 options

2. What interface must GraphQL resolvers implement?

Question 2 options

3. How do you pass arguments to a GraphQL resolver?

Question 3 options

Flashcards

Question

Where is the GraphQL schema file?

Answer

etc/schema.graphqls in the module directory

Question

How do you define a custom query?

Answer

type Query { fieldName(args): ReturnType @resolver(class: "...") }

Question

How do you define a mutation?

Answer

type Mutation { fieldName(input: InputType!): ReturnType }

Question

What is the resolver method signature?

Answer

resolve(Field $field, $context, ResolveInfo $info, array $value, array $args)

Question

How do you add pagination?

Answer

Add pageSize and currentPage arguments, return PageInfo type

Revision Notes

Key Takeaways

  • 1. schema.graphqls defines all types, queries, mutations, and inputs
  • 2. Resolvers implement ResolverInterface with resolve() method
  • 3. Queries use @resolver attribute to map to resolver classes
  • 4. Mutations accept Input types and return modified types
  • 5. Authentication uses context to get customer ID

Interview Tips

  • Explain the difference between REST and GraphQL in Magento
  • Describe how to design a GraphQL schema for a business entity
  • Discuss error handling patterns in GraphQL resolvers
  • Talk about pagination and filtering in GraphQL queries

Cheat Sheet

GraphQL Module:
  etc/schema.graphqls → Schema
  GraphQl/Resolver/ → Resolver classes
  @resolver(class: "...\Resolver") → Maps to resolver

Schema Types:
  type Query { ... } → Read operations
  type Mutation { ... } → Write operations
  input InputType { ... } → Mutation arguments
  type ObjectType { ... } → Response types

Resolver:
  resolve(field, context, info, value, args)
  Return array matching schema type

Error Handling:
  GraphQlInputException → Input validation
  GraphQlAuthorizationException → Access denied
  GraphQlNoSuchEntityException → Not found

Pagination:
  pageSize, currentPage arguments
  PageInfo { current_page, total_pages, page_size }