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