====== PHP RFC: Allow __sleep() to return null or no value to delegate to default serialization ======
* Version: 0.4
* Date: 2025-10-20
* Author: Dmytro Kulyk, lnkvisitor.ts@gmail.com
* Status: Draft
* Implementation: https://github.com/php/php-src/pull/20252
===== Introduction =====
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 [[https://wiki.php.net/rfc/no_serialize_attribute|#[NoSerialize]]] and [[https://wiki.php.net/rfc/not_serializable_attribute|#[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.
===== Motivation =====
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:
* performing pre-serialization preparation, and
* defining which properties should be serialized.
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.
===== Use Cases and Modern Equivalents =====
This RFC formalizes a clear division of responsibility:
==== 1. Avoiding class serialization ====
Previously:
class A {
function __sleep() {}
}
Can be replaced with:
#[NoSerialize]
class A {}
(see [[https://wiki.php.net/rfc/no_serialize_attribute]])
==== 2. Forbidding serialization ====
Previously:
class A {
function __sleep() {
throw new Exception('Unable to serialize');
}
}
Can be replaced with:
#[NotSerializable]
class A {}
(see [[https://wiki.php.net/rfc/not_serializable_attribute]])
==== 3. Excluding specific properties from serialization ====
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.
==== 4. Preparing data before serialization (the main use case of this RFC) ====
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.
===== Proposal =====
This RFC updates __sleep() semantics as follows:
* If __sleep() returns an **array** → unchanged; only the listed properties are serialized.
* If __sleep() returns **`null`** or **no value (void)** → execute side effects, then perform **default serialization** (as if no __sleep() were defined).
* If it returns **any other type** → emit a warning and then perform **default serialization** as a fallback, identical to the previous case.
This ensures that invalid return types do not interrupt serialization,
while still providing diagnostic feedback to the developer.
Default serialization means:
* serialize all non-static properties according to standard visibility scoping;
* skip properties marked with #[NoSerialize] (separate RFC);
* if a class is marked with #[NotSerializable], serialization is forbidden (separate RFC).
===== Examples =====
==== Example 1: Default serialization after side effects ====
class UserSession {
public string $id;
public Cache $cache;
public function __sleep(): void {
$this->cache->flush();
// Returning nothing → default serialization
}
}
==== Example 2: Using with #[NoSerialize] ====
class Example {
public string $name;
#[NoSerialize]
private Logger $log;
public function __sleep(): ?array {
$this->prepare();
return null; // equivalent to returning nothing
}
}
==== Example 3: For classes that should not be serialized ====
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.
}
}
===== Implementation Details =====
When __sleep() is invoked during serialize():
1. The engine executes the method.
2. If the return value is:
* **array** — serialize only those properties.
* **null** or **IS_UNDEF** — perform default serialization.
* otherwise — trigger a warning and perform default serialization.
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"
===== Backward Compatibility =====
Low impact.
Previously:
* If it returned a value of not array type, PHP emitted a warning and, due to the internal fallback, serialized the object as NULL.
With this RFC:
* Returning null or no value will execute __sleep() for its side effects, and then perform **default serialization** (instead of producing NULL).
* Returning an invalid type will emit a **warning**, but still fall back to **default serialization** rather than replacing the object with NULL.
* Returning an 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.
===== Rationale =====
* Makes __sleep() a **pure lifecycle hook**.
* Reduces boilerplate and synchronization errors.
* Aligns with attribute-based serialization control.
* Simplifies migration away from deprecated patterns.
* Improves IDE and static analysis consistency.
===== Proposed PHP Version(s) =====
Next minor or major version (PHP 8.6 / PHP 9.0)
===== RFC Impact =====
==== On the Engine ====
Minor changes in serialization path.
==== On Tools and IDEs ====
Add support for nullable array return types from __sleep().
==== On Extensions and SAPIs ====
None.
===== Future Scope =====
* Unified attribute-based serialization model (#[NoSerialize], #[NotSerializable])
* Deprecate the legacy multi-purpose use of __sleep() in favor of a more explicit and unified serialization model.
===== Voting Choices =====
* Yes
* No
* Abstain
===== Patches and Tests =====
[[https://github.com/php/php-src/pull/20252]]
===== References =====
* [[https://wiki.php.net/rfc/no_serialize_attribute|RFC: #[NoSerialize]]]
* [[https://wiki.php.net/rfc/not_serializable_attribute|RFC: #[NotSerializable]]]
===== Changelog =====
-