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.
Updated over 2 years ago