jdeniau/api-platform-filter-validator
Adds Symfony Validator constraint support to API Platform filters, so query parameters can be validated before they reach your data layer and invalid filter values return clean errors instead of failing deep in queries.
First, a scoping caveat. This is a Symfony bundle for API Platform, not a Laravel package. A Laravel developer will realistically only touch it in two situations: (a) you maintain a hybrid estate where an API Platform service sits next to Laravel apps, or (b) you are porting API Platform-style filter conventions into a Laravel API and want a reference for how to validate query-string filter values. If neither applies, budget ten minutes to read the README, decide the pattern is sound, and reimplement it with Laravel's Validator — do not add a Composer dependency for this.
Where to look first. In this order:
api-platform/core PR #1723 — this package explicitly exists waiting for that PR, so the PR is the de facto spec. Read it before reading the code; it tells you what the maintainer considered the correct place to hook filter validation.Resources/config/services.xml or equivalent) — that is where you learn which filter classes are decorated and therefore which filters are actually covered.Minimal setup (assuming an existing API Platform 2.x app):
composer require jdeniau/api-platform-filter-validator
// config/bundles.php
Jdeniau\ApiPlatformFilterValidator\ApiPlatformFilterValidatorBundle::class => ['all' => true],
Then annotate the filter argument you want guarded on your ApiResource:
#[ApiResource]
#[QueryParameter] // or the older @ApiFilter annotation
First use case. Take the classic failure mode: a collection endpoint with SearchFilter on price, and a client sends ?price[gt]=abc. Without validation you get a database-level cast error, a 500, or a silently empty collection — all of which are terrible API contracts. The first thing to verify after install is that this request now returns a 400 with a Hydra/ConstraintViolationList body naming the offending filter and argument. If it still 500s, the bundle is not wired for that filter type and you should stop and check coverage before going further.
Validate at the filter boundary, not the entity. The value in this approach is that the constraint attaches to the filter argument, so it runs before the Doctrine query is built. Mirror that discipline if you reimplement: use a Laravel FormRequest with rules keyed on filter.* rather than validating inside a query scope or repository. You want the 422 to be produced by the HTTP layer, not by a QueryException handler.
Reuse stock Symfony constraints rather than writing custom ones. The idiomatic configuration is Assert\Type, Assert\Range, Assert\Choice, and Assert\Regex applied to the filter argument, because filter values arrive as strings from the query string and the built-in constraints already express "coerce then check." A representative pattern:
#[ApiFilter(SearchFilter::class, properties: ['price' => 'partial'])]
with the constraint declared alongside the filter on the property so that the validation group matches the operation (GetCollection).
Centralize via a trait or base resource class. Because every collection resource is exposed to the same class of malformed input, define a FilterValidatedTrait (or a base AbstractApiResource) that applies a default validation group, then opt specific properties into stricter rules. This keeps the per-resource diff down to one attribute per filter instead of a block of constraints.
Treat the validation group as part of the public API. Pick one group name (e.g. filter_validation) and use it consistently across resources. Mixing serialization groups and validation groups is the single most common source of "the constraint is configured but never runs."
Test at the HTTP boundary. Use API Platform's ApiTestCase (or a Behat feature) to assert the 400/422 status and the presence of the violation propertyPath. Unit-testing the constraint in isolation will pass while the wiring is broken; only an end-to-end request catches the case where the bundle is registered but the filter type is not decorated.
If you are the Laravel-side consumer, the direct translation is: a FormRequest whose rules() returns ['price.gt' => ['numeric']], plus $request->validated() passed into your query builder. Same boundary, same guarantee, zero abandoned dependencies.
Adoption risk is the headline gotcha. Zero stars, zero dependents, one maintainer, and an explicit "waiting for the upstream PR to merge" in the description. The package is a shim with a self-declared expiry date. Treat it as a reference implementation to vendor or reimplement, not as a dependency you commit to for years. Pin the exact version and add a TODO tied to the core PR.
Check the core PR before installing. If api-platform/core has since absorbed the equivalent behaviour, this bundle will either be a no-op or actively conflict (double decoration of the same filter service). Run composer show api-platform/core and compare against PR #1723's merge status first — this step alone resolves most "it does nothing" reports.
Everything from the query string is a string. Assert\Type(type: 'integer') will fail for ?price[gt]=5 unless you also set the constraint's coercion behaviour or use Assert\Range/Assert\Positive instead, which coerce. This is the most common reason a correctly-wired constraint rejects valid input.
Nested and array filters need explicit handling. ?price[gt]=10&price[lt]=20 produces an array of arguments; ?id[]=1&id[]=2 produces a list. A scalar constraint on the parent will not traverse into the children. Validate each documented operator explicitly rather than assuming a single constraint covers the array shape.
Unknown filter keys are not this package's job. ?totallymadeup=1 will typically be ignored by API Platform before validation ever runs. If you need strict rejection of unrecognised query parameters, that is a separate middleware/listener — do not file it as a bug against this bundle.
Debugging checklist, in order: bin/console debug:container --tag=… to confirm the decorator is registered; bin/console debug:validator to confirm your constraint is loaded under the expected group; then a raw HTTP request with -v to confirm you are not being short-circuited by a gateway or an access_control rule returning 401 before validation.
Extension point. If you need coverage for a filter type the bundle misses, the clean move is to decorate the filter itself (Symfony's service decoration) and throw a ValidationException with a ConstraintViolationList — that is exactly what the upstream PR does, and it means you can drop the bundle the day the PR merges without rewriting call sites.
How can I help you explore Laravel packages today?