[4.x] Bug: Nested JSON fields break with same-name helper/accessor method on model (#18199)

* snapshot updates

* tests and test model/resource updates

* `hasAttribute()` check

As a workaround for Laravel's false-positives in `hasRelationship()`.

* Update Post.php

Signed-off-by: Dan Harrin <git@danharrin.com>

* Update config method to use array syntax

Signed-off-by: Dan Harrin <git@danharrin.com>

* Update HasCellState.php

Signed-off-by: Dan Harrin <git@danharrin.com>

* clean up elsewhere

---------

Signed-off-by: Dan Harrin <git@danharrin.com>
Co-authored-by: Dan Harrin <git@danharrin.com>
This commit is contained in:
Casey Dwyer
2025-10-14 10:08:15 +01:00
committed by GitHub
co-authored by Dan Harrin
parent 7986448fac
commit 72ead269c9
24 changed files with 118 additions and 7 deletions
@@ -298,7 +298,7 @@ class CheckboxList extends Field implements Contracts\CanDisableOptions, Contrac
$record = $this->getModelInstance();
if (! $record->isRelation($name)) {
if ($record->hasAttribute($name) || (! $record->isRelation($name))) {
throw new LogicException("The relationship [{$name}] does not exist on the model [{$this->getModel()}].");
}
@@ -568,6 +568,12 @@ class ModalTableSelect extends Field
$relationshipName = $this->getRelationshipName();
foreach (explode('.', $relationshipName) as $nestedRelationshipName) {
if ($record->hasAttribute($nestedRelationshipName)) {
$relationship = null;
break;
}
if (! $record->isRelation($nestedRelationshipName)) {
$relationship = null;
@@ -203,7 +203,7 @@ class MorphToSelect extends Component
$relationshipName = $this->getName();
if (! $record->isRelation($relationshipName)) {
if ($record->hasAttribute($relationshipName) || (! $record->isRelation($relationshipName))) {
throw new LogicException("The relationship [{$relationshipName}] does not exist on the model [{$this->getModel()}].");
}
+1 -1
View File
@@ -1119,7 +1119,7 @@ class Repeater extends Field implements CanConcealComponents, HasExtraItemAction
$relationshipName = $this->getRelationshipName();
if (! $record->isRelation($relationshipName)) {
if ($record->hasAttribute($relationshipName) || (! $record->isRelation($relationshipName))) {
throw new LogicException("The relationship [{$relationshipName}] does not exist on the model [{$this->getModel()}].");
}
+6
View File
@@ -1255,6 +1255,12 @@ class Select extends Field implements Contracts\CanDisableOptions, Contracts\Has
$relationshipName = $this->getRelationshipName();
foreach (explode('.', $relationshipName) as $nestedRelationshipName) {
if ($record->hasAttribute($nestedRelationshipName)) {
$relationship = null;
break;
}
if (! $record->isRelation($nestedRelationshipName)) {
$relationship = null;
@@ -253,6 +253,12 @@ class TableSelect extends Field
$relationshipName = $this->getRelationshipName();
foreach (explode('.', $relationshipName) as $nestedRelationshipName) {
if ($record->hasAttribute($nestedRelationshipName)) {
$relationship = null;
break;
}
if (! $record->isRelation($nestedRelationshipName)) {
$relationship = null;
@@ -71,7 +71,7 @@ trait BelongsToTenant
{
$relationshipName = static::getTenantOwnershipRelationshipName();
if (! $record->isRelation($relationshipName)) {
if ($record->hasAttribute($relationshipName) || (! $record->isRelation($relationshipName))) {
$resourceClass = static::class;
$recordClass = $record::class;
@@ -93,7 +93,7 @@ trait BelongsToTenant
{
$relationshipName = static::getTenantRelationshipName();
if (! $tenant->isRelation($relationshipName)) {
if ($tenant->hasAttribute($relationshipName) || (! $tenant->isRelation($relationshipName))) {
$resourceClass = static::class;
$tenantClass = $tenant::class;
@@ -30,6 +30,12 @@ trait CanGetStateFromRelationships
$relationship = null;
foreach (explode('.', $statePath ?? $this->getStateRelationshipName()) as $nestedRelationshipName) {
if ($record->hasAttribute($nestedRelationshipName)) {
$relationship = null;
break;
}
if (! $record->isRelation($nestedRelationshipName)) {
$relationship = null;
+26 -2
View File
@@ -188,6 +188,10 @@ trait HasCellState
return false;
}
if ($record->hasAttribute((string) str($name)->before('.'))) {
return false;
}
return $record->isRelation((string) str($name)->before('.'));
}
@@ -221,6 +225,10 @@ trait HasCellState
$relationship = null;
foreach ($nameParts as $namePart) {
if ($record->hasAttribute($namePart)) {
break;
}
if (! $record->isRelation($namePart)) {
break;
}
@@ -329,6 +337,10 @@ trait HasCellState
$lastPart = array_pop($nameParts);
foreach ($nameParts as $namePart) {
if ($record->hasAttribute($namePart)) {
break;
}
if (! $record->isRelation($namePart)) {
break;
}
@@ -352,6 +364,10 @@ trait HasCellState
$lastPart = array_pop($nameParts);
foreach ($nameParts as $namePart) {
if ($record->hasAttribute($namePart)) {
break;
}
if (! $record->isRelation($namePart)) {
break;
}
@@ -375,6 +391,10 @@ trait HasCellState
$inverseRelationshipParts = [];
foreach ($nameParts as $namePart) {
if ($record->hasAttribute($namePart)) {
break;
}
if (! $record->isRelation($namePart)) {
break;
}
@@ -391,11 +411,11 @@ trait HasCellState
)
->camel();
if (! $record->isRelation($inverseNestedRelationshipName)) {
if ($record->hasAttribute($inverseNestedRelationshipName) || (! $record->isRelation($inverseNestedRelationshipName))) {
// The conventional relationship doesn't exist, but we can
// attempt to use the original relationship name instead.
if (! $record->isRelation($namePart)) {
if ($record->hasAttribute($namePart) || (! $record->isRelation($namePart))) {
$recordClass = $record::class;
throw new LogicException("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.");
@@ -424,6 +444,10 @@ trait HasCellState
$relationshipParts = [];
foreach ($nameParts as $namePart) {
if ($record->hasAttribute($namePart)) {
break;
}
if (! $record->isRelation($namePart)) {
break;
}
@@ -786,6 +786,12 @@ class SelectColumn extends Column implements Editable, HasEmbeddedView
$relationshipName = $this->getOptionsRelationshipName();
foreach (explode('.', $relationshipName) as $nestedRelationshipName) {
if ($record->hasAttribute($nestedRelationshipName)) {
$relationship = null;
break;
}
if (! $record->isRelation($nestedRelationshipName)) {
$relationship = null;
@@ -64,6 +64,12 @@ trait HasRelationship
$relationshipName = $this->getRelationshipName();
foreach (explode('.', $relationshipName) as $nestedRelationshipName) {
if ($record->hasAttribute($nestedRelationshipName)) {
$relationship = null;
break;
}
if (! $record->isRelation($nestedRelationshipName)) {
$relationship = null;
@@ -232,6 +232,12 @@ class IsRelatedToOperator extends Operator
$relationship = null;
foreach (explode('.', $constraint->getRelationshipName()) as $nestedRelationshipName) {
if ($record->hasAttribute($nestedRelationshipName)) {
$relationship = null;
break;
}
if (! $record->isRelation($nestedRelationshipName)) {
$relationship = null;
+6
View File
@@ -393,6 +393,12 @@ class Group extends Component
$relationship = null;
foreach (explode('.', $name ?? $this->getRelationshipName()) as $nestedRelationshipName) {
if ($record->hasAttribute($nestedRelationshipName)) {
$relationship = null;
break;
}
if (! $record->isRelation($nestedRelationshipName)) {
$relationship = null;
@@ -248,6 +248,12 @@ trait CanSearchRecords
$relationship = null;
foreach (str($name)->beforeLast('.')->explode('.')->all() as $nestedRelationshipName) {
if ($record->hasAttribute($nestedRelationshipName)) {
$relationship = null;
break;
}
if (! $record->isRelation($nestedRelationshipName)) {
$relationship = null;
@@ -31,6 +31,8 @@ class PostForm
->columnSpanFull(),
TextInput::make('title')
->required(),
Textarea::make('config')
->columnSpanFull(),
Textarea::make('json')
->columnSpanFull(),
Textarea::make('json_array_of_objects')
@@ -47,6 +47,8 @@ class CreatePostWithFields extends Component implements HasActions, HasSchemas
->columnSpanFull(),
TextInput::make('title')
->required(),
Textarea::make('config')
->columnSpanFull(),
Textarea::make('json')
->columnSpanFull(),
Textarea::make('json_array_of_objects')
@@ -39,6 +39,8 @@ class PostResource extends Resource
->columnSpanFull(),
Forms\Components\TextInput::make('title')
->required(),
Forms\Components\Textarea::make('config')
->columnSpanFull(),
Forms\Components\Textarea::make('json')
->columnSpanFull(),
Forms\Components\Textarea::make('json_array_of_objects')
@@ -51,6 +51,8 @@ class PostResource extends Resource
->columnSpanFull(),
TextInput::make('title')
->required(),
Textarea::make('config')
->columnSpanFull(),
Textarea::make('json')
->columnSpanFull(),
Textarea::make('json_array_of_objects')
@@ -77,6 +79,9 @@ class PostResource extends Resource
->placeholder('-')
->columnSpanFull(),
TextEntry::make('title'),
TextEntry::make('config')
->placeholder('-')
->columnSpanFull(),
TextEntry::make('json')
->placeholder('-')
->columnSpanFull(),
@@ -30,6 +30,8 @@ class PostForm
->columnSpanFull(),
TextInput::make('title')
->required(),
Textarea::make('config')
->columnSpanFull(),
Textarea::make('json')
->columnSpanFull(),
Textarea::make('json_array_of_objects')
@@ -26,6 +26,9 @@ class PostInfolist
->placeholder('-')
->columnSpanFull(),
TextEntry::make('title'),
TextEntry::make('config')
->placeholder('-')
->columnSpanFull(),
TextEntry::make('json')
->placeholder('-')
->columnSpanFull(),
@@ -16,6 +16,7 @@ return new class extends Migration
$table->unsignedTinyInteger('rating')->default(0);
$table->json('tags')->nullable();
$table->string('title');
$table->json('config')->nullable();
$table->json('json')->nullable();
$table->json('json_array_of_objects')->nullable();
$table->string('string_backed_enum')->nullable();
@@ -97,6 +97,7 @@ class PostsTable extends Component implements HasActions, HasSchemas, Tables\Con
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('json_array_of_objects.*.value'),
Tables\Columns\TextColumn::make('config.setting'),
Tables\Columns\TextColumn::make('author.json.foo')
->searchable()
->sortable(),
+6
View File
@@ -25,6 +25,7 @@ class Post extends Model
'tags' => 'array',
'json' => 'array',
'json_array_of_objects' => 'array',
'config' => 'array',
];
}
@@ -35,6 +36,11 @@ class Post extends Model
return $this->belongsTo(User::class, 'author_id');
}
public function config(string $key): mixed
{
return $this->config[$key] ?? null;
}
protected static function newFactory()
{
return PostFactory::new();
+9
View File
@@ -320,6 +320,15 @@ it('can output values in a JSON array column of objects', function (): void {
->assertTableColumnStateSet('json_array_of_objects.*.value', ['foo', 'bar', 'baz'], $post);
});
it('can output values in a JSON column with a non-relationship accessor method', function (): void {
$post = Post::factory()->create([
'config' => ['setting' => 'foo'],
]);
livewire(PostsTable::class)
->assertTableColumnStateSet('config.setting', 'foo', $post);
});
it('can state whether a column has extra attributes', function (): void {
$post = Post::factory()->create();