This tutorial explains how to implement a BatchServiceContractResolverInterface using the example of product prices. It covers creating a service class, creating the resolver, and updating the schema.
Let's say you want to include a field s_price
that contains the min_price
, max_price
, and currency
. Here is an example query:
query {
products(
search: ""
pageSize: 3
) {
items {
name
sku
s_price{
min_price
max_price
final_price
regular_price
currency
}
}
}
}
This is a good example of using the BatchServiceContractResolverInterface
because it resolves the s_price
field by getting the products in batches, instead of one by one. For more information on what interfaces a resolver should implement, see What interfaces should a resolver implement?
<aside>
✅ This tutorial utilizes a module called Price
located in the Scandiweb
vendor, but you can name it whatever you prefer. (how to create a module?)
</aside>
data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==
To implement a BatchServiceContractResolverInterface
you need to:
BatchServiceContractResolverInterface
.<aside> ⚠️ To make these changes available, you need to flush the cache. If you're using CMA, run:
npm run cli
magento cache:flush
</aside>
Creating a service class is necessary for processing inputs from the resolver and returning the desired output.
Consider the necessary information to obtain the desired outcome. Do you require a list of IDs, products, or categories to fetch the desired data from the database?
To access the price information for the example, only the product IDs are needed. However, for convenience, the function will accept a list of products. To accomplish this, create a class called PriceHelper
and a public function called getPriceOfProducts
.
<aside> ➡️ Feel free to choose any name for the class and method.
</aside>
This function is responsible for:
catalog_product_index_price
table.<?php
namespace Scandiweb\\Price\\Model\\Product;
use Magento\\Customer\\Api\\Data\\GroupInterface;
use Magento\\Customer\\Model\\Session;
use Magento\\Framework\\DB\\Adapter\\AdapterInterface;
use Magento\\Store\\Model\\StoreManagerInterface;
use Magento\\Framework\\App\\ResourceConnection;
class PriceHelper {
protected StoreManagerInterface $storeManager;
protected Session $customerSession;
protected AdapterInterface $connection;
public function __construct(
StoreManagerInterface $storeManager,
ResourceConnection $resource,
Session $customerSession
) {
$this->storeManager = $storeManager;
$this->connection = $resource->getConnection();
$this->customerSession = $customerSession;
}
public function getPriceOfProducts($products): array
{
$currency = $this->storeManager->getStore()->getCurrentCurrencyCode();
$productIds = [];
foreach ($products as $product) {
$productIds[] = $product->getId();
}
$select = $this->connection
->select()
->from(
$this->connection->getTableName('catalog_product_index_price')
)->where(
'catalog_product_index_price.customer_group_id = (?)',
$this->customerSession->getCustomerGroupId() ?? GroupInterface::NOT_LOGGED_IN_ID
)->where(
'catalog_product_index_price.website_id = (?)',
$this->storeManager->getStore()->getWebsiteId()
)->where(
'catalog_product_index_price.entity_id IN (?)',
$productIds
);
$prices = $this->connection->fetchAll($select);
$indexedPrice = [];
foreach ($prices as $price) {
$indexedPrice[$price['entity_id']] = $price;
}
$resultArray = [];
foreach ($products as $product) {
$productId = $product->getId();
$minPrice = $indexedPrice[$productId]['min_price'] ?? 0;
$maxPrice = $indexedPrice[$productId]['max_price'] ?? 0;
$price = $indexedPrice[$productId]['price'] ?? 0;
$finalPrice = $indexedPrice[$productId]['final_price'] ?? 0;
// TODO: calculate tax here...
$resultArray[] = [
'min_price' => $minPrice,
'max_price' => $maxPrice,
'final_price' => $finalPrice,
'regular_price' => $price,
'currency' => $currency
];
}
return $resultArray;
}
}
If you consider the example query from the beginning of the tutorial (a GraphQL query for three products), getPriceOfProducts
would be called with $products = [product1, product2, product3]
, and the output would look something like this:
return [
//product1 prices
[
'min_price' => 1,
'max_price' => 3,
'final_price' => 2,
'regular_price' => 2,
'currency' => "USD"
],
//product2 prices
[
'min_price' => 2,
'max_price' => 4,
'final_price' => 3,
'regular_price' => 3,
'currency' => "USD"
],
//product3 prices
[
'min_price' => 3,
'max_price' => 5,
'final_price' => 4,
'regular_price' => 4,
'currency' => "USD"
],
]
<aside> ⚠️ The output should be in the same order as the input
</aside>
BatchServiceContractResolverInterface
Resolver?The BatchServiceContractResolverInterface
requires the implementation of 3 methods:
getServiceContract
must return an array with the service class name and the method responsible for processing the input.
In this example, the PriceHelper
class and the getPriceOfProducts
method are defined. Therefore, the function should return:
use Scandiweb\\Price\\Model\\Product\\PriceHelper;
//...
public function getServiceContract(): array
{
return [PriceHelper::class, 'getPriceOfProducts'];
}
convertToServiceArgument
must use the upcoming request
and transform it into the desired input item to be used in the service class.
In the given example, the getPriceOfProducts
method in the PriceHelper
service class requires the product instance associated with the request. Therefore, convertToServiceArgument
should be as follows:
public function convertToServiceArgument(ResolveRequestInterface $request)
{
$value = $request->getValue();
if (empty($value['model'])) {
throw new LocalizedException(__('"model" value should be specified'));
}
return $value['model'];
}
<aside>
➡️ $request->getValue()['model']
gives you access to the product instance. Learn more about $value
.
</aside>
If you make the example query from the beginning of the tutorial (a GraphQL query for three products), convertToServiceArgument
will be called three times - once for each product. Each call appends the result to an array, and at the end, this array looks like: [product1, product2, product3]
. Later, this array is sent to the service contract method.
convertFromServiceResult
is responsible for taking the output item of the service class method and transforming it into the desired output defined in the schema.
In this example, the getPriceOfProducts
method from the PriceHelper
class already returns data in the correct format. Therefore, the convertFromServiceResult
method simply returns the result
. If the service method returns an item that needs to be adjusted, use the $result
and $request
parameters to do so.
public function convertFromServiceResult($result, ResolveRequestInterface $request)
{
return $result;
}
The convertFromServiceResult
method will be called for each request, similar to the convertToServiceArgument
method. However, it will also include the output(response) from the contract service method associated with it.
When considering the example query with three products and the output from the service method, the convertFromServiceResult
method will be called three times. Each call will receive a different item from the output as the $result
, along with the $request
associated with the product.
The PriceResolver
class should look like this:
<?php
namespace Scandiweb\\Price\\Model\\Resolver;
use Magento\\Framework\\Exception\\LocalizedException;
use Magento\\Framework\\GraphQl\\Query\\Resolver\\BatchServiceContractResolverInterface;
use Magento\\Framework\\GraphQl\\Query\\Resolver\\ResolveRequestInterface;
use Scandiweb\\Price\\Model\\Product\\PriceHelper;
class PriceResolver implements BatchServiceContractResolverInterface
{
public function getServiceContract(): array
{
return [PriceHelper::class, 'getPriceOfProducts'];
}
public function convertToServiceArgument(ResolveRequestInterface $request)
{
$value = $request->getValue();
if (empty($value['model'])) {
throw new LocalizedException(__('"model" value should be specified'));
}
return $value['model'];
}
public function convertFromServiceResult($result, ResolveRequestInterface $request)
{
return $result;
}
}
In order to make the GraphQL request you need to define your field in the schema.graphqls
file.(how to work with schema?)
To update the schema, add your custom field and assign your newly created resolver to it, consider how you want to make the query. (What data should a resolver return?)
In the example query, the product entity should include the field s_price
that contains the min_price
, max_price
, and currency
. The schema file should look like this:
extends interface ProductInterface {
s_price: PriceOutput @resolver(class: "Scandiweb\\\\Price\\\\Model\\\\Resolver\\\\PriceResolver")
}
type PriceOutput {
min_price: Float
max_price: Float
final_price: Float
regular_price: Float
currency: String
}
This schema adds a field s_price
to the ProductInterface
, which the output should be of type PriceOutput
and the resolver associated with it is the PriceResolver
.
<aside> ✅ To make these changes available, you need to flush the cache. If you're using CMA, run:
npm run cli
magento cache:flush
</aside>