---
title: "Operator filters — Query Builder for Laravel | Roundly"
description: "Fixed server-side comparisons with operator(), and client-chosen operators such as not:, contains: and gte: with operators()."
url: https://roundly-consulting.com/open-source/docs/query-builder-for-laravel/operator-filters
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Query Builder for Laravel](https://roundly-consulting.com/open-source/docs/query-builder-for-laravel.md)

# 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:

```php
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 — except under FilterOperator::NotEqual, where a list reads as “none of these” (whereNotIn), since an OR of != would match nearly every row. It stays plain SQL, so rows where the column is NULL match neither form; for the NULL-inclusive negation, use operators() with not.

## 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:

```php
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 nothing
```

Signature: 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 crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

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

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/query-builder-for-laravel.md)
