Custom filters & sorts
Implement the contracts to plug in bespoke logic, then register it with AllowedFilter::custom() or AllowedSort::custom():
use Illuminate\Database\Eloquent\Builder;
use RoundlyConsulting\QueryBuilder\Contracts\Filter;
final class EvenViewsFilter implements Filter
{
public function apply(Builder $query, mixed $value, string $property): void
{
$query->whereRaw('views % 2 = 0');
}
}
// ...->allowedFilters(AllowedFilter::custom('even', new EvenViewsFilter))use Illuminate\Database\Eloquent\Builder;
use RoundlyConsulting\QueryBuilder\Contracts\Sort;
use RoundlyConsulting\QueryBuilder\Enums\SortDirection;
final class TitleLengthSort implements Sort
{
public function apply(Builder $query, SortDirection $direction, string $property): void
{
$query->orderByRaw('LENGTH(title) '.$direction->value);
}
}
// ...->allowedSorts(AllowedSort::custom('length', new TitleLengthSort))Contracts
- Filter::apply(Builder $query, mixed $value, string $property): void — $value is already normalised (comma lists are arrays; true/false stay text unless you register it with custom(…, booleans: true)); $property is the internal name.
- Sort::apply(Builder $query, SortDirection $direction, string $property): void — $direction->value is asc or desc.
Operators in a callback
When a callback has to honour the operator wire itself, don’t hand-roll the parse — RequestedOperator::split() performs the same split the built-in filters use. The $allowed list is the whole security boundary: a token not in it is not an operator, and the whole string comes back as the value:
use Illuminate\Database\Eloquent\Builder;
use RoundlyConsulting\QueryBuilder\Enums\RequestedOperator;
AllowedFilter::callback('priority', function (Builder $query, mixed $value, string $property): void {
if (! is_string($value)) {
return;
}
$parsed = RequestedOperator::split($value, [RequestedOperator::Is, RequestedOperator::Not]);
$parsed->operatorOr(RequestedOperator::Is) === RequestedOperator::Not
? $query->where($property, '!=', $parsed->value)
: $query->where($property, $parsed->value);
}),
// ?filter[priority]=high → priority = 'high'
// ?filter[priority]=not:high → priority != 'high'
// ?filter[priority]=gte:high → priority = 'gte:high' (gte was not allowed)split() returns a RequestedFilterValue: its operator is null when the request named none (which differs from naming is), and operatorOr($default) resolves it. For multi-value parameters, RequestedFilterValues::parse($raw, $allowed) returns the operator plus the list of non-empty values.
Text matching
If a custom filter builds its own LIKE, escape user input the way the built-ins do: RoundlyConsulting\PackageToolkit\Support\LikeEscaper::escape(), from the package-toolkit dependency, escapes %, _ and \ — and pair it with an explicit ESCAPE clause, because SQLite has no default escape character.
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.