Operator filters
Two constructors cover comparisons. operator() fixes the comparison in code; operators() lets the client pick one from a set you declare.
Fixed comparisons — operator()
operator() picks the comparison server-side from the FilterOperator enum. The wire stays filter[<name>]=<value> — the operator is never read from the request. Use it instead of a hand-written comparison callback:
use RoundlyConsulting\QueryBuilder\AllowedFilter;
use RoundlyConsulting\QueryBuilder\Enums\FilterOperator;
->allowedFilters(
AllowedFilter::operator('min_views', FilterOperator::GreaterThanOrEqual, 'views'),
AllowedFilter::operator('max_price', FilterOperator::LessThan, 'price'),
)
// ?filter[min_views]=10 → where('views', '>=', 10)
// ?filter[min_views]=10,20 → views >= 10 OR views >= 20| Case | SQL |
|---|---|
FilterOperator::Equal | = |
FilterOperator::NotEqual | != |
FilterOperator::GreaterThan | > |
FilterOperator::GreaterThanOrEqual | >= |
FilterOperator::LessThan | < |
FilterOperator::LessThanOrEqual | <= |
A comma list becomes a grouped OR of the same comparison. That is right for =, > and <, but a grouped OR of != matches nearly every row — for a multi-value “neither”, use operators() with not, which ANDs its values.
Client-chosen operators — operators()
operators() is the one constructor where the request influences which comparison runs — and it is still an allow-list decision made in code. The request supplies a token before a colon (filter[status]=not:draft); a bare value keeps meaning the filter’s default, so existing URLs keep working:
use RoundlyConsulting\QueryBuilder\Enums\RequestedOperator;
->allowedFilters(
AllowedFilter::operators('status', [RequestedOperator::Not]),
AllowedFilter::operators('title', [RequestedOperator::NotContains], partialByDefault: true),
AllowedFilter::operators('views', [RequestedOperator::GreaterThan, RequestedOperator::LessThanOrEqual]),
)
// ?filter[status]=draft → status = 'draft'
// ?filter[status]=not:draft,archived → status NOT IN ('draft', 'archived'), or status IS NULL
// ?filter[title]=laravel → title contains "laravel" (partialByDefault)
// ?filter[title]=ncontains:draft → title does not contain "draft", or title IS NULL
// ?filter[views]=gt:20 → views > 20
// ?filter[views]=lte:10 → views <= 10
// ?filter[status]=1=1 or 1:draft → the literal text — matches nothingSignature: operators(string $name, array $operators, ?string $internalName = null, bool $partialByDefault = false, FilterValueShape $shape = FilterValueShape::Text). The tokens:
| Token | Case | Effect |
|---|---|---|
is | RequestedOperator::Is | Equality; a comma list is whereIn. |
not | RequestedOperator::Not | whereNotIn — “neither”; includes rows where the column is NULL. |
contains | RequestedOperator::Contains | %value% match, case folding as the engine does it; a list is a grouped OR. |
ncontains | RequestedOperator::NotContains | Does not contain; values are ANDed and NULL rows included. An empty value is a no-op. |
starts | RequestedOperator::StartsWith | Anchored value% prefix match. |
gt | RequestedOperator::GreaterThan | > |
gte | RequestedOperator::GreaterThanOrEqual | >= |
lt | RequestedOperator::LessThan | < |
lte | RequestedOperator::LessThanOrEqual | <= |
The rules
- An unknown token, or one this filter did not declare, is not an operator — the whole string becomes the value. filter[status]=1=1 or 1:draft filters for that literal text and matches nothing.
- The filter’s own default — is, or contains under partialByDefault — is always nameable, so a client can switch back. Nothing else is implicit: on a partialByDefault field, is:done searches for that text.
- Only a known token before the first colon counts, so filter[url]=https://example.test still filters for itself.
- The operator is per filter, not per value: it is read off the first item and applied to all. not:draft,archived means neither.
- Both negations (not, ncontains) include rows where the column is NULL — “not draft” plainly includes “no status at all”.
- true and false are text like any other value. On a boolean column declare shape: FilterValueShape::Boolean, and not:true / not:false work as negations of real booleans (see Value shapes).
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.