For further documentation on filters (including for Eloquent and Elasticsearch), please see the Filters documentation.
For maximum flexibility and to ensure future compatibility, it is strongly recommended
to configure your filters via the parameters attribute using
QueryParameter. The legacy method using theApiFilterattribute is not recommended.
The modern way to declare filters is to associate them directly with an operation’s parameters. This allows for more precise control over the exposed properties.
Here is the recommended approach to apply a PartialSearchFilter only to the title and author
properties of a Book resource.
<?php
// api/src/Resource/Book.php
#[ApiResource(operations: [
new GetCollection(
parameters: [
// This WILL restrict to only title and author properties
'search[:property]' => new QueryParameter(
properties: ['title', 'author'], // Only these properties get parameters created
filter: new PartialSearchFilter()
)
]
)
])]
class Book {
// ...
}This filter can be also defined directly on a specific operation like
#[GetCollection(...)])for finer control, like the following code:
<?php
// api/src/Resource/Book.php
#[GetCollection(
parameters: [
// This WILL restrict to only title and author properties
'search[:property]' => new QueryParameter(
properties: ['title', 'author'], // Only these properties get parameters created
filter: new PartialSearchFilter()
)
]
)]
class Book {
// ...
}Filters are services (see the section on custom filters), the can be linked to an API Platform Operation through parameters:
For example, having a filter service declaration in services.yaml:
# api/config/filters.yaml
services:
offer.date_filter:
parent: "api_platform.doctrine.orm.date_filter"
arguments: [{ dateProperty: ~ }]
tags: ["api_platform.filter"]We’re linking the filter offer.date_filter with the resource like this:
<?php
// api/src/Entity/Offer.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
#[GetCollection(parameters: ['date' => new QueryParameter(filter: 'offer.date_filter')])]
class Offer
{
// ...
}Its discouraged to use a filter with properties in the dependency injection as it may
conflict with how
QueryParameterworks. We recommend to use a per-parameter filter or to use the :property placeholder with a definedfilterContextspecifying your strategy for a given set of parameters.
Since API platform 4.2 we’re allowing singleton objects, indeed a filter now acts on a single
parameter associated with a single scalar value (or a list). You may use the
:property placeholder)
<?php
// api/src/ApiResource/Offer.php
namespace App\ApiResource;
#[GetCollection(
parameters: [
'createdAt' => new QueryParameter(
filter: new DateFilter(),
),
],
)]
class Offer
{
// ...
}For MongoDB ODM, all the filters are in the namespace ApiPlatform\Doctrine\Odm\Filter. The filter
services all begin with api_platform.doctrine_mongodb.odm.
The SearchFilter is a multi-type filter that may have inconsistencies (eg: you can
search a partial date with LIKE) we recommend to use type-specific filters such as
PartialSearchFilterorDateFilterinstead.
To add some search filters, choose over this new list:
LIKE %value%; supports nested
properties via dot notation)orWhere instead of andWhere )gt, gte, lt, lte, ne operators to an
equality or UUID filter)If Doctrine ORM or MongoDB ODM support is enabled, using the search filter service requires you to
registering a filter service in the api/config/services.yaml file and adding an attribute to your
resource configuration:
app_search_filter_via_parameter:
parent: "api_platform.doctrine.orm.search_filter"
arguments: [{ "id": "exact", "price": "exact", "description": "partial" }] # Declare strategies for each property
tags: [{ name: "api_platform.filter", id: "app_search_filter_via_parameter" }]The search filter supports exact, partial, start, end, and word_start matching strategies:
partial strategy uses LIKE %text% to search for fields that contain text.start strategy uses LIKE text% to search for fields that start with text.end strategy uses LIKE %text to search for fields that end with text.word_start strategy uses LIKE text% OR LIKE % text% to search for fields that contain words
starting with text.Prepend the letter i to the filter if you want it to be case insensitive. For example ipartial
or iexact. Note that this will use the LOWER function and will impact performance
as described in the performance documentation.
Case insensitivity may already be enforced at the database level depending on the
collation used. If you are using MySQL, note that the
commonly used utf8_unicode_ci collation (and its sibling utf8mb4_unicode_ci) are already
case-insensitive, as indicated by the _ci part in their names.
Note: Search filters with the exact strategy can have multiple values for the same property (in
this case the condition will be similar to a SQL IN clause).
Syntax: ?property[]=foo&property[]=bar
In the following example, we will see how to allow the filtering of e-commerce offers (a list):
<?php
// api/src/Entity/Offer.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
#[GetCollection(
parameters: [
new QueryParameter(
filter: 'app_search_filter_via_parameter', // the previously declared filter
properties: ['id', 'price', 'description'],
)
]
)]
class Offer
{
// ...
}http://localhost:8000/api/offers?price=10 will return all offers with a price being exactly 10.
http://localhost:8000/api/offers?description=shirt will return all offers with a description
containing the word “shirt”.
Filters can be combined: http://localhost:8000/api/offers?price=10&description=shirt
The IRI filter allows filtering a resource using IRIs.
Syntax: ?property=value
The value can take any IRI (Internationalized Resource Identifier).
This filter can be used on the ApiResource attribute or in the operation attribute, for e.g., the
#GetCollection() attribute:
// api/src/ApiResource/Chicken.php
#[GetCollection(
parameters: [
'chickenCoop' => new QueryParameter(filter: new IriFilter()),
],
)]
class Chicken
{
//...
}Given that the endpoint is /chickens, you can filter chickens by chicken coop with the following
query: /chickens?chickenCoop=/chickenCoop/1.
It will return all the chickens that live in chicken coop number 1.
IriFilter supports filtering through nested associations using dot notation in the property
argument. See Filtering on Nested Properties.
The exact filter allows filtering a resource using exact values.
Syntax: ?property=value
The value can take any scalar value or array of values.
This filter can be used on the ApiResource attribute or in the operation attribute, for e.g., the
#GetCollection() attribute:
// api/src/ApiResource/Chicken.php
#[GetCollection(
parameters: [
'name' => new QueryParameter(filter: new ExactFilter()),
],
)]
class Chicken
{
//...
}Given that the endpoint is /chickens, you can filter chickens by name with the following query:
/chikens?name=Gertrude.
It will return all the chickens that are exactly named Gertrude.
ExactFilter supports filtering on nested properties using dot notation in the property argument.
See Filtering on Nested Properties.
The partial search filter allows filtering a resource using partial values.
Syntax: ?property=value
The value can take any scalar value or array of values.
This filter can be used on the ApiResource attribute or in the operation attribute, for e.g., the
#GetCollection() attribute:
// api/src/ApiResource/Chicken.php
#[GetCollection(
parameters: [
'name' => new QueryParameter(filter: new PartialSearchFilter()),
],
)]
class Chicken
{
//...
}Given that the endpoint is /chickens, you can filter chickens by name with the following query:
/chikens?name=tom.
It will return all chickens where the name contains the substring tom.
This filter performs a case-insensitive search. It automatically normalizes both the input
value and the stored data (for e.g., by converting them to lowercase) before making the comparison.
PartialSearchFilter supports searching on nested properties using dot notation in the property
argument. See Filtering on Nested Properties.
The free text query filter allows filtering allows you to apply a single filter across a list of properties. Its primary role is to repeat a filter’s logic for each specified field.
Syntax: ?property=value
The value can take any scalar value or array of values.
This filter can be used on the ApiResource attribute or in the operation attribute, for e.g., the
#GetCollection() attribute:
// api/src/ApiResource/Chicken.php
#[GetCollection(
parameters: [
'q' => new QueryParameter(
filter: new FreeTextQueryFilter(new PartialSearchFilter()),
properties: ['name', 'ean']
),
],
)]
class Chicken
{
//...
}Given that the endpoint is /chickens, you can filter chickens by name with the following query:
/chikens?q=tom.
Result:
This request will return all chickens where:
name is exactly “FR123456”ean is exactly “FR123456”.For the OR option refer to the OrFilter.
The or filter allows you to explicitly change the logical condition used by the filter it wraps. Its sole purpose is to force a filter to combine its criteria with OR instead of the default AND.
It’s the ideal tool for creating a search parameter that should find a match in any of the specified fields, but not necessarily all of them.
Syntax: ?property=value
The value can take any scalar value or array of values.
The OrFilter is a decorator: it is used by “wrapping” another, more specific filter (like for e.g.
PartialSearchFilter or ExactFilter).
The real power emerges when you combine these decorators. For instance, to create an “autocomplete” feature that finds exact matches in one of several fields. Example of usage:
// api/src/ApiResource/Chicken.php
#[GetCollection(
parameters: [
'autocomplete' => new QueryParameter(
filter: new FreeTextQueryFilter(new OrFilter(new ExactFilter())),
properties: ['name', 'ean']
),
],
)]
class Chicken
{
//...
}Given that the endpoint is /chickens, you can filter chickens by name with the following query:
/chikens?autocomplete=tom.
Result:
This request will return all chickens where:
name is exactly “FR123456”ean is exactly “FR123456”.
ComparisonFilteris experimental and its API may change before a stable release.
The comparison filter is a decorator that wraps an equality filter (such as ExactFilter) and adds
comparison operators to it. It lets clients filter a collection using greater-than,
greater-than-or-equal, less-than, less-than-or-equal, and not-equal comparisons on any filterable
property.
Syntax: ?parameter[<gt|gte|lt|lte|ne>]=value
Available operators:
| Operator | SQL equivalent | Description |
|---|---|---|
gt | > | Strictly greater than |
gte | >= | Greater than or equal to |
lt | < | Strictly less than |
lte | <= | Less than or equal to |
ne | != | Not equal to |
ComparisonFilter is a decorator: it is applied by wrapping another filter. The canonical pairing
is with ExactFilter for standard properties, or with UuidFilter for UUID columns. It works for
Doctrine ORM (ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter) and Doctrine MongoDB ODM
(ApiPlatform\Doctrine\Odm\Filter\ComparisonFilter).
<?php
// api/src/ApiResource/Product.php
namespace App\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter;
use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;
#[ApiResource]
#[GetCollection(
parameters: [
'price' => new QueryParameter(
filter: new ComparisonFilter(new ExactFilter()),
property: 'price',
),
],
)]
class Product
{
// ...
}Given that the collection endpoint is /products, you can filter products by price range with the
following queries:
/products?price[gt]=10 — products whose price is strictly greater than 10/products?price[gte]=10 — products whose price is greater than or equal to 10/products?price[lt]=100 — products whose price is strictly less than 100/products?price[lte]=100 — products whose price is less than or equal to 100/products?price[ne]=0 — products whose price is not equal to 0There is no dedicated between operator. To filter within a range, combine gte and lte (or gt
and lt) in a single request:
GET /products?price[gte]=10&price[lte]=100This returns all products whose price is between 10 and 100 inclusive.
ComparisonFilter accepts DateTimeInterface values. When the underlying property is typed as a
DateTime or DateTimeImmutable, API Platform automatically casts the raw string from the query
string into a DateTimeImmutable before passing it to the filter. Any format accepted by the PHP
DateTimeImmutable constructor is valid.
<?php
// api/src/ApiResource/Event.php
namespace App\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter;
use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;
#[ApiResource]
#[GetCollection(
parameters: [
'startDate' => new QueryParameter(
filter: new ComparisonFilter(new ExactFilter()),
property: 'startDate',
),
],
)]
class Event
{
// ...
}Example request to fetch events starting after a given date:
GET /events?startDate[gt]=2025-01-01T00:00:00ZComparisonFilter can also wrap UuidFilter to enable comparison operators on UUID columns. This
is especially useful for cursor-based pagination on time-ordered UUIDs (UUID v7), where the
lexicographic order of UUIDs matches their chronological order.
<?php
// api/src/ApiResource/Device.php
namespace App\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter;
use ApiPlatform\Doctrine\Orm\Filter\UuidFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;
#[ApiResource]
#[GetCollection(
parameters: [
'id' => new QueryParameter(
filter: new ComparisonFilter(new UuidFilter()),
property: 'id',
),
],
)]
class Device
{
// ...
}Example requests:
/devices?id[gt]=0192d4e0-7b5a-7a3f-9e1c-4b8f2a1c3d5e — devices created after the given UUID/devices?id[gte]=...&id[lte]=... — devices within a UUID range/devices?id[ne]=... — exclude a specific deviceUuidFilter handles the conversion of UUID strings to their database binary representation via
Doctrine’s type system, which is required for correct comparisons on binary UUID columns.
ComparisonFilter automatically generates five OpenAPI query parameters for each configured
parameter key, one per operator. For a parameter named price, the generated parameters are
price[gt], price[gte], price[lt], price[lte], and price[ne].
The date filter allows filtering a collection by date intervals.
Syntax: ?property[<after|before|strictly_after|strictly_before>]=value
The value can take any date format supported by the
\DateTime constructor.
The after and before filters will filter including the value whereas strictly_after and
strictly_before will filter excluding the value.
Like other filters, the Date Filter must be explicitly enabled:
<?php
// api/src/ApiResource/Offer.php
namespace App\ApiResource;
#[GetCollection(
parameters: [
'created' => new QueryParameter(
filter: new DateFilter(),
property: 'createdAt' // Facultative if you use the exact property name for the parameter name (for e.g., if you use "createdAt" instead of "created", the property is auto-discovered)
),
],
)]
class Offer
{
// ...
}For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take
a look in the Introduction section.
Basically the ApiFilter declares the correct service under the hood. We recommend to use
QueryParameter as they’re more declarative and hide less complexity.
<?php
// api/src/ApiResource/Offer.php
namespace App\ApiResource;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
#[ApiResource]
#[ApiFilter(DateFilter::class, properties: ['createdAt'])]
class Offer
{
// ...
}# config/services.yaml
services:
offer.date_filter:
parent: "api_platform.doctrine.orm.date_filter"
arguments: [{ createdAt: ~ }]
tags: ["api_platform.filter"]
# The following are mandatory only if a _defaults section is defined with inverted values.
# You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the "defaults" section)
autowire: false
autoconfigure: false
public: false
# config/api/Offer.yaml
App\ApiResource\Offer:
# ...
operations:
ApiPlatform\Metadata\GetCollection:
filters: ["offer.date_filter"]Given that the collection endpoint is /offers, you can filter offers by date with the following
query: /offers?createdAt[after]=2018-03-19.
It will return all offers where createdAt is superior or equal to 2018-03-19.
null ValuesThe date filter is able to deal with date properties having null values. Four behaviors are
available at the property level of the filter:
| Description | Strategy to set |
|---|---|
| Use the default behavior of the DBMS | null |
| Exclude items | ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::EXCLUDE_NULL (exclude_null) |
| Consider items as oldest | ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::INCLUDE_NULL_BEFORE (include_null_before) |
| Consider items as youngest | ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::INCLUDE_NULL_AFTER (include_null_after) |
| Always include items | ApiPlatform\Doctrine\Common\Filter\DateFilterInterface::INCLUDE_NULL_BEFORE_AND_AFTER (include_null_before_and_after) |
For instance, exclude entries with a property value of null with the following service definition:
null Values with the Date Filter using the QueryParameter Syntax (recommended)<?php
// api/src/ApiResource/Offer.php
namespace App\ApiResource;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Metadata\QueryParamater;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
#[Get(parameters: [
'dateProperty' => new QueryParameter(
filter: new DateFilter(),
filterContext: DateFilterInterface::EXCLUDE_NULL,
),
])]
class Offer
{
// ...
}Or you can also use the properties attribute on the DateFilter to apply your
null strategy:
<?php
// api/src/ApiResource/Offer.php
namespace App\ApiResource;
use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface;
use ApiPlatform\Metadata\QueryParamater;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
#[Get(parameters: [
'dateProperty' => new QueryParameter(
filter: new DateFilter(properties: ['dateProperty' => DateFilterInterface::EXCLUDE_NULL]),
),
])]
class Offer
{
// ...
}For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take
a look in the Introduction section.
The boolean filter allows you to search on boolean fields and values.
Syntax: ?property=<true|false|1|0>
Enable the filter:
<?php
// api/src/ApiResource/Offer.php
namespace App\ApiResource;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
#[GetCollection(
parameters: [
'isAvailableGenericallyInMyCountry' => new QueryParameter(filter: new BooleanFilter()),
]
)]
class Offer
{
// ...
}For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take
a look in the Introduction section.
Given that the collection endpoint is /offers, you can filter offers with the following query:
/offers?isAvailableGenericallyInMyCountry=true.
It will return all offers where isAvailableGenericallyInMyCountry equals true.
The numeric filter allows you to search on numeric fields and values.
Syntax: ?property=<int|bigint|decimal...>
Enable the filter:
<?php
// api/src/ApiResource/Offer.php
namespace App\ApiResource;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\NumericFilter;
#[GetCollection(
parameters: [
'sold' => new QueryParameter(filter: new NumericFilter()),
]
)]
class Offer
{
// ...
}For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take
a look in the Introduction section.
Given that the collection endpoint is /offers, you can filter offers with the following query:
/offers?sold=1.
It will return all offers with sold equals 1.
The range filter allows you to filter by a value lower than, greater than, lower than or equal, greater than or equal and between two values.
Syntax: ?property[<lt|gt|lte|gte|between>]=value
Enable the filter:
<?php
// api/src/ApiResource/Offer.php
namespace App\ApiResource;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
#[GetCollection(
parameters: [
'price' => new QueryParameter(filter: new RangeFilter()),
]
)]
class Offer
{
// ...
}For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take
a look in the Introduction section.
Given that the collection endpoint is /offers, you can filter the price with the following query:
/offers?price[between]=12.99..15.99.
It will return all offers with price between 12.99 and 15.99.
You can filter offers by joining two values, for example: /offers?price[gt]=12.99&price[lt]=19.99.
The “exists” filter allows you to select items based on a nullable field value. It will also check the emptiness of a collection association.
Syntax: ?exists[property]=<true|false|1|0>
Enable the filter:
<?php
// api/src/ApiResource/Offer.php
namespace App\ApiResource;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\ExistsFilter;
#[GetCollection(
parameters: [
'transportFees' => new QueryParameter(filter: new ExistsFilter()),
]
)]
class Offer
{
// ...
}For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take
a look in the Introduction section.
Given that the collection endpoint is /offers, you can filter offers on the nullable field with
the following query: /offers?exists[transportFees]=true.
It will return all offers where transportFees is not null.
Since API Platform 4.2 defined the query parameter yourself and you don’t need the above
configuration.
A conflict will occur if exists is also the name of a property with the search filter enabled.
Luckily, the query parameter name to use is configurable:
# api/config/packages/api_platform.yaml
api_platform:
collection:
exists_parameter_name: "not_null" # the URL query parameter to use is now "not_null"The order filter allows sorting a collection against the given properties.
Syntax: ?order[property]=<asc|desc>
Enable the filter:
<?php
// api/src/ApiResource/Offer.php
namespace App\ApiResource;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
#[GetCollection(
parameters: [
'transportFees' => new QueryParameter(filter: new OrderFilter()),
]
)]
class Offer
{
// ...
}Or you can define one Query Parameter 'order[:property]', which uses an Order Filter and allow you
to sort on all available properties, thanks to this code:
<?php
// api/src/ApiResource/Offer.php
namespace App\ApiResource;
use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
#[GetCollection(
parameters: [
'order[:property]' => new QueryParameter(filter: new OrderFilter()),
]
)]
class Offer
{
// ...
}After that, you can use it with the following query: /offers?order[name]=desc&order[id]=asc.
For other syntaxes, for e.g., if you want to new syntax with the ApiResource attribute take
a look in the Introduction section.
Given that the collection endpoint is /offers, you can filter offers by name in ascending order
and then by ID in descending order with the following query:
/offers?order[name]=desc&order[id]=asc.
By default, whenever the query does not specify the direction explicitly (e.g.:
/offers?order[name]&order[id]), filters will not be applied unless you configure a default order
direction to use:
| Description | Strategy to set |
|---|---|
| Ascending | ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::DIRECTION_DESC (DESC) |
| Descending | ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::DIRECTION_ASC (ASC) |
For other sort strategies (about null values), please refer to the
Handling Null Values with the Order Filter section.
<?php
// api/src/ApiResource/Offer.php
namespace App\ApiResource;
use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
#[GetCollection(
parameters: [
'id' => new QueryParameter(filter: new OrderFilter(), filterContext: OrderFilterInterface::DIRECTION_ASC ),
'name' => new QueryParameter(filter: new OrderFilter(), filterContext: OrderFilterInterface::DIRECTION_DESC),
]
)]
class Offer
{
// ...
}Or you can also use the properties attribute on the OrderFilter to apply your
direction strategy:
<?php
// api/src/ApiResource/Offer.php
namespace App\ApiResource;
use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
#[GetCollection(
parameters: [
'id' => new QueryParameter(filter: new OrderFilter(properties: ['id' => OrderFilterInterface::DIRECTION_ASC])),
'name' => new QueryParameter(filter: new OrderFilter(properties: ['name' => OrderFilterInterface::DIRECTION_DESC])),
]
)]
class Offer
{
// ...
}When the property used for ordering can contain null values, you may want to specify how null
values are treated in the comparison:
| Description | Strategy to set |
|---|---|
| Use the default behavior of the DBMS | null |
| Consider items as smallest | ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_SMALLEST (nulls_smallest) |
| Consider items as largest | ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_LARGEST (nulls_largest) |
| Order items always first | ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_ALWAYS_FIRST (nulls_always_first) |
| Order items always last | ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface::NULLS_ALWAYS_LAST (nulls_always_last) |
For other sort strategies (including
ASCandDESC), please refer to the
For instance, treat entries with a property value of null as the smallest, with the following
service definition:
<?php
// api/src/ApiResource/Offer.php
namespace App\ApiResource;
use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
#[GetCollection(
parameters: [
'nulls_comparison' => new QueryParameter(
filter: new OrderFilter(),
property: 'validFrom',
filterContext: OrderFilterInterface::NULLS_SMALLEST,
),
'default_direction' => new QueryParameter(
filter: new OrderFilter(),
property: 'validFrom',
filterContext: OrderFilterInterface::DIRECTION_DESC,
),
]
)]
class Offer
{
// ...
}The SortFilter is a parameter-based filter designed exclusively for use with QueryParameter.
Unlike the OrderFilter, it does not extend AbstractFilter and works
with a single parameter per sorted property. This makes it straightforward to declare sort
parameters with full control over naming and behavior.
ORM: ApiPlatform\Doctrine\Orm\Filter\SortFilter ODM:
ApiPlatform\Doctrine\Odm\Filter\SortFilter
Each QueryParameter using SortFilter controls sorting for one property. The filter accepts
asc, desc, ASC, and DESC as values. Any other value causes a 422 validation error, because
the filter publishes a JSON Schema enum constraint automatically.
<?php
// api/src/Entity/Book.php
namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\SortFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;
#[ApiResource(
operations: [
new GetCollection(
parameters: [
'order' => new QueryParameter(filter: new SortFilter(), property: 'name'),
'orderDate' => new QueryParameter(filter: new SortFilter(), property: 'createdAt'),
]
),
]
)]
class Book
{
// ...
}Clients can then sort with:
GET /books?order=asc — sort by name ascendingGET /books?orderDate=desc — sort by creation date descendingGET /books?order=asc&orderDate=desc — combine bothWhen a sorted property can be null, use the nullsComparison constructor argument to specify how
null values are ordered relative to non-null values:
| Strategy | Constant |
|---|---|
| Use the default DBMS behavior | null (default) |
| Null values always sort first | OrderFilterInterface::NULLS_ALWAYS_FIRST |
| Null values always sort last | OrderFilterInterface::NULLS_ALWAYS_LAST |
| Null values treated as smallest | OrderFilterInterface::NULLS_SMALLEST |
| Null values treated as largest | OrderFilterInterface::NULLS_LARGEST |
<?php
// api/src/Entity/Book.php
namespace App\Entity;
use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\SortFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;
#[ApiResource(
operations: [
new GetCollection(
parameters: [
'order' => new QueryParameter(filter: new SortFilter(), property: 'name'),
'orderDate' => new QueryParameter(
filter: new SortFilter(nullsComparison: OrderFilterInterface::NULLS_ALWAYS_LAST),
property: 'createdAt'
),
]
),
]
)]
class Book
{
// ...
}The SortFilter supports dot notation to sort by properties of related entities (associations). API
Platform resolves the necessary JOINs (ORM) or aggregation pipeline stages (ODM) at metadata time,
so no runtime overhead is added for each request.
<?php
// api/src/Entity/Employee.php
namespace App\Entity;
use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\SortFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;
use Doctrine\ORM\Mapping as ORM;
#[ApiResource(
operations: [
new GetCollection(
parameters: [
// Sort by a direct property of the related entity (one hop)
'orderDept' => new QueryParameter(
filter: new SortFilter(),
property: 'department.name'
),
// Sort by a property two hops away (employee → department → company)
'orderCompany' => new QueryParameter(
filter: new SortFilter(nullsComparison: OrderFilterInterface::NULLS_ALWAYS_LAST),
property: 'department.company.name'
),
]
),
]
)]
class Employee
{
#[ORM\ManyToOne(targetEntity: Department::class)]
private Department $department;
// ...
}Example queries:
GET /employees?orderDept=asc — sort by department nameGET /employees?orderCompany=desc — sort by company name through two associationsFor MongoDB ODM, the SortFilter uses the aggregation pipeline. References between documents must
use storeAs: 'id' (not DBRef) for the $lookup stage to work correctly. Embedded documents are
accessed via dot notation without a $lookup.
<?php
// api/src/Document/Employee.php
namespace App\Document;
use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface;
use ApiPlatform\Doctrine\Odm\Filter\SortFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
#[ODM\Document]
#[ApiResource(
operations: [
new GetCollection(
parameters: [
'orderDept' => new QueryParameter(
filter: new SortFilter(),
property: 'department.name'
),
'orderDate' => new QueryParameter(
filter: new SortFilter(nullsComparison: OrderFilterInterface::NULLS_ALWAYS_LAST),
property: 'createdAt'
),
]
),
]
)]
class Employee
{
// storeAs: 'id' is required for $lookup to work; DBRef is not supported
#[ODM\ReferenceOne(targetDocument: Department::class, storeAs: 'id')]
private Department $department;
// ...
}Parameter-based filters (QueryParameter) support nested/related properties via dot notation. The
following filters handle the necessary JOINs (ORM) or $lookup/$unwind pipeline stages (ODM)
automatically:
| Filter | ORM nested support | ODM nested support |
|---|---|---|
SortFilter | Yes | Yes |
IriFilter | Yes | Yes |
ExactFilter | Yes | Yes |
PartialSearchFilter | Yes | Yes |
FreeTextQueryFilter | Yes (via delegate) | Yes (via delegate) |
Use the property argument on QueryParameter with dot notation to target nested properties:
<?php
// api/src/Entity/Employee.php
namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\IriFilter;
use ApiPlatform\Doctrine\Orm\Filter\SortFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;
use Doctrine\ORM\Mapping as ORM;
#[ApiResource(
operations: [
new GetCollection(
parameters: [
// Filter by the department IRI (direct association)
'department' => new QueryParameter(filter: new IriFilter(), property: 'department'),
// Sort by a property of the related department (one hop)
'orderDept' => new QueryParameter(filter: new SortFilter(), property: 'department.name'),
// Filter by company IRI through department (two hops)
'departmentCompany' => new QueryParameter(
filter: new IriFilter(),
property: 'department.company'
),
// Sort by company name (two hops)
'orderCompany' => new QueryParameter(filter: new SortFilter(), property: 'department.company.name'),
]
),
]
)]
class Employee
{
#[ORM\ManyToOne(targetEntity: Department::class)]
private Department $department;
// ...
}Example queries:
GET /employees?department=/api/departments/1 — filter by department IRIGET /employees?orderDept=asc — sort by department nameGET /employees?departmentCompany=/api/companies/1 — filter by company through departmentGET /employees?orderCompany=desc — sort by company nameMultiple parameters targeting the same relation path share the same JOIN (ORM) or $lookup stage
(ODM), so there is no duplication in the generated query.
The legacy method using the
ApiFilterattribute is deprecated and scheduled forremoval in API Platform 5.0. We strongly recommend migrating to the new
QueryParametersyntax described above.
For legacy code, the built-in filters that extend AbstractFilter support nested properties using
the dot (.) syntax, e.g.:
<?php
// api/src/Entity/Offer.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
#[ApiResource]
#[ApiFilter(OrderFilter::class, properties: ['product.releaseDate'])]
#[ApiFilter(SearchFilter::class, properties: ['product.color' => 'exact'])]
class Offer
{
// ...
}# config/services.yaml
services:
offer.order_filter:
parent: "api_platform.doctrine.orm.order_filter"
arguments: [{ product.releaseDate: ~ }]
tags: ["api_platform.filter"]
# The following are mandatory only if a _defaults section is defined with inverted values.
# You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the "defaults" section)
autowire: false
autoconfigure: false
public: false
offer.search_filter:
parent: "api_platform.doctrine.orm.search_filter"
arguments: [{ product.color: "exact" }]
tags: ["api_platform.filter"]
# The following are mandatory only if a _defaults section is defined with inverted values.
# You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the "defaults" section)
autowire: false
autoconfigure: false
public: false
# config/api/Offer.yaml
App\Entity\Offer:
# ...
operations:
ApiPlatform\Metadata\GetCollection:
filters: ["offer.order_filter", "offer.search_filter"]The above allows you to find offers by their respective product’s color:
http://localhost:8000/api/offers?product.color=red, or order offers by the product’s release date:
http://localhost:8000/api/offers?order[product.releaseDate]=desc
The legacy method using the
ApiFilterattribute is deprecated and scheduled forremoval in API Platform 5.0. We strongly recommend migrating to the new
QueryParametersyntax, which is detailed in the Introduction. You can use the:propertyplaceholder instead and it is recommended to use a filter for each type of data you are filtering.
As we have seen in previous examples, properties where filters can be applied must be explicitly declared. If you don’t care about security and performance (for e.g., an API with restricted access), it is also possible to enable built-in filters for all properties:
<?php
// api/src/Entity/Offer.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
#[ApiResource]
#[ApiFilter(OrderFilter::class)]
class Offer
{
// ...
}# config/services.yaml
services:
offer.order_filter:
parent: "api_platform.doctrine.orm.order_filter"
arguments: [~] # Pass null to enable the filter for all properties
tags: ["api_platform.filter"]
# The following are mandatory only if a _defaults section is defined with inverted values.
# You may want to isolate filters in a dedicated file to avoid adding the following lines (by adding them in the "defaults" section)
autowire: false
autoconfigure: false
public: false
# config/api/Offer.yaml
App\Entity\Offer:
# ...
operations:
ApiPlatform\Metadata\GetCollection:
filters: ["offer.order_filter"]Note: Filters on nested properties must still be enabled explicitly to keep things sane.
Regardless of this option, filters can be applied on a property only if:
asc or desc for the order filters).It means that the filter will be silently ignored if the property:
A filter that implements the ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface
interface can be decorated:
namespace App\Doctrine\Filter;
use ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface;
use ApiPlatform\Doctrine\Orm\Filter\FilterInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class SearchTextAndDateFilter implements FilterInterface
{
public function __construct(#[Autowire('@api_platform.doctrine.orm.search_filter.instance')] readonly FilterInterface $searchFilter, #[Autowire('@api_platform.doctrine.orm.date_filter.instance')] readonly FilterInterface $dateFilter, protected ?array $properties = null, private array $dateFilterProperties = [], private array $searchFilterProperties = [])
{
}
// This function is only used to hook in documentation generators (supported by Swagger and Hydra)
public function getDescription(string $resourceClass): array
{
if ($this->searchFilter instanceof PropertyAwareFilterInterface) {
$this->searchFilter->setProperties($this->searchFilterProperties);
}
if ($this->dateFilter instanceof PropertyAwareFilterInterface) {
$this->dateFilter->setProperties($this->dateFilterProperties);
}
return array_merge($this->searchFilter->getDescription($resourceClass), $this->dateFilter->getDescription($resourceClass));
}
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
{
if ($this->searchFilter instanceof PropertyAwareFilterInterface) {
$this->searchFilter->setProperties($this->searchFilterProperties);
}
if ($this->dateFilter instanceof PropertyAwareFilterInterface) {
$this->dateFilter->setProperties($this->dateFilterProperties);
}
$this->searchFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context);
$this->dateFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context);
}
}This can be used with parameters using attributes:
namespace App\Entity;
use ApiPlatform\Metadata\QueryParameter;
#[GetCollection(
uriTemplate: 'search_filter_parameter{._format}',
parameters: [
'searchOnTextAndDate[:property]' => new QueryParameter(filter: 'app_filter_date_and_search'),
]
)]
// Note that we link the parameter filter and this filter using the "alias" option:
#[ApiFilter(SearchTextAndDateFilter::class, alias: 'app_filter_date_and_search', properties: ['foo', 'createdAt'], arguments: ['dateFilterProperties' => ['createdAt' => 'exclude_null'], 'searchFilterProperties' => ['foo' => 'exact']])]
#[ORM\Entity]
class SearchFilterParameter
{
/**
* @var int The id
*/
#[ORM\Column(type: 'integer')]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id = null;
#[ORM\Column(type: 'string')]
private string $foo = '';
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
private ?\DateTimeImmutable $createdAt = null;
public function getId(): ?int
{
return $this->id;
}
public function getFoo(): string
{
return $this->foo;
}
public function setFoo(string $foo): void
{
$this->foo = $foo;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeImmutable $createdAt): void
{
$this->createdAt = $createdAt;
}
}Doctrine ORM features a filter system that allows the developer to add SQL to the conditional clauses of queries, regardless of the place where the SQL is generated (for e.g., from a DQL query, or by loading associated entities). These are applied to collections and items and therefore are incredibly useful.
The following information, specific to Doctrine filters in Symfony, is based upon a great article posted on Michaël Perrin’s blog.
Suppose we have a User entity and an Order entity related to the User one. A user should only
see his orders and no one else’s.
<?php
// api/src/Entity/User.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
#[ApiResource]
class User
{
// ...
}<?php
// api/src/Entity/Order.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;
#[ApiResource]
class Order
{
// ...
#[ORM\ManyToOne(User::class)]
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id')]
public User $user;
// ...
}The whole idea is that any query on the order table should add a WHERE user_id = :user_id
condition.
Start by creating a custom attribute to mark restricted entities:
<?php
// api/src/Attribute/UserAware.php
namespace App\Attribute;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
final class UserAware
{
public $userFieldName;
}Then, let’s mark the Order entity as a “user aware” entity.
<?php
// api/src/Entity/Order.php
namespace App\Entity;
use App\Attribute\UserAware;
#[UserAware(userFieldName: "user_id")]
class Order {
// ...
}Now, create a Doctrine filter class:
<?php
// api/src/Filter/UserFilter.php
namespace App\Filter;
use App\Attribute\UserAware;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
final class UserFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
// The Doctrine filter is called for any query on any entity
// Check if the current entity is "user aware" (marked with an attribute)
$userAware = $targetEntity->getReflectionClass()->getAttributes(UserAware::class)[0] ?? null;
$fieldName = $userAware?->getArguments()['userFieldName'] ?? null;
if ($fieldName === '' || is_null($fieldName)) {
return '';
}
try {
// Don't worry, getParameter automatically escapes parameters
$userId = $this->getParameter('id');
} catch (\InvalidArgumentException $e) {
// No user ID has been defined
return '';
}
if (empty($fieldName) || empty($userId)) {
return '';
}
return sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $userId);
}
}Now, we must configure the Doctrine filter.
# api/config/packages/api_platform.yaml
doctrine:
orm:
filters:
user_filter:
class: App\Filter\UserFilter
enabled: trueDone: Doctrine will automatically filter all UserAwareentities!
Doctrine ORM filters have access to the context created from the HTTP request and to the
QueryBuilder instance used to retrieve data from the database. They are only applied to
collections. If you want to deal with the DQL query generated to retrieve items,
extensions are the way to go.
A Doctrine ORM filter is basically a class implementing the
ApiPlatform\Doctrine\Orm\Filter\FilterInterface.
For MongoDB (ODM) filters, please refer to
Creating Custom Doctrine ODM Filters documentation.
Advantages of the new approach:
AbstractFilter. A simple implementation of FilterInterface
is all it takes.To get started, API Platform includes a very handy make command to generate the basic structure of an ORM filter:
bin/console make:filter orm
Then, provide the name of your filter, for example MonthFilter, or pass it directly as an
argument:
make:filter orm MyCustomFilter
You will get a file at api/src/Filter/MonthFilter.php with the following content:
<?php
// api/src/Filter/MonthFilter.php
declare(strict_types=1);
namespace App\Filter;
use ApiPlatform\Doctrine\Orm\Filter\FilterInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;
class MyCustomFilter implements FilterInterface
{
use BackwardCompatibleFilterDescriptionTrait; // Here for backward compatibility, keep it until 5.0.
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
{
// Retrieve the parameter and it's value
// $parameter = $context['parameter'];
// $value = $parameter->getValue();
// Retrieve the property
// $property = $parameter->getProperty();
// Retrieve alias and parameter name
// $alias = $queryBuilder->getRootAliases()[0];
// $parameterName = $queryNameGenerator->generateParameterName($property);
// TODO: make your awesome query using the $queryBuilder
// $queryBuilder->
}
}Let’s create a concrete filter that allows fetching entities based on the month of a date field (for
e.g., createdAt).
The goal is to be able to call a URL like GET /invoices?createdAtMonth=7 to get all invoices
created in July.
Here is the complete and corrected code for the filter:
<?php
// api/src/Filter/MonthFilter.php
declare(strict_types=1);
namespace App\Filter;
use ApiPlatform\Doctrine\Orm\Filter\FilterInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;
class MonthFilter implements FilterInterface
{
use BackwardCompatibleFilterDescriptionTrait; // Here for backward compatibility, keep it until 5.0.
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
{
$parameter = $context['parameter'];
$monthValue = $parameter->getValue();
$parameterName = $queryNameGenerator->generateParameterName($property);
$alias = $queryBuilder->getRootAliases()[0];
$queryBuilder
->andWhere(sprintf('MONTH(%s.%s) = :%s', $alias, $property, $parameterName))
->setParameter($parameterName, $monthValue);
}
}Now that the filter is created, it must be associated with an API resource. We use the
QueryParameter object on a #[GetCollection] operation attribute for this. For other syntax
please refer to this documentation.
<?php
// src/ApiResource/Invoice.php
namespace App\ApiResource;
use ApiPlatform\Metadata\QueryParameter;
use App\Filters\MonthFilter;
#[GetCollection(
parameters: [
'createdAtMonth' => new QueryParameter(
filter: new MonthFilter(),
property: 'createdAt'
),
]
)]
class Invoice
{
// ...
}And that’s it! ✅
Your filter is operational.
A request like GET /invoices?createdAtMonth=7 will now correctly return the invoices from July!
Currently, our filter accepts any value, like createdAtMonth=99 or createdAtMonth=foo, which
could cause errors. To validate inputs and ensure the correct type, we can implement the
JsonSchemaFilterInterface.
This allows delegating validation to API Platform, respecting the SOLID Principles.
Even with our internal systems, some additional manual validation is needed to ensure
greater accuracy. However, we already take care of a lot of these validations for you.
You can see how this works directly in our code components:
- The
ParameterValidatorProviderfor Symfony can be found in the Symfony ParameterValidatorProvider.php file.- The
ParameterValidatorProviderfor Laravel is located in the Laravel ParameterValidatorProvider.php file.Additionally, we filter out empty values within our
ParameterExtensionclasses. For instance, the Doctrine ORMParameterExtensionhandles this filtering here.
<?php
// api/src/Filters/MonthFilter.php
namespace App\Filters;
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
// ...
final class MonthFilter implements FilterInterface, JsonSchemaFilterInterface
{
public function apply(...): void {}
public function getSchema(Parameter $parameter): array
{
return [
'type' => 'integer',
// <=> Symfony\Component\Validator\Constraints\Range
'minimum' => 1,
'maximum' => 12,
];
}
}With this code, under the hood, API Platform automatically adds a
Symfony Range constraint. This
ensures the parameter only accepts values between 1 and 12 (inclusive), which is exactly what we
need.
This approach offers two key benefits:
$monthValue = $parameter->getValue(); knowing
it’s already aThis means you don’t have to add custom validation to your filter class, entity, or model. The validation is handled for you, making your code cleaner and more efficient.
For a complete list of constraints, see the
If your filter expects a simple type (int, string, bool, or arrays of these types), the
quickest way is to use the OpenApiFilterTrait.
<?php
// api/src/Filters/MonthFilter.php
namespace App\Filters;
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
use ApiPlatform\Doctrine\Common\Filter\OpenApiFilterTrait;
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
// ...
final class MonthFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
{
use OpenApiFilterTrait;
// ...
}That’s all! The trait takes care of generating the corresponding OpenAPI documentation. 🚀
If your filter expects more complex data (an object, a specific format), you must implement the
getOpenApiParameters method manually.
<?php
// api/src/Filter/MyComplexFilter.php
namespace App\Filter;
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
use ApiPlatform\Metadata\Parameter;
final class MyComplexFilter implements FilterInterface, OpenApiParameterFilterInterface
{
public function apply(...): void {}
/**
* @return array<OpenApiParameter>
*/
public function getOpenApiParameters(Parameter $parameter): array
{
// Example for a filter that expects an array of values
// like ?myParam[key1]=value1&myParam[key2]=value2
return [
new OpenApiParameter(
name: $parameter->getKey(),
in: 'query',
description: 'A custom filter for complex objects.',
style: 'deepObject',
explode: true
)
];
}
}For Doctrine ORM filters, please refer to
Creating Custom Doctrine ORM Filters documentation.
Doctrine MongoDB ODM filters have access to the context created from the HTTP request and to the aggregation builder instance used to retrieve data from the database and to execute complex operations on data. They are only applied to collections. If you want to deal with the aggregation pipeline generated to retrieve items, extensions are the way to go.
A Doctrine MongoDB ODM filter is basically a class implementing the
ApiPlatform\Doctrine\Odm\Filter\FilterInterface.
Advantages of the new approach:
AbstractFilter. A simple implementation of FilterInterface
is all it takes.To get started, API Platform includes a very handy make command to generate the basic structure of an ODM filter:
bin/console make:filter odm
Then, provide the name of your filter, for example MonthFilter, or pass it directly as an
argument:
make:filter orm MyCustomFilter
You will get a file at api/src/Filter/MonthFilter.php with the following content:
<?php
// api/src/Filter/MonthFilter.php
declare(strict_types=1);
namespace App\Filter;
use ApiPlatform\Doctrine\Odm\Filter\FilterInterface;
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
use ApiPlatform\Metadata\Operation;
use Doctrine\ODM\MongoDB\Aggregation\Builder;
class MonthFilter implements FilterInterface
{
use BackwardCompatibleFilterDescriptionTrait; // Here for backward compatibility, keep it until 5.0.
public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
{
// Retrieve the parameter and it's value
// $parameter = $context['parameter'];
// $value = $parameter->getValue();
// Retrieve the property
// $property = $parameter->getProperty();
// TODO: make your awesome query using the $aggregationBuilder
// $aggregationBuilder->
}
}Let’s create a concrete filter that allows fetching entities based on the month of a date field (for
e.g., createdAt).
The goal is to be able to call a URL like GET /invoices?createdAtMonth=7 to get all invoices
created in July.
Here is the complete and corrected code for the filter:
<?php
// api/src/Filter/MonthFilter.php
declare(strict_types=1);
namespace App\Filter;
use ApiPlatform\Doctrine\Odm\Filter\FilterInterface;
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
use ApiPlatform\Metadata\Operation;
use Doctrine\ODM\MongoDB\Aggregation\Builder;
class MonthFilter implements FilterInterface
{
use BackwardCompatibleFilterDescriptionTrait; // Here for backward compatibility, keep it until 5.0.
public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void
{
$parameter = $context['parameter'];
$value = $parameter->getValue();
$property = $parameter->getProperty();
$aggregationBuilder->match(
$aggregationBuilder->expr()->operator('$expr', [
'$eq' => [
['$month' => '$' . $property],
$monthValue
]
])
);
}
}Now that the filter is created, it must be associated with an API resource. We use the
QueryParameter object on a #[GetCollection] operation attribute for this. For other syntax
please refer to this documentation.
<?php
// src/ApiResource/Invoice.php
namespace App\ApiResource;
use ApiPlatform\Metadata\QueryParameter;
use App\Filters\MonthFilter;
#[GetCollection(
parameters: [
'createdAtMonth' => new QueryParameter(
filter: new MonthFilter(),
property: 'createdAt'
),
]
)]
class Invoice
{
// ...
}And that’s it! ✅
Your filter is operational.
A request like GET /invoices?createdAtMonth=7 will now correctly return the invoices from July!
Currently, our filter accepts any value, like createdAtMonth=99 or createdAtMonth=foo, which
could cause errors. To validate inputs and ensure the correct type, we can implement the
JsonSchemaFilterInterface.
This allows delegating validation to API Platform, respecting the SOLID Principles.
Even with our internal systems, some additional manual validation is needed to ensure
greater accuracy. However, we already take care of a lot of these validations for you.
You can see how this works directly in our code components:
- The
ParameterValidatorProviderfor Symfony can be found in the Symfony ParameterValidatorProvider.php file.- The
ParameterValidatorProviderfor Laravel is located in the Laravel ParameterValidatorProvider.php file.Additionally, we filter out empty values within our
ParameterExtensionclasses. For instance, the Doctrine ODMParameterExtensionhandles this filtering here.
<?php
// api/src/Filters/MonthFilter.php
namespace App\Filters;
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
// ...
final class MonthFilter implements FilterInterface, JsonSchemaFilterInterface
{
public function apply(...): void {}
public function getSchema(Parameter $parameter): array
{
return [
'type' => 'integer',
// <=> Symfony\Component\Validator\Constraints\Range
'minimum' => 1,
'maximum' => 12,
];
}
}With this code, under the hood, API Platform automatically adds a
Symfony Range constraint. This
ensures the parameter only accepts values between 1 and 12 (inclusive), which is exactly what we
need.
This approach offers two key benefits:
$monthValue = $parameter->getValue(); knowing
it’s already aThis means you don’t have to add custom validation to your filter class, entity, or model. The validation is handled for you, making your code cleaner and more efficient.
For a complete list of constraints, see the
If your filter expects a simple type (int, string, bool, or arrays of these types), the
quickest way is to use the OpenApiFilterTrait.
<?php
// api/src/Filters/MonthFilter.php
namespace App\Filters;
use ApiPlatform\Metadata\JsonSchemaFilterInterface;
use ApiPlatform\Doctrine\Common\Filter\OpenApiFilterTrait;
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
// ...
final class MonthFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface
{
use OpenApiFilterTrait;
// ...
}That’s all! The trait takes care of generating the corresponding OpenAPI documentation. 🚀
If your filter expects more complex data (an object, a specific format), you must implement the
getOpenApiParameters method manually.
<?php
// api/src/Filter/MyComplexFilter.php
namespace App\Filter;
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
use ApiPlatform\Metadata\Parameter;
final class MyComplexFilter implements FilterInterface, OpenApiParameterFilterInterface
{
public function apply(...): void {}
/**
* @return array<OpenApiParameter>
*/
public function getOpenApiParameters(Parameter $parameter): array
{
// Example for a filter that expects an array of values
// like ?myParam[key1]=value1&myParam[key2]=value2
return [
new OpenApiParameter(
name: $parameter->getKey(),
in: 'query',
description: 'A custom filter for complex objects.',
style: 'deepObject',
explode: true
)
];
}
}You can also help us improve the documentation of this page.
Made with love by
Les-Tilleuls.coop can help you design and develop your APIs and web projects, and train your teams in API Platform, Symfony, Next.js, Kubernetes and a wide range of other technologies.
Learn more