====== 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 [[deprecations_php_8_5#deprecate_the_sleep_and_wakeup_magic_methods|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 “[[custom_object_serialization|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: * Move ''%%__sleep()%%'' / ''%%__wakeup()%%'' below ''%%__serialize()%%'' / ''%%__unserialize()%%'' on the [[https://www.php.net/language.oop5.magic|magic methods page]]. * 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.