Pagination and sorting with commerce backends

📘

This is an article for advanced users of Frontastic.

When implementing pagination on your side using commercetools (or maybe another backend), you may see some products are displayed twice on different pages. The reason for this is pretty simple: If you don't provide sorting to the commerce backend while implementing pagination, the results you get back will be randomly sorted.

But don't worry we've got you covered and this article will explain how to prevent such cases from happening with Frontastic.

When you implement pagination with our streams, you'll need to modify the offset parameter of the stream and therefore switch through the pages. When no sorting is given – as is the default – the commerce backends may return the products randomly ordered. This may lead to a product showing up on page 1 but also on page 3 or any other page.

You can prevent this by setting a default sorting using our API decorators overview.

For listing products, the query() method on the product API should be used, so we'll need to implement a beforeQuery() decorator that sets the default sorting according to your needs on the ProductQuery before the query is actually executed.

Let's have a look at this short example:

namespace YourCustomerNamespace\ProductBundle\Domain;

use Frontastic\Common\ProductApiBundle\Domain\ProductApi;

class DefaultProductSortOrder
{
    public function beforeQuery(ProductApi $productApi, ProductApi\Query\ProductQuery $query): void
    {
        if (isset($query->query)) {
            // Don't change sort order in real searches
            return;
        }

        if (!$query->sortAttributes) {
            $query->sortAttributes = [
                'variants.attributes.popularity' => ProductApi\Query\ProductQuery::SORT_ORDER_DESCENDING,
                'variants.availability.availableQuantity' => ProductApi\Query\ProductQuery::SORT_ORDER_DESCENDING,
                'variants.attributes.brand_value' => ProductApi\Query\ProductQuery::SORT_ORDER_ASCENDING,
            ];
        }

        // Ensure we always get a deterministic sort order
        $query->sortAttributes += [
            'variants.sku' => ProductApi\Query\ProductQuery::SORT_ORDER_ASCENDING,
        ];
    }
}

This example will ignore searches where the query parameter is set and if no sortAttributes are set it will set the default ones. By default it'll also always add a sorting by sku to get deterministic results.

👍

Tip

Be sure to name your file the same as your ClassName, so in this example, it would be: DefaultProductSortOrder.php.

Feel free to adapt this example to your needs and add it to your codebase but you may need to adjust it to your actual available properties.

Don't forget to source the decorator properly as described in the API decorators article.

Cursor-based pagination

Cursor-based pagination is the strategy of choice in GraphQL APIs. The main idea behind this strategy is that each element of a collection has an index identifier ( cursor) and this identifier can be used to return a given amount of elements before or after a given cursor.

How navigation works in cursor-based pagination

Let's say that you want to fetch the first page with 10 elements, you should pass the following variables to the query:

'limit' => 10,
		'cursor' => null,

Here, the limit value indicates how many elements we want to query, and cursor = null indicates that you want to get the 1st page.

After this, for every request you make, the response will contain 2 properties, previousCursor and nextCursor. For example:

'previousCursor' => null,  // Indicates that there's no previous page		
		'nextCursor' => 'after:AAA_999', // The cursor ID of the last element prefixed with `after:`

Then, you can pass this cursor to the query. That way you'll be able to move forward and backward in the product list.

So if nextCursor isn't null there's another page, and if there's no previous page, previousCursor is null.

To get to the next page, you could create a new query using the value from nextCursor:

'limit' => 10,
		'cursor' => 'after:AAA_999',

Here, you're still specifying the limit number of elements to receive as well as after which element you'd want to list based on the cursor of the last element received in the previous page (AAA_999). As you can see, there's a prefix to the string after: on the cursor id. This will help the paginator to build the query to get the next page in the GraphQL format. This is handled by the backend implementation.

To navigate to a previous page, this time we need to create a query with the value from previousCursor as cursor in PaginatedQuery:

'limit' => 10,
		'cursor' => 'before:BBB_000',

If you continue querying forward the API for more products, we'll know that we are on the last page when the nextCursor value is null.

For more information on GraphQL pagination, see their site.