Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] add filter by operator #940

Merged
merged 4 commits into from
Sep 27, 2024
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
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);
$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()) {
AlexVanderbist marked this conversation as resolved.
Show resolved Hide resolved
$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 @@ -10,6 +10,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 @@ -658,3 +659,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