Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ GET /posts?filter[id][in][]=1&filter[id][in][]=2
GET /posts?filter[tags][name]=featured
```

Supported operators: `eq` `neq` `gt` `gte` `lt` `lte` `like` `nlike` `in` `between`
Supported operators: `eq` `neq` `gt` `gte` `lt` `lte` `like` `nlike` `in` `nin` `between` `nbetween` `null` `notnull`

### Expression Engine

Expand Down
17 changes: 17 additions & 0 deletions config/filterable.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,19 @@
'nulls_position' => null,
],

/*
|--------------------------------------------------------------------------
| Custom Operator Strategies
|--------------------------------------------------------------------------
|
| Map a resolved operator to a class implementing the Operator contract.
| Entries here override the built-in strategies.
|
*/
'operator_strategies' => [
// 'contains' => App\Filtering\Operators\ContainsOperator::class,
],

/*
|--------------------------------------------------------------------------
| Filter Engines
Expand Down Expand Up @@ -264,6 +277,7 @@
'null' => 'is null',
'notnull' => 'is not null',
'between' => 'between',
'nbetween' => 'not between',
],
],

Expand Down Expand Up @@ -312,6 +326,7 @@
'null' => 'is null',
'notnull' => 'is not null',
'between' => 'between',
'nbetween' => 'not between',
],

/*
Expand Down Expand Up @@ -411,6 +426,7 @@
'null' => 'is null',
'notnull' => 'is not null',
'between' => 'between',
'nbetween' => 'not between',
],

/*
Expand Down Expand Up @@ -478,6 +494,7 @@
'null' => 'is null',
'notnull' => 'is not null',
'between' => 'between',
'nbetween' => 'not between',
],

/*
Expand Down
7 changes: 6 additions & 1 deletion docs/api/filterable.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,12 @@ Return the allowed fields.

#### `allowedOperators(array $operators): static`

Override globally allowed operators for this instance.
Restrict the instance to selected operator aliases or resolved SQL values.

```php
$filterable->allowedOperators(['gte', 'in']);
$filterable->allowedOperators(['>=', 'in']);
```

#### `getAllowedOperators(): array`

Expand Down
5 changes: 4 additions & 1 deletion docs/engines/rule-set.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,11 @@ This default can be set via the engine configuration.
| like | LIKE | `filter[title][like]=%laravel%` |
| in | IN | `filter[id][in][]=1&filter[id][in][]=2` |
| between | BETWEEN | `filter[price][between][]=100&filter[price][between][]=200` |
| nbetween | NOT BETWEEN | `filter[price][nbetween][]=100&filter[price][nbetween][]=200` |
| null | IS NULL | `filter[deleted_at][null]` |
| notnull | IS NOT NULL | `filter[published_at][notnull]` |

> Operators are customizable and extendable. You may add your own by overriding the engine's resolver.
> Operators are customizable through [operator strategies](/features/operators).

---

Expand Down
3 changes: 3 additions & 0 deletions docs/engines/tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ This structure is then translated into an Eloquent query builder statement in La
| `null` | is null |
| `notnull` | is not null |
| `between` | between |
| `nbetween` | not between |

See [Operator Strategies](/features/operators) for value formats, per-filter allow-listing, and custom operators.

## Error Handling

Expand Down
65 changes: 65 additions & 0 deletions docs/features/operators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Operator Strategies

Ruleset, Expression, and Tree filters share the same operator pipeline. An operator is first validated against the selected engine's `allowed_operators` map, resolved to its database representation, and then applied by an operator strategy.

## Built-in operators

| Alias | Resolved operator | Behavior |
| --- | --- | --- |
| `eq`, `neq` | `=`, `!=` | Comparison |
| `gt`, `gte`, `lt`, `lte` | `>`, `>=`, `<`, `<=` | Ordered comparison |
| `like`, `nlike` | `like`, `not like` | Pattern comparison |
| `in`, `nin` | `in`, `not in` | Accepts an array or comma-separated value |
| `between`, `nbetween` | `between`, `not between` | Requires exactly two values |
| `null`, `notnull` | `is null`, `is not null` | Does not require a value |

`allowedOperators()` accepts either aliases or resolved values:

```php
$filterable->allowedOperators(['gte', 'in']);
$filterable->allowedOperators(['>=', 'in']);
```

## Custom strategies

A custom strategy implements the `Operator` contract:

```php
use Illuminate\Contracts\Database\Eloquent\Builder;
use Kettasoft\Filterable\Support\Payload;
use Kettasoft\Filterable\Engines\Foundation\Operators\Contracts\Operator;

final class ContainsOperator implements Operator
{
public function apply(Builder $builder, Payload $payload): Builder
{
return $builder->where(
$payload->field,
'like',
"%{$payload->value}%"
);
}
}
```

Register its public alias and resolved name in the engine, then map that resolved name to the strategy:

```php
// config/filterable.php
'operator_strategies' => [
'contains' => App\Filtering\Operators\ContainsOperator::class,
],

'engines' => [
'ruleset' => [
'allowed_operators' => [
// ...
'contains' => 'contains',
],
],
],
```

Strategies are resolved through Laravel's container, so constructor dependencies can be injected. A configured class must implement `Operator`; invalid definitions fail explicitly instead of silently falling back to equality.

The same strategy is used for direct and relational fields.
35 changes: 0 additions & 35 deletions src/Engines/Contracts/OperatorDefinitionContract.php

This file was deleted.

13 changes: 13 additions & 0 deletions src/Engines/Exceptions/InvalidOperatorValueException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Kettasoft\Filterable\Engines\Exceptions;

use Kettasoft\Filterable\Support\Payload;

class InvalidOperatorValueException extends SkipExecution
{
public function __construct(Payload $payload, string $message)
{
parent::__construct($message, $payload);
}
}
2 changes: 1 addition & 1 deletion src/Engines/Expression.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function execute(Builder $builder): Builder
$filters = RelationFieldParser::parse(
$this->context->getData(),
$this->context->getRelations(),
array_keys($this->allowedOperators())
array_merge(array_keys($this->allowedOperators()), array_values($this->allowedOperators()))
);

foreach ($filters as $field => $condition) {
Expand Down
10 changes: 8 additions & 2 deletions src/Engines/Foundation/Engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Kettasoft\Filterable\Engines\Foundation;

use Illuminate\Support\Arr;
use Kettasoft\Filterable\Filterable;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Kettasoft\Filterable\Foundation\Resources;
Expand Down Expand Up @@ -119,7 +118,14 @@ public function allowedOperators(): array
return $this->getOperatorsFromConfig();
}

return Arr::only($this->getOperatorsFromConfig(), $this->context->getAllowedOperators());
$requested = $this->context->getAllowedOperators();

return array_filter(
$this->getOperatorsFromConfig(),
fn($operator, $alias) => in_array($alias, $requested, true)
|| in_array($operator, $requested, true),
ARRAY_FILTER_USE_BOTH
);
}

/**
Expand Down
14 changes: 9 additions & 5 deletions src/Engines/Foundation/Enums/Operators.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ enum Operators: string
case NOT_IN = 'NOT IN';
case IS_NULL = 'IS NULL';
case IS_NOT_NULL = 'IS NOT NULL';
case BETWEEN = 'BETWEEN';
case NOT_BETWEEN = 'NOT BETWEEN';

public function toString(): string
{
Expand All @@ -28,17 +30,19 @@ public static function fromString(string $operator): string
{
return match ($operator) {
'eq' => self::EQUALS->value,
'ne' => self::NOT_EQUALS->value,
'ne', 'neq' => self::NOT_EQUALS->value,
'gt' => self::GREATER_THAN->value,
'lt' => self::LESS_THAN->value,
'gte' => self::GREATER_THAN_OR_EQUAL->value,
'lte' => self::LESS_THAN_OR_EQUAL->value,
'like' => self::LIKE->value,
'not_like' => self::NOT_LIKE->value,
'nlike', 'not_like' => self::NOT_LIKE->value,
'in' => self::IN->value,
'not_in' => self::NOT_IN->value,
'is_null' => self::IS_NULL->value,
'is_not_null' => self::IS_NOT_NULL->value,
'nin', 'not_in' => self::NOT_IN->value,
'null', 'is_null' => self::IS_NULL->value,
'notnull', 'is_not_null' => self::IS_NOT_NULL->value,
'between' => self::BETWEEN->value,
'nbetween', 'not_between' => self::NOT_BETWEEN->value,
default => throw new InvalidOperatorException($operator),
};
}
Expand Down
35 changes: 0 additions & 35 deletions src/Engines/Foundation/Mappers/OperatorMapper.php

This file was deleted.

68 changes: 0 additions & 68 deletions src/Engines/Foundation/OperatorDefinition.php

This file was deleted.

Loading
Loading