serialize() и unserialize()
Сериализация -- процесс преобразования объекта в строку для хранения (кеш, сессии, файлы) и обратного восстановления.
<?php
declare(strict_types=1);
class User
{
public function __construct(
public string $name,
public string $email,
public int $age,
) {
}
}
$user = new User('Alice', '[email protected]', 30);
// Serialize to string
$serialized = serialize($user);
echo $serialized;
// O:4:"User":3:{s:4:"name";s:5:"Alice";s:5:"email";s:14:"[email protected]";s:3:"age";i:30;}
// Unserialize back to object
$restored = unserialize($serialized);
var_dump($restored instanceof User); // true
echo $restored->name; // Alice
echo $restored->email; // [email protected]
// Serialization format explained:
// O:4:"User" — Object, class name length 4, name "User"
// 3:{...} — 3 properties
// s:4:"name" — string, length 4, value "name" (property key)
// s:5:"Alice" — string, length 5, value "Alice" (property value)
// i:30 — integer, value 30
Сериализация разных типов
<?php
declare(strict_types=1);
// Scalars
echo serialize(42); // i:42;
echo serialize(3.14); // d:3.14;
echo serialize('hello'); // s:5:"hello";
echo serialize(true); // b:1;
echo serialize(false); // b:0;
echo serialize(null); // N;
// Arrays
echo serialize([1, 2, 3]);
// a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}
echo serialize(['key' => 'value']);
// a:1:{s:3:"key";s:5:"value";}
// What CANNOT be serialized:
// - Resources (file handles, DB connections)
// - Closures
// - Fibers
// - Some internal objects
$closure = function() { return 42; };
// serialize($closure); // Fatal error!
__serialize() и __unserialize() (PHP 7.4+, Recommended)
Современный способ контролировать сериализацию. Заменяет __sleep()/__wakeup().
<?php
declare(strict_types=1);
class DatabaseConnection
{
private ?\PDO $pdo = null;
public function __construct(
private readonly string $dsn,
private readonly string $username,
private readonly string $password,
) {
$this->connect();
}
private function connect(): void
{
// In real code: $this->pdo = new PDO($this->dsn, ...);
echo "Connected to {$this->dsn}\n";
$this->pdo = null; // Simplified for demo
}
// Called when serializing — return array of data to serialize
public function __serialize(): array
{
return [
'dsn' => $this->dsn,
'username' => $this->username,
// NOTE: We intentionally exclude $password for security
// NOTE: We exclude $pdo because resources can't be serialized
];
}
// Called when unserializing — receive the array from __serialize
public function __unserialize(array $data): void
{
// Restore properties from serialized data
// Use Reflection or direct assignment for readonly
$this->dsn = $data['dsn'];
$this->username = $data['username'];
$this->password = ''; // Password not restored — needs re-auth
// Reconnect to database
$this->connect();
}
public function getDsn(): string
{
return $this->dsn;
}
}
$db = new DatabaseConnection('mysql:host=localhost', 'root', 'secret');
// Connected to mysql:host=localhost
$serialized = serialize($db);
// Password is NOT in serialized string
$restored = unserialize($serialized);
// Connected to mysql:host=localhost (reconnected automatically)
echo $restored->getDsn(); // mysql:host=localhost
Преимущества __serialize() над __sleep()
<?php
declare(strict_types=1);
class ModernEntity
{
private int $computedValue;
public function __construct(
public readonly int $id,
public readonly string $name,
private readonly array $metadata,
) {
$this->computedValue = strlen($name) * $id;
}
// __serialize: full control over serialized data
public function __serialize(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'metadata' => $this->metadata,
// computedValue is excluded — will be recalculated
'version' => 2, // Can add extra data!
];
}
public function __unserialize(array $data): void
{
// Handle version migration
if (($data['version'] ?? 1) < 2) {
$data['metadata'] = []; // Default for old versions
}
// Reconstruct object
$this->id = $data['id'];
$this->name = $data['name'];
$this->metadata = $data['metadata'];
$this->computedValue = strlen($data['name']) * $data['id'];
}
public function getComputedValue(): int
{
return $this->computedValue;
}
}
$entity = new ModernEntity(5, 'Hello', ['type' => 'demo']);
echo $entity->getComputedValue(); // 25
$serialized = serialize($entity);
$restored = unserialize($serialized);
echo $restored->getComputedValue(); // 25 (recalculated)
__sleep() и __wakeup() (Legacy)
<?php
declare(strict_types=1);
// LEGACY approach — use __serialize/__unserialize instead
class LegacyCache
{
private string $data;
private mixed $connection = null; // Not serializable
public function __construct(string $data)
{
$this->data = $data;
$this->connection = 'active';
}
// Called BEFORE serialize — return array of property NAMES to include
public function __sleep(): array
{
// Only include $data, not $connection
return ['data'];
}
// Called AFTER unserialize — restore state
public function __wakeup(): void
{
$this->connection = 'reconnected';
}
}
$cache = new LegacyCache('important data');
$serialized = serialize($cache); // __sleep called — only 'data' serialized
$restored = unserialize($serialized); // __wakeup called — connection restored
Разница: __serialize vs __sleep
| Аспект | __sleep() |
__serialize() |
|---|---|---|
| Возвращает | Массив имен свойств | Массив данных (key => value) |
| Контроль | Выбор свойств для сериализации | Полный контроль над данными |
| Добавить данные | Нельзя | Можно (версия, метаданные) |
| Приоритет | Ниже | Выше (если оба определены) |
| Версия PHP | 4.0+ | 7.4+ |
Важно: Если определены оба метода,
__serialize()имеет приоритет над__sleep(). Используйте__serialize()/__unserialize()во всем новом коде.
Serializable interface (Deprecated 8.1)
<?php
declare(strict_types=1);
// DEPRECATED since PHP 8.1 — do NOT use in new code
class LegacySerializable implements Serializable
{
public function __construct(
private string $data,
) {
}
// Custom serialization format
public function serialize(): string
{
return json_encode(['data' => $this->data]);
}
// Custom unserialization
public function unserialize(string $data): void
{
$decoded = json_decode($data, true);
$this->data = $decoded['data'];
}
}
// Migration: replace Serializable with __serialize/__unserialize
class ModernReplacement
{
public function __construct(
private string $data,
) {
}
public function __serialize(): array
{
return ['data' => $this->data];
}
public function __unserialize(array $data): void
{
$this->data = $data['data'];
}
}
JsonSerializable для JSON
<?php
declare(strict_types=1);
class Product implements JsonSerializable
{
public function __construct(
private readonly int $id,
private readonly string $name,
private readonly float $price,
private readonly \DateTimeImmutable $createdAt,
private readonly ?string $description = null,
) {
}
public function jsonSerialize(): mixed
{
$data = [
'id' => $this->id,
'name' => $this->name,
'price' => round($this->price, 2),
'created_at' => $this->createdAt->format('c'),
];
if ($this->description !== null) {
$data['description'] = $this->description;
}
return $data;
}
}
$product = new Product(1, 'Widget', 19.99, new DateTimeImmutable('2024-01-15'));
echo json_encode($product, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
// {
// "id": 1,
// "name": "Widget",
// "price": 19.99,
// "created_at": "2024-01-15T00:00:00+00:00"
// }
// Without JsonSerializable, json_encode only encodes public properties
class SimpleProduct
{
public function __construct(
public int $id,
public string $name,
private string $secret = 'hidden', // Won't appear in JSON
) {
}
}
$simple = new SimpleProduct(1, 'Widget');
echo json_encode($simple);
// {"id":1,"name":"Widget"} — only public properties, secret excluded
Десериализация JSON (нет встроенного интерфейса)
<?php
declare(strict_types=1);
final readonly class UserDto implements JsonSerializable
{
public function __construct(
public int $id,
public string $name,
public string $email,
) {
}
// Manual factory method for deserialization
public static function fromJson(string $json): self
{
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
return new self(
id: (int) ($data['id'] ?? 0),
name: (string) ($data['name'] ?? ''),
email: (string) ($data['email'] ?? ''),
);
}
/**
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
id: (int) ($data['id'] ?? 0),
name: (string) ($data['name'] ?? ''),
email: (string) ($data['email'] ?? ''),
);
}
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
];
}
}
$json = '{"id": 1, "name": "Alice", "email": "[email protected]"}';
$user = UserDto::fromJson($json);
echo json_encode($user);
// {"id":1,"name":"Alice","email":"[email protected]"}
var_export() и __set_state()
<?php
declare(strict_types=1);
class Config
{
public function __construct(
public string $host = 'localhost',
public int $port = 5432,
public bool $ssl = false,
) {
}
// Called by var_export to create valid PHP code
public static function __set_state(array $properties): self
{
return new self(
host: $properties['host'],
port: $properties['port'],
ssl: $properties['ssl'],
);
}
}
$config = new Config('db.example.com', 5433, true);
// var_export generates valid PHP code
$code = var_export($config, true);
echo $code;
// \Config::__set_state(array(
// 'host' => 'db.example.com',
// 'port' => 5433,
// 'ssl' => true,
// ))
// Can be used to save/restore configs
$exportedCode = '<?php return ' . var_export($config, true) . ';';
// file_put_contents('/tmp/config.php', $exportedCode);
// $restored = require '/tmp/config.php';
// var_dump — for debugging (NOT for serialization)
var_dump($config);
// object(Config)#1 (3) {
// ["host"]=> string(14) "db.example.com"
// ["port"]=> int(5433)
// ["ssl"]=> bool(true)
// }
Безопасность unserialize()
unserialize() -- один из самых опасных вызовов в PHP. Неконтролируемая десериализация может привести к RCE (Remote Code Execution).
<?php
declare(strict_types=1);
// DANGER: Never unserialize untrusted data without restrictions!
// Attack vector: serialized data can trigger magic methods
class FileManager
{
private string $file;
public function __destruct()
{
// Attacker can trigger this with crafted serialized data
if (isset($this->file)) {
unlink($this->file); // Deletes arbitrary files!
}
}
}
// PROTECTION 1: allowed_classes option (PHP 7.0+)
$safe = unserialize($data, [
'allowed_classes' => [User::class, Product::class],
]);
// Only User and Product objects will be created
// All other classes → __PHP_Incomplete_Class (harmless)
// PROTECTION 2: Disallow all classes
$safest = unserialize($data, [
'allowed_classes' => false,
]);
// All objects become __PHP_Incomplete_Class
// PROTECTION 3: Don't use unserialize for user input at all
// Use json_decode instead — it NEVER creates objects with magic methods
// BEST PRACTICES:
// 1. NEVER unserialize user input
// 2. Always use 'allowed_classes' option
// 3. Prefer JSON for external data exchange
// 4. Use serialize/unserialize only for trusted internal caching
Безопасные альтернативы
<?php
declare(strict_types=1);
// For data exchange — use JSON
$json = json_encode($data, JSON_THROW_ON_ERROR);
$decoded = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
// For caching objects — use igbinary (faster, smaller)
// $binary = igbinary_serialize($object);
// $restored = igbinary_unserialize($binary);
// For API communication — use JSON with DTO hydration
final readonly class UserResponse
{
public function __construct(
public int $id,
public string $name,
) {
}
/**
* @param array{id: int, name: string} $data
*/
public static function fromArray(array $data): self
{
return new self(
id: $data['id'],
name: $data['name'],
);
}
}
// Safe: decode JSON then hydrate DTO
$json = '{"id": 1, "name": "Alice"}';
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
$user = UserResponse::fromArray($data);
Практический пример: Кеширование
<?php
declare(strict_types=1);
final class CacheItem
{
public function __construct(
private readonly mixed $value,
private readonly int $expiresAt,
) {
}
public function isExpired(): bool
{
return time() > $this->expiresAt;
}
public function getValue(): mixed
{
if ($this->isExpired()) {
throw new RuntimeException('Cache item expired');
}
return $this->value;
}
public function __serialize(): array
{
return [
'value' => $this->value,
'expires_at' => $this->expiresAt,
];
}
public function __unserialize(array $data): void
{
$this->value = $data['value'];
$this->expiresAt = $data['expires_at'];
}
}
final class FileCache
{
public function __construct(
private readonly string $directory,
) {
if (!is_dir($directory)) {
mkdir($directory, 0o755, true);
}
}
public function set(string $key, mixed $value, int $ttl = 3600): void
{
$item = new CacheItem($value, time() + $ttl);
$path = $this->getPath($key);
file_put_contents($path, serialize($item));
}
public function get(string $key): mixed
{
$path = $this->getPath($key);
if (!file_exists($path)) {
return null;
}
$item = unserialize(
file_get_contents($path),
['allowed_classes' => [CacheItem::class]], // Security!
);
if (!$item instanceof CacheItem || $item->isExpired()) {
unlink($path);
return null;
}
return $item->getValue();
}
private function getPath(string $key): string
{
return $this->directory . '/' . md5($key) . '.cache';
}
}
// Usage
$cache = new FileCache('/tmp/app-cache');
$cache->set('user:1', ['name' => 'Alice', 'email' => '[email protected]'], 3600);
$user = $cache->get('user:1'); // ['name' => 'Alice', 'email' => '[email protected]']