Skip to content

Commit 55ab6fb

Browse files
committed
Merge branch '4.x' into 5.x
2 parents 430dba1 + dec0ce3 commit 55ab6fb

3 files changed

Lines changed: 188 additions & 10 deletions

File tree

packages/support/src/Concerns/HasCellState.php

Lines changed: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Illuminate\Database\Eloquent\Model;
1010
use Illuminate\Database\Eloquent\Relations\BelongsTo;
1111
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
12+
use Illuminate\Database\Eloquent\Relations\MorphTo;
1213
use Illuminate\Database\Eloquent\Relations\Relation;
1314
use Illuminate\Support\Arr;
1415
use Illuminate\Support\Collection;
@@ -45,6 +46,8 @@ trait HasCellState
4546

4647
protected ?string $attributeNameCache = null;
4748

49+
protected ?bool $hasNestedMorphToRelationshipCache = null;
50+
4851
public function inverseRelationship(?string $name): static
4952
{
5053
$this->inverseRelationshipName = $name;
@@ -217,7 +220,9 @@ public function queriesRelationships(Model $record): bool
217220

218221
public function getRelationship(Model $record, ?string $relationshipName = null): ?Relation
219222
{
220-
if ($this->relationshipCache) {
223+
$hasNestedMorphToRelationship = $this->hasNestedMorphToRelationship($record);
224+
225+
if ($this->relationshipCache && (! $hasNestedMorphToRelationship)) {
221226
return $this->relationshipCache;
222227
}
223228

@@ -249,15 +254,23 @@ public function getRelationship(Model $record, ?string $relationshipName = null)
249254
$record = $relationship->getRelated();
250255
}
251256

257+
if ($hasNestedMorphToRelationship) {
258+
return $relationship;
259+
}
260+
252261
return $this->relationshipCache = $relationship;
253262
}
254263

255264
public function hasMultipleRelationship(Model $record): bool
256265
{
257-
if (isset($this->hasMultipleRelationshipCache)) {
266+
$hasNestedMorphToRelationship = $this->hasNestedMorphToRelationship($record);
267+
268+
if (isset($this->hasMultipleRelationshipCache) && (! $hasNestedMorphToRelationship)) {
258269
return $this->hasMultipleRelationshipCache;
259270
}
260271

272+
$hasMultipleRelationship = false;
273+
261274
$relationships = explode('.', $this->getRelationshipName($record));
262275

263276
while (count($relationships)) {
@@ -266,7 +279,9 @@ public function hasMultipleRelationship(Model $record): bool
266279
$currentRelationshipValue = $record->getRelationValue($currentRelationshipName);
267280

268281
if ($currentRelationshipValue instanceof Collection) {
269-
return $this->hasMultipleRelationshipCache = true;
282+
$hasMultipleRelationship = true;
283+
284+
break;
270285
}
271286

272287
if (! $currentRelationshipValue instanceof Model) {
@@ -280,7 +295,11 @@ public function hasMultipleRelationship(Model $record): bool
280295
$record = $currentRelationshipValue;
281296
}
282297

283-
return $this->hasMultipleRelationshipCache = false;
298+
if ($hasNestedMorphToRelationship) {
299+
return $hasMultipleRelationship;
300+
}
301+
302+
return $this->hasMultipleRelationshipCache = $hasMultipleRelationship;
284303
}
285304

286305
/**
@@ -339,7 +358,9 @@ public function getRelationshipResults(Model $record, ?array $relationships = nu
339358

340359
public function getAttributeName(Model $record): string
341360
{
342-
if ($this->attributeNameCache !== null) {
361+
$hasNestedMorphToRelationship = $this->hasNestedMorphToRelationship($record);
362+
363+
if (($this->attributeNameCache !== null) && (! $hasNestedMorphToRelationship)) {
343364
return $this->attributeNameCache;
344365
}
345366

@@ -365,12 +386,20 @@ public function getAttributeName(Model $record): string
365386
$record = $record->{$namePart}()->getRelated();
366387
}
367388

368-
return $this->attributeNameCache = Arr::first([...$nameParts, $lastPart]);
389+
$attributeName = Arr::first([...$nameParts, $lastPart]);
390+
391+
if ($hasNestedMorphToRelationship) {
392+
return $attributeName;
393+
}
394+
395+
return $this->attributeNameCache = $attributeName;
369396
}
370397

371398
public function getFullAttributeName(Model $record): string
372399
{
373-
if ($this->fullAttributeNameCache !== null) {
400+
$hasNestedMorphToRelationship = $this->hasNestedMorphToRelationship($record);
401+
402+
if (($this->fullAttributeNameCache !== null) && (! $hasNestedMorphToRelationship)) {
374403
return $this->fullAttributeNameCache;
375404
}
376405

@@ -396,7 +425,13 @@ public function getFullAttributeName(Model $record): string
396425
$record = $record->{$namePart}()->getRelated();
397426
}
398427

399-
return $this->fullAttributeNameCache = implode('.', [...$nameParts, $lastPart]);
428+
$fullAttributeName = implode('.', [...$nameParts, $lastPart]);
429+
430+
if ($hasNestedMorphToRelationship) {
431+
return $fullAttributeName;
432+
}
433+
434+
return $this->fullAttributeNameCache = $fullAttributeName;
400435
}
401436

402437
public function getInverseRelationshipName(Model $record): string
@@ -452,7 +487,9 @@ public function getInverseRelationshipName(Model $record): string
452487

453488
public function getRelationshipName(Model $record): ?string
454489
{
455-
if ($this->relationshipNameCache !== null) {
490+
$hasNestedMorphToRelationship = $this->hasNestedMorphToRelationship($record);
491+
492+
if (($this->relationshipNameCache !== null) && (! $hasNestedMorphToRelationship)) {
456493
return $this->relationshipNameCache;
457494
}
458495

@@ -480,7 +517,58 @@ public function getRelationshipName(Model $record): ?string
480517
$record = $record->{$namePart}()->getRelated();
481518
}
482519

483-
return $this->relationshipNameCache = implode('.', $relationshipParts);
520+
$relationshipName = implode('.', $relationshipParts);
521+
522+
if ($hasNestedMorphToRelationship) {
523+
return $relationshipName;
524+
}
525+
526+
return $this->relationshipNameCache = $relationshipName;
527+
}
528+
529+
/**
530+
* When the name of a cell traverses a `MorphTo` relationship and then continues deeper,
531+
* the remainder of the path resolves against the concrete related model of each record,
532+
* which may differ between records and is unknown while the query is being built. In
533+
* that case, the relationship and attribute names cannot be cached across records and
534+
* must be resolved fresh for each one.
535+
*/
536+
public function hasNestedMorphToRelationship(Model $record): bool
537+
{
538+
if (isset($this->hasNestedMorphToRelationshipCache)) {
539+
return $this->hasNestedMorphToRelationshipCache;
540+
}
541+
542+
$name = $this->getName();
543+
544+
if (! str($name)->contains('.')) {
545+
return $this->hasNestedMorphToRelationshipCache = false;
546+
}
547+
548+
$nameParts = explode('.', $name);
549+
array_pop($nameParts);
550+
551+
$lastNamePartIndex = count($nameParts) - 1;
552+
553+
foreach ($nameParts as $namePartIndex => $namePart) {
554+
if ($record->hasAttribute($namePart)) {
555+
break;
556+
}
557+
558+
if (! $record->isRelation($namePart)) {
559+
break;
560+
}
561+
562+
$relationship = $record->{$namePart}();
563+
564+
if ($relationship instanceof MorphTo) {
565+
return $this->hasNestedMorphToRelationshipCache = ($namePartIndex < $lastNamePartIndex);
566+
}
567+
568+
$record = $relationship->getRelated();
569+
}
570+
571+
return $this->hasNestedMorphToRelationshipCache = false;
484572
}
485573

486574
protected function cacheState(Closure $state): mixed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Filament\Tests\Fixtures\Livewire;
4+
5+
use Filament\Actions\Concerns\InteractsWithActions;
6+
use Filament\Actions\Contracts\HasActions;
7+
use Filament\Schemas\Concerns\InteractsWithSchemas;
8+
use Filament\Schemas\Contracts\HasSchemas;
9+
use Filament\Tables;
10+
use Filament\Tables\Table;
11+
use Filament\Tests\Fixtures\Models\Image;
12+
use Illuminate\Contracts\View\View;
13+
use Livewire\Component;
14+
15+
class ImagesTable extends Component implements HasActions, HasSchemas, Tables\Contracts\HasTable
16+
{
17+
use InteractsWithActions;
18+
use InteractsWithSchemas;
19+
use Tables\Concerns\InteractsWithTable;
20+
21+
public function table(Table $table): Table
22+
{
23+
return $table
24+
->query(Image::query())
25+
->columns([
26+
Tables\Columns\TextColumn::make('url'),
27+
Tables\Columns\TextColumn::make('imageable.team.name')
28+
->label('Imageable Team (MorphTo -> BelongsTo)'),
29+
Tables\Columns\TextColumn::make('imageable.company.name')
30+
->label('Imageable Company (MorphTo -> BelongsTo / BelongsToThrough)'),
31+
]);
32+
}
33+
34+
public function render(): View
35+
{
36+
return view('livewire.table');
37+
}
38+
}

tests/src/Tables/ColumnTest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use Filament\Tables\Columns\IconColumn;
44
use Filament\Tables\Columns\TextColumn;
55
use Filament\Tests\Fixtures\Livewire\CustomDataTable;
6+
use Filament\Tests\Fixtures\Livewire\ImagesTable;
67
use Filament\Tests\Fixtures\Livewire\PostsTable;
78
use Filament\Tests\Fixtures\Livewire\PostsTableWithColumnIndividualSearchTermSplittingDisabled;
89
use Filament\Tests\Fixtures\Livewire\PostsTableWithColumnIndividualSearchTermSplittingEnabled;
@@ -756,6 +757,57 @@
756757
});
757758

758759
describe('relationship columns', function (): void {
760+
it('can output the state of a nested relationship through a `MorphTo` relationship', function (): void {
761+
$team = Team::factory()->create(['name' => 'Team Alpha']);
762+
$user = User::factory()->create([
763+
'name' => 'Alice',
764+
'team_id' => $team->id,
765+
]);
766+
$image = Image::factory()->for($user, 'imageable')->create();
767+
768+
livewire(ImagesTable::class)
769+
->assertTableColumnStateSet('imageable.team.name', 'Team Alpha', $image)
770+
->assertTableColumnStateNotSet('imageable.team.name', 'Alice', $image);
771+
});
772+
773+
it('can output the state of multiple nested relationships through the same `MorphTo` relationship', function (): void {
774+
$company = Company::factory()->create(['name' => 'Acme Corporation']);
775+
$team = Team::factory()->create([
776+
'name' => 'Team Alpha',
777+
'company_id' => $company->id,
778+
]);
779+
$user = User::factory()->create([
780+
'name' => 'Alice',
781+
'team_id' => $team->id,
782+
]);
783+
$image = Image::factory()->for($user, 'imageable')->create();
784+
785+
livewire(ImagesTable::class)
786+
->assertTableColumnStateSet('imageable.team.name', 'Team Alpha', $image)
787+
->assertTableColumnStateSet('imageable.company.name', 'Acme Corporation', $image);
788+
});
789+
790+
it('can output the state of nested relationships through a `MorphTo` relationship with mixed related model types', function (): void {
791+
$userCompany = Company::factory()->create(['name' => 'Acme Corporation']);
792+
$team = Team::factory()->create(['company_id' => $userCompany->id]);
793+
$user = User::factory()->create([
794+
'name' => 'Alice',
795+
'team_id' => $team->id,
796+
]);
797+
$userImage = Image::factory()->for($user, 'imageable')->create();
798+
799+
$profileCompany = Company::factory()->create(['name' => 'Globex Corporation']);
800+
$profile = Profile::factory()->create([
801+
'user_id' => $user->id,
802+
'company_id' => $profileCompany->id,
803+
]);
804+
$profileImage = Image::factory()->for($profile, 'imageable')->create();
805+
806+
livewire(ImagesTable::class)
807+
->assertTableColumnStateSet('imageable.company.name', 'Acme Corporation', $userImage)
808+
->assertTableColumnStateSet('imageable.company.name', 'Globex Corporation', $profileImage);
809+
});
810+
759811
it('can search and sort by relationship column when both tables have the same column name', function (): void {
760812
$teamAlpha = Team::factory()->create(['name' => 'Team Alpha']);
761813
$teamBeta = Team::factory()->create(['name' => 'Team Beta']);

0 commit comments

Comments
 (0)