hyvor / doctrine-filterq
Advanced filtering for Doctrine ORM APIs
Requires
- php: ^8.1
- doctrine/orm: ^2.17|^3.0
Requires (Dev)
- doctrine/dbal: ^3.0|^4.0
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.5
- symfony/cache: ^8.1
This package is auto-updated.
Last update: 2026-06-04 22:43:10 UTC
README
FilterQ allows advanced filtering in Symfony APIs using Doctrine ORM. You can accept a single-line expression from your users like:
name=starter&(type=image|type=video)
And FilterQ will convert it to DQL WHERE conditions in your Doctrine QueryBuilder.
WHERE name = 'starter' AND (type = 'image' OR type = 'video')
FilterQ was built for Hyvor Blogs' Data API. It was initially written for Laravel Eloquent and later ported to Doctrine ORM.
Features
- Easy-to-write, single or multi-line expressions.
- Logical operators (
&and|) and nesting/grouping (with()) - Secure. FilterQ only gives access to the columns and operators you define.
- Supports joining related entities. Users can filter by joined entity fields.
- Supports "type hinting" for keys.
- Extensible. You can add your own operators easily (e.g., SQL
LIKE).
FilterQ Expressions
Example: (published_at > 1639665890 & published_at < 1639695890) | is_featured=true
A FilterQ Expression is a combination of conditions, connected and grouped using one or more of the following.
&- AND|- OR()- to group logic
A condition has three parts:
keyoperatorvalue
Key
Usually, a key maps to a DQL column reference (e.g., p.id, a.name). It should match [a-zA-Z0-9_.]+.
Operators
By default, the following operators are supported.
=- equals!=- not equal>- greater than<- less than>=- greater than or equals<=- less than or equals
Values
- null:
null - boolean:
trueorfalse - strings:
'hey'orhey - number:
250,-250, or2.5
Basic Usage
composer require hyvor/doctrine-filterq
use Hyvor\FilterQ\FilterQ; $qb = $entityManager->createQueryBuilder() ->select('p') ->from(Post::class, 'p'); $qb = FilterQ::expression('id=100|slug=hello') ->queryBuilder($qb) ->keys(function ($keys) { $keys->add('id', 'p.id'); $keys->add('slug', 'p.slug'); }) ->addWhere(); $posts = $qb->getQuery()->getResult();
The FilterQ::expression() static method is the entry point. The chain must end with addWhere(), which adds WHERE conditions to the QueryBuilder and returns it.
1. Setting the Expression and QueryBuilder
$qb = $em->createQueryBuilder()->select('p')->from(Post::class, 'p'); FilterQ::expression($request->query->get('filter')) ->queryBuilder($qb) // ...
addWhere() returns the Doctrine ORM QueryBuilder so you can continue chaining:
$posts = FilterQ::expression(...) ->queryBuilder($qb) ->keys(...) ->addWhere() ->setMaxResults(25) ->addOrderBy('p.id', 'DESC') ->getQuery() ->getResult();
2. Set Keys
Define all keys the user is allowed to filter on. This prevents SQL injection — only the columns you explicitly allow can be used in expressions.
->keys(function ($keys) { $keys->add('id', 'p.id'); $keys->add('slug', 'p.slug'); })
$keys->add($key, $column)registers a key and returns aKeyobject for further configuration.$columnis the DQL column reference — always include the entity alias (e.g.,p.id, not justid).Key::join()sets a join callback.Key::operators()sets allowed operators.Key::valueType()defines supported value types.Key::values()defines supported values.
Joins
To filter on a related entity's field, use a join callback. The callback receives the QueryBuilder.
FilterQ::expression('author.name=hyvor') ->queryBuilder($qb) ->keys(function ($keys) { $keys->add('author.name', 'a.name') ->join(function ($qb) { $qb->leftJoin('p.author', 'a'); }); }) ->addWhere();
Even if the same key appears multiple times in the expression, the join callback is only called once.
Key Operators
Restrict which operators are allowed for a key.
->keys(function ($keys) { // only allow these operators $keys->add('id', 'p.id')->operators('=,>,<'); // or use an array $keys->add('slug', 'p.slug')->operators(['=', '!=']); // exclude specific operators $keys->add('age', 'p.age')->operators('>', true); })
Key Value Types
Define the expected type for a key's value. Highly recommended for security and data integrity.
->keys(function ($keys) { $keys->add('id', 'p.id')->valueType('integer'); $keys->add('name', 'p.name')->valueType('string'); $keys->add('description', 'p.description')->valueType('string|null'); $keys->add('created_at', 'p.created_at')->valueType('date'); })
Supported types:
Scalar: int, float, string, null, bool
Special:
numeric— int, float, or numeric stringdate— a valid date/time string or Unix timestamp. Returns a\DateTimeImmutable. (Uses PHP'sstrtotime(), so relative dates like"-7 days"are supported.)
Multiple types can be combined with | or as an array:
$keys->add('created_at', 'p.created_at')->valueType('date|null'); // or $keys->add('created_at', 'p.created_at')->valueType(['date', 'null']);
Key Values
Restrict a key to a specific set of allowed values. Useful for enum columns.
->keys(function ($keys) { $keys->add('status', 'p.status')->values(['published', 'draft']); $keys->add('id', 'p.id')->values(200); })
Custom Operators
Add custom DQL operators. The callback receives the QueryBuilder, an auto-generated parameter name, and the value. It must bind the parameter and return the DQL expression string.
FilterQ::expression("title~'Hello%'") ->queryBuilder($qb) ->keys(function ($keys) { $keys->add('title', 'p.title'); }) ->operators(function ($operators) { $operators->add('~', 'LIKE'); }) ->addWhere();
For more complex cases, use a callback:
->operators(function ($operators) { $operators->add('!', function ($qb, string $paramName, mixed $value): string { $qb->setParameter($paramName, $value); return 'MATCH (p.title) AGAINST (:' . $paramName . ')'; }); })
The callback signature is (QueryBuilder $qb, string $paramName, mixed $value): string. It should:
- Bind the value with
$qb->setParameter($paramName, $value)if needed. - Return a DQL expression string.
Removing an Operator
->operators(function ($operators) { $operators->remove('>'); })
Exception Handling
FilterQ throws three exceptions, all extending FilterQException:
Hyvor\FilterQ\Exceptions\FilterQException— base exceptionHyvor\FilterQ\Exceptions\ParserException— invalid expression syntaxHyvor\FilterQ\Exceptions\InvalidValueException— value fails key value/type validation
use Hyvor\FilterQ\Exceptions\FilterQException; try { $qb = FilterQ::expression($filter) ->queryBuilder($qb) ->keys(...) ->addWhere(); } catch (FilterQException $e) { // $e->getMessage() is safe to display to API consumers }