skip to content

In PHP 8.5, how do __serialize() and __unserialize() differ from __sleep() and __wakeup(), and which should a new class implement?

level: middleimportance: should knowfreq 45%

answer

  1. names only vs an arbitrary array
  2. __serialize() wins over __sleep()
  3. constructor never runs on unserialize
  4. soft-deprecated in 8.5, no warning
  5. Serializable deprecated alone since 8.1

basics

~20 s

__sleep() returns property names to keep and __wakeup() runs after they are restored; __serialize() returns any array and __unserialize() rebuilds the object from it. New classes should use __serialize()/__unserialize(): the older pair is soft-deprecated in PHP 8.5.

solid answer

~30 s

`__sleep()` can only return a **list of property names** to serialize, and `__wakeup()` runs with no arguments after PHP has restored those properties itself. `__serialize(): array` returns **any array you design**, with computed values, renamed keys or a version marker, and `__unserialize(array $data): void` receives that array and assigns state itself. When both pairs exist, PHP calls only `__serialize()`/`__unserialize()` and ignores `__sleep()`/`__wakeup()`; `__serialize()` also overrides a `Serializable::serialize()` method. PHP 8.5 **soft-deprecates** `__sleep()`/`__wakeup()` (documentation only, no diagnostic), and since 8.1 implementing `Serializable` without the new pair raises `E_DEPRECATED`. New code implements `__serialize()`/`__unserialize()` only.

code

php · 26 lines
php
<?php
declare(strict_types=1);

final class TariffTable
{
    private array $index = [];   // derived lookup, never stored

    public function __construct(
        public readonly string $region,
        private readonly array $rates,   // band => rate
    ) {
        $this->index = array_flip(array_keys($rates));
    }

    public function __serialize(): array
    {
        return ['v' => 2, 'region' => $this->region, 'rates' => $this->rates];
    }

    public function __unserialize(array $data): void
    {
        $this->region = $data['region'];
        $this->rates = ($data['v'] ?? 1) >= 2 ? $data['rates'] : $data['bands'];
        $this->index = array_flip(array_keys($this->rates));
    }
}

go deeper

for a junior

Recall the two pairs and that __serialize()/__unserialize() is the one to write today; __sleep() only lists property names.

for a middle

Explain precedence, that the constructor is skipped, the array contract of __serialize(), and the deprecation status of __sleep()/__wakeup() and Serializable.

for a senior

Show how a version key and derived-state rebuild in __unserialize() keep cached objects readable across deploys, and how to migrate Serializable classes.

for a principal

Decide whether domain objects should be serialized at all, or whether stores should hold explicit arrays mapped by factories, and set that as a codebase rule.

## Three generations of hooks PHP lets a class control how `serialize()` and `unserialize()` treat it. Three mechanisms exist, and interviewers ask which one to use and why. | Mechanism | Status in PHP 8.5 | Format tag | |---|---|---| | `__sleep()` / `__wakeup()` | **soft-deprecated** (8.5): documented, no diagnostic | `O:` | | `Serializable` interface | **deprecated** (8.1) unless the class also has the new pair | `C:` | | `__serialize()` / `__unserialize()` (added in 7.4) | the recommended mechanism | `O:` | ## __sleep() and __wakeup() - `__sleep()` runs before serialization and must return an **array of property names**. Only those properties are written. It cannot change a value, add a computed key or rename anything. - Names that do not exist produce a warning (`"x" returned as member variable from __sleep() but does not exist`), and a typed property that is still uninitialized is skipped. - `__wakeup()` runs after unserialization with no arguments; PHP has already copied the stored values into the properties, so the method can only re-establish derived state, such as reopening a connection. The weakness is that the stored shape *is* the property layout. Rename a property and old payloads no longer line up. ## The Serializable interface `Serializable` has two methods, `serialize()` returning a string and `unserialize(string $data)`. The class produces its own opaque string, written as a `C:` entry. It was hard to compose (nested objects needed their own calls) and awkward with references. Since PHP 8.1 a class that implements it **without** also defining `__serialize()`/`__unserialize()` triggers `E_DEPRECATED`: `... implements the Serializable interface, which is deprecated. Implement __serialize() and __unserialize() instead`. Keeping both is only for payloads written by very old PHP versions. ## __serialize() and __unserialize() - `public function __serialize(): array` returns **an arbitrary array** representing the object. Keys need not match property names. Returning anything but an array throws `TypeError` (`... __serialize() must return an array`). - `public function __unserialize(array $data): void` receives that array. PHP creates the object **without calling the constructor**, then calls this method to assign state. - Because the method runs in the class's own scope on a fresh object, it can initialize **readonly** properties, validate the data, and translate old payload shapes. ## Which one wins 1. `__serialize()` is used if present; `__sleep()` and `Serializable::serialize()` are then ignored. 2. `__unserialize()` is used if present; `__wakeup()` is then ignored. 3. Enum cases need none of this: they serialize as `E:` entries and come back as the same case. ## Mistakes seen in reviews - Returning an object or `null` from `__serialize()`: the method must return an array, or `TypeError` is thrown. - Putting a `PDO` handle, a closure or a generator into the returned array: those classes refuse serialization and throw `Exception`. - Assuming constructor validation protects unserialized objects: the constructor never runs, so `__unserialize()` must enforce the same invariants. - Keeping `__wakeup()` next to a new `__unserialize()` and expecting both to run: only `__unserialize()` is called. ## What to write in a new class Implement `__serialize()`/`__unserialize()` and nothing else. Useful habits: - put a **version key** in the array, so `__unserialize()` can read payloads written by an earlier release of the class; - leave out **derived state** (lookup indexes, open handles) and rebuild it in `__unserialize()`; - do not store resources, closures or connections; store what is needed to recreate them. Existing classes that still have `__sleep()`/`__wakeup()` keep working in 8.5 with no runtime notice, so the deprecation will not show up in logs. Find them with a search for the method names and migrate each class when you next touch it: 1. Move the property list from `__sleep()` into the array `__serialize()` returns, as explicit keys. 2. Move the body of `__wakeup()` to the end of `__unserialize()`, after the properties have been assigned from `$data`. 3. Keep reading old payloads: an `O:` entry written through `__sleep()` reaches `__unserialize()` as an array of stored property names, with protected and private names still NUL-mangled, so either translate those keys or version the cache so old entries are never read. 4. Delete `__sleep()`/`__wakeup()` once no stored payload depends on them.

  • Does the constructor run when unserialize() rebuilds an object?
    No. PHP creates the object without calling `__construct()`, applies declared default values, and then either copies the stored properties in or calls `__unserialize()`. Invariants the constructor enforces must therefore be re-checked in `__unserialize()`, and derived state must be rebuilt there.
  • How can __unserialize() assign a readonly property?
    A readonly property may be initialized once from inside the class's own scope. On unserialization the object is fresh and its readonly properties are uninitialized, and `__unserialize()` runs as a method of the class, so its first assignment is a legal initialization.
  • What does soft-deprecated mean for __sleep() and __wakeup() in PHP 8.5?
    The manual and the 8.5 upgrade notes mark them as deprecated in favour of `__serialize()`/`__unserialize()`, but PHP emits no `E_DEPRECATED` when they are used. Existing code keeps working; the signal is to migrate, not a runtime warning.

saying these in an interview costs you the question

  • __sleep() can return computed values to store in place of properties
  • The constructor runs again when unserialize() rebuilds the object
  • When both exist, __wakeup() runs after __unserialize()
  • Serializable is the modern replacement for __sleep() and __wakeup()
  • PHP 8.5 emits E_DEPRECATED every time __sleep() is called