PHP RFC: Soft-Deprecate __sleep() and __wakeup()
- Version: 1.0
- Date: 2025-09-05
- Author: Nicolas Grekas, nicolas.grekas@php.net
- Status: Under Discussion
Introduction
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:
- Preserve backward compatibility and avoid unnecessary burden on users.
- Still encourage migrating to
__serialize()
/__unserialize()
.
Background
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:
- The original “New Custom Object Serialization” RFC (PHP 7.4) explicitly stated:
> “There is no particular pressing need to phase out __sleep()
and __wakeup()
.”
- That RFC positioned the new API as additional, not a replacement.
- The recent vote passed with minimal examples of migration, and no performance or compatibility analysis.
Problems with the Current Deprecation
1. Migration is not drop-in, produces worse code for many users
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']; }
- For classes with 50+ properties, the boilerplate grows significantly.
- No benefit is gained for simple mappings.
- Performance is slightly worse and code readability suffers.
- Compatibility with payloads created before this change is broken or needs additional code.
- Supposing
User
can be extended, backward compatibility is broken for child classes.
2. Complex compatibility handling for inheritance and payload formats
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); }
3. No technical urgency
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.- They serve valid, common use cases with minimal complexity overhead.
- The primary motivation is language tidiness, not a security or correctness problem.
4. The cost of the deprecation was understated
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:
- Step 1 - in the next patch release: Address the deprecation of
__sleep()
/__wakeup()
but don't emit a deprecation warning yet, except maybe when running on PHP 8.5. - Step 2 - in the next minor release: Emit a deprecation warning when
__sleep()
/__wakeup()
is implemented by child classes. - Step 3 - in the next major release: Remove
__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:
- The RFC omitted critical migration examples and performance considerations.
- The complexity and risks of migration were understated.
- Several voters have since stated they would have voted differently with complete information.
- The narrow margin (18-9) suggests the vote may not reflect the true community consensus when fully informed.
Proposal
1. Convert the voted deprecation to a soft deprecation (documentation-only).
2. Update documentation to reflect the soft deprecation:
- Add a prominent note similar to this one on this page:
“This mechanism is maintained for backward compatibility. New and existing code should be migrated to__serialize()
/__unserialize()
instead.”
Benefits
- Avoids costly and risky rewrites for large codebases.
- Reduces ecosystem churn.
- Still promotes modern best practices for newcomers.
- Allows time to figure out a smoother migration path.
Drawbacks
- Keeps multiple serialization APIs in the language longer.
- Potential delay in achieving a single unified serialization mechanism.
Future Scope
- Emit a deprecation notice once its impact and migration paths are well described, understood and accepted
Vote
The vote will require 2/3 majority.
Yes / No — Convert the deprecation to a documentation-based soft deprecation.