Framework Bridges
Bridges allow you to apply derafu/query expressions to existing third-party query builder instances — without replacing them. You keep your existing query builder and add Derafu filters on top.
All bridges accept a parsed ConditionInterface | CompositeConditionInterface and apply it to the query builder. You parse expressions with CompositeExpressionParser and pass the result to the bridge.
Shared Setup: The Expression Parser
All bridges share the same expression parser setup:
use Derafu\Query\Filter\CompositeExpressionParser;
use Derafu\Query\Filter\ExpressionParser;
use Derafu\Query\Filter\FilterParser;
use Derafu\Query\Filter\PathParser;
use Derafu\Query\Operator\OperatorLoader;
use Derafu\Query\Operator\OperatorManager;
$loader = new OperatorLoader();
$manager = new OperatorManager(
$loader->loadFromFile('vendor/derafu/query/resources/operators.yaml')
);
$parser = new CompositeExpressionParser(
new ExpressionParser(new PathParser(), new FilterParser($manager))
);
Doctrine DBAL Bridge
Class: Derafu\Query\Bridge\DoctrineDBALQueryBuilderConditionApplier
Applies conditions to a Doctrine DBAL QueryBuilder. Supports all operators including date, regex, and bitwise operators.
Methods
public function apply(object $queryBuilder, ConditionInterface|CompositeConditionInterface $condition): void
public function applyHaving(object $queryBuilder, ConditionInterface|CompositeConditionInterface $condition): void
Usage
use Derafu\Query\Bridge\DoctrineDBALQueryBuilderConditionApplier;
$applier = new DoctrineDBALQueryBuilderConditionApplier();
// Parse the expression.
$condition = $parser->parse('status?=active&&total?>1000');
// Apply to an existing DBAL QueryBuilder.
$applier->apply($dbalQb, $condition);
// The QB now has: WHERE (status = :param_status_... AND total > :param_total_...)
$rows = $dbalQb->executeQuery()->fetchAllAssociative();
FROM and JOIN Inference
When a condition uses multi-segment paths, the bridge:
- Infers
FROMfrom the first segment of the first multi-segment path, if noFROMhas been set yet. - Generates JOINs from intermediate segments, with deduplication (same alias → skip).
// No from() call needed — it's inferred from the path.
$condition = $parser->parse(
'customers[alias:c]__invoices[on:id=customer_id,alias:i]__total?>1000'
);
$applier->apply($dbalQb, $condition);
// Sets FROM customers AS c, adds INNER JOIN invoices AS i ON c.id = i.customer_id
// WHERE i.total > :p
Driver Detection
The bridge reads the database platform from the DBAL connection (via reflection on the internal connection property) and maps it to the driver name used by SqlBuilderWhere:
| Platform | Driver |
|---|---|
PostgreSQLPlatform |
pgsql |
MySQLPlatform |
mysql |
SQLitePlatform |
sqlite |
SQLServerPlatform |
sqlsrv |
OraclePlatform |
oci |
| Other | pgsql (fallback) |
HAVING
$dbalQb->groupBy('status');
$condition = $parser->parse('COUNT(*)?>1');
$applier->applyHaving($dbalQb, $condition);
Doctrine ORM Bridge
Class: Derafu\Query\Bridge\DoctrineORMQueryBuilderConditionApplier
Applies conditions to a Doctrine ORM QueryBuilder as DQL. This bridge generates DQL-compatible fragments — it does not produce raw SQL.
Methods
public function apply(object $queryBuilder, ConditionInterface|CompositeConditionInterface $condition): void
public function applyHaving(object $queryBuilder, ConditionInterface|CompositeConditionInterface $condition): void
Usage
use Derafu\Query\Bridge\DoctrineORMQueryBuilderConditionApplier;
$applier = new DoctrineORMQueryBuilderConditionApplier();
$condition = $parser->parse('status?=active&&total?>1000');
$ormQb = $em->createQueryBuilder()
->select('i')
->from(Invoice::class, 'i');
$applier->apply($ormQb, $condition);
$invoices = $ormQb->getQuery()->getResult();
Single-Segment Path Qualification
Single-segment paths like status are automatically qualified with the root entity alias. The root alias is read from the first FROM clause:
status?=active → i.status = :param_status_... (when root alias is 'i')
Multi-segment paths are left as-is, allowing explicit alias control.
JOIN Generation
The ORM bridge adds JOINs to the QueryBuilder using Doctrine’s association names — the on: option in path segments is ignored. Doctrine derives join conditions from entity mappings.
invoices[alias:i]__payments[alias:p]__status
If Invoice has a payments association, the bridge calls:
$ormQb->innerJoin('i.payments', 'p');
EXISTS Paths in DQL
Subquery paths (___) are translated to DQL:
| Expression | DQL |
|---|---|
___payments?is:empty |
SIZE(i.payments) = 0 |
___payments?isnot:empty |
SIZE(i.payments) > 0 |
___payments__status?=pending |
EXISTS(SELECT _payments0.id FROM App\Entity\Payment _payments0 WHERE _payments0.invoice = i AND _payments0.status = :p) |
___invoices__AVG(total)?<1000 |
(SELECT AVG(_i0.total) FROM App\Entity\Invoice _i0 WHERE _i0.customer = i) < :p |
___invoices__COUNT(*)?=0 |
SIZE(i.invoices) = 0 (optimized) |
DQL-Incompatible Operators
The following operator types throw UnsupportedOperatorException when used with this bridge:
datetype:date:,month:,year:,period:— require SQL functions not available in DQL.binarytype:b&,b|,b^, etc. — bitwise SQL not supported in DQL.regexptype:~,~*,similarto:, etc. — database-specific regex not in DQL.ilike:andnotilike:— ILIKE is not DQL.
In the API Platform SmartFilter, these exceptions are caught and the filter is silently skipped.
Illuminate (Laravel) Bridge
Class: Derafu\Query\Bridge\IlluminateQueryBuilderConditionApplier
Applies conditions to Illuminate’s Query\Builder or Eloquent’s Builder. Both are accepted — Eloquent builders are resolved to their underlying Query\Builder via toBase().
Methods
public function apply(object $queryBuilder, ConditionInterface|CompositeConditionInterface $condition): void
public function applyHaving(object $queryBuilder, ConditionInterface|CompositeConditionInterface $condition): void
Usage
use Derafu\Query\Bridge\IlluminateQueryBuilderConditionApplier;
$applier = new IlluminateQueryBuilderConditionApplier();
$condition = $parser->parse('status?=active&&total?>1000');
// With a plain Query\Builder.
$qb = DB::table('invoices');
$applier->apply($qb, $condition);
$rows = $qb->get();
// With an Eloquent Builder.
$builder = Invoice::query();
$applier->apply($builder, $condition);
$invoices = $builder->get();
FROM and JOIN Inference
Same behavior as the DBAL bridge: infers the FROM table from multi-segment paths and adds join(), leftJoin(), or rightJoin() calls as needed. Duplicate joins (same table reference) are skipped.
Driver Detection
The bridge reads the driver name directly from Connection::getDriverName():
| Driver name | SQL generated |
|---|---|
pgsql |
PostgreSQL SQL |
mysql |
MySQL SQL |
sqlite |
SQLite SQL |
HAVING
$condition = $parser->parse('AVG(price)?>500');
$applier->applyHaving($qb, $condition);
// Calls: $qb->havingRaw($sql, $params)
API Platform Bridge
Class: Derafu\Query\Bridge\ApiPlatform\SmartFilter
Integrates derafu/query as an API Platform filter via the QueryParameter attribute. When a request comes in, the filter builds a Derafu expression from the parameter’s property name and value, parses it, and applies it to the ORM QueryBuilder.
Registration
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\QueryParameter;
use Derafu\Query\Bridge\ApiPlatform\SmartFilter;
#[ApiResource]
#[QueryParameter(key: 'price', property: 'price', filter: SmartFilter::class)]
#[QueryParameter(key: 'status', property: 'status', filter: SmartFilter::class)]
class Product
{
// …
}
URL Filter Syntax
The URL parameter value is the filter part (operator + value); the property name provides the path segment:
GET /api/products?price=>1000&status=in:paid,issued
The SmartFilter builds expressions internally:
priceproperty +>1000value → expressionprice?>1000statusproperty +in:paid,issuedvalue → expressionstatus?in:paid,issued
Generic SmartFilter Property
For a single parameter that accepts a full composite expression, use the special __derafu_smart_filter property name:
#[QueryParameter(key: 'filter', property: '__derafu_smart_filter', filter: SmartFilter::class)]
GET /api/products?filter=status?=active&&price?>1000
Dependency Injection (Symfony)
Register CompositeExpressionParser and DoctrineORMQueryBuilderConditionApplier as services and inject them into SmartFilter:
# services.yaml
services:
Derafu\Query\Bridge\ApiPlatform\SmartFilter:
arguments:
$compositeParser: '@Derafu\Query\Filter\Contract\CompositeExpressionParserInterface'
$applier: '@Derafu\Query\Bridge\DoctrineORMQueryBuilderConditionApplier'
Error Handling
SmartFilter silently catches two exceptions:
UnsupportedOperatorException: DQL-incompatible operators (date:,b&,ilike:, regex) — the filter is skipped.- Any
Throwable: Malformed expressions — the filter is skipped.
This prevents a bad filter parameter from crashing the entire collection endpoint.
Bridge Comparison
| Feature | Doctrine DBAL | Doctrine ORM | Illuminate |
|---|---|---|---|
| SQL operators (all) | ✅ | ✅† | ✅ |
| Date operators | ✅ | ❌ | ✅ |
| Regex operators | ✅ | ❌ | ✅ |
| Bitwise operators | ✅ | ❌ | ✅ |
ilike: / notilike: |
✅ | ❌ | ✅ |
| FROM inference from paths | ✅ | ❌‡ | ✅ |
| JOIN from path segments | ✅ | ✅ (ORM assoc) | ✅ |
| EXISTS paths | ✅ | ✅ (SIZE/EXISTS DQL) | ✅ |
| Aggregate subqueries | ✅ | ✅ | ✅ |
† Compatible operators only — DQL-incompatible operators throw UnsupportedOperatorException.
‡ ORM bridge requires an explicit FROM / from() call before applying conditions.