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:
| Strategy | Description | Example |
|---|---|---|
exact | Exact match (default) | ref=PROD-001 |
partial | Contains anywhere | title=shirt matches "T-shirt" |
start | Starts with | ref=PROD matches "PROD-001" |
end | Ends with | ref=001 matches "PROD-001" |
word_start | Word boundary start | title=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 constant | Behavior on NULL values |
|---|---|
EXCLUDE_NULL (default) | Rows where the column is NULL are excluded |
INCLUDE_NULL_BEFORE | NULL rows kept, ordered ascending |
INCLUDE_NULL_AFTER | NULL rows kept, ordered descending |
INCLUDE_NULL_BEFORE_AND_AFTER | NULL 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
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
| Parameter | Description | Default | Maximum |
|---|---|---|---|
page | Page number (1-based) | 1 | n/a |
itemsPerPage | Items per page | 30 | 100 |
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>
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
| Filter | Import |
|---|---|
| SearchFilter | Thelia\Api\Bridge\Propel\Filter\SearchFilter |
| BooleanFilter | Thelia\Api\Bridge\Propel\Filter\BooleanFilter |
| OrderFilter | Thelia\Api\Bridge\Propel\Filter\OrderFilter |
| RangeFilter | Thelia\Api\Bridge\Propel\Filter\RangeFilter |
| DateFilter | Thelia\Api\Bridge\Propel\Filter\DateFilter |
| NotInFilter | Thelia\Api\Bridge\Propel\Filter\NotInFilter |
| TheliaFilter | Thelia\Api\Bridge\Propel\Filter\CustomFilters\TheliaFilter |
| ProductPriceOrderFilter | Thelia\Api\Bridge\Propel\Filter\CustomFilters\ProductFilter\ProductPriceOrderFilter |
| DepthProductFilter | Thelia\Api\Bridge\Propel\Filter\CustomFilters\ProductFilter\DepthProductFilter |
Best practices
- Add database indexes on the columns you filter on.
- Expose only the filters a client actually needs.
- Pick the search strategy that fits how users will query the field.
- Paginate large collections.
- Cache frequent filter combinations.
Next steps
- Rate Limiting - The caps on API use
- Endpoints Reference - Complete API endpoints
- Resources - Creating API resources
- DataAccess Service - Using filters in templates