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',
],
)]

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

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

ParameterDescriptionDefault
pagePage number (1-based)1
itemsPerPageItems per page30

Usage:

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

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