diff --git a/CHANGELOG.md b/CHANGELOG.md index e8d6704..6227483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ 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. + +### 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/Filters/Concerns/HasColumns.php b/src/Filters/Concerns/HasColumns.php new file mode 100644 index 0000000..7cd536d --- /dev/null +++ b/src/Filters/Concerns/HasColumns.php @@ -0,0 +1,103 @@ + + */ + private array $columns = []; + + /** + * @return array + */ + public function columns(): array + { + return $this->columns; + } + + /** + * Add a column to the filter. + * + * @param string $column + * @return $this + */ + public function withColumn(string $column): static + { + $this->columns[] = $column; + + return $this; + } + + /** + * Add columns to the filter. + * + * @param string ...$columns + * @return $this + */ + public function withColumns(string ...$columns): static + { + $this->columns = [ + ...$this->columns, + ...$columns, + ]; + + 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): static + { + $this->table = $table; + + return $this; + } + + /** + * Get qualified columns. + * + * @return array + */ + protected function qualifiedColumns(?Model $model = null): array + { + if ($this->table) { + 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 new file mode 100644 index 0000000..9acf9a2 --- /dev/null +++ b/src/Filters/WhereAll.php @@ -0,0 +1,74 @@ +|null $columns + * @return static + */ + public static function make(string $name, array $columns = null): static + { + return new static($name, $columns); + } + + /** + * WhereAll constructor. + * + * @param string $name + * @param array|null $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) + { + return $query->whereAll( + $this->qualifiedColumns($query->getModel()), + $this->operator(), + $this->deserialize($value) + ); + } +} diff --git a/src/Filters/WhereAny.php b/src/Filters/WhereAny.php new file mode 100644 index 0000000..8a08c42 --- /dev/null +++ b/src/Filters/WhereAny.php @@ -0,0 +1,74 @@ +|null $columns + * @return static + */ + public static function make(string $name, array $columns = null): static + { + return new static($name, $columns); + } + + /** + * WhereAny constructor. + * + * @param string $name + * @param array|null $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) + { + return $query->whereAny( + $this->qualifiedColumns($query->getModel()), + $this->operator(), + $this->deserialize($value) + ); + } +} 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/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/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'], + ], ]; } 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); + } +}