gl3n/http-query-string-filter-bundle
A Symfony bundle for filtering HTTP query string parameters, letting you validate, sanitize, and normalize incoming request data before it reaches your application logic. Keeps controllers clean by centralizing query-string filtering rules.
This is a small, single-purpose Symfony bundle that turns the messy string soup of $request->query into validated, typed, authorized values. Treat the README and the bundle's own Tests/ directory as the primary documentation — with a package this size, the test cases are usually more accurate than the prose.
Install (Symfony 2.x/3.x era, which is what this targets):
composer require gl3n/http-query-string-filter-bundle
Register the bundle in app/AppKernel.php:
$bundles = [
// ...
new Gl3n\HttpQueryStringFilterBundle\HttpQueryStringFilterBundle(),
];
Then confirm the service is wired — look in Resources/config/ for the service definition and run:
php bin/console debug:container | grep -i query
Where to look first, in order:
README.md — the annotated usage example is the whole API surface.Tests/ — actual signatures, constraint names, and expected casting behavior.Resources/config/services.xml (or .yml) — the service id you'll inject.DependencyInjection/Configuration.php — any bundle-level config keys.First use case — a filtered collection endpoint. The canonical scenario is a paginated list where you accept ?page=2&limit=50&sort=-created_at&active=1 and need to reject bad input, cast page/limit to integers, and refuse sort values the caller isn't authorized to use.
Instead of (int) $request->query->get('page', 1) scattered through the controller, you declare the expected parameters once, attach Symfony Validator constraints, and cast to native types. The bundle gives you back a clean, typed bag you can pass straight into a repository query. That single replace is the entire day-to-day value: one declaration replaces a dozen defensive get() calls.
The typical pattern is a declarative spec attached to the controller action (annotation) or built via the injected service. The shape is:
/**
* @QueryStringFilter({
* "page": {type: "int", default: 1, constraints: {"Range": {min: 1}}},
* "limit": {type: "int", default: 25, constraints: {"Range": {min: 1, max: 100}}},
* "sort": {type: "string", default: "created_at", constraints: {"Choice": {choices: {"created_at", "name", "-created_at"}}}},
* "q": {type: "string", required: false}
* })
*/
public function listAction(Request $request) { /* ... */ }
(Exact annotation/option names come from the README — the structure above is the mental model: name → type → default → constraints.)
The bundle's real win is that casting happens after validation, so page arrives as int 3, not the string "3", and only if it passed Range. Do not re-validate in the controller — that defeats the point and creates two sources of truth.
For parameters that gate data access (e.g. ?tenant_id=, ?include_deleted=1), attach the authorization check to the parameter itself. This keeps the "can this user even ask for this?" decision next to the "is this well-formed?" decision, and yields the correct HTTP semantics: malformed → 400, forbidden → 403. Push it into a security expression where possible (is_granted('ROLE_ADMIN') style) so the rule lives in security config, not controller code.
$criteria = $this->queryFilter->filter($request); // typed, validated array
$qb = $this->repo->createQueryBuilder('e')
->setFirstResult(($criteria['page'] - 1) * $criteria['limit'])
->setMaxResults($criteria['limit']);
Because the output is a plain array of native types, it slots into a Criteria object or a query builder with zero glue code.
Inject the bundle service into a BaseController and expose a small helper ($this->filter($request, $spec)). That localizes the bundle dependency to one file — critical, given the maintenance status — and makes it trivial to swap later.
Functional tests are the cheapest way to pin behavior: hit the endpoint with ?page=0, ?limit=abc, ?sort=password_hash and assert 400/403. Because the bundle does casting, also assert the type reaching your service (mock the repository and inspect arguments) — mixed "25" versus 25 bugs are the classic failure here.
bundles.php. On modern Symfony expect to either register it manually (config/bundles.php) or, more realistically, treat the README as a spec and reimplement the declarative filter with symfony/validator + a DTO. Budget for this before committing.?active=false casts to a truthy value under naive rules; ?limit= (empty) is not the same as absent. Confirm the bundle's caster behavior for "", "0", "false", "null" in the tests before trusting it with booleans.?limit[]=1&limit[]=2 arrives as an array. If your type is int, make sure the filter rejects or coerces it rather than leaking an array into your query builder.default and required: true is contradictory — pick one. Missing-parameter errors should map to 400, not 500; verify the bundle throws a catchable validation exception you can convert in a kernel exception listener._locale, _format, _route into query attributes in some setups. Never feed the whole $request->query->all() through a filter that whitelists nothing — always declare an explicit allowlist, both for safety and for predictable behavior.?page=2 can serve page 1's body. Build the key from the normalized output, not the raw query string.dump() the filter output immediately; enable the profiler and inspect the request panel to see what arrived versus what survived. When a constraint unexpectedly passes, check you're using the Symfony constraint name the bundle expects (the README's constraint syntax is a shallow wrapper, not a reimplementation).DateTime, comma-separated lists, or enum-like values. Look for a caster/type registry in the bundle config; if it's hardcoded, that's your signal to fork.Validator, or a current query-filter library) without surprises.How can I help you explore Laravel packages today?