From api-platform
Adds search, sort, date range, boolean, enum, numeric, IRI, and free-text filters to API Platform collections using the QueryParameter approach. Covers migration from legacy #[ApiFilter] code.
How this skill is triggered — by the user, by Claude, or both
Slash command
/api-platform:api-filterThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Declare filters with `QueryParameter` in the operation's `parameters:` array. The
Declare filters with QueryParameter in the operation's parameters: array. The
key is the query-string parameter exposed to clients; the filter: is the filter
instance. This is the canonical approach for API Platform 4.4+ — it works with any
state provider and is per-operation explicit.
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
#[GetCollection(
parameters: [
'status' => new QueryParameter(filter: new ExactFilter()),
],
)]
class Order {}
Client: GET /orders?status=shipped
#[ApiFilter]is legacy. The class-level#[ApiFilter(SearchFilter::class, ...)]attribute and the multi-strategy filters (SearchFilter,BooleanFilter,NumericFilter,OrderFilter,BackedEnumFilter) are deprecated in 4.4 and removed in 6.0. Don't teach or add them in new code. If you encounter them, the migration target is theQueryParameter+ single-purpose filter set below. The mapping is in the table at the bottom.
Each filter does one thing. They live in two parallel namespaces — pick the one matching your persistence layer:
| Filter | Purpose | ORM | MongoDB ODM |
|---|---|---|---|
ExactFilter | equality, multi-value (IN); booleans/ints/enums via nativeType | ApiPlatform\Doctrine\Orm\Filter\ExactFilter | ApiPlatform\Doctrine\Odm\Filter\ExactFilter |
PartialSearchFilter | LIKE %x% substring | …\Orm\Filter\PartialSearchFilter | …\Odm\Filter\PartialSearchFilter |
ComparisonFilter | gt/gte/lt/lte/ne (decorates an equality filter) | …\Orm\Filter\ComparisonFilter | …\Odm\Filter\ComparisonFilter |
SortFilter | ORDER BY | …\Orm\Filter\SortFilter | …\Odm\Filter\SortFilter |
DateFilter | before/after date ranges | …\Orm\Filter\DateFilter | …\Odm\Filter\DateFilter |
RangeFilter | between/gt/lt on numbers | …\Orm\Filter\RangeFilter | …\Odm\Filter\RangeFilter |
ExistsFilter | IS NULL / IS NOT NULL | …\Orm\Filter\ExistsFilter | …\Odm\Filter\ExistsFilter |
IriFilter | relationship lookup by IRI | …\Orm\Filter\IriFilter | …\Odm\Filter\IriFilter |
OrFilter | decorator: switches a primary's WHERE to orWhere | …\Orm\Filter\OrFilter | …\Odm\Filter\OrFilter |
FreeTextQueryFilter | decorator: broadcasts one value across N properties | …\Orm\Filter\FreeTextQueryFilter | …\Odm\Filter\FreeTextQueryFilter |
ComparisonFilterandOrFilterare still marked@experimentalin 4.4 — they are the intended migration target but their API may shift before stabilizing.
'status' => new QueryParameter(filter: new ExactFilter())
GET /orders?status=shipped. Arrays produce an IN: ?status[]=draft&status[]=sent.
ExactFilter + nativeTypeThere is no BooleanFilter in the canonical set. Use ExactFilter and declare the
native type so values are cast and documented correctly:
use Symfony\Component\TypeInfo\Type\BuiltinType;
use Symfony\Component\TypeInfo\TypeIdentifier;
'active' => new QueryParameter(
filter: new ExactFilter(),
nativeType: new BuiltinType(TypeIdentifier::BOOL),
),
GET /orders?active=true. Use TypeIdentifier::INT for integers; pass an enum's
native type for backed enums.
use ApiPlatform\Doctrine\Orm\Filter\PartialSearchFilter;
'q' => new QueryParameter(filter: new PartialSearchFilter(), property: 'title')
GET /books?q=harry. property: maps the public param name to the entity field.
gt/gte/lt/lte/ne)use ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter;
'price' => new QueryParameter(filter: new ComparisonFilter(new ExactFilter()))
GET /products?price[gt]=100&price[lte]=500.
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
'createdAt' => new QueryParameter(filter: new DateFilter())
GET /orders?createdAt[after]=2024-01-01&createdAt[before]=2024-12-31.
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
'price' => new QueryParameter(filter: new RangeFilter())
GET /products?price[between]=10..100.
use ApiPlatform\Doctrine\Orm\Filter\ExistsFilter;
'deletedAt' => new QueryParameter(filter: new ExistsFilter())
GET /orders?deletedAt[exists]=false.
use ApiPlatform\Doctrine\Orm\Filter\IriFilter;
'author' => new QueryParameter(filter: new IriFilter())
GET /books?author=/authors/1.
SortFilterThere is no OrderFilter in the canonical set. Use SortFilter. Two forms:
use ApiPlatform\Doctrine\Orm\Filter\SortFilter;
// Per-property named parameter
'orderName' => new QueryParameter(filter: new SortFilter(), property: 'name'),
// → GET /books?orderName=desc
// Dynamic, OrderFilter-style: one parameter, any allowed property
'order[:property]' => new QueryParameter(filter: new SortFilter()),
// → GET /books?order[name]=asc&order[createdAt]=desc
property: can traverse relations ('department.company.name' sorts across joins).
Control null placement with nullsComparison:
use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface;
'orderHireDate' => new QueryParameter(
filter: new SortFilter(nullsComparison: OrderFilterInterface::NULLS_ALWAYS_FIRST),
property: 'hireDate',
),
FreeTextQueryFilter decorates a primary and broadcasts one value to several
properties; wrap with OrFilter to match any of them:
use ApiPlatform\Doctrine\Orm\Filter\FreeTextQueryFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrFilter;
use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
'autocomplete' => new QueryParameter(
filter: new FreeTextQueryFilter(new OrFilter(new ExactFilter())),
properties: ['name', 'email', 'description'],
),
GET /users?autocomplete=alice.
use Symfony\Component\Validator\Constraints as Assert;
'length' => new QueryParameter(
filter: new ExactFilter(),
constraints: [new Assert\Length(max: 100)],
)
Invalid values yield a 422 before the query runs.
Set a default sort directly on the operation (no parameter needed):
new GetCollection(order: ['createdAt' => 'DESC'])
| Legacy (deprecated 4.4, removed 6.0) | Canonical replacement |
|---|---|
#[ApiFilter(SearchFilter::class, ['x' => 'exact'])] | 'x' => new QueryParameter(filter: new ExactFilter()) |
SearchFilter strategy partial | PartialSearchFilter |
BooleanFilter | ExactFilter + nativeType: BOOL |
NumericFilter | ExactFilter + nativeType: INT |
BackedEnumFilter | ExactFilter + enum nativeType |
OrderFilter | SortFilter |
RangeFilter, DateFilter, ExistsFilter | same class — survives as drop-in, just move to QueryParameter |
The QueryParameter wiring is identical, but Laravel ships its own Eloquent
filter set under ApiPlatform\Laravel\Eloquent\Filter\ — these are not the Doctrine
classes and the names differ. Declare them with class-level #[QueryParameter] /
#[GetCollection(parameters: …)] on the model (or DTO). The key, property,
properties, :property placeholder and constraints options work the same.
| Need | Doctrine (above) | Laravel Eloquent class |
|---|---|---|
| equality | ExactFilter | EqualsFilter |
LIKE %x% | PartialSearchFilter | PartialSearchFilter |
LIKE x% / %x | StartSearchFilter / EndSearchFilter | StartSearchFilter / EndSearchFilter |
| boolean | ExactFilter + nativeType | BooleanFilter |
| date range | DateFilter | DateFilter (filterContext: ['include_nulls' => true] to keep nulls; default excludes) |
| numeric range | RangeFilter | RangeFilter |
| sort | SortFilter | OrderFilter |
| OR | OrFilter | OrFilter (decorator: new OrFilter(new EqualsFilter())) |
use ApiPlatform\Laravel\Eloquent\Filter\EqualsFilter;
use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\QueryParameter;
#[ApiResource]
#[QueryParameter(key: 'name', filter: EqualsFilter::class)]
#[QueryParameter(key: 'sort[:property]', filter: OrderFilter::class, properties: ['name', 'id'])]
class Book extends Model {}
Constraints are Laravel validation rules (a string/array), not Symfony
constraints: new QueryParameter(key: 'name', filter: PartialSearchFilter::class, constraints: 'min:2').
Custom filters implement ApiPlatform\Laravel\Eloquent\Filter\FilterInterface
(apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder);
scaffold with php artisan make:filter. There is no ExistsFilter, ComparisonFilter,
IriFilter or FreeTextQueryFilter in the Eloquent set — nativeType on a parameter
is also unused (use BooleanFilter for booleans). #[ApiFilter] is not the Laravel
idiom; models declare #[QueryParameter] directly.
npx claudepluginhub api-platform/skillset --plugin api-platformImplements API Platform filters including v4 Parameters API (QueryParameter) and legacy #[ApiFilter] for search, date, range, boolean, and custom filtering in Symfony projects.
Guides designing filter and sort query parameters for list endpoints, covering operator syntax, injection prevention, and sort conventions.
Builds flexible API filtering and sorting systems with query parameter parsing, operators like gte/lte/in, validation, and security whitelists. For search endpoints, data grids, dynamic query APIs.