From faf2ab8384c0e656d00d644c0a69f215f3fb33f3 Mon Sep 17 00:00:00 2001 From: ug-christoph Date: Sat, 1 Nov 2025 09:58:04 +0100 Subject: [PATCH 1/9] Only merge cached casts for accessed attribute --- .../Eloquent/Concerns/HasAttributes.php | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 7591252b5148..d0aa73a70349 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -537,7 +537,7 @@ public function getAttributeValue($key) */ protected function getAttributeFromArray($key) { - return $this->getAttributes()[$key] ?? null; + return $this->getAttributes($key)[$key] ?? null; } /** @@ -1890,10 +1890,10 @@ protected function parseCasterClass($class) * * @return void */ - protected function mergeAttributesFromCachedCasts() + protected function mergeAttributesFromCachedCasts(?string $onlyMergeCachedCastsForThisKey = null) { - $this->mergeAttributesFromClassCasts(); - $this->mergeAttributesFromAttributeCasts(); + $this->mergeAttributesFromClassCasts($onlyMergeCachedCastsForThisKey); + $this->mergeAttributesFromAttributeCasts($onlyMergeCachedCastsForThisKey); } /** @@ -1901,9 +1901,13 @@ protected function mergeAttributesFromCachedCasts() * * @return void */ - protected function mergeAttributesFromClassCasts() + protected function mergeAttributesFromClassCasts(?string $onlyMergeCachedCastsForThisKey = null) { foreach ($this->classCastCache as $key => $value) { + if(!is_null($onlyMergeCachedCastsForThisKey) && $key !== $onlyMergeCachedCastsForThisKey) { + continue; + } + $caster = $this->resolveCasterClass($key); $this->attributes = array_merge( @@ -1920,9 +1924,12 @@ protected function mergeAttributesFromClassCasts() * * @return void */ - protected function mergeAttributesFromAttributeCasts() + protected function mergeAttributesFromAttributeCasts(?string $onlyMergeCachedCastsForThisKey = null) { foreach ($this->attributeCastCache as $key => $value) { + if(!is_null($onlyMergeCachedCastsForThisKey) && $key !== $onlyMergeCachedCastsForThisKey) { + continue; + } $attribute = $this->{Str::camel($key)}(); if ($attribute->get && ! $attribute->set) { @@ -1959,9 +1966,9 @@ protected function normalizeCastClassResponse($key, $value) * * @return array */ - public function getAttributes() + public function getAttributes(?string $onlyMergeCachedCastsForThisKey = null) { - $this->mergeAttributesFromCachedCasts(); + $this->mergeAttributesFromCachedCasts($onlyMergeCachedCastsForThisKey); return $this->attributes; } From 8c2a6fbe1917f2b18faa0e2a1011c558629adcb5 Mon Sep 17 00:00:00 2001 From: ug-christoph Date: Sat, 1 Nov 2025 10:46:21 +0100 Subject: [PATCH 2/9] Fix formatting of conditional statements --- .../Database/Eloquent/Concerns/HasAttributes.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index d0aa73a70349..bc3ec63aafbf 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1904,8 +1904,8 @@ protected function mergeAttributesFromCachedCasts(?string $onlyMergeCachedCastsF protected function mergeAttributesFromClassCasts(?string $onlyMergeCachedCastsForThisKey = null) { foreach ($this->classCastCache as $key => $value) { - if(!is_null($onlyMergeCachedCastsForThisKey) && $key !== $onlyMergeCachedCastsForThisKey) { - continue; + if (! is_null($onlyMergeCachedCastsForThisKey) && $key !== $onlyMergeCachedCastsForThisKey) { + continue; } $caster = $this->resolveCasterClass($key); @@ -1927,8 +1927,8 @@ protected function mergeAttributesFromClassCasts(?string $onlyMergeCachedCastsFo protected function mergeAttributesFromAttributeCasts(?string $onlyMergeCachedCastsForThisKey = null) { foreach ($this->attributeCastCache as $key => $value) { - if(!is_null($onlyMergeCachedCastsForThisKey) && $key !== $onlyMergeCachedCastsForThisKey) { - continue; + if (! is_null($onlyMergeCachedCastsForThisKey) && $key !== $onlyMergeCachedCastsForThisKey) { + continue; } $attribute = $this->{Str::camel($key)}(); From 4026ff4ed74080547ce43e52562e68491018e420 Mon Sep 17 00:00:00 2001 From: ug-christoph Date: Sat, 1 Nov 2025 10:48:20 +0100 Subject: [PATCH 3/9] remove extra space --- src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index bc3ec63aafbf..324108e13676 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1905,7 +1905,7 @@ protected function mergeAttributesFromClassCasts(?string $onlyMergeCachedCastsFo { foreach ($this->classCastCache as $key => $value) { if (! is_null($onlyMergeCachedCastsForThisKey) && $key !== $onlyMergeCachedCastsForThisKey) { - continue; + continue; } $caster = $this->resolveCasterClass($key); @@ -1928,7 +1928,7 @@ protected function mergeAttributesFromAttributeCasts(?string $onlyMergeCachedCas { foreach ($this->attributeCastCache as $key => $value) { if (! is_null($onlyMergeCachedCastsForThisKey) && $key !== $onlyMergeCachedCastsForThisKey) { - continue; + continue; } $attribute = $this->{Str::camel($key)}(); From 3ad3900f9d934551d9cdea5c27e576b838e3210e Mon Sep 17 00:00:00 2001 From: ug-christoph Date: Sat, 1 Nov 2025 10:53:58 +0100 Subject: [PATCH 4/9] Remove blank line in HasAttributes.php Remove unnecessary blank line in HasAttributes.php. --- src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 324108e13676..4a63c03e594e 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1907,7 +1907,6 @@ protected function mergeAttributesFromClassCasts(?string $onlyMergeCachedCastsFo if (! is_null($onlyMergeCachedCastsForThisKey) && $key !== $onlyMergeCachedCastsForThisKey) { continue; } - $caster = $this->resolveCasterClass($key); $this->attributes = array_merge( From 391dc2189930bd499c1f45681ccb9732861a5988 Mon Sep 17 00:00:00 2001 From: ug-christoph Date: Sat, 1 Nov 2025 11:09:46 +0100 Subject: [PATCH 5/9] fix test: The affected tests are quite brittle (they do an assertion on the exact call count to a method). The PRs goal is to directly reduce those calls. The test is a good example of where the optimization helps. In the tests ->id is called which causes a cast on the encrypted column. This is no expectedly not happening anymore and I have therefore just reduced the call count in the assertion by one to reflect this improvement --- .../Database/EloquentModelEncryptedCastingTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Integration/Database/EloquentModelEncryptedCastingTest.php b/tests/Integration/Database/EloquentModelEncryptedCastingTest.php index 0c87c7440022..7c637a756cba 100644 --- a/tests/Integration/Database/EloquentModelEncryptedCastingTest.php +++ b/tests/Integration/Database/EloquentModelEncryptedCastingTest.php @@ -189,7 +189,7 @@ public function testAsEncryptedCollection() ->with('{"key1":"value1"}') ->andReturn('encrypted-secret-collection-string-1'); $this->encrypter->expects('encryptString') - ->times(10) + ->times(9) ->with('{"key1":"value1","key2":"value2"}') ->andReturn('encrypted-secret-collection-string-2'); $this->encrypter->expects('decryptString') @@ -239,7 +239,7 @@ public function testAsEncryptedCollectionMap() ->with('[{"key1":"value1"}]') ->andReturn('encrypted-secret-collection-string-1'); $this->encrypter->expects('encryptString') - ->times(12) + ->times(11) ->with('[{"key1":"value1"},{"key2":"value2"}]') ->andReturn('encrypted-secret-collection-string-2'); $this->encrypter->expects('decryptString') @@ -295,7 +295,7 @@ public function testAsEncryptedArrayObject() ->with('encrypted-secret-array-string-1') ->andReturn('{"key1":"value1"}'); $this->encrypter->expects('encryptString') - ->times(10) + ->times(9) ->with('{"key1":"value1","key2":"value2"}') ->andReturn('encrypted-secret-array-string-2'); $this->encrypter->expects('decryptString') From 98fa005ae4c7a846d919a22ff663b5edbafcbfd7 Mon Sep 17 00:00:00 2001 From: ug-christoph Date: Sun, 2 Nov 2025 13:33:45 +0100 Subject: [PATCH 6/9] break code into functions to preserve the original signatures --- .../Eloquent/Concerns/HasAttributes.php | 97 ++++++++++++------- 1 file changed, 63 insertions(+), 34 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 4a63c03e594e..437abe2d1b0b 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -537,7 +537,9 @@ public function getAttributeValue($key) */ protected function getAttributeFromArray($key) { - return $this->getAttributes($key)[$key] ?? null; + $this->mergeAttributeFromCachedCasts($key); + + return $this->attributes[$key] ?? null; } /** @@ -1890,10 +1892,21 @@ protected function parseCasterClass($class) * * @return void */ - protected function mergeAttributesFromCachedCasts(?string $onlyMergeCachedCastsForThisKey = null) + protected function mergeAttributesFromCachedCasts() + { + $this->mergeAttributesFromClassCasts(); + $this->mergeAttributesFromAttributeCasts(); + } + + /** + * Merge the a cast class and attribute cast attribute back into the model. + * + * @return void + */ + protected function mergeAttributeFromCachedCasts(string $key) { - $this->mergeAttributesFromClassCasts($onlyMergeCachedCastsForThisKey); - $this->mergeAttributesFromAttributeCasts($onlyMergeCachedCastsForThisKey); + $this->mergeAttributeFromClassCasts($key); + $this->mergeAttributeFromAttributeCasts($key); } /** @@ -1901,21 +1914,29 @@ protected function mergeAttributesFromCachedCasts(?string $onlyMergeCachedCastsF * * @return void */ - protected function mergeAttributesFromClassCasts(?string $onlyMergeCachedCastsForThisKey = null) + protected function mergeAttributesFromClassCasts() { foreach ($this->classCastCache as $key => $value) { - if (! is_null($onlyMergeCachedCastsForThisKey) && $key !== $onlyMergeCachedCastsForThisKey) { - continue; - } - $caster = $this->resolveCasterClass($key); + $this->mergeAttributeFromClassCasts($key); + } + } - $this->attributes = array_merge( - $this->attributes, - $caster instanceof CastsInboundAttributes - ? [$key => $value] - : $this->normalizeCastClassResponse($key, $caster->set($this, $key, $value, $this->attributes)) - ); + private function mergeAttributeFromClassCasts(string $key): void + { + if (! isset($this->classCastCache[$key])) { + return; } + + $value = $this->classCastCache[$key]; + + $caster = $this->resolveCasterClass($key); + + $this->attributes = array_merge( + $this->attributes, + $caster instanceof CastsInboundAttributes + ? [$key => $value] + : $this->normalizeCastClassResponse($key, $caster->set($this, $key, $value, $this->attributes)) + ); } /** @@ -1923,29 +1944,37 @@ protected function mergeAttributesFromClassCasts(?string $onlyMergeCachedCastsFo * * @return void */ - protected function mergeAttributesFromAttributeCasts(?string $onlyMergeCachedCastsForThisKey = null) + protected function mergeAttributesFromAttributeCasts() { foreach ($this->attributeCastCache as $key => $value) { - if (! is_null($onlyMergeCachedCastsForThisKey) && $key !== $onlyMergeCachedCastsForThisKey) { - continue; - } - $attribute = $this->{Str::camel($key)}(); + $this->mergeAttributeFromAttributeCasts($key); + } + } - if ($attribute->get && ! $attribute->set) { - continue; - } + private function mergeAttributeFromAttributeCasts(string $key): void + { + if (! isset($this->attributeCastCache[$key])) { + return; + } - $callback = $attribute->set ?: function ($value) use ($key) { - $this->attributes[$key] = $value; - }; + $value = $this->attributeCastCache[$key]; - $this->attributes = array_merge( - $this->attributes, - $this->normalizeCastClassResponse( - $key, $callback($value, $this->attributes) - ) - ); + $attribute = $this->{Str::camel($key)}(); + + if ($attribute->get && ! $attribute->set) { + return; } + + $callback = $attribute->set ?: function ($value) use ($key) { + $this->attributes[$key] = $value; + }; + + $this->attributes = array_merge( + $this->attributes, + $this->normalizeCastClassResponse( + $key, $callback($value, $this->attributes) + ) + ); } /** @@ -1965,9 +1994,9 @@ protected function normalizeCastClassResponse($key, $value) * * @return array */ - public function getAttributes(?string $onlyMergeCachedCastsForThisKey = null) + public function getAttributes() { - $this->mergeAttributesFromCachedCasts($onlyMergeCachedCastsForThisKey); + $this->mergeAttributesFromCachedCasts(); return $this->attributes; } From a09a642f90e138db93c924326fa1fd85fa306244 Mon Sep 17 00:00:00 2001 From: ug-christoph Date: Sun, 2 Nov 2025 13:36:39 +0100 Subject: [PATCH 7/9] fix styling --- src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 437abe2d1b0b..2cb6eb1c8da3 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1947,7 +1947,7 @@ private function mergeAttributeFromClassCasts(string $key): void protected function mergeAttributesFromAttributeCasts() { foreach ($this->attributeCastCache as $key => $value) { - $this->mergeAttributeFromAttributeCasts($key); + $this->mergeAttributeFromAttributeCasts($key); } } From f9fed571a68269c8fc0fce126ae299d892d902df Mon Sep 17 00:00:00 2001 From: ug-christoph Date: Tue, 4 Nov 2025 18:29:28 +0100 Subject: [PATCH 8/9] add test for mutator - cast dependency --- ...DatabaseEloquentModelCustomCastingTest.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php b/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php index e9a4fbab68e4..0d2727d6688e 100644 --- a/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php +++ b/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php @@ -287,6 +287,16 @@ public function testSetToUndefinedCast() $model->undefined_cast_column = 'Glāžšķūņu rūķīši'; } + + public function testMutatorCanDependOnAnotherCastedAttribute() + { + $model = new TestEloquentModelWithCustomCast([ + 'address_line_one' => '110 Kingsbrook St.', + 'address_line_two' => 'My Childhood House', + ]); + $model->address->lineOne = 'Changed St.'; + $this->assertSame('Changed St. (My Childhood House)', $model->address_string); + } } class TestEloquentModelWithCustomCast extends Model @@ -319,6 +329,27 @@ class TestEloquentModelWithCustomCast extends Model 'anniversary_on_with_object_caching' => DateTimezoneCasterWithObjectCaching::class.':America/New_York', 'anniversary_on_without_object_caching' => DateTimezoneCasterWithoutObjectCaching::class.':America/New_York', ]; + + + /** + * A computed attribute that depends on another casted attribute. + * + * This simulates a mutator that uses the value of a casted property. + */ + protected function addressString(): \Illuminate\Database\Eloquent\Casts\Attribute + { + return \Illuminate\Database\Eloquent\Casts\Attribute::get(function () { + $address = $this->address; + + // If mergeAttributesFromClassCasts() hasn't prepared casts properly, + // this could be an array instead of an Address instance. + if (! $address instanceof Address) { + throw new \RuntimeException('Address was not cast before mutator access.'); + } + + return "{$address->lineOne} ({$address->lineTwo})"; + }); + } } class HashCaster implements CastsInboundAttributes From cca9ca99216dc526240467f56b906ab9076f247a Mon Sep 17 00:00:00 2001 From: ug-christoph Date: Tue, 4 Nov 2025 18:31:41 +0100 Subject: [PATCH 9/9] fix styling --- .../Database/DatabaseEloquentModelCustomCastingTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php b/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php index 0d2727d6688e..4a7825f62d86 100644 --- a/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php +++ b/tests/Integration/Database/DatabaseEloquentModelCustomCastingTest.php @@ -330,7 +330,6 @@ class TestEloquentModelWithCustomCast extends Model 'anniversary_on_without_object_caching' => DateTimezoneCasterWithoutObjectCaching::class.':America/New_York', ]; - /** * A computed attribute that depends on another casted attribute. *