Skip to main content
Version: Next

Filters & Pagination

Thelia's API supports filtering, sorting, and pagination through API Platform's filter system, with custom Propel adapters.

Basic usage​

Filters are applied via query parameters:

GET /api/front/products?visible=true&brand.id=5&order[position]=asc&page=2&itemsPerPage=20

Available filters​

SearchFilter​

Exact and partial matching on text fields.

#[ApiFilter(
filterClass: SearchFilter::class,
properties: [
'ref', // Exact match
'title' => 'word_start', // Starts with (word boundary)
'productCategories.category.id', // Related entity ID
'brand.id', // Related entity ID
],
)]

Usage:

# Exact match
GET /api/admin/products?ref=PROD-001

# Partial match (word_start)
GET /api/admin/products?title=shirt

# Related entity
GET /api/front/products?brand.id=5
GET /api/front/products?productCategories.category.id=10

Match strategies:

StrategyDescriptionExample
exactExact match (default)ref=PROD-001
partialContains anywheretitle=shirt matches "T-shirt"
startStarts withref=PROD matches "PROD-001"
endEnds withref=001 matches "PROD-001"
word_startWord boundary starttitle=blue matches "Blue shirt"

BooleanFilter​

Filter on boolean fields.

#[ApiFilter(
filterClass: BooleanFilter::class,
properties: [
'visible',
'virtual',
'productCategories.defaultCategory',
'productSaleElements.isDefault',
'productSaleElements.promo',
'productSaleElements.newness',
],
)]

Usage:

# Direct boolean
GET /api/front/products?visible=true
GET /api/front/products?virtual=false

# Related boolean
GET /api/front/products?productSaleElements.promo=true
GET /api/front/products?productSaleElements.newness=true

OrderFilter​

Sort results by field.

#[ApiFilter(
filterClass: OrderFilter::class,
properties: [
'ref',
'position',
'createdAt',
'productCategories.position',
'productCategories.createdAt',
],
)]

Usage:

# Single field
GET /api/front/products?order[position]=asc
GET /api/admin/products?order[createdAt]=desc

# Multiple fields
GET /api/front/products?order[position]=asc&order[ref]=asc

# Related field
GET /api/front/products?order[productCategories.position]=asc

# Products most recently added to a category first
GET /api/front/products?productCategories.category.id=10&order[productCategories.createdAt]=desc

productCategories.position and productCategories.createdAt sort on the link between a product and a category. They apply together with the productCategories.category.id filter, in the same request. productCategories.createdAt is the date the product was put in the category, which is not the creation date of the product. A product created years ago can have been added to a range today. With the depth filter, the date read is the one of the link to the listed category or branch, never a link to a category outside it.

RangeFilter​

Filter by numeric ranges.

#[ApiFilter(
filterClass: RangeFilter::class,
properties: [
'productSaleElements.productPrices.price',
'productSaleElements.productPrices.promoPrice',
'productSaleElements.weight',
'productSaleElements.quantity',
],
)]

Usage:

# Greater than
GET /api/front/products?productSaleElements.productPrices.price[gt]=50

# Less than
GET /api/front/products?productSaleElements.productPrices.price[lt]=100

# Greater than or equal
GET /api/front/products?productSaleElements.productPrices.price[gte]=10

# Less than or equal
GET /api/front/products?productSaleElements.productPrices.price[lte]=100

# Between (combine gte and lte)
GET /api/front/products?productSaleElements.productPrices.price[gte]=10&productSaleElements.productPrices.price[lte]=100

DateFilter​

Filter on TIMESTAMP / date columns by range. The operators are before, after, strictly_before and strictly_after (not gte/lte).

// core/lib/Thelia/Api/Resource/Order.php
#[ApiFilter(
filterClass: DateFilter::class,
properties: [
'createdAt' => DateFilter::INCLUDE_NULL_BEFORE_AND_AFTER,
'updatedAt' => DateFilter::INCLUDE_NULL_BEFORE_AND_AFTER,
],
)]

The value mapped to each property is a null-handling strategy:

Strategy constantBehavior on NULL values
EXCLUDE_NULL (default)Rows where the column is NULL are excluded
INCLUDE_NULL_BEFORENULL rows kept, ordered ascending
INCLUDE_NULL_AFTERNULL rows kept, ordered descending
INCLUDE_NULL_BEFORE_AND_AFTERNULL rows always kept

Usage:

# On or before a date (LESS_EQUAL)
GET /api/admin/orders?createdAt[before]=2026-01-31

# On or after a date (GREATER_EQUAL)
GET /api/admin/orders?createdAt[after]=2026-01-01

# Strictly before / after (excludes the boundary)
GET /api/admin/orders?createdAt[strictly_before]=2026-02-01
GET /api/admin/orders?createdAt[strictly_after]=2025-12-31

# Between two dates (combine after + before)
GET /api/admin/orders?createdAt[after]=2026-01-01&createdAt[before]=2026-01-31
note

before and after are inclusive (<= / >=). Use strictly_before and strictly_after for exclusive comparisons (< / >).

NotInFilter​

Exclude specific values.

#[ApiFilter(
filterClass: NotInFilter::class,
properties: [
'id',
'ref',
'productCategories.category.id',
],
)]

Usage:

# Exclude IDs
GET /api/front/products?id[not_in]=1,2,3

# Exclude categories
GET /api/front/products?productCategories.category.id[not_in]=5,10

# Exclude refs
GET /api/admin/products?ref[not_in]=HIDDEN-001,HIDDEN-002

TheliaFilter​

Custom Thelia-specific filters (varies by resource).

#[ApiFilter(
filterClass: TheliaFilter::class,
)]

Custom filters​

ProductPriceOrderFilter​

Thelia\Api\Bridge\Propel\Filter\CustomFilters\ProductFilter\ProductPriceOrderFilter

Sort products by untaxed price. The query parameter is untaxed_price_order, with asc or desc as accepted values.

GET /api/front/products?untaxed_price_order=asc
GET /api/front/products?untaxed_price_order=desc

DepthProductFilter​

Thelia\Api\Bridge\Propel\Filter\CustomFilters\ProductFilter\DepthProductFilter

Filter by category depth.

GET /api/front/products?depth=2&productCategories.category.id=5

Pagination​

Query parameters​

ParameterDescriptionDefaultMaximum
pagePage number (1-based)1n/a
itemsPerPageItems per page30100

Usage:

GET /api/front/products?page=2&itemsPerPage=20

The page ceiling​

A page never returns more than 100 items, whatever itemsPerPage asks for. itemsPerPage=100000 returns 100, and hydra:totalItems still reports the real size of the collection, so a client walks the pages instead of asking for everything at once.

Without a ceiling, a single anonymous call can make the shop load, hydrate and serialize a whole table into one response. A hundred is well above what the shipped themes ask for: thirty on the front, twenty-five in the back-office.

Change it for the whole API in your own configuration:

# config/packages/api_platform.yaml
api_platform:
defaults:
pagination_maximum_items_per_page: 200

Or for one operation, on its metadata:

#[GetCollection(paginationMaximumItemsPerPage: 500)]

See Rate Limiting for the other caps on API use.

Response format​

By default, the API returns a simple JSON array:

[
{"id": 1, "ref": "PROD-001", ...},
{"id": 2, "ref": "PROD-002", ...}
]

To get pagination metadata (total items, page info), use JSON-LD format with Accept: application/ld+json header:

{
"@context": "/api/contexts/Product",
"@id": "/api/front/products",
"@type": "hydra:Collection",
"hydra:totalItems": 150,
"hydra:member": [...],
"hydra:view": {
"@id": "/api/front/products?page=2",
"@type": "hydra:PartialCollectionView",
"hydra:first": "/api/front/products?page=1",
"hydra:last": "/api/front/products?page=8",
"hydra:previous": "/api/front/products?page=1",
"hydra:next": "/api/front/products?page=3"
}
}

Accessing pagination in Twig​

The Twig resources() function returns a simple array by default. For pagination, use itemsPerPage and page parameters:

{% set products = resources('/api/front/products', {
'productCategories.category.id': categoryId,
'itemsPerPage': 20,
'page': currentPage
}) %}

<div class="products">
{% for product in products %}
{{ include('partials/product-card.html.twig', {product: product}) }}
{% endfor %}
</div>

{# Simple pagination without total count #}
<nav class="pagination">
{% if currentPage > 1 %}
<a href="?page={{ currentPage - 1 }}">Previous</a>
{% endif %}

{% if products|length == 20 %}
<a href="?page={{ currentPage + 1 }}">Next</a>
{% endif %}
</nav>
Pagination with Total Count

For pagination with total item count, use DataAccessService with 'jsonld' format in a PHP service or LiveComponent, as the Twig function doesn't support JSON-LD format.

Combining filters​

Filters can be combined:

GET /api/front/products?visible=true&productCategories.category.id=5&productSaleElements.promo=true&productSaleElements.productPrices.price[gte]=10&productSaleElements.productPrices.price[lte]=100&order[position]=asc&itemsPerPage=20

In Twig:

{% set products = resources('/api/front/products', {
'visible': true,
'productCategories.category.id': categoryId,
'productSaleElements.promo': true,
'productSaleElements.productPrices.price[gte]': '10',
'productSaleElements.productPrices.price[lte]': '100',
'order[position]': 'asc',
'itemsPerPage': 20
}) %}

Adding filters to resources​

On the resource class​

use ApiPlatform\Metadata\ApiFilter;
use Thelia\Api\Bridge\Propel\Filter\SearchFilter;
use Thelia\Api\Bridge\Propel\Filter\BooleanFilter;
use Thelia\Api\Bridge\Propel\Filter\OrderFilter;

#[ApiFilter(
filterClass: SearchFilter::class,
properties: ['ref', 'title' => 'word_start'],
)]
#[ApiFilter(
filterClass: BooleanFilter::class,
properties: ['visible', 'active'],
)]
#[ApiFilter(
filterClass: OrderFilter::class,
properties: ['position', 'createdAt'],
)]
class MyResource implements PropelResourceInterface
{
// ...
}

Per operation​

new GetCollection(
uriTemplate: '/front/products',
filters: [
'api_platform.filter.search',
'api_platform.filter.boolean',
],
)

Filter classes reference​

FilterImport
SearchFilterThelia\Api\Bridge\Propel\Filter\SearchFilter
BooleanFilterThelia\Api\Bridge\Propel\Filter\BooleanFilter
OrderFilterThelia\Api\Bridge\Propel\Filter\OrderFilter
RangeFilterThelia\Api\Bridge\Propel\Filter\RangeFilter
DateFilterThelia\Api\Bridge\Propel\Filter\DateFilter
NotInFilterThelia\Api\Bridge\Propel\Filter\NotInFilter
TheliaFilterThelia\Api\Bridge\Propel\Filter\CustomFilters\TheliaFilter
ProductPriceOrderFilterThelia\Api\Bridge\Propel\Filter\CustomFilters\ProductFilter\ProductPriceOrderFilter
DepthProductFilterThelia\Api\Bridge\Propel\Filter\CustomFilters\ProductFilter\DepthProductFilter

Best practices​

  1. Add database indexes on the columns you filter on.
  2. Expose only the filters a client actually needs.
  3. Pick the search strategy that fits how users will query the field.
  4. Paginate large collections.
  5. Cache frequent filter combinations.

Next steps​