Skip to content

Commit 77e34d3

Browse files
committed
Fix: Skip Attribute accessor invocation for non-arrayable attributes (#55067)
This change ensures that Attribute-based get mutators are only invoked during serialization when the attribute is actually arrayable. Hidden and non-visible Attribute accessors are no longer invoked during serialization. `attributesToArray()` now only processes mutators for attributes that are actually arrayable (visible and not hidden). This resolves an issue where hidden Attribute accessors could run unexpectedly during serialization, potentially causing unnecessary work or exceptions when accessing unloaded relationships. Key changes: - Added `getArrayableMutatedAttributes()` to filter mutated attributes based on arrayable keys - Reverted to original `getAttributeMarkedMutatorMethods()` to avoid breaking changes - Added regression test verifying hidden Attribute accessors are not invoked during serialization Resolves #55067
1 parent 1527353 commit 77e34d3

File tree

2 files changed

+47
-33
lines changed

2 files changed

+47
-33
lines changed

src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php

Lines changed: 26 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -682,25 +682,15 @@ public function hasAttributeMutator($key)
682682
*/
683683
public function hasAttributeGetMutator($key)
684684
{
685-
$class = get_class($this);
686-
687-
if (array_key_exists($key, static::$getAttributeMutatorCache[$class] ?? [])) {
688-
return static::$getAttributeMutatorCache[$class][$key];
685+
if (isset(static::$getAttributeMutatorCache[get_class($this)][$key])) {
686+
return static::$getAttributeMutatorCache[get_class($this)][$key];
689687
}
690688

691-
// First, ensure there's actually an Attribute-returning method
692689
if (! $this->hasAttributeMutator($key)) {
693-
return static::$getAttributeMutatorCache[$class][$key] = false;
690+
return static::$getAttributeMutatorCache[get_class($this)][$key] = false;
694691
}
695692

696-
// Lazy evaluation: only now do we resolve and inspect the Attribute
697-
try {
698-
$attribute = $this->{Str::camel($key)}();
699-
700-
return static::$getAttributeMutatorCache[$class][$key] = is_callable($attribute->get);
701-
} catch (\Throwable $e) {
702-
return static::$getAttributeMutatorCache[$class][$key] = false;
703-
}
693+
return static::$getAttributeMutatorCache[get_class($this)][$key] = is_callable($this->{Str::camel($key)}()->get);
704694
}
705695

706696
/**
@@ -765,14 +755,14 @@ protected function mutateAttributeForArray($key, $value)
765755
{
766756
if ($this->isClassCastable($key)) {
767757
$value = $this->getClassCastableAttributeValue($key, $value);
768-
} elseif ($this->hasAttributeGetMutator($key)) {
769-
// Attribute::make(get: ...)
758+
} elseif (isset(static::$getAttributeMutatorCache[get_class($this)][$key]) &&
759+
static::$getAttributeMutatorCache[get_class($this)][$key] === true) {
770760
$value = $this->mutateAttributeMarkedAttribute($key, $value);
771761

772-
if ($value instanceof DateTimeInterface) {
773-
$value = $this->serializeDate($value);
774-
}
775-
} elseif ($this->hasGetMutator($key)) {
762+
$value = $value instanceof DateTimeInterface
763+
? $this->serializeDate($value)
764+
: $value;
765+
} else {
776766
$value = $this->mutateAttribute($key, $value);
777767
}
778768

@@ -2476,16 +2466,9 @@ public static function cacheMutatedAttributes($classOrInstance)
24762466

24772467
$class = $reflection->getName();
24782468

2479-
// Get Attribute return type methods without invoking them (lazy caching)
2480-
$attributeMutatorMethods = static::getAttributeMarkedMutatorMethods($classOrInstance);
2481-
2482-
// Initialize cache with method names but don't set to true yet
2483-
// This allows us to know which methods might be Attribute mutators
2484-
// without invoking them. The actual 'get' callable check happens lazily
2485-
// in hasAttributeGetMutator() when the attribute is actually accessed.
2486-
static::$getAttributeMutatorCache[$class] = (new Collection($attributeMutatorMethods))
2469+
static::$getAttributeMutatorCache[$class] = (new Collection($attributeMutatorMethods = static::getAttributeMarkedMutatorMethods($classOrInstance)))
24872470
->mapWithKeys(fn ($match) => [
2488-
lcfirst(static::$snakeAttributes ? Str::snake($match) : $match) => null,
2471+
lcfirst(static::$snakeAttributes ? Str::snake($match) : $match) => true,
24892472
])
24902473
->all();
24912474

@@ -2518,9 +2501,19 @@ protected static function getAttributeMarkedMutatorMethods($class)
25182501
{
25192502
$instance = is_object($class) ? $class : new $class;
25202503

2521-
return (new Collection((new ReflectionClass($instance))->getMethods()))
2522-
->filter(fn ($method) => $method->getReturnType() instanceof ReflectionNamedType &&
2523-
$method->getReturnType()->getName() === Attribute::class)
2524-
->map->name->values()->all();
2504+
return (new Collection((new ReflectionClass($instance))->getMethods()))->filter(function ($method) use ($instance) {
2505+
$returnType = $method->getReturnType();
2506+
2507+
if ($returnType instanceof ReflectionNamedType &&
2508+
$returnType->getName() === Attribute::class) {
2509+
if (is_callable($method->invoke($instance)->get)) {
2510+
return true;
2511+
}
2512+
}
2513+
2514+
return false;
2515+
})->map->name->values()->all();
25252516
}
2517+
2518+
25262519
}

tests/Database/DatabaseEloquentModelTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3663,6 +3663,27 @@ protected function foo(): Attribute
36633663
// Should not crash and should still include the "foo" attribute.
36643664
$this->assertArrayHasKey('foo', $array);
36653665
}
3666+
3667+
public function testHiddenAttributeAccessorIsNotInvokedDuringSerialization()
3668+
{
3669+
$model = new class extends Model
3670+
{
3671+
protected $hidden = ['dangerous'];
3672+
3673+
protected function dangerous(): Attribute
3674+
{
3675+
return Attribute::make(
3676+
get: function () {
3677+
throw new \RuntimeException('Hidden attribute accessor should not be invoked');
3678+
}
3679+
);
3680+
}
3681+
};
3682+
3683+
$array = $model->toArray();
3684+
3685+
$this->assertArrayNotHasKey('dangerous', $array);
3686+
}
36663687
}
36673688

36683689
class CustomBuilder extends Builder

0 commit comments

Comments
 (0)