From 9b9bd8e5ac2dc33959d7e880df6709b8012c4693 Mon Sep 17 00:00:00 2001 From: Sajid Date: Sun, 19 Oct 2025 17:29:38 +0600 Subject: [PATCH 1/5] Fix serialization of models with PHP property hooks --- src/Illuminate/Database/Eloquent/Model.php | 2 +- .../Database/EloquentModelTest.php | 187 ++++++++++++++++++ 2 files changed, 188 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 90260a57ca32..1c2a97621d19 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -2595,7 +2595,7 @@ public function __sleep() $this->relationAutoloadCallback = null; $this->relationAutoloadContext = null; - return array_keys(get_object_vars($this)); + return array_keys(get_mangled_object_vars($this)); } /** diff --git a/tests/Integration/Database/EloquentModelTest.php b/tests/Integration/Database/EloquentModelTest.php index 80bc917e250c..c98296565ad6 100644 --- a/tests/Integration/Database/EloquentModelTest.php +++ b/tests/Integration/Database/EloquentModelTest.php @@ -7,6 +7,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; +use PHPUnit\Framework\Attributes\RequiresPhp; class EloquentModelTest extends DatabaseTestCase { @@ -135,6 +136,129 @@ public function testInsertRecordWithReservedWordFieldName() 'analyze' => true, ]); } + + #[RequiresPhp('>=8.4')] + public function testModelWithPropertyHooksCanBeSerialized() + { + $model = new TestModelWithPropertyHooks; + $model->first_name = 'John'; + $model->last_name = 'Doe'; + + // Access the property hook to ensure it works + $this->assertEquals('John Doe', $model->full_name); + + $serialized = serialize($model); + + $this->assertIsString($serialized); + + // Verify unserialization works + $unserialized = unserialize($serialized); + + $this->assertEquals('John', $unserialized->first_name); + $this->assertEquals('Doe', $unserialized->last_name); + // Property hook should still work after unserialization + $this->assertEquals('John Doe', $unserialized->full_name); + } + + #[RequiresPhp('>=8.4')] + public function testModelWithMultiplePropertyHooksCanBeSerialized() + { + $model = new TestModelWithMultiplePropertyHooks; + $model->first_name = 'John'; + $model->last_name = 'Doe'; + $model->middle_name = 'Smith'; + + $this->assertEquals('Doe John Smith', $model->full_name); + $this->assertEquals('John Doe', $model->short_name); + + $serialized = serialize($model); + $this->assertIsString($serialized); + + // Test unserialization + $unserialized = unserialize($serialized); + $this->assertInstanceOf(Model::class, $unserialized); + + // Verify the property hooks still work after unserialization + $this->assertEquals('Doe John Smith', $unserialized->full_name); + $this->assertEquals('John Doe', $unserialized->short_name); + } + + #[RequiresPhp('>=8.4')] + public function testModelWithSetterPropertyHookCanBeSerialized() + { + $model = new TestModelWithSetterPropertyHook; + $model->email = ' JOHN@EXAMPLE.COM '; + + // Verify setter hook worked + $this->assertEquals('john@example.com', $model->email); + + // Test serialization + $serialized = serialize($model); + $unserialized = unserialize($serialized); + + // Verify data persists after unserialization + $this->assertEquals('john@example.com', $unserialized->email); + } + + #[RequiresPhp('>=8.4')] + public function testModelWithPropertyHooksCanBeQueuedForRedis() + { + $model = new TestModelWithPropertyHooks; + $model->first_name = 'John'; + $model->last_name = 'Doe'; + $model->middle_name = 'Smith'; + + $payload = serialize([ + 'model' => $model, + 'some_data' => 'test' + ]); + + $this->assertIsString($payload); + + + $restored = unserialize($payload); + + $this->assertIsArray($restored); + $this->assertInstanceOf(Model::class, $restored['model']); + $this->assertEquals('John', $restored['model']->first_name); + $this->assertEquals('John Doe', $restored['model']->full_name); + } + + public function testModelWithoutPropertyHooksStillWorks() + { + $model = new TestModel2; + $model->name = 'John Doe'; + $model->title = 'Developer'; + + $serialized = serialize($model); + $unserialized = unserialize($serialized); + + $this->assertEquals('John Doe', $unserialized->name); + $this->assertEquals('Developer', $unserialized->title); + } + + #[RequiresPhp('>=8.4')] + public function testModelWithMixedPropertiesAndHooks() + { + // Test a model with both regular properties and property hooks + $model = new TestModelWithMixedPropertiesAndHooks; + $model->first_name = 'john'; + $model->last_name = 'doe'; + $model->metadata = ['role' => 'admin']; + + $this->assertEquals('JOHN', $model->display_name); + + $serialized = serialize($model); + $unserialized = unserialize($serialized); + + // Regular properties should be preserved + $this->assertEquals('john', $unserialized->first_name); + $this->assertEquals('doe', $unserialized->last_name); + $this->assertEquals(['role' => 'admin'], $unserialized->metadata); + + // Property hook should still work + $this->assertEquals('JOHN', $unserialized->display_name); + } } class TestModel1 extends Model @@ -151,3 +275,66 @@ class TestModel2 extends Model public $timestamps = false; protected $guarded = []; } + +// PHP 8.4+ Property Hooks Test Models +if (PHP_VERSION_ID >= 80400) { + class TestModelWithPropertyHooks extends Model + { + protected $table = 'test_model2'; + public $timestamps = false; + protected $fillable = ['first_name', 'last_name', 'middle_name']; + + // Property hook - virtual property + public string $full_name { + get => "{$this->first_name} {$this->last_name}"; + } + } + + class TestModelWithMultiplePropertyHooks extends Model + { + protected $table = 'test_model2'; + public $timestamps = false; + protected $fillable = ['first_name', 'last_name', 'middle_name']; + + // Multiple property hooks + public string $full_name { + get => trim("{$this->last_name} {$this->first_name} {$this->middle_name}"); + } + + public string $short_name { + get => "{$this->first_name} {$this->last_name}"; + } + } + + class TestModelWithSetterPropertyHook extends Model + { + protected $table = 'test_model2'; + public $timestamps = false; + protected $fillable = ['email']; + + private string $_email = ''; + + // Property hook with both get and set + public string $email { + get => $this->_email; + set (string $value) { + $this->_email = strtolower(trim($value)); + } + } + } + + class TestModelWithMixedPropertiesAndHooks extends Model + { + protected $table = 'test_model2'; + public $timestamps = false; + protected $fillable = ['first_name', 'last_name']; + + // Regular property (will be serialized) + public $metadata = ['key' => 'value']; + + // Property hook (should NOT be serialized as it's virtual) + public string $display_name { + get => strtoupper($this->first_name ?? ''); + } + } +} From f5b66f5ad8f6e71ab0af2555201423e21ca8efdb Mon Sep 17 00:00:00 2001 From: Sajid Date: Sun, 19 Oct 2025 18:05:18 +0600 Subject: [PATCH 2/5] Fix test case --- .../EloquentModelPropertyHooksTest.php | 180 +++++++++++++++++ .../Database/EloquentModelTest.php | 187 ------------------ 2 files changed, 180 insertions(+), 187 deletions(-) create mode 100644 tests/Integration/Database/EloquentModelPropertyHooksTest.php diff --git a/tests/Integration/Database/EloquentModelPropertyHooksTest.php b/tests/Integration/Database/EloquentModelPropertyHooksTest.php new file mode 100644 index 000000000000..86cce475ee9f --- /dev/null +++ b/tests/Integration/Database/EloquentModelPropertyHooksTest.php @@ -0,0 +1,180 @@ +=8.4')] +class EloquentModelPropertyHooksTest extends DatabaseTestCase +{ + public function testModelWithPropertyHooksCanBeSerialized() + { + $model = new TestModelWithPropertyHooks; + $model->first_name = 'John'; + $model->last_name = 'Doe'; + + // Access the property hook to ensure it works + $this->assertEquals('John Doe', $model->full_name); + + $serialized = serialize($model); + + $this->assertIsString($serialized); + + // Verify unserialization works + $unserialized = unserialize($serialized); + + $this->assertEquals('John', $unserialized->first_name); + $this->assertEquals('Doe', $unserialized->last_name); + // Property hook should still work after unserialization + $this->assertEquals('John Doe', $unserialized->full_name); + } + + public function testModelWithMultiplePropertyHooksCanBeSerialized() + { + $model = new TestModelWithMultiplePropertyHooks; + $model->first_name = 'John'; + $model->last_name = 'Doe'; + $model->middle_name = 'Smith'; + + // Verify property hooks work before serialization + $this->assertEquals('Doe John Smith', $model->full_name); + $this->assertEquals('John Doe', $model->short_name); + + // Test serialization + $serialized = serialize($model); + $this->assertIsString($serialized); + + // Test unserialization + $unserialized = unserialize($serialized); + $this->assertInstanceOf(Model::class, $unserialized); + + // Verify the property hooks still work after unserialization + $this->assertEquals('Doe John Smith', $unserialized->full_name); + $this->assertEquals('John Doe', $unserialized->short_name); + } + + public function testModelWithSetterPropertyHookCanBeSerialized() + { + $model = new TestModelWithSetterPropertyHook; + $model->email = ' JOHN@EXAMPLE.COM '; + + // Verify setter hook worked + $this->assertEquals('john@example.com', $model->email); + + // Test serialization + $serialized = serialize($model); + $unserialized = unserialize($serialized); + + // Verify data persists after unserialization + $this->assertEquals('john@example.com', $unserialized->email); + } + + public function testModelWithPropertyHooksCanBeQueuedForRedis() + { + // This simulates what happens when a model is queued + $model = new TestModelWithPropertyHooks; + $model->first_name = 'John'; + $model->last_name = 'Doe'; + $model->middle_name = 'Smith'; + + // Simulate what queue does + $payload = serialize([ + 'model' => $model, + 'some_data' => 'test' + ]); + + $this->assertIsString($payload); + + // Unserialize the payload + $restored = unserialize($payload); + + $this->assertIsArray($restored); + $this->assertInstanceOf(Model::class, $restored['model']); + $this->assertEquals('John', $restored['model']->first_name); + $this->assertEquals('John Doe', $restored['model']->full_name); + } + + public function testModelWithMixedPropertiesAndHooks() + { + // Test a model with both regular properties and property hooks + $model = new TestModelWithMixedPropertiesAndHooks; + $model->first_name = 'john'; + $model->last_name = 'doe'; + $model->metadata = ['role' => 'admin']; + + $this->assertEquals('JOHN', $model->display_name); + + $serialized = serialize($model); + $unserialized = unserialize($serialized); + + // Regular properties should be preserved + $this->assertEquals('john', $unserialized->first_name); + $this->assertEquals('doe', $unserialized->last_name); + $this->assertEquals(['role' => 'admin'], $unserialized->metadata); + + // Property hook should still work + $this->assertEquals('JOHN', $unserialized->display_name); + } +} + +// Test model classes with property hooks +class TestModelWithPropertyHooks extends Model +{ + protected $table = 'test_model2'; + public $timestamps = false; + protected $fillable = ['first_name', 'last_name', 'middle_name']; + + // Property hook - virtual property + public string $full_name { + get => "{$this->first_name} {$this->last_name}"; + } +} + +class TestModelWithMultiplePropertyHooks extends Model +{ + protected $table = 'test_model2'; + public $timestamps = false; + protected $fillable = ['first_name', 'last_name', 'middle_name']; + + // Multiple property hooks + public string $full_name { + get => trim("{$this->last_name} {$this->first_name} {$this->middle_name}"); + } + + public string $short_name { + get => "{$this->first_name} {$this->last_name}"; + } +} + +class TestModelWithSetterPropertyHook extends Model +{ + protected $table = 'test_model2'; + public $timestamps = false; + protected $fillable = ['email']; + + private string $_email = ''; + + // Property hook with both get and set + public string $email { + get => $this->_email; + set (string $value) { + $this->_email = strtolower(trim($value)); + } + } +} + +class TestModelWithMixedPropertiesAndHooks extends Model +{ + protected $table = 'test_model2'; + public $timestamps = false; + protected $fillable = ['first_name', 'last_name']; + + // Regular property (will be serialized) + public $metadata = ['key' => 'value']; + + // Property hook (should NOT be serialized as it's virtual) + public string $display_name { + get => strtoupper($this->first_name ?? ''); + } +} diff --git a/tests/Integration/Database/EloquentModelTest.php b/tests/Integration/Database/EloquentModelTest.php index c98296565ad6..80bc917e250c 100644 --- a/tests/Integration/Database/EloquentModelTest.php +++ b/tests/Integration/Database/EloquentModelTest.php @@ -7,7 +7,6 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; -use PHPUnit\Framework\Attributes\RequiresPhp; class EloquentModelTest extends DatabaseTestCase { @@ -136,129 +135,6 @@ public function testInsertRecordWithReservedWordFieldName() 'analyze' => true, ]); } - - #[RequiresPhp('>=8.4')] - public function testModelWithPropertyHooksCanBeSerialized() - { - $model = new TestModelWithPropertyHooks; - $model->first_name = 'John'; - $model->last_name = 'Doe'; - - // Access the property hook to ensure it works - $this->assertEquals('John Doe', $model->full_name); - - $serialized = serialize($model); - - $this->assertIsString($serialized); - - // Verify unserialization works - $unserialized = unserialize($serialized); - - $this->assertEquals('John', $unserialized->first_name); - $this->assertEquals('Doe', $unserialized->last_name); - // Property hook should still work after unserialization - $this->assertEquals('John Doe', $unserialized->full_name); - } - - #[RequiresPhp('>=8.4')] - public function testModelWithMultiplePropertyHooksCanBeSerialized() - { - $model = new TestModelWithMultiplePropertyHooks; - $model->first_name = 'John'; - $model->last_name = 'Doe'; - $model->middle_name = 'Smith'; - - $this->assertEquals('Doe John Smith', $model->full_name); - $this->assertEquals('John Doe', $model->short_name); - - $serialized = serialize($model); - $this->assertIsString($serialized); - - // Test unserialization - $unserialized = unserialize($serialized); - $this->assertInstanceOf(Model::class, $unserialized); - - // Verify the property hooks still work after unserialization - $this->assertEquals('Doe John Smith', $unserialized->full_name); - $this->assertEquals('John Doe', $unserialized->short_name); - } - - #[RequiresPhp('>=8.4')] - public function testModelWithSetterPropertyHookCanBeSerialized() - { - $model = new TestModelWithSetterPropertyHook; - $model->email = ' JOHN@EXAMPLE.COM '; - - // Verify setter hook worked - $this->assertEquals('john@example.com', $model->email); - - // Test serialization - $serialized = serialize($model); - $unserialized = unserialize($serialized); - - // Verify data persists after unserialization - $this->assertEquals('john@example.com', $unserialized->email); - } - - #[RequiresPhp('>=8.4')] - public function testModelWithPropertyHooksCanBeQueuedForRedis() - { - $model = new TestModelWithPropertyHooks; - $model->first_name = 'John'; - $model->last_name = 'Doe'; - $model->middle_name = 'Smith'; - - $payload = serialize([ - 'model' => $model, - 'some_data' => 'test' - ]); - - $this->assertIsString($payload); - - - $restored = unserialize($payload); - - $this->assertIsArray($restored); - $this->assertInstanceOf(Model::class, $restored['model']); - $this->assertEquals('John', $restored['model']->first_name); - $this->assertEquals('John Doe', $restored['model']->full_name); - } - - public function testModelWithoutPropertyHooksStillWorks() - { - $model = new TestModel2; - $model->name = 'John Doe'; - $model->title = 'Developer'; - - $serialized = serialize($model); - $unserialized = unserialize($serialized); - - $this->assertEquals('John Doe', $unserialized->name); - $this->assertEquals('Developer', $unserialized->title); - } - - #[RequiresPhp('>=8.4')] - public function testModelWithMixedPropertiesAndHooks() - { - // Test a model with both regular properties and property hooks - $model = new TestModelWithMixedPropertiesAndHooks; - $model->first_name = 'john'; - $model->last_name = 'doe'; - $model->metadata = ['role' => 'admin']; - - $this->assertEquals('JOHN', $model->display_name); - - $serialized = serialize($model); - $unserialized = unserialize($serialized); - - // Regular properties should be preserved - $this->assertEquals('john', $unserialized->first_name); - $this->assertEquals('doe', $unserialized->last_name); - $this->assertEquals(['role' => 'admin'], $unserialized->metadata); - - // Property hook should still work - $this->assertEquals('JOHN', $unserialized->display_name); - } } class TestModel1 extends Model @@ -275,66 +151,3 @@ class TestModel2 extends Model public $timestamps = false; protected $guarded = []; } - -// PHP 8.4+ Property Hooks Test Models -if (PHP_VERSION_ID >= 80400) { - class TestModelWithPropertyHooks extends Model - { - protected $table = 'test_model2'; - public $timestamps = false; - protected $fillable = ['first_name', 'last_name', 'middle_name']; - - // Property hook - virtual property - public string $full_name { - get => "{$this->first_name} {$this->last_name}"; - } - } - - class TestModelWithMultiplePropertyHooks extends Model - { - protected $table = 'test_model2'; - public $timestamps = false; - protected $fillable = ['first_name', 'last_name', 'middle_name']; - - // Multiple property hooks - public string $full_name { - get => trim("{$this->last_name} {$this->first_name} {$this->middle_name}"); - } - - public string $short_name { - get => "{$this->first_name} {$this->last_name}"; - } - } - - class TestModelWithSetterPropertyHook extends Model - { - protected $table = 'test_model2'; - public $timestamps = false; - protected $fillable = ['email']; - - private string $_email = ''; - - // Property hook with both get and set - public string $email { - get => $this->_email; - set (string $value) { - $this->_email = strtolower(trim($value)); - } - } - } - - class TestModelWithMixedPropertiesAndHooks extends Model - { - protected $table = 'test_model2'; - public $timestamps = false; - protected $fillable = ['first_name', 'last_name']; - - // Regular property (will be serialized) - public $metadata = ['key' => 'value']; - - // Property hook (should NOT be serialized as it's virtual) - public string $display_name { - get => strtoupper($this->first_name ?? ''); - } - } -} From c09479d0512867db0049a520d117a94bf04efdb4 Mon Sep 17 00:00:00 2001 From: Sajid Date: Sun, 19 Oct 2025 18:12:33 +0600 Subject: [PATCH 3/5] testing failed test --- .../Database/EloquentModelPropertyHooksTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/Integration/Database/EloquentModelPropertyHooksTest.php b/tests/Integration/Database/EloquentModelPropertyHooksTest.php index 86cce475ee9f..a1162f9c9659 100644 --- a/tests/Integration/Database/EloquentModelPropertyHooksTest.php +++ b/tests/Integration/Database/EloquentModelPropertyHooksTest.php @@ -8,6 +8,14 @@ #[RequiresPhp('>=8.4')] class EloquentModelPropertyHooksTest extends DatabaseTestCase { + protected function setUp(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Property hooks require PHP 8.4+'); + } + parent::setUp(); + } + public function testModelWithPropertyHooksCanBeSerialized() { $model = new TestModelWithPropertyHooks; From 65d48188af6724b369f028068beb18c98f3810aa Mon Sep 17 00:00:00 2001 From: Sajid Date: Sun, 19 Oct 2025 18:34:46 +0600 Subject: [PATCH 4/5] testing failed test --- .../EloquentModelPropertyHooksTest.php | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/tests/Integration/Database/EloquentModelPropertyHooksTest.php b/tests/Integration/Database/EloquentModelPropertyHooksTest.php index a1162f9c9659..6002a9d1cb63 100644 --- a/tests/Integration/Database/EloquentModelPropertyHooksTest.php +++ b/tests/Integration/Database/EloquentModelPropertyHooksTest.php @@ -126,20 +126,36 @@ public function testModelWithMixedPropertiesAndHooks() } } -// Test model classes with property hooks -class TestModelWithPropertyHooks extends Model +/* +|-------------------------------------------------------------------------- +| Define the runtime-only test model classes that use PHP 8.4 property +| hook syntax. Wrapped in a namespace declaration inside eval() so the +| classes are created in the expected namespace and avoid parse errors +| on older PHP versions. +|-------------------------------------------------------------------------- +*/ + +if (PHP_VERSION_ID >= 80400) { + // Only define if not already defined (prevents redeclare errors). + if (!class_exists(__NAMESPACE__ . '\\TestModelWithPropertyHooks', false)) { + eval(<<<'PHP' +namespace Illuminate\Tests\Integration\Database; + +use Illuminate\Database\Eloquent\Model as EloquentModel; + +class TestModelWithPropertyHooks extends EloquentModel { protected $table = 'test_model2'; public $timestamps = false; protected $fillable = ['first_name', 'last_name', 'middle_name']; - // Property hook - virtual property + // Property hook - virtual property (PHP 8.4) public string $full_name { get => "{$this->first_name} {$this->last_name}"; } } -class TestModelWithMultiplePropertyHooks extends Model +class TestModelWithMultiplePropertyHooks extends EloquentModel { protected $table = 'test_model2'; public $timestamps = false; @@ -155,7 +171,7 @@ class TestModelWithMultiplePropertyHooks extends Model } } -class TestModelWithSetterPropertyHook extends Model +class TestModelWithSetterPropertyHook extends EloquentModel { protected $table = 'test_model2'; public $timestamps = false; @@ -172,7 +188,7 @@ class TestModelWithSetterPropertyHook extends Model } } -class TestModelWithMixedPropertiesAndHooks extends Model +class TestModelWithMixedPropertiesAndHooks extends EloquentModel { protected $table = 'test_model2'; public $timestamps = false; @@ -186,3 +202,7 @@ class TestModelWithMixedPropertiesAndHooks extends Model get => strtoupper($this->first_name ?? ''); } } +PHP + ); + } +} From ddd1ded0859f2b2ed92f1805cce69b05dcb784f6 Mon Sep 17 00:00:00 2001 From: Sajid Date: Sun, 19 Oct 2025 19:44:03 +0600 Subject: [PATCH 5/5] remove redundent code --- .../Database/EloquentModelPropertyHooksTest.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/Integration/Database/EloquentModelPropertyHooksTest.php b/tests/Integration/Database/EloquentModelPropertyHooksTest.php index 6002a9d1cb63..dc0972497694 100644 --- a/tests/Integration/Database/EloquentModelPropertyHooksTest.php +++ b/tests/Integration/Database/EloquentModelPropertyHooksTest.php @@ -8,14 +8,6 @@ #[RequiresPhp('>=8.4')] class EloquentModelPropertyHooksTest extends DatabaseTestCase { - protected function setUp(): void - { - if (PHP_VERSION_ID < 80400) { - $this->markTestSkipped('Property hooks require PHP 8.4+'); - } - parent::setUp(); - } - public function testModelWithPropertyHooksCanBeSerialized() { $model = new TestModelWithPropertyHooks;