From 3702578a8cef887bd67e6287914b8307f3dd3f6d Mon Sep 17 00:00:00 2001 From: Gregory Haddow Date: Thu, 27 Jun 2024 09:04:33 +0100 Subject: [PATCH 1/4] feat: add WhereAll and WhereAny filters --- src/Filters/Concerns/HasColumns.php | 81 +++++++++++++++++++ src/Filters/WhereAll.php | 79 ++++++++++++++++++ src/Filters/WhereAny.php | 79 ++++++++++++++++++ tests/app/Schemas/PostSchema.php | 4 + tests/lib/Acceptance/Filters/WhereAllTest.php | 56 +++++++++++++ tests/lib/Acceptance/Filters/WhereAnyTest.php | 55 +++++++++++++ 6 files changed, 354 insertions(+) create mode 100644 src/Filters/Concerns/HasColumns.php create mode 100644 src/Filters/WhereAll.php create mode 100644 src/Filters/WhereAny.php create mode 100644 tests/lib/Acceptance/Filters/WhereAllTest.php create mode 100644 tests/lib/Acceptance/Filters/WhereAnyTest.php diff --git a/src/Filters/Concerns/HasColumns.php b/src/Filters/Concerns/HasColumns.php new file mode 100644 index 0000000..95ec744 --- /dev/null +++ b/src/Filters/Concerns/HasColumns.php @@ -0,0 +1,81 @@ + + */ + private array $columns = []; + + /** + * @return array + */ + public function columns(): array + { + return $this->columns; + } + + public function withColumn(string $column): self + { + $this->columns[] = $column; + + return $this; + } + + /** + * Force the table name when qualifying the columns. + * + * This allows the developer to force the table that the columns are qualified with. + * + * @param string $table + * @return $this + */ + public function qualifyAs(string $table): self + { + $this->table = $table; + + return $this; + } + + /** + * Determine if developer has forced a table to qualify columns as + * + * @return bool + */ + public function isQualified(): bool + { + return $this->table === null; + } + + /** + * Get qualified columns. + * + * @return array + */ + protected function qualifiedColumns(): array + { + if ($this->table) { + return array_map(fn($column) => $this->table . '.' . $column, $this->columns); + } + + return $this->columns; + } +} diff --git a/src/Filters/WhereAll.php b/src/Filters/WhereAll.php new file mode 100644 index 0000000..c6864fb --- /dev/null +++ b/src/Filters/WhereAll.php @@ -0,0 +1,79 @@ + $columns + * @return static + */ + public static function make(string $name, array $columns = null): self + { + return new static($name, $columns); + } + + /** + * WhereAny constructor. + * + * @param string $name + * @param array $columns + */ + public function __construct(string $name, array $columns = null) + { + $this->name = $name; + $this->columns = $columns ?? []; + $this->operator = '='; + } + + /** + * @inheritDoc + */ + public function key(): string + { + return $this->name; + } + + /** + * @inheritDoc + */ + public function apply($query, $value) + { + if (!$this->isQualified()){ + $this->qualifyAs($query->getModel()->getTable()); + } + + return $query->whereAll( + $this->qualifiedColumns(), + $this->operator(), + $this->deserialize($value) + ); + } +} diff --git a/src/Filters/WhereAny.php b/src/Filters/WhereAny.php new file mode 100644 index 0000000..43fa0b0 --- /dev/null +++ b/src/Filters/WhereAny.php @@ -0,0 +1,79 @@ + $columns + * @return static + */ + public static function make(string $name, array $columns = null): self + { + return new static($name, $columns); + } + + /** + * WhereAny constructor. + * + * @param string $name + * @param array $columns + */ + public function __construct(string $name, array $columns = null) + { + $this->name = $name; + $this->columns = $columns ?? []; + $this->operator = '='; + } + + /** + * @inheritDoc + */ + public function key(): string + { + return $this->name; + } + + /** + * @inheritDoc + */ + public function apply($query, $value) + { + if (!$this->isQualified()){ + $this->qualifyAs($query->getModel()->getTable()); + } + + return $query->whereAny( + $this->qualifiedColumns(), + $this->operator(), + $this->deserialize($value) + ); + } +} diff --git a/tests/app/Schemas/PostSchema.php b/tests/app/Schemas/PostSchema.php index 6fbb229..872c686 100644 --- a/tests/app/Schemas/PostSchema.php +++ b/tests/app/Schemas/PostSchema.php @@ -24,6 +24,8 @@ use LaravelJsonApi\Eloquent\Fields\Str; use LaravelJsonApi\Eloquent\Filters\OnlyTrashed; use LaravelJsonApi\Eloquent\Filters\Where; +use LaravelJsonApi\Eloquent\Filters\WhereAll; +use LaravelJsonApi\Eloquent\Filters\WhereAny; use LaravelJsonApi\Eloquent\Filters\WhereDoesntHave; use LaravelJsonApi\Eloquent\Filters\WhereHas; use LaravelJsonApi\Eloquent\Filters\WhereIdIn; @@ -94,6 +96,8 @@ public function filters(): iterable Where::make('slug')->singular(), WhereIn::make('slugs')->delimiter(','), WithTrashed::make('withTrashed'), + WhereAll::make('all', ['title','content'])->withColumn('slug')->using('like'), + WhereAny::make('any', ['title','content'])->withColumn('slug')->using('like'), ]; } diff --git a/tests/lib/Acceptance/Filters/WhereAllTest.php b/tests/lib/Acceptance/Filters/WhereAllTest.php new file mode 100644 index 0000000..ee1980c --- /dev/null +++ b/tests/lib/Acceptance/Filters/WhereAllTest.php @@ -0,0 +1,56 @@ +schema = $this->schemas()->schemaFor('posts'); + } + + /** + * @return void + */ + public function testWhereAll(): void + { + Post::factory()->count(5)->create(); + + $all = Post::factory()->create(['title' => "foobar boofar", 'content' => "boofar foobar", 'slug' => "totally_foobar_1"]); + Post::factory()->create(['title' => "foobar boofar"]); + Post::factory()->create(['content' => "boofar foobar"]); + Post::factory()->create(['slug' => "totally_foobar"]); + + $expected = [$all]; + + $actual = $this->schema + ->repository() + ->queryAll() + ->filter(['all' => '%foobar%']) + ->get(); + + $this->assertFilteredModels($expected, $actual); + } +} diff --git a/tests/lib/Acceptance/Filters/WhereAnyTest.php b/tests/lib/Acceptance/Filters/WhereAnyTest.php new file mode 100644 index 0000000..3669b28 --- /dev/null +++ b/tests/lib/Acceptance/Filters/WhereAnyTest.php @@ -0,0 +1,55 @@ +schema = $this->schemas()->schemaFor('posts'); + } + + /** + * @return void + */ + public function testWhereAny(): void + { + Post::factory()->count(5)->create(); + + $title = Post::factory()->create(['title' => "foobar boofar"]); + $content = Post::factory()->create(['content' => "boofar foobar"]); + $slug = Post::factory()->create(['slug' => "totally_foobar"]); + + $expected = [$title,$content, $slug]; + + $actual = $this->schema + ->repository() + ->queryAll() + ->filter(['any' => '%foobar%']) + ->get(); + + $this->assertFilteredModels($expected, $actual); + } +} From 18559d967caef4f8b439fd8feec1169dbc8ec38d Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Sun, 13 Oct 2024 12:52:07 +0100 Subject: [PATCH 2/4] refactor: tidy up where all and where any filters --- CHANGELOG.md | 4 +++ src/Filters/Concerns/HasColumns.php | 52 ++++++++++++++++++++--------- src/Filters/WhereAll.php | 15 +++------ src/Filters/WhereAny.php | 13 +++----- 4 files changed, 50 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8d6704..2daf1e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. This projec ## Unreleased +### Added + +- [#38](https://github.com/laravel-json-api/eloquent/pull/38) Added `WhereAll` and `WhereAny` filters. + ## [4.2.0] - 2024-08-26 ### Added diff --git a/src/Filters/Concerns/HasColumns.php b/src/Filters/Concerns/HasColumns.php index 95ec744..7cd536d 100644 --- a/src/Filters/Concerns/HasColumns.php +++ b/src/Filters/Concerns/HasColumns.php @@ -11,15 +11,15 @@ namespace LaravelJsonApi\Eloquent\Filters\Concerns; +use Illuminate\Database\Eloquent\Model; + trait HasColumns { - /** * @var string|null */ private ?string $table = null; - /** * @var array */ @@ -33,7 +33,13 @@ public function columns(): array return $this->columns; } - public function withColumn(string $column): self + /** + * Add a column to the filter. + * + * @param string $column + * @return $this + */ + public function withColumn(string $column): static { $this->columns[] = $column; @@ -41,28 +47,34 @@ public function withColumn(string $column): self } /** - * Force the table name when qualifying the columns. + * Add columns to the filter. * - * This allows the developer to force the table that the columns are qualified with. - * - * @param string $table + * @param string ...$columns * @return $this */ - public function qualifyAs(string $table): self + public function withColumns(string ...$columns): static { - $this->table = $table; + $this->columns = [ + ...$this->columns, + ...$columns, + ]; return $this; } /** - * Determine if developer has forced a table to qualify columns as + * Force the table name when qualifying the columns. + * + * This allows the developer to force the table that the columns are qualified with. * - * @return bool + * @param string $table + * @return $this */ - public function isQualified(): bool + public function qualifyAs(string $table): static { - return $this->table === null; + $this->table = $table; + + return $this; } /** @@ -70,10 +82,20 @@ public function isQualified(): bool * * @return array */ - protected function qualifiedColumns(): array + protected function qualifiedColumns(?Model $model = null): array { if ($this->table) { - return array_map(fn($column) => $this->table . '.' . $column, $this->columns); + return array_map( + fn($column) => $this->table . '.' . $column, + $this->columns, + ); + } + + if ($model) { + return array_map( + static fn($column) => $model->qualifyColumn($column), + $this->columns, + ); } return $this->columns; diff --git a/src/Filters/WhereAll.php b/src/Filters/WhereAll.php index c6864fb..9acf9a2 100644 --- a/src/Filters/WhereAll.php +++ b/src/Filters/WhereAll.php @@ -16,7 +16,6 @@ class WhereAll implements Filter { - use Concerns\DeserializesValue; use Concerns\HasColumns; use Concerns\HasOperator; @@ -32,19 +31,19 @@ class WhereAll implements Filter * Create a new filter. * * @param string $name - * @param array $columns + * @param array|null $columns * @return static */ - public static function make(string $name, array $columns = null): self + public static function make(string $name, array $columns = null): static { return new static($name, $columns); } /** - * WhereAny constructor. + * WhereAll constructor. * * @param string $name - * @param array $columns + * @param array|null $columns */ public function __construct(string $name, array $columns = null) { @@ -66,12 +65,8 @@ public function key(): string */ public function apply($query, $value) { - if (!$this->isQualified()){ - $this->qualifyAs($query->getModel()->getTable()); - } - return $query->whereAll( - $this->qualifiedColumns(), + $this->qualifiedColumns($query->getModel()), $this->operator(), $this->deserialize($value) ); diff --git a/src/Filters/WhereAny.php b/src/Filters/WhereAny.php index 43fa0b0..8a08c42 100644 --- a/src/Filters/WhereAny.php +++ b/src/Filters/WhereAny.php @@ -16,7 +16,6 @@ class WhereAny implements Filter { - use Concerns\DeserializesValue; use Concerns\HasColumns; use Concerns\HasOperator; @@ -32,10 +31,10 @@ class WhereAny implements Filter * Create a new filter. * * @param string $name - * @param array $columns + * @param array|null $columns * @return static */ - public static function make(string $name, array $columns = null): self + public static function make(string $name, array $columns = null): static { return new static($name, $columns); } @@ -44,7 +43,7 @@ public static function make(string $name, array $columns = null): self * WhereAny constructor. * * @param string $name - * @param array $columns + * @param array|null $columns */ public function __construct(string $name, array $columns = null) { @@ -66,12 +65,8 @@ public function key(): string */ public function apply($query, $value) { - if (!$this->isQualified()){ - $this->qualifyAs($query->getModel()->getTable()); - } - return $query->whereAny( - $this->qualifiedColumns(), + $this->qualifiedColumns($query->getModel()), $this->operator(), $this->deserialize($value) ); From 119ed645b386d24efc9e3612de47355f2fffd0ce Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Sun, 13 Oct 2024 13:41:50 +0100 Subject: [PATCH 3/4] fix: amend eager load iterator incorrectly filtering some paths Closes #39 --- CHANGELOG.md | 5 +++++ src/QueryBuilder/EagerLoading/EagerLoadIterator.php | 11 ++++++----- tests/app/Models/Mechanic.php | 9 +++++++++ tests/app/Schemas/MechanicSchema.php | 2 ++ tests/lib/Acceptance/EagerLoading/EagerLoaderTest.php | 5 +++++ 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2daf1e2..9d74704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ All notable changes to this project will be documented in this file. This projec - [#38](https://github.com/laravel-json-api/eloquent/pull/38) Added `WhereAll` and `WhereAny` filters. +### Fixed + +- [#39](https://github.com/laravel-json-api/eloquent/issues/39) Fixed a bug in the eager loader iterator where include + paths starting with the same word were incorrectly removed. E.g. `car` and `carOwner` would result in just `carOwner`. + ## [4.2.0] - 2024-08-26 ### Added diff --git a/src/QueryBuilder/EagerLoading/EagerLoadIterator.php b/src/QueryBuilder/EagerLoading/EagerLoadIterator.php index 0a3a3db..267049f 100644 --- a/src/QueryBuilder/EagerLoading/EagerLoadIterator.php +++ b/src/QueryBuilder/EagerLoading/EagerLoadIterator.php @@ -25,7 +25,6 @@ */ class EagerLoadIterator implements IteratorAggregate { - /** * @var Schema */ @@ -70,11 +69,13 @@ public function __construct(Schema $schema, IncludePaths $paths) */ public function collect(): Collection { - $values = collect($this); + $values = Collection::make($this); - return $values->reject( - fn($path) => $values->contains(fn($check) => $path !== $check && Str::startsWith($check, $path)) - )->sort()->values(); + return $values + ->reject(static fn(string $path) => $values + ->contains(fn(string $check) => $path !== $check && Str::startsWith($check, $path . '.'))) + ->sort() + ->values(); } /** diff --git a/tests/app/Models/Mechanic.php b/tests/app/Models/Mechanic.php index 563a202..c613ca6 100644 --- a/tests/app/Models/Mechanic.php +++ b/tests/app/Models/Mechanic.php @@ -13,6 +13,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOneThrough; class Mechanic extends Model @@ -25,6 +26,14 @@ class Mechanic extends Model */ protected $fillable = ['name']; + /** + * @return HasOne + */ + public function car(): HasOne + { + return $this->hasOne(Car::class); + } + /** * @return HasOneThrough */ diff --git a/tests/app/Schemas/MechanicSchema.php b/tests/app/Schemas/MechanicSchema.php index 052a1de..8eff7c9 100644 --- a/tests/app/Schemas/MechanicSchema.php +++ b/tests/app/Schemas/MechanicSchema.php @@ -15,6 +15,7 @@ use LaravelJsonApi\Eloquent\Contracts\Paginator; use LaravelJsonApi\Eloquent\Fields\DateTime; use LaravelJsonApi\Eloquent\Fields\ID; +use LaravelJsonApi\Eloquent\Fields\Relations\HasOne; use LaravelJsonApi\Eloquent\Fields\Relations\HasOneThrough; use LaravelJsonApi\Eloquent\Fields\Str; use LaravelJsonApi\Eloquent\Filters\WhereIdIn; @@ -39,6 +40,7 @@ public function fields(): array ID::make(), DateTime::make('createdAt')->readOnly(), Str::make('name'), + HasOne::make('car'), HasOneThrough::make('carOwner'), DateTime::make('updatedAt')->readOnly(), ]; diff --git a/tests/lib/Acceptance/EagerLoading/EagerLoaderTest.php b/tests/lib/Acceptance/EagerLoading/EagerLoaderTest.php index 5f3b406..2dfc33c 100644 --- a/tests/lib/Acceptance/EagerLoading/EagerLoaderTest.php +++ b/tests/lib/Acceptance/EagerLoading/EagerLoaderTest.php @@ -62,6 +62,11 @@ public static function includePathsProvider(): array 'profile', // auto included for users ], ], + 'mechanic' => [ + 'mechanics', + 'car,carOwner,carOwner.car,carOwner.car.mechanic,car.mechanic', + ['car.mechanic', 'carOwner.car.mechanic'], + ], ]; } From 8875021d1cc05e4c8a5c86e83266076e91b5a6b4 Mon Sep 17 00:00:00 2001 From: Christopher Gammie Date: Sun, 13 Oct 2024 13:52:55 +0100 Subject: [PATCH 4/4] docs: update changelog and bump version --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d74704..6227483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. This projec ## Unreleased +## [4.3.0] - 2024-10-13 + ### Added - [#38](https://github.com/laravel-json-api/eloquent/pull/38) Added `WhereAll` and `WhereAny` filters.