Skip to content

Serializable 接口

概述

Serializable 接口提供了自定义对象序列化/反序列化的能力。通过实现该接口,开发者可以完全控制对象在 serialize()unserialize() 过程中的行为,比 __sleep()/__wakeup() 魔术方法提供了更精细的控制。

PHP 7.4 引入了新的序列化魔术方法 __serialize()__unserialize(),PHP 官方推荐使用它们替代 Serializable 接口。PHP 8.0 中 Serializable 接口被标记为已弃用的替代方案。

版本说明

  • Serializable 接口从 PHP 5.1.0 起可用
  • PHP 7.4 引入 __serialize()/__unserialize() 作为推荐替代
  • PHP 8.1+ 中 Serializable 接口仍然可用但不推荐用于新项目

基础概念

接口定义

php
<?php

declare(strict_types=1);

// Serializable 接口定义
// interface Serializable
// {
//     public function serialize(): ?string;
//     public function unserialize(string $data): void;
// }

序列化机制概述

PHP 序列化是将 PHP 值转换为可存储/传输的字符串表示的过程。反序列化则相反,将字符串还原为 PHP 值。

php
<?php

declare(strict_types=1);

$data = ['name' => 'Alice', 'age' => 30];
$serialized = serialize($data);
// O:8:"stdClass":0:{}

$restored = unserialize($serialized);
// 恢复为原始数组

语法与代码

Serializable 接口实现

php
<?php

declare(strict_types=1);

class UserProfile implements Serializable
{
    private string $username;
    private string $email;
    private ?string $accessToken = null;

    public function __construct(string $username, string $email)
    {
        $this->username = $username;
        $this->email = $email;
    }

    public function getUsername(): string
    {
        return $this->username;
    }

    public function getEmail(): string
    {
        return $this->email;
    }

    public function setAccessToken(?string $token): void
    {
        $this->accessToken = $token;
    }

    public function serialize(): ?string
    {
        // 只序列化需要的属性(排除敏感的 accessToken)
        return serialize([
            'username' => $this->username,
            'email'    => $this->email,
        ]);
    }

    public function unserialize(string $data): void
    {
        $decoded = unserialize($data);
        $this->username = $decoded['username'];
        $this->email = $decoded['email'];
        // accessToken 不恢复
    }
}

$user = new UserProfile('alice', 'alice@example.com');
$user->setAccessToken('secret_token_123');

$serialized = serialize($user);
$restored = unserialize($serialized);

echo $restored->getUsername();  // alice
echo $restored->getEmail();     // alice@example.com

__sleep/__wakeup 方式

php
<?php

declare(strict_types=1);

class OldStyleUser
{
    private string $name;
    private string $password;  // 不应被序列化

    public function __construct(string $name, string $password)
    {
        $this->name = $name;
        $this->password = $password;
    }

    // __sleep 返回需要序列化的属性名列表
    public function __sleep(): array
    {
        return ['name'];  // 只序列化 name
    }

    // __wakeup 在反序列化后调用
    public function __wakeup(): void
    {
        // 重新初始化不应序列化的属性
        $this->password = '';
    }
}

详细说明

Serializable vs __sleep/__wakeup 对比

特性Serializable__sleep/__wakeup
控制粒度完全控制序列化格式只能选择序列化哪些属性
格式自由度完全自定义PHP 默认序列化格式
反序列化构造unserialize() 方法__wakeup() 方法
数据格式自定义字符串PHP 序列化字符串
嵌套对象处理手动处理自动处理
PHP 7.4+ 推荐不推荐不推荐

PHP 7.4+ 推荐方式:__serialize/__unserialize

php
<?php

declare(strict_types=1);

// PHP 7.4+ 推荐的新方式
class ModernUser
{
    private string $name;
    private string $email;
    private DateTimeImmutable $createdAt;

    public function __construct(string $name, string $email)
    {
        $this->name = $name;
        $this->email = $email;
        $this->createdAt = new DateTimeImmutable();
    }

    // 返回需要序列化的数据(数组)
    public function __serialize(): array
    {
        return [
            'name'      => $this->name,
            'email'     => $this->email,
            'createdAt' => $this->createdAt->format('Y-m-d H:i:s'),
        ];
    }

    // 从序列化数据中恢复对象
    public function __unserialize(array $data): void
    {
        $this->name = $data['name'];
        $this->email = $data['email'];
        $this->createdAt = new DateTimeImmutable($data['createdAt']);
    }
}

$user = new ModernUser('Bob', 'bob@example.com');
$serialized = serialize($user);
$restored = unserialize($serialized);

__serialize/__unserialize 与 __sleep/__wakeup 的关系

php
<?php

declare(strict_types=1);

class HybridUser
{
    private string $name;
    private string $email;

    // 如果同时定义了 __serialize 和 __sleep
    // PHP 优先使用 __serialize
    public function __sleep(): array
    {
        echo "__sleep 被调用\n";  // 不会被调用
        return ['name', 'email'];
    }

    public function __serialize(): array
    {
        echo "__serialize 被调用\n";  // 优先调用此方法
        return [
            'name'  => $this->name,
            'email' => $this->email,
        ];
    }

    public function __unserialize(array $data): void
    {
        echo "__unserialize 被调用\n";
        $this->name = $data['name'];
        $this->email = $data['email'];
    }

    public function __wakeup(): void
    {
        echo "__wakeup 被调用\n";  // 不会被调用
    }
}

优先级规则

当类同时定义了 __serialize()__sleep() 时,PHP 优先使用 __serialize()。同理,__unserialize() 优先于 __wakeup()

安全注意事项

php
<?php

declare(strict_types=1);

// 反序列化安全风险示例
class DangerousClass
{
    public function __unserialize(array $data): void
    {
        // 永远不要直接执行反序列化传入的数据
        // if (isset($data['callback'])) {
        //     call_user_func($data['callback']);  // 危险!
        // }

        // 始终验证数据
        $this->name = is_string($data['name'] ?? '') ? $data['name'] : '';
        $this->email = filter_var(
            $data['email'] ?? '',
            FILTER_VALIDATE_EMAIL
        ) ?: '';
    }

    private string $name = '';
    private string $email = '';
}

安全警告

反序列化不可信数据是 PHP 中常见的安全风险。unserialize() 可以触发 __wakeup()__unserialize() 中的任意代码执行。对于不可信数据,应使用 json_encode()/json_decode() 替代,或使用 allowed_classes 选项限制反序列化的类。

实战示例

安全的会话序列化

php
<?php

declare(strict_types=1);

class SessionData
{
    private string $userId;
    private string $userName;
    private int $lastActivity;
    private array $metadata = [];

    public function __construct(string $userId, string $userName)
    {
        $this->userId = $userId;
        $this->userName = $userName;
        $this->lastActivity = time();
    }

    public function setMeta(string $key, mixed $value): void
    {
        $this->metadata[$key] = $value;
    }

    public function __serialize(): array
    {
        return [
            'userId'       => $this->userId,
            'userName'     => $this->userName,
            'lastActivity' => $this->lastActivity,
            'metadata'     => $this->metadata,
        ];
    }

    public function __unserialize(array $data): void
    {
        // 验证必要字段
        if (!isset($data['userId']) || !is_string($data['userId'])) {
            throw new InvalidArgumentException('无效的 userId');
        }

        $this->userId = $data['userId'];
        $this->userName = $data['userName'] ?? 'Unknown';
        $this->lastActivity = (int) ($data['lastActivity'] ?? 0);
        $this->metadata = is_array($data['metadata'] ?? null)
            ? $data['metadata']
            : [];
    }

    public function getUserId(): string
    {
        return $this->userId;
    }

    public function isExpired(int $timeout = 1800): bool
    {
        return (time() - $this->lastActivity) > $timeout;
    }
}

// 使用
$session = new SessionData('u_12345', 'Alice');
$session->setMeta('role', 'admin');
$session->setMeta('ip', '192.168.1.1');

$serialized = serialize($session);
$restored = unserialize($serialized);

注意事项

私有属性的序列化

php
<?php

declare(strict_types=1);

class ParentClass
{
    private string $parentPrivate = 'parent';

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

class ChildClass extends ParentClass
{
    private string $childPrivate = 'child';

    public function __serialize(): array
    {
        // 必须包含父类的序列化数据
        return [
            'parentData' => parent::__serialize(),
            'childPrivate' => $this->childPrivate,
        ];
    }

    public function __unserialize(array $data): void
    {
        parent::__unserialize($data['parentData']);
        $this->childPrivate = $data['childPrivate'];
    }
}

闭包不可序列化

php
<?php

declare(strict_types=1);

class FilterOptions
{
    private string $field;
    private $callback;  // 闭包不能被序列化!

    public function __construct(string $field, callable $callback)
    {
        $this->field = $field;
        $this->callback = $callback;
    }

    public function __serialize(): array
    {
        // 闭包不能序列化,需要排除或使用其他方式保存
        return [
            'field' => $this->field,
            // 不包含 callback
        ];
    }

    public function __unserialize(array $data): void
    {
        $this->field = $data['field'];
        $this->callback = null;  // 恢复后回调丢失
    }
}

闭包限制

PHP 中的闭包(Closure)不能被序列化。如果对象包含闭包属性,必须在序列化时排除它,反序列化后重新设置。

最佳实践

  1. 使用 __serialize/__unserialize:PHP 7.4+ 新项目应优先使用 __serialize()/__unserialize()
  2. 排除敏感数据:密码、令牌、连接资源等不应被序列化
  3. 验证反序列化数据:在 __unserialize() 中严格验证数据格式和类型
  4. 使用 json 替代 serialize:对于需要与外部系统交互的数据,使用 JSON 格式更安全
  5. 考虑 allowed_classesunserialize($data, ['allowed_classes' => true]) 限制反序列化的类
php
<?php

declare(strict_types=1);

// 最佳实践:安全反序列化
$data = $_COOKIE['session'] ?? '';
if ($data === '') {
    throw new RuntimeException('缺少会话数据');
}

// 使用 allowed_classes 限制可反序列化的类
$session = unserialize($data, [
    'allowed_classes' => [SessionData::class]
]);

if (!$session instanceof SessionData) {
    throw new RuntimeException('无效的会话数据');
}

// 最佳实践:跨平台数据使用 JSON
$json = json_encode($user, JSON_THROW_ON_ERROR);
$restored = json_decode($json, true, 512, JSON_THROW_ON_ERROR);

参考链接