# RFC: Allow __sleep() to return null or no value to delegate to default serialization

- Status: Draft
- Authors: Dmytro Kulyk, lnkvisitor.ts@gmail.com
- Date: 2025-10-20
- Version: 0.4
- Implementation: https://github.com/php/php-src/pull/20252
- Original: https://wiki.php.net/rfc/sleep_without_return_array
- Page: https://externals.io/rfc/sleep_without_return_array
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

## Votes

### Allow __sleep() to return null or no value (void) to delegate to default serialization? (closed)

Yes: 0 · No: 0 · Abstain: 0

## Discussions on the mailing list

- [\[RFC\]\[Discussion\] Add #\[NoSerialize\] attribute for excluding properties or classes from serialization](https://externals.io/message/128988.md) — started on 2025-10-27, 17 messages

---

## 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 [\#\[NoSerialize\]](https://wiki.php.net/rfc/no_serialize_attribute) and [\#\[NotSerializable\]](https://wiki.php.net/rfc/not_serializable_attribute) 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. Forbidding serialization

Previously:

```php
class A {
    function __sleep() {
        throw new Exception('Unable to serialize');
    }
}
```

Can be replaced with:

```php
#[NotSerializable]
class A {}
```

(see [https://wiki.php.net/rfc/not\_serializable\_attribute](https://wiki.php.net/rfc/not_serializable_attribute))

### 2. Excluding specific properties from serialization

Previously, developers had to manually list serializable properties:

```php
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:

```php
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.

### 3. Preparing data before serialization (the main use case of this RFC)

Previously:

```php
class A {
    public $a;
    function __sleep() {
        $this->cleanup();
        return ['a'];
    }
}
```

With this RFC:

```php
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 **&#96;null&#96;** 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

```php
class UserSession {
    public string $id;
    public Cache $cache;
 
    public function __sleep(): void {
        $this->cache->flush();
        // Returning nothing → default serialization
    }
}
```

### Example 2: Using with #\[NoSerialize\]

```php
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:

```php
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:

```php
#[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:

```php
"%s::__sleep() must return an array of property names, or return null/void to delegate to default serialization"
```

This replaces the older message:

```php
"%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

**Vote: Allow \_\_sleep() to return null or no value (void) to delegate to default serialization?** — Yes: 0 · No: 0 · Abstain: 0 (closed)

## Patches and Tests

<https://github.com/php/php-src/pull/20252>

## References

- [RFC: #\[NoSerialize\]](https://wiki.php.net/rfc/no_serialize_attribute)
- [RFC: #\[NotSerializable\]](https://wiki.php.net/rfc/not_serializable_attribute)

## Changelog

\-
