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?
2. What interface must GraphQL resolvers implement?
3. How do you pass arguments to a GraphQL resolver?
Flashcards
Question
Where is the GraphQL schema file?
Click to reveal answer
Answer
etc/schema.graphqls in the module directory
Question
How do you define a custom query?
Click to reveal answer
Answer
type Query { fieldName(args): ReturnType @resolver(class: "...") }
Question
How do you define a mutation?
Click to reveal answer
Answer
type Mutation { fieldName(input: InputType!): ReturnType }
Question
What is the resolver method signature?
Click to reveal answer
Answer
resolve(Field $field, $context, ResolveInfo $info, array $value, array $args)
Question
How do you add pagination?
Click to reveal answer
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 }