Operators Reference
Every operator has a symbol — the prefix that appears immediately after ? in an expression. The filter parser matches operators longest-first, so !~~* is matched before !~~ and !~* before !~.
This page documents all built-in operators grouped by type.
Value Format Conventions
| Placeholder | Meaning |
|---|---|
VALUE |
Any string value |
PATTERN |
A LIKE pattern (may contain % and _) |
DATE |
YYYYMMDD or YYMMDD |
MONTH |
MM or M (1–12) |
YEAR |
YYYY or YY |
PERIOD |
YYYYMM or YYMM |
INT |
Non-negative integer (no decimals) |
V1,V2 |
Two comma-separated values |
V1,V2,… |
One or more comma-separated values |
| (empty) | No value; the symbol alone is the full filter |
Standard Comparison Operators
Map directly to SQL comparison operators. Accept any string or numeric value.
| Symbol | Name | Value | SQL Template |
|---|---|---|---|
= |
Equals | VALUE |
{{column}} = {{value}} |
!= |
Not Equal | VALUE |
{{column}} != {{value}} |
! |
Not Equal (shorthand) | VALUE |
alias of != |
<> |
Not Equal (SQL standard) | VALUE |
alias of != |
>= |
Greater Than or Equal | VALUE |
{{column}} >= {{value}} |
<= |
Less Than or Equal | VALUE |
{{column}} <= {{value}} |
> |
Greater Than | VALUE |
{{column}} > {{value}} |
< |
Less Than | VALUE |
{{column}} < {{value}} |
Examples
price?=1000 → price = :p (:p = '1000')
status?!=archived → status != :p (:p = 'archived')
total?>=500 → total >= :p (:p = '500')
created_at?<2025-01-01 → created_at < :p (:p = '2025-01-01')
Tip: Standard operators accept string operands for numbers, text and ISO dates. The database interprets them in the comparison context. For correlated COUNT, SUM and AVG comparisons, the SQL compiler explicitly casts PostgreSQL parameters to NUMERIC, including list/range operands. Thus COUNT(*)?>1.5 remains a decimal comparison, without truncating the value or converting it to a PHP float. The ORM bridge also uses mapped field types to preserve numeric versus text/date aggregate semantics.
AutoLike Operators (Automatic Pattern Generation)
These operators accept a plain text value and wrap it in % wildcards automatically before binding. They delegate to the like: / ilike: family for the actual SQL.
Starts With
| Symbol | Case | Value | Bound value | SQL |
|---|---|---|---|---|
^ |
Sensitive | VALUE |
VALUE% |
col LIKE :p (pgsql/sqlite), col LIKE BINARY :p (mysql) |
^* |
Insensitive | VALUE |
VALUE% |
col ILIKE :p (pgsql), col LIKE :p (mysql/sqlite) |
!^ |
Sensitive | VALUE |
VALUE% |
col NOT LIKE :p |
!^* |
Insensitive | VALUE |
VALUE% |
col NOT ILIKE :p / col NOT LIKE :p |
Contains
| Symbol | Case | Value | Bound value | SQL |
|---|---|---|---|---|
~~ |
Sensitive | VALUE |
%VALUE% |
col LIKE :p |
~~* |
Insensitive | VALUE |
%VALUE% |
col ILIKE :p / col LIKE :p |
!~~ |
Sensitive | VALUE |
%VALUE% |
col NOT LIKE :p |
!~~* |
Insensitive | VALUE |
%VALUE% |
col NOT ILIKE :p / col NOT LIKE :p |
Ends With
| Symbol | Case | Value | Bound value | SQL |
|---|---|---|---|---|
$ |
Sensitive | VALUE |
%VALUE |
col LIKE :p |
$* |
Insensitive | VALUE |
%VALUE |
col ILIKE :p / col LIKE :p |
!$ |
Sensitive | VALUE |
%VALUE |
col NOT LIKE :p |
!$* |
Insensitive | VALUE |
%VALUE |
col NOT ILIKE :p / col NOT LIKE :p |
Examples
name?^John → name LIKE :p (:p = 'John%')
name?$*LLC → name ILIKE :p (:p = '%LLC') (pgsql)
description?~~keyword → description LIKE :p (:p = '%keyword%')
title?!~~*draft → title NOT ILIKE :p (:p = '%draft%') (pgsql)
Note on case sensitivity: PostgreSQL natively supports ILIKE; MySQL uses LIKE BINARY for case-sensitive and plain LIKE for case-insensitive; SQLite uses plain LIKE for both (SQLite’s LIKE is case-insensitive by default for ASCII).
Pattern (LIKE) Operators
Explicit LIKE operators where you supply the full pattern including % and _ wildcards.
| Symbol | Case | Value | SQL |
|---|---|---|---|
like: |
Sensitive | PATTERN |
pgsql: col LIKE :p · mysql: col LIKE BINARY :p · sqlite: col LIKE :p |
notlike: |
Sensitive | PATTERN |
pgsql: col NOT LIKE :p · mysql: col NOT LIKE BINARY :p |
ilike: |
Insensitive | PATTERN |
pgsql: col ILIKE :p · mysql: col LIKE :p · sqlite: col LIKE :p |
notilike: |
Insensitive | PATTERN |
pgsql: col NOT ILIKE :p · mysql/sqlite: col NOT LIKE :p |
Examples
email?like:%.example.com → email LIKE :p (:p = '%.example.com')
name?ilike:jo_n% → name ILIKE :p (:p = 'jo_n%') (pgsql)
code?notlike:TEST_% → code NOT LIKE BINARY :p (mysql)
For Doctrine ORM on MySQL, case-sensitive LIKE templates use the automatically registered DERAFU_MYSQL_BINARY_STRING function instead of embedding BINARY in DQL. The same applies to inherited auto-pattern operators.
Note: ilike: and notilike:, including aliases such as ^*, ~~*, $* and their negations, are not supported in Doctrine ORM DQL. They require SQL/DBAL/Illuminate compilation.
List Operators
Work with comma-separated lists of values. The list is split and each item becomes a separate named parameter.
| Symbol | Value | SQL |
|---|---|---|
in: |
V1,V2,… |
{{column}} IN (:p1, :p2, …) |
notin: |
V1,V2,… |
{{column}} NOT IN (:p1, :p2, …) |
Value Format
Values are split at every comma. The compiler does not unescape \, or treat it as a literal comma inside an item.
status?in:active,pending,review → status IN (:p1, :p2, :p3)
Although the validation pattern accepts \,, in:a\,b,c\,d binds four values: a\, b, c\ and d. Do not use backslash escaping to represent list items containing commas.
Validation
The value must contain at least one item. Each item may contain word characters, dots, hyphens, and Unicode letters/marks. Whitespace inside values is not supported.
Examples
status?in:paid,issued → status IN (:p1, :p2)
category?notin:archived,draft → category NOT IN (:p1, :p2)
id?in:1,2,3,4,5 → id IN (:p1, :p2, :p3, :p4, :p5)
Range Operators
Filter values between two bounds (inclusive) or outside them.
| Symbol | Value | SQL |
|---|---|---|
between: |
V1,V2 |
{{column}} BETWEEN {{value_1}} AND {{value_2}} |
notbetween: |
V1,V2 |
{{column}} NOT BETWEEN {{value_1}} AND {{value_2}} |
Value Format
Exactly two comma-separated values. Each value may contain word characters, dots, and hyphens (no spaces).
Examples
price?between:100,500 → price BETWEEN :p1 AND :p2
price?notbetween:0,10 → price NOT BETWEEN :p1 AND :p2
created_at?between:2024-01-01,2024-12-31 → created_at BETWEEN :p1 AND :p2
Note: BETWEEN is inclusive on both ends. For exclusive ranges, use > and < separately.
Date Operators
Specialized operators for date and time filtering. They apply SQL date functions and normalize the value.
date: — Specific Date
date:YYYYMMDD or date:YYMMDD
Matches rows where the date portion of a datetime column equals a specific day.
| Engine | SQL |
|---|---|
| All | DATE({{column}}) = {{value}} |
Value normalization:
YYYYMMDD(8 digits) →YYYY-MM-DDYYMMDD(6 digits) →20YY-MM-DD
created_at?date:20240823 → DATE(created_at) = :p (:p = '2024-08-23')
created_at?date:240101 → DATE(created_at) = :p (:p = '2024-01-01')
month: — Month Number
month:MM or month:M
Matches rows in a specific calendar month, regardless of year.
| Engine | SQL |
|---|---|
| All | MONTH({{column}}) = {{value}} |
Value normalization: zero-padded to 2 digits (8 → 08).
created_at?month:08 → MONTH(created_at) = :p (:p = '08')
created_at?month:3 → MONTH(created_at) = :p (:p = '03')
year: — Year
year:YYYY or year:YY
Matches rows in a specific year.
| Engine | SQL |
|---|---|
| All | YEAR({{column}}) = {{value}} |
Value normalization: 2-digit years are expanded to 20YY.
created_at?year:2024 → YEAR(created_at) = :p (:p = '2024')
created_at?year:24 → YEAR(created_at) = :p (:p = '2024')
period: — Year and Month
period:YYYYMM or period:YYMM
Matches rows in a specific year+month.
| Engine | SQL |
|---|---|
| pgsql | TO_CHAR({{column}}, "YYYYMM") = {{value}} |
| mysql | DATE_FORMAT({{column}}, "%Y%m") = {{value}} |
| sqlite | strftime("%Y%m", {{column}}) = {{value}} |
Value normalization: 4-digit YYMM is expanded to 20YYMM.
created_at?period:202408 → strftime("%Y%m", created_at) = :p (:p = '202408') (sqlite)
created_at?period:2408 → … (:p = '202408')
date:, month: and year: emit the universal SQL templates shown above; MONTH() and YEAR() are not rewritten for each engine. Database function availability still matters, so these templates alone do not guarantee PostgreSQL/SQLite portability.
Note: Date operators use SQL functions (DATE(), MONTH(), etc.) and are not compatible with Doctrine ORM DQL. Use the Doctrine DBAL or Illuminate bridges for date filtering.
NULL Operators
| Symbol | Alias of | SQL |
|---|---|---|
is:null |
— | {{column}} IS NULL |
<=> |
is:null |
{{column}} IS NULL |
isnot:null |
— | {{column}} IS NOT NULL |
Value Format
The symbol is the complete filter — no value is expected after the symbol. is:null means the expression is literally column?is:null.
Examples
deleted_at?is:null → deleted_at IS NULL
deleted_at?<=> → deleted_at IS NULL (alternative syntax)
email?isnot:null → email IS NOT NULL
Note: NULL operators produce no bound parameters.
Subquery Operators
Used exclusively with exists paths (___). They determine whether the correlated subquery checks for existence or absence.
| Symbol | SQL |
|---|---|
is:empty |
NOT EXISTS (SELECT 1 FROM … WHERE …) |
isnot:empty |
EXISTS (SELECT 1 FROM … WHERE …) |
___payments[on:id=invoice_id]?is:empty
→ NOT EXISTS (SELECT 1 FROM payments WHERE invoices.id = payments.invoice_id)
___payments[on:id=invoice_id]?isnot:empty
→ EXISTS (SELECT 1 FROM payments WHERE invoices.id = payments.invoice_id)
In Doctrine ORM DQL, a single collection association uses SIZE(alias.assoc) = 0 or SIZE(alias.assoc) > 0. Single-valued and nested associations use correlated NOT EXISTS / EXISTS based on their mappings.
Regular Expression Operators
These operators match POSIX extended regular expressions. Support varies by engine.
Core Operators
| Symbol | Case | SQL (pgsql) | SQL (mysql) |
|---|---|---|---|
~ |
Sensitive | {{column}} ~ {{value}} |
REGEXP_LIKE({{column}}, {{value}}, 'c') |
~* |
Insensitive | {{column}} ~* {{value}} |
REGEXP_LIKE({{column}}, {{value}}, 'i') |
!~ |
Sensitive | {{column}} !~ {{value}} |
NOT REGEXP_LIKE({{column}}, {{value}}, 'c') |
!~* |
Insensitive | {{column}} !~* {{value}} |
NOT REGEXP_LIKE({{column}}, {{value}}, 'i') |
SQLite does not have built-in regex support — these operators have no SQLite template and will fail if used against a SQLite connection.
MySQL templates use REGEXP_LIKE with explicit c (case-sensitive) or i (case-insensitive) matching. They target MySQL 8 and do not convert the operands to binary strings. These templates do not establish compatibility with MySQL 5.7 or MariaDB.
MySQL Aliases
| Symbol | Alias of |
|---|---|
regexp: |
~ |
notregexp: |
!~ |
rlike: |
~ |
notrlike: |
!~ |
These are provided for familiarity with MySQL’s SQL syntax:
name?regexp:^John → REGEXP_LIKE(name, :p, 'c') (mysql)
name?rlike:John.* → REGEXP_LIKE(name, :p, 'c') (mysql)
PostgreSQL SIMILAR TO
| Symbol | SQL |
|---|---|
similarto: |
{{column}} SIMILAR TO {{value}} |
notsimilarto: |
{{column}} NOT SIMILAR TO {{value}} |
SIMILAR TO uses SQL-standard regex patterns (a hybrid of LIKE wildcards and regex). It is PostgreSQL-specific.
Pattern rules: may contain %, _, [a-z], (a\|b), ?, *, +. Unbalanced (, ), [, ] are rejected at parse time.
name?similarto:John% → name SIMILAR TO :p
code?similarto:[0-9]+ → code SIMILAR TO :p
status?notsimilarto:(a|b)% → status NOT SIMILAR TO :p
Examples
email?~@example\.com → email ~ :p (pgsql)
name?~*john → name ~* :p (pgsql) / REGEXP_LIKE(name, :p, 'i') (mysql)
phone?!~^\+ → phone !~ :p (pgsql)
Validation: Regex patterns must contain at least one non-metacharacter character to prevent trivially-matching expressions.
Note: Regex operators are not compatible with Doctrine ORM DQL.
Bitwise Operators
Operate on integer values at the bit level. All values must be non-negative integers (no decimals).
| Symbol | Name | Value | SQL |
|---|---|---|---|
b& |
Bitwise AND | INT |
pgsql/sqlite: col & :p · mysql: BIT_AND(col, :p) |
b| |
Bitwise OR | INT |
all: col | :p |
b^ |
Bitwise XOR | INT |
pgsql: col # :p · mysql: BIT_XOR(col, :p) · sqlite: (col | :p) & ~(col & :p) |
b<< |
Left Shift | INT |
pgsql/sqlite: col << :p · mysql: << col, :p |
b>> |
Right Shift | INT |
pgsql/sqlite: col >> :p · mysql: >> col, :p |
b&~ |
AND NOT | INT |
all: col & ~( :p ) |
Common Use Case: Feature Flags
Bitwise AND is commonly used to test whether a flag bit is set in a bitmask column:
flags?b&1 → flags & :p (:p = '1') — tests bit 0 (value 1)
flags?b&2 → flags & :p (:p = '2') — tests bit 1 (value 2)
flags?b&4 → flags & :p (:p = '4') — tests bit 2 (value 4)
Examples
permissions?b&4 → permissions & :p (:p = '4') (pgsql)
permissions?b|3 → permissions | :p (:p = '3')
version?b^255 → (version | :p) & ~(version & :p) (sqlite)
offset?b<<2 → offset << :p (:p = '2')
mask?b&~8 → mask & ~( :p ) (:p = '8')
Validation: Non-integer values (letters, decimals like 1.5) are rejected at parse time.
Note: Bitwise operators are not compatible with Doctrine ORM DQL.
Operator Aliases
Several operators are aliases that reuse another operator’s SQL template:
| Symbol | Alias of | Reason |
|---|---|---|
! |
!= |
Shorthand not-equal |
<> |
!= |
SQL-standard not-equal |
<=> |
is:null |
MySQL NULL-safe equality syntax |
^ |
like: |
auto-casts with like_start |
^* |
ilike: |
auto-casts with like_start |
!^ |
notlike: |
auto-casts with like_start |
!^* |
notilike: |
auto-casts with like_start |
~~ |
like: |
auto-casts with like |
~~* |
ilike: |
auto-casts with like |
!~~ |
notlike: |
auto-casts with like |
!~~* |
notilike: |
auto-casts with like |
$ |
like: |
auto-casts with like_end |
$* |
ilike: |
auto-casts with like_end |
!$ |
notlike: |
auto-casts with like_end |
!$* |
notilike: |
auto-casts with like_end |
regexp: |
~ |
MySQL familiarity |
notregexp: |
!~ |
MySQL familiarity |
rlike: |
~ |
MySQL familiarity |
notrlike: |
!~ |
MySQL familiarity |
Casting Rules
Casting rules transform the raw value string before it is bound as a parameter:
| Rule | Input | Output |
|---|---|---|
like_start |
John |
John% |
like |
John |
%John% |
like_end |
Smith |
%Smith |
list |
a,b,c |
['a', 'b', 'c'] (split) |
date |
20240823 |
2024-08-23 |
date |
240823 |
2024-08-23 |
month |
8 |
08 |
year |
24 |
2024 |
period |
202408 |
202408 (unchanged) |
period |
2408 |
202408 |
DQL Compatibility Matrix
The Doctrine ORM bridge generates DQL, which does not support all SQL features:
| Operator type | DQL compatible? |
|---|---|
Standard (=, !=, >, etc.) |
✅ |
Case-sensitive AutoLike (^, ~~, $ and negations) |
✅ |
like:, notlike: |
✅ |
ilike:, notilike: and their auto-pattern aliases |
❌ |
in:, notin: |
✅ |
between:, notbetween: |
✅ |
Date (date:, month:, year:, period:) |
❌ |
NULL (is:null, isnot:null) |
✅ |
Subquery (is:empty, isnot:empty) |
✅ (SIZE for a single collection; otherwise EXISTS) |
RegExp (~, ~*, similarto:, etc.) |
❌ |
Binary (b&, b|, etc.) |
❌ |
For incompatible operators, use the Doctrine DBAL bridge or IlluminateQueryBuilderConditionApplier instead. In API Platform’s SmartFilter, incompatible operators are silently skipped.
Custom Operators
Operators are defined in resources/operators.yaml. You can load a custom YAML file to add or override operators:
# my-operators.yaml
version: '1.0.0'
description: 'Custom operators'
types:
standard:
name: 'Standard Operators'
description: 'SQL comparison operators'
operators:
'=':
type: standard
name: Equals
description: Exact match
sql: '{{column}} = {{value}}'
'startswith:':
type: like
name: Starts With (verbose)
description: Explicit starts-with LIKE operator
sql: '{{column}} LIKE {{value}}'
cast: ['like_start']
use Derafu\Query\Operator\OperatorLoader;
use Derafu\Query\Operator\OperatorManagerFactory;
$factory = new OperatorManagerFactory(new OperatorLoader());
$manager = $factory->create('/path/to/my-operators.yaml');
Required fields per operator: type, name, description, all strings. The type key must reference an entry in the types section. Operator symbols must be strings. An alias must refer to an operator loaded earlier in the same file.
SQL Template Forms
sql accepts a universal string or a map keyed by engine. Each engine entry accepts a string or a map containing a required string default and an optional string dql:
sql:
pgsql: '{{column}} LIKE {{value}}'
mysql:
default: '{{column}} LIKE BINARY {{value}}'
dql: '{{column}} LIKE DERAFU_MYSQL_BINARY_STRING({{value}})'
sqlite: '{{column}} LIKE {{value}}'
| Configuration | SQL compilation | Doctrine ORM |
|---|---|---|
Universal sql string |
That string | That string |
| Engine entry is a string | That string | That string |
| Engine entry has variants | default |
dql if present; otherwise default |
Every engine entry is validated, including unselected engines. Unknown variant keys, deeper nesting, a missing/null default and a present non-string/null dql are rejected.
An absent or null sql inherits the base operator’s template. An explicitly provided map replaces that template; an engine missing from the map does not fall back to the base operator. Normal and aggregate conditions require an available non-empty template. An EXISTS column filter without a template leaves only the correlation condition.
A custom dql template must be valid DQL and any referenced custom functions must be registered with Doctrine. The built-in MySQL binary-string function is registered by the bridge.
Casting Validation
cast is an array of string rule names. Rules run in order. list produces an array; a later recognized rule requiring a string is rejected rather than applied to that array. Unknown rule names are ignored.