-
Notifications
You must be signed in to change notification settings - Fork 11.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[6.x] Serialization of models on PHP 7.4 with compatibility with PHP 7.3 and below #30605
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,6 @@ | |
|
||
namespace Illuminate\Queue; | ||
|
||
use Illuminate\Contracts\Database\ModelIdentifier; | ||
use ReflectionClass; | ||
use ReflectionProperty; | ||
|
||
|
@@ -11,67 +10,109 @@ trait SerializesModels | |
use SerializesAndRestoresModelIdentifiers; | ||
|
||
/** | ||
* The list of serialized model identifiers. | ||
* Prepare the instance for serialization. | ||
* | ||
* @var array | ||
* @return array | ||
*/ | ||
protected $modelIdentifiers = []; | ||
public function __sleep() | ||
{ | ||
$properties = (new ReflectionClass($this))->getProperties(); | ||
|
||
foreach ($properties as $property) { | ||
$property->setValue($this, $this->getSerializedPropertyValue( | ||
$this->getPropertyValue($property) | ||
)); | ||
} | ||
|
||
return array_values(array_filter(array_map(function ($p) { | ||
return $p->isStatic() ? null : $p->getName(); | ||
}, $properties))); | ||
} | ||
|
||
/** | ||
* Prepare the instance for serialization. | ||
* Restore the model after serialization. | ||
* | ||
* @return void | ||
*/ | ||
public function __wakeup() | ||
{ | ||
foreach ((new ReflectionClass($this))->getProperties() as $property) { | ||
if ($property->isStatic()) { | ||
continue; | ||
} | ||
|
||
$property->setValue($this, $this->getRestoredPropertyValue( | ||
$this->getPropertyValue($property) | ||
)); | ||
} | ||
} | ||
|
||
/** | ||
* Prepare the instance values for serialization. | ||
* | ||
* @return array | ||
*/ | ||
public function __sleep() | ||
public function __serialize() | ||
{ | ||
$values = []; | ||
|
||
$properties = (new ReflectionClass($this))->getProperties(); | ||
|
||
$class = get_class($this); | ||
|
||
foreach ($properties as $property) { | ||
if ($property->getName() === 'modelIdentifiers') { | ||
if ($property->isStatic()) { | ||
continue; | ||
} | ||
|
||
$serializedValue = $this->getSerializedPropertyValue( | ||
$value = $this->getPropertyValue($property) | ||
); | ||
$name = $property->getName(); | ||
|
||
if ($serializedValue instanceof ModelIdentifier) { | ||
$this->modelIdentifiers[$property->getName()] = $serializedValue; | ||
|
||
// Empty instance of the model or collection to support typed properties... | ||
$property->setValue($this, new $value); | ||
} else { | ||
$property->setValue($this, $value); | ||
if ($property->isPrivate()) { | ||
$name = "\0{$class}\0{$name}"; | ||
} elseif ($property->isProtected()) { | ||
$name = "\0*\0{$name}"; | ||
} | ||
|
||
$values[$name] = $this->getSerializedPropertyValue($this->getPropertyValue($property)); | ||
} | ||
|
||
return array_values(array_filter(array_map(function ($p) { | ||
return $p->isStatic() ? null : $p->getName(); | ||
}, $properties))); | ||
return $values; | ||
} | ||
|
||
/** | ||
* Restore the model after serialization. | ||
* | ||
* @return void | ||
* @param array $values | ||
* @return array | ||
*/ | ||
public function __wakeup() | ||
public function __unserialize(array $values) | ||
{ | ||
foreach ((new ReflectionClass($this))->getProperties() as $property) { | ||
if ($property->isStatic() || $property->getName() === 'modelIdentifiers') { | ||
$properties = (new ReflectionClass($this))->getProperties(); | ||
|
||
$class = get_class($this); | ||
|
||
foreach ($properties as $property) { | ||
if ($property->isStatic()) { | ||
continue; | ||
} | ||
|
||
if (isset($this->modelIdentifiers[$property->getName()])) { | ||
$value = $this->modelIdentifiers[$property->getName()]; | ||
} else { | ||
$value = $this->getPropertyValue($property); | ||
$name = $property->getName(); | ||
|
||
if ($property->isPrivate()) { | ||
$name = "\0{$class}\0{$name}"; | ||
} elseif ($property->isProtected()) { | ||
$name = "\0*\0{$name}"; | ||
} | ||
|
||
$property->setValue($this, $this->getRestoredPropertyValue( | ||
$value | ||
)); | ||
if (! array_key_exists($name, $values)) { | ||
continue; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This part will unfortunately break jobs which are already queued. If any app is upgraded with these changes those jobs won't be able to unserialize anymore. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PHP <7.4 does not use __serialize. If you will upgrade only Laravel php still used __slee/__wakeup. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because any already queued job will still have their model identifiers set on the properties. If you upgrade to php 7.4 then your queued jobs will break while with the current implementation you can upgrade to php 7.4 smoothly. We can do the unserialize thing in a year or two perhaps when we know most people have transitioned to php 7.4 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ... then php pass all properties to __unserialize as array. See #30604 (comment) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added serialization structure test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah thanks for that. If @taylorotwell is okay with it then this PR is fine by me. |
||
|
||
$property->setAccessible(true); | ||
$property->setValue($this, $this->getRestoredPropertyValue($values[$name])); | ||
} | ||
|
||
return $values; | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -292,8 +292,8 @@ public function testItSerializesTypedProperties() | |
|
||
$this->assertSame('testbench', $unSerialized->user->getConnectionName()); | ||
$this->assertSame('[email protected]', $unSerialized->user->email); | ||
$this->assertSame(5, $unSerialized->id); | ||
$this->assertSame(['James', 'Taylor', 'Mohamed'], $unSerialized->names); | ||
$this->assertSame(5, $unSerialized->getId()); | ||
$this->assertSame(['James', 'Taylor', 'Mohamed'], $unSerialized->getNames()); | ||
|
||
$serialized = serialize(new TypedPropertyCollectionTestClass(ModelSerializationTestUser::on('testbench')->get())); | ||
|
||
|
@@ -304,6 +304,19 @@ public function testItSerializesTypedProperties() | |
$this->assertSame('testbench', $unSerialized->users[1]->getConnectionName()); | ||
$this->assertSame('[email protected]', $unSerialized->users[1]->email); | ||
} | ||
|
||
public function test_model_serialization_structure() | ||
{ | ||
$user = ModelSerializationTestUser::create([ | ||
'email' => '[email protected]', | ||
]); | ||
|
||
$serialized = serialize(new ModelSerializationParentAccessibleTestClass($user, $user, $user)); | ||
|
||
$this->assertEquals( | ||
'O:78:"Illuminate\\Tests\\Integration\\Queue\\ModelSerializationParentAccessibleTestClass":2:{s:4:"user";O:45:"Illuminate\\Contracts\\Database\\ModelIdentifier":4:{s:5:"class";s:61:"Illuminate\\Tests\\Integration\\Queue\\ModelSerializationTestUser";s:2:"id";i:1;s:9:"relations";a:0:{}s:10:"connection";s:9:"testbench";}s:8:"'."\0".'*'."\0".'user2";O:45:"Illuminate\\Contracts\\Database\\ModelIdentifier":4:{s:5:"class";s:61:"Illuminate\\Tests\\Integration\\Queue\\ModelSerializationTestUser";s:2:"id";i:1;s:9:"relations";a:0:{}s:10:"connection";s:9:"testbench";}}', $serialized | ||
); | ||
} | ||
} | ||
|
||
class ModelSerializationTestUser extends Model | ||
|
@@ -420,6 +433,27 @@ public function __construct($user) | |
} | ||
} | ||
|
||
class ModelSerializationAccessibleTestClass | ||
{ | ||
use SerializesModels; | ||
|
||
public $user; | ||
protected $user2; | ||
private $user3; | ||
|
||
public function __construct($user, $user2, $user3) | ||
{ | ||
$this->user = $user; | ||
$this->user2 = $user2; | ||
$this->user3 = $user3; | ||
} | ||
} | ||
|
||
class ModelSerializationParentAccessibleTestClass extends ModelSerializationAccessibleTestClass | ||
{ | ||
// | ||
} | ||
|
||
class ModelRelationSerializationTestClass | ||
{ | ||
use SerializesModels; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this stuff?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PHP adds
\0*\0
for protected and\0className\0
for private properties in serialization.serialization_objects_test, zend_mangle_property_name and usage 1, 2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's needed for backward compatibility with php7.3 workers.
A php7.3 serialize command will produce this name in the serialized array so by naming it this way a 7.3 worker can work together with a 7.4 worker (as long as it's not using typed properties).