Table of Contents

PHP RFC: Soft-Deprecate __sleep() and __wakeup()

Introduction

A recent RFC to deprecate the __sleep() and __wakeup() magic methods in favor of __serialize() and __unserialize() passed (18–9), but only just met the required 2/3 majority. 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:

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:

> “There is no particular pressing need to phase out __sleep() and __wakeup().”

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'];
}

2. Complex handling of mangled property names with persistent storage

When applications store serialized objects in databases or other persistent storage, migration becomes significantly more complex due to PHP's property name mangling for private properties.

Consider this class that has multiple private properties but only serializes a subset of them:

class UserSession {
    private string $sessionId;
    private array $userData;
    private DateTime $lastAccess;
    private PDO $dbConnection;      // Not serialized - database connection
    private Logger $logger;         // Not serialized - logger instance
    private array $temporaryCache;  // Not serialized - runtime cache
 
    public function __sleep(): array {
        // Only serialize these three properties
        return ['sessionId', 'userData', 'lastAccess'];
    }
}

When __sleep() is used with private properties, PHP automatically mangles the property names in the serialized string using the format "\0" . $className . "\0" . $propertyName. For example, a serialized UserSession object would contain keys like:

"\0UserSession\0sessionId"
"\0UserSession\0userData"
"\0UserSession\0lastAccess"

These mangled names are stored in the database. During migration to __serialize(), developers face three problematic options:

Option 1: Manual property name mangling

public function __serialize(): array {
    return [
        "\0" . self::class . "\0sessionId" => $this->sessionId,
        "\0" . self::class . "\0userData" => $this->userData,
        "\0" . self::class . "\0lastAccess" => $this->lastAccess,
    ];
}

Option 2: Using get_mangled_object_vars() and filtering

public function __serialize(): array {
    $mangled = get_mangled_object_vars($this);
    // Filter to only include the properties we want to serialize
    return array_intersect_key($mangled, array_flip([
        "\0" . self::class . "\0sessionId",
        "\0" . self::class . "\0userData",
        "\0" . self::class . "\0lastAccess",
    ]));
}

Option 3: Use clean names but handle both formats in __unserialize()

public function __serialize(): array {
    return [
        'sessionId' => $this->sessionId,
        'userData' => $this->userData,
        'lastAccess' => $this->lastAccess,
    ];
}
 
public function __unserialize(array $data): void {
    // Handle old mangled format from database
    $sessionIdKey = "\0" . self::class . "\0sessionId";
    $userDataKey = "\0" . self::class . "\0userData";
    $lastAccessKey = "\0" . self::class . "\0lastAccess";
 
    $this->sessionId = $data['sessionId'] ?? $data[$sessionIdKey] ?? '';
    $this->userData = $data['userData'] ?? $data[$userDataKey] ?? [];
    $this->lastAccess = $data['lastAccess'] ?? $data[$lastAccessKey] ?? new DateTime();
}

All approaches require developers to understand PHP's internal property name mangling mechanism - a complex implementation detail that most developers shouldn't need to know. This significantly increases the cognitive load and error potential of the migration.

3. 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);
}

4. No technical urgency

Some consider that __sleep() is broken because it is incompatible with nested private properties. Yet, this is more of a limitation rather than something being broken: many use cases don't need access to private properties and are just fine with the method. When one needs to overcome this limitation, one can migrate to __serialize, on an opt-in basis.

Forcing this migration (by deprecating first) won't improve much in practice:

5. 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 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:

Proposal

1. Convert the voted deprecation to a soft deprecation (documentation-only).

2. Update documentation to reflect the soft deprecation:

“This mechanism is maintained for backward compatibility. New and existing code should be migrated to __serialize() / __unserialize() instead.”

Benefits

Drawbacks

Future Scope

Vote

The vote will require 2/3 majority.

Convert the deprecation to a documentation-based soft deprecation?
Real name Yes No Abstain
alcaeus   
alexandredaubois   
asgrim   
ashnazg   
ayesh   
bukka   
bwoebi   
crell   
cschneid   
daniels   
derick   
dharman   
dunglas   
galvao   
kalle   
mbeccati   
nicolasgrekas   
pmjones   
seld   
theodorejb   
timwolla   
Count: 17 4 0
This poll will close on 2025-10-04 00:00:00 UTC.