MidТеория8 min

Сериализация объектов

serialize/unserialize, __serialize/__unserialize, JsonSerializable, безопасность

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!

Современный способ контролировать сериализацию. Заменяет __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]']

Вопросы с экзамена ZCE

Проверь себя

5 из 13

Какой метод вызывается при `var_export()` для восстановления объекта?

Что делает `JsonSerializable::jsonSerialize()`?

Какие из утверждений о сериализации объектов верны?

Если определены и `__serialize()` и `__sleep()`, какой вызовется?

Зачем нужен параметр `allowed_classes` в `unserialize()`?