fivelab / ruler
Apply string filter rules to Doctrine ORM, Elasticsearch/Elastica and ClickHouse query builders.
Requires
- php: ~8.2
Requires (Dev)
- doctrine/orm: ~2.10 | ~3.0
- escapestudios/symfony2-coding-standard: ~3.15.0
- phpmetrics/phpmetrics: ~3.0
- phpstan/phpstan: ~2.0
- phpunit/phpunit: ~11.5
- ruflin/elastica: ~7.3.2
Suggests
- doctrine/orm: To apply rules to a Doctrine ORM QueryBuilder (DoctrineOrmTarget).
- ruflin/elastica: To apply rules to Elasticsearch/OpenSearch queries (ElasticaTarget).
Provides
None
Conflicts
None
Replaces
None
README
#StandWithUkraineRuler
Write a filter as a single string rule and apply it to different query builders — Doctrine ORM, Elasticsearch / OpenSearch (via Elastica or the native clients) and ClickHouse — without rewriting the condition for each backend.
$ruler->apply($queryBuilder, 'category.key in (:categories) and price > :price', [ 'categories' => ['cat1', 'cat2'], 'price' => 100, ]);
The same rule string produces a DQL WHERE for Doctrine, a bool query for Elasticsearch, or a
WHERE clause for ClickHouse — you only change the target you pass in.
Why Ruler?
- One rule, many backends. Reuse a filter across your database and your search index.
- Safe values. Rule values are always bound as query parameters, never concatenated into the query.
- Composable. Build rules from reusable specifications (
and/or, per-target overrides). - Extensible. Add your own operators or targets through small interfaces.
- Maintained. PHP 8.2+, Doctrine ORM 2 & 3, actively developed — a drop-in idea for the
unmaintained
kphoen/rulerz.
Installation
composer require fivelab/ruler
Install the packages for the targets you use (they are declared as suggest):
composer require doctrine/orm # for the Doctrine ORM target composer require ruflin/elastica # for the Elasticsearch / OpenSearch target
Usage
Create a Ruler for the target(s) you need, then apply() a rule to a query object.
Doctrine ORM
use FiveLab\Component\Ruler\Ruler; use FiveLab\Component\Ruler\Target\DoctrineOrmTarget; $ruler = new Ruler(new DoctrineOrmTarget()); $qb = $entityManager->createQueryBuilder() ->select('products') ->from(Product::class, 'products'); $ruler->apply($qb, 'category.key in (:categories) and price > :price', [ 'categories' => ['cat1', 'cat2'], 'price' => 100, ]); // The query builder now has the WHERE condition, the parameters and the joins applied. $products = $qb->getQuery()->getResult();
Joins are detected automatically from the dotted path: category.key adds a LEFT JOIN on the
category association and filters on its key field. Nested associations (variants.category.key)
are supported too.
Elasticsearch / OpenSearch
For the ruflin/elastica client, pass an Elastica\Query:
use Elastica\Query; use FiveLab\Component\Ruler\Ruler; use FiveLab\Component\Ruler\Target\ElasticaTarget; $ruler = new Ruler(new ElasticaTarget()); $query = new Query(); $query->setSize(20); $ruler->apply($query, 'price > :price and tag = :tag', [ 'price' => 100, 'tag' => 'sale', ]); $results = $index->search($query);
For the native elasticsearch/elasticsearch or opensearch-project/opensearch-php clients, use
RawSearchQuery and read the built body:
use FiveLab\Component\Ruler\Query\RawSearchQuery; $query = new RawSearchQuery(); $ruler->apply($query, 'price > :price', ['price' => 100]); $response = $client->search([ 'index' => 'products', 'body' => $query->toArray(), ]);
apply()only sets thequerypart, sosize,sort,aggs, etc. are preserved. Calling it several times combines the conditions withbool.must.
ClickHouse
The ClickHouse target builds a WHERE string and its parameters for you to embed in your own SQL
(placeholders use the smi2/phpclickhouse format):
use FiveLab\Component\Ruler\Query\ClickHouseQuery; use FiveLab\Component\Ruler\Ruler; use FiveLab\Component\Ruler\Target\ClickHouseTarget; $ruler = new Ruler(new ClickHouseTarget()); $query = new ClickHouseQuery(); $ruler->apply($query, 'shop = :shop and amount > :amount', [ 'shop' => 'foo', 'amount' => 100, ]); $where = $query->getWhere(); // ((shop = :shop) AND (amount > :amount)) $parameters = $query->getParameters(); // ['shop' => 'foo', 'amount' => 100]
Multiple targets at once
Wrap several targets in Targets and reuse one Ruler for all of them; it picks the right
executor by the query object you pass:
use FiveLab\Component\Ruler\Target\Targets; $ruler = new Ruler(new Targets( new DoctrineOrmTarget(), new ElasticaTarget(), new ClickHouseTarget() )); $ruler->apply($doctrineQb, $rule, $params); $ruler->apply($elasticaQuery, $rule, $params);
Rule syntax
A rule is a string of conditions combined with and / or. Values are always passed as named
parameters (:name); the array you pass to apply() provides them.
Operators
| Category | Operators |
|---|---|
| Comparison | =, !=, <, <=, >, >= |
| Set | in (:param), not in (:param) |
| Text | like |
| Logical | and, or |
| Arithmetic | +, -, *, / |
- Grouping: use parentheses —
(a = :a or b = :b) and c > :c. - Arithmetic (
+,-,*,/) is available for the SQL targets (Doctrine ORM, ClickHouse). - Constants: integers, floats,
true,falseandnullmay be written inline (published = true,price > 100). - Null:
field = null/field != nullbecomeIS NULL/IS NOT NULLfor SQL targets and anexistscheck for Elasticsearch (it has noNULL). - Nested paths: a dot builds joins (Doctrine) or a nested query (Elasticsearch). To treat a dot
as part of the field name, escape it:
money\.amount.
Good to know
A few current constraints of the parser:
- Operators must be surrounded by spaces:
price > :price, notprice>:price. - String and negative-number values cannot be written inline — pass them as parameters
(
name = :name, notname = 'John';price > :min, notprice > -5). - Elasticsearch nested paths support a single level (
variants.name).
Specifications
Rules can be wrapped in reusable, composable specifications and applied with applySpec():
use FiveLab\Component\Ruler\Specification\AndX; use FiveLab\Component\Ruler\Specification\SimpleSpecification; $specification = new AndX( new SimpleSpecification('shop = :shop', ['shop' => 'foo']), new SimpleSpecification('amount > :amount', ['amount' => 100]) ); $ruler->applySpec($query, $specification);
OrX, EmptySpecification and TargetableSpecification (a single specification that carries a
different rule per target, resolved with SpecificationFilter::filterByTarget()) are available too.
Extending
- Custom operators — implement
OperatorsConfiguratorInterfaceand register handlers on theOperatorscollection. - Custom targets — implement
TargetInterface(orIdentifiableTargetInterface) to support another query builder.
Security
- Rule values are safe. Values from the parameters array are passed to the backend as bound parameters (SQL) or structured values (Elasticsearch) — never concatenated into the query string — so they are safe from injection.
- The rule string is code, not input. Field names and operators from the rule string are interpreted and written into the query. Do not build the rule string from untrusted user input. If users drive the filtering, keep the rule template static and let them supply only parameter values, or validate field names against an allow-list.
To report a vulnerability, see SECURITY.md.
Migrating from RulerZ
kphoen/rulerz solves a similar problem but has had no release
since 2018. Ruler is a maintained alternative for the Doctrine ORM and Elasticsearch use cases, with
ClickHouse support added. A few differences to keep in mind when migrating:
- Ruler mutates your query object in place via
apply()(RulerZ returns filtered results fromfilter()); you keep full control of the query builder. - Values must be passed as named parameters — inline literals in the rule string are not supported.
- Ruler has no in-memory/array target yet; it targets query builders (Doctrine ORM, Elasticsearch, ClickHouse).
Requirements
- PHP
~8.2 doctrine/orm~2.10 || ~3.0— for the Doctrine ORM targetruflin/elastica~7.3— for the Elasticsearch / OpenSearch target
Development
For easy development you can use Docker.
docker build -t ruler . docker run -it -v $(pwd):/code --name ruler ruler bash
After the container starts, install the vendors:
composer update
Before opening a PR, please run the checks:
./bin/phpunit ./bin/phpstan ./bin/phpcs --standard=src/phpcs.xml src/ ./bin/phpcs --standard=tests/phpcs.xml tests/
License
Ruler is released under the MIT License.