Resolver is what performs GraphQL request processing. Resolvers are bound to a specific fields in GraphQL schema. They can execute database queries, and perform other necessary calculations to return data matching the bound GraphQL schema field type. This process is called GraphQL field resolution, hence the name – GraphQL Resolver.
In the context of Magento 2, a resolver is a PHP class that implements one of the special GraphQL resolver interfaces: ResolverInterface
and BatchContractResolverInterface
. It is important to understand the use-cases of each interface in order to avoid performance degradation.
There are additional best-practices at play when implementing each resolver type.
For each request that ScandiPWA sends to a GraphQL schema, it is essential to have a dedicated resolver associated with the schema. This resolver plays a vital role in handling the request and efficiently returning the requested data to the front-end.
Resolvers are connected to a GraphQl schema field using the @resolver
directive. This directive must be added to a field in order for a resolver to get invoked for this field resolution.
For example, to bind a Magento\\CustomerGraphQl\\...
resolver to a customer field, the @resolver
directive must be added to the customer field definition. This can be done by modifying the schema in the following way:
extends type Query {
customer: Customer @resolver(class: "Magento\\\\CustomerGraphQl\\\\Model\\\\Resolver\\\\Customer") @doc(description: "The customer query returns information about a customer account")
...
}
The @resolver
directive specifies that the Magento\\CustomerGraphQl\\Model\\Resolver\\Customer
class should be used to resolve the customer
field.
A developer must create and assign a resolver for each GraphQL schema field that they will design.
A resolver plays a crucial role in the GraphQL ecosystem. It fetches and returns data that corresponds to the field type specified in the associated GraphQL schema.
<aside>
➡️ You can also include keys not defined in the schema in your resolver response. They won't be available for querying, but you may need them in child fields using the $value
.
</aside>
The following schema indicates that the HelloResolver
can include the fields a
and b
in the response. a
should be a string, and b
should be an Int
.
type HelloOutput {
a: String
b: Int
}
extends type Query {
hello: HelloOutput @resolver(class: "Vendor\\\\Module\\\\Model\\\\Resolver\\\\HelloResolver")
}
It is the resolver's responsibility to provide values that adhere to these types. Therefore, a possible response in this case would be:
// Vendor\\Module\\Model\\Resolver\\HelloResolver.php
return [
'a' => 'world',
'b' => 123
];
Note that the a
and b
fields are optional. If the resolver does not include them in the response, as in the example below, no error will be thrown and they will be defined as null
in the final response.
A field in a schema may be mandatory, which is specified by adding a !
after the field type:
type HelloOutput {
a: String!
b: Int!
}
extends type Query {
hello: HelloOutput @resolver(class: "Vendor\\\\Module\\\\Model\\\\Resolver\\\\HelloResolver")
}
Using this schema will throw an error if one of the required fields is not included in the resolver response:
// Vendor\\Module\\Model\\Resolver\\HelloResolver.php
return [
'a' => 'world',
];
// Throws error message: Cannot return null for non-nullable field
There are cases where your schema defines fields inside fields(they are nested), for example:
type SubFieldOutput {
field_3: String
}
type HelloOutput {
field_1: String
field_2: Int
fieldWithSubField: SubFieldOutput
}
extends type Query {
hello: HelloOutput @resolver(class:"…\\\\HelloResolver")
}
In this case, the subField
will also be processed by the HelloResolver
. To include it in the response, you can add another array for the specified field. For example:
// Vendor\\Module\\Model\\Resolver\\HelloResolver.php
return [
'field_1' => 'world',
'field_2' => 123,
'fieldWithSubField' => [
'field_3' => 'Resolved by HelloResolver'
]
];
It is also possible to delegate the resolution of a subfield to another resolver, by indicating it in the subfield with the @resolver
directive, for example:
type SubFieldOutput {
field_3: String
}
type HelloOutput {
field_1: String
field_2: Int
fieldWithSubField: SubFieldOutput @resolver(class:"…\\\\HelloSubFieldResolver")
}
extends type Query {
hello: HelloOutput @resolver(class:"…\\\\HelloResolver")
}
In this case, the HelloResolver
is responsible for fields field_1
and field_2
:
//HelloResolver resolve method
return [
'field_1' => 'Resolved by HelloResolver',
'field_2' => 41,
];
The HelloSubFieldResolver
is responsible for the field field_3
inside the fieldWithSubField
:
//HelloSubFieldResolver resolve method
return [
'field_3' => 'resolved by HelloSubFieldResolver',
];
A resolver is a specialized class that implements one of the GraphQL interfaces. Its purpose is to handle data retrieval and computation based on the schema's request. The resolver then returns an array of values that align with the field types specified in the schema.
Magento uses the webonyx/graphql-php
library to implement the GraphQL API. This library generates GraphQL responses by traversing fields starting from the root of the tree (fields declared on Mutation or Query) and going down until the last requested field. It calls resolvers (if bound) for each field along the way.
When the parent resolver returns an array, the child resolvers executes for each entry in the array. Since PHP execution is synchronous, each time a child field resolver is executed on an array entry, it waits for the resolution of the previous entry to complete. This behavior is not optimal, especially for fields related to products and cart items.
To address this issue, the library introduces the concept of a batch contract resolver. These resolvers "promise" to resolve all child fields on an array at once, without encountering the performance problem of fetching data in a loop.
Therefore, there are two types of resolvers:
BatchServiceContractResolverInterface
: Capable of resolving all fields on an array simultaneously.ResolverInterface
: Resolves a field for each entry in an array.<aside>
🛠 If the field being resolved is associated with an entity that is always singular (meaning you only request one instance at a time), such as a cart, the appropriate interface to use would be the ResolverInterface
.
</aside>
<aside>
🔧 If you're working with entities that can be returned in bulk, like products, cart items, or wishlist items, you can utilize the BatchServiceContractResolverInterface
.
</aside>
Having a clear understanding of these interfaces is important as they define the behavior and necessary methods for a resolver. It directly affects the performance, making it essential to comprehend the differences between them and how they operate.
This comprehensive section provides a detailed exploration of the primary approaches to effectively utilize the ResolverInterface
and BatchServiceContractResolverInterface
.
This section covers each mandatory method required for implementing these interfaces. It provides clear explanations of the purpose and functionality of each argument and its associated data access.
By studying these methods, developers can gain the necessary expertise to fully leverage the potential of these interfaces in their projects in order to fetch necessary data with high performance.
To enhance the learning experience, the section includes a wealth of illustrative examples. These practical demonstrations not only reinforce the concepts discussed but also provide valuable insights into real-world implementation scenarios.
By combining theoretical explanations with hands-on examples, this section equips developers with the confidence and skills needed to work proficiently with the ResolverInterface
and BatchServiceContractResolverInterface
, opening up new possibilities for their development endeavors.
ResolverInterface
?ScandiPWA provides a good example of implementing ResolverInterface
to clear the wishlist. For that, was created a mutation s_clearWishlist
that uses the ClearWishlist
(view source code) resolver implementing a ResolverInterface
.
GraphQL schema:
wishlist-graphql/src/etc/schema.graphqls
Resolver implementation:
wishlist-graphql/src/Model/Resolver/ClearWishlist.php
The ResolverInterface
requires the implementation of the resolve()
method, which must process the request and return data matching the schema. The resolve
method takes the following arguments:
$field
– Contains information about the current field the resolver works with(View the source code).
$context
– GraphQL passes the same instance of this interface to each field resolver. This allows the resolvers to have shared access to the same data, making implementation easier.
<aside>
➡️ This field is of the type ContextInterface
(view source code), which has a preference for the Context
(view source code) set in the module-graph-ql di.xml
file(view source code).
</aside>
It could be used, for example, to access user id:
$customerId = $context->getUserId();
It also gives access to the getExtensionAttributes
method that allows you to access extension attributes:
//The current customer is authorized(or authenticated)?
$context->getExtensionAttributes()->getIsCustomer()
$storeId = (int) $context->getExtensionAttributes()->getStore()->getId();
$baseUrl = $context->getExtensionAttributes()->getStore()->getBaseUrl();
//...
<aside>
➡️ $context->getExtensionAttributes()->getStore()
provides access to an object that implements the StoreInterface
(view source code). This class has a preference for Store
(view source code), which is defined in the store module's di.xml
(view source).
</aside>
$info
– This contains information that could be used to determine what child fields were requested, in order to optimize the data-retrieval algorithm. The object class is ResolveInfo which extends another ResolveInfo.
//The name of the field being resolved.
$fieldName = $info->fieldName;
$value
- This parameter may allow access to values returned in parent fields if there is one.
Consider the following schema:
type SubOutput {
c: String
}
type HelloOutput {
a: String
b: Int
sub: SubOutput @resolver(class:"...\\\\<SUB RESOLVER>")
}
extends type Query {
hello: HelloOutput @resolver(class:"...\\\\<HELLO RESOLVER>")
}
The <HELLO RESOLVER>
:
//<HELLO RESOLVER> resolve method
public function resolve(...arguments){
//...
return [
'a' => 'world',
'b' => 41,
'key_for_the_future' => helloModel
];
}
And in the <SUB RESOLVER>
you can access the values returned by the <HELLO RESOLVER>
.
//<SUB RESOLVER> resolve method
public function resolve(...arguments){
// you can access the fields on the parent resolver
$a = $value['a'];
// same applies to fields not defined in the schema
$helloModel = $value['key_for_the_future'];
}
This principle also applies to fields that are not defined in the schema but are returned by the parent resolver, as demonstrated in the example involving the key_for_the_future
field. To access the value of this field in the child field resolver, use the notation $value['key_for_the_future']
.
This feature is particularly beneficial for resolving child fields in product models, enabling seamless utilization of the currently resolved model via $value['model']
. One example is the IdResolver from ScandiPWA, which accesses the Wishlist
object.
<aside> ➡️ Fields not defined in the schema will not be available for querying.
</aside>
$args
– Incoming request parameters are available for access from this variable.
//access the cartID request parameter
$cartId = $args['cartId'];
//...
A resolver, which implements ResolverInterface
, must try to return an object that matches the requirements of the data structure assigned to it which is specified in the schema.graphqls
.
There is a tutorial that teaches you how to implement a new resolver using a ResolverInterface. This includes defining a query or mutation and creating the resolver:
Tutorial: How to implement a resolver using ResolverInterface
BatchServiceContractResolverInterface
?The BatchServiceContractResolverInterface
is a type of resolver that receives all requests at once and the resolution is delegated to a batch service contract(What is a service contract?).
<aside>
✅ To reduce the number of requests for child fields in every parent, the BatchServiceContractResolverInterface
collects all the necessary data at once. First, a list of all parents is gathered. Then, all the respective children are retrieved and assigned to their appropriate parents.
</aside>
The class that implements BatchServiceContractResolverInterface
is only responsible for:
getServiceContract
method.convertToServiceArgument
method.convertFromServiceResult
method.getServiceContract
method do?convertToServiceArgument
method do?convertFromServiceResult
method do?The Tutorial: How to Implement a Resolver Using BatchServiceContractResolverInterface provides an excellent example of how to use the BatchServiceContractResolverInterface
to resolve product prices.
Magento also offers an example implementation in the BatchProductLinks
resolver (view source code).
BatchProductLinks
:Tutorial: How to implement a resolver using BatchServiceContractResolverInterface