A recent RFC to deprecate
the __sleep()
and __wakeup()
magic methods in favor of __serialize()
and __unserialize()
passed with a narrow margin (18–9).
However, during and after the vote, substantial discussion revealed that the RFC understated the complexity, cost, and risks of migration.
This follow-up RFC proposes to convert the deprecation to a soft deprecation in the documentation only.
The aim is to:
__serialize()
/ __unserialize()
.The original deprecation RFC argued:
Having multiple serialization methods… is confusing and adds unnecessary complexity…
__serialize()
/__unserialize()
are a straight up improvement over__sleep()
/__wakeup()
.
However:
> “There is no particular pressing need to phase out __sleep()
and __wakeup()
.”
Example:
class User { public function __construct( public readonly string $id, public readonly string $email, public readonly DateTime $createdAt ) {} public function __sleep(): array { return ['id', 'email', 'createdAt']; } public function __wakeup(): void { $this->validate(); } private function validate(): void { if (!filter_var($this->email, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException('Invalid email'); } } }
After migration to __serialize()
/ __unserialize()
:
public function __serialize(): array { return [ 'id' => $this->id, 'email' => $this->email, 'createdAt' => $this->createdAt, ]; } public function __unserialize(array $data): void { $this->validateData($data); $this->id = $data['id']; $this->email = $data['email']; $this->createdAt = $data['createdAt']; }
User
can be extended, backward compatibility is broken for child classes.The last two points in the previous section can be accommodated with additional code. As you'll see, this code requires quite a bit of knowledge and boilerplate, to say the least:
public function __serialize(): array { // BC with child classes: if the __sleep() method is not redefined // or if the __serialize() method is redefined, then return the // data to serialize with property names as keys. if (self::class === (new \ReflectionMethod($this, '__sleep'))->class || self::class !== (new \ReflectionMethod($this, '__serialize'))->class ) { return [ 'prop1' => $this->prop1, 'prop2' => $this->prop2, // etc. ]; } trigger_error(sprintf('Implementing "%s::__sleep()" is deprecated since "foo/bar" vX.y, use "__serialize()" instead.', get_debug_type($this)), E_USER_DEPRECATED); // Here, a child class did redefine the __sleep() method, // so we have to call it ourselves to preserve BC. $data = []; foreach ($this->__sleep() as $key) { try { if (($r = new \ReflectionProperty($this, $key))->isInitialized($this)) { $data[$key] = $r->getValue($this); } } catch (\ReflectionException) { // Handle dynamic properties. $data[$key] = $this->$key; } } return $data; }
If the class being migrated doesn't implement __wakeup()
, then we're done:
if __unserialize()
is not implemented, the PHP engine will automatically map all keys returned by __serialize()
to the corresponding properties of the object on unserialization - including private properties (but not
overridden ones, which reflects the main drawback of __sleep()
).
If the class being migrated implements __wakeup()
, then additional code is needed to migrate the behavior implemented
in __wakeup()
. This concern becomes more complex when __wakeup()
can be extended by a third-party child class.
Here is a non-generic implementation of __unserialize()
that also preserves compatibility with payloads created before
the introduction of __serialize()
. When this concern arises, this code should deal with mangled property names in
the $data
array.
public function __unserialize(array $data): void { // Check for child classes: if the __wakeup() method is redefined // and if the __unserialize() method is not, then emit a deprecation warning. if ($wakeup = self::class !== (new \ReflectionMethod($this, '__wakeup'))->class && self::class === (new \ReflectionMethod($this, '__unserialize'))->class ) { trigger_error(sprintf('Implementing "%s::__wakeup()" is deprecated since "foo/bar" vX.y, use "__unserialize()" instead.', get_debug_type($this)), E_USER_DEPRECATED); } // Check for BC with payloads created before the introduction of __serialize(): // if the $data array contains only the keys 'data' or "\0*\0data", then map // them to the corresponding properties of the object. // This check would need to be adapted to the specific properties of the class; // here we assume only one named 'data'. if (\in_array(array_keys($data), [['data'], ["\0*\0data"]], true)) { $this->data = $data['data'] ?? $data["\0*\0data"] ?? null; if ($wakeup) { // Call the __wakeup() method but only if it's overridden. $this->__wakeup(); } return; } trigger_error(sprintf('Passing more than just key "data" to "%s::__unserialize()" is deprecated since "foo/bar" vX.y, populate properties in "%s::__unserialize()" instead.', self::class, get_debug_type($this)), E_USER_DEPRECATED); // If the data array contains other keys, then map them to the corresponding // properties of the object to mimic the behavior of the PHP engine. // The rebound closure allows accessing protected and private properties, // but not nested ones; additional code would be needed to handle them. \Closure::bind(function ($data) use ($wakeup) { foreach ($data as $key => $value) { $this->{("\0" === $key[0] ?? '') ? substr($key, 1 + strrpos($key, "\0")) : $key} = $value; } if ($wakeup) { // Call the __wakeup() method but only if it's overridden. $this->__wakeup(); } }, $this, static::class)($data); }
While __sleep()
is incompatible with nested private properties, this is not a problem for most use cases,
and a solution exists when it is: migrating to __serialize()
.
Forcing this migration (by deprecating first) won't improve much in practice:
__sleep()
/ __wakeup()
are not fundamentally broken.
The above code snippets are not generic, and not trivial to write.
Yet, they're the only way for a shared library to preserve backward compatibility while addressing the deprecation
of __sleep()
/ __wakeup()
as presented in the original RFC.
Unlike other deprecations, addressing this one requires significant changes to existing codebases. For shared codebases that will need to preserve backward compatibility of both payloads and inheritance, respecting semantic versioning constraints would be done in three steps:
__sleep()
/ __wakeup()
but don't emit a deprecation warning yet, except maybe when running on PHP 8.5.__sleep()
/ __wakeup()
is implemented by child classes.__sleep()
/ __wakeup()
from the base class and cleanup the deprecation layer added in step 1.Step 1 is the most risky one as it means adding non-trivial code like the ones above, and may introduce subtle bugs in user code.
In short:
1. Convert the voted deprecation to a soft deprecation (documentation-only).
2. Update documentation to reflect the soft deprecation:
“This mechanism is maintained for backward compatibility. New and existing code should be migrated to__serialize()
/__unserialize()
instead.”
The vote will require 2/3 majority.
Yes / No — Convert the deprecation to a documentation-based soft deprecation.