The __sleep()
and __wakeup()
magic methods have been part of PHP’s object serialization model since early versions.
However, they have long served multiple unrelated responsibilities — from filtering properties and forbidding serialization to preparing data before serialization.
As PHP evolves toward a more explicit and attribute-driven model of serialization (see #[NoSerialize] and #[NotSerializable] RFCs),
this RFC proposes to make __sleep()
more specialized — responsible only for pre-serialization preparation — by allowing it to return null
or no value (void).
This signals the engine to perform default serialization automatically after executing __sleep()
.
This effectively separates “serialization rules” (handled via attributes) from “serialization lifecycle hooks” (handled via __sleep()
), simplifying class design and improving readability.
In the ongoing effort to modernize and simplify PHP’s object serialization model,
there is a clear intention to gradually reduce the overlapping responsibilities of __sleep()
.
Historically, __sleep()
has served two unrelated purposes:
This RFC explicitly limits __sleep()
to the first purpose only —
executing preparation or cleanup logic before serialization —
and removes its responsibility for determining which data members are included in serialization.
The second responsibility (controlling which properties or classes may be serialized)
is intended to be handled by future declarative mechanisms —
namely the proposed attributes #[NoSerialize]
and #[NotSerializable]
(currently RFCs under discussion).
By allowing __sleep()
to return null
or no value,
this proposal ensures that __sleep()
remains a valid and useful lifecycle hook,
focused solely on preparation rather than on serialization control.
This narrows its semantics, aligns it with modern PHP design goals,
and provides a clear migration path for future versions of the language.
This RFC formalizes a clear division of responsibility:
Previously:
class A { function __sleep() {} }
Can be replaced with:
#[NoSerialize] class A {}
Previously:
class A { function __sleep() { throw new Exception('Unable to serialize'); } }
Can be replaced with:
#[NotSerializable] class A {}
Previously, developers had to manually list serializable properties:
class A { public $a; public $b; private $p; function __sleep() { return ['a']; } } class B extends A { public $c; function __sleep() { return array_merge(parent::__sleep(), ['c']); } }
This approach introduces structural fragility, as private properties declared in parent classes cannot be directly accessed or included in the serialization process by a child class’s implementation of __sleep()
.
It can be expressed declaratively:
class A { public $a; #[NoSerialize] public $b; private $p; } class B extends A { public $c; }
The #[NoSerialize]
attribute clearly defines serialization behavior without the need for manual arrays or merges.
Previously:
class A { public $a; function __sleep() { $this->cleanup(); return ['a']; } }
With this RFC:
class A { public $a; function __sleep(): void { $this->cleanup(); // no return → engine performs default serialization } }
This is the intended specialization: __sleep()
acts purely as a hook for pre-serialization preparation.
This RFC updates __sleep()
semantics as follows:
__sleep()
returns an array → unchanged; only the listed properties are serialized.__sleep()
returns `null` or no value (void) → execute side effects, then perform default serialization (as if no __sleep()
were defined).This ensures that invalid return types do not interrupt serialization, while still providing diagnostic feedback to the developer.
Default serialization means:
#[NoSerialize]
(separate RFC);#[NotSerializable]
, serialization is forbidden (separate RFC).class UserSession { public string $id; public Cache $cache; public function __sleep(): void { $this->cache->flush(); // Returning nothing → default serialization } }
class Example { public string $name; #[NoSerialize] private Logger $log; public function __sleep(): ?array { $this->prepare(); return null; // equivalent to returning nothing } }
Classes marked with #[NotSerializable]
completely forbid serialization —
no data processing, cleanup, or side effects from __sleep()
will occur.
This attribute acts as a strict, declarative replacement for the old pattern of throwing an exception inside __sleep()
.
Previously:
class A { function __sleep() { // This code never reaches serialization but still runs before throwing. $this->cleanup(); throw new Exception('Unable to serialize'); } }
With the attribute-based approach:
#[NotSerializable] class A { private Socket $connection; function __sleep(): void { // This method will never be called. // The engine blocks serialization before executing user code. } }
When __sleep()
is invoked during serialize()
:
1. The engine executes the method.
2. If the return value is:
The updated warning message:
"%s::__sleep() must return an array of property names, or return null/void to delegate to default serialization"
This replaces the older message:
"%s::__sleep() should return an array only containing the names of instance-variables to serialize"
Low impact.
Previously:
NULL
.With this RFC:
null
or no value will execute __sleep()
for its side effects, and then perform default serialization (instead of producing NULL
).NULL
.array
or throwing an exception behaves as before.
To preserve the old behavior (where objects effectively serialized as NULL
),
developers should explicitly use the #[NoSerialize]
attribute
(proposed separately).
This change reduces silent data loss and makes __sleep()
behavior more consistent
without meaningfully breaking existing code.
__sleep()
a pure lifecycle hook.Next minor or major version (PHP 8.6 / PHP 9.0)
Minor changes in serialization path.
Add support for nullable array return types from __sleep()
.
None.
#[NoSerialize]
, #[NotSerializable]
)__sleep()
in favor of a more explicit and unified serialization model.-