From ba4c2e15a2fc6d9033a04fda889bc3fddd1e315d Mon Sep 17 00:00:00 2001 From: Adrian Date: Fri, 15 Sep 2023 12:51:13 +0200 Subject: [PATCH 01/10] Added support for nested relation filtering --- src/AgGridExport.php | 2 +- src/AgGridQueryBuilder.php | 40 +++---- src/Support/ColumnMetadata.php | 110 ++++++++++++++++++ src/Support/RelationMetadata.php | 14 +++ tests/NestedRelationFilterTest.php | 34 ++++++ tests/NestedRelationJsonFilterTest.php | 44 +++++++ tests/TestCase.php | 11 +- tests/TestClasses/Factories/KeeperFactory.php | 2 + tests/TestClasses/Factories/ZooFactory.php | 21 ++++ tests/TestClasses/Models/Flamingo.php | 2 +- tests/TestClasses/Models/Keeper.php | 6 + tests/TestClasses/Models/Zoo.php | 25 ++++ 12 files changed, 288 insertions(+), 23 deletions(-) create mode 100644 src/Support/ColumnMetadata.php create mode 100644 src/Support/RelationMetadata.php create mode 100644 tests/NestedRelationFilterTest.php create mode 100644 tests/NestedRelationJsonFilterTest.php create mode 100644 tests/TestClasses/Factories/ZooFactory.php create mode 100644 tests/TestClasses/Models/Zoo.php diff --git a/src/AgGridExport.php b/src/AgGridExport.php index f364c0d..1842491 100644 --- a/src/AgGridExport.php +++ b/src/AgGridExport.php @@ -15,7 +15,7 @@ use Maatwebsite\Excel\Concerns\WithMapping; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; -class AgGridExport implements FromQuery, WithHeadings, WithColumnFormatting, ShouldAutoSize, WithMapping +class AgGridExport implements FromQuery, ShouldAutoSize, WithColumnFormatting, WithHeadings, WithMapping { /** * @var Collection diff --git a/src/AgGridQueryBuilder.php b/src/AgGridQueryBuilder.php index 4bdeb07..40893f5 100644 --- a/src/AgGridQueryBuilder.php +++ b/src/AgGridQueryBuilder.php @@ -10,6 +10,7 @@ use Clickbar\AgGrid\Enums\AgGridRowModel; use Clickbar\AgGrid\Enums\AgGridTextFilterType; use Clickbar\AgGrid\Requests\AgGridGetRowsRequest; +use Clickbar\AgGrid\Support\ColumnMetadata; use Illuminate\Contracts\Support\Responsable; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Model; @@ -17,7 +18,6 @@ use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\ResourceCollection; -use Illuminate\Support\Str; use Illuminate\Support\Traits\ForwardsCalls; use Maatwebsite\Excel\Facades\Excel; @@ -201,14 +201,14 @@ protected function addFiltersToQuery(): void foreach ($filters as $column => $filter) { - [$relation, $column] = $this->getRelation($column); + $columnInformation = ColumnMetadata::fromString($this->subject, $column); - if ($relation !== null) { - $this->subject->whereHas($relation, function (EloquentBuilder $builder) use ($column, $filter) { - $this->addFilterToQuery($builder, $column, $filter); + if ($columnInformation->hasRelations()) { + $this->subject->whereHas($columnInformation->getDottedRelation(), function (EloquentBuilder $builder) use ($columnInformation, $filter) { + $this->addFilterToQuery($builder, $columnInformation, $filter); }); } else { - $this->addFilterToQuery($this->subject, $column, $filter); + $this->addFilterToQuery($this->subject, $columnInformation, $filter); } } } @@ -246,21 +246,21 @@ protected function addLimitAndOffsetToQuery(): void $this->subject->offset($startRow)->limit($endRow - $startRow); } - protected function addFilterToQuery(EloquentBuilder|Relation $subject, string $column, array $filter): void + protected function addFilterToQuery(EloquentBuilder|Relation $subject, ColumnMetadata $columnInformation, array $filter): void { $filterType = AgGridFilterType::from($filter['filterType']); match ($filterType) { - AgGridFilterType::Set => $this->addSetFilterToQuery($subject, $column, $filter), - AgGridFilterType::Text => $this->addTextFilterToQuery($subject, $column, $filter), - AgGridFilterType::Number => $this->addNumberFilterToQuery($subject, $column, $filter), - AgGridFilterType::Date => $this->addDateFilterToQuery($subject, $column, $filter), + AgGridFilterType::Set => $this->addSetFilterToQuery($subject, $columnInformation, $filter), + AgGridFilterType::Text => $this->addTextFilterToQuery($subject, $columnInformation, $filter), + AgGridFilterType::Number => $this->addNumberFilterToQuery($subject, $columnInformation, $filter), + AgGridFilterType::Date => $this->addDateFilterToQuery($subject, $columnInformation, $filter), }; } - protected function addSetFilterToQuery(EloquentBuilder|Relation $subject, string $column, array $filter): void + protected function addSetFilterToQuery(EloquentBuilder|Relation $subject, ColumnMetadata $columnInformation, array $filter): void { - $isJsonColumn = $this->isJsonColumn($column); - $column = $this->toJsonPath($column); + $isJsonColumn = $columnInformation->isJsonColumn(); + $column = $columnInformation->getColumnAsJsonPath(); $values = $filter['values']; $all = $filter['all'] ?? false; $filteredValues = array_filter($values, fn ($value) => $value !== null); @@ -284,9 +284,9 @@ protected function addSetFilterToQuery(EloquentBuilder|Relation $subject, string }); } - protected function addTextFilterToQuery(EloquentBuilder|Relation $subject, string $column, array $filter): void + protected function addTextFilterToQuery(EloquentBuilder|Relation $subject, ColumnMetadata $columnInformation, array $filter): void { - $column = $this->toJsonPath($column); + $column = $columnInformation->getColumnAsJsonPath(); $value = $filter['filter'] ?? null; $type = AgGridTextFilterType::from($filter['type']); @@ -302,9 +302,9 @@ protected function addTextFilterToQuery(EloquentBuilder|Relation $subject, strin }; } - protected function addNumberFilterToQuery(EloquentBuilder|Relation $subject, string $column, array $filter): void + protected function addNumberFilterToQuery(EloquentBuilder|Relation $subject, ColumnMetadata $columnInformation, array $filter): void { - $column = $this->toJsonPath($column); + $column = $columnInformation->getColumnAsJsonPath(); $value = $filter['filter']; $type = AgGridNumberFilterType::from($filter['type']); @@ -321,9 +321,9 @@ protected function addNumberFilterToQuery(EloquentBuilder|Relation $subject, str }; } - protected function addDateFilterToQuery(EloquentBuilder|Relation $subject, string $column, array $filter): void + protected function addDateFilterToQuery(EloquentBuilder|Relation $subject, ColumnMetadata $columnInformation, array $filter): void { - $column = $this->toJsonPath($column); + $column = $columnInformation->getColumnAsJsonPath(); $dateFrom = isset($filter['dateFrom']) ? new \DateTime($filter['dateFrom']) : null; $dateTo = isset($filter['dateTo']) ? new \DateTime($filter['dateTo']) : null; diff --git a/src/Support/ColumnMetadata.php b/src/Support/ColumnMetadata.php new file mode 100644 index 0000000..11b95f4 --- /dev/null +++ b/src/Support/ColumnMetadata.php @@ -0,0 +1,110 @@ +explode('.'); + + if ($parts->count() === 1) { + // --> No nested information + return new self($subject->getModel(), [], $column); + } + + $relations = []; + $model = $subject->getModel(); + + foreach ($parts as $index => $part) { + + $relationName = Str::camel($part); + $modelRelations = self::getRelations($model::class); + + if ($modelRelations->contains($relationName)) { + $relation = $model->$relationName(); + $model = $relation->getModel(); + + $relations[] = new RelationMetadata($relationName, $model); + } else { + // --> End of relation (further dots must be json nesting) + $remaining = $parts->skip($index + 1)->implode('.'); + if (! empty($remaining)) { + $remaining = '.'.$remaining; + } + $column = $part.$remaining; + break; + } + } + + return new self($subject->getModel(), $relations, $column); + + } + + protected static function getRelations(string $modelClass): Collection + { + return collect((new ReflectionClass($modelClass))->getMethods(ReflectionMethod::IS_PUBLIC)) + ->filter(function (ReflectionMethod $reflectionMethod) { + $returnType = (string) $reflectionMethod->getReturnType(); + + return $returnType != null && is_subclass_of($returnType, Relation::class); + }) + ->map(fn (ReflectionMethod $reflectionMethod) => $reflectionMethod->getName()); + } + + public function hasRelations(): bool + { + return ! empty($this->relations); + } + + public function getDottedRelation(): string + { + return collect($this->relations) + ->implode('name', '.'); + } + + public function getColumn(): string + { + return $this->column; + } + + public function isJsonColumn(): bool + { + $model = collect($this->relations)->last()?->model ?? $this->baseModel; + $colum = Str::before($this->column, '.'); + + return str_contains($this->column, '.') || $model->hasCast($colum, [ + 'array', + 'json', + 'object', + 'collection', + 'encrypted:array', + 'encrypted:collection', + 'encrypted:json', + 'encrypted:object', + ]); + + } + + public function getColumnAsJsonPath(): string + { + return str_replace('.', '->', $this->column); + } +} diff --git a/src/Support/RelationMetadata.php b/src/Support/RelationMetadata.php new file mode 100644 index 0000000..2ad4023 --- /dev/null +++ b/src/Support/RelationMetadata.php @@ -0,0 +1,14 @@ +zooNamedVivarium = Zoo::factory()->state(['name' => 'Vivarium'])->createOne(); + $this->zooNamedOpelZoo = Zoo::factory()->state(['name' => 'Opel-Zoo'])->createOne(); + + $this->keeperNamedJohn = Keeper::factory()->for($this->zooNamedVivarium)->state(['name' => 'John'])->createOne(); + $this->keeperNamedOliver = Keeper::factory()->for($this->zooNamedOpelZoo)->state(['name' => 'Oliver'])->createOne(); + + $this->johnsFlamingos = Flamingo::factory()->count(2)->for($this->keeperNamedJohn)->create(); + $this->oliversFlamingos = Flamingo::factory()->count(3)->for($this->keeperNamedOliver)->create(); +}); + +it('handles filters on nested relations correctly', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'filterModel' => [ + 'keeper.zoo.name' => [ + 'filterType' => 'text', + 'type' => 'equals', + 'filter' => 'Vivarium', + ], + ], + ], + Flamingo::class, + ); + + expect($queryBuilder->get()->count())->toBe($this->johnsFlamingos->count()); +}); diff --git a/tests/NestedRelationJsonFilterTest.php b/tests/NestedRelationJsonFilterTest.php new file mode 100644 index 0000000..3580d25 --- /dev/null +++ b/tests/NestedRelationJsonFilterTest.php @@ -0,0 +1,44 @@ +zooNamedVivarium = Zoo::factory()->state(['name' => 'Vivarium', 'address' => [ + 'street' => 'Schnampelweg', + 'house_number' => '5', + 'postcode' => '64287', + 'city' => 'Darmstadt', + ]])->createOne(); + $this->zooNamedOpelZoo = Zoo::factory()->state(['name' => 'Opel-Zoo', 'address' => [ + 'street' => 'Am Opelzoo', + 'house_number' => '3', + 'postcode' => '61476', + 'city' => 'Kronberg im Taunus', + ]])->createOne(); + + $this->keeperNamedJohn = Keeper::factory()->for($this->zooNamedVivarium)->state(['name' => 'John'])->createOne(); + $this->keeperNamedOliver = Keeper::factory()->for($this->zooNamedOpelZoo)->state(['name' => 'Oliver'])->createOne(); + + $this->johnsFlamingos = Flamingo::factory()->count(2)->for($this->keeperNamedJohn)->create(); + $this->oliversFlamingos = Flamingo::factory()->count(3)->for($this->keeperNamedOliver)->create(); +}); + +it('handles filters on nested relations with json field correctly', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'filterModel' => [ + 'keeper.zoo.address.street' => [ + 'filterType' => 'text', + 'type' => 'equals', + 'filter' => 'Schnampelweg', + ], + ], + ], + Flamingo::class, + ); + + expect($queryBuilder->get()->count())->toBe($this->johnsFlamingos->count()); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index 931f0e6..7e04b1e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -38,9 +38,17 @@ protected function setUp(): void /** @var DatabaseManager $db */ $db = $this->app->get('db'); + $db->connection()->getSchemaBuilder()->create('zoos', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->jsonb('address'); + $table->timestamps(); + }); + $db->connection()->getSchemaBuilder()->create('keepers', function (Blueprint $table) { $table->id(); $table->string('name'); + $table->foreignId('zoo_id')->index()->constrained(); $table->timestamps(); }); @@ -54,7 +62,8 @@ protected function setUp(): void $table->date('last_vaccinated_on')->nullable(); $table->boolean('is_hungry')->default(false); $table->softDeletes(); - $table->foreignId('keeper_id')->constrained(); + $table->foreignId('keeper_id')->index()->constrained(); }); + } } diff --git a/tests/TestClasses/Factories/KeeperFactory.php b/tests/TestClasses/Factories/KeeperFactory.php index 04fbdbe..6e15b3d 100644 --- a/tests/TestClasses/Factories/KeeperFactory.php +++ b/tests/TestClasses/Factories/KeeperFactory.php @@ -2,6 +2,7 @@ namespace Clickbar\AgGrid\Tests\TestClasses\Factories; +use Clickbar\AgGrid\Tests\TestClasses\Models\Zoo; use Illuminate\Database\Eloquent\Factories\Factory; class KeeperFactory extends Factory @@ -10,6 +11,7 @@ public function definition(): array { return [ 'name' => $this->faker->name(), + 'zoo_id' => Zoo::factory(), ]; } } diff --git a/tests/TestClasses/Factories/ZooFactory.php b/tests/TestClasses/Factories/ZooFactory.php new file mode 100644 index 0000000..6558517 --- /dev/null +++ b/tests/TestClasses/Factories/ZooFactory.php @@ -0,0 +1,21 @@ + $this->faker->name(), + 'address' => [ + 'street' => $this->faker->streetName(), + 'house_number' => $this->faker->buildingNumber(), + 'postcode' => $this->faker->postcode(), + 'city' => $this->faker->city(), + ], + ]; + } +} diff --git a/tests/TestClasses/Models/Flamingo.php b/tests/TestClasses/Models/Flamingo.php index f835a31..de33085 100644 --- a/tests/TestClasses/Models/Flamingo.php +++ b/tests/TestClasses/Models/Flamingo.php @@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; -class Flamingo extends Model implements AgGridExportable, AgGridCustomFilterable +class Flamingo extends Model implements AgGridCustomFilterable, AgGridExportable { use HasFactory; use SoftDeletes; diff --git a/tests/TestClasses/Models/Keeper.php b/tests/TestClasses/Models/Keeper.php index ac38fb7..cfa29b7 100644 --- a/tests/TestClasses/Models/Keeper.php +++ b/tests/TestClasses/Models/Keeper.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; class Keeper extends Model @@ -21,4 +22,9 @@ public function flamingos(): HasMany { return $this->hasMany(Flamingo::class); } + + public function zoo(): BelongsTo + { + return $this->belongsTo(Zoo::class); + } } diff --git a/tests/TestClasses/Models/Zoo.php b/tests/TestClasses/Models/Zoo.php new file mode 100644 index 0000000..f9574bc --- /dev/null +++ b/tests/TestClasses/Models/Zoo.php @@ -0,0 +1,25 @@ + 'immutable_datetime', + 'updated_at' => 'immutable_datetime', + 'address' => 'array', + ]; + + public function keepers(): HasMany + { + return $this->hasMany(Keeper::class); + } +} From 8b3be9b76440f0256d4ab42c4707d9ea5f0d3ef2 Mon Sep 17 00:00:00 2001 From: Adrian Date: Fri, 15 Sep 2023 12:53:04 +0200 Subject: [PATCH 02/10] Updated change log --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd34f65..7b7bf04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to `ag-grid-laravel` will be documented in this file. +## Unreleased + +### Improved + +- Added support for nested relations in filters + ## 0.2.0 (2023-08-24) - Rename `$params` to `$filters` in `AgGridCustomFilterable` From bcfd427459639ac5422b23fe671adf71086304d8 Mon Sep 17 00:00:00 2001 From: Adrian Date: Fri, 15 Sep 2023 12:59:31 +0200 Subject: [PATCH 03/10] Removed unused code --- src/AgGridQueryBuilder.php | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/src/AgGridQueryBuilder.php b/src/AgGridQueryBuilder.php index 40893f5..cbeb84a 100644 --- a/src/AgGridQueryBuilder.php +++ b/src/AgGridQueryBuilder.php @@ -338,35 +338,6 @@ protected function addDateFilterToQuery(EloquentBuilder|Relation $subject, Colum }; } - protected function getRelation(string $column): array - { - $pos = strpos($column, '.'); - if ($pos === false) { - return [null, $column]; - } - // guess the name of the relation - $relationName = Str::camel(substr($column, 0, $pos)); - if ($this->subject->getModel()->isRelation($relationName)) { - return [$relationName, substr($column, $pos + 1)]; - } - - return [null, $column]; - } - - protected function isJsonColumn(string $column): bool - { - return str_contains($column, '.') || $this->subject->getModel()->hasCast($column, [ - 'array', - 'json', - 'object', - 'collection', - 'encrypted:array', - 'encrypted:collection', - 'encrypted:json', - 'encrypted:object', - ]); - } - protected function toJsonPath(string $key): string { return str_replace('.', '->', $key); From 1092372e90745ddf00f4dda12a6e29e0ac996b7a Mon Sep 17 00:00:00 2001 From: Adrian Date: Fri, 15 Sep 2023 13:01:17 +0200 Subject: [PATCH 04/10] Updated limitation section in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d411c9..53fddc9 100644 --- a/README.md +++ b/README.md @@ -286,7 +286,7 @@ function onFilterChanged(event: FilterChangedEvent) { - Only works with PostgreSQL as a storage backend due to some special SQL operators being used in set and json queries. - Does not support multiple conditions per filter (AND, OR) - Does not support server-side grouping for AG Grid's pivot mode -- Filtering for values in relations is only supported one level deep. E.g you can filter for `relation.value` but not `relation.otherRelation.value` +- ~~Filtering for values in relations is only supported one level deep. E.g you can filter for `relation.value` but not `relation.otherRelation.value`~~ ## TODOs From 69751e23727ce2258577c1321f85a5f7be9c384f Mon Sep 17 00:00:00 2001 From: Adrian Date: Fri, 15 Sep 2023 16:36:30 +0200 Subject: [PATCH 05/10] Added Handling of set values --- src/AgGridQueryBuilder.php | 72 +++++++++++++ .../ViewManipulationNotAllowedException.php | 16 +++ src/Model/AgGridViewModel.php | 100 ++++++++++++++++++ src/Requests/AgGridSetValuesRequest.php | 16 +++ 4 files changed, 204 insertions(+) create mode 100644 src/Exceptions/ViewManipulationNotAllowedException.php create mode 100644 src/Model/AgGridViewModel.php create mode 100644 src/Requests/AgGridSetValuesRequest.php diff --git a/src/AgGridQueryBuilder.php b/src/AgGridQueryBuilder.php index cbeb84a..f2a6714 100644 --- a/src/AgGridQueryBuilder.php +++ b/src/AgGridQueryBuilder.php @@ -10,6 +10,7 @@ use Clickbar\AgGrid\Enums\AgGridRowModel; use Clickbar\AgGrid\Enums\AgGridTextFilterType; use Clickbar\AgGrid\Requests\AgGridGetRowsRequest; +use Clickbar\AgGrid\Requests\AgGridSetValuesRequest; use Clickbar\AgGrid\Support\ColumnMetadata; use Illuminate\Contracts\Support\Responsable; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; @@ -18,6 +19,8 @@ use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\ResourceCollection; +use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Illuminate\Support\Traits\ForwardsCalls; use Maatwebsite\Excel\Facades\Excel; @@ -68,6 +71,16 @@ public static function forRequest(AgGridGetRowsRequest $request, EloquentBuilder return new AgGridQueryBuilder($request->validated(), $subject); } + /** + * Returns a new AgGridQueryBuilder for an AgGridGetRowsRequest. + * + * @param EloquentBuilder|Relation|Model|class-string $subject + */ + public static function forSetValuesRequest(AgGridSetValuesRequest $request, EloquentBuilder|Relation|Model|string $subject): AgGridQueryBuilder + { + return new AgGridQueryBuilder($request->validated(), $subject); + } + /** * Returns a new AgGridQueryBuilder for a selection. * @@ -102,6 +115,40 @@ public function resource(string $resourceClass): self return $this; } + public function toSetValues(array $allowedColumns = []): Collection + { + $column = Arr::get($this->params, 'column'); + if ($column == null) { + throw new \Exception("To SetValues can only be called from AFFridSetValueRequest or when params contains 'column'"); + } + + if (collect($allowedColumns)->first() !== '*' && ! in_array($column, $allowedColumns)) { + throw new \Exception("Set value for column $column is not available or cannot be accessed"); + } + + $columnMetadata = ColumnMetadata::fromString($this->subject, $column); + + if ($columnMetadata->hasRelations()) { + + $dottedRelation = $columnMetadata->getDottedRelation(); + + return $this->subject->with($dottedRelation) + ->get() + ->map(fn (Model $model) => Arr::get($this->traverse($model, $dottedRelation)->toArray(), $columnMetadata->getColumn())) + ->unique() + ->values() + ->sort(); + } + + $column = $columnMetadata->isJsonColumn() ? $columnMetadata->getColumnAsJsonPath() : $columnMetadata->getColumn(); + + return $this->subject + ->select($column) + ->distinct() + ->orderBy($column) + ->pluck($column); + } + public function __call($name, $arguments) { $result = $this->forwardCallTo($this->subject, $name, $arguments); @@ -342,4 +389,29 @@ protected function toJsonPath(string $key): string { return str_replace('.', '->', $key); } + + protected function traverse($model, $key, $default = null): Model + { + if (is_array($model)) { + return Arr::get($model, $key, $default); + } + + if (is_null($key)) { + return $model; + } + + if (isset($model[$key])) { + return $model[$key]; + } + + foreach (explode('.', $key) as $segment) { + try { + $model = $model->$segment; + } catch (\Exception $e) { // @phpstan-ignore-line + return value($default); + } + } + + return $model; + } } diff --git a/src/Exceptions/ViewManipulationNotAllowedException.php b/src/Exceptions/ViewManipulationNotAllowedException.php new file mode 100644 index 0000000..f8b2ac1 --- /dev/null +++ b/src/Exceptions/ViewManipulationNotAllowedException.php @@ -0,0 +1,16 @@ + ['required', 'string'], + 'filterModel' => ['sometimes', 'array'], + ]; + } +} From d37a0d07bfa702247037fe4b7c2e4bc035a7363f Mon Sep 17 00:00:00 2001 From: Adrian Date: Fri, 15 Sep 2023 16:38:15 +0200 Subject: [PATCH 06/10] Added todos --- src/AgGridQueryBuilder.php | 2 ++ tests/SetValuesTest.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 tests/SetValuesTest.php diff --git a/src/AgGridQueryBuilder.php b/src/AgGridQueryBuilder.php index f2a6714..c8a6fdc 100644 --- a/src/AgGridQueryBuilder.php +++ b/src/AgGridQueryBuilder.php @@ -119,10 +119,12 @@ public function toSetValues(array $allowedColumns = []): Collection { $column = Arr::get($this->params, 'column'); if ($column == null) { + // TODO: Consider custom exception throw new \Exception("To SetValues can only be called from AFFridSetValueRequest or when params contains 'column'"); } if (collect($allowedColumns)->first() !== '*' && ! in_array($column, $allowedColumns)) { + // TODO: Consider custom exception throw new \Exception("Set value for column $column is not available or cannot be accessed"); } diff --git a/tests/SetValuesTest.php b/tests/SetValuesTest.php new file mode 100644 index 0000000..245682f --- /dev/null +++ b/tests/SetValuesTest.php @@ -0,0 +1,28 @@ +zooNamedVivarium = Zoo::factory()->state(['name' => 'Vivarium', 'address' => [ + 'street' => 'Schnampelweg', + 'house_number' => '5', + 'postcode' => '64287', + 'city' => 'Darmstadt', + ]])->createOne(); + $this->zooNamedOpelZoo = Zoo::factory()->state(['name' => 'Opel-Zoo', 'address' => [ + 'street' => 'Am Opelzoo', + 'house_number' => '3', + 'postcode' => '61476', + 'city' => 'Kronberg im Taunus', + ]])->createOne(); + + $this->keeperNamedJohn = Keeper::factory()->for($this->zooNamedVivarium)->state(['name' => 'John'])->createOne(); + $this->keeperNamedOliver = Keeper::factory()->for($this->zooNamedOpelZoo)->state(['name' => 'Oliver'])->createOne(); + + $this->johnsFlamingos = Flamingo::factory()->count(2)->for($this->keeperNamedJohn)->create(); + $this->oliversFlamingos = Flamingo::factory()->count(3)->for($this->keeperNamedOliver)->create(); +}); + +// TODO: Add tests From d6d8add39ee8175ab5d5e9ccfd8c73cbd97b93d8 Mon Sep 17 00:00:00 2001 From: Adrian Date: Tue, 19 Sep 2023 11:55:37 +0200 Subject: [PATCH 07/10] Added check for own column filter when retrieving the set values for a specific column --- src/AgGridQueryBuilder.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/AgGridQueryBuilder.php b/src/AgGridQueryBuilder.php index c8a6fdc..ec23fd1 100644 --- a/src/AgGridQueryBuilder.php +++ b/src/AgGridQueryBuilder.php @@ -248,6 +248,12 @@ protected function addFiltersToQuery(): void $filters = collect($this->params['filterModel']); + // Check if we are in set values mode and exclude the filter for the given set value column + $column = Arr::get($this->params, 'column'); + if ($column) { + $filters = $filters->filter(fn ($value, $key) => $key !== $column); + } + foreach ($filters as $column => $filter) { $columnInformation = ColumnMetadata::fromString($this->subject, $column); From c018ddadc6b6b4fffb4c6c4d61e77b17e81af4de Mon Sep 17 00:00:00 2001 From: Adrian Date: Tue, 19 Sep 2023 12:29:35 +0200 Subject: [PATCH 08/10] Added tests for AgGrid SetValue functionality Added own exceptions for error case while trying to retrieve set value data --- src/AgGridQueryBuilder.php | 14 +- src/Exceptions/InvalidSetValueOperation.php | 13 ++ .../UnauthorizedSetFilterColumn.php | 18 ++ tests/SetValuesTest.php | 154 +++++++++++++++++- 4 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 src/Exceptions/InvalidSetValueOperation.php create mode 100644 src/Exceptions/UnauthorizedSetFilterColumn.php diff --git a/src/AgGridQueryBuilder.php b/src/AgGridQueryBuilder.php index ec23fd1..c4a9653 100644 --- a/src/AgGridQueryBuilder.php +++ b/src/AgGridQueryBuilder.php @@ -9,6 +9,8 @@ use Clickbar\AgGrid\Enums\AgGridNumberFilterType; use Clickbar\AgGrid\Enums\AgGridRowModel; use Clickbar\AgGrid\Enums\AgGridTextFilterType; +use Clickbar\AgGrid\Exceptions\InvalidSetValueOperation; +use Clickbar\AgGrid\Exceptions\UnauthorizedSetFilterColumn; use Clickbar\AgGrid\Requests\AgGridGetRowsRequest; use Clickbar\AgGrid\Requests\AgGridSetValuesRequest; use Clickbar\AgGrid\Support\ColumnMetadata; @@ -118,14 +120,12 @@ public function resource(string $resourceClass): self public function toSetValues(array $allowedColumns = []): Collection { $column = Arr::get($this->params, 'column'); - if ($column == null) { - // TODO: Consider custom exception - throw new \Exception("To SetValues can only be called from AFFridSetValueRequest or when params contains 'column'"); + if (empty($column)) { + throw InvalidSetValueOperation::make(); } if (collect($allowedColumns)->first() !== '*' && ! in_array($column, $allowedColumns)) { - // TODO: Consider custom exception - throw new \Exception("Set value for column $column is not available or cannot be accessed"); + throw UnauthorizedSetFilterColumn::make($column); } $columnMetadata = ColumnMetadata::fromString($this->subject, $column); @@ -138,8 +138,8 @@ public function toSetValues(array $allowedColumns = []): Collection ->get() ->map(fn (Model $model) => Arr::get($this->traverse($model, $dottedRelation)->toArray(), $columnMetadata->getColumn())) ->unique() - ->values() - ->sort(); + ->sort() + ->values(); } $column = $columnMetadata->isJsonColumn() ? $columnMetadata->getColumnAsJsonPath() : $columnMetadata->getColumn(); diff --git a/src/Exceptions/InvalidSetValueOperation.php b/src/Exceptions/InvalidSetValueOperation.php new file mode 100644 index 0000000..b30f02b --- /dev/null +++ b/src/Exceptions/InvalidSetValueOperation.php @@ -0,0 +1,13 @@ +oliversFlamingos = Flamingo::factory()->count(3)->for($this->keeperNamedOliver)->create(); }); -// TODO: Add tests +it('can retrieve set filter values for a regular column', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => 'name', + ], + Flamingo::class, + ); + + $names = + $this->johnsFlamingos->pluck('name') + ->concat($this->oliversFlamingos->pluck('name')) + ->unique() + ->sort() + ->values(); + + expect($queryBuilder->toSetValues(['*'])->toArray())->toMatchArray($names->toArray()); +})->only(); + +it('can retrieve set filter values for a related column', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => 'keeper.name', + ], + Flamingo::class, + ); + + $names = + $this->johnsFlamingos->map(fn (Flamingo $flamingo) => $flamingo->keeper->name) + ->concat($this->oliversFlamingos->map(fn (Flamingo $flamingo) => $flamingo->keeper->name)) + ->unique() + ->sort() + ->values(); + + expect($queryBuilder->toSetValues(['*'])->toArray())->toMatchArray($names->toArray()); +})->only(); + +it('can retrieve set filter values for a nested related column', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => 'keeper.zoo.name', + ], + Flamingo::class, + ); + + $names = + $this->johnsFlamingos->map(fn (Flamingo $flamingo) => $flamingo->keeper->zoo->name) + ->concat($this->oliversFlamingos->map(fn (Flamingo $flamingo) => $flamingo->keeper->zoo->name)) + ->unique() + ->sort() + ->values(); + + expect($queryBuilder->toSetValues(['*'])->toArray())->toMatchArray($names->toArray()); +})->only(); + +it('can retrieve set filter values for a nested related json column field', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => 'keeper.zoo.address.street', + ], + Flamingo::class, + ); + + $names = + $this->johnsFlamingos->map(fn (Flamingo $flamingo) => $flamingo->keeper->zoo->address['street']) + ->concat($this->oliversFlamingos->map(fn (Flamingo $flamingo) => $flamingo->keeper->zoo->address['street'])) + ->unique() + ->sort() + ->values(); + + expect($queryBuilder->toSetValues(['*'])->toArray())->toMatchArray($names->toArray()); +})->only(); + +it('throws exception when trying to retrieve set filter values without wildcard', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => 'name', + ], + Flamingo::class, + ); + + $queryBuilder->toSetValues(); +})->throws(UnauthorizedSetFilterColumn::class)->only(); + +it('throws exception when trying to retrieve set filter values with wrong allowed column name', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => 'name', + ], + Flamingo::class, + ); + + $queryBuilder->toSetValues(['flamingo_name']); +})->throws(UnauthorizedSetFilterColumn::class)->only(); + +it('applies filters when retrieving set filter values', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => 'name', + 'filterModel' => [ + 'keeper.name' => [ + 'filterType' => 'text', + 'type' => 'equals', + 'filter' => 'John', + ], + ], + ], + Flamingo::class, + ); + + $names = + $this->johnsFlamingos->pluck('name') + ->unique() + ->sort() + ->values(); + + expect($queryBuilder->toSetValues(['*'])->toArray())->toMatchArray($names->toArray()); +})->only(); + +it('throws exception when trying to retrieve set filter values without the column in params', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column2' => 'name', + ], + Flamingo::class, + ); + + $queryBuilder->toSetValues(['flamingo_name']); +})->throws(InvalidSetValueOperation::class)->only(); + +it('throws exception when trying to retrieve set filter with empty column in params', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => '', + ], + Flamingo::class, + ); + + $queryBuilder->toSetValues(['flamingo_name']); +})->throws(InvalidSetValueOperation::class)->only(); + +it('throws exception when trying to retrieve set filter with null column in params', function () { + $queryBuilder = new AgGridQueryBuilder( + [ + 'column' => null, + ], + Flamingo::class, + ); + + $queryBuilder->toSetValues(['flamingo_name']); +})->throws(InvalidSetValueOperation::class)->only(); From e49eee60630bcd2b13248c25eef5a502f4304eb4 Mon Sep 17 00:00:00 2001 From: Adrian Date: Mon, 25 Sep 2023 16:19:55 +0200 Subject: [PATCH 09/10] Added row group stuff --- src/AgGridQueryBuilder.php | 93 ++++++++++++++++++++------- src/Requests/AgGridGetRowsRequest.php | 3 + src/Support/RowGroupMetadata.php | 86 +++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 22 deletions(-) create mode 100644 src/Support/RowGroupMetadata.php diff --git a/src/AgGridQueryBuilder.php b/src/AgGridQueryBuilder.php index c4a9653..c9fa136 100644 --- a/src/AgGridQueryBuilder.php +++ b/src/AgGridQueryBuilder.php @@ -14,6 +14,7 @@ use Clickbar\AgGrid\Requests\AgGridGetRowsRequest; use Clickbar\AgGrid\Requests\AgGridSetValuesRequest; use Clickbar\AgGrid\Support\ColumnMetadata; +use Clickbar\AgGrid\Support\RowGroupMetadata; use Illuminate\Contracts\Support\Responsable; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Model; @@ -23,6 +24,7 @@ use Illuminate\Http\Resources\Json\ResourceCollection; use Illuminate\Support\Arr; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Traits\ForwardsCalls; use Maatwebsite\Excel\Facades\Excel; @@ -40,8 +42,10 @@ class AgGridQueryBuilder implements Responsable /** @var class-string | null */ protected ?string $resourceClass = null; + protected RowGroupMetadata $rowGroupMetadata; + /** - * @param EloquentBuilder|Relation|Model|class-string $subject + * @param EloquentBuilder|Relation|Model|class-string $subject */ public function __construct(array $params, EloquentBuilder|Relation|Model|string $subject) { @@ -57,16 +61,19 @@ public function __construct(array $params, EloquentBuilder|Relation|Model|string $model->applyAgGridCustomFilters($this->subject, $this->params['customFilters'] ?? []); } + $this->rowGroupMetadata = RowGroupMetadata::fromParams($params); + $this->addFiltersToQuery(); $this->addToggledFilterToQuery(); $this->addSortsToQuery(); $this->addLimitAndOffsetToQuery(); + $this->addRowGrouping(); } /** * Returns a new AgGridQueryBuilder for an AgGridGetRowsRequest. * - * @param EloquentBuilder|Relation|Model|class-string $subject + * @param EloquentBuilder|Relation|Model|class-string $subject */ public static function forRequest(AgGridGetRowsRequest $request, EloquentBuilder|Relation|Model|string $subject): AgGridQueryBuilder { @@ -76,7 +83,7 @@ public static function forRequest(AgGridGetRowsRequest $request, EloquentBuilder /** * Returns a new AgGridQueryBuilder for an AgGridGetRowsRequest. * - * @param EloquentBuilder|Relation|Model|class-string $subject + * @param EloquentBuilder|Relation|Model|class-string $subject */ public static function forSetValuesRequest(AgGridSetValuesRequest $request, EloquentBuilder|Relation|Model|string $subject): AgGridQueryBuilder { @@ -86,7 +93,7 @@ public static function forSetValuesRequest(AgGridSetValuesRequest $request, Eloq /** * Returns a new AgGridQueryBuilder for a selection. * - * @param EloquentBuilder|Relation|Model|class-string $subject + * @param EloquentBuilder|Relation|Model|class-string $subject */ public static function forSelection(array $selection, EloquentBuilder|Relation|Model|string $subject): AgGridQueryBuilder { @@ -108,7 +115,7 @@ public function getSubject(): Relation|EloquentBuilder } /** - * @param class-string $resourceClass + * @param class-string $resourceClass */ public function resource(string $resourceClass): self { @@ -124,7 +131,7 @@ public function toSetValues(array $allowedColumns = []): Collection throw InvalidSetValueOperation::make(); } - if (collect($allowedColumns)->first() !== '*' && ! in_array($column, $allowedColumns)) { + if (collect($allowedColumns)->first() !== '*' && !in_array($column, $allowedColumns)) { throw UnauthorizedSetFilterColumn::make($column); } @@ -136,7 +143,7 @@ public function toSetValues(array $allowedColumns = []): Collection return $this->subject->with($dottedRelation) ->get() - ->map(fn (Model $model) => Arr::get($this->traverse($model, $dottedRelation)->toArray(), $columnMetadata->getColumn())) + ->map(fn(Model $model) => Arr::get($this->traverse($model, $dottedRelation)->toArray(), $columnMetadata->getColumn())) ->unique() ->sort() ->values(); @@ -179,7 +186,7 @@ public function toResponse($request): mixed return Excel::download( new AgGridExport($this->subject, $this->params['exportColumns'] ?? null), - 'export.'.strtolower($writerType), + 'export.' . strtolower($writerType), $writerType ); } @@ -190,7 +197,15 @@ public function toResponse($request): mixed $query->limit = $query->offset = $query->orders = null; $query->cleanBindings(['order']); }); - $total = $clone->count(); + + if ($this->isGrouped()) { + // TODO: Check for better way + $total = DB::query() + ->fromSub($clone, 'rows') + ->count(); + } else { + $total = $clone->count(); + } $data = $this->get(); @@ -215,7 +230,7 @@ public function toResponse($request): mixed protected function addToggledFilterToQuery(): void { - if (! isset($this->params['rowModel'])) { + if (!isset($this->params['rowModel'])) { return; } match (AgGridRowModel::from($this->params['rowModel'])) { @@ -240,9 +255,14 @@ protected function addServerSideToggledFilterToQuery(): void } } + protected function addRowGrouping(): void + { + $this->rowGroupMetadata->appendQueryBuilderMethods($this->subject); + } + protected function addFiltersToQuery(): void { - if (! isset($this->params['filterModel'])) { + if (!isset($this->params['filterModel'])) { return; } @@ -251,7 +271,7 @@ protected function addFiltersToQuery(): void // Check if we are in set values mode and exclude the filter for the given set value column $column = Arr::get($this->params, 'column'); if ($column) { - $filters = $filters->filter(fn ($value, $key) => $key !== $column); + $filters = $filters->filter(fn($value, $key) => $key !== $column); } foreach ($filters as $column => $filter) { @@ -270,7 +290,7 @@ protected function addFiltersToQuery(): void protected function addSortsToQuery(): void { - if (! isset($this->params['sortModel'])) { + if (!isset($this->params['sortModel'])) { return; } @@ -282,11 +302,23 @@ protected function addSortsToQuery(): void } foreach ($sorts as $sort) { - $this->subject->orderBy($this->toJsonPath($sort['colId']), $sort['sort']); + + // Check if the sort field is included in the current grouping + if ($this->rowGroupMetadata->isColumnAvailable($sort['colId'])) { + $this->subject->orderBy($this->toJsonPath($sort['colId']), $sort['sort']); + } + } + + if ($this->rowGroupMetadata->isGrouped()) { + // TODO: Check for current grouping level + + // TODO: Add more context for better accessibility of groups + $this->subject->orderBy($this->rowGroupMetadata->getCurrentRowGroupCol()['field']); + } else { + // we need an additional sort condition so that the order is stable in all cases + $this->subject->orderBy($this->subject->getModel()->getKeyName()); } - // we need an additional sort condition so that the order is stable in all cases - $this->subject->orderBy($this->subject->getModel()->getKeyName()); } protected function addLimitAndOffsetToQuery(): void @@ -318,7 +350,7 @@ protected function addSetFilterToQuery(EloquentBuilder|Relation $subject, Column $column = $columnInformation->getColumnAsJsonPath(); $values = $filter['values']; $all = $filter['all'] ?? false; - $filteredValues = array_filter($values, fn ($value) => $value !== null); + $filteredValues = array_filter($values, fn($value) => $value !== null); $subject->where(function (EloquentBuilder $query) use ($all, $column, $values, $filteredValues, $isJsonColumn) { if (count($filteredValues) !== count($values)) { @@ -331,7 +363,7 @@ protected function addSetFilterToQuery(EloquentBuilder|Relation $subject, Column // TODO: find a workaround! $query->orWhere( $column, - $all ? '?&' : '?|', '{'.implode(',', $filteredValues).'}', + $all ? '?&' : '?|', '{' . implode(',', $filteredValues) . '}', ); } else { $query->orWhereIn($column, $filteredValues); @@ -348,10 +380,10 @@ protected function addTextFilterToQuery(EloquentBuilder|Relation $subject, Colum match ($type) { AgGridTextFilterType::Equals => $subject->where($column, '=', $value), AgGridTextFilterType::NotEqual => $subject->where($column, '!=', $value), - AgGridTextFilterType::Contains => $subject->where($column, 'ilike', '%'.$value.'%'), - AgGridTextFilterType::NotContains => $subject->where($column, 'not ilike', '%'.$value.'%'), - AgGridTextFilterType::StartsWith => $subject->where($column, 'ilike', $value.'%'), - AgGridTextFilterType::EndsWith => $subject->where($column, 'ilike', '%'.$value), + AgGridTextFilterType::Contains => $subject->where($column, 'ilike', '%' . $value . '%'), + AgGridTextFilterType::NotContains => $subject->where($column, 'not ilike', '%' . $value . '%'), + AgGridTextFilterType::StartsWith => $subject->where($column, 'ilike', $value . '%'), + AgGridTextFilterType::EndsWith => $subject->where($column, 'ilike', '%' . $value), AgGridTextFilterType::Blank => $subject->whereNull($column), AgGridTextFilterType::NotBlank => $subject->whereNotNull($column), }; @@ -422,4 +454,21 @@ protected function traverse($model, $key, $default = null): Model return $model; } + + protected function isGrouped(): bool + { + if (!isset($this->params['rowGroupCols']) || empty($this->params['rowGroupCols'])) { + return false; + } + + // --> rowGroupCols available and not empty + + if (!isset($this->params['groupKeys']) || empty($this->params['groupKeys'])) { + return true; + } + + // --> groupKeys available and not empty + + return count($this->params['rowGroupCols']) !== count($this->params['groupKeys']); + } } diff --git a/src/Requests/AgGridGetRowsRequest.php b/src/Requests/AgGridGetRowsRequest.php index 6d5a3d4..816b80f 100644 --- a/src/Requests/AgGridGetRowsRequest.php +++ b/src/Requests/AgGridGetRowsRequest.php @@ -21,6 +21,9 @@ public function rules(): array 'selectAll' => ['sometimes', 'boolean'], 'toggledNodes' => ['sometimes', 'array'], 'customFilters' => ['sometimes', 'array'], + // Row Grouping + 'rowGroupCols' => ['sometimes', 'array'], + 'groupKeys' => ['sometimes', 'array'], ]; } } diff --git a/src/Support/RowGroupMetadata.php b/src/Support/RowGroupMetadata.php new file mode 100644 index 0000000..34e1366 --- /dev/null +++ b/src/Support/RowGroupMetadata.php @@ -0,0 +1,86 @@ +groupKeys ?? []) - 1; + for ($index = 0; $index <= $lastEquippedRowGroupColIndex; $index++) { + $builder->where($this->rowGroupCols[$index]['field'], $this->groupKeys[$index]); + } + + // Add the group by column + $currentRowGroupCol = $this->getCurrentRowGroupCol(); + if ($currentRowGroupCol){ + $builder->cleanBindings(['select']); + $builder->select($currentRowGroupCol['field']); + $builder->groupBy($currentRowGroupCol['field']); + } + + return $builder; + + } + + public function getCurrentRowGroupCol(): ?array { + + if (!$this->isGrouped()){ + return null; + } + + return $this->rowGroupCols[count($this->groupKeys)]; + } + + public function isColumnAvailable(string $column): bool { + + if (!$this->isGrouped()){ + return true; + } + + return $this->getCurrentRowGroupCol()['field'] === $column; + } + + public function isGrouped(): bool + { + if (empty($this->rowGroupCols)) { + return false; + } + + // --> rowGroupCols available and not empty + + if (empty($this->groupKeys)) { + return true; + } + + // --> groupKeys available and not empty + + // Check if the all rowGroupCols are equipped with a key => no grouping anymore + return count($this->rowGroupCols) !== count($this->groupKeys); + } + +} From 8a8971e44fcfc0794819ed42f48d7f88c78589ee Mon Sep 17 00:00:00 2001 From: ahawlitschek Date: Mon, 25 Sep 2023 14:20:25 +0000 Subject: [PATCH 10/10] Fix styling --- src/AgGridQueryBuilder.php | 40 ++++++++++++++++---------------- src/Support/RowGroupMetadata.php | 19 +++++++-------- 2 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/AgGridQueryBuilder.php b/src/AgGridQueryBuilder.php index c9fa136..bd6c342 100644 --- a/src/AgGridQueryBuilder.php +++ b/src/AgGridQueryBuilder.php @@ -45,7 +45,7 @@ class AgGridQueryBuilder implements Responsable protected RowGroupMetadata $rowGroupMetadata; /** - * @param EloquentBuilder|Relation|Model|class-string $subject + * @param EloquentBuilder|Relation|Model|class-string $subject */ public function __construct(array $params, EloquentBuilder|Relation|Model|string $subject) { @@ -73,7 +73,7 @@ public function __construct(array $params, EloquentBuilder|Relation|Model|string /** * Returns a new AgGridQueryBuilder for an AgGridGetRowsRequest. * - * @param EloquentBuilder|Relation|Model|class-string $subject + * @param EloquentBuilder|Relation|Model|class-string $subject */ public static function forRequest(AgGridGetRowsRequest $request, EloquentBuilder|Relation|Model|string $subject): AgGridQueryBuilder { @@ -83,7 +83,7 @@ public static function forRequest(AgGridGetRowsRequest $request, EloquentBuilder /** * Returns a new AgGridQueryBuilder for an AgGridGetRowsRequest. * - * @param EloquentBuilder|Relation|Model|class-string $subject + * @param EloquentBuilder|Relation|Model|class-string $subject */ public static function forSetValuesRequest(AgGridSetValuesRequest $request, EloquentBuilder|Relation|Model|string $subject): AgGridQueryBuilder { @@ -93,7 +93,7 @@ public static function forSetValuesRequest(AgGridSetValuesRequest $request, Eloq /** * Returns a new AgGridQueryBuilder for a selection. * - * @param EloquentBuilder|Relation|Model|class-string $subject + * @param EloquentBuilder|Relation|Model|class-string $subject */ public static function forSelection(array $selection, EloquentBuilder|Relation|Model|string $subject): AgGridQueryBuilder { @@ -115,7 +115,7 @@ public function getSubject(): Relation|EloquentBuilder } /** - * @param class-string $resourceClass + * @param class-string $resourceClass */ public function resource(string $resourceClass): self { @@ -131,7 +131,7 @@ public function toSetValues(array $allowedColumns = []): Collection throw InvalidSetValueOperation::make(); } - if (collect($allowedColumns)->first() !== '*' && !in_array($column, $allowedColumns)) { + if (collect($allowedColumns)->first() !== '*' && ! in_array($column, $allowedColumns)) { throw UnauthorizedSetFilterColumn::make($column); } @@ -143,7 +143,7 @@ public function toSetValues(array $allowedColumns = []): Collection return $this->subject->with($dottedRelation) ->get() - ->map(fn(Model $model) => Arr::get($this->traverse($model, $dottedRelation)->toArray(), $columnMetadata->getColumn())) + ->map(fn (Model $model) => Arr::get($this->traverse($model, $dottedRelation)->toArray(), $columnMetadata->getColumn())) ->unique() ->sort() ->values(); @@ -186,7 +186,7 @@ public function toResponse($request): mixed return Excel::download( new AgGridExport($this->subject, $this->params['exportColumns'] ?? null), - 'export.' . strtolower($writerType), + 'export.'.strtolower($writerType), $writerType ); } @@ -230,7 +230,7 @@ public function toResponse($request): mixed protected function addToggledFilterToQuery(): void { - if (!isset($this->params['rowModel'])) { + if (! isset($this->params['rowModel'])) { return; } match (AgGridRowModel::from($this->params['rowModel'])) { @@ -262,7 +262,7 @@ protected function addRowGrouping(): void protected function addFiltersToQuery(): void { - if (!isset($this->params['filterModel'])) { + if (! isset($this->params['filterModel'])) { return; } @@ -271,7 +271,7 @@ protected function addFiltersToQuery(): void // Check if we are in set values mode and exclude the filter for the given set value column $column = Arr::get($this->params, 'column'); if ($column) { - $filters = $filters->filter(fn($value, $key) => $key !== $column); + $filters = $filters->filter(fn ($value, $key) => $key !== $column); } foreach ($filters as $column => $filter) { @@ -290,7 +290,7 @@ protected function addFiltersToQuery(): void protected function addSortsToQuery(): void { - if (!isset($this->params['sortModel'])) { + if (! isset($this->params['sortModel'])) { return; } @@ -350,7 +350,7 @@ protected function addSetFilterToQuery(EloquentBuilder|Relation $subject, Column $column = $columnInformation->getColumnAsJsonPath(); $values = $filter['values']; $all = $filter['all'] ?? false; - $filteredValues = array_filter($values, fn($value) => $value !== null); + $filteredValues = array_filter($values, fn ($value) => $value !== null); $subject->where(function (EloquentBuilder $query) use ($all, $column, $values, $filteredValues, $isJsonColumn) { if (count($filteredValues) !== count($values)) { @@ -363,7 +363,7 @@ protected function addSetFilterToQuery(EloquentBuilder|Relation $subject, Column // TODO: find a workaround! $query->orWhere( $column, - $all ? '?&' : '?|', '{' . implode(',', $filteredValues) . '}', + $all ? '?&' : '?|', '{'.implode(',', $filteredValues).'}', ); } else { $query->orWhereIn($column, $filteredValues); @@ -380,10 +380,10 @@ protected function addTextFilterToQuery(EloquentBuilder|Relation $subject, Colum match ($type) { AgGridTextFilterType::Equals => $subject->where($column, '=', $value), AgGridTextFilterType::NotEqual => $subject->where($column, '!=', $value), - AgGridTextFilterType::Contains => $subject->where($column, 'ilike', '%' . $value . '%'), - AgGridTextFilterType::NotContains => $subject->where($column, 'not ilike', '%' . $value . '%'), - AgGridTextFilterType::StartsWith => $subject->where($column, 'ilike', $value . '%'), - AgGridTextFilterType::EndsWith => $subject->where($column, 'ilike', '%' . $value), + AgGridTextFilterType::Contains => $subject->where($column, 'ilike', '%'.$value.'%'), + AgGridTextFilterType::NotContains => $subject->where($column, 'not ilike', '%'.$value.'%'), + AgGridTextFilterType::StartsWith => $subject->where($column, 'ilike', $value.'%'), + AgGridTextFilterType::EndsWith => $subject->where($column, 'ilike', '%'.$value), AgGridTextFilterType::Blank => $subject->whereNull($column), AgGridTextFilterType::NotBlank => $subject->whereNotNull($column), }; @@ -457,13 +457,13 @@ protected function traverse($model, $key, $default = null): Model protected function isGrouped(): bool { - if (!isset($this->params['rowGroupCols']) || empty($this->params['rowGroupCols'])) { + if (! isset($this->params['rowGroupCols']) || empty($this->params['rowGroupCols'])) { return false; } // --> rowGroupCols available and not empty - if (!isset($this->params['groupKeys']) || empty($this->params['groupKeys'])) { + if (! isset($this->params['groupKeys']) || empty($this->params['groupKeys'])) { return true; } diff --git a/src/Support/RowGroupMetadata.php b/src/Support/RowGroupMetadata.php index 34e1366..9873c1d 100644 --- a/src/Support/RowGroupMetadata.php +++ b/src/Support/RowGroupMetadata.php @@ -6,15 +6,12 @@ class RowGroupMetadata { - public function __construct( protected ?array $rowGroupCols, protected ?array $groupKeys, - ) - { + ) { } - public static function fromParams(array $params): self { @@ -25,7 +22,6 @@ public static function fromParams(array $params): self } - public function appendQueryBuilderMethods(Builder &$builder): Builder { @@ -37,7 +33,7 @@ public function appendQueryBuilderMethods(Builder &$builder): Builder // Add the group by column $currentRowGroupCol = $this->getCurrentRowGroupCol(); - if ($currentRowGroupCol){ + if ($currentRowGroupCol) { $builder->cleanBindings(['select']); $builder->select($currentRowGroupCol['field']); $builder->groupBy($currentRowGroupCol['field']); @@ -47,18 +43,20 @@ public function appendQueryBuilderMethods(Builder &$builder): Builder } - public function getCurrentRowGroupCol(): ?array { + public function getCurrentRowGroupCol(): ?array + { - if (!$this->isGrouped()){ + if (! $this->isGrouped()) { return null; } return $this->rowGroupCols[count($this->groupKeys)]; } - public function isColumnAvailable(string $column): bool { + public function isColumnAvailable(string $column): bool + { - if (!$this->isGrouped()){ + if (! $this->isGrouped()) { return true; } @@ -82,5 +80,4 @@ public function isGrouped(): bool // Check if the all rowGroupCols are equipped with a key => no grouping anymore return count($this->rowGroupCols) !== count($this->groupKeys); } - }