From 88358c4731b2084b3273cd22d595dbba47c82006 Mon Sep 17 00:00:00 2001 From: Tom Janssen Date: Thu, 19 Dec 2024 15:52:55 +0100 Subject: [PATCH 1/6] discover relationships --- .../Concerns/InteractsWithTableQuery.php | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php b/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php index 626fd6dfef9..928d806b23b 100644 --- a/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php +++ b/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php @@ -95,8 +95,52 @@ public function applySearchConstraint(EloquentBuilder $query, string $search, bo "%{$nonTranslatableSearch}%", ), function (EloquentBuilder $query) use ($databaseConnection, $isSearchForcedCaseInsensitive, $nonTranslatableSearch, $searchColumn, $whereClause): EloquentBuilder { - // Treat the missing "relationship" as a JSON column if dot notation is used in the column name. if (filled($relationshipName = $this->getRelationshipName())) { + // Check if it is a JSON column on a relationship + if (str($relationshipName)->contains('.')) { + $record = $query->getModel(); + $parts = explode('.', $relationshipName); + + $relationshipNameParts = []; + while (count($parts) > 0) { + // Grab the first name element to check + $nestedRelationshipName = array_shift($parts); + + if (! $record->isRelation($nestedRelationshipName)) { + // Not a relation, assume that all next dots represent json accessors + array_unshift($parts, $nestedRelationshipName); + + break; + } + + // Append the name element as relation + $relationshipNameParts[] = $nestedRelationshipName; + + /** @var Relation */ + $relationship = $record->{$nestedRelationshipName}(); + $record = $relationship->getRelated(); + } + + if (filled($relationshipNameParts)) { + // There is a relationship before JSON column using dot notation + if (filled($parts)) { + // When there are remaining part(s), assume it is a JSON column + $searchColumn = implode('->', [ + ...$parts, + $searchColumn, + ]); + } + + return $query->{"{$whereClause}Relation"}( + implode('.', $relationshipNameParts), + generate_search_column_expression($searchColumn, $isSearchForcedCaseInsensitive, $databaseConnection), + 'like', + "%{$nonTranslatableSearch}%", + ); + } + } + + // Treat the missing "relationship" as a JSON column if dot notation is used in the column name. $searchColumn = (string) str($relationshipName) ->append('.') ->append($searchColumn) From 2405289679a9d000dece988713d88c951a06c1f2 Mon Sep 17 00:00:00 2001 From: Dan Harrin Date: Mon, 3 Feb 2025 08:27:16 +0000 Subject: [PATCH 2/6] Revert "discover relationships" This reverts commit 88358c4731b2084b3273cd22d595dbba47c82006. --- .../Concerns/InteractsWithTableQuery.php | 46 +------------------ 1 file changed, 1 insertion(+), 45 deletions(-) diff --git a/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php b/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php index 928d806b23b..626fd6dfef9 100644 --- a/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php +++ b/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php @@ -95,52 +95,8 @@ public function applySearchConstraint(EloquentBuilder $query, string $search, bo "%{$nonTranslatableSearch}%", ), function (EloquentBuilder $query) use ($databaseConnection, $isSearchForcedCaseInsensitive, $nonTranslatableSearch, $searchColumn, $whereClause): EloquentBuilder { + // Treat the missing "relationship" as a JSON column if dot notation is used in the column name. if (filled($relationshipName = $this->getRelationshipName())) { - // Check if it is a JSON column on a relationship - if (str($relationshipName)->contains('.')) { - $record = $query->getModel(); - $parts = explode('.', $relationshipName); - - $relationshipNameParts = []; - while (count($parts) > 0) { - // Grab the first name element to check - $nestedRelationshipName = array_shift($parts); - - if (! $record->isRelation($nestedRelationshipName)) { - // Not a relation, assume that all next dots represent json accessors - array_unshift($parts, $nestedRelationshipName); - - break; - } - - // Append the name element as relation - $relationshipNameParts[] = $nestedRelationshipName; - - /** @var Relation */ - $relationship = $record->{$nestedRelationshipName}(); - $record = $relationship->getRelated(); - } - - if (filled($relationshipNameParts)) { - // There is a relationship before JSON column using dot notation - if (filled($parts)) { - // When there are remaining part(s), assume it is a JSON column - $searchColumn = implode('->', [ - ...$parts, - $searchColumn, - ]); - } - - return $query->{"{$whereClause}Relation"}( - implode('.', $relationshipNameParts), - generate_search_column_expression($searchColumn, $isSearchForcedCaseInsensitive, $databaseConnection), - 'like', - "%{$nonTranslatableSearch}%", - ); - } - } - - // Treat the missing "relationship" as a JSON column if dot notation is used in the column name. $searchColumn = (string) str($relationshipName) ->append('.') ->append($searchColumn) From 0fd1e833c5037112ad21dd7de41f123d47653f0b Mon Sep 17 00:00:00 2001 From: Dan Harrin Date: Mon, 3 Feb 2025 10:02:06 +0000 Subject: [PATCH 3/6] Start refactoring --- packages/actions/src/Exports/ExportColumn.php | 2 +- .../Columns/SpatieMediaLibraryImageColumn.php | 2 +- .../src/Tables/Columns/SpatieTagsColumn.php | 2 +- .../support/src/Concerns/HasCellState.php | 107 ++++++++++++++---- .../src/Columns/Concerns/CanBeSearchable.php | 9 +- .../src/Columns/Concerns/CanBeSortable.php | 9 +- .../src/Columns/Concerns/CanUpdateState.php | 12 +- .../Concerns/InteractsWithTableQuery.php | 42 ++----- .../src/Columns/Summarizers/Summarizer.php | 2 +- .../src/Concerns/CanSummarizeRecords.php | 2 +- 10 files changed, 117 insertions(+), 72 deletions(-) diff --git a/packages/actions/src/Exports/ExportColumn.php b/packages/actions/src/Exports/ExportColumn.php index acd48a61d92..cf02c716748 100644 --- a/packages/actions/src/Exports/ExportColumn.php +++ b/packages/actions/src/Exports/ExportColumn.php @@ -126,7 +126,7 @@ public function applyEagerLoading(EloquentBuilder $query): EloquentBuilder return $query; } - $relationshipName = $this->getRelationshipName(); + $relationshipName = $this->getRelationshipName($query->getModel()); if (array_key_exists($relationshipName, $query->getEagerLoads())) { return $query; diff --git a/packages/spatie-laravel-media-library-plugin/src/Tables/Columns/SpatieMediaLibraryImageColumn.php b/packages/spatie-laravel-media-library-plugin/src/Tables/Columns/SpatieMediaLibraryImageColumn.php index 2a9dac33c94..155654ec1d0 100644 --- a/packages/spatie-laravel-media-library-plugin/src/Tables/Columns/SpatieMediaLibraryImageColumn.php +++ b/packages/spatie-laravel-media-library-plugin/src/Tables/Columns/SpatieMediaLibraryImageColumn.php @@ -173,7 +173,7 @@ public function applyEagerLoading(Builder | Relation $query): Builder | Relation if ($this->hasRelationship($query->getModel())) { return $query->with([ - "{$this->getRelationshipName()}.media" => $modifyMediaQuery, + "{$this->getRelationshipName($query->getModel())}.media" => $modifyMediaQuery, ]); } diff --git a/packages/spatie-laravel-tags-plugin/src/Tables/Columns/SpatieTagsColumn.php b/packages/spatie-laravel-tags-plugin/src/Tables/Columns/SpatieTagsColumn.php index 5d4fae938fd..74ca3011866 100644 --- a/packages/spatie-laravel-tags-plugin/src/Tables/Columns/SpatieTagsColumn.php +++ b/packages/spatie-laravel-tags-plugin/src/Tables/Columns/SpatieTagsColumn.php @@ -91,7 +91,7 @@ public function applyEagerLoading(Builder | Relation $query): Builder | Relation } if ($this->hasRelationship($query->getModel())) { - return $query->with(["{$this->getRelationshipName()}.tags"]); + return $query->with(["{$this->getRelationshipName($query->getModel())}.tags"]); } return $query->with(['tags']); diff --git a/packages/support/src/Concerns/HasCellState.php b/packages/support/src/Concerns/HasCellState.php index fb1715d40b8..0be4fc1a57e 100644 --- a/packages/support/src/Concerns/HasCellState.php +++ b/packages/support/src/Concerns/HasCellState.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Stringable; use Znck\Eloquent\Relations\BelongsToThrough; @@ -113,11 +114,12 @@ public function getStateFromRecord(): mixed return null; } - $relationshipAttribute = $this->getRelationshipAttribute(); + $attributeName = $this->getAttributeName($record); + $fullAttributeName = $this->getFullAttributeName($record); $state = collect($this->getRelationshipResults($record)) - ->filter(fn (Model $record): bool => array_key_exists($relationshipAttribute, $record->attributesToArray())) - ->pluck($relationshipAttribute) + ->filter(fn (Model $record): bool => array_key_exists($attributeName, $record->attributesToArray())) + ->pluck($fullAttributeName) ->filter(fn ($state): bool => filled($state)) ->when($this->isDistinctList(), fn (Collection $state) => $state->unique()) ->values(); @@ -143,7 +145,13 @@ public function getSeparator(): ?string public function hasRelationship(Model $record): bool { - return $this->getRelationship($record) !== null; + $name = $this->getName(); + + if (! str($name)->contains('.')) { + return false; + } + + return $record->isRelation((string) str($name)->before('.')); } /** @@ -156,20 +164,23 @@ public function queriesRelationships(Model $record): bool public function getRelationship(Model $record, ?string $name = null): ?Relation { - if (blank($name) && (! str($this->getName())->contains('.'))) { + $name ??= $this->getName(); + + if (! str($name)->contains('.')) { return null; } - $relationship = null; + $nameParts = explode('.', $name); + array_pop($nameParts); - foreach (explode('.', $name ?? $this->getRelationshipName()) as $nestedRelationshipName) { - if (! $record->isRelation($nestedRelationshipName)) { - $relationship = null; + $relationship = null; + foreach ($nameParts as $namePart) { + if (! $record->isRelation($namePart)) { break; } - $relationship = $record->{$nestedRelationshipName}(); + $relationship = $record->{$namePart}(); $record = $relationship->getRelated(); } @@ -184,7 +195,7 @@ public function getRelationshipResults(Model $record, ?array $relationships = nu { $results = []; - $relationships ??= explode('.', $this->getRelationshipName()); + $relationships ??= explode('.', $this->getRelationshipName($record)); while (count($relationships)) { $currentRelationshipName = array_shift($relationships); @@ -230,7 +241,7 @@ public function getRelationshipResults(Model $record, ?array $relationships = nu return $results; } - public function getRelationshipAttribute(?string $name = null): string + public function getAttributeName(Model $record, ?string $name = null): string { $name ??= $this->getName(); @@ -238,7 +249,38 @@ public function getRelationshipAttribute(?string $name = null): string return $name; } - return (string) str($name)->afterLast('.'); + $nameParts = explode('.', $name); + + foreach ($nameParts as $namePart) { + if (! $record->isRelation($namePart)) { + break; + } + + array_shift($nameParts); + } + + return Arr::first($nameParts); + } + + public function getFullAttributeName(Model $record, ?string $name = null): string + { + $name ??= $this->getName(); + + if (! str($name)->contains('.')) { + return $name; + } + + $nameParts = explode('.', $name); + + foreach ($nameParts as $namePart) { + if (! $record->isRelation($namePart)) { + break; + } + + array_shift($nameParts); + } + + return implode('.', $nameParts); } public function getInverseRelationshipName(Model $record): string @@ -247,10 +289,17 @@ public function getInverseRelationshipName(Model $record): string return $this->inverseRelationshipName; } - $inverseRelationships = []; + $nameParts = explode('.', $this->getName()); + array_pop($nameParts); + + $inverseRelationshipParts = []; - foreach (explode('.', $this->getRelationshipName()) as $nestedRelationshipName) { - $relationship = $record->{$nestedRelationshipName}(); + foreach ($nameParts as $namePart) { + if (! $record->isRelation($namePart)) { + break; + } + + $relationship = $record->{$namePart}(); $record = $relationship->getRelated(); $inverseNestedRelationshipName = (string) str(class_basename($relationship->getParent()::class)) @@ -266,22 +315,22 @@ public function getInverseRelationshipName(Model $record): string // The conventional relationship doesn't exist, but we can // attempt to use the original relationship name instead. - if (! $record->isRelation($nestedRelationshipName)) { + if (! $record->isRelation($namePart)) { $recordClass = $record::class; throw new Exception("When trying to guess the inverse relationship for column [{$this->getName()}], relationship [{$inverseNestedRelationshipName}] was not found on model [{$recordClass}]. Please define a custom [inverseRelationship()] for this column."); } - $inverseNestedRelationshipName = $nestedRelationshipName; + $inverseNestedRelationshipName = $namePart; } - array_unshift($inverseRelationships, $inverseNestedRelationshipName); + array_unshift($inverseRelationshipParts, $inverseNestedRelationshipName); } - return implode('.', $inverseRelationships); + return implode('.', $inverseRelationshipParts); } - public function getRelationshipName(?string $name = null): ?string + public function getRelationshipName(Model $record, ?string $name = null): ?string { $name ??= $this->getName(); @@ -289,6 +338,20 @@ public function getRelationshipName(?string $name = null): ?string return null; } - return (string) str($name)->beforeLast('.'); + $nameParts = explode('.', $name); + array_pop($nameParts); + + $relationshipParts = []; + + foreach ($nameParts as $namePart) { + if (! $record->isRelation($namePart)) { + break; + } + + $relationshipParts[] = $namePart; + $record = $record->{$namePart}()->getRelated(); + } + + return implode('.', $relationshipParts); } } diff --git a/packages/tables/src/Columns/Concerns/CanBeSearchable.php b/packages/tables/src/Columns/Concerns/CanBeSearchable.php index b72834d6127..fba7e264b20 100644 --- a/packages/tables/src/Columns/Concerns/CanBeSearchable.php +++ b/packages/tables/src/Columns/Concerns/CanBeSearchable.php @@ -3,6 +3,7 @@ namespace Filament\Tables\Columns\Concerns; use Closure; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; trait CanBeSearchable @@ -56,9 +57,9 @@ public function forceSearchCaseInsensitive(bool | Closure | null $condition = tr /** * @return array */ - public function getSearchColumns(): array + public function getSearchColumns(Model $record): array { - return $this->searchColumns ?? $this->getDefaultSearchColumns(); + return $this->searchColumns ?? $this->getDefaultSearchColumns($record); } public function isSearchable(): bool @@ -84,8 +85,8 @@ public function isSearchForcedCaseInsensitive(): ?bool /** * @return array{0: string} */ - public function getDefaultSearchColumns(): array + public function getDefaultSearchColumns(Model $record): array { - return [(string) str($this->getName())->afterLast('.')]; + return [$this->getFullAttributeName($record)]; } } diff --git a/packages/tables/src/Columns/Concerns/CanBeSortable.php b/packages/tables/src/Columns/Concerns/CanBeSortable.php index 458ea745a6e..96012741f80 100644 --- a/packages/tables/src/Columns/Concerns/CanBeSortable.php +++ b/packages/tables/src/Columns/Concerns/CanBeSortable.php @@ -3,6 +3,7 @@ namespace Filament\Tables\Columns\Concerns; use Closure; +use Illuminate\Database\Eloquent\Model; trait CanBeSortable { @@ -36,9 +37,9 @@ public function sortable(bool | array | Closure $condition = true, ?Closure $que /** * @return array */ - public function getSortColumns(): array + public function getSortColumns(Model $record): array { - return $this->sortColumns ?? $this->getDefaultSortColumns(); + return $this->sortColumns ?? $this->getDefaultSortColumns($record); } public function isSortable(): bool @@ -49,8 +50,8 @@ public function isSortable(): bool /** * @return array{0: string} */ - public function getDefaultSortColumns(): array + public function getDefaultSortColumns(Model $record): array { - return [str($this->getName())->afterLast('.')]; + return [$this->getFullAttributeName($record)]; } } diff --git a/packages/tables/src/Columns/Concerns/CanUpdateState.php b/packages/tables/src/Columns/Concerns/CanUpdateState.php index c79793ecaa5..4da878dd61c 100644 --- a/packages/tables/src/Columns/Concerns/CanUpdateState.php +++ b/packages/tables/src/Columns/Concerns/CanUpdateState.php @@ -58,9 +58,9 @@ public function updateState(mixed $state): mixed $columnName = $this->getName(); - if ($this->getRelationship($record)) { - $columnName = $this->getRelationshipAttribute(); - $columnRelationshipName = $this->getRelationshipName(); + if ($this->hasRelationship($record)) { + $columnName = $this->getFullAttributeName($record); + $columnRelationshipName = $this->getRelationshipName($record); $record = Arr::get( $record->load($columnRelationshipName), @@ -68,18 +68,16 @@ public function updateState(mixed $state): mixed ); } elseif ( (($tableRelationship = $this->getTable()->getRelationship()) instanceof BelongsToMany) && - in_array($columnName, $tableRelationship->getPivotColumns()) + in_array($this->getAttributeName($record), $tableRelationship->getPivotColumns()) ) { $record = $record->{$tableRelationship->getPivotAccessor()}; - } else { - $columnName = (string) str($columnName)->replace('.', '->'); } if (! ($record instanceof Model)) { return null; } - $record->setAttribute($columnName, $state); + $record->setAttribute((string) str($columnName)->replace('.', '->'), $state); $record->save(); $this->callAfterStateUpdated($state); diff --git a/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php b/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php index 626fd6dfef9..2823b58ad47 100644 --- a/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php +++ b/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php @@ -42,7 +42,7 @@ public function applyEagerLoading(EloquentBuilder | Relation $query): EloquentBu return $query; } - $relationshipName = $this->getRelationshipName(); + $relationshipName = $this->getRelationshipName($query->getModel()); if (array_key_exists($relationshipName, $query->getEagerLoads())) { return $query; @@ -80,7 +80,7 @@ public function applySearchConstraint(EloquentBuilder $query, string $search, bo $translatableContentDriver = $this->getLivewire()->makeFilamentTranslatableContentDriver(); - foreach ($this->getSearchColumns() as $searchColumn) { + foreach ($this->getSearchColumns($query->getModel()) as $searchColumn) { $whereClause = $isFirst ? 'where' : 'orWhere'; $query->when( @@ -89,26 +89,16 @@ public function applySearchConstraint(EloquentBuilder $query, string $search, bo fn (EloquentBuilder $query) => $query->when( $this->hasRelationship($query->getModel()), fn (EloquentBuilder $query): EloquentBuilder => $query->{"{$whereClause}Relation"}( - $this->getRelationshipName(), - generate_search_column_expression($searchColumn, $isSearchForcedCaseInsensitive, $databaseConnection), + $this->getRelationshipName($query->getModel()), + generate_search_column_expression((string) str($searchColumn)->replace('.', '->'), $isSearchForcedCaseInsensitive, $databaseConnection), + 'like', + "%{$nonTranslatableSearch}%", + ), + fn (EloquentBuilder $query) => $query->{$whereClause}( + generate_search_column_expression((string) str($searchColumn)->replace('.', '->'), $isSearchForcedCaseInsensitive, $databaseConnection), 'like', "%{$nonTranslatableSearch}%", ), - function (EloquentBuilder $query) use ($databaseConnection, $isSearchForcedCaseInsensitive, $nonTranslatableSearch, $searchColumn, $whereClause): EloquentBuilder { - // Treat the missing "relationship" as a JSON column if dot notation is used in the column name. - if (filled($relationshipName = $this->getRelationshipName())) { - $searchColumn = (string) str($relationshipName) - ->append('.') - ->append($searchColumn) - ->replace('.', '->'); - } - - return $query->{$whereClause}( - generate_search_column_expression($searchColumn, $isSearchForcedCaseInsensitive, $databaseConnection), - 'like', - "%{$nonTranslatableSearch}%", - ); - }, ), ); @@ -129,7 +119,7 @@ public function applySort(EloquentBuilder $query, string $direction = 'asc'): El return $query; } - foreach (array_reverse($this->getSortColumns()) as $sortColumn) { + foreach (array_reverse($this->getSortColumns($query->getModel())) as $sortColumn) { $query->orderBy($this->getSortColumnForQuery($query, $sortColumn), $direction); } @@ -141,26 +131,18 @@ public function applySort(EloquentBuilder $query, string $direction = 'asc'): El */ protected function getSortColumnForQuery(EloquentBuilder $query, string $sortColumn, ?array $relationships = null): string | Builder { - $relationships ??= ($relationshipName = $this->getRelationshipName()) ? + $relationships ??= ($relationshipName = $this->getRelationshipName($query->getModel())) ? explode('.', $relationshipName) : []; if (! count($relationships)) { - return $sortColumn; + return (string) str($sortColumn)->replace('.', '->'); } $currentRelationshipName = array_shift($relationships); $relationship = $this->getRelationship($query->getModel(), $currentRelationshipName); - if (! $relationship) { - // Treat the missing "relationship" as a JSON column if dot notation is used in the column name. - return (string) str($relationshipName ?? $this->getRelationshipName()) - ->append('.') - ->append($sortColumn) - ->replace('.', '->'); - } - $relatedQuery = $relationship->getRelated()::query(); return $relationship diff --git a/packages/tables/src/Columns/Summarizers/Summarizer.php b/packages/tables/src/Columns/Summarizers/Summarizer.php index 7862d19c0c6..f34604ee710 100644 --- a/packages/tables/src/Columns/Summarizers/Summarizer.php +++ b/packages/tables/src/Columns/Summarizers/Summarizer.php @@ -85,7 +85,7 @@ public function getState(): mixed if ($column->hasRelationship($query->getModel())) { $relationship = $column->getRelationship($query->getModel()); - $attribute = $column->getRelationshipAttribute(); + $attribute = $column->getFullAttributeName($query->getModel()); $inverseRelationship = $column->getInverseRelationshipName($query->getModel()); diff --git a/packages/tables/src/Concerns/CanSummarizeRecords.php b/packages/tables/src/Concerns/CanSummarizeRecords.php index a0a096c4bdb..fbc81609c3f 100644 --- a/packages/tables/src/Concerns/CanSummarizeRecords.php +++ b/packages/tables/src/Concerns/CanSummarizeRecords.php @@ -40,7 +40,7 @@ public function getTableSummarySelectedState(Builder $query, ?Closure $modifyQue continue; } - if (filled($column->getRelationshipName())) { + if ($column->hasRelationship($query->getModel())) { continue; } From f14762f065f68af91f37844c4ac9386e9ddd135a Mon Sep 17 00:00:00 2001 From: Dan Harrin Date: Mon, 3 Feb 2025 10:09:29 +0000 Subject: [PATCH 4/6] Fix tests --- .../support/src/Concerns/HasCellState.php | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/support/src/Concerns/HasCellState.php b/packages/support/src/Concerns/HasCellState.php index 0be4fc1a57e..0963e8c1478 100644 --- a/packages/support/src/Concerns/HasCellState.php +++ b/packages/support/src/Concerns/HasCellState.php @@ -162,16 +162,20 @@ public function queriesRelationships(Model $record): bool return $this->hasRelationship($record); } - public function getRelationship(Model $record, ?string $name = null): ?Relation + public function getRelationship(Model $record, ?string $relationshipName = null): ?Relation { - $name ??= $this->getName(); + if (isset($relationshipName)) { + $nameParts = explode('.', $relationshipName); + } else { + $name = $this->getName(); - if (! str($name)->contains('.')) { - return null; - } + if (! str($name)->contains('.')) { + return null; + } - $nameParts = explode('.', $name); - array_pop($nameParts); + $nameParts = explode('.', $name); + array_pop($nameParts); + } $relationship = null; @@ -241,9 +245,9 @@ public function getRelationshipResults(Model $record, ?array $relationships = nu return $results; } - public function getAttributeName(Model $record, ?string $name = null): string + public function getAttributeName(Model $record): string { - $name ??= $this->getName(); + $name = $this->getName(); if (! str($name)->contains('.')) { return $name; @@ -262,9 +266,9 @@ public function getAttributeName(Model $record, ?string $name = null): string return Arr::first($nameParts); } - public function getFullAttributeName(Model $record, ?string $name = null): string + public function getFullAttributeName(Model $record): string { - $name ??= $this->getName(); + $name = $this->getName(); if (! str($name)->contains('.')) { return $name; @@ -330,9 +334,9 @@ public function getInverseRelationshipName(Model $record): string return implode('.', $inverseRelationshipParts); } - public function getRelationshipName(Model $record, ?string $name = null): ?string + public function getRelationshipName(Model $record): ?string { - $name ??= $this->getName(); + $name = $this->getName(); if (! str($name)->contains('.')) { return null; From 86cd634991903fdb0d189113395522338a159c1c Mon Sep 17 00:00:00 2001 From: Dan Harrin Date: Mon, 3 Feb 2025 12:15:21 +0000 Subject: [PATCH 5/6] Add tests --- .../migrations/create_posts_table.php | 1 + .../migrations/modify_users_table.php | 1 + tests/src/Fixtures/Livewire/PostsTable.php | 12 ++ tests/src/Fixtures/Models/Post.php | 1 + tests/src/Fixtures/Models/User.php | 1 + tests/src/Tables/ColumnTest.php | 166 ++++++++++++++++++ 6 files changed, 182 insertions(+) diff --git a/tests/database/migrations/create_posts_table.php b/tests/database/migrations/create_posts_table.php index 361458285f8..a03ae7439de 100644 --- a/tests/database/migrations/create_posts_table.php +++ b/tests/database/migrations/create_posts_table.php @@ -16,6 +16,7 @@ public function up(): void $table->unsignedTinyInteger('rating')->default(0); $table->json('tags')->nullable(); $table->string('title'); + $table->json('json')->nullable(); $table->json('json_array_of_objects')->nullable(); $table->timestamps(); $table->softDeletes(); diff --git a/tests/database/migrations/modify_users_table.php b/tests/database/migrations/modify_users_table.php index 51c96cd3e3b..a97d33f1582 100644 --- a/tests/database/migrations/modify_users_table.php +++ b/tests/database/migrations/modify_users_table.php @@ -13,6 +13,7 @@ public function up(): void { Schema::table('users', function (Blueprint $table) { $table->after('password', function (Blueprint $table) { + $table->json('json')->nullable(); $table->string('email_code_authentication_secret')->nullable(); $table->string('google_two_factor_authentication_secret')->nullable(); $table->text('google_two_factor_authentication_recovery_codes')->nullable(); diff --git a/tests/src/Fixtures/Livewire/PostsTable.php b/tests/src/Fixtures/Livewire/PostsTable.php index 3598ec91519..ea6d19762b0 100644 --- a/tests/src/Fixtures/Livewire/PostsTable.php +++ b/tests/src/Fixtures/Livewire/PostsTable.php @@ -90,7 +90,19 @@ public function table(Table $table): Table ->state('correct state'), Tables\Columns\TextColumn::make('formatted_state') ->formatStateUsing(fn () => 'formatted state'), + Tables\Columns\TextColumn::make('json.foo') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('json.bar.baz') + ->searchable() + ->sortable(), Tables\Columns\TextColumn::make('json_array_of_objects.*.value'), + Tables\Columns\TextColumn::make('author.json.foo') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('author.json.bar.baz') + ->searchable() + ->sortable(), Tables\Columns\TextColumn::make('extra_attributes') ->extraAttributes([ 'class' => 'text-danger-500', diff --git a/tests/src/Fixtures/Models/Post.php b/tests/src/Fixtures/Models/Post.php index 2c76ff94898..38901352847 100644 --- a/tests/src/Fixtures/Models/Post.php +++ b/tests/src/Fixtures/Models/Post.php @@ -16,6 +16,7 @@ class Post extends Model protected $casts = [ 'is_published' => 'boolean', 'tags' => 'array', + 'json' => 'array', 'json_array_of_objects' => 'array', ]; diff --git a/tests/src/Fixtures/Models/User.php b/tests/src/Fixtures/Models/User.php index e0d3edafa8a..6e00c21b593 100644 --- a/tests/src/Fixtures/Models/User.php +++ b/tests/src/Fixtures/Models/User.php @@ -37,6 +37,7 @@ class User extends Authenticatable implements FilamentUser, HasEmailCodeAuthenti * @var array */ protected $casts = [ + 'json' => 'array', 'email_verified_at' => 'datetime', 'google_two_factor_authentication_secret' => 'encrypted', 'google_two_factor_authentication_recovery_codes' => 'encrypted:array', diff --git a/tests/src/Tables/ColumnTest.php b/tests/src/Tables/ColumnTest.php index 4643006bb3f..1e9b1ac2d57 100644 --- a/tests/src/Tables/ColumnTest.php +++ b/tests/src/Tables/ColumnTest.php @@ -4,7 +4,9 @@ use Filament\Tables\Columns\TextColumn; use Filament\Tests\Fixtures\Livewire\PostsTable; use Filament\Tests\Fixtures\Models\Post; +use Filament\Tests\Fixtures\Models\User; use Filament\Tests\Tables\TestCase; +use Illuminate\Support\Str; use function Filament\Tests\livewire; @@ -44,6 +46,54 @@ ->assertCanSeeTableRecords($posts->sortByDesc('author.name'), inOrder: true); }); +it('can sort records with JSON column', function () { + $posts = Post::factory()->count(10)->state(fn (): array => [ + 'json' => ['foo' => Str::random()], + ])->create(); + + livewire(PostsTable::class) + ->sortTable('json.foo') + ->assertCanSeeTableRecords($posts->sortBy('json.foo'), inOrder: true) + ->sortTable('json.foo', 'desc') + ->assertCanSeeTableRecords($posts->sortByDesc('json.foo'), inOrder: true); +}); + +it('can sort records with nested JSON column', function () { + $posts = Post::factory()->count(10)->state(fn (): array => [ + 'json' => ['bar' => ['baz' => Str::random()]], + ])->create(); + + livewire(PostsTable::class) + ->sortTable('json.bar.baz') + ->assertCanSeeTableRecords($posts->sortBy('json.bar.baz'), inOrder: true) + ->sortTable('json.bar.baz', 'desc') + ->assertCanSeeTableRecords($posts->sortByDesc('json.bar.baz'), inOrder: true); +}); + +it('can sort records with relationship JSON column', function () { + $posts = Post::factory()->count(10)->state(fn (): array => [ + 'author_id' => User::factory()->state(['json' => ['foo' => Str::random()]]), + ])->create(); + + livewire(PostsTable::class) + ->sortTable('author.json.foo') + ->assertCanSeeTableRecords($posts->sortBy('author.json.foo'), inOrder: true) + ->sortTable('author.json.foo', 'desc') + ->assertCanSeeTableRecords($posts->sortByDesc('author.json.foo'), inOrder: true); +}); + +it('can sort records with relationship nested JSON column', function () { + $posts = Post::factory()->count(10)->state(fn (): array => [ + 'author_id' => User::factory()->state(['json' => ['bar' => ['baz' => Str::random()]]]), + ])->create(); + + livewire(PostsTable::class) + ->sortTable('author.json.bar.baz') + ->assertCanSeeTableRecords($posts->sortBy('author.json.bar.baz'), inOrder: true) + ->sortTable('author.json.bar.baz', 'desc') + ->assertCanSeeTableRecords($posts->sortByDesc('author.json.bar.baz'), inOrder: true); +}); + it('can search records', function () { $posts = Post::factory()->count(10)->create(); @@ -77,6 +127,82 @@ ->assertCanNotSeeTableRecords($posts->where('author.name', '!=', $author)); }); +it('can search posts with JSON column', function () { + $search = Str::random(); + + $matchingPosts = Post::factory()->count(5)->create([ + 'json' => ['foo' => $search], + ]); + + $notMatchingPosts = Post::factory()->count(5)->create([ + 'json' => ['foo' => Str::random()], + ]); + + livewire(PostsTable::class) + ->searchTable($search) + ->assertCanSeeTableRecords($matchingPosts) + ->assertCanNotSeeTableRecords($notMatchingPosts); +}); + +it('can search posts with nested JSON column', function () { + $search = Str::random(); + + $matchingPosts = Post::factory()->count(5)->create([ + 'json' => ['bar' => ['baz' => $search]], + ]); + + $notMatchingPosts = Post::factory()->count(5)->create([ + 'json' => ['bar' => ['baz' => Str::random()]], + ]); + + livewire(PostsTable::class) + ->searchTable($search) + ->assertCanSeeTableRecords($matchingPosts) + ->assertCanNotSeeTableRecords($notMatchingPosts); +}); + +it('can search posts with relationship JSON column', function () { + $search = Str::random(); + + $matchingAuthor = User::factory() + ->create(['json' => ['foo' => $search]]); + + $matchingPosts = Post::factory() + ->for($matchingAuthor, 'author') + ->count(5) + ->create(); + + $notMatchingPosts = Post::factory() + ->count(5) + ->create(); + + livewire(PostsTable::class) + ->searchTable($search) + ->assertCanSeeTableRecords($matchingPosts) + ->assertCanNotSeeTableRecords($notMatchingPosts); +}); + +it('can search posts with relationship nested JSON column', function () { + $search = Str::random(); + + $matchingAuthor = User::factory() + ->create(['json' => ['bar' => ['baz' => $search]]]); + + $matchingPosts = Post::factory() + ->for($matchingAuthor, 'author') + ->count(5) + ->create(); + + $notMatchingPosts = Post::factory() + ->count(5) + ->create(); + + livewire(PostsTable::class) + ->searchTable($search) + ->assertCanSeeTableRecords($matchingPosts) + ->assertCanNotSeeTableRecords($notMatchingPosts); +}); + it('can search individual column records with relationship', function () { $posts = Post::factory()->count(10)->create(); @@ -141,6 +267,46 @@ ->assertTableColumnFormattedStateNotSet('formatted_state', 'incorrect formatted state', $post); }); +it('can output JSON values', function () { + $post = Post::factory()->create([ + 'json' => ['foo' => 'bar'], + ]); + + livewire(PostsTable::class) + ->assertTableColumnStateSet('json.foo', 'bar', $post); +}); + +it('can output nested JSON values', function () { + $post = Post::factory()->create([ + 'json' => ['bar' => ['baz' => 'qux']], + ]); + + livewire(PostsTable::class) + ->assertTableColumnStateSet('json.bar.baz', 'qux', $post); +}); + +it('can output relationship JSON values', function () { + $post = Post::factory()->create([ + 'author_id' => User::factory()->state([ + 'json' => ['foo' => 'bar'], + ]), + ]); + + livewire(PostsTable::class) + ->assertTableColumnStateSet('author.json.foo', 'bar', $post); +}); + +it('can output relationship nested JSON values', function () { + $post = Post::factory()->create([ + 'author_id' => User::factory()->state([ + 'json' => ['bar' => ['baz' => 'qux']], + ]), + ]); + + livewire(PostsTable::class) + ->assertTableColumnStateSet('author.json.bar.baz', 'qux', $post); +}); + it('can output values in a JSON array column of objects', function () { $post = Post::factory()->create([ 'json_array_of_objects' => [ From 202cab4ff2ffb40f286535c286c575789f223721 Mon Sep 17 00:00:00 2001 From: Dan Harrin Date: Mon, 3 Feb 2025 12:20:41 +0000 Subject: [PATCH 6/6] update snapshots --- .../it_can_generate_the_form_and_table_of_a_resource_class.snap | 2 ++ ...the_form_and_table_content_embedded_in_a_resource_class.snap | 2 ++ .../it_can_generate_the_resource_form_content.snap | 2 ++ 3 files changed, 6 insertions(+) diff --git a/tests/.pest/snapshots/src/Panels/Commands/Legacy/LegacyMakeResourceCommandTest/it_can_generate_the_form_and_table_of_a_resource_class.snap b/tests/.pest/snapshots/src/Panels/Commands/Legacy/LegacyMakeResourceCommandTest/it_can_generate_the_form_and_table_of_a_resource_class.snap index a9aef330bc7..6b88afc06f9 100644 --- a/tests/.pest/snapshots/src/Panels/Commands/Legacy/LegacyMakeResourceCommandTest/it_can_generate_the_form_and_table_of_a_resource_class.snap +++ b/tests/.pest/snapshots/src/Panels/Commands/Legacy/LegacyMakeResourceCommandTest/it_can_generate_the_form_and_table_of_a_resource_class.snap @@ -38,6 +38,8 @@ class PostResource extends Resource ->columnSpanFull(), Forms\Components\TextInput::make('title') ->required(), + Forms\Components\Textarea::make('json') + ->columnSpanFull(), Forms\Components\Textarea::make('json_array_of_objects') ->columnSpanFull(), ]); diff --git a/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_form_and_table_content_embedded_in_a_resource_class.snap b/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_form_and_table_content_embedded_in_a_resource_class.snap index 70c4c90a450..3459ffd5339 100644 --- a/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_form_and_table_content_embedded_in_a_resource_class.snap +++ b/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_form_and_table_content_embedded_in_a_resource_class.snap @@ -46,6 +46,8 @@ class PostResource extends Resource ->columnSpanFull(), TextInput::make('title') ->required(), + Textarea::make('json') + ->columnSpanFull(), Textarea::make('json_array_of_objects') ->columnSpanFull(), ]); diff --git a/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_resource_form_content.snap b/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_resource_form_content.snap index 41e7b42cac5..e56eb697dea 100644 --- a/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_resource_form_content.snap +++ b/tests/.pest/snapshots/src/Panels/Commands/MakeResourceCommandTest/it_can_generate_the_resource_form_content.snap @@ -29,6 +29,8 @@ class PostForm ->columnSpanFull(), TextInput::make('title') ->required(), + Textarea::make('json') + ->columnSpanFull(), Textarea::make('json_array_of_objects') ->columnSpanFull(), ]);