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)不能被序列化。如果对象包含闭包属性,必须在序列化时排除它,反序列化后重新设置。
最佳实践
- 使用 __serialize/__unserialize:PHP 7.4+ 新项目应优先使用
__serialize()/__unserialize() - 排除敏感数据:密码、令牌、连接资源等不应被序列化
- 验证反序列化数据:在
__unserialize()中严格验证数据格式和类型 - 使用 json 替代 serialize:对于需要与外部系统交互的数据,使用 JSON 格式更安全
- 考虑 allowed_classes:
unserialize($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);