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.

How resolvers are bound to GraphQL schema?

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.

Untitled

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.

What data should a resolver return?

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>

What data should a resolver return for optional fields?

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.

What data should a resolver return for required fields?

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

What data should a resolver return for subfields?

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'
    ]
];

What data should a resolver return when a subfield’s resolution is delegated to another resolver?

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',
];

What interfaces should a resolver implement?

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.

Untitled

Therefore, there are two types of resolvers:

  1. BatchServiceContractResolverInterface: Capable of resolving all fields on an array simultaneously.
  2. 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.

How to work with resolvers?

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.

How to work with 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:

Resolver implementation:

What is the resolve() method?

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:

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

How to work with 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>

Untitled

The class that implements BatchServiceContractResolverInterface is only responsible for:

  1. Specifying the service class and method to call. This is done in the getServiceContract method.
  2. Taking the requests and transforming them into an acceptable format for the batch service contract method specified. This is done in the convertToServiceArgument method.
  3. Taking the batch service contract output and addressing the corresponding request. This is done in the convertFromServiceResult method.

Untitled

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).

Tutorial: How to implement a resolver using BatchServiceContractResolverInterface