From f1a3bef2a7278cc390b4063b8b3b7c097ca44910 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Mon, 29 Sep 2025 02:42:18 -0300 Subject: [PATCH 01/10] [12.x] Fixes incorrectly serializing virtual properties --- src/Illuminate/Database/Eloquent/Model.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 90260a57ca32..41148ee185b4 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -2595,7 +2595,15 @@ public function __sleep() $this->relationAutoloadCallback = null; $this->relationAutoloadContext = null; - return array_keys(get_object_vars($this)); + // When serializing the model, we may accidentally catch up some virtual properties. + // We will cast the model to a native array to skip them and then apply a function + // to clean each of the property names which is miles faster than using a regex. + return array_map( + fn ($key) => $key[0] === "\0" + ? substr($key, strpos($key, "\0", 1) + 1) + : $key, + array_keys((array) $this) + ); } /** From 1e2158fc2ab8d70028b0b4a5841554cea0fcbcd4 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Mon, 29 Sep 2025 16:43:09 -0300 Subject: [PATCH 02/10] Adds tests to check if virtual properties are not part of serialization. --- tests/Database/DatabaseEloquentModelTest.php | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index dd724ac2e389..8a89dbe7b4ef 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -3356,6 +3356,20 @@ public function testCastsMethodIsTakenInConsiderationOnSerialization() $this->assertEquals(1, $model->getAttribute('duplicatedAttribute')); } + public function testModelSkipsVirtualPropertiesOnSerialization() + { + if (!class_exists(EloquentModelWithVirtualPropertiesStub::class)) { + $this->markTestSkipped('Virtual properties are not supported for PHP 8.3 or below.'); + } + + $model = new EloquentModelWithVirtualPropertiesStub(); + + $serialized = serialize($model); + + $this->assertStringNotContainsString('virtualGet', $serialized); + $this->assertStringNotContainsString('virtualSet', $serialized); + } + public function testCastOnArrayFormatWithOneElement() { $model = new EloquentModelCastingStub; @@ -3932,6 +3946,22 @@ public function doNotGetFourthInvalidAttributeEither() } } +if (version_compare(PHP_VERSION, '8.4.0-dev', '>=')) { + class EloquentModelWithVirtualPropertiesStub extends Model + { + public $virtualGet { + get => $this->foo; + } + + public $virtualSet { + get => $this->foo; + set { + // + } + } + } +} + class EloquentModelCastingStub extends Model { protected $casts = [ From c1b986aa4a46ac88580914e52086292c08177973 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Mon, 29 Sep 2025 16:49:34 -0300 Subject: [PATCH 03/10] Skips CS Fixer for PHP 8.2 with code for PHP 8.4. --- tests/Database/DatabaseEloquentModelTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 8a89dbe7b4ef..829f645a4798 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -3949,6 +3949,7 @@ public function doNotGetFourthInvalidAttributeEither() if (version_compare(PHP_VERSION, '8.4.0-dev', '>=')) { class EloquentModelWithVirtualPropertiesStub extends Model { + // php-cs-fixer-disable public $virtualGet { get => $this->foo; } @@ -3959,6 +3960,7 @@ class EloquentModelWithVirtualPropertiesStub extends Model // } } + // php-cs-fixer-enable } } From 41d97c3d7c7fcc4471557d6abe0c13f44b85a1af Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Mon, 29 Sep 2025 17:02:05 -0300 Subject: [PATCH 04/10] Fixes Style CI with rule to skip specifically named tests. --- .styleci.yml | 1 + tests/Database/DatabaseEloquentModelTest.php | 32 ---------------- ...ntSerializationExcludedFromStyleCiTest.php | 37 +++++++++++++++++++ 3 files changed, 38 insertions(+), 32 deletions(-) create mode 100644 tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php diff --git a/.styleci.yml b/.styleci.yml index 05af66632431..f32a07e6c2ad 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -4,6 +4,7 @@ php: finder: not-name: - bad-syntax-strategy.php + - "*ExcludedFromStyleCiTest.php" js: finder: exclude: diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 829f645a4798..dd724ac2e389 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -3356,20 +3356,6 @@ public function testCastsMethodIsTakenInConsiderationOnSerialization() $this->assertEquals(1, $model->getAttribute('duplicatedAttribute')); } - public function testModelSkipsVirtualPropertiesOnSerialization() - { - if (!class_exists(EloquentModelWithVirtualPropertiesStub::class)) { - $this->markTestSkipped('Virtual properties are not supported for PHP 8.3 or below.'); - } - - $model = new EloquentModelWithVirtualPropertiesStub(); - - $serialized = serialize($model); - - $this->assertStringNotContainsString('virtualGet', $serialized); - $this->assertStringNotContainsString('virtualSet', $serialized); - } - public function testCastOnArrayFormatWithOneElement() { $model = new EloquentModelCastingStub; @@ -3946,24 +3932,6 @@ public function doNotGetFourthInvalidAttributeEither() } } -if (version_compare(PHP_VERSION, '8.4.0-dev', '>=')) { - class EloquentModelWithVirtualPropertiesStub extends Model - { - // php-cs-fixer-disable - public $virtualGet { - get => $this->foo; - } - - public $virtualSet { - get => $this->foo; - set { - // - } - } - // php-cs-fixer-enable - } -} - class EloquentModelCastingStub extends Model { protected $casts = [ diff --git a/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php b/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php new file mode 100644 index 000000000000..59ec621204c4 --- /dev/null +++ b/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php @@ -0,0 +1,37 @@ +markTestSkipped('Requires Virtual Properties.'); + } + + $model = new EloquentModelWithVirtualPropertiesStub(); + + $serialized = serialize($model); + + $this->assertStringNotContainsString('virtualGet', $serialized); + $this->assertStringNotContainsString('virtualSet', $serialized); + } +} + +class EloquentModelWithVirtualPropertiesStub extends Model +{ + public $virtualGet { + get => $this->foo; + } + + public $virtualSet { + get => $this->foo; + set { + // + } + } +} From 7e87b853c89103aa8d47a3f8597a022794166088 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Mon, 29 Sep 2025 17:03:51 -0300 Subject: [PATCH 05/10] Fixes declaring a class for PHP 8.4 running previous versions. --- ...ntSerializationExcludedFromStyleCiTest.php | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php b/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php index 59ec621204c4..d2b6354d222b 100644 --- a/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php +++ b/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php @@ -22,16 +22,18 @@ public function testModelSkipsVirtualPropertiesOnSerialization() } } -class EloquentModelWithVirtualPropertiesStub extends Model -{ - public $virtualGet { - get => $this->foo; - } +if (version_compare(PHP_VERSION, '8.4.0-dev', '>=')) { + class EloquentModelWithVirtualPropertiesStub extends Model + { + public $virtualGet { + get => $this->foo; + } - public $virtualSet { - get => $this->foo; - set { - // + public $virtualSet { + get => $this->foo; + set { + // + } } } } From 90d317704b9c8f5d3d556a9631621adce5d3ef47 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Mon, 29 Sep 2025 17:10:45 -0300 Subject: [PATCH 06/10] Should fix loading a PHP 8.4 class on previous versions interpreters. --- ...ntSerializationExcludedFromStyleCiTest.php | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php b/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php index d2b6354d222b..c257b70e9f1c 100644 --- a/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php +++ b/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php @@ -2,7 +2,6 @@ namespace Illuminate\Tests\Database; -use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Testing\TestCase; class DatabaseEloquentSerializationExcludedFromStyleCiTest extends TestCase @@ -23,17 +22,26 @@ public function testModelSkipsVirtualPropertiesOnSerialization() } if (version_compare(PHP_VERSION, '8.4.0-dev', '>=')) { - class EloquentModelWithVirtualPropertiesStub extends Model - { - public $virtualGet { - get => $this->foo; - } + eval(<<<'MODEL' + $this->foo; - set { - // - } +use Illuminate\Database\Eloquent\Model; + +class EloquentModelWithVirtualPropertiesStub extends Model +{ + public $virtualGet { + get => $this->foo; + } + + public $virtualSet { + get => $this->foo; + set { + // } } } +MODEL + ); +} From ab1828745fc027c9de8cfc2263a18b7d9fd99f46 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Mon, 29 Sep 2025 17:14:14 -0300 Subject: [PATCH 07/10] Should fix eval code. --- ...atabaseEloquentSerializationExcludedFromStyleCiTest.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php b/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php index c257b70e9f1c..1c9120366ad0 100644 --- a/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php +++ b/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php @@ -22,9 +22,7 @@ public function testModelSkipsVirtualPropertiesOnSerialization() } if (version_compare(PHP_VERSION, '8.4.0-dev', '>=')) { - eval(<<<'MODEL' - Date: Mon, 29 Sep 2025 17:32:11 -0300 Subject: [PATCH 08/10] Fixes TestCase base class. --- .../DatabaseEloquentSerializationExcludedFromStyleCiTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php b/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php index 1c9120366ad0..0311989bcbb5 100644 --- a/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php +++ b/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php @@ -2,7 +2,7 @@ namespace Illuminate\Tests\Database; -use Illuminate\Foundation\Testing\TestCase; +use PHPUnit\Framework\TestCase; class DatabaseEloquentSerializationExcludedFromStyleCiTest extends TestCase { From d5ba25ade327738aabc122e7a62d16739cc73847 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Wed, 1 Oct 2025 15:43:53 -0300 Subject: [PATCH 09/10] Removes class caches flush and unsets them from serializable attributes. --- src/Illuminate/Database/Eloquent/Model.php | 18 +++++++++++------- ...entSerializationExcludedFromStyleCiTest.php | 9 +++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 41148ee185b4..909435cd1505 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -2584,25 +2584,29 @@ public function escapeWhenCastingToString($escape = true) /** * Prepare the object for serialization. * - * @return array + * @return string[] */ public function __sleep() { $this->mergeAttributesFromCachedCasts(); - $this->classCastCache = []; - $this->attributeCastCache = []; - $this->relationAutoloadCallback = null; - $this->relationAutoloadContext = null; - // When serializing the model, we may accidentally catch up some virtual properties. // We will cast the model to a native array to skip them and then apply a function // to clean each of the property names which is miles faster than using a regex. + $props = (array) $this; + + unset( + $props["\0*\0classCastCache"], + $props["\0*\0attributeCastCache"], + $props["\0*\0relationAutoloadCallback"], + $props["\0*\0relationAutoloadContext"], + ); + return array_map( fn ($key) => $key[0] === "\0" ? substr($key, strpos($key, "\0", 1) + 1) : $key, - array_keys((array) $this) + array_keys($props) ); } diff --git a/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php b/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php index 0311989bcbb5..0782e37b984d 100644 --- a/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php +++ b/tests/Database/DatabaseEloquentSerializationExcludedFromStyleCiTest.php @@ -13,11 +13,18 @@ public function testModelSkipsVirtualPropertiesOnSerialization() } $model = new EloquentModelWithVirtualPropertiesStub(); + $model->foo = 'bar'; $serialized = serialize($model); $this->assertStringNotContainsString('virtualGet', $serialized); $this->assertStringNotContainsString('virtualSet', $serialized); + + // Ensure attributes and protected normal attributes are also serialized. + $this->assertStringContainsString('foo', $serialized); + $this->assertStringContainsString('bar', $serialized); + $this->assertStringContainsString('isVisible', $serialized); + $this->assertStringContainsString('yes', $serialized); } } @@ -39,6 +46,8 @@ class EloquentModelWithVirtualPropertiesStub extends Model // } } + + protected $isVisible = 'yes'; } PHP From 4e45546e1cbd64cf6b512cc9601e014702dc510b Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Thu, 2 Oct 2025 14:06:08 -0300 Subject: [PATCH 10/10] Uses `__serialize` instead of `__wakeup` --- src/Illuminate/Database/Eloquent/Model.php | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 909435cd1505..3ed36188ebbb 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -2586,14 +2586,14 @@ public function escapeWhenCastingToString($escape = true) * * @return string[] */ - public function __sleep() + public function __serialize(): array { $this->mergeAttributesFromCachedCasts(); // When serializing the model, we may accidentally catch up some virtual properties. - // We will cast the model to a native array to skip them and then apply a function - // to clean each of the property names which is miles faster than using a regex. - $props = (array) $this; + // We will transform the instance to an array to skip them, and then we will remove + // the model caches so those values are not serialized with the rest of the model. + $props = get_mangled_object_vars($this); unset( $props["\0*\0classCastCache"], @@ -2602,12 +2602,7 @@ public function __sleep() $props["\0*\0relationAutoloadContext"], ); - return array_map( - fn ($key) => $key[0] === "\0" - ? substr($key, strpos($key, "\0", 1) + 1) - : $key, - array_keys($props) - ); + return $props; } /**