NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Query Builder for Laravel

Validating filter values

A list endpoint usually validates what a filter may contain — Rule::in(), integer, uuid — so a typo is a helpful 422 instead of a silently empty list. Once a filter accepts operators, those rules start rejecting the wire: gte:500 is not an integer and not:debug is not a valid level. The FilterValue rule strips the operator prefix and sentinels, then runs your original rules against what remains:

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use RoundlyConsulting\QueryBuilder\Concerns\HasPageSize;
use RoundlyConsulting\QueryBuilder\Enums\RequestedOperator;
use RoundlyConsulting\QueryBuilder\Rules\FilterValue;
use RoundlyConsulting\QueryBuilder\Support\FilterSentinel;

final class ListLogsRequest extends FormRequest
{
    use HasPageSize;

    public function rules(): array
    {
        return [
            ...$this->pageSizeRules(),
            'filter.level' => ['nullable', new FilterValue(Rule::enum(LogLevel::class))],
            'filter.code' => ['nullable', new FilterValue('integer', operators: [
                RequestedOperator::GreaterThanOrEqual,
                RequestedOperator::LessThan,
            ])],
            'filter.project' => ['nullable', new FilterValue('uuid', sentinels: [FilterSentinel::NONE])],
        ];
    }
}

// ?filter[level]=not:debug  → passes (debug is a LogLevel case)
// ?filter[code]=gte:500     → passes
// ?filter[code]=gte:garbage → 422 with the endpoint's own "integer" message
// ?filter[project]=none     → passes (the sentinel is skipped)

The matching filters in the controller:

->allowedFilters(
    AllowedFilter::operators('level', [RequestedOperator::Not]),
    AllowedFilter::operators('code', [RequestedOperator::GreaterThanOrEqual, RequestedOperator::LessThan]),
    AllowedFilter::nullable('project', 'project_id', FilterValueShape::Uuid),
)

Rule options

new FilterValue($rules, ?array $operators = null, array $sentinels = [], bool $partialByDefault = false):

  • $rules — the rules a bare value would get: a rule string, a list, or a rule object. The endpoint keeps its own messages and attribute names.
  • operators — the tokens the filter declared. Defaults to the equality pair (is, not); a comparison or partial filter has to say so.
  • sentinels — reserved values the filter answers itself, such as FilterSentinel::NONE, which the column rules would otherwise reject.
  • partialByDefault — mirror the filter’s flag; it decides which operator a bare value means, and that one is always stripped.

Pass the same operators and sentinel the filter declared — the two halves are one declaration. Every item of a comma/array value is validated (a multi-select sends several), capped by limits.max_filter_values.

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 crypto

By 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.