From 365c498aaa71c70efe76ba706015c4bbe86b468f Mon Sep 17 00:00:00 2001 From: henzeb Date: Mon, 14 Mar 2022 16:09:58 +0100 Subject: [PATCH] allow is to receive null --- CHANGELOG.md | 4 + src/Filters/Contracts/QueryFilter.php | 4 +- src/Filters/Query.php | 12 ++- tests/Helpers/DataProviders.php | 74 ++++++++++--------- tests/Unit/Filters/QueryTest.php | 13 ++-- .../Unit/Illuminate/Builders/BuilderTest.php | 56 ++++++++------ 6 files changed, 96 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d8d5ff..345be9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `Query Filter Builder` will be documented in this file +## 1.1.4 - 2022-03-14 + +- when `is` or `not` receives `null`, it should turn into `empty`/`notEmpty` + ## 1.1.3 - 2022-03-14 - added filterArray to easily handle comma separated strings as per recommendation in the JSON:API specification diff --git a/src/Filters/Contracts/QueryFilter.php b/src/Filters/Contracts/QueryFilter.php index 0f2965a..35bb9e8 100644 --- a/src/Filters/Contracts/QueryFilter.php +++ b/src/Filters/Contracts/QueryFilter.php @@ -8,9 +8,9 @@ interface QueryFilter { - public function is(string $key, string|float|int|bool $value): self; + public function is(string $key, string|float|int|bool|null $value): self; - public function not(string $key, string|float|int|bool $value): self; + public function not(string $key, string|float|int|bool|null $value): self; public function empty(string $key): self; diff --git a/src/Filters/Query.php b/src/Filters/Query.php index 0220d7b..f0dcd8b 100644 --- a/src/Filters/Query.php +++ b/src/Filters/Query.php @@ -66,13 +66,21 @@ private function shouldApplyAnd(string $action): bool return true; } - public function is(string $key, float|bool|int|string $value): QueryFilter + public function is(string $key, float|bool|int|string|null $value): QueryFilter { + if(null===$value) { + return $this->empty($key); + } + return $this->addFilter(__FUNCTION__, get_defined_vars()); } - public function not(string $key, float|bool|int|string $value): QueryFilter + public function not(string $key, float|bool|int|string|null $value): QueryFilter { + if(null===$value) { + return $this->notEmpty($key); + } + return $this->addFilter(__FUNCTION__, get_defined_vars()); } diff --git a/tests/Helpers/DataProviders.php b/tests/Helpers/DataProviders.php index 93a685e..58efaf3 100644 --- a/tests/Helpers/DataProviders.php +++ b/tests/Helpers/DataProviders.php @@ -18,55 +18,57 @@ public function providesOperators(): array public function providesFilterTestcases(): array { return [ - 'is' => ['method' => 'is', 'parameters' => ['key' => 'animal', 'value' => 'cat']], - 'not' => ['method' => 'not', 'parameters' => ['key' => 'animal', 'value' => 'dog']], + 'is' => ['method' => 'is', 'parameters' => ['key' => 'animal', 'value' => 'cat'], null], + 'is-null' => ['method' => 'is', 'parameters' => ['key' => 'animal', 'value' => null], 'empty'], + 'not' => ['method' => 'not', 'parameters' => ['key' => 'animal', 'value' => 'dog'], null], + 'not-null' => ['method' => 'not', 'parameters' => ['key' => 'animal', 'value' => null], 'notEmpty'], - 'empty' => ['method' => 'empty', 'parameters' => ['key' => 'name']], - 'notEmpty' => ['method' => 'notEmpty', 'parameters' => ['key' => 'name']], + 'empty' => ['method' => 'empty', 'parameters' => ['key' => 'name'], null], + 'notEmpty' => ['method' => 'notEmpty', 'parameters' => ['key' => 'name'], null], - 'in' => ['method' => 'in', 'parameters' => ['key' => 'animal', 'in' => ['dog']]], - 'in-multi' => ['method' => 'in', 'parameters' => ['key' => 'animal', 'in' => ['dog', 'cat']]], + 'in' => ['method' => 'in', 'parameters' => ['key' => 'animal', 'in' => ['dog']], null], + 'in-multi' => ['method' => 'in', 'parameters' => ['key' => 'animal', 'in' => ['dog', 'cat']], null], - 'notIn' => ['method' => 'notIn', 'parameters' => ['key' => 'animal', 'notIn' => ['dog']]], - 'notIn-multi' => ['method' => 'notIn', 'parameters' => ['key' => 'animal', 'notIn' => ['dog', 'cat']]], + 'notIn' => ['method' => 'notIn', 'parameters' => ['key' => 'animal', 'notIn' => ['dog']], null], + 'notIn-multi' => ['method' => 'notIn', 'parameters' => ['key' => 'animal', 'notIn' => ['dog', 'cat']], null], - 'like' => ['method' => 'like', 'parameters' => ['key' => 'animal', 'like' => '%dog%']], - 'notLike' => ['method' => 'notLike', 'parameters' => ['key' => 'animal', 'notLike' => '%dog%']], + 'like' => ['method' => 'like', 'parameters' => ['key' => 'animal', 'like' => '%dog%'], null], + 'notLike' => ['method' => 'notLike', 'parameters' => ['key' => 'animal', 'notLike' => '%dog%'], null], - 'less-int' => ['method' => 'less', 'parameters' => ['key' => 'height', 'less' => 10]], - 'less-float' => ['method' => 'less', 'parameters' => ['key' => 'height', 'less' => 10.5]], - 'less-date' => ['method' => 'less', 'parameters' => ['key' => 'age', 'less' => new DateTime()]], + 'less-int' => ['method' => 'less', 'parameters' => ['key' => 'height', 'less' => 10], null], + 'less-float' => ['method' => 'less', 'parameters' => ['key' => 'height', 'less' => 10.5], null], + 'less-date' => ['method' => 'less', 'parameters' => ['key' => 'age', 'less' => new DateTime()], null], - 'greater-int' => ['method' => 'greater', 'parameters' => ['key' => 'height', 'greater' => 10]], - 'greater-float' => ['method' => 'greater', 'parameters' => ['key' => 'height', 'greater' => 10.5]], - 'greater-date' => ['method' => 'greater', 'parameters' => ['key' => 'age', 'greater' => new DateTime()]], + 'greater-int' => ['method' => 'greater', 'parameters' => ['key' => 'height', 'greater' => 10], null], + 'greater-float' => ['method' => 'greater', 'parameters' => ['key' => 'height', 'greater' => 10.5], null], + 'greater-date' => ['method' => 'greater', 'parameters' => ['key' => 'age', 'greater' => new DateTime()], null], - 'lessOrEqual-int' => ['method' => 'lessOrEqual', 'parameters' => ['key' => 'height', 'lessOrEqual' => 10]], - 'lessOrEqual-float' => ['method' => 'lessOrEqual', 'parameters' => ['key' => 'height', 'lessOrEqual' => 10.5]], - 'lessOrEqual-date' => ['method' => 'lessOrEqual', 'parameters' => ['key' => 'age', 'lessOrEqual' => new DateTime()]], + 'lessOrEqual-int' => ['method' => 'lessOrEqual', 'parameters' => ['key' => 'height', 'lessOrEqual' => 10], null], + 'lessOrEqual-float' => ['method' => 'lessOrEqual', 'parameters' => ['key' => 'height', 'lessOrEqual' => 10.5], null], + 'lessOrEqual-date' => ['method' => 'lessOrEqual', 'parameters' => ['key' => 'age', 'lessOrEqual' => new DateTime()], null], - 'greaterOrEqual-int' => ['method' => 'greaterOrEqual', 'parameters' => ['key' => 'height', 'greaterOrEqual' => 10]], - 'greaterOrEqual-float' => ['method' => 'greaterOrEqual', 'parameters' => ['key' => 'height', 'greaterOrEqual' => 10.5]], - 'greaterOrEqual-date' => ['method' => 'greaterOrEqual', 'parameters' => ['key' => 'age', 'greaterOrEqual' => new DateTime()]], + 'greaterOrEqual-int' => ['method' => 'greaterOrEqual', 'parameters' => ['key' => 'height', 'greaterOrEqual' => 10], null], + 'greaterOrEqual-float' => ['method' => 'greaterOrEqual', 'parameters' => ['key' => 'height', 'greaterOrEqual' => 10.5], null], + 'greaterOrEqual-date' => ['method' => 'greaterOrEqual', 'parameters' => ['key' => 'age', 'greaterOrEqual' => new DateTime()], null], - 'between' => ['method' => 'between', 'parameters' => ['key' => 'age', 'low' => 1, 'high' => 1]], - 'between-mixed-1' => ['method' => 'between', 'parameters' => ['key' => 'age', 'low' => 1, 'high' => 1.5]], - 'between-mixed-2' => ['method' => 'between', 'parameters' => ['key' => 'age', 'low' => 1.1, 'high' => 2]], + 'between' => ['method' => 'between', 'parameters' => ['key' => 'age', 'low' => 1, 'high' => 1], null], + 'between-mixed-1' => ['method' => 'between', 'parameters' => ['key' => 'age', 'low' => 1, 'high' => 1.5], null], + 'between-mixed-2' => ['method' => 'between', 'parameters' => ['key' => 'age', 'low' => 1.1, 'high' => 2], null], - 'notBetween' => ['method' => 'notBetween', 'parameters' => ['key' => 'age', 'low' => 1, 'high' => 1]], - 'notBetween-mixed-1' => ['method' => 'notBetween', 'parameters' => ['key' => 'age', 'low' => 1, 'high' => 1.5]], - 'notBetween-mixed-2' => ['method' => 'notBetween', 'parameters' => ['key' => 'age', 'low' => 1.1, 'high' => 2]], + 'notBetween' => ['method' => 'notBetween', 'parameters' => ['key' => 'age', 'low' => 1, 'high' => 1], null], + 'notBetween-mixed-1' => ['method' => 'notBetween', 'parameters' => ['key' => 'age', 'low' => 1, 'high' => 1.5], null], + 'notBetween-mixed-2' => ['method' => 'notBetween', 'parameters' => ['key' => 'age', 'low' => 1.1, 'high' => 2], null], - 'dateBetween' => ['method' => 'dateBetween', 'parameters' => ['key' => 'age', 'low' => new DateTime(), 'high' => new DateTime()]], - 'dateNotBetween' => ['method' => 'dateNotBetween', 'parameters' => ['key' => 'age', 'low' => new DateTime(), 'high' => new DateTime()]], + 'dateBetween' => ['method' => 'dateBetween', 'parameters' => ['key' => 'age', 'low' => new DateTime(), 'high' => new DateTime()], null], + 'dateNotBetween' => ['method' => 'dateNotBetween', 'parameters' => ['key' => 'age', 'low' => new DateTime(), 'high' => new DateTime()], null], - 'filter-object' => ['method' => 'filter', 'parameters' => ['filter' => new OwnerFilter()]], + 'filter-object' => ['method' => 'filter', 'parameters' => ['filter' => new OwnerFilter()], null], - 'limit' => ['method' => 'limit', 'parameters' => ['limit' => 100]], - 'offset' => ['method' => 'offset', 'parameters' => ['offset' => 50]], + 'limit' => ['method' => 'limit', 'parameters' => ['limit' => 100], null], + 'offset' => ['method' => 'offset', 'parameters' => ['offset' => 50], null], - 'asc' => ['method' => 'asc', 'parameters' => ['key' => 'animal']], - 'desc' => ['method' => 'desc', 'parameters' => ['key' => 'animal']], + 'asc' => ['method' => 'asc', 'parameters' => ['key' => 'animal'], null], + 'desc' => ['method' => 'desc', 'parameters' => ['key' => 'animal'], null], ]; } @@ -76,7 +78,9 @@ public function providesFilterWithQueryTestcases(): array $this->providesFilterTestcases(), [ 'is' => ['query' => '`animal` = ?'], + 'is-null' => ['query' => '`animal` is null'], 'not' => ['query' => '`animal` != ?'], + 'not-null' => ['query' => '`animal` is not null'], 'empty' => ['query' => '`name` is null'], 'notEmpty' => ['query' => '`name` is not null'], 'in' => ['query' => '`animal` in (?)'], diff --git a/tests/Unit/Filters/QueryTest.php b/tests/Unit/Filters/QueryTest.php index 4c0c435..a782217 100644 --- a/tests/Unit/Filters/QueryTest.php +++ b/tests/Unit/Filters/QueryTest.php @@ -40,7 +40,7 @@ public function getMock(bool $withGetFilters = true): Query|Mock * * @dataProvider providesFilterTestcases */ - public function testShouldAddFilter(string $method, array $parameters): void + public function testShouldAddFilter(string $method, array $parameters, string $expectedMethod = null): void { $queryFilter = $this->getMock(); $expectedParameters = $parameters; @@ -51,7 +51,7 @@ public function testShouldAddFilter(string $method, array $parameters): void $this->assertEquals( [ - ['action' => $method, 'parameters' => $expectedParameters] + ['action' => $expectedMethod ?? $method, 'parameters' => array_filter($expectedParameters)] ], $queryFilter->getFilters() ); @@ -207,7 +207,7 @@ public function testChainingOfGroupWhenPassing(): void * @return void * @dataProvider providesFilterTestcases */ - public function testShouldBuildSimple(string $method, $parameters) + public function testShouldBuildSimple(string $method, array $parameters, string $expectedMethod = null): void { $query = $this->getMock(false); @@ -217,7 +217,8 @@ public function testShouldBuildSimple(string $method, $parameters) $builder = Mockery::mock(QueryBuilder::class.'[empty]'); - $builder->expects($method)->withArgs(array_values($parameters)); + + $builder->expects($expectedMethod ?? $method)->withArgs(array_filter(array_values($parameters))); $query->build($builder); } @@ -228,7 +229,7 @@ public function testShouldBuildSimple(string $method, $parameters) * @return void * @dataProvider providesFilterTestcases */ - public function testShouldBuildOr(string $method, $parameters): void + public function testShouldBuildOr(string $method, array $parameters, string $expectedMethod = null): void { $queryFilter = $this->getMock(false); $queryFilter->is('is', 'test'); @@ -238,7 +239,7 @@ public function testShouldBuildOr(string $method, $parameters): void $builder = Mockery::mock(QueryBuilder::class.'[empty]'); $builder->expects('is')->once(); - $orMethod = 'or' . ucfirst($method); + $orMethod = 'or' . ucfirst($expectedMethod ?? $method); if (method_exists($builder, $orMethod)) { $method = $orMethod; } diff --git a/tests/Unit/Illuminate/Builders/BuilderTest.php b/tests/Unit/Illuminate/Builders/BuilderTest.php index 804cfbd..0474289 100644 --- a/tests/Unit/Illuminate/Builders/BuilderTest.php +++ b/tests/Unit/Illuminate/Builders/BuilderTest.php @@ -2,13 +2,13 @@ namespace Henzeb\Query\Tests\Unit\Illuminate\Builders; -use Henzeb\Query\Filters\Contracts\Filter; use Henzeb\Query\Filters\Query; +use Orchestra\Testbench\TestCase; +use Illuminate\Support\Facades\DB; +use Illuminate\Database\Eloquent\Model; +use Henzeb\Query\Filters\Contracts\Filter; use Henzeb\Query\Illuminate\Builders\Builder; use Henzeb\Query\Tests\Helpers\DataProviders; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Facades\DB; -use Orchestra\Testbench\TestCase; use Illuminate\Database\Query\Builder as IlluminateBuilder; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Henzeb\Query\Illuminate\Filters\Contracts\Filter as IlluminateFilter; @@ -27,19 +27,21 @@ class BuilderTest extends TestCase * * @dataProvider providesFilterWithQueryTestcases */ - public function testShouldBuild(string $method, array $parameters, string|array $query, bool $noParameters = false): void + public function testShouldBuild(string $method, array $parameters, ?string $expectedMethod, string|array $query, bool $noParameters = false): void { $laravelBuilder = DB::query()->from('animals'); $builder = new Builder($laravelBuilder); - $flattenedParameters = $this->flattenArray($parameters); + $flattenedParameters = array_filter($this->flattenArray($parameters)); + + $method = $expectedMethod ?? $method; $builder->$method(...$flattenedParameters); array_shift($flattenedParameters); - if(is_array($query)) { + if (is_array($query)) { $flattenedParameters = $query['parameters']; $query = $query['query']; } @@ -57,12 +59,14 @@ public function testShouldBuild(string $method, array $parameters, string|array * * @dataProvider providesFilterWithQueryTestcases */ - public function testShouldBuildWithOr(string $method, array $parameters, array|string $query, bool $noParameters = false): void + public function testShouldBuildWithOr(string $method, array $parameters, ?string $expectedMethod, array|string $query, bool $noParameters = false): void { $illuminateBuilder = DB::query()->from('animals')->whereRaw('true'); $builder = new Builder($illuminateBuilder); + $method = $expectedMethod ?? $method; + $flattenedParameters = $this->flattenArray($parameters); if (!$noParameters) { $method = 'or' . ucfirst($method); @@ -71,13 +75,13 @@ public function testShouldBuildWithOr(string $method, array $parameters, array|s array_shift($flattenedParameters); - if(is_array($query)) { + if (is_array($query)) { $flattenedParameters = $query['parameters']; $query = $query['query']; } $this->assertEquals('select * from `animals` where true ' . ($noParameters ? '' : 'or ') . $query, $illuminateBuilder->toSql()); - $this->assertEquals($illuminateBuilder->getBindings(), $noParameters ? [] : array_values($flattenedParameters)); + $this->assertEquals($illuminateBuilder->getBindings(), $noParameters ? [] : array_filter(array_values($flattenedParameters))); } /** @@ -89,11 +93,13 @@ public function testShouldBuildWithOr(string $method, array $parameters, array|s * * @dataProvider providesFilterWithQueryTestcases */ - public function testShouldBuildWithGroup(string $method, array $parameters, array|string $query, bool $noParameters = false): void + public function testShouldBuildWithNest(string $method, array $parameters, ?string $expectedMethod, array|string $query, bool $noParameters = false): void { $illuminateBuilder = DB::query()->from('animals')->whereRaw('true'); $builder = new Builder($illuminateBuilder); + $method = $expectedMethod ?? $method; + $queryFilter = new Query(); $parameters = $this->flattenArray($parameters); $queryFilter->nest()->$method(...$parameters); @@ -101,13 +107,13 @@ public function testShouldBuildWithGroup(string $method, array $parameters, arra array_shift($parameters); - if(is_array($query)) { + if (is_array($query)) { $parameters = $query['parameters']; $query = $query['query']; } $this->assertEquals('select * from `animals` where true' . ($noParameters ? '' : ' and (' . $query . ')'), $illuminateBuilder->toSql()); - $this->assertEquals($noParameters ? [] : $parameters, $illuminateBuilder->getBindings()); + $this->assertEquals($noParameters ? [] : array_filter($parameters), $illuminateBuilder->getBindings()); } /** @@ -119,11 +125,13 @@ public function testShouldBuildWithGroup(string $method, array $parameters, arra * * @dataProvider providesFilterWithQueryTestcases */ - public function testShouldBuildWithGroupOr(string $method, array $parameters, array|string $query, bool $noParameters = false): void + public function testShouldBuildWithGroupOr(string $method, array $parameters, ?string $expectedMethod, array|string $query, bool $noParameters = false): void { $laravelBuilder = DB::query()->from('animals'); $builder = new Builder($laravelBuilder); + $method = $expectedMethod ?? $method; + $queryFilter = new Query(); $parameters = $this->flattenArray($parameters); $queryFilter->is('animal', 'horse')->or()->nest()->$method(...$parameters); @@ -132,7 +140,7 @@ public function testShouldBuildWithGroupOr(string $method, array $parameters, ar array_shift($parameters); - if(is_array($query)) { + if (is_array($query)) { $parameters = $query['parameters']; $query = $query['query']; } @@ -140,7 +148,7 @@ public function testShouldBuildWithGroupOr(string $method, array $parameters, ar array_unshift($parameters, 'horse'); $this->assertEquals('select * from `animals` where `animal` = ?' . ($noParameters ? '' : ' or (' . $query . ')'), $laravelBuilder->toSql()); - $this->assertEquals($noParameters ? ['horse'] : $parameters, $laravelBuilder->getBindings()); + $this->assertEquals($noParameters ? ['horse'] : array_filter($parameters), $laravelBuilder->getBindings()); } public function testAcceptsEloquentBuilderInstance() @@ -160,7 +168,8 @@ public function testAcceptsEloquentBuilderInstance() ); } - public function testShouldNotAllowNonIlluminateFilters() { + public function testShouldNotAllowNonIlluminateFilters() + { $laravelBuilder = DB::query()->from('animals'); @@ -173,7 +182,8 @@ public function testShouldNotAllowNonIlluminateFilters() { (new Builder($laravelBuilder))->filter($myFilter); } - public function testShouldNotAllowNonIlluminateFiltersWhenOr() { + public function testShouldNotAllowNonIlluminateFiltersWhenOr() + { $laravelBuilder = DB::query()->from('animals'); @@ -186,7 +196,8 @@ public function testShouldNotAllowNonIlluminateFiltersWhenOr() { (new Builder($laravelBuilder))->orFilter($myFilter); } - public function testShouldBuildFilter() { + public function testShouldBuildFilter() + { $laravelBuilder = DB::query()->from('animals')->where('animal', 'dog');; $myFilter = new class implements IlluminateFilter { @@ -205,13 +216,14 @@ public function build(EloquentBuilder|IlluminateBuilder $builder): void ); } - public function testShouldBuildOrFilter() { + public function testShouldBuildOrFilter() + { $laravelBuilder = DB::query()->from('animals')->where('animal', 'dog'); $myFilter = new class implements IlluminateFilter { public function build(EloquentBuilder|IlluminateBuilder $builder): void { - $builder->joinSub(function(IlluminateBuilder $builder){ + $builder->joinSub(function (IlluminateBuilder $builder) { $builder->from('owners')->where('country', 'NL'); }, 'owner', 'animal_id', 'id')->where('owner_id', 5); } @@ -224,7 +236,7 @@ public function build(EloquentBuilder|IlluminateBuilder $builder): void $laravelBuilder->toSql() ); $this->assertEquals( - ['NL','dog', 5], + ['NL', 'dog', 5], $laravelBuilder->getBindings() ); }