From 6d29e56dabbd9d54e10eacc28dbcb8967d0dde7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Al=C3=AD=20Padr=C3=B3n?= <34191509+alipadron@users.noreply.github.com> Date: Fri, 14 Jun 2024 07:21:01 -0400 Subject: [PATCH 01/51] Add issue #175 link in selecting-fields.md --- docs/features/selecting-fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/selecting-fields.md b/docs/features/selecting-fields.md index 2ab16d32..9a12451a 100644 --- a/docs/features/selecting-fields.md +++ b/docs/features/selecting-fields.md @@ -51,7 +51,7 @@ QueryBuilder::for(Post::class) // All posts will be fetched including _only_ the name of the author. ``` -⚠️ Keep in mind that the fields query will completely override the `SELECT` part of the query. This means that you'll need to manually specify any columns required for Eloquent relationships to work, in the above example `author.id`. See issue #175 as well. +⚠️ Keep in mind that the fields query will completely override the `SELECT` part of the query. This means that you'll need to manually specify any columns required for Eloquent relationships to work, in the above example `author.id`. See issue [#175](https://github.com/spatie/laravel-query-builder/issues/175) as well. ⚠️ `allowedFields` must be called before `allowedIncludes`. Otherwise the query builder won't know what fields to include for the requested includes and an exception will be thrown. From 663b5b6b6a8e171ae0de0282a34d70c34ab5fe12 Mon Sep 17 00:00:00 2001 From: Kekeocha Justin Chetachukwu Date: Sun, 30 Jun 2024 18:56:45 +0100 Subject: [PATCH 02/51] Update filtering.md --- docs/features/filtering.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/features/filtering.md b/docs/features/filtering.md index 178bb8cd..c75ce64b 100644 --- a/docs/features/filtering.md +++ b/docs/features/filtering.md @@ -148,6 +148,12 @@ public function scopeEvent(Builder $query, \App\Models\Event $event): Builder // GET /events?filter[event]=1 - the event with ID 1 will automatically be resolved and passed to the scoped filter ``` +If you use any other column aside `id` column for route model binding (ULID,UUID). Remeber to specify the value of the column used in route model binding + +```php +// GET /events?filter[event]=01j0rcpkx5517v0aqyez5vnwn - supposing we use a ULID column for route model binding. +``` + Scopes are usually not named with query filters in mind. Use [filter aliases](#filter-aliases) to alias them to something more appropriate: ```php From 0eb0ffdf6eb1330029bfd6aafeced217bce281c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:49:43 +0000 Subject: [PATCH 03/51] Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 2.1.0 to 2.2.0. - [Release notes](https://github.com/dependabot/fetch-metadata/releases) - [Commits](https://github.com/dependabot/fetch-metadata/compare/v2.1.0...v2.2.0) --- updated-dependencies: - dependency-name: dependabot/fetch-metadata dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/dependabot-auto-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index c09678fd..9db8ce33 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -13,7 +13,7 @@ jobs: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v2.1.0 + uses: dependabot/fetch-metadata@v2.2.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" compat-lookup: true From 8e2673fe62f022e93cd17290bf2c814b628470ab Mon Sep 17 00:00:00 2001 From: Carlos Garcia <155784995+cgarciagarcia@users.noreply.github.com> Date: Sat, 3 Aug 2024 11:56:47 -0300 Subject: [PATCH 04/51] [DOCS] Update Frontend implementation with a new one --- docs/advanced-usage/front-end-implementation.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/advanced-usage/front-end-implementation.md b/docs/advanced-usage/front-end-implementation.md index fe90478f..f532888a 100644 --- a/docs/advanced-usage/front-end-implementation.md +++ b/docs/advanced-usage/front-end-implementation.md @@ -1,6 +1,6 @@ --- title: Front-end implementation -weight: 3 +weight: 6 --- If you're interested in building query urls on the front-end to match this package, you could use one of the below: @@ -11,3 +11,4 @@ If you're interested in building query urls on the front-end to match this packa Pascal Baljet](https://github.com/pascalbaljet). - React: [cogent-js package](https://www.npmjs.com/package/cogent-js) by [Joel Male](https://github.com/joelwmale). - Typescript: [query-builder-ts package](https://www.npmjs.com/package/@vortechron/query-builder-ts) by [Amirul Adli](https://www.npmjs.com/~vortechron) +- Typescript + React [react-query-builder](https://www.npmjs.com/package/@cgarciagarcia/react-query-builder) by [Carlos Garcia](https://github.com/cgarciagarcia) From 0eb7bcc6e875f50221e32be5929593b6becb6c10 Mon Sep 17 00:00:00 2001 From: Walter K Date: Wed, 14 Aug 2024 13:29:29 +0300 Subject: [PATCH 05/51] AllowedFilter should return static rather than self. --- src/AllowedFilter.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/AllowedFilter.php b/src/AllowedFilter.php index a9ba5bb0..8d87d679 100644 --- a/src/AllowedFilter.php +++ b/src/AllowedFilter.php @@ -52,54 +52,54 @@ public static function setFilterArrayValueDelimiter(string $delimiter = null): v } } - public static function exact(string $name, ?string $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): self + public static function exact(string $name, ?string $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): static { static::setFilterArrayValueDelimiter($arrayValueDelimiter); return new static($name, new FiltersExact($addRelationConstraint), $internalName); } - public static function partial(string $name, $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): self + public static function partial(string $name, $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): static { static::setFilterArrayValueDelimiter($arrayValueDelimiter); return new static($name, new FiltersPartial($addRelationConstraint), $internalName); } - public static function beginsWithStrict(string $name, $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): self + public static function beginsWithStrict(string $name, $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): static { static::setFilterArrayValueDelimiter($arrayValueDelimiter); return new static($name, new FiltersBeginsWithStrict($addRelationConstraint), $internalName); } - public static function endsWithStrict(string $name, $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): self + public static function endsWithStrict(string $name, $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): static { static::setFilterArrayValueDelimiter($arrayValueDelimiter); return new static($name, new FiltersEndsWithStrict($addRelationConstraint), $internalName); } - public static function scope(string $name, $internalName = null, string $arrayValueDelimiter = null): self + public static function scope(string $name, $internalName = null, string $arrayValueDelimiter = null): static { static::setFilterArrayValueDelimiter($arrayValueDelimiter); return new static($name, new FiltersScope(), $internalName); } - public static function callback(string $name, $callback, $internalName = null, string $arrayValueDelimiter = null): self + public static function callback(string $name, $callback, $internalName = null, string $arrayValueDelimiter = null): static { static::setFilterArrayValueDelimiter($arrayValueDelimiter); return new static($name, new FiltersCallback($callback), $internalName); } - public static function trashed(string $name = 'trashed', $internalName = null): self + public static function trashed(string $name = 'trashed', $internalName = null): static { return new static($name, new FiltersTrashed(), $internalName); } - public static function custom(string $name, Filter $filterClass, $internalName = null, string $arrayValueDelimiter = null): self + public static function custom(string $name, Filter $filterClass, $internalName = null, string $arrayValueDelimiter = null): static { static::setFilterArrayValueDelimiter($arrayValueDelimiter); @@ -121,7 +121,7 @@ public function isForFilter(string $filterName): bool return $this->name === $filterName; } - public function ignore(...$values): self + public function ignore(...$values): static { $this->ignored = $this->ignored ->merge($values) @@ -140,7 +140,7 @@ public function getInternalName(): string return $this->internalName; } - public function default($value): self + public function default($value): static { $this->hasDefault = true; $this->default = $value; @@ -162,14 +162,14 @@ public function hasDefault(): bool return $this->hasDefault; } - public function nullable(bool $nullable = true): self + public function nullable(bool $nullable = true): static { $this->nullable = $nullable; return $this; } - public function unsetDefault(): self + public function unsetDefault(): static { $this->hasDefault = false; unset($this->default); From ee31e0c6596e56da8f1a68328188f6d44d376531 Mon Sep 17 00:00:00 2001 From: Ching Cheng Kang Date: Mon, 16 Sep 2024 14:31:50 +0800 Subject: [PATCH 06/51] Update selecting-fields.md --- docs/features/selecting-fields.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/selecting-fields.md b/docs/features/selecting-fields.md index 9a12451a..b613e719 100644 --- a/docs/features/selecting-fields.md +++ b/docs/features/selecting-fields.md @@ -9,8 +9,8 @@ Sometimes you'll want to fetch only a couple fields to reduce the overall size o The following example fetches only the users' `id` and `name`: -``` -GET /users?fields[users]=id,name +```php +// GET /users?fields[users]=id,name $users = QueryBuilder::for(User::class) ->allowedFields(['id', 'name']) From fb289265ab513bf494d7a6100901f8de84473cba Mon Sep 17 00:00:00 2001 From: Nielsvanpach Date: Mon, 23 Sep 2024 08:25:01 +0000 Subject: [PATCH 07/51] Fix styling --- src/Filters/FiltersPartial.php | 2 +- tests/FilterTest.php | 1 - tests/SortTest.php | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Filters/FiltersPartial.php b/src/Filters/FiltersPartial.php index f5c38385..77db819c 100644 --- a/src/Filters/FiltersPartial.php +++ b/src/Filters/FiltersPartial.php @@ -74,7 +74,7 @@ protected static function escapeLike(string $value): string */ protected static function maybeSpecifyEscapeChar(string $driver): string { - if(! in_array($driver, ['sqlite','pgsql','sqlsrv'])) { + if (! in_array($driver, ['sqlite','pgsql','sqlsrv'])) { return ''; } diff --git a/tests/FilterTest.php b/tests/FilterTest.php index 80b971be..650abbd9 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -4,7 +4,6 @@ use Illuminate\Http\Request; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; - use Pest\Expectation; use function PHPUnit\Framework\assertObjectHasProperty; diff --git a/tests/SortTest.php b/tests/SortTest.php index 95534beb..6fd3de5b 100644 --- a/tests/SortTest.php +++ b/tests/SortTest.php @@ -14,7 +14,6 @@ use Spatie\QueryBuilder\Sorts\Sort as SortInterface; use Spatie\QueryBuilder\Sorts\SortsField; use Spatie\QueryBuilder\Tests\Concerns\AssertsCollectionSorting; - use Spatie\QueryBuilder\Tests\TestClasses\Models\TestModel; uses(AssertsCollectionSorting::class); From 34f35ce7013cca0f1e9e6c2d141b3fe04577179b Mon Sep 17 00:00:00 2001 From: Nielsvanpach Date: Mon, 23 Sep 2024 08:28:13 +0000 Subject: [PATCH 08/51] Update CHANGELOG --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39724625..f762f5a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,27 @@ All notable changes to `laravel-query-builder` will be documented in this file +## 6.1.0 - 2024-09-23 + +### What's Changed + +* Bump ramsey/composer-install from 2 to 3 by @dependabot in https://github.com/spatie/laravel-query-builder/pull/939 +* Add issue #175 link in selecting-fields.md by @alipadron in https://github.com/spatie/laravel-query-builder/pull/951 +* Update filtering.md by @justinkekeocha in https://github.com/spatie/laravel-query-builder/pull/954 +* Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot in https://github.com/spatie/laravel-query-builder/pull/955 +* [DOCS] Update Frontend implementation with a new one by @cgarciagarcia in https://github.com/spatie/laravel-query-builder/pull/961 +* Update Documentation for php markdown by @chengkangzai in https://github.com/spatie/laravel-query-builder/pull/969 +* AllowedFilter should return static rather than self by @kosarinin in https://github.com/spatie/laravel-query-builder/pull/964 + +### New Contributors + +* @alipadron made their first contribution in https://github.com/spatie/laravel-query-builder/pull/951 +* @cgarciagarcia made their first contribution in https://github.com/spatie/laravel-query-builder/pull/961 +* @chengkangzai made their first contribution in https://github.com/spatie/laravel-query-builder/pull/969 +* @kosarinin made their first contribution in https://github.com/spatie/laravel-query-builder/pull/964 + +**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/6.0.1...6.1.0 + ## 6.0.1 - 2024-05-21 ### What's Changed From 09d91622f696b34165c00c90e5f13ff2cb0c6d6c Mon Sep 17 00:00:00 2001 From: Abdelrahman Gamal <35730155+AbdelrahmanBl@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:55:47 +0300 Subject: [PATCH 09/51] Add operator filter (<, =, >, ...) (#940) * [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 --- src/AllowedFilter.php | 9 +++ src/Enums/FilterOperator.php | 19 ++++++ src/Filters/FiltersOperator.php | 68 +++++++++++++++++++ tests/FilterTest.php | 114 ++++++++++++++++++++++++++++++++ tests/RelationFilterTest.php | 13 ++++ tests/TestCase.php | 1 + 6 files changed, 224 insertions(+) create mode 100644 src/Enums/FilterOperator.php create mode 100644 src/Filters/FiltersOperator.php diff --git a/src/AllowedFilter.php b/src/AllowedFilter.php index 8d87d679..b4d2b63c 100644 --- a/src/AllowedFilter.php +++ b/src/AllowedFilter.php @@ -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; @@ -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; diff --git a/src/Enums/FilterOperator.php b/src/Enums/FilterOperator.php new file mode 100644 index 00000000..451f37e7 --- /dev/null +++ b/src/Enums/FilterOperator.php @@ -0,0 +1,19 @@ +'; + case LESS_THAN_OR_EQUAL = '<='; + case GREATER_THAN_OR_EQUAL = '>='; + case NOT_EQUAL = '<>'; + + public function isDynamic() + { + return self::DYNAMIC === $this; + } +} diff --git a/src/Filters/FiltersOperator.php b/src/Filters/FiltersOperator.php new file mode 100644 index 00000000..2d918b83 --- /dev/null +++ b/src/Filters/FiltersOperator.php @@ -0,0 +1,68 @@ + + */ +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()) { + $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)); + } + } +} diff --git a/tests/FilterTest.php b/tests/FilterTest.php index 650abbd9..e4cea386 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -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; @@ -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); +}); diff --git a/tests/RelationFilterTest.php b/tests/RelationFilterTest.php index c6144482..31902883 100644 --- a/tests/RelationFilterTest.php +++ b/tests/RelationFilterTest.php @@ -1,6 +1,7 @@ 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` = ?'); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index 62427dc3..c8777655 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -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); }); From 474b34182984c34f82e50763f8e6bdf6c2b19d5b Mon Sep 17 00:00:00 2001 From: AlexVanderbist Date: Fri, 27 Sep 2024 07:56:20 +0000 Subject: [PATCH 10/51] Fix styling --- src/Filters/FiltersOperator.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Filters/FiltersOperator.php b/src/Filters/FiltersOperator.php index 2d918b83..3323f9d8 100644 --- a/src/Filters/FiltersOperator.php +++ b/src/Filters/FiltersOperator.php @@ -30,14 +30,13 @@ public function __invoke(Builder $query, $value, string $property) if (is_array($value)) { $query->where(function ($query) use ($value, $property) { - foreach($value as $item) { + foreach ($value as $item) { $this->__invoke($query, $item, $property); } }); return; - } - else if ($this->filterOperator->isDynamic()) { + } elseif ($this->filterOperator->isDynamic()) { $filterOperator = $this->getDynamicFilterOperator($value, $this); $this->removeDynamicFilterOperatorFromValue($value, $filterOperator); } @@ -50,7 +49,7 @@ protected function getDynamicFilterOperator(string $value): FilterOperator $filterOperator = FilterOperator::EQUAL; // match filter operators and assign the filter operator. - foreach(FilterOperator::cases() as $filterOperatorCase) { + foreach (FilterOperator::cases() as $filterOperatorCase) { if (str_starts_with($value, $filterOperatorCase->value) && ! $filterOperatorCase->isDynamic()) { $filterOperator = $filterOperatorCase; } From b8bf3fd6c1f5ca99f5be62db89ffb7caeccd1eeb Mon Sep 17 00:00:00 2001 From: Alex Vanderbist Date: Fri, 27 Sep 2024 09:59:18 +0200 Subject: [PATCH 11/51] Add documentation for the operator filter (#974) --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/spatie/laravel-query-builder?shareId=XXXX-XXXX-XXXX-XXXX). --- docs/features/filtering.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/features/filtering.md b/docs/features/filtering.md index c75ce64b..e51e57fe 100644 --- a/docs/features/filtering.md +++ b/docs/features/filtering.md @@ -86,6 +86,40 @@ $users = QueryBuilder::for(User::class) // $users will contain all admin users with id 1, 2, 3, 4 or 5 ``` +## Operator filters + +Operator filters allow you to filter results based on different operators such as EQUAL, NOT_EQUAL, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL, and DYNAMIC. You can use the `AllowedFilter::operator` method to create operator filters. + +```php +use Spatie\QueryBuilder\AllowedFilter; +use Spatie\QueryBuilder\Enums\FilterOperator; + +// GET /users?filter[salary]=>3000 +$users = QueryBuilder::for(User::class) + ->allowedFilters([ + AllowedFilter::operator('salary', FilterOperator::GREATER_THAN), + ]) + ->get(); + +// $users will contain all users with a salary greater than 3000 +``` + +You can also use dynamic operator filters, which allow you to specify the operator in the filter value: + +```php +use Spatie\QueryBuilder\AllowedFilter; +use Spatie\QueryBuilder\Enums\FilterOperator; + +// GET /users?filter[salary]=>3000 +$users = QueryBuilder::for(User::class) + ->allowedFilters([ + AllowedFilter::operator('salary', FilterOperator::DYNAMIC), + ]) + ->get(); + +// $users will contain all users with a salary greater than 3000 +``` + ## Exact or partial filters for related properties You can also add filters for a relationship property using the dot-notation: `AllowedFilter::exact('posts.title')`. This works for exact and partial filters. Under the hood we'll add a `whereHas` statement for the `posts` that filters for the given `title` property as well. From 46018b31d30558c9aff1d45b3371e621c5878343 Mon Sep 17 00:00:00 2001 From: AlexVanderbist Date: Fri, 27 Sep 2024 08:01:09 +0000 Subject: [PATCH 12/51] Update CHANGELOG --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f762f5a5..c232f50c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to `laravel-query-builder` will be documented in this file +## 6.2.0 - 2024-09-27 + +### What's Changed + +* [FEAT] add filter by operator by @AbdelrahmanBl in https://github.com/spatie/laravel-query-builder/pull/940 +* Add documentation for the operator filter by @AlexVanderbist in https://github.com/spatie/laravel-query-builder/pull/974 + +### New Contributors + +* @AbdelrahmanBl made their first contribution in https://github.com/spatie/laravel-query-builder/pull/940 + +**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/6.1.0...6.2.0 + ## 6.1.0 - 2024-09-23 ### What's Changed From bc2daf1f854516b49cacceaf21b5dbfab1f0556d Mon Sep 17 00:00:00 2001 From: Talpx1 <69934344+Talpx1@users.noreply.github.com> Date: Thu, 3 Oct 2024 13:07:28 +0200 Subject: [PATCH 13/51] Removed explicit escaping for pgsql driver in FilterPartial#maybeSpecifyEscapeChar. Fixes #941 (#968) * Fix styling * Removed explicit escaping for pgsql driver in FilterPartial#maybeSpecifyEscapeChar. Fixes #941 Added mariadb driver in FilterPartial#maybeSpecifyEscapeChar phpdoc for param $driver. Also adjusted test in order to run with mariadb driver only if the installed version of illuminate/database dependency supports the driver. * Fix styling --------- Co-authored-by: Talpx1 Co-authored-by: Alex Vanderbist --- src/Filters/FiltersPartial.php | 4 ++-- tests/FilterTest.php | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Filters/FiltersPartial.php b/src/Filters/FiltersPartial.php index 77db819c..31d77100 100644 --- a/src/Filters/FiltersPartial.php +++ b/src/Filters/FiltersPartial.php @@ -69,12 +69,12 @@ protected static function escapeLike(string $value): string } /** - * @param 'sqlite'|'pgsql'|'sqlsrc'|'mysql' $driver + * @param 'sqlite'|'pgsql'|'sqlsrc'|'mysql'|'mariadb' $driver * @return string */ protected static function maybeSpecifyEscapeChar(string $driver): string { - if (! in_array($driver, ['sqlite','pgsql','sqlsrv'])) { + if (! in_array($driver, ['sqlite','sqlsrv'])) { return ''; } diff --git a/tests/FilterTest.php b/tests/FilterTest.php index e4cea386..56486037 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -92,6 +92,10 @@ }); it('specifies escape character in supported databases', function (string $dbDriver) { + if ($dbDriver === 'mariadb' && ! in_array('mariadb', DB::supportedDrivers())) { + $this->markTestSkipped('mariadb driver not supported in the installed version of illuminate/database dependency'); + } + $fakeConnection = "test_{$dbDriver}"; DB::connectUsing($fakeConnection, [ @@ -100,6 +104,7 @@ ]); DB::usingConnection($fakeConnection, function () use ($dbDriver) { + $request = new Request([ 'filter' => ['name' => 'to_find'], ]); @@ -108,10 +113,10 @@ ->allowedFilters('name', 'id') ->toSql(); - expect($queryBuilderSql)->when(in_array($dbDriver, ["sqlite","pgsql","sqlsrv"]), fn (Expectation $query) => $query->toContain("ESCAPE '\'")); - expect($queryBuilderSql)->when($dbDriver === 'mysql', fn (Expectation $query) => $query->not->toContain("ESCAPE '\'")); + expect($queryBuilderSql)->when(in_array($dbDriver, ["sqlite","sqlsrv"]), fn (Expectation $query) => $query->toContain("ESCAPE '\'")); + expect($queryBuilderSql)->when(in_array($dbDriver, ["mysql","mariadb", "pgsql"]), fn (Expectation $query) => $query->not->toContain("ESCAPE '\'")); }); -})->with(['sqlite', 'mysql', 'pgsql', 'sqlsrv']); +})->with(['sqlite', 'mysql', 'pgsql', 'sqlsrv', 'mariadb']); it('can filter results based on the existence of a property in an array', function () { $results = createQueryFromFilterRequest([ From 64f0453f4dea6a6fabf1ce4ddbb553e14da67bb6 Mon Sep 17 00:00:00 2001 From: Alex Vanderbist Date: Thu, 3 Oct 2024 13:10:10 +0200 Subject: [PATCH 14/51] Fix phpstan issue --- src/Filters/FiltersOperator.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Filters/FiltersOperator.php b/src/Filters/FiltersOperator.php index 3323f9d8..157649e7 100644 --- a/src/Filters/FiltersOperator.php +++ b/src/Filters/FiltersOperator.php @@ -37,7 +37,7 @@ public function __invoke(Builder $query, $value, string $property) return; } elseif ($this->filterOperator->isDynamic()) { - $filterOperator = $this->getDynamicFilterOperator($value, $this); + $filterOperator = $this->getDynamicFilterOperator($value); $this->removeDynamicFilterOperatorFromValue($value, $filterOperator); } @@ -48,7 +48,6 @@ 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; From 9ee75498cd2a257a501def127c281f1eaf118f30 Mon Sep 17 00:00:00 2001 From: AlexVanderbist Date: Thu, 3 Oct 2024 11:11:44 +0000 Subject: [PATCH 15/51] Update CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c232f50c..9f4b4da1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to `laravel-query-builder` will be documented in this file +## 6.2.1 - 2024-10-03 + +### What's Changed + +* Removed explicit escaping for `pgsql` driver in `FiltersPartial` - Fixes #941 by @Talpx1 in https://github.com/spatie/laravel-query-builder/pull/968 + +**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/6.2.0...6.2.1 + ## 6.2.0 - 2024-09-27 ### What's Changed From cf1cb4c11f04618657dbdc231700f29c3fbeab0e Mon Sep 17 00:00:00 2001 From: Alex Vanderbist Date: Thu, 3 Oct 2024 13:18:49 +0200 Subject: [PATCH 16/51] Add additional tests --- .phpunit.cache/test-results | 2 +- tests/FilterTest.php | 198 +++++++++++++++++++++--------------- 2 files changed, 115 insertions(+), 85 deletions(-) diff --git a/.phpunit.cache/test-results b/.phpunit.cache/test-results index 3f964581..4f4f8281 100644 --- a/.phpunit.cache/test-results +++ b/.phpunit.cache/test-results @@ -1 +1 @@ -{"version":"pest_2.34.7","defects":{"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_models_by_an_array_as_filter_value":7,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_based_on_the_partial_existence_of_a_property_in_an_array":7,"P\\Tests\\FilterTest::__pest_evaluable_it_ignores_empty_values_in_an_array_partial_filter":7,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_results_based_on_the_partial_existence_of_a_property_in_an_array":7},"times":{"P\\Tests\\FieldsTest::__pest_evaluable_it_fetches_all_columns_if_no_field_was_requested":0.009,"P\\Tests\\FieldsTest::__pest_evaluable_it_fetches_all_columns_if_no_field_was_requested_but_allowed_fields_were_specified":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_replaces_selected_array_columns_on_the_query":0.003,"P\\Tests\\FieldsTest::__pest_evaluable_it_replaces_selected_string_columns_on_the_query":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_specific_array_columns":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_specific_string_columns":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_wont_fetch_a_specific_array_column_if_its_not_allowed":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_wont_fetch_a_specific_string_column_if_its_not_allowed":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_sketchy_array_columns_if_they_are_allowed_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_sketchy_string_columns_if_they_are_allowed_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_guards_against_not_allowed_array_fields":0.004,"P\\Tests\\FieldsTest::__pest_evaluable_it_guards_against_not_allowed_string_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_guards_against_not_allowed_array_fields_from_an_included_resource":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_guards_against_not_allowed_string_fields_from_an_included_resource":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_only_requested_array_columns_from_an_included_model":0.017,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_only_requested_string_columns_from_an_included_model":0.004,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_requested_array_columns_from_included_models_up_to_two_levels_deep":0.006,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_requested_string_columns_from_included_models_up_to_two_levels_deep":0.003,"P\\Tests\\FieldsTest::__pest_evaluable_it_throws_an_exception_when_calling_allowed_includes_before_allowed_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_throws_an_exception_when_calling_allowed_includes_before_allowed_fields_but_with_requested_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_throws_an_exception_when_requesting_fields_for_an_allowed_included_without_any_allowed_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_allow_specific_fields_on_an_included_model":0.003,"P\\Tests\\FieldsTest::__pest_evaluable_it_wont_use_sketchy_field_requests":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_models_by_partial_property_by_default":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_models_by_an_array_as_filter_value":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_partially_and_case_insensitive":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_based_on_the_partial_existence_of_a_property_in_an_array":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_models_and_return_an_empty_collection":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_a_custom_base_query_with_select":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_specifies_escape_character_in_supported_databases#('sqlite')":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_specifies_escape_character_in_supported_databases#('mysql')":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_specifies_escape_character_in_supported_databases#('pgsql')":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_specifies_escape_character_in_supported_databases#('sqlsrv')":0.004,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_based_on_the_existence_of_a_property_in_an_array":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_ignores_empty_values_in_an_array_partial_filter":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_ignores_an_empty_array_partial_filter":0.002,"P\\Tests\\FilterTest::__pest_evaluable_falsy_values_are_not_ignored_when_applying_a_partial_filter":0.003,"P\\Tests\\FilterTest::__pest_evaluable_falsy_values_are_not_ignored_when_applying_a_begins_with_strict_filter":0.002,"P\\Tests\\FilterTest::__pest_evaluable_falsy_values_are_not_ignored_when_applying_a_ends_with_strict_filter":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_partial_using_begins_with_strict":0.004,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_partial_using_ends_with_strict":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_and_match_results_by_exact_property":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_and_reject_results_by_exact_property":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_scope":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_nested_relation_scope":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_type_hinted_scope":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_regular_and_type_hinted_scope":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_scope_with_multiple_parameters":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_scope_with_multiple_parameters_in_an_associative_array":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_a_custom_filter_class":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_allow_multiple_filters":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_allow_multiple_filters_as_an_array":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_by_multiple_filters":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_guards_against_invalid_filters":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_does_not_throw_invalid_filter_exception_when_disable_in_config":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_create_a_custom_filter_with_an_instantiated_filter":0.003,"P\\Tests\\FilterTest::__pest_evaluable_an_invalid_filter_query_exception_contains_the_unknown_and_allowed_filters":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_allows_for_adding_ignorable_values":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_should_not_apply_a_filter_if_the_supplied_value_is_ignored":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_the_filter_on_the_subset_of_allowed_values":0.007,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_the_filter_on_the_subset_of_allowed_values_regardless_of_the_keys_order":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_can_take_an_argument_for_custom_column_name_resolution":0.008,"P\\Tests\\FilterTest::__pest_evaluable_it_sets_property_column_name_to_property_name_by_default":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_resolves_queries_using_property_column_name":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_using_boolean_flags":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_a_default_filter_value_if_nothing_in_request":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_does_not_apply_default_filter_when_filter_exists_and_default_is_set":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_a_null_default_filter_value_if_nothing_in_request":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_does_not_apply_default_filter_when_filter_exists_and_default_null_is_set":0.006,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_a_nullable_filter_when_filter_exists_and_is_null":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_a_nullable_filter_when_filter_exists_and_is_set":0.013,"P\\Tests\\FilterTest::__pest_evaluable_it_should_filter_by_query_parameters_if_a_default_value_is_set_and_unset_afterwards":0.007,"P\\Tests\\FilterTest::__pest_evaluable_it_should_not_filter_at_all_if_a_default_value_is_set_and_unset_afterwards":0.004,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_a_filter_with_a_multi_dimensional_array_value":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_can_override_the_array_value_delimiter_for_single_filters":0.005,"P\\Tests\\FiltersCallbackTest::__pest_evaluable_it_should_filter_by_closure":0.003,"P\\Tests\\FiltersCallbackTest::__pest_evaluable_it_should_filter_by_array_callback":0.018,"P\\Tests\\FiltersTrashedTest::__pest_evaluable_it_should_filter_not_trashed_by_default":0.002,"P\\Tests\\FiltersTrashedTest::__pest_evaluable_it_can_filter_only_trashed":0.002,"P\\Tests\\FiltersTrashedTest::__pest_evaluable_it_can_filter_only_trashed_by_scope_directly":0.002,"P\\Tests\\FiltersTrashedTest::__pest_evaluable_it_can_filter_with_trashed":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_does_not_require_includes":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_handle_empty_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_model_relations":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_model_relations_by_alias":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_an_includes_callback":0.004,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_an_includes_count":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_allowing_an_include_also_allows_the_include_count":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_an_includes_exists":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_allowing_an_include_also_allows_the_include_exists":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_nested_model_relations":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_nested_model_relations_by_alias":0.005,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_model_relations_from_nested_model_relations":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_allowing_a_nested_include_only_allows_the_include_count_for_the_first_level":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_allowing_a_nested_include_only_allows_the_include_exists_for_the_first_level":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_morph_model_relations":0.005,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_reverse_morph_model_relations":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_camel_case_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_models_on_an_empty_collection":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_guards_against_invalid_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_does_not_throw_invalid_include_query_exception_when_disable_in_config":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_allow_multiple_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_allow_multiple_includes_as_an_array":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_remove_duplicate_includes_from_nested_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_multiple_model_relations":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_query_included_many_to_many_relationships":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_returns_correct_id_when_including_many_to_many_relationship":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_an_invalid_include_query_exception_contains_the_unknown_and_allowed_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_alias_multiple_allowed_includes":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_custom_include_class":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_custom_include_class_by_alias":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_take_an_argument_for_custom_column_name_resolution":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_a_custom_base_query_with_select":0.002,"P\\Tests\\QueryBuilderEndpointTest::__pest_evaluable_it_can_instantiate_the_query_builder_and_filter_the_query_for_an_actual_api_request":0.015,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_filter_nested_arrays":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_empty_filters_recursively":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_map_true_and_false_as_booleans_recursively":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_sort_query_param_from_the_request":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_sort_query_param_from_the_request_body":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_different_sort_query_parameter_name":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_return_an_empty_collection_when_no_sort_query_param_is_specified":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_multiple_sort_parameters_from_the_request":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_return_an_empty_collection_when_no_sort_query_params_are_specified":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_filter_query_params_from_the_request":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_filter_query_params_from_the_request_body":0.01,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_use_different_filter_query_parameter_name":0.003,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_use_null_as_the_filter_query_parameter_name":0.004,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_empty_filters":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_return_an_empty_collection_when_no_filter_query_params_are_specified":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_map_true_and_false_as_booleans_when_given_in_a_filter_query_string":0.005,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_map_comma_separated_values_as_arrays_when_given_in_a_filter_query_string":0.004,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_map_array_in_filter_recursively_when_given_in_a_filter_query_string":0.003,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_map_comma_separated_values_as_arrays_when_given_in_a_filter_query_string_and_get_those_by_key":0.006,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_include_query_params_from_the_request":0.009,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_include_from_the_request_body":0.004,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_different_include_query_parameter_name":0.005,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_return_an_empty_collection_when_no_include_query_params_are_specified":0.004,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_requested_fields":0.005,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_requested_fields_without_a_table_name":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_nested_fields":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_nested_fields_from_a_string_fields_request":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_requested_fields_from_the_request_body":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_different_fields_parameter_name":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_append_query_params_from_the_request":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_different_append_query_parameter_name":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_return_an_empty_collection_when_no_append_query_params_are_specified":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_append_query_params_from_the_request_body":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_takes_custom_delimiters_for_splitting_request_parameters":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_be_given_an_eloquent_query_using_where":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_be_given_an_eloquent_query_using_select":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_be_given_a_belongs_to_many_relation_query":0.005,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_be_given_a_belongs_to_many_relation_query_with_pivot":0.003,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_be_given_a_model_class_name":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_not_be_given_a_string_that_is_not_a_class_name":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_not_be_given_an_object_that_is_neither_relation_nor_eloquent_builder":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_will_determine_the_request_when_its_not_given":0.004,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_query_soft_deletes":0.006,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_query_global_scopes":0.004,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_keeps_eager_loaded_relationships_from_the_base_query":0.004,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_keeps_local_macros_added_to_the_base_query":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_keeps_the_on_delete_callback_added_to_the_base_query":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_query_local_scopes":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_executes_the_same_query_regardless_of_the_order_of_applied_filters_or_sorts":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_filter_when_sorting_by_joining_a_related_model_which_contains_the_same_field_name":0.003,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_queries_the_correct_data_for_a_relationship_query":0.004,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_does_not_lose_pivot_values_with_belongs_to_many_relation":0.005,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_clones_the_subject_upon_cloning":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_supports_clone_as_method":0.002,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_related_model_property":0.002,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_results_based_on_the_partial_existence_of_a_property_in_an_array":0.003,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_models_and_return_an_empty_collection":0.003,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_related_nested_model_property":0.003,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_related_model_and_related_nested_model_property":0.002,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_results_based_on_the_existence_of_a_property_in_an_array":0.004,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_and_reject_results_by_exact_property":0.003,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_disable_exact_filtering_based_on_related_model_properties":0.002,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_disable_partial_filtering_based_on_related_model_properties":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_query_ascending":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_has_the_allowed_sorts_property_set_even_if_no_sorts_are_requested":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_query_descending":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_query_by_alias":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_wont_sort_by_columns_that_werent_allowed_first":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_allow_a_descending_sort_by_still_sort_ascending":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_query_by_a_related_property":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_by_json_property_if_its_an_allowed_sort":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_by_sketchy_alias_if_its_an_allowed_sort":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_query_with_custom_select":0.009,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_chunk_query":0.007,"P\\Tests\\SortTest::__pest_evaluable_it_can_guard_against_sorts_that_are_not_allowed":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_will_throw_an_exception_if_a_sort_property_is_not_allowed":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_does_not_throw_invalid_sort_query_exception_when_disable_in_config":0.002,"P\\Tests\\SortTest::__pest_evaluable_an_invalid_sort_query_exception_contains_the_unknown_and_allowed_sorts":0.004,"P\\Tests\\SortTest::__pest_evaluable_it_wont_sort_if_no_sort_query_parameter_is_given":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_wont_sort_sketchy_sort_requests":0.005,"P\\Tests\\SortTest::__pest_evaluable_it_uses_default_sort_parameter_when_no_sort_was_requested":0.005,"P\\Tests\\SortTest::__pest_evaluable_it_doesnt_use_the_default_sort_parameter_when_a_sort_was_requested":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_allows_default_custom_sort_class_parameter":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_uses_default_descending_sort_parameter":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_allows_multiple_default_sort_parameters":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_allows_multiple_default_sort_parameters_in_an_array":0.005,"P\\Tests\\SortTest::__pest_evaluable_it_can_allow_multiple_sort_parameters":0.005,"P\\Tests\\SortTest::__pest_evaluable_it_can_allow_multiple_sort_parameters_as_an_array":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_by_multiple_columns":0.008,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_by_a_custom_sort_class":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_take_an_argument_for_custom_column_name_resolution":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_sets_property_column_name_to_property_name_by_default":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_resolves_queries_using_property_column_name":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_descending_with_an_alias":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_does_not_add_sort_clauses_multiple_times":0.003,"P\\Tests\\SortTest::__pest_evaluable_given_a_default_sort_a_sort_alias_will_still_be_resolved":0.002,"P\\Tests\\SortTest::__pest_evaluable_late_specified_sorts_still_check_for_allowance":0.004,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_and_use_scoped_filters_at_the_same_time":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_ignores_non_existing_sorts_before_adding_them_as_an_alias":0.002,"P\\Tests\\SortTest::__pest_evaluable_raw_sorts_do_not_get_purged_when_specifying_allowed_sorts":0.002,"P\\Tests\\SortTest::__pest_evaluable_the_default_direction_of_an_allow_sort_can_be_set":0.004,"P\\Tests\\SortsCallbackTest::__pest_evaluable_it_should_sort_by_closure":0.003,"P\\Tests\\SortsCallbackTest::__pest_evaluable_it_should_sort_by_array_callback":0.002}} \ No newline at end of file +{"version":"pest_2.34.7","defects":{"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_models_by_an_array_as_filter_value":7,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_based_on_the_partial_existence_of_a_property_in_an_array":7,"P\\Tests\\FilterTest::__pest_evaluable_it_ignores_empty_values_in_an_array_partial_filter":7,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_results_based_on_the_partial_existence_of_a_property_in_an_array":7},"times":{"P\\Tests\\FieldsTest::__pest_evaluable_it_fetches_all_columns_if_no_field_was_requested":0.009,"P\\Tests\\FieldsTest::__pest_evaluable_it_fetches_all_columns_if_no_field_was_requested_but_allowed_fields_were_specified":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_replaces_selected_array_columns_on_the_query":0.003,"P\\Tests\\FieldsTest::__pest_evaluable_it_replaces_selected_string_columns_on_the_query":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_specific_array_columns":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_specific_string_columns":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_wont_fetch_a_specific_array_column_if_its_not_allowed":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_wont_fetch_a_specific_string_column_if_its_not_allowed":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_sketchy_array_columns_if_they_are_allowed_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_sketchy_string_columns_if_they_are_allowed_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_guards_against_not_allowed_array_fields":0.004,"P\\Tests\\FieldsTest::__pest_evaluable_it_guards_against_not_allowed_string_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_guards_against_not_allowed_array_fields_from_an_included_resource":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_guards_against_not_allowed_string_fields_from_an_included_resource":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_only_requested_array_columns_from_an_included_model":0.017,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_only_requested_string_columns_from_an_included_model":0.004,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_requested_array_columns_from_included_models_up_to_two_levels_deep":0.006,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_requested_string_columns_from_included_models_up_to_two_levels_deep":0.003,"P\\Tests\\FieldsTest::__pest_evaluable_it_throws_an_exception_when_calling_allowed_includes_before_allowed_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_throws_an_exception_when_calling_allowed_includes_before_allowed_fields_but_with_requested_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_throws_an_exception_when_requesting_fields_for_an_allowed_included_without_any_allowed_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_allow_specific_fields_on_an_included_model":0.003,"P\\Tests\\FieldsTest::__pest_evaluable_it_wont_use_sketchy_field_requests":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_models_by_partial_property_by_default":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_models_by_an_array_as_filter_value":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_partially_and_case_insensitive":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_based_on_the_partial_existence_of_a_property_in_an_array":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_models_and_return_an_empty_collection":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_a_custom_base_query_with_select":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_specifies_escape_character_in_supported_databases#('sqlite')":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_specifies_escape_character_in_supported_databases#('mysql')":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_specifies_escape_character_in_supported_databases#('pgsql')":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_specifies_escape_character_in_supported_databases#('sqlsrv')":0.004,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_based_on_the_existence_of_a_property_in_an_array":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_ignores_empty_values_in_an_array_partial_filter":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_ignores_an_empty_array_partial_filter":0.002,"P\\Tests\\FilterTest::__pest_evaluable_falsy_values_are_not_ignored_when_applying_a_partial_filter":0.003,"P\\Tests\\FilterTest::__pest_evaluable_falsy_values_are_not_ignored_when_applying_a_begins_with_strict_filter":0.002,"P\\Tests\\FilterTest::__pest_evaluable_falsy_values_are_not_ignored_when_applying_a_ends_with_strict_filter":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_partial_using_begins_with_strict":0.004,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_partial_using_ends_with_strict":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_and_match_results_by_exact_property":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_and_reject_results_by_exact_property":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_scope":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_nested_relation_scope":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_type_hinted_scope":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_regular_and_type_hinted_scope":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_scope_with_multiple_parameters":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_scope_with_multiple_parameters_in_an_associative_array":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_a_custom_filter_class":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_allow_multiple_filters":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_allow_multiple_filters_as_an_array":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_by_multiple_filters":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_guards_against_invalid_filters":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_does_not_throw_invalid_filter_exception_when_disable_in_config":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_create_a_custom_filter_with_an_instantiated_filter":0.003,"P\\Tests\\FilterTest::__pest_evaluable_an_invalid_filter_query_exception_contains_the_unknown_and_allowed_filters":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_allows_for_adding_ignorable_values":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_should_not_apply_a_filter_if_the_supplied_value_is_ignored":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_the_filter_on_the_subset_of_allowed_values":0.007,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_the_filter_on_the_subset_of_allowed_values_regardless_of_the_keys_order":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_can_take_an_argument_for_custom_column_name_resolution":0.008,"P\\Tests\\FilterTest::__pest_evaluable_it_sets_property_column_name_to_property_name_by_default":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_resolves_queries_using_property_column_name":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_using_boolean_flags":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_a_default_filter_value_if_nothing_in_request":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_does_not_apply_default_filter_when_filter_exists_and_default_is_set":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_a_null_default_filter_value_if_nothing_in_request":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_does_not_apply_default_filter_when_filter_exists_and_default_null_is_set":0.006,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_a_nullable_filter_when_filter_exists_and_is_null":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_a_nullable_filter_when_filter_exists_and_is_set":0.013,"P\\Tests\\FilterTest::__pest_evaluable_it_should_filter_by_query_parameters_if_a_default_value_is_set_and_unset_afterwards":0.007,"P\\Tests\\FilterTest::__pest_evaluable_it_should_not_filter_at_all_if_a_default_value_is_set_and_unset_afterwards":0.004,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_a_filter_with_a_multi_dimensional_array_value":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_can_override_the_array_value_delimiter_for_single_filters":0.005,"P\\Tests\\FiltersCallbackTest::__pest_evaluable_it_should_filter_by_closure":0.003,"P\\Tests\\FiltersCallbackTest::__pest_evaluable_it_should_filter_by_array_callback":0.018,"P\\Tests\\FiltersTrashedTest::__pest_evaluable_it_should_filter_not_trashed_by_default":0.002,"P\\Tests\\FiltersTrashedTest::__pest_evaluable_it_can_filter_only_trashed":0.002,"P\\Tests\\FiltersTrashedTest::__pest_evaluable_it_can_filter_only_trashed_by_scope_directly":0.002,"P\\Tests\\FiltersTrashedTest::__pest_evaluable_it_can_filter_with_trashed":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_does_not_require_includes":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_handle_empty_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_model_relations":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_model_relations_by_alias":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_an_includes_callback":0.004,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_an_includes_count":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_allowing_an_include_also_allows_the_include_count":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_an_includes_exists":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_allowing_an_include_also_allows_the_include_exists":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_nested_model_relations":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_nested_model_relations_by_alias":0.005,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_model_relations_from_nested_model_relations":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_allowing_a_nested_include_only_allows_the_include_count_for_the_first_level":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_allowing_a_nested_include_only_allows_the_include_exists_for_the_first_level":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_morph_model_relations":0.005,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_reverse_morph_model_relations":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_camel_case_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_models_on_an_empty_collection":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_guards_against_invalid_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_does_not_throw_invalid_include_query_exception_when_disable_in_config":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_allow_multiple_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_allow_multiple_includes_as_an_array":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_remove_duplicate_includes_from_nested_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_multiple_model_relations":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_query_included_many_to_many_relationships":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_returns_correct_id_when_including_many_to_many_relationship":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_an_invalid_include_query_exception_contains_the_unknown_and_allowed_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_alias_multiple_allowed_includes":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_custom_include_class":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_custom_include_class_by_alias":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_take_an_argument_for_custom_column_name_resolution":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_a_custom_base_query_with_select":0.002,"P\\Tests\\QueryBuilderEndpointTest::__pest_evaluable_it_can_instantiate_the_query_builder_and_filter_the_query_for_an_actual_api_request":0.015,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_filter_nested_arrays":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_empty_filters_recursively":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_map_true_and_false_as_booleans_recursively":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_sort_query_param_from_the_request":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_sort_query_param_from_the_request_body":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_different_sort_query_parameter_name":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_return_an_empty_collection_when_no_sort_query_param_is_specified":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_multiple_sort_parameters_from_the_request":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_return_an_empty_collection_when_no_sort_query_params_are_specified":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_filter_query_params_from_the_request":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_filter_query_params_from_the_request_body":0.01,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_use_different_filter_query_parameter_name":0.003,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_use_null_as_the_filter_query_parameter_name":0.004,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_empty_filters":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_return_an_empty_collection_when_no_filter_query_params_are_specified":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_map_true_and_false_as_booleans_when_given_in_a_filter_query_string":0.005,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_map_comma_separated_values_as_arrays_when_given_in_a_filter_query_string":0.004,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_map_array_in_filter_recursively_when_given_in_a_filter_query_string":0.003,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_map_comma_separated_values_as_arrays_when_given_in_a_filter_query_string_and_get_those_by_key":0.006,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_include_query_params_from_the_request":0.009,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_include_from_the_request_body":0.004,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_different_include_query_parameter_name":0.005,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_return_an_empty_collection_when_no_include_query_params_are_specified":0.004,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_requested_fields":0.005,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_requested_fields_without_a_table_name":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_nested_fields":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_nested_fields_from_a_string_fields_request":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_requested_fields_from_the_request_body":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_different_fields_parameter_name":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_append_query_params_from_the_request":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_different_append_query_parameter_name":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_return_an_empty_collection_when_no_append_query_params_are_specified":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_append_query_params_from_the_request_body":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_takes_custom_delimiters_for_splitting_request_parameters":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_be_given_an_eloquent_query_using_where":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_be_given_an_eloquent_query_using_select":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_be_given_a_belongs_to_many_relation_query":0.005,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_be_given_a_belongs_to_many_relation_query_with_pivot":0.003,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_be_given_a_model_class_name":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_not_be_given_a_string_that_is_not_a_class_name":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_not_be_given_an_object_that_is_neither_relation_nor_eloquent_builder":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_will_determine_the_request_when_its_not_given":0.004,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_query_soft_deletes":0.006,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_query_global_scopes":0.004,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_keeps_eager_loaded_relationships_from_the_base_query":0.004,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_keeps_local_macros_added_to_the_base_query":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_keeps_the_on_delete_callback_added_to_the_base_query":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_query_local_scopes":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_executes_the_same_query_regardless_of_the_order_of_applied_filters_or_sorts":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_filter_when_sorting_by_joining_a_related_model_which_contains_the_same_field_name":0.003,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_queries_the_correct_data_for_a_relationship_query":0.004,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_does_not_lose_pivot_values_with_belongs_to_many_relation":0.005,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_clones_the_subject_upon_cloning":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_supports_clone_as_method":0.002,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_related_model_property":0.002,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_results_based_on_the_partial_existence_of_a_property_in_an_array":0.003,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_models_and_return_an_empty_collection":0.003,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_related_nested_model_property":0.003,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_related_model_and_related_nested_model_property":0.002,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_results_based_on_the_existence_of_a_property_in_an_array":0.004,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_and_reject_results_by_exact_property":0.003,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_disable_exact_filtering_based_on_related_model_properties":0.002,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_disable_partial_filtering_based_on_related_model_properties":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_query_ascending":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_has_the_allowed_sorts_property_set_even_if_no_sorts_are_requested":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_query_descending":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_query_by_alias":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_wont_sort_by_columns_that_werent_allowed_first":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_allow_a_descending_sort_by_still_sort_ascending":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_query_by_a_related_property":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_by_json_property_if_its_an_allowed_sort":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_by_sketchy_alias_if_its_an_allowed_sort":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_query_with_custom_select":0.009,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_chunk_query":0.007,"P\\Tests\\SortTest::__pest_evaluable_it_can_guard_against_sorts_that_are_not_allowed":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_will_throw_an_exception_if_a_sort_property_is_not_allowed":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_does_not_throw_invalid_sort_query_exception_when_disable_in_config":0.002,"P\\Tests\\SortTest::__pest_evaluable_an_invalid_sort_query_exception_contains_the_unknown_and_allowed_sorts":0.004,"P\\Tests\\SortTest::__pest_evaluable_it_wont_sort_if_no_sort_query_parameter_is_given":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_wont_sort_sketchy_sort_requests":0.005,"P\\Tests\\SortTest::__pest_evaluable_it_uses_default_sort_parameter_when_no_sort_was_requested":0.005,"P\\Tests\\SortTest::__pest_evaluable_it_doesnt_use_the_default_sort_parameter_when_a_sort_was_requested":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_allows_default_custom_sort_class_parameter":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_uses_default_descending_sort_parameter":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_allows_multiple_default_sort_parameters":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_allows_multiple_default_sort_parameters_in_an_array":0.005,"P\\Tests\\SortTest::__pest_evaluable_it_can_allow_multiple_sort_parameters":0.005,"P\\Tests\\SortTest::__pest_evaluable_it_can_allow_multiple_sort_parameters_as_an_array":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_by_multiple_columns":0.008,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_by_a_custom_sort_class":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_take_an_argument_for_custom_column_name_resolution":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_sets_property_column_name_to_property_name_by_default":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_resolves_queries_using_property_column_name":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_descending_with_an_alias":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_does_not_add_sort_clauses_multiple_times":0.003,"P\\Tests\\SortTest::__pest_evaluable_given_a_default_sort_a_sort_alias_will_still_be_resolved":0.002,"P\\Tests\\SortTest::__pest_evaluable_late_specified_sorts_still_check_for_allowance":0.004,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_and_use_scoped_filters_at_the_same_time":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_ignores_non_existing_sorts_before_adding_them_as_an_alias":0.002,"P\\Tests\\SortTest::__pest_evaluable_raw_sorts_do_not_get_purged_when_specifying_allowed_sorts":0.002,"P\\Tests\\SortTest::__pest_evaluable_the_default_direction_of_an_allow_sort_can_be_set":0.004,"P\\Tests\\SortsCallbackTest::__pest_evaluable_it_should_sort_by_closure":0.003,"P\\Tests\\SortsCallbackTest::__pest_evaluable_it_should_sort_by_array_callback":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_use_a_custom_filter_query_string_parameter":0.007,"P\\Tests\\FilterTest::__pest_evaluable_it_can_work_without_a_general_filter_query_string_parameter_configured":0.007}} \ No newline at end of file diff --git a/tests/FilterTest.php b/tests/FilterTest.php index 56486037..ea05e267 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -31,10 +31,38 @@ expect($models)->toHaveCount(1); }); +it('can use a custom filter query string parameter', function () { + config(['query-builder.parameters.filter' => 'custom_filter']); + + $request = new Request([ + 'custom_filter' => ['name' => $this->models->first()->name], + ]); + + $models = QueryBuilder::for(TestModel::class, $request) + ->allowedFilters('name') + ->get(); + + expect($models)->toHaveCount(1); +}); + +it('can work without a general filter query string parameter configured', function () { + config(['query-builder.parameters.filter' => null]); + + $request = new Request([ + 'name' => $this->models->first()->name, + ]); + + $models = QueryBuilder::for(TestModel::class, $request) + ->allowedFilters('name') + ->get(); + + expect($models)->toHaveCount(1); +}); + it('can filter models by an array as filter value', function () { $models = createQueryFromFilterRequest([ - 'name' => ['first' => $this->models->first()->name], - ]) + 'name' => ['first' => $this->models->first()->name], + ]) ->allowedFilters('name') ->get(); @@ -43,8 +71,8 @@ it('can filter partially and case insensitive', function () { $models = createQueryFromFilterRequest([ - 'name' => strtoupper($this->models->first()->name), - ]) + 'name' => strtoupper($this->models->first()->name), + ]) ->allowedFilters('name') ->get(); @@ -56,8 +84,8 @@ $model2 = TestModel::create(['name' => 'uvwxyz']); $results = createQueryFromFilterRequest([ - 'name' => 'abc,xyz', - ]) + 'name' => 'abc,xyz', + ]) ->allowedFilters('name') ->get(); @@ -67,8 +95,8 @@ it('can filter models and return an empty collection', function () { $models = createQueryFromFilterRequest([ - 'name' => 'None existing first name', - ]) + 'name' => 'None existing first name', + ]) ->allowedFilters('name') ->get(); @@ -92,7 +120,7 @@ }); it('specifies escape character in supported databases', function (string $dbDriver) { - if ($dbDriver === 'mariadb' && ! in_array('mariadb', DB::supportedDrivers())) { + if ($dbDriver === 'mariadb' && !in_array('mariadb', DB::supportedDrivers())) { $this->markTestSkipped('mariadb driver not supported in the installed version of illuminate/database dependency'); } @@ -113,15 +141,17 @@ ->allowedFilters('name', 'id') ->toSql(); - expect($queryBuilderSql)->when(in_array($dbDriver, ["sqlite","sqlsrv"]), fn (Expectation $query) => $query->toContain("ESCAPE '\'")); - expect($queryBuilderSql)->when(in_array($dbDriver, ["mysql","mariadb", "pgsql"]), fn (Expectation $query) => $query->not->toContain("ESCAPE '\'")); + expect($queryBuilderSql)->when(in_array($dbDriver, ["sqlite", "sqlsrv"]), fn(Expectation $query + ) => $query->toContain("ESCAPE '\'")); + expect($queryBuilderSql)->when(in_array($dbDriver, ["mysql", "mariadb", "pgsql"]), fn(Expectation $query + ) => $query->not->toContain("ESCAPE '\'")); }); })->with(['sqlite', 'mysql', 'pgsql', 'sqlsrv', 'mariadb']); it('can filter results based on the existence of a property in an array', function () { $results = createQueryFromFilterRequest([ - 'id' => '1,2', - ]) + 'id' => '1,2', + ]) ->allowedFilters(AllowedFilter::exact('id')) ->get(); @@ -131,8 +161,8 @@ it('ignores empty values in an array partial filter', function () { $results = createQueryFromFilterRequest([ - 'id' => '2,', - ]) + 'id' => '2,', + ]) ->allowedFilters(AllowedFilter::partial('id')) ->get(); @@ -142,8 +172,8 @@ it('ignores an empty array partial filter', function () { $results = createQueryFromFilterRequest([ - 'id' => ',,', - ]) + 'id' => ',,', + ]) ->allowedFilters(AllowedFilter::partial('id')) ->get(); @@ -154,8 +184,8 @@ DB::enableQueryLog(); createQueryFromFilterRequest([ - 'id' => [0], - ]) + 'id' => [0], + ]) ->allowedFilters(AllowedFilter::partial('id')) ->get(); @@ -166,8 +196,8 @@ DB::enableQueryLog(); createQueryFromFilterRequest([ - 'id' => [0], - ]) + 'id' => [0], + ]) ->allowedFilters(AllowedFilter::beginsWithStrict('id')) ->get(); @@ -178,8 +208,8 @@ DB::enableQueryLog(); createQueryFromFilterRequest([ - 'id' => [0], - ]) + 'id' => [0], + ]) ->allowedFilters(AllowedFilter::endsWithStrict('id')) ->get(); @@ -231,8 +261,8 @@ ->get(); $modelsResult = createQueryFromFilterRequest([ - 'id' => $testModel->id, - ]) + 'id' => $testModel->id, + ]) ->allowedFilters(AllowedFilter::exact('id')) ->get(); @@ -243,8 +273,8 @@ $testModel = TestModel::create(['name' => 'John Testing Doe']); $modelsResult = createQueryFromFilterRequest([ - 'name' => ' Testing ', - ]) + 'name' => ' Testing ', + ]) ->allowedFilters(AllowedFilter::exact('name')) ->get(); @@ -328,8 +358,8 @@ public function __invoke(Builder $query, $value, string $property): Builder }; $modelResult = createQueryFromFilterRequest([ - 'custom_name' => $testModel->name, - ]) + 'custom_name' => $testModel->name, + ]) ->allowedFilters(AllowedFilter::custom('custom_name', $filterClass)) ->first(); @@ -341,8 +371,8 @@ public function __invoke(Builder $query, $value, string $property): Builder $model2 = TestModel::create(['name' => 'abcdef']); $results = createQueryFromFilterRequest([ - 'name' => 'abc', - ]) + 'name' => 'abc', + ]) ->allowedFilters('name', AllowedFilter::exact('id')) ->get(); @@ -355,8 +385,8 @@ public function __invoke(Builder $query, $value, string $property): Builder $model2 = TestModel::create(['name' => 'abcdef']); $results = createQueryFromFilterRequest([ - 'name' => 'abc', - ]) + 'name' => 'abc', + ]) ->allowedFilters(['name', AllowedFilter::exact('id')]) ->get(); @@ -369,9 +399,9 @@ public function __invoke(Builder $query, $value, string $property): Builder $model2 = TestModel::create(['name' => 'abcdef']); $results = createQueryFromFilterRequest([ - 'name' => 'abc', - 'id' => "1,{$model1->id}", - ]) + 'name' => 'abc', + 'id' => "1,{$model1->id}", + ]) ->allowedFilters('name', AllowedFilter::exact('id')) ->get(); @@ -414,8 +444,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['name' => 'abcdef']); $results = createQueryFromFilterRequest([ - '*' => '*', - ]) + '*' => '*', + ]) ->allowedFilters('name', AllowedFilter::custom('*', $customFilter)) ->get(); @@ -444,8 +474,8 @@ public function __invoke(Builder $query, $value, string $property): Builder it('should not apply a filter if the supplied value is ignored', function () { $models = createQueryFromFilterRequest([ - 'name' => '-1', - ]) + 'name' => '-1', + ]) ->allowedFilters(AllowedFilter::exact('name')->ignore('-1')) ->get(); @@ -457,8 +487,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['name' => 'John Deer']); $models = createQueryFromFilterRequest([ - 'name' => 'John Deer,John Doe', - ]) + 'name' => 'John Deer,John Doe', + ]) ->allowedFilters(AllowedFilter::exact('name')->ignore('John Doe')) ->get(); @@ -470,8 +500,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['id' => 7, 'name' => 'John Deer']); $models = createQueryFromFilterRequest([ - 'id' => [ 7, 6 ], - ]) + 'id' => [7, 6], + ]) ->allowedFilters(AllowedFilter::exact('id')->ignore(6)) ->get(); @@ -497,8 +527,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['name' => 'abcdef']); $models = createQueryFromFilterRequest([ - 'nickname' => 'abcdef', - ]) + 'nickname' => 'abcdef', + ]) ->allowedFilters($filter) ->get(); @@ -533,8 +563,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['name' => 'UniqueJohn Deer']); $models = createQueryFromFilterRequest([ - 'name' => 'UniqueDoe', - ]) + 'name' => 'UniqueDoe', + ]) ->allowedFilters(AllowedFilter::partial('name')->default('UniqueJohn')) ->get(); @@ -557,8 +587,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['name' => 'UniqueJohn Deer']); $models = createQueryFromFilterRequest([ - 'name' => 'UniqueJohn Deer', - ]) + 'name' => 'UniqueJohn Deer', + ]) ->allowedFilters(AllowedFilter::exact('name')->default(null)) ->get(); @@ -570,8 +600,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['name' => 'UniqueJohn Deer']); $models = createQueryFromFilterRequest([ - 'name' => null, - ]) + 'name' => null, + ]) ->allowedFilters(AllowedFilter::exact('name')->nullable()) ->get(); @@ -583,8 +613,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['name' => 'UniqueJohn Deer']); $models = createQueryFromFilterRequest([ - 'name' => 'UniqueJohn Deer', - ]) + 'name' => 'UniqueJohn Deer', + ]) ->allowedFilters(AllowedFilter::exact('name')->nullable()) ->get(); @@ -596,8 +626,8 @@ public function __invoke(Builder $query, $value, string $property): Builder $filterWithDefault = AllowedFilter::exact('name')->default('some default value'); $models = createQueryFromFilterRequest([ - 'name' => 'John Doe', - ]) + 'name' => 'John Doe', + ]) ->allowedFilters($filterWithDefault->unsetDefault()) ->get(); @@ -617,10 +647,10 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['name' => 'John Doe']); $models = createQueryFromFilterRequest(['conditions' => [[ - 'attribute' => 'name', - 'operator' => '=', - 'value' => 'John Doe', - ]]]) + 'attribute' => 'name', + 'operator' => '=', + 'value' => 'John Doe', + ]]]) ->allowedFilters(AllowedFilter::callback('conditions', function ($query, $conditions) { foreach ($conditions as $condition) { $query->where( @@ -641,24 +671,24 @@ public function __invoke(Builder $query, $value, string $property): Builder // First use default delimiter $models = createQueryFromFilterRequest([ - 'ref_id' => 'h4S4MG3(+>azv4z/I,>XZII/Q1On', - ]) + 'ref_id' => 'h4S4MG3(+>azv4z/I,>XZII/Q1On', + ]) ->allowedFilters(AllowedFilter::exact('ref_id', 'name', true)) ->get(); expect($models->count())->toEqual(2); // Custom delimiter $models = createQueryFromFilterRequest([ - 'ref_id' => 'h4S4MG3(+>azv4z/I|>XZII/Q1On', - ]) + 'ref_id' => 'h4S4MG3(+>azv4z/I|>XZII/Q1On', + ]) ->allowedFilters(AllowedFilter::exact('ref_id', 'name', true, '|')) ->get(); expect($models->count())->toEqual(2); // Custom delimiter, but default in request $models = createQueryFromFilterRequest([ - 'ref_id' => 'h4S4MG3(+>azv4z/I,>XZII/Q1On', - ]) + 'ref_id' => 'h4S4MG3(+>azv4z/I,>XZII/Q1On', + ]) ->allowedFilters(AllowedFilter::exact('ref_id', 'name', true, '|')) ->get(); expect($models->count())->toEqual(0); @@ -668,8 +698,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['name' => 'John Doe']); $results = createQueryFromFilterRequest([ - 'name' => 'John Doe', - ]) + 'name' => 'John Doe', + ]) ->allowedFilters(AllowedFilter::operator('name', FilterOperator::EQUAL)) ->get(); @@ -680,8 +710,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['name' => 'John Doe']); $results = createQueryFromFilterRequest([ - 'name' => 'John Doe', - ]) + 'name' => 'John Doe', + ]) ->allowedFilters(AllowedFilter::operator('name', FilterOperator::NOT_EQUAL)) ->get(); @@ -692,8 +722,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['salary' => 5000]); $results = createQueryFromFilterRequest([ - 'salary' => 3000, - ]) + 'salary' => 3000, + ]) ->allowedFilters(AllowedFilter::operator('salary', FilterOperator::GREATER_THAN)) ->get(); @@ -704,8 +734,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['salary' => 5000]); $results = createQueryFromFilterRequest([ - 'salary' => 7000, - ]) + 'salary' => 7000, + ]) ->allowedFilters(AllowedFilter::operator('salary', FilterOperator::LESS_THAN)) ->get(); @@ -716,8 +746,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['salary' => 5000]); $results = createQueryFromFilterRequest([ - 'salary' => 3000, - ]) + 'salary' => 3000, + ]) ->allowedFilters(AllowedFilter::operator('salary', FilterOperator::GREATER_THAN_OR_EQUAL)) ->get(); @@ -728,8 +758,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['salary' => 5000]); $results = createQueryFromFilterRequest([ - 'salary' => 7000, - ]) + 'salary' => 7000, + ]) ->allowedFilters(AllowedFilter::operator('salary', FilterOperator::LESS_THAN_OR_EQUAL)) ->get(); @@ -741,8 +771,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['name' => 'Max Doe']); $results = createQueryFromFilterRequest([ - 'name' => 'John Doe,Max Doe', - ]) + 'name' => 'John Doe,Max Doe', + ]) ->allowedFilters(AllowedFilter::operator('name', FilterOperator::EQUAL, 'or')) ->get(); @@ -754,8 +784,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['salary' => 2000]); $results = createQueryFromFilterRequest([ - 'salary' => '>2000', - ]) + 'salary' => '>2000', + ]) ->allowedFilters(AllowedFilter::operator('salary', FilterOperator::DYNAMIC)) ->get(); @@ -769,8 +799,8 @@ public function __invoke(Builder $query, $value, string $property): Builder TestModel::create(['salary' => 4000]); $results = createQueryFromFilterRequest([ - 'salary' => '>1000,<4000', - ]) + 'salary' => '>1000,<4000', + ]) ->allowedFilters(AllowedFilter::operator('salary', FilterOperator::DYNAMIC)) ->get(); From 5dd89d1fc1281ee39faa2a3347000989e25153d7 Mon Sep 17 00:00:00 2001 From: AlexVanderbist Date: Thu, 3 Oct 2024 11:35:33 +0000 Subject: [PATCH 17/51] Fix styling --- tests/FilterTest.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/FilterTest.php b/tests/FilterTest.php index ea05e267..5e724565 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -120,7 +120,7 @@ }); it('specifies escape character in supported databases', function (string $dbDriver) { - if ($dbDriver === 'mariadb' && !in_array('mariadb', DB::supportedDrivers())) { + if ($dbDriver === 'mariadb' && ! in_array('mariadb', DB::supportedDrivers())) { $this->markTestSkipped('mariadb driver not supported in the installed version of illuminate/database dependency'); } @@ -141,9 +141,11 @@ ->allowedFilters('name', 'id') ->toSql(); - expect($queryBuilderSql)->when(in_array($dbDriver, ["sqlite", "sqlsrv"]), fn(Expectation $query + expect($queryBuilderSql)->when(in_array($dbDriver, ["sqlite", "sqlsrv"]), fn ( + Expectation $query ) => $query->toContain("ESCAPE '\'")); - expect($queryBuilderSql)->when(in_array($dbDriver, ["mysql", "mariadb", "pgsql"]), fn(Expectation $query + expect($queryBuilderSql)->when(in_array($dbDriver, ["mysql", "mariadb", "pgsql"]), fn ( + Expectation $query ) => $query->not->toContain("ESCAPE '\'")); }); })->with(['sqlite', 'mysql', 'pgsql', 'sqlsrv', 'mariadb']); From 9fd030e538f61cad991c56a64c21c56452bb64d9 Mon Sep 17 00:00:00 2001 From: Alex Vanderbist Date: Thu, 3 Oct 2024 13:45:39 +0200 Subject: [PATCH 18/51] Update larastan/phpstan --- .phpunit.cache/test-results | 2 +- composer.json | 4 ++-- phpstan.neon.dist | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.phpunit.cache/test-results b/.phpunit.cache/test-results index 4f4f8281..452701fe 100644 --- a/.phpunit.cache/test-results +++ b/.phpunit.cache/test-results @@ -1 +1 @@ -{"version":"pest_2.34.7","defects":{"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_models_by_an_array_as_filter_value":7,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_based_on_the_partial_existence_of_a_property_in_an_array":7,"P\\Tests\\FilterTest::__pest_evaluable_it_ignores_empty_values_in_an_array_partial_filter":7,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_results_based_on_the_partial_existence_of_a_property_in_an_array":7},"times":{"P\\Tests\\FieldsTest::__pest_evaluable_it_fetches_all_columns_if_no_field_was_requested":0.009,"P\\Tests\\FieldsTest::__pest_evaluable_it_fetches_all_columns_if_no_field_was_requested_but_allowed_fields_were_specified":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_replaces_selected_array_columns_on_the_query":0.003,"P\\Tests\\FieldsTest::__pest_evaluable_it_replaces_selected_string_columns_on_the_query":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_specific_array_columns":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_specific_string_columns":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_wont_fetch_a_specific_array_column_if_its_not_allowed":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_wont_fetch_a_specific_string_column_if_its_not_allowed":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_sketchy_array_columns_if_they_are_allowed_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_sketchy_string_columns_if_they_are_allowed_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_guards_against_not_allowed_array_fields":0.004,"P\\Tests\\FieldsTest::__pest_evaluable_it_guards_against_not_allowed_string_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_guards_against_not_allowed_array_fields_from_an_included_resource":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_guards_against_not_allowed_string_fields_from_an_included_resource":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_only_requested_array_columns_from_an_included_model":0.017,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_only_requested_string_columns_from_an_included_model":0.004,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_requested_array_columns_from_included_models_up_to_two_levels_deep":0.006,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_fetch_requested_string_columns_from_included_models_up_to_two_levels_deep":0.003,"P\\Tests\\FieldsTest::__pest_evaluable_it_throws_an_exception_when_calling_allowed_includes_before_allowed_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_throws_an_exception_when_calling_allowed_includes_before_allowed_fields_but_with_requested_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_throws_an_exception_when_requesting_fields_for_an_allowed_included_without_any_allowed_fields":0.002,"P\\Tests\\FieldsTest::__pest_evaluable_it_can_allow_specific_fields_on_an_included_model":0.003,"P\\Tests\\FieldsTest::__pest_evaluable_it_wont_use_sketchy_field_requests":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_models_by_partial_property_by_default":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_models_by_an_array_as_filter_value":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_partially_and_case_insensitive":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_based_on_the_partial_existence_of_a_property_in_an_array":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_models_and_return_an_empty_collection":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_a_custom_base_query_with_select":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_specifies_escape_character_in_supported_databases#('sqlite')":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_specifies_escape_character_in_supported_databases#('mysql')":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_specifies_escape_character_in_supported_databases#('pgsql')":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_specifies_escape_character_in_supported_databases#('sqlsrv')":0.004,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_based_on_the_existence_of_a_property_in_an_array":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_ignores_empty_values_in_an_array_partial_filter":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_ignores_an_empty_array_partial_filter":0.002,"P\\Tests\\FilterTest::__pest_evaluable_falsy_values_are_not_ignored_when_applying_a_partial_filter":0.003,"P\\Tests\\FilterTest::__pest_evaluable_falsy_values_are_not_ignored_when_applying_a_begins_with_strict_filter":0.002,"P\\Tests\\FilterTest::__pest_evaluable_falsy_values_are_not_ignored_when_applying_a_ends_with_strict_filter":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_partial_using_begins_with_strict":0.004,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_partial_using_ends_with_strict":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_and_match_results_by_exact_property":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_and_reject_results_by_exact_property":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_scope":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_nested_relation_scope":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_type_hinted_scope":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_regular_and_type_hinted_scope":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_scope_with_multiple_parameters":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_scope_with_multiple_parameters_in_an_associative_array":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_results_by_a_custom_filter_class":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_allow_multiple_filters":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_allow_multiple_filters_as_an_array":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_by_multiple_filters":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_guards_against_invalid_filters":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_does_not_throw_invalid_filter_exception_when_disable_in_config":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_can_create_a_custom_filter_with_an_instantiated_filter":0.003,"P\\Tests\\FilterTest::__pest_evaluable_an_invalid_filter_query_exception_contains_the_unknown_and_allowed_filters":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_allows_for_adding_ignorable_values":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_should_not_apply_a_filter_if_the_supplied_value_is_ignored":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_the_filter_on_the_subset_of_allowed_values":0.007,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_the_filter_on_the_subset_of_allowed_values_regardless_of_the_keys_order":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_can_take_an_argument_for_custom_column_name_resolution":0.008,"P\\Tests\\FilterTest::__pest_evaluable_it_sets_property_column_name_to_property_name_by_default":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_resolves_queries_using_property_column_name":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_can_filter_using_boolean_flags":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_a_default_filter_value_if_nothing_in_request":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_does_not_apply_default_filter_when_filter_exists_and_default_is_set":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_a_null_default_filter_value_if_nothing_in_request":0.003,"P\\Tests\\FilterTest::__pest_evaluable_it_does_not_apply_default_filter_when_filter_exists_and_default_null_is_set":0.006,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_a_nullable_filter_when_filter_exists_and_is_null":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_a_nullable_filter_when_filter_exists_and_is_set":0.013,"P\\Tests\\FilterTest::__pest_evaluable_it_should_filter_by_query_parameters_if_a_default_value_is_set_and_unset_afterwards":0.007,"P\\Tests\\FilterTest::__pest_evaluable_it_should_not_filter_at_all_if_a_default_value_is_set_and_unset_afterwards":0.004,"P\\Tests\\FilterTest::__pest_evaluable_it_should_apply_a_filter_with_a_multi_dimensional_array_value":0.005,"P\\Tests\\FilterTest::__pest_evaluable_it_can_override_the_array_value_delimiter_for_single_filters":0.005,"P\\Tests\\FiltersCallbackTest::__pest_evaluable_it_should_filter_by_closure":0.003,"P\\Tests\\FiltersCallbackTest::__pest_evaluable_it_should_filter_by_array_callback":0.018,"P\\Tests\\FiltersTrashedTest::__pest_evaluable_it_should_filter_not_trashed_by_default":0.002,"P\\Tests\\FiltersTrashedTest::__pest_evaluable_it_can_filter_only_trashed":0.002,"P\\Tests\\FiltersTrashedTest::__pest_evaluable_it_can_filter_only_trashed_by_scope_directly":0.002,"P\\Tests\\FiltersTrashedTest::__pest_evaluable_it_can_filter_with_trashed":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_does_not_require_includes":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_handle_empty_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_model_relations":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_model_relations_by_alias":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_an_includes_callback":0.004,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_an_includes_count":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_allowing_an_include_also_allows_the_include_count":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_an_includes_exists":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_allowing_an_include_also_allows_the_include_exists":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_nested_model_relations":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_nested_model_relations_by_alias":0.005,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_model_relations_from_nested_model_relations":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_allowing_a_nested_include_only_allows_the_include_count_for_the_first_level":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_allowing_a_nested_include_only_allows_the_include_exists_for_the_first_level":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_morph_model_relations":0.005,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_reverse_morph_model_relations":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_camel_case_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_models_on_an_empty_collection":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_guards_against_invalid_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_does_not_throw_invalid_include_query_exception_when_disable_in_config":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_allow_multiple_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_allow_multiple_includes_as_an_array":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_remove_duplicate_includes_from_nested_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_multiple_model_relations":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_query_included_many_to_many_relationships":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_returns_correct_id_when_including_many_to_many_relationship":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_an_invalid_include_query_exception_contains_the_unknown_and_allowed_includes":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_alias_multiple_allowed_includes":0.003,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_custom_include_class":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_custom_include_class_by_alias":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_take_an_argument_for_custom_column_name_resolution":0.002,"P\\Tests\\IncludeTest::__pest_evaluable_it_can_include_a_custom_base_query_with_select":0.002,"P\\Tests\\QueryBuilderEndpointTest::__pest_evaluable_it_can_instantiate_the_query_builder_and_filter_the_query_for_an_actual_api_request":0.015,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_filter_nested_arrays":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_empty_filters_recursively":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_map_true_and_false_as_booleans_recursively":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_sort_query_param_from_the_request":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_sort_query_param_from_the_request_body":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_different_sort_query_parameter_name":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_return_an_empty_collection_when_no_sort_query_param_is_specified":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_multiple_sort_parameters_from_the_request":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_return_an_empty_collection_when_no_sort_query_params_are_specified":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_filter_query_params_from_the_request":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_filter_query_params_from_the_request_body":0.01,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_use_different_filter_query_parameter_name":0.003,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_use_null_as_the_filter_query_parameter_name":0.004,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_empty_filters":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_return_an_empty_collection_when_no_filter_query_params_are_specified":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_map_true_and_false_as_booleans_when_given_in_a_filter_query_string":0.005,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_map_comma_separated_values_as_arrays_when_given_in_a_filter_query_string":0.004,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_map_array_in_filter_recursively_when_given_in_a_filter_query_string":0.003,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_map_comma_separated_values_as_arrays_when_given_in_a_filter_query_string_and_get_those_by_key":0.006,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_include_query_params_from_the_request":0.009,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_include_from_the_request_body":0.004,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_different_include_query_parameter_name":0.005,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_return_an_empty_collection_when_no_include_query_params_are_specified":0.004,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_requested_fields":0.005,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_requested_fields_without_a_table_name":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_nested_fields":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_nested_fields_from_a_string_fields_request":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_requested_fields_from_the_request_body":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_different_fields_parameter_name":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_append_query_params_from_the_request":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_different_append_query_parameter_name":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_will_return_an_empty_collection_when_no_append_query_params_are_specified":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_can_get_the_append_query_params_from_the_request_body":0.002,"P\\Tests\\QueryBuilderRequestTest::__pest_evaluable_it_takes_custom_delimiters_for_splitting_request_parameters":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_be_given_an_eloquent_query_using_where":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_be_given_an_eloquent_query_using_select":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_be_given_a_belongs_to_many_relation_query":0.005,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_be_given_a_belongs_to_many_relation_query_with_pivot":0.003,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_be_given_a_model_class_name":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_not_be_given_a_string_that_is_not_a_class_name":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_not_be_given_an_object_that_is_neither_relation_nor_eloquent_builder":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_will_determine_the_request_when_its_not_given":0.004,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_query_soft_deletes":0.006,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_query_global_scopes":0.004,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_keeps_eager_loaded_relationships_from_the_base_query":0.004,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_keeps_local_macros_added_to_the_base_query":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_keeps_the_on_delete_callback_added_to_the_base_query":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_query_local_scopes":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_executes_the_same_query_regardless_of_the_order_of_applied_filters_or_sorts":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_can_filter_when_sorting_by_joining_a_related_model_which_contains_the_same_field_name":0.003,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_queries_the_correct_data_for_a_relationship_query":0.004,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_does_not_lose_pivot_values_with_belongs_to_many_relation":0.005,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_clones_the_subject_upon_cloning":0.002,"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_supports_clone_as_method":0.002,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_related_model_property":0.002,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_results_based_on_the_partial_existence_of_a_property_in_an_array":0.003,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_models_and_return_an_empty_collection":0.003,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_related_nested_model_property":0.003,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_related_model_and_related_nested_model_property":0.002,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_results_based_on_the_existence_of_a_property_in_an_array":0.004,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_filter_and_reject_results_by_exact_property":0.003,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_disable_exact_filtering_based_on_related_model_properties":0.002,"P\\Tests\\RelationFilterTest::__pest_evaluable_it_can_disable_partial_filtering_based_on_related_model_properties":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_query_ascending":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_has_the_allowed_sorts_property_set_even_if_no_sorts_are_requested":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_query_descending":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_query_by_alias":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_wont_sort_by_columns_that_werent_allowed_first":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_allow_a_descending_sort_by_still_sort_ascending":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_query_by_a_related_property":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_by_json_property_if_its_an_allowed_sort":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_by_sketchy_alias_if_its_an_allowed_sort":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_query_with_custom_select":0.009,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_a_chunk_query":0.007,"P\\Tests\\SortTest::__pest_evaluable_it_can_guard_against_sorts_that_are_not_allowed":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_will_throw_an_exception_if_a_sort_property_is_not_allowed":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_does_not_throw_invalid_sort_query_exception_when_disable_in_config":0.002,"P\\Tests\\SortTest::__pest_evaluable_an_invalid_sort_query_exception_contains_the_unknown_and_allowed_sorts":0.004,"P\\Tests\\SortTest::__pest_evaluable_it_wont_sort_if_no_sort_query_parameter_is_given":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_wont_sort_sketchy_sort_requests":0.005,"P\\Tests\\SortTest::__pest_evaluable_it_uses_default_sort_parameter_when_no_sort_was_requested":0.005,"P\\Tests\\SortTest::__pest_evaluable_it_doesnt_use_the_default_sort_parameter_when_a_sort_was_requested":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_allows_default_custom_sort_class_parameter":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_uses_default_descending_sort_parameter":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_allows_multiple_default_sort_parameters":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_allows_multiple_default_sort_parameters_in_an_array":0.005,"P\\Tests\\SortTest::__pest_evaluable_it_can_allow_multiple_sort_parameters":0.005,"P\\Tests\\SortTest::__pest_evaluable_it_can_allow_multiple_sort_parameters_as_an_array":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_by_multiple_columns":0.008,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_by_a_custom_sort_class":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_can_take_an_argument_for_custom_column_name_resolution":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_sets_property_column_name_to_property_name_by_default":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_resolves_queries_using_property_column_name":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_descending_with_an_alias":0.003,"P\\Tests\\SortTest::__pest_evaluable_it_does_not_add_sort_clauses_multiple_times":0.003,"P\\Tests\\SortTest::__pest_evaluable_given_a_default_sort_a_sort_alias_will_still_be_resolved":0.002,"P\\Tests\\SortTest::__pest_evaluable_late_specified_sorts_still_check_for_allowance":0.004,"P\\Tests\\SortTest::__pest_evaluable_it_can_sort_and_use_scoped_filters_at_the_same_time":0.002,"P\\Tests\\SortTest::__pest_evaluable_it_ignores_non_existing_sorts_before_adding_them_as_an_alias":0.002,"P\\Tests\\SortTest::__pest_evaluable_raw_sorts_do_not_get_purged_when_specifying_allowed_sorts":0.002,"P\\Tests\\SortTest::__pest_evaluable_the_default_direction_of_an_allow_sort_can_be_set":0.004,"P\\Tests\\SortsCallbackTest::__pest_evaluable_it_should_sort_by_closure":0.003,"P\\Tests\\SortsCallbackTest::__pest_evaluable_it_should_sort_by_array_callback":0.002,"P\\Tests\\FilterTest::__pest_evaluable_it_use_a_custom_filter_query_string_parameter":0.007,"P\\Tests\\FilterTest::__pest_evaluable_it_can_work_without_a_general_filter_query_string_parameter_configured":0.007}} \ No newline at end of file +{"version":"pest_2.35.1","defects":{"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_has_the_right_phpstan_type":8},"times":{"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_has_the_right_phpstan_type":0.027}} \ No newline at end of file diff --git a/composer.json b/composer.json index b99787e1..03edeae6 100644 --- a/composer.json +++ b/composer.json @@ -28,11 +28,11 @@ }, "require-dev": { "ext-json": "*", + "larastan/larastan": "^2.9", "mockery/mockery": "^1.4", - "nunomaduro/larastan": "^2.0", "orchestra/testbench": "^7.0|^8.0", - "phpunit/phpunit": "^10.0", "pestphp/pest": "^2.0", + "phpunit/phpunit": "^10.0", "spatie/invade": "^2.0" }, "autoload": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index c590d9e3..6dcbbdec 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ includes: - - ./vendor/nunomaduro/larastan/extension.neon + - ./vendor/larastan/larastan/extension.neon - phpstan-baseline.neon parameters: @@ -14,11 +14,9 @@ parameters: checkModelProperties: true checkOctaneCompatibility: true - checkMissingIterableValueType: false reportUnmatchedIgnoredErrors: false noUnnecessaryCollectionCall: true checkNullables: true - checkGenericClassInNonGenericObjectType: false treatPhpDocTypesAsCertain: false ignoreErrors: From 8012da9d654d5ccec6e85a6490943601549b71ff Mon Sep 17 00:00:00 2001 From: Gustav Gullstrand Date: Fri, 18 Oct 2024 17:03:31 +0200 Subject: [PATCH 19/51] Update filtering.md to clarify handling of array scope parameter (#976) Problem discussed here https://github.com/spatie/laravel-query-builder/issues/334 --- docs/features/filtering.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/features/filtering.md b/docs/features/filtering.md index e51e57fe..fd34edf3 100644 --- a/docs/features/filtering.md +++ b/docs/features/filtering.md @@ -171,6 +171,14 @@ You can even pass multiple parameters to the scope by passing a comma separated GET /events?filter[schedule.starts_between]=2018-01-01,2018-12-31 ``` +When passing an array as a parameter you can access it, as an array, in the scope by using the spread operator. +```php +public function scopeInvitedUsers(Builder $query, ...$users): Builder +{ + return $query->whereIn('id', $users); +} +``` + When using scopes that require model instances in the parameters, we'll automatically try to inject the model instances into your scope. This works the same way as route model binding does for injecting Eloquent models into controllers. For example: ```php From 59e3157caed9229e3259fb8f3e366800d4040a6b Mon Sep 17 00:00:00 2001 From: Arushad Ahmed Date: Sat, 19 Oct 2024 13:31:42 +0500 Subject: [PATCH 20/51] Fixed IncludedCount.php Fixed bug where the include count fails if a relation has the word `Count` in it. For example, it fails for `billingCountry` by removing the word `Country` and trying to load a `billing` relation. --- src/Includes/IncludedCount.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Includes/IncludedCount.php b/src/Includes/IncludedCount.php index 4e2c05d6..67f3eb1a 100644 --- a/src/Includes/IncludedCount.php +++ b/src/Includes/IncludedCount.php @@ -9,6 +9,9 @@ class IncludedCount implements IncludeInterface { public function __invoke(Builder $query, string $count) { - $query->withCount(Str::before($count, config('query-builder.count_suffix', 'Count'))); + $suffix = config('query-builder.count_suffix', 'Count'); + $relation = Str::endsWith($count, $suffix) ? Str::beforeLast($count, $suffix) : $count; + + $query->withCount($relation); } } From b1b6b2c208ec192a804e74f1593bf50f38d6349b Mon Sep 17 00:00:00 2001 From: tarexme Date: Tue, 12 Nov 2024 20:42:37 +0500 Subject: [PATCH 21/51] add .phpunit.cache to .gitignore --- .gitignore | 2 +- .phpunit.cache/test-results | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 .phpunit.cache/test-results diff --git a/.gitignore b/.gitignore index 7d3ee0ff..78fe5956 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ composer.lock vendor .php_cs.cache coverage +.phpunit.cache .phpunit.result.cache /.idea .php-cs-fixer.cache - diff --git a/.phpunit.cache/test-results b/.phpunit.cache/test-results deleted file mode 100644 index 452701fe..00000000 --- a/.phpunit.cache/test-results +++ /dev/null @@ -1 +0,0 @@ -{"version":"pest_2.35.1","defects":{"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_has_the_right_phpstan_type":8},"times":{"P\\Tests\\QueryBuilderTest::__pest_evaluable_it_has_the_right_phpstan_type":0.027}} \ No newline at end of file From 47d86e2104cefcbe41face1a9549b4d1e940b348 Mon Sep 17 00:00:00 2001 From: "RG." Date: Fri, 6 Dec 2024 11:31:32 +0200 Subject: [PATCH 22/51] Fix typo in filtering.md --- docs/features/filtering.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/filtering.md b/docs/features/filtering.md index fd34edf3..838f627b 100644 --- a/docs/features/filtering.md +++ b/docs/features/filtering.md @@ -94,7 +94,7 @@ Operator filters allow you to filter results based on different operators such a use Spatie\QueryBuilder\AllowedFilter; use Spatie\QueryBuilder\Enums\FilterOperator; -// GET /users?filter[salary]=>3000 +// GET /users?filter[salary]=3000 $users = QueryBuilder::for(User::class) ->allowedFilters([ AllowedFilter::operator('salary', FilterOperator::GREATER_THAN), From d12e6b7d372ee4be6e4d2c066715433ed3898744 Mon Sep 17 00:00:00 2001 From: freekmurze Date: Wed, 11 Dec 2024 09:55:50 +0000 Subject: [PATCH 23/51] Update CHANGELOG --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f4b4da1..3d38c69f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to `laravel-query-builder` will be documented in this file +## 6.2.2 - 2024-12-11 + +### What's Changed + +* Update filtering.md to clarify handling of array scope parameter by @g-gullstrand in https://github.com/spatie/laravel-query-builder/pull/976 +* Remove PHPUnit cache by @tarexme in https://github.com/spatie/laravel-query-builder/pull/982 +* Fix typo in filtering.md by @yngc0der in https://github.com/spatie/laravel-query-builder/pull/984 +* Fixed IncludedCount.php by @dash8x in https://github.com/spatie/laravel-query-builder/pull/978 + +### New Contributors + +* @g-gullstrand made their first contribution in https://github.com/spatie/laravel-query-builder/pull/976 +* @tarexme made their first contribution in https://github.com/spatie/laravel-query-builder/pull/982 +* @yngc0der made their first contribution in https://github.com/spatie/laravel-query-builder/pull/984 +* @dash8x made their first contribution in https://github.com/spatie/laravel-query-builder/pull/978 + +**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/6.2.1...6.2.2 + ## 6.2.1 - 2024-10-03 ### What's Changed From e5af869c7d20e65dfe89bc4fb43eea00fdbf8b6e Mon Sep 17 00:00:00 2001 From: Rasmus Christoffer Nielsen Date: Mon, 23 Dec 2024 08:48:08 +0100 Subject: [PATCH 24/51] Fix selecting fields on belongs to many relations --- src/Includes/IncludedRelationship.php | 2 +- tests/FieldsTest.php | 28 ++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Includes/IncludedRelationship.php b/src/Includes/IncludedRelationship.php index 0df9e279..59e39927 100644 --- a/src/Includes/IncludedRelationship.php +++ b/src/Includes/IncludedRelationship.php @@ -28,7 +28,7 @@ public function __invoke(Builder $query, string $relationship) } return [$fullRelationName => function ($query) use ($fields) { - $query->select($fields); + $query->select($query->qualifyColumns($fields)); }]; }) ->toArray(); diff --git a/tests/FieldsTest.php b/tests/FieldsTest.php index af5c12f1..0f46d822 100644 --- a/tests/FieldsTest.php +++ b/tests/FieldsTest.php @@ -174,7 +174,7 @@ $queryBuilder->first()->relatedModels; $this->assertQueryLogContains('select `test_models`.`id` from `test_models`'); - $this->assertQueryLogContains('select `name` from `related_models`'); + $this->assertQueryLogContains('select `related_models`.`name` from `related_models`'); }); it('can fetch only requested string columns from an included model', function () { @@ -197,7 +197,29 @@ $queryBuilder->first()->relatedModels; $this->assertQueryLogContains('select `test_models`.`id` from `test_models`'); - $this->assertQueryLogContains('select `name` from `related_models`'); + $this->assertQueryLogContains('select `related_models`.`name` from `related_models`'); +}); + +it('can fetch only requested string columns from an included belongs to many model', function () { + TestModel::first()->relatedThroughPivotModels()->create([ + 'name' => 'related', + ]); + + $request = new Request([ + 'fields' => 'id,related_through_pivot_models.id,related_through_pivot_models.name', + 'include' => ['relatedThroughPivotModels'], + ]); + + $queryBuilder = QueryBuilder::for(TestModel::class, $request) + ->allowedFields('id', 'related_through_pivot_models.id', 'related_through_pivot_models.name') + ->allowedIncludes('relatedThroughPivotModels'); + + DB::enableQueryLog(); + + $queryBuilder->first()->relatedThroughPivotModels; + + $this->assertQueryLogContains('select `test_models`.`id` from `test_models`'); + $this->assertQueryLogContains('select `related_through_pivot_models`.`id`, `related_through_pivot_models`.`name`, `pivot_models`.`test_model_id` as `pivot_test_model_id`, `pivot_models`.`related_through_pivot_model_id` as `pivot_related_through_pivot_model_id` from `related_through_pivot_models` inner join `pivot_models` on `related_through_pivot_models`.`id` = `pivot_models`.`related_through_pivot_model_id` where `pivot_models`.`test_model_id` in ('); }); it('can fetch requested array columns from included models up to two levels deep', function () { @@ -299,7 +321,7 @@ $queryBuilder->first()->relatedModels; $this->assertQueryLogContains('select * from `test_models`'); - $this->assertQueryLogContains('select `id`, `name` from `related_models`'); + $this->assertQueryLogContains('select `related_models`.`id`, `related_models`.`name` from `related_models`'); }); it('wont use sketchy field requests', function () { From db9fb455f5833e62789f71b04e47f06358c6c6f7 Mon Sep 17 00:00:00 2001 From: freekmurze Date: Mon, 23 Dec 2024 09:52:07 +0000 Subject: [PATCH 25/51] Update CHANGELOG --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d38c69f..f0e3b9d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to `laravel-query-builder` will be documented in this file +## 6.2.3 - 2024-12-23 + +### What's Changed + +* Fix selecting fields on belongs to many relations by @rasmuscnielsen in https://github.com/spatie/laravel-query-builder/pull/986 + +### New Contributors + +* @rasmuscnielsen made their first contribution in https://github.com/spatie/laravel-query-builder/pull/986 + +**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/6.2.2...6.2.3 + ## 6.2.2 - 2024-12-11 ### What's Changed From 0348ab07383dfbb6835d9c5496fcbcc087fb25b1 Mon Sep 17 00:00:00 2001 From: Gerardo Ibarra Date: Mon, 23 Dec 2024 18:44:23 -0300 Subject: [PATCH 26/51] New Filter belongs to (#975) * new filter BelongsTo * test new filter BelongsTo * wip doc new filter BelongsTo * Fix styling * change RelationNotFoundException instead of InvalidFilterProperty, add tests --------- Co-authored-by: gpibarra --- docs/features/filtering.md | 49 +++++++++++++++++++ src/AllowedFilter.php | 8 +++ src/Filters/FiltersBelongsTo.php | 83 ++++++++++++++++++++++++++++++++ tests/FilterTest.php | 83 ++++++++++++++++++++++++++++++++ 4 files changed, 223 insertions(+) create mode 100644 src/Filters/FiltersBelongsTo.php diff --git a/docs/features/filtering.md b/docs/features/filtering.md index 838f627b..f2a7e0e5 100644 --- a/docs/features/filtering.md +++ b/docs/features/filtering.md @@ -134,6 +134,55 @@ QueryBuilder::for(User::class) ->allowedFilters(AllowedFilter::exact('posts.title', null, $addRelationConstraint)); ``` +## BelongsTo filters + +In Model: +```php +class Comment extends Model +{ + public function post(): BelongsTo + { + return $this->belongsTo(Post::class); + } +} +``` + +```php +QueryBuilder::for(Comment::class) + ->allowedFilters([ + AllowedFilter::belongsTo('post'), + ]) + ->get(); +``` + +Alias +```php +QueryBuilder::for(Comment::class) + ->allowedFilters([ + AllowedFilter::belongsTo('post_id', 'post'), + ]) + ->get(); +``` + +Nested +```php +class Post extends Model +{ + public function author(): BelongsTo + { + return $this->belongsTo(User::class); + } +} +``` + +```php +QueryBuilder::for(Comment::class) + ->allowedFilters([ + AllowedFilter::belongsTo('author_post_id', 'post.author'), + ]) + ->get(); +``` + ## Scope filters Sometimes more advanced filtering options are necessary. This is where scope filters, callback filters and custom filters come in handy. diff --git a/src/AllowedFilter.php b/src/AllowedFilter.php index b4d2b63c..fcdbd106 100644 --- a/src/AllowedFilter.php +++ b/src/AllowedFilter.php @@ -6,6 +6,7 @@ use Spatie\QueryBuilder\Enums\FilterOperator; use Spatie\QueryBuilder\Filters\Filter; use Spatie\QueryBuilder\Filters\FiltersBeginsWithStrict; +use Spatie\QueryBuilder\Filters\FiltersBelongsTo; use Spatie\QueryBuilder\Filters\FiltersCallback; use Spatie\QueryBuilder\Filters\FiltersEndsWithStrict; use Spatie\QueryBuilder\Filters\FiltersExact; @@ -82,6 +83,13 @@ public static function endsWithStrict(string $name, $internalName = null, bool $ return new static($name, new FiltersEndsWithStrict($addRelationConstraint), $internalName); } + public static function belongsTo(string $name, $internalName = null, string $arrayValueDelimiter = null): static + { + static::setFilterArrayValueDelimiter($arrayValueDelimiter); + + return new static($name, new FiltersBelongsTo(), $internalName); + } + public static function scope(string $name, $internalName = null, string $arrayValueDelimiter = null): static { static::setFilterArrayValueDelimiter($arrayValueDelimiter); diff --git a/src/Filters/FiltersBelongsTo.php b/src/Filters/FiltersBelongsTo.php new file mode 100644 index 00000000..d3ef1d3d --- /dev/null +++ b/src/Filters/FiltersBelongsTo.php @@ -0,0 +1,83 @@ + + */ +class FiltersBelongsTo implements Filter +{ + /** {@inheritdoc} */ + public function __invoke(Builder $query, $value, string $property) + { + $values = array_values(Arr::wrap($value)); + + $propertyParts = collect(explode('.', $property)); + $relation = $propertyParts->pop(); + $relationParent = $propertyParts->implode('.'); + $relatedModel = $this->getRelatedModel($query->getModel(), $relation, $relationParent); + + $relatedCollection = $relatedModel->newCollection(); + array_walk($values, fn ($v) => $relatedCollection->add( + tap($relatedModel->newInstance(), fn ($m) => $m->setAttribute($m->getKeyName(), $v)) + )); + + if ($relatedCollection->isEmpty()) { + return $query; + } + + if ($relationParent) { + $query->whereHas($relationParent, fn (Builder $q) => $q->whereBelongsTo($relatedCollection, $relation)); + } else { + $query->whereBelongsTo($relatedCollection, $relation); + } + } + + protected function getRelatedModel(Model $modelQuery, string $relationName, string $relationParent): Model + { + if ($relationParent) { + $modelParent = $this->getModelFromRelation($modelQuery, $relationParent); + } else { + $modelParent = $modelQuery; + } + + $relatedModel = $this->getRelatedModelFromRelation($modelParent, $relationName); + + return $relatedModel; + } + + protected function getRelatedModelFromRelation(Model $model, string $relationName): ?Model + { + $relationObject = $model->$relationName(); + if (! is_subclass_of($relationObject, Relation::class)) { + throw RelationNotFoundException::make($model, $relationName); + } + + $relatedModel = $relationObject->getRelated(); + + return $relatedModel; + } + + protected function getModelFromRelation(Model $model, string $relation, int $level = 0): ?Model + { + $relationParts = explode('.', $relation); + if (count($relationParts) == 1) { + return $this->getRelatedModelFromRelation($model, $relation); + } else { + $firstRelation = $relationParts[0]; + $firstRelatedModel = $this->getRelatedModelFromRelation($model, $firstRelation); + if (! $firstRelatedModel) { + return null; + } + + return $this->getModelFromRelation($firstRelatedModel, implode('.', array_slice($relationParts, 1)), $level + 1); + } + } +} diff --git a/tests/FilterTest.php b/tests/FilterTest.php index 5e724565..371a1f2c 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -15,6 +15,8 @@ use Spatie\QueryBuilder\Filters\Filter as FilterInterface; use Spatie\QueryBuilder\Filters\FiltersExact; use Spatie\QueryBuilder\QueryBuilder; +use Spatie\QueryBuilder\Tests\TestClasses\Models\NestedRelatedModel; +use Spatie\QueryBuilder\Tests\TestClasses\Models\RelatedModel; use Spatie\QueryBuilder\Tests\TestClasses\Models\TestModel; beforeEach(function () { @@ -283,6 +285,87 @@ expect($modelsResult)->toHaveCount(0); }); +it('can filter results by belongs to', function () { + $relatedModel = RelatedModel::create(['name' => 'John Related Doe', 'test_model_id' => 0]); + $nestedModel = NestedRelatedModel::create(['name' => 'John Nested Doe', 'related_model_id' => $relatedModel->id]); + + $modelsResult = createQueryFromFilterRequest(['relatedModel' => $relatedModel->id], NestedRelatedModel::class) + ->allowedFilters(AllowedFilter::belongsTo('relatedModel')) + ->get(); + + expect($modelsResult)->toHaveCount(1); +}); + +it('can filter results by belongs to no match', function () { + $relatedModel = RelatedModel::create(['name' => 'John Related Doe', 'test_model_id' => 0]); + $nestedModel = NestedRelatedModel::create(['name' => 'John Nested Doe', 'related_model_id' => $relatedModel->id + 1]); + + $modelsResult = createQueryFromFilterRequest(['relatedModel' => $relatedModel->id], NestedRelatedModel::class) + ->allowedFilters(AllowedFilter::belongsTo('relatedModel')) + ->get(); + + expect($modelsResult)->toHaveCount(0); +}); + +it('can filter results by belongs multiple', function () { + $relatedModel1 = RelatedModel::create(['name' => 'John Related Doe 1', 'test_model_id' => 0]); + $nestedModel1 = NestedRelatedModel::create(['name' => 'John Nested Doe 1', 'related_model_id' => $relatedModel1->id]); + $relatedModel2 = RelatedModel::create(['name' => 'John Related Doe 2', 'test_model_id' => 0]); + $nestedModel2 = NestedRelatedModel::create(['name' => 'John Nested Doe 2', 'related_model_id' => $relatedModel2->id]); + + $modelsResult = createQueryFromFilterRequest(['relatedModel' => $relatedModel1->id.','.$relatedModel2->id], NestedRelatedModel::class) + ->allowedFilters(AllowedFilter::belongsTo('relatedModel')) + ->get(); + + expect($modelsResult)->toHaveCount(2); +}); + +it('can filter results by belongs multiple with different internal name', function () { + $relatedModel1 = RelatedModel::create(['name' => 'John Related Doe 1', 'test_model_id' => 0]); + $nestedModel1 = NestedRelatedModel::create(['name' => 'John Nested Doe 1', 'related_model_id' => $relatedModel1->id]); + $relatedModel2 = RelatedModel::create(['name' => 'John Related Doe 2', 'test_model_id' => 0]); + $nestedModel2 = NestedRelatedModel::create(['name' => 'John Nested Doe 2', 'related_model_id' => $relatedModel2->id]); + + $modelsResult = createQueryFromFilterRequest(['testFilter' => $relatedModel1->id.','.$relatedModel2->id], NestedRelatedModel::class) + ->allowedFilters(AllowedFilter::belongsTo('testFilter', 'relatedModel')) + ->get(); + + expect($modelsResult)->toHaveCount(2); +}); + +it('can filter results by belongs multiple with different internal name and nested model', function () { + $testModel1 = TestModel::create(['name' => 'John Test Doe 1']); + $relatedModel1 = RelatedModel::create(['name' => 'John Related Doe 1', 'test_model_id' => $testModel1->id]); + $nestedModel1 = NestedRelatedModel::create(['name' => 'John Nested Doe 1', 'related_model_id' => $relatedModel1->id]); + $testModel2 = TestModel::create(['name' => 'John Test Doe 2']); + $relatedModel2 = RelatedModel::create(['name' => 'John Related Doe 2', 'test_model_id' => $testModel2->id]); + $nestedModel2 = NestedRelatedModel::create(['name' => 'John Nested Doe 2', 'related_model_id' => $relatedModel2->id]); + + $modelsResult = createQueryFromFilterRequest(['test_filter' => $testModel1->id.','.$testModel2->id], NestedRelatedModel::class) + ->allowedFilters(AllowedFilter::belongsTo('test_filter', 'relatedModel.testModel')) + ->get(); + + expect($modelsResult)->toHaveCount(2); +}); + +it('throws an exception when trying to filter by belongs to with an inexistent relation', function ($relationName, $exceptionClass) { + $this->expectException($exceptionClass); + + $modelsResult = createQueryFromFilterRequest(['test_filter' => 1], RelatedModel::class) + ->allowedFilters(AllowedFilter::belongsTo('test_filter', $relationName)) + ->get(); + +})->with([ + ['inexistentRelation', \BadMethodCallException::class], + ['testModel.inexistentRelation', \BadMethodCallException::class], // existing 'testModel' belongsTo relation + ['inexistentRelation.inexistentRelation', \BadMethodCallException::class], + ['getTable', \Illuminate\Database\Eloquent\RelationNotFoundException::class], + ['testModel.getTable', \Illuminate\Database\Eloquent\RelationNotFoundException::class], // existing 'testModel' belongsTo relation + ['getTable.getTable', \Illuminate\Database\Eloquent\RelationNotFoundException::class], + ['nestedRelatedModels', \Illuminate\Database\Eloquent\RelationNotFoundException::class], // existing 'nestedRelatedModels' relation but not a belongsTo relation + ['testModel.relatedModels', \Illuminate\Database\Eloquent\RelationNotFoundException::class], // existing 'testModel' belongsTo relation and existing 'relatedModels' relation but not a belongsTo relation +]); + it('can filter results by scope', function () { $testModel = TestModel::create(['name' => 'John Testing Doe']); From de6a10d444cac71dea5bc44c2bbbb2f95807fb62 Mon Sep 17 00:00:00 2001 From: Alex Vanderbist Date: Mon, 23 Dec 2024 22:48:16 +0100 Subject: [PATCH 27/51] Update docs to v6 --- docs/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_index.md b/docs/_index.md index f17c1c36..2c3ffc19 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -1,5 +1,5 @@ --- -title: v5 +title: v6 slogan: Easily build Eloquent queries from API requests. githubUrl: https://github.com/spatie/laravel-query-builder branch: main From d0648825b66e5637854a6d848374cfc6523759be Mon Sep 17 00:00:00 2001 From: Alex Vanderbist Date: Mon, 23 Dec 2024 22:59:30 +0100 Subject: [PATCH 28/51] Tweak tests --- tests/FilterTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/FilterTest.php b/tests/FilterTest.php index 371a1f2c..75d5042a 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -681,6 +681,8 @@ public function __invoke(Builder $query, $value, string $property): Builder }); it('should apply a nullable filter when filter exists and is null', function () { + DB::enableQueryLog(); + TestModel::create(['name' => null]); TestModel::create(['name' => 'UniqueJohn Deer']); @@ -690,6 +692,7 @@ public function __invoke(Builder $query, $value, string $property): Builder ->allowedFilters(AllowedFilter::exact('name')->nullable()) ->get(); + $this->assertQueryLogContains("select * from `test_models` where `test_models`.`name` is null"); expect($models->count())->toEqual(1); }); From cf5350e26615dda0930bfd525f3dfd85f5b1c0e8 Mon Sep 17 00:00:00 2001 From: Alexandru Bucur Date: Mon, 23 Dec 2024 23:15:32 +0100 Subject: [PATCH 29/51] feat: implement a way of matching the fields to the jsonapi spec (#983) * feat: implement a way of matching the fields to the jsonapi spec * feat: introduce tests to cover the new jsonapi functionality * feat: introduce an easy way of running tests locally * feat: implement a way of matching the fields to the jsonapi spec * feat: introduce tests to cover the new jsonapi functionality * feat: introduce an easy way of running tests locally * fix: allow usage of the main table filtering the fields --- config/query-builder.php | 19 +++ database/factories/AppendModelFactory.php | 2 +- database/factories/TestModelFactory.php | 1 - src/Concerns/AddsFieldsToQuery.php | 50 +++++- src/Includes/IncludedRelationship.php | 21 ++- tests/FieldsTest.php | 120 ++++++++++++++ tests/TestCase.php | 4 +- tests/TestClasses/Models/TestModel.php | 13 ++ utils/run_tests.sh | 193 ++++++++++++++++++++++ 9 files changed, 409 insertions(+), 14 deletions(-) create mode 100755 utils/run_tests.sh diff --git a/config/query-builder.php b/config/query-builder.php index 36d3d9f5..94fca196 100644 --- a/config/query-builder.php +++ b/config/query-builder.php @@ -60,4 +60,23 @@ * GET /users?fields[userOwner]=id,name */ 'convert_relation_names_to_snake_case_plural' => true, + + /* + * By default, the package expects relationship names to be snake case plural when using fields[relationship]. + * For example, fetching the id and name for a userOwner relation would look like this: + * GET /users?fields[user_owner]=id,name + * + * Set this to one of `snake_case`, `camelCase` or `none` if you want to enable table name resolution in addition to the relation name resolution + * GET /users?include=topOrders&fields[orders]=id,name + */ + 'convert_relation_table_name_strategy' => false, + + /* + * By default, the package expects the field names to match the database names + * For example, fetching the field named firstName would look like this: + * GET /users?fields=firstName + * + * Set this to `true` if you want to convert the firstName into first_name for the underlying query + */ + 'convert_field_names_to_snake_case' => false, ]; diff --git a/database/factories/AppendModelFactory.php b/database/factories/AppendModelFactory.php index 3591b07e..26b38331 100644 --- a/database/factories/AppendModelFactory.php +++ b/database/factories/AppendModelFactory.php @@ -2,8 +2,8 @@ namespace Spatie\QueryBuilder\Database\Factories; -use Spatie\QueryBuilder\Tests\TestClasses\Models\AppendModel; use Illuminate\Database\Eloquent\Factories\Factory; +use Spatie\QueryBuilder\Tests\TestClasses\Models\AppendModel; class AppendModelFactory extends Factory { diff --git a/database/factories/TestModelFactory.php b/database/factories/TestModelFactory.php index 8b1001ad..8965abfb 100644 --- a/database/factories/TestModelFactory.php +++ b/database/factories/TestModelFactory.php @@ -16,4 +16,3 @@ public function definition() ]; } } - diff --git a/src/Concerns/AddsFieldsToQuery.php b/src/Concerns/AddsFieldsToQuery.php index 120c000e..905e1508 100644 --- a/src/Concerns/AddsFieldsToQuery.php +++ b/src/Concerns/AddsFieldsToQuery.php @@ -38,7 +38,16 @@ protected function addRequestedModelFieldsToQuery(): void $fields = $this->request->fields(); - $modelFields = $fields->has($modelTableName) ? $fields->get($modelTableName) : $fields->get('_'); + if (! $fields->isEmpty() && config('query-builder.convert_field_names_to_snake_case', false)) { + $fields = $fields->mapWithKeys(fn ($fields, $table) => [$table => collect($fields)->map(fn ($field) => Str::snake($field))->toArray()]); + } + + // Apply additional table name conversion based on strategy + if (config('query-builder.convert_relation_table_name_strategy', false) === 'camelCase') { + $modelFields = $fields->has(Str::camel($modelTableName)) ? $fields->get(Str::camel($modelTableName)) : $fields->get('_'); + } else { + $modelFields = $fields->has($modelTableName) ? $fields->get($modelTableName) : $fields->get('_'); + } if (empty($modelFields)) { return; @@ -49,23 +58,46 @@ protected function addRequestedModelFieldsToQuery(): void $this->select($prependedFields); } - public function getRequestedFieldsForRelatedTable(string $relation): array + public function getRequestedFieldsForRelatedTable(string $relation, ?string $tableName = null): array { - $tableOrRelation = config('query-builder.convert_relation_names_to_snake_case_plural', true) - ? Str::plural(Str::snake($relation)) - : $relation; + // Possible table names to check + $possibleRelatedNames = [ + // Preserve existing relation name conversion logic + config('query-builder.convert_relation_names_to_snake_case_plural', true) + ? Str::plural(Str::snake($relation)) + : $relation, + ]; + + $strategy = config('query-builder.convert_relation_table_name_strategy', false); + + // Apply additional table name conversion based on strategy + if ($strategy === 'snake_case' && $tableName) { + $possibleRelatedNames[] = Str::snake($tableName); + } elseif ($strategy === 'camelCase' && $tableName) { + $possibleRelatedNames[] = Str::camel($tableName); + } elseif ($strategy === 'none') { + $possibleRelatedNames = $tableName; + } + + // Remove any null values + $possibleRelatedNames = array_filter($possibleRelatedNames); $fields = $this->request->fields() - ->mapWithKeys(fn ($fields, $table) => [$table => $fields]) - ->get($tableOrRelation); + ->mapWithKeys(fn ($fields, $table) => [$table => collect($fields)->map(fn ($field) => config('query-builder.convert_field_names_to_snake_case', false) ? Str::snake($field) : $field)]) + ->filter(fn ($value, $table) => in_array($table, $possibleRelatedNames)) + ->first(); if (! $fields) { return []; } - if (! $this->allowedFields instanceof Collection) { - // We have requested fields but no allowed fields (yet?) + $fields = $fields->toArray(); + + if ($tableName !== null) { + $fields = $this->prependFieldsWithTableName($fields, $tableName); + } + if (! $this->allowedFields instanceof Collection) { throw new UnknownIncludedFieldsQuery($fields); } diff --git a/src/Includes/IncludedRelationship.php b/src/Includes/IncludedRelationship.php index 59e39927..9aca7d8d 100644 --- a/src/Includes/IncludedRelationship.php +++ b/src/Includes/IncludedRelationship.php @@ -3,6 +3,7 @@ namespace Spatie\QueryBuilder\Includes; use Closure; +use Exception; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; @@ -16,11 +17,27 @@ public function __invoke(Builder $query, string $relationship) $relatedTables = collect(explode('.', $relationship)); $withs = $relatedTables - ->mapWithKeys(function ($table, $key) use ($relatedTables) { + ->mapWithKeys(function ($table, $key) use ($relatedTables, $query) { $fullRelationName = $relatedTables->slice(0, $key + 1)->implode('.'); if ($this->getRequestedFieldsForRelatedTable) { - $fields = ($this->getRequestedFieldsForRelatedTable)($fullRelationName); + + $tableName = null; + $strategy = config('query-builder.convert_relation_table_name_strategy', false); + + if ($strategy !== false) { + // Try to resolve the related model's table name + try { + // Use the current query's model to resolve the relationship + $relatedModel = $query->getModel()->{$fullRelationName}()->getRelated(); + $tableName = $relatedModel->getTable(); + } catch (Exception $e) { + // If we can not figure out the table don't do anything + $tableName = null; + } + } + + $fields = ($this->getRequestedFieldsForRelatedTable)($fullRelationName, $tableName); } if (empty($fields)) { diff --git a/tests/FieldsTest.php b/tests/FieldsTest.php index 0f46d822..a8148b0a 100644 --- a/tests/FieldsTest.php +++ b/tests/FieldsTest.php @@ -83,6 +83,21 @@ expect($query)->toEqual($expected); }); +it('can fetch specific string columns jsonApi Format', function () { + config(['query-builder.convert_field_names_to_snake_case' => true]); + config(['query-builder.convert_relation_table_name_strategy' => 'camelCase']); + + $query = createQueryFromFieldRequest('firstName,id') + ->allowedFields(['firstName', 'id']) + ->toSql(); + + $expected = TestModel::query() + ->select("{$this->modelTableName}.first_name", "{$this->modelTableName}.id") + ->toSql(); + + expect($query)->toEqual($expected); +}); + it('wont fetch a specific array column if its not allowed', function () { $query = createQueryFromFieldRequest(['test_models' => 'random-column'])->toSql(); @@ -222,6 +237,81 @@ $this->assertQueryLogContains('select `related_through_pivot_models`.`id`, `related_through_pivot_models`.`name`, `pivot_models`.`test_model_id` as `pivot_test_model_id`, `pivot_models`.`related_through_pivot_model_id` as `pivot_related_through_pivot_model_id` from `related_through_pivot_models` inner join `pivot_models` on `related_through_pivot_models`.`id` = `pivot_models`.`related_through_pivot_model_id` where `pivot_models`.`test_model_id` in ('); }); +it('can fetch only requested string columns from an included model jsonApi format', function () { + config(['query-builder.convert_relation_table_name_strategy' => 'camelCase']); + RelatedModel::create([ + 'test_model_id' => $this->model->id, + 'name' => 'related', + ]); + + $request = new Request([ + 'fields' => 'id,relatedModels.name', + 'include' => ['relatedModels'], + ]); + + $queryBuilder = QueryBuilder::for(TestModel::class, $request) + ->allowedFields('relatedModels.name', 'id') + ->allowedIncludes('relatedModels'); + + DB::enableQueryLog(); + + $queryBuilder->first()->relatedModels; + + $this->assertQueryLogContains('select `test_models`.`id` from `test_models`'); + $this->assertQueryLogContains('select `related_models`.`name` from `related_models`'); +}); + +it('can fetch only requested string columns from an included model jsonApi format with field conversion', function () { + config(['query-builder.convert_field_names_to_snake_case' => true]); + config(['query-builder.convert_relation_table_name_strategy' => 'camelCase']); + + RelatedModel::create([ + 'test_model_id' => $this->model->id, + 'name' => 'related', + ]); + + $request = new Request([ + 'fields' => 'id,relatedModels.fullName', + 'include' => ['relatedModels'], + ]); + + $queryBuilder = QueryBuilder::for(TestModel::class, $request) + ->allowedFields('relatedModels.fullName', 'id') + ->allowedIncludes('relatedModels'); + + DB::enableQueryLog(); + + $queryBuilder->first()->relatedModels; + + $this->assertQueryLogContains('select `test_models`.`id` from `test_models`'); + $this->assertQueryLogContains('select `related_models`.`full_name` from `related_models`'); +}); + +it('can fetch only requested string columns from an included model through pivot jsonApi format', function () { + config(['query-builder.convert_relation_table_name_strategy' => 'camelCase']); + + $this->model->relatedThroughPivotModels()->create([ + 'id' => $this->model->id + 1, + 'name' => 'Test', + ]); + + $request = new Request([ + 'fields' => 'id,relatedThroughPivotModels.name', + 'include' => ['relatedThroughPivotModels'], + ]); + + $queryBuilder = QueryBuilder::for(TestModel::class, $request) + ->allowedFields('relatedThroughPivotModels.name', 'id') + ->allowedIncludes('relatedThroughPivotModels'); + + DB::enableQueryLog(); + + $queryBuilder->first()->relatedThroughPivotModels; + + $this->assertQueryLogContains('select `test_models`.`id` from `test_models`'); + $this->assertQueryLogContains('select `related_through_pivot_models`.`name`, `pivot_models`.`test_model_id` as `pivot_test_model_id`, `pivot_models`.`related_through_pivot_model_id` as `pivot_related_through_pivot_model_id` from `related_through_pivot_models`'); +}); + it('can fetch requested array columns from included models up to two levels deep', function () { RelatedModel::create([ 'test_model_id' => $this->model->id, @@ -246,6 +336,36 @@ expect($result->relatedModels->first()->testModel->toArray())->toEqual(['id' => $this->model->id]); }); +it('can fetch requested array columns from included models up to two levels deep jsonApi mapper', function () { + config(['query-builder.convert_field_names_to_snake_case' => true]); + config(['query-builder.convert_relation_table_name_strategy' => 'camelCase']); + + $relatedModel = RelatedModel::create([ + 'test_model_id' => $this->model->id, + 'name' => 'related', + ]); + + $relatedModel->nestedRelatedModels()->create([ + 'name' => 'nested related', + ]); + + $request = new Request([ + 'fields' => 'id,name,relatedModels.id,relatedModels.name,nestedRelatedModels.id,nestedRelatedModels.name', + 'include' => ['nestedRelatedModels', 'relatedModels'], + ]); + + + $queryBuilder = QueryBuilder::for(TestModel::class, $request) + ->allowedFields('id', 'name', 'relatedModels.id', 'relatedModels.name', 'nestedRelatedModels.id', 'nestedRelatedModels.name') + ->allowedIncludes('relatedModels', 'nestedRelatedModels'); + + DB::enableQueryLog(); + $queryBuilder->first(); + + $this->assertQueryLogContains('select `test_models`.`id`, `test_models`.`name` from `test_models`'); + $this->assertQueryLogContains('select `nested_related_models`.`id`, `nested_related_models`.`name`, `related_models`.`test_model_id` as `laravel_through_key` from `nested_related_models`'); +}); + it('can fetch requested string columns from included models up to two levels deep', function () { RelatedModel::create([ 'test_model_id' => $this->model->id, diff --git a/tests/TestCase.php b/tests/TestCase.php index c8777655..591c22b6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -37,6 +37,7 @@ protected function setUpDatabase(Application $app) $table->increments('id'); $table->timestamps(); $table->string('name')->nullable(); + $table->string('full_name')->nullable(); $table->double('salary')->nullable(); $table->boolean('is_visible')->default(true); }); @@ -62,6 +63,7 @@ protected function setUpDatabase(Application $app) $table->increments('id'); $table->integer('test_model_id'); $table->string('name'); + $table->string('full_name')->nullable(); }); $app['db']->connection()->getSchemaBuilder()->create('nested_related_models', function (Blueprint $table) { @@ -92,7 +94,7 @@ protected function setUpDatabase(Application $app) protected function getPackageProviders($app) { return [ - RayServiceProvider::class, + // RayServiceProvider::class, QueryBuilderServiceProvider::class, ]; } diff --git a/tests/TestClasses/Models/TestModel.php b/tests/TestClasses/Models/TestModel.php index 16797919..e182b028 100644 --- a/tests/TestClasses/Models/TestModel.php +++ b/tests/TestClasses/Models/TestModel.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Support\Carbon; @@ -27,6 +28,18 @@ public function relatedModel(): BelongsTo return $this->belongsTo(RelatedModel::class); } + public function nestedRelatedModels(): HasManyThrough + { + return $this->hasManyThrough( + NestedRelatedModel::class, // Target model + RelatedModel::class, // Intermediate model + 'test_model_id', // Foreign key on RelatedModel + 'related_model_id', // Foreign key on NestedRelatedModel + 'id', // Local key on TestModel + 'id' // Local key on RelatedModel + ); + } + public function otherRelatedModels(): HasMany { return $this->hasMany(RelatedModel::class); diff --git a/utils/run_tests.sh b/utils/run_tests.sh new file mode 100755 index 00000000..97d633bc --- /dev/null +++ b/utils/run_tests.sh @@ -0,0 +1,193 @@ +#!/bin/bash + +# Exit on any error +set -euo pipefail + +# Define constants +readonly PREFIX="laravel-query-builder" +readonly DEFAULT_PHP_VERSION="8.3" +readonly DEFAULT_LARAVEL_VERSION="11.*" + +# Function to display help message +show_help() { + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " -h, --help Display this help message" + echo " -v, --version Display version information" + echo " -p PHP_VERSION Set the PHP version (default: ${DEFAULT_PHP_VERSION})" + echo " -l LARAVEL_VERSION Set the Laravel version (default: ${DEFAULT_LARAVEL_VERSION})" + echo " --filter FILTER Specify test filter(s)" + echo "" + echo "Example:" + echo " $0 --filter FieldsTest" +} + +# Parse command-line arguments +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + show_help + exit 0 + ;; + -v|--version) + echo "Version: 1.0" + exit 0 + ;; + -p|--php-version) + shift + PHP_VERSION="$1" + ;; + -l|--laravel-version) + shift + LARAVEL_VERSION="$1" + ;; + --filter) + shift + FILTER="$1" + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac + shift +done + +# Set default values if not provided +PHP_VERSION="${PHP_VERSION:-$DEFAULT_PHP_VERSION}" +LARAVEL_VERSION="${LARAVEL_VERSION:-$DEFAULT_LARAVEL_VERSION}" + +# Ensure we're in the project root +cd "$(dirname "$0")" + +# Create a custom Docker network +DOCKER_NETWORK_NAME="${PREFIX}-network" +docker network create "${DOCKER_NETWORK_NAME}" || true + +# Function to remove and recreate a container if it exists +recreate_container() { + local container_name="$1" + + # Remove the container if it exists (forcefully) + if docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then + echo "Removing existing container: ${container_name}" + docker rm -f "${container_name}" + fi +} + +# Prepare container names with prefix +MYSQL_CONTAINER_NAME="${PREFIX}-mysql" +REDIS_CONTAINER_NAME="${PREFIX}-redis" +TEST_RUNNER_IMAGE_NAME="${PREFIX}-test-runner" +TEST_CONTAINER_NAME="${PREFIX}-test-runner-container" + +# Recreate containers +recreate_container "${MYSQL_CONTAINER_NAME}" +recreate_container "${REDIS_CONTAINER_NAME}" +recreate_container "${TEST_CONTAINER_NAME}" + +# Set project root (parent of script directory) +PROJECT_ROOT="$(dirname "$(pwd)")" + +# Build the Docker image +docker build -t "${TEST_RUNNER_IMAGE_NAME}" -f - "$PROJECT_ROOT" </dev/null; then + echo "MySQL is ready!" + break + fi + sleep 4 + tries=$((tries+1)) +done + +if [ $tries -eq $max_tries ]; then + echo "MySQL did not become ready in time" + exit 1 +fi + +# Run tests in Docker +docker run --rm \ + --name "${TEST_CONTAINER_NAME}" \ + --network "${DOCKER_NETWORK_NAME}" \ + -e DB_HOST="${MYSQL_CONTAINER_NAME}" \ + -e DB_PORT=3306 \ + -e DB_USERNAME=user \ + -e DB_PASSWORD=secret \ + -e REDIS_HOST="${REDIS_CONTAINER_NAME}" \ + -e REDIS_PORT=6379 \ + "${TEST_RUNNER_IMAGE_NAME}" \ + vendor/bin/pest ${FILTER:+--filter "$FILTER"} + +# Cleanup containers +docker stop "${MYSQL_CONTAINER_NAME}" "${REDIS_CONTAINER_NAME}" "${TEST_CONTAINER_NAME}" +docker rm "${MYSQL_CONTAINER_NAME}" "${REDIS_CONTAINER_NAME}" "${TEST_CONTAINER_NAME}" +docker network rm "${DOCKER_NETWORK_NAME}" + +echo "Tests completed successfully!" \ No newline at end of file From 96746011866fc3c67953557c17b7d017bf5b3e07 Mon Sep 17 00:00:00 2001 From: Alex Vanderbist Date: Mon, 23 Dec 2024 23:16:11 +0100 Subject: [PATCH 30/51] Delete utils/run_tests.sh --- utils/run_tests.sh | 193 --------------------------------------------- 1 file changed, 193 deletions(-) delete mode 100755 utils/run_tests.sh diff --git a/utils/run_tests.sh b/utils/run_tests.sh deleted file mode 100755 index 97d633bc..00000000 --- a/utils/run_tests.sh +++ /dev/null @@ -1,193 +0,0 @@ -#!/bin/bash - -# Exit on any error -set -euo pipefail - -# Define constants -readonly PREFIX="laravel-query-builder" -readonly DEFAULT_PHP_VERSION="8.3" -readonly DEFAULT_LARAVEL_VERSION="11.*" - -# Function to display help message -show_help() { - echo "Usage: $0 [options]" - echo "" - echo "Options:" - echo " -h, --help Display this help message" - echo " -v, --version Display version information" - echo " -p PHP_VERSION Set the PHP version (default: ${DEFAULT_PHP_VERSION})" - echo " -l LARAVEL_VERSION Set the Laravel version (default: ${DEFAULT_LARAVEL_VERSION})" - echo " --filter FILTER Specify test filter(s)" - echo "" - echo "Example:" - echo " $0 --filter FieldsTest" -} - -# Parse command-line arguments -while [[ $# -gt 0 ]]; do - case "$1" in - -h|--help) - show_help - exit 0 - ;; - -v|--version) - echo "Version: 1.0" - exit 0 - ;; - -p|--php-version) - shift - PHP_VERSION="$1" - ;; - -l|--laravel-version) - shift - LARAVEL_VERSION="$1" - ;; - --filter) - shift - FILTER="$1" - ;; - *) - echo "Unknown option: $1" >&2 - exit 1 - ;; - esac - shift -done - -# Set default values if not provided -PHP_VERSION="${PHP_VERSION:-$DEFAULT_PHP_VERSION}" -LARAVEL_VERSION="${LARAVEL_VERSION:-$DEFAULT_LARAVEL_VERSION}" - -# Ensure we're in the project root -cd "$(dirname "$0")" - -# Create a custom Docker network -DOCKER_NETWORK_NAME="${PREFIX}-network" -docker network create "${DOCKER_NETWORK_NAME}" || true - -# Function to remove and recreate a container if it exists -recreate_container() { - local container_name="$1" - - # Remove the container if it exists (forcefully) - if docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then - echo "Removing existing container: ${container_name}" - docker rm -f "${container_name}" - fi -} - -# Prepare container names with prefix -MYSQL_CONTAINER_NAME="${PREFIX}-mysql" -REDIS_CONTAINER_NAME="${PREFIX}-redis" -TEST_RUNNER_IMAGE_NAME="${PREFIX}-test-runner" -TEST_CONTAINER_NAME="${PREFIX}-test-runner-container" - -# Recreate containers -recreate_container "${MYSQL_CONTAINER_NAME}" -recreate_container "${REDIS_CONTAINER_NAME}" -recreate_container "${TEST_CONTAINER_NAME}" - -# Set project root (parent of script directory) -PROJECT_ROOT="$(dirname "$(pwd)")" - -# Build the Docker image -docker build -t "${TEST_RUNNER_IMAGE_NAME}" -f - "$PROJECT_ROOT" </dev/null; then - echo "MySQL is ready!" - break - fi - sleep 4 - tries=$((tries+1)) -done - -if [ $tries -eq $max_tries ]; then - echo "MySQL did not become ready in time" - exit 1 -fi - -# Run tests in Docker -docker run --rm \ - --name "${TEST_CONTAINER_NAME}" \ - --network "${DOCKER_NETWORK_NAME}" \ - -e DB_HOST="${MYSQL_CONTAINER_NAME}" \ - -e DB_PORT=3306 \ - -e DB_USERNAME=user \ - -e DB_PASSWORD=secret \ - -e REDIS_HOST="${REDIS_CONTAINER_NAME}" \ - -e REDIS_PORT=6379 \ - "${TEST_RUNNER_IMAGE_NAME}" \ - vendor/bin/pest ${FILTER:+--filter "$FILTER"} - -# Cleanup containers -docker stop "${MYSQL_CONTAINER_NAME}" "${REDIS_CONTAINER_NAME}" "${TEST_CONTAINER_NAME}" -docker rm "${MYSQL_CONTAINER_NAME}" "${REDIS_CONTAINER_NAME}" "${TEST_CONTAINER_NAME}" -docker network rm "${DOCKER_NETWORK_NAME}" - -echo "Tests completed successfully!" \ No newline at end of file From 181856511aa0cc8d595db2dfd8392aff05a9ebda Mon Sep 17 00:00:00 2001 From: AlexVanderbist Date: Mon, 23 Dec 2024 22:17:46 +0000 Subject: [PATCH 31/51] Update CHANGELOG --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0e3b9d0..6c8f4ebd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to `laravel-query-builder` will be documented in this file +## 6.3.0 - 2024-12-23 + +### What's Changed + +* Feature: Add "belongs to" filter by @gpibarra in https://github.com/spatie/laravel-query-builder/pull/975 +* Feature: Additional config options to better match the API spec by @CoolGoose in https://github.com/spatie/laravel-query-builder/pull/983 + +### New Contributors + +* @CoolGoose made their first contribution in https://github.com/spatie/laravel-query-builder/pull/983 + +**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/6.2.3...6.3.0 + ## 6.2.3 - 2024-12-23 ### What's Changed From d96c963a1e28a0ad2ad0297cd162e6cdb7eaa304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Magyar?= <14284867+xHeaven@users.noreply.github.com> Date: Mon, 30 Dec 2024 10:37:49 +0100 Subject: [PATCH 32/51] General code health improvements (#988) * Remove unused parameter * Use null-safe operator instead of optional() helper Also the model is not nullable at this point, so we can safely remove the null-safe operator below the if condition. * Mark nullable parameters explicitly * Sort use() variables * Use direct empty string check to reduce cognitive load * Remove unnecessary else branch --- src/AllowedFilter.php | 22 +++++++++++----------- src/Filters/FiltersBelongsTo.php | 14 +++++++------- src/Filters/FiltersExact.php | 2 +- src/Filters/FiltersPartial.php | 6 +++--- src/Filters/FiltersScope.php | 4 ++-- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/AllowedFilter.php b/src/AllowedFilter.php index fcdbd106..c4ea46ee 100644 --- a/src/AllowedFilter.php +++ b/src/AllowedFilter.php @@ -48,56 +48,56 @@ public function filter(QueryBuilder $query, $value): void ($this->filterClass)($query->getEloquentBuilder(), $valueToFilter, $this->internalName); } - public static function setFilterArrayValueDelimiter(string $delimiter = null): void + public static function setFilterArrayValueDelimiter(?string $delimiter = null): void { if (isset($delimiter)) { QueryBuilderRequest::setFilterArrayValueDelimiter($delimiter); } } - public static function exact(string $name, ?string $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): static + public static function exact(string $name, ?string $internalName = null, bool $addRelationConstraint = true, ?string $arrayValueDelimiter = null): static { static::setFilterArrayValueDelimiter($arrayValueDelimiter); return new static($name, new FiltersExact($addRelationConstraint), $internalName); } - public static function partial(string $name, $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): static + public static function partial(string $name, $internalName = null, bool $addRelationConstraint = true, ?string $arrayValueDelimiter = null): static { static::setFilterArrayValueDelimiter($arrayValueDelimiter); return new static($name, new FiltersPartial($addRelationConstraint), $internalName); } - public static function beginsWithStrict(string $name, $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): static + public static function beginsWithStrict(string $name, $internalName = null, bool $addRelationConstraint = true, ?string $arrayValueDelimiter = null): static { static::setFilterArrayValueDelimiter($arrayValueDelimiter); return new static($name, new FiltersBeginsWithStrict($addRelationConstraint), $internalName); } - public static function endsWithStrict(string $name, $internalName = null, bool $addRelationConstraint = true, string $arrayValueDelimiter = null): static + public static function endsWithStrict(string $name, $internalName = null, bool $addRelationConstraint = true, ?string $arrayValueDelimiter = null): static { static::setFilterArrayValueDelimiter($arrayValueDelimiter); return new static($name, new FiltersEndsWithStrict($addRelationConstraint), $internalName); } - public static function belongsTo(string $name, $internalName = null, string $arrayValueDelimiter = null): static + public static function belongsTo(string $name, $internalName = null, ?string $arrayValueDelimiter = null): static { static::setFilterArrayValueDelimiter($arrayValueDelimiter); return new static($name, new FiltersBelongsTo(), $internalName); } - public static function scope(string $name, $internalName = null, string $arrayValueDelimiter = null): static + public static function scope(string $name, $internalName = null, ?string $arrayValueDelimiter = null): static { static::setFilterArrayValueDelimiter($arrayValueDelimiter); return new static($name, new FiltersScope(), $internalName); } - public static function callback(string $name, $callback, $internalName = null, string $arrayValueDelimiter = null): static + public static function callback(string $name, $callback, $internalName = null, ?string $arrayValueDelimiter = null): static { static::setFilterArrayValueDelimiter($arrayValueDelimiter); @@ -109,18 +109,18 @@ public static function trashed(string $name = 'trashed', $internalName = null): return new static($name, new FiltersTrashed(), $internalName); } - public static function custom(string $name, Filter $filterClass, $internalName = null, string $arrayValueDelimiter = null): static + public static function custom(string $name, Filter $filterClass, $internalName = null, ?string $arrayValueDelimiter = null): static { static::setFilterArrayValueDelimiter($arrayValueDelimiter); 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 + 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); + return new static($name, new FiltersOperator($addRelationConstraint, $filterOperator, $boolean), $internalName); } public function getFilterClass(): Filter diff --git a/src/Filters/FiltersBelongsTo.php b/src/Filters/FiltersBelongsTo.php index d3ef1d3d..059924b9 100644 --- a/src/Filters/FiltersBelongsTo.php +++ b/src/Filters/FiltersBelongsTo.php @@ -70,14 +70,14 @@ protected function getModelFromRelation(Model $model, string $relation, int $lev $relationParts = explode('.', $relation); if (count($relationParts) == 1) { return $this->getRelatedModelFromRelation($model, $relation); - } else { - $firstRelation = $relationParts[0]; - $firstRelatedModel = $this->getRelatedModelFromRelation($model, $firstRelation); - if (! $firstRelatedModel) { - return null; - } + } - return $this->getModelFromRelation($firstRelatedModel, implode('.', array_slice($relationParts, 1)), $level + 1); + $firstRelation = $relationParts[0]; + $firstRelatedModel = $this->getRelatedModelFromRelation($model, $firstRelation); + if (! $firstRelatedModel) { + return null; } + + return $this->getModelFromRelation($firstRelatedModel, implode('.', array_slice($relationParts, 1)), $level + 1); } } diff --git a/src/Filters/FiltersExact.php b/src/Filters/FiltersExact.php index 58c07818..b84ad991 100644 --- a/src/Filters/FiltersExact.php +++ b/src/Filters/FiltersExact.php @@ -66,7 +66,7 @@ protected function withRelationConstraint(Builder $query, mixed $value, string $ $parts->last(), ]); - $query->whereHas($relation, function (Builder $query) use ($value, $property) { + $query->whereHas($relation, function (Builder $query) use ($property, $value) { $this->relationConstraints[] = $property = $query->qualifyColumn($property); $this->__invoke($query, $value, $property); diff --git a/src/Filters/FiltersPartial.php b/src/Filters/FiltersPartial.php index 31d77100..455336a3 100644 --- a/src/Filters/FiltersPartial.php +++ b/src/Filters/FiltersPartial.php @@ -25,12 +25,12 @@ public function __invoke(Builder $query, $value, string $property) $databaseDriver = $this->getDatabaseDriver($query); if (is_array($value)) { - if (count(array_filter($value, fn ($item) => strlen($item) > 0)) === 0) { + if (count(array_filter($value, fn ($item) => $item != '')) === 0) { return $query; } - $query->where(function (Builder $query) use ($databaseDriver, $value, $wrappedProperty) { - foreach (array_filter($value, fn ($item) => strlen($item) > 0) as $partialValue) { + $query->where(function (Builder $query) use ($value, $wrappedProperty, $databaseDriver) { + foreach (array_filter($value, fn ($item) => $item != '') as $partialValue) { [$sql, $bindings] = $this->getWhereRawParameters($partialValue, $wrappedProperty, $databaseDriver); $query->orWhereRaw($sql, $bindings); } diff --git a/src/Filters/FiltersScope.php b/src/Filters/FiltersScope.php index eca6829a..5be5d508 100644 --- a/src/Filters/FiltersScope.php +++ b/src/Filters/FiltersScope.php @@ -54,12 +54,12 @@ protected function resolveParameters(Builder $query, $values, string $scope): ar } foreach ($parameters as $parameter) { - if (! optional($this->getClass($parameter))->isSubclassOf(Model::class)) { + if (! $this->getClass($parameter)?->isSubclassOf(Model::class)) { continue; } /** @var TModelClass $model */ - $model = $this->getClass($parameter)?->newInstance(); + $model = $this->getClass($parameter)->newInstance(); $index = $parameter->getPosition() - 1; $value = $values[$index]; From c183430d4a50e2011640294edd7de99a2183dbe4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:56:34 +0000 Subject: [PATCH 33/51] Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 2.2.0 to 2.3.0. - [Release notes](https://github.com/dependabot/fetch-metadata/releases) - [Commits](https://github.com/dependabot/fetch-metadata/compare/v2.2.0...v2.3.0) --- updated-dependencies: - dependency-name: dependabot/fetch-metadata dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/dependabot-auto-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 9db8ce33..e99b9aa1 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -13,7 +13,7 @@ jobs: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v2.2.0 + uses: dependabot/fetch-metadata@v2.3.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" compat-lookup: true From db8cb6cc9b5d77f2f455158f64111d92cd1f4ece Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Tue, 18 Feb 2025 17:27:30 -0500 Subject: [PATCH 34/51] Exclude .github folder and .php_cs from being included in composer download --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitattributes b/.gitattributes index 6ca01898..b800f758 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,10 +4,12 @@ * text eol=lf # Ignore all test and documentation with "export-ignore". +/.github export-ignore /.gitattributes export-ignore /.gitignore export-ignore /.travis.yml export-ignore /phpunit.xml.dist export-ignore +/.php_cs.dist.php export-ignore /.scrutinizer.yml export-ignore /tests export-ignore /.editorconfig export-ignore From 01bb302fdc85f1a1265085a86459c03f32004ea2 Mon Sep 17 00:00:00 2001 From: Shift Date: Wed, 19 Feb 2025 03:55:41 +0000 Subject: [PATCH 35/51] Bump dependencies for Laravel 12 --- composer.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 03edeae6..9f921214 100644 --- a/composer.json +++ b/composer.json @@ -21,18 +21,18 @@ ], "require": { "php": "^8.2", - "illuminate/database": "^10.0|^11.0", - "illuminate/http": "^10.0|^11.0", - "illuminate/support": "^10.0|^11.0", + "illuminate/database": "^10.0|^11.0|^12.0", + "illuminate/http": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", "spatie/laravel-package-tools": "^1.11" }, "require-dev": { "ext-json": "*", "larastan/larastan": "^2.9", "mockery/mockery": "^1.4", - "orchestra/testbench": "^7.0|^8.0", - "pestphp/pest": "^2.0", - "phpunit/phpunit": "^10.0", + "orchestra/testbench": "^7.0|^8.0|^10.0", + "pestphp/pest": "^2.0|^3.7", + "phpunit/phpunit": "^10.0|^11.5.3", "spatie/invade": "^2.0" }, "autoload": { From 8feb781ea3216fc7b91def245f9ecfca78abd700 Mon Sep 17 00:00:00 2001 From: Shift Date: Wed, 19 Feb 2025 03:55:42 +0000 Subject: [PATCH 36/51] Update GitHub Actions for Laravel 12 --- .github/workflows/run-tests.yml | 134 ++++++++++++++++---------------- 1 file changed, 66 insertions(+), 68 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 0039050c..ea8cf9dd 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,82 +1,80 @@ name: run-tests on: - push: - paths: - - '**.php' - - '.github/workflows/run-tests.yml' - - 'phpunit.xml.dist' - - 'composer.json' - - 'composer.lock' + push: + paths: + - **.php + - .github/workflows/run-tests.yml + - phpunit.xml.dist + - composer.json + - composer.lock jobs: - test: - runs-on: ${{ matrix.os }} - timeout-minutes: 5 - strategy: - fail-fast: true - matrix: - os: [ubuntu-latest] - php: [8.3, 8.2] - laravel: [10.*, 11.*] - stability: [prefer-stable] - include: - - laravel: 11.* - testbench: 9.* - carbon: ^2.63 - - laravel: 10.* - testbench: 8.* + test: + runs-on: ${{ matrix.os }} - name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} + timeout-minutes: 5 - services: - mysql: - image: mysql:8.0 - env: - MYSQL_USER: user - MYSQL_PASSWORD: secret - MYSQL_DATABASE: laravel_query_builder - MYSQL_ROOT_PASSWORD: secretroot - ports: - - 3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest] + php: [8.3, 8.2] + laravel: ['10.*', '11.*', '12.*'] + stability: [prefer-stable] + include: + - laravel: 11.* + testbench: 9.* + carbon: ^2.63 + - laravel: 10.* + testbench: 8.* + - laravel: 12.* + testbench: 10.* - redis: - image: redis - ports: - - 6379:6379 - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} - steps: - - name: Checkout code - uses: actions/checkout@v4 + services: + mysql: + image: mysql:8.0 + env: + MYSQL_USER: user + MYSQL_PASSWORD: secret + MYSQL_DATABASE: laravel_query_builder + MYSQL_ROOT_PASSWORD: secretroot + ports: + - 3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + redis: + image: redis + ports: + - 6379:6379 + options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo - coverage: none + steps: + - name: Checkout code + uses: actions/checkout@v4 - - name: Setup problem matchers - run: | - echo "::add-matcher::${{ runner.tool_cache }}/php.json" - echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + coverage: none - - name: Install dependencies - run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update - composer update --${{ matrix.stability }} --prefer-dist --no-interaction + - name: Setup problem matchers + run: | + echo "::add-matcher::${{ runner.tool_cache }}/php.json" + echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - name: Execute tests - run: vendor/bin/pest - env: - DB_USERNAME: user - DB_PASSWORD: secret - DB_PORT: ${{ job.services.mysql.ports[3306] }} - REDIS_PORT: 6379 + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update + composer update --${{ matrix.stability }} --prefer-dist --no-interaction + - name: Execute tests + run: vendor/bin/pest + env: + DB_USERNAME: user + DB_PASSWORD: secret + DB_PORT: ${{ job.services.mysql.ports[3306] }} + REDIS_PORT: 6379 From 743f5b6264b0bfd6e9f8179ca8dbcbf76b1ff362 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Wed, 19 Feb 2025 08:06:10 +0100 Subject: [PATCH 37/51] Update run-tests.yml --- .github/workflows/run-tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index ea8cf9dd..60ab2948 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -25,7 +25,6 @@ jobs: include: - laravel: 11.* testbench: 9.* - carbon: ^2.63 - laravel: 10.* testbench: 8.* - laravel: 12.* From 71b89e5b72d82156dc27b8b6cf0aaca7e2900a9e Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Wed, 19 Feb 2025 08:08:58 +0100 Subject: [PATCH 38/51] Update run-tests.yml --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 60ab2948..1462a9e6 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -19,7 +19,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - php: [8.3, 8.2] + php: [8.4, 8.3, 8.2] laravel: ['10.*', '11.*', '12.*'] stability: [prefer-stable] include: From 278ee9ce7a2b0b937d1ffac6998eb0c8ef93f18e Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Wed, 19 Feb 2025 08:13:08 +0100 Subject: [PATCH 39/51] Update run-tests.yml --- .github/workflows/run-tests.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 1462a9e6..2c782727 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,13 +1,7 @@ name: run-tests on: - push: - paths: - - **.php - - .github/workflows/run-tests.yml - - phpunit.xml.dist - - composer.json - - composer.lock + push jobs: test: From 465d9b7364590c9ae3ee3738ff8a293e685dd588 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Wed, 19 Feb 2025 08:14:53 +0100 Subject: [PATCH 40/51] Update composer.json --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 9f921214..d7d917c8 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,6 @@ }, "require-dev": { "ext-json": "*", - "larastan/larastan": "^2.9", "mockery/mockery": "^1.4", "orchestra/testbench": "^7.0|^8.0|^10.0", "pestphp/pest": "^2.0|^3.7", From a3cd80d28307b4b9019fdfff44c0419ddf0999a6 Mon Sep 17 00:00:00 2001 From: freekmurze <483853+freekmurze@users.noreply.github.com> Date: Fri, 21 Feb 2025 09:24:28 +0000 Subject: [PATCH 41/51] Update CHANGELOG --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c8f4ebd..5a76a7dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to `laravel-query-builder` will be documented in this file +## 6.3.1 - 2025-02-21 + +### What's Changed + +* General code health improvements by @xHeaven in https://github.com/spatie/laravel-query-builder/pull/988 +* Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/spatie/laravel-query-builder/pull/992 +* Laravel 12.x Compatibility by @laravel-shift in https://github.com/spatie/laravel-query-builder/pull/994 +* Exclude `.github` folder and `.php_cs` from being included in composer installation by @stevebauman in https://github.com/spatie/laravel-query-builder/pull/993 + +### New Contributors + +* @xHeaven made their first contribution in https://github.com/spatie/laravel-query-builder/pull/988 + +**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/6.3.0...6.3.1 + ## 6.3.0 - 2024-12-23 ### What's Changed From e01179f0b1495f49170a377b82b966a2bc44530d Mon Sep 17 00:00:00 2001 From: Jimi Robaer Date: Tue, 8 Apr 2025 16:54:04 +0200 Subject: [PATCH 42/51] Update README.md --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2e041f11..3cc3a3a2 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,19 @@ -# Build Eloquent queries from API requests +
+ + + + Logo for laravel-query-builder + + + +

Build Eloquent queries from API requests

[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-query-builder.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-query-builder) ![Test Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-query-builder/run-tests.yml?label=tests&branch=main) ![Code Style Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-query-builder/php-cs-fixer.yml?label=code%20style&branch=main) [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-query-builder.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-query-builder) - -This package allows you to filter, sort and include eloquent relations based on a request. The `QueryBuilder` used in this package extends Laravel's default Eloquent builder. This means all your favorite methods and macros are still available. Query parameter names follow the [JSON API specification](http://jsonapi.org/) as closely as possible. + +
## Basic usage From c3a763745e4af1e6aad21f3cca57278e7ec9e04c Mon Sep 17 00:00:00 2001 From: Alexander Kartavenko Date: Tue, 15 Apr 2025 19:13:49 +0300 Subject: [PATCH 43/51] Enhance QueryBuilder with generics support for better type inference --- src/QueryBuilder.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 80366e85..805ec896 100755 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -14,7 +14,8 @@ use Spatie\QueryBuilder\Concerns\SortsQuery; /** - * @mixin EloquentBuilder + * @template TModel of Model + * @mixin EloquentBuilder */ class QueryBuilder implements ArrayAccess { @@ -49,6 +50,9 @@ public function getSubject(): Relation|EloquentBuilder return $this->subject; } + /** + * @return static + */ public static function for( EloquentBuilder|Relation|string $subject, ?Request $request = null @@ -57,7 +61,10 @@ public static function for( $subject = $subject::query(); } - return new static($subject, $request); + /** @var static $queryBuilder */ + $queryBuilder = new static($subject, $request); + + return $queryBuilder; } public function __call($name, $arguments) From f73be49ce4a4781f74e4499edb1e14f21fbf2f0d Mon Sep 17 00:00:00 2001 From: Alexander Kartavenko Date: Tue, 15 Apr 2025 20:37:25 +0300 Subject: [PATCH 44/51] Refactor clone method in QueryBuilder to improve type safety with generics --- src/QueryBuilder.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 805ec896..7b1baa3a 100755 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -82,9 +82,14 @@ public function __call($name, $arguments) return $result; } + /** + * @return static + */ public function clone(): static { - return clone $this; + /** @var static $cloned */ + $cloned = clone $this; + return $cloned; } public function __clone() From fc207334ca9738fa2576fda18293d1ce1e87b6af Mon Sep 17 00:00:00 2001 From: Alexander Kartavenko Date: Tue, 15 Apr 2025 21:27:37 +0300 Subject: [PATCH 45/51] revert some changes --- src/QueryBuilder.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 7b1baa3a..eb181915 100755 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -50,9 +50,6 @@ public function getSubject(): Relation|EloquentBuilder return $this->subject; } - /** - * @return static - */ public static function for( EloquentBuilder|Relation|string $subject, ?Request $request = null @@ -82,14 +79,9 @@ public function __call($name, $arguments) return $result; } - /** - * @return static - */ public function clone(): static { - /** @var static $cloned */ - $cloned = clone $this; - return $cloned; + return clone $this; } public function __clone() From 1d9de6da8d1d61a1fcc12e6c6de9c3845836965d Mon Sep 17 00:00:00 2001 From: freekmurze <483853+freekmurze@users.noreply.github.com> Date: Wed, 16 Apr 2025 07:30:37 +0000 Subject: [PATCH 46/51] Update CHANGELOG --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a76a7dd..581968cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to `laravel-query-builder` will be documented in this file +## 6.3.2 - 2025-04-16 + +### What's Changed + +* Enhance QueryBuilder with generics support for better type inference by @alexkart in https://github.com/spatie/laravel-query-builder/pull/1002 + +### New Contributors + +* @alexkart made their first contribution in https://github.com/spatie/laravel-query-builder/pull/1002 + +**Full Changelog**: https://github.com/spatie/laravel-query-builder/compare/6.3.1...6.3.2 + ## 6.3.1 - 2025-02-21 ### What's Changed From 87e7a9671f4c9630f256ad1309cfc7bc14521e61 Mon Sep 17 00:00:00 2001 From: Jimi Robaer Date: Tue, 22 Apr 2025 09:34:59 +0200 Subject: [PATCH 47/51] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3cc3a3a2..1c9bba42 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ - Logo for laravel-query-builder + Logo for laravel-query-builder From ff95255a136c4e9c9127325cac600dd3667af181 Mon Sep 17 00:00:00 2001 From: Knud Hollander <26556793+KnudH@users.noreply.github.com> Date: Sat, 3 May 2025 10:58:23 +0200 Subject: [PATCH 48/51] update some links so point to v6 docs --- docs/introduction.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/introduction.md b/docs/introduction.md index 3baf681f..b7f5b797 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -23,7 +23,7 @@ $users = QueryBuilder::for(User::class) // all `User`s that contain the string "John" in their name ``` -[Read more about filtering features like: partial filters, exact filters, scope filters, custom filters, ignored values, default filter values, ...](https://spatie.be/docs/laravel-query-builder/v5/features/filtering/) +[Read more about filtering features like: partial filters, exact filters, scope filters, custom filters, ignored values, default filter values, ...](https://spatie.be/docs/laravel-query-builder/v6/features/filtering/) ### Including relations based on a request: `/users?include=posts`: @@ -35,7 +35,7 @@ $users = QueryBuilder::for(User::class) // all `User`s with their `posts` loaded ``` -[Read more about include features like: including nested relationships, including relationship count, ...](https://spatie.be/docs/laravel-query-builder/v5/features/including-relationships/) +[Read more about include features like: including nested relationships, including relationship count, ...](https://spatie.be/docs/laravel-query-builder/v6/features/including-relationships/) ### Sorting a query based on a request: `/users?sort=id`: @@ -47,7 +47,7 @@ $users = QueryBuilder::for(User::class) // all `User`s sorted by ascending id ``` -[Read more about sorting features like: custom sorts, sort direction, ...](https://spatie.be/docs/laravel-query-builder/v5/features/sorting/) +[Read more about sorting features like: custom sorts, sort direction, ...](https://spatie.be/docs/laravel-query-builder/v6/features/sorting/) ### Works together nicely with existing queries: @@ -70,7 +70,7 @@ $users = QueryBuilder::for(User::class) // the fetched `User`s will only have their id & email set ``` -[Read more about selecting fields.](https://spatie.be/docs/laravel-query-builder/v5/features/selecting-fields/) +[Read more about selecting fields.](https://spatie.be/docs/laravel-query-builder/v6/features/selecting-fields/) ## We have badges! From 7de97f9ebb0ffbdf88eda32dce30a6d7d9f851df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 17:34:12 +0000 Subject: [PATCH 49/51] Bump dependabot/fetch-metadata from 2.3.0 to 2.4.0 Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 2.3.0 to 2.4.0. - [Release notes](https://github.com/dependabot/fetch-metadata/releases) - [Commits](https://github.com/dependabot/fetch-metadata/compare/v2.3.0...v2.4.0) --- updated-dependencies: - dependency-name: dependabot/fetch-metadata dependency-version: 2.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/dependabot-auto-merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index e99b9aa1..531772b8 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -13,7 +13,7 @@ jobs: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v2.3.0 + uses: dependabot/fetch-metadata@v2.4.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" compat-lookup: true From e0d1db5ab276dbdf2c871659604bbc83608108ca Mon Sep 17 00:00:00 2001 From: ruuddeenen <33053199+ruuddeenen@users.noreply.github.com> Date: Thu, 15 May 2025 07:34:50 +0000 Subject: [PATCH 50/51] Fix styling --- src/Filters/FiltersPartial.php | 2 +- tests/FilterTest.php | 1 - tests/SortTest.php | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Filters/FiltersPartial.php b/src/Filters/FiltersPartial.php index f5c38385..77db819c 100644 --- a/src/Filters/FiltersPartial.php +++ b/src/Filters/FiltersPartial.php @@ -74,7 +74,7 @@ protected static function escapeLike(string $value): string */ protected static function maybeSpecifyEscapeChar(string $driver): string { - if(! in_array($driver, ['sqlite','pgsql','sqlsrv'])) { + if (! in_array($driver, ['sqlite','pgsql','sqlsrv'])) { return ''; } diff --git a/tests/FilterTest.php b/tests/FilterTest.php index 80b971be..650abbd9 100644 --- a/tests/FilterTest.php +++ b/tests/FilterTest.php @@ -4,7 +4,6 @@ use Illuminate\Http\Request; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; - use Pest\Expectation; use function PHPUnit\Framework\assertObjectHasProperty; diff --git a/tests/SortTest.php b/tests/SortTest.php index 95534beb..6fd3de5b 100644 --- a/tests/SortTest.php +++ b/tests/SortTest.php @@ -14,7 +14,6 @@ use Spatie\QueryBuilder\Sorts\Sort as SortInterface; use Spatie\QueryBuilder\Sorts\SortsField; use Spatie\QueryBuilder\Tests\Concerns\AssertsCollectionSorting; - use Spatie\QueryBuilder\Tests\TestClasses\Models\TestModel; uses(AssertsCollectionSorting::class); From 6b5bf0ca0d343ebd87faef896d65b5878ec3e5c8 Mon Sep 17 00:00:00 2001 From: ruuddeenen <33053199+ruuddeenen@users.noreply.github.com> Date: Thu, 15 May 2025 08:48:15 +0000 Subject: [PATCH 51/51] Fix styling --- src/Concerns/AddsFieldsToQuery.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Concerns/AddsFieldsToQuery.php b/src/Concerns/AddsFieldsToQuery.php index 4c054f62..f276cd73 100644 --- a/src/Concerns/AddsFieldsToQuery.php +++ b/src/Concerns/AddsFieldsToQuery.php @@ -108,7 +108,7 @@ public function getRequestedFieldsForRelatedTable(string $relation, ?string $tab if (config('query-builder.convert_field_names_to_snake_case', false)) { $fields = $fields->mapWithKeys(fn ($fields, $table) => [ - $table => collect($fields)->map(fn ($field) => Str::snake($field)) + $table => collect($fields)->map(fn ($field) => Str::snake($field)), ]); } @@ -117,18 +117,19 @@ public function getRequestedFieldsForRelatedTable(string $relation, ?string $tab foreach ($possibleRelatedNames as $tableName) { if ($fields->has($tableName)) { $matchedFields = $fields->get($tableName); + break; } } - if (!$matchedFields) { + if (! $matchedFields) { return []; } $matchedFields = $matchedFields->toArray(); // Validate against allowed fields as in original implementation - if (!$this->allowedFields instanceof Collection) { + if (! $this->allowedFields instanceof Collection) { throw new UnknownIncludedFieldsQuery($matchedFields); }