Skip to content

Commit

Permalink
Add operator filter (<, =, >, ...) (#940)
Browse files Browse the repository at this point in the history
* [FEAT] add filter by operators

* [TEST] add test cases for filter by operator

* [FEAT] add dynamic operator to filer by operators

* [TEST] test dynamic operator filter
  • Loading branch information
AbdelrahmanBl authored Sep 27, 2024
1 parent 34f35ce commit 09d9162
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 0 deletions.
9 changes: 9 additions & 0 deletions src/AllowedFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
namespace Spatie\QueryBuilder;

use Illuminate\Support\Collection;
use Spatie\QueryBuilder\Enums\FilterOperator;
use Spatie\QueryBuilder\Filters\Filter;
use Spatie\QueryBuilder\Filters\FiltersBeginsWithStrict;
use Spatie\QueryBuilder\Filters\FiltersCallback;
use Spatie\QueryBuilder\Filters\FiltersEndsWithStrict;
use Spatie\QueryBuilder\Filters\FiltersExact;
use Spatie\QueryBuilder\Filters\FiltersOperator;
use Spatie\QueryBuilder\Filters\FiltersPartial;
use Spatie\QueryBuilder\Filters\FiltersScope;
use Spatie\QueryBuilder\Filters\FiltersTrashed;
Expand Down Expand Up @@ -106,6 +108,13 @@ public static function custom(string $name, Filter $filterClass, $internalName =
return new static($name, $filterClass, $internalName);
}

public static function operator(string $name, FilterOperator $filterOperator, string $boolean = 'and', ?string $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): self
{
static::setFilterArrayValueDelimiter($arrayValueDelimiter);

return new static($name, new FiltersOperator($addRelationConstraint, $filterOperator, $boolean), $internalName, $filterOperator);
}

public function getFilterClass(): Filter
{
return $this->filterClass;
Expand Down
19 changes: 19 additions & 0 deletions src/Enums/FilterOperator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Spatie\QueryBuilder\Enums;

enum FilterOperator: string
{
case DYNAMIC = '';
case EQUAL = '=';
case LESS_THAN = '<';
case GREATER_THAN = '>';
case LESS_THAN_OR_EQUAL = '<=';
case GREATER_THAN_OR_EQUAL = '>=';
case NOT_EQUAL = '<>';

public function isDynamic()
{
return self::DYNAMIC === $this;
}
}
68 changes: 68 additions & 0 deletions src/Filters/FiltersOperator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace Spatie\QueryBuilder\Filters;

use Illuminate\Database\Eloquent\Builder;
use Spatie\QueryBuilder\Enums\FilterOperator;

/**
* @template TModelClass of \Illuminate\Database\Eloquent\Model
* @template-implements \Spatie\QueryBuilder\Filters\Filter<TModelClass>
*/
class FiltersOperator extends FiltersExact implements Filter
{
public function __construct(protected bool $addRelationConstraint, protected FilterOperator $filterOperator, protected string $boolean)
{
}

/** {@inheritdoc} */
public function __invoke(Builder $query, $value, string $property)
{
$filterOperator = $this->filterOperator;

if ($this->addRelationConstraint) {
if ($this->isRelationProperty($query, $property)) {
$this->withRelationConstraint($query, $value, $property);

return;
}
}

if (is_array($value)) {
$query->where(function ($query) use ($value, $property) {
foreach($value as $item) {
$this->__invoke($query, $item, $property);
}
});

return;
}
else if ($this->filterOperator->isDynamic()) {
$filterOperator = $this->getDynamicFilterOperator($value, $this);

Check failure on line 41 in src/Filters/FiltersOperator.php

View workflow job for this annotation

GitHub Actions / phpstan

Method Spatie\QueryBuilder\Filters\FiltersOperator<TModelClass of Illuminate\Database\Eloquent\Model>::getDynamicFilterOperator() invoked with 2 parameters, 1 required.
$this->removeDynamicFilterOperatorFromValue($value, $filterOperator);
}

$query->where($query->qualifyColumn($property), $filterOperator->value, $value, $this->boolean);
}

protected function getDynamicFilterOperator(string $value): FilterOperator
{
$filterOperator = FilterOperator::EQUAL;

// match filter operators and assign the filter operator.
foreach(FilterOperator::cases() as $filterOperatorCase) {
if (str_starts_with($value, $filterOperatorCase->value) && ! $filterOperatorCase->isDynamic()) {
$filterOperator = $filterOperatorCase;
}
}

return $filterOperator;
}

protected function removeDynamicFilterOperatorFromValue(string &$value, FilterOperator $filterOperator)
{
if (str_contains($value, $filterOperator->value)) {
$value = substr_replace($value, '', 0, strlen($filterOperator->value));
}
}
}
114 changes: 114 additions & 0 deletions tests/FilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use function PHPUnit\Framework\assertObjectHasProperty;

use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\Enums\FilterOperator;
use Spatie\QueryBuilder\Exceptions\InvalidFilterQuery;
use Spatie\QueryBuilder\Filters\Filter as CustomFilter;
use Spatie\QueryBuilder\Filters\Filter as FilterInterface;
Expand Down Expand Up @@ -657,3 +658,116 @@ public function __invoke(Builder $query, $value, string $property): Builder
->get();
expect($models->count())->toEqual(0);
});

it('can filter name with equal operator filter', function () {
TestModel::create(['name' => 'John Doe']);

$results = createQueryFromFilterRequest([
'name' => 'John Doe',
])
->allowedFilters(AllowedFilter::operator('name', FilterOperator::EQUAL))
->get();

expect($results)->toHaveCount(1);
});

it('can filter name with not equal operator filter', function () {
TestModel::create(['name' => 'John Doe']);

$results = createQueryFromFilterRequest([
'name' => 'John Doe',
])
->allowedFilters(AllowedFilter::operator('name', FilterOperator::NOT_EQUAL))
->get();

expect($results)->toHaveCount(5);
});

it('can filter salary with greater than operator filter', function () {
TestModel::create(['salary' => 5000]);

$results = createQueryFromFilterRequest([
'salary' => 3000,
])
->allowedFilters(AllowedFilter::operator('salary', FilterOperator::GREATER_THAN))
->get();

expect($results)->toHaveCount(1);
});

it('can filter salary with less than operator filter', function () {
TestModel::create(['salary' => 5000]);

$results = createQueryFromFilterRequest([
'salary' => 7000,
])
->allowedFilters(AllowedFilter::operator('salary', FilterOperator::LESS_THAN))
->get();

expect($results)->toHaveCount(1);
});

it('can filter salary with greater than or equal operator filter', function () {
TestModel::create(['salary' => 5000]);

$results = createQueryFromFilterRequest([
'salary' => 3000,
])
->allowedFilters(AllowedFilter::operator('salary', FilterOperator::GREATER_THAN_OR_EQUAL))
->get();

expect($results)->toHaveCount(1);
});

it('can filter salary with less than or equal operator filter', function () {
TestModel::create(['salary' => 5000]);

$results = createQueryFromFilterRequest([
'salary' => 7000,
])
->allowedFilters(AllowedFilter::operator('salary', FilterOperator::LESS_THAN_OR_EQUAL))
->get();

expect($results)->toHaveCount(1);
});

it('can filter array of names with equal operator filter', function () {
TestModel::create(['name' => 'John Doe']);
TestModel::create(['name' => 'Max Doe']);

$results = createQueryFromFilterRequest([
'name' => 'John Doe,Max Doe',
])
->allowedFilters(AllowedFilter::operator('name', FilterOperator::EQUAL, 'or'))
->get();

expect($results)->toHaveCount(2);
});

it('can filter salary with dynamic operator filter', function () {
TestModel::create(['salary' => 5000]);
TestModel::create(['salary' => 2000]);

$results = createQueryFromFilterRequest([
'salary' => '>2000',
])
->allowedFilters(AllowedFilter::operator('salary', FilterOperator::DYNAMIC))
->get();

expect($results)->toHaveCount(1);
});

it('can filter salary with dynamic array operator filter', function () {
TestModel::create(['salary' => 1000]);
TestModel::create(['salary' => 2000]);
TestModel::create(['salary' => 3000]);
TestModel::create(['salary' => 4000]);

$results = createQueryFromFilterRequest([
'salary' => '>1000,<4000',
])
->allowedFilters(AllowedFilter::operator('salary', FilterOperator::DYNAMIC))
->get();

expect($results)->toHaveCount(2);
});
13 changes: 13 additions & 0 deletions tests/RelationFilterTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\Enums\FilterOperator;
use Spatie\QueryBuilder\Tests\TestClasses\Models\TestModel;

beforeEach(function () {
Expand Down Expand Up @@ -115,3 +116,15 @@

expect($sql)->toContain('LOWER(`relatedModels`.`name`) LIKE ?');
});

it('can disable operator filtering based on related model properties', function () {
$addRelationConstraint = false;

$sql = createQueryFromFilterRequest([
'relatedModels.name' => $this->models->first()->name,
])
->allowedFilters(AllowedFilter::operator('relatedModels.name', FilterOperator::EQUAL, 'and', null, $addRelationConstraint))
->toSql();

expect($sql)->toContain('`relatedModels`.`name` = ?');
});
1 change: 1 addition & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ protected function setUpDatabase(Application $app)
$table->increments('id');
$table->timestamps();
$table->string('name')->nullable();
$table->double('salary')->nullable();
$table->boolean('is_visible')->default(true);
});

Expand Down

0 comments on commit 09d9162

Please sign in to comment.