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:

  1. Create a service class and define a function that accepts an array as a parameter.
  2. Create the resolver that implements the BatchServiceContractResolverInterface.
  3. Update the schema field to assign the resolver to it

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

How to create a service class?

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:

  1. Getting Currency Code
  2. Creating a list of product IDs(from the list of products).
  3. Retrieve price information for products in the list from the catalog_product_index_price table.
  4. Return the desired data in the desired format, in the same order as the input argument.
<?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>

How to create a BatchServiceContractResolverInterface Resolver?

The BatchServiceContractResolverInterface requires the implementation of 3 methods:

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

How to update the schema?

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>