[PHP]serialize完全解説|PHPの値をシリアライズして保存・転送する方法と安全な使い方

PHP

はじめに

PHPで配列やオブジェクトをファイルやデータベースに保存したり、セッションに格納したりするとき、そのままでは保存できません。このような場面で活躍するのが serialize() です。

serialize() はPHPのあらゆる値(配列・オブジェクト・スカラー値など)を1本の文字列に変換する関数です。逆変換は unserialize() で行います。

ただし、unserialize() には重大なセキュリティリスクが存在するため、本記事では安全な使い方についても詳しく解説します。


関数の基本情報

項目内容
関数名serialize()
利用可能バージョンPHP 4以降
所属変数操作関数(Variable Handling)
戻り値string(シリアライズされた文字列)
拡張機能不要(コア関数)

シグネチャ

serialize(mixed $value): string

パラメータ

パラメータ説明
$valuemixedシリアライズする値。resource型を除くすべての型が対象

注意: resource型(ファイルハンドルやDB接続)はシリアライズできません。シリアライズすると null に変換されます。


シリアライズ形式の基礎知識

serialize() が生成する文字列にはPHP独自の書式があります。

var_dump(serialize(true));          // string(4) "b:1;"
var_dump(serialize(42));            // string(5) "i:42;"
var_dump(serialize(3.14));          // string(8) "d:3.14;"
var_dump(serialize("hello"));       // string(12) "s:5:"hello";"
var_dump(serialize(null));          // string(2) "N;"
var_dump(serialize([1, 2, 3]));     // string(30) "a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}"

書式の読み方

プレフィックス
b:boolb:1;(true)、b:0;(false)
i:inti:42;
d:floatd:3.14;
s:strings:5:"hello";(長さ:値)
N;nullN;
a:arraya:要素数:{...}
O:objectO:クラス名長:"クラス名":プロパティ数:{...}
C:カスタムシリアライズSerializable実装クラス

json_encode() との比較

観点serialize()json_encode()
対象PHP全型(resourceを除く)JSON互換型のみ
オブジェクト復元✅ クラス情報保持❌ stdClassになる
private/protectedプロパティ✅ 保持❌ 不可
可読性△ PHP専用書式✅ 人間が読める
言語間互換❌ PHP専用✅ 言語非依存
セキュリティリスク⚠️ unserializeに注意比較的安全
速度やや速いやや遅い

方針: PHP内部でのみ使うデータにはserialize()、外部API・フロントエンドとのやり取りにはjson_encode()が適しています。


実践サンプル集(PHP 8.x対応)

サンプル1:基本的なシリアライズと復元

<?php
declare(strict_types=1);

// スカラー値
$values = [true, 42, 3.14, 'hello', null];
foreach ($values as $v) {
    $serialized = serialize($v);
    $restored   = unserialize($serialized);
    echo sprintf(
        "%-10s → %-30s → %s\n",
        var_export($v, true),
        $serialized,
        var_export($restored, true)
    );
}

echo "\n";

// 配列
$data = [
    'name'   => '中島太郎',
    'scores' => [85, 92, 78],
    'active' => true,
];

$serialized = serialize($data);
echo "シリアライズ:\n{$serialized}\n\n";

$restored = unserialize($serialized);
echo "復元後:\n";
print_r($restored);

実行結果:

true       → b:1;                          → true
42         → i:42;                         → 42
3.14       → d:3.14;                       → 3.14
'hello'    → s:5:"hello";                 → 'hello'
NULL       → N;                            → NULL

シリアライズ:
a:3:{s:4:"name";s:12:"中島太郎";s:6:"scores";a:3:{i:0;i:85;i:1;i:92;i:2;i:78;}s:6:"active";b:1;}

復元後:
Array
(
    [name] => 中島太郎
    [scores] => Array ( [0] => 85 [1] => 92 [2] => 78 )
    [active] => 1
)

解説: serialize()unserialize()は完全に対称です。文字列長がバイト数で記録されるため、マルチバイト文字(UTF-8)も正確に保存・復元できます。


サンプル2:オブジェクトのシリアライズ

<?php
declare(strict_types=1);

class UserProfile
{
    public function __construct(
        private readonly int $id,
        private string $name,
        private string $email,
        private array $roles = [],
        private \DateTimeImmutable $createdAt = new \DateTimeImmutable(),
    ) {}

    public function getId(): int   { return $this->id; }
    public function getName(): string { return $this->name; }
    public function getEmail(): string { return $this->email; }
    public function getRoles(): array  { return $this->roles; }

    public function withRole(string $role): self
    {
        $clone = clone $this;
        $clone->roles[] = $role;
        return $clone;
    }

    public function __toString(): string
    {
        return sprintf(
            'UserProfile(id=%d, name=%s, roles=[%s])',
            $this->id, $this->name, implode(',', $this->roles)
        );
    }
}

// オブジェクト生成
$user = (new UserProfile(1, '田中花子', 'hanako@example.com'))
    ->withRole('editor')
    ->withRole('viewer');

echo "元のオブジェクト: {$user}\n";

// シリアライズ
$serialized = serialize($user);
echo "\nシリアライズ文字列(抜粋):\n";
echo substr($serialized, 0, 120) . "...\n";

// 復元
$restored = unserialize($serialized);
echo "\n復元後: {$restored}\n";
echo "IDの一致: " . ($user->getId() === $restored->getId() ? '✅' : '❌') . "\n";
echo "ロール数: " . count($restored->getRoles()) . "\n";

実行結果:

元のオブジェクト: UserProfile(id=1, name=田中花子, roles=[editor,viewer])

シリアライズ文字列(抜粋):
O:11:"UserProfile":5:{s:15:"UserProfileid";i:1;s:17:"UserProfilename";s:9:"...

復元後: UserProfile(id=1, name=田中花子, roles=[editor,viewer])
IDの一致: ✅
ロール数: 2

解説: privateprotectedプロパティも含めてクラス情報ごと保存されます。unserialize()時に対象クラスが存在しない場合は __PHP_Incomplete_Class になるため、復元時にはオートロードが必要です。


サンプル3:__sleep() と __wakeup() による制御

シリアライズ・復元時の動作をフックするマジックメソッドです。

<?php
declare(strict_types=1);

class DatabaseConnection
{
    private mixed $connection = null;
    private string $dsn;
    private string $user;
    private string $password;
    private bool $connected = false;

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

    public function connect(): void
    {
        // 実際にはPDOなどで接続
        $this->connected = true;
        $this->connection = 'connection_handle'; // ダミー
        echo "接続しました: {$this->dsn}\n";
    }

    /**
     * シリアライズ対象のプロパティ名を返す
     * connection(resourceなど)は除外、passwordも保存しない
     */
    public function __sleep(): array
    {
        echo "__sleep() 呼び出し → connectionとpasswordを除外\n";
        return ['dsn', 'user']; // 保存するプロパティ名のみ
    }

    /**
     * unserialize() 後に自動で呼ばれる
     */
    public function __wakeup(): void
    {
        echo "__wakeup() 呼び出し → 再接続を促す\n";
        $this->connected  = false;
        $this->connection = null;
        // 自動再接続したい場合はここで $this->connect() を呼ぶ
    }

    public function isConnected(): bool { return $this->connected; }
    public function getDsn(): string    { return $this->dsn; }
}

$db = new DatabaseConnection('mysql:host=localhost;dbname=myapp', 'root', 'secret');
$db->connect();

echo "\n--- シリアライズ ---\n";
$serialized = serialize($db);
echo "文字列: {$serialized}\n";

echo "\n--- 復元 ---\n";
$restored = unserialize($serialized);
echo "DSN: "         . $restored->getDsn() . "\n";
echo "接続状態: "    . ($restored->isConnected() ? '接続中' : '未接続') . "\n";

実行結果:

接続しました: mysql:host=localhost;dbname=myapp

--- シリアライズ ---
__sleep() 呼び出し → connectionとpasswordを除外
文字列: O:18:"DatabaseConnection":2:{s:21:"DatabaseConnectiondsn";s:32:"mysql:host=localhost;dbname=myapp";s:22:"DatabaseConnectionuser";s:4:"root";}

--- 復元 ---
__wakeup() 呼び出し → 再接続を促す
DSN: mysql:host=localhost;dbname=myapp
接続状態: 未接続

解説: __sleep() でパスワードやDB接続ハンドルを除外し、__wakeup() で復元後の状態を初期化することで、機密情報の漏洩やリソースの不整合を防げます。


サンプル4:Serializableインターフェースと__serialize()/__unserialize()

PHP 7.4以降で推奨される新しいシリアライズフックです。

<?php
declare(strict_types=1);

/**
 * PHP 7.4+ 推奨:__serialize() / __unserialize()
 */
class SecureToken
{
    private string $token;
    private int $expiresAt;
    private string $userId;

    public function __construct(string $userId, int $ttlSeconds = 3600)
    {
        $this->userId    = $userId;
        $this->token     = bin2hex(random_bytes(32));
        $this->expiresAt = time() + $ttlSeconds;
    }

    /**
     * シリアライズ時に呼ばれる(__sleep()より高機能)
     * 返す配列がそのままシリアライズされる
     */
    public function __serialize(): array
    {
        return [
            'u' => $this->userId,
            't' => $this->token,
            'e' => $this->expiresAt,
            // 署名を追加して改ざん検知
            's' => hash_hmac('sha256', $this->userId . $this->token . $this->expiresAt, 'app-secret'),
        ];
    }

    /**
     * 復元時に呼ばれる(__wakeup()より高機能)
     * __serialize()が返した配列を受け取る
     */
    public function __unserialize(array $data): void
    {
        // 署名の検証
        $expected = hash_hmac(
            'sha256',
            $data['u'] . $data['t'] . $data['e'],
            'app-secret'
        );

        if (!hash_equals($expected, $data['s'])) {
            throw new \RuntimeException('トークンが改ざんされています');
        }

        $this->userId    = $data['u'];
        $this->token     = $data['t'];
        $this->expiresAt = $data['e'];
    }

    public function isExpired(): bool    { return time() > $this->expiresAt; }
    public function getUserId(): string  { return $this->userId; }
    public function getToken(): string   { return $this->token; }
}

$token = new SecureToken('user_42', 7200);

$serialized = serialize($token);
echo "シリアライズ:\n{$serialized}\n\n";

$restored = unserialize($serialized);
echo "userId: " . $restored->getUserId() . "\n";
echo "期限切れ: " . ($restored->isExpired() ? 'yes' : 'no') . "\n";

// 改ざんテスト
$tampered = str_replace($token->getUserId(), 'hacker', $serialized);
try {
    unserialize($tampered);
} catch (\RuntimeException $e) {
    echo "\n改ざん検知: " . $e->getMessage() . "\n";
}

実行結果:

シリアライズ:
O:11:"SecureToken":4:{s:1:"u";s:7:"user_42";s:1:"t";s:64:"...";s:1:"e";i:...;s:1:"s";s:64:"...";}

userId: user_42
期限切れ: no

改ざん検知: トークンが改ざんされています

解説: __serialize()/__unserialize()__sleep()/__wakeup() より柔軟で、返す配列の構造を自由に設計できます。HMAC署名を埋め込むことで、復元時に改ざん検知も実現しています。


サンプル5:セッションキャッシュへの応用

重い計算結果やDBクエリ結果をセッションにキャッシュするパターンです。

<?php
declare(strict_types=1);

class SessionCache
{
    private string $prefix;

    public function __construct(string $prefix = 'cache_')
    {
        $this->prefix = $prefix;
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
    }

    public function set(string $key, mixed $value, int $ttl = 300): void
    {
        $_SESSION[$this->prefix . $key] = serialize([
            'data'    => $value,
            'expires' => time() + $ttl,
        ]);
    }

    public function get(string $key, mixed $default = null): mixed
    {
        $raw = $_SESSION[$this->prefix . $key] ?? null;
        if ($raw === null) {
            return $default;
        }

        $entry = unserialize($raw, ['allowed_classes' => false]);
        if ($entry === false || time() > $entry['expires']) {
            $this->delete($key);
            return $default;
        }

        return $entry['data'];
    }

    public function has(string $key): bool
    {
        return $this->get($key) !== null;
    }

    public function delete(string $key): void
    {
        unset($_SESSION[$this->prefix . $key]);
    }

    public function remember(string $key, int $ttl, callable $callback): mixed
    {
        if ($this->has($key)) {
            echo "[キャッシュヒット] {$key}\n";
            return $this->get($key);
        }

        echo "[キャッシュミス] {$key} → 生成中...\n";
        $value = $callback();
        $this->set($key, $value, $ttl);
        return $value;
    }
}

// --- 使用例 ---
$cache = new SessionCache('myapp_');

// 重い処理をキャッシュ
$result = $cache->remember('user_list', 60, function () {
    // 実際にはDBクエリなど
    sleep(0); // ダミー処理
    return [
        ['id' => 1, 'name' => 'Alice', 'score' => 95],
        ['id' => 2, 'name' => 'Bob',   'score' => 87],
        ['id' => 3, 'name' => 'Carol', 'score' => 92],
    ];
});

echo "ユーザー数: " . count($result) . "\n";

// 2回目はキャッシュから
$result2 = $cache->remember('user_list', 60, function () {
    return []; // 呼ばれないはず
});

echo "2回目ユーザー数: " . count($result2) . "\n";

実行結果:

[キャッシュミス] user_list → 生成中...
ユーザー数: 3
[キャッシュヒット] user_list
2回目ユーザー数: 3

解説: unserialize() の第2引数 ['allowed_classes' => false] を指定することで、オブジェクトの復元を禁止しセキュリティリスクを低減しています。セッションキャッシュでは配列で十分な場合がほとんどです。


サンプル6:ファイルベースのオブジェクトキャッシュ

オブジェクトをファイルに永続化するシンプルなキャッシュシステムです。

<?php
declare(strict_types=1);

class FileObjectCache
{
    private string $cacheDir;

    public function __construct(string $cacheDir = '/tmp/php_cache')
    {
        $this->cacheDir = rtrim($cacheDir, '/');
        if (!is_dir($this->cacheDir)) {
            mkdir($this->cacheDir, 0700, true);
        }
    }

    public function save(string $key, mixed $value, int $ttl = 600): void
    {
        $payload = [
            'value'     => $value,
            'expires'   => time() + $ttl,
            'createdAt' => date('Y-m-d H:i:s'),
        ];

        $path = $this->path($key);
        file_put_contents($path, serialize($payload), LOCK_EX);
        chmod($path, 0600); // 自プロセスのみ読み取り可
    }

    public function load(string $key, array $allowedClasses = []): mixed
    {
        $path = $this->path($key);
        if (!file_exists($path)) {
            return null;
        }

        $raw = file_get_contents($path);
        if ($raw === false) {
            return null;
        }

        // allowed_classes で許可クラスを明示
        $options = empty($allowedClasses)
            ? ['allowed_classes' => false]
            : ['allowed_classes' => $allowedClasses];

        $payload = unserialize($raw, $options);
        if ($payload === false || time() > $payload['expires']) {
            $this->delete($key);
            return null;
        }

        return $payload['value'];
    }

    public function delete(string $key): void
    {
        $path = $this->path($key);
        if (file_exists($path)) {
            unlink($path);
        }
    }

    public function clear(): void
    {
        foreach (glob($this->cacheDir . '/*.cache') ?: [] as $file) {
            unlink($file);
        }
    }

    private function path(string $key): string
    {
        return $this->cacheDir . '/' . hash('sha256', $key) . '.cache';
    }
}

// --- 使用例 ---
class Product
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly float $price,
    ) {}
}

$cache = new FileObjectCache();

// Productオブジェクトをキャッシュ
$product = new Product(101, 'PHPマニュアル', 2980.0);
$cache->save('product_101', $product, 300);

// 復元(Productクラスを許可)
$loaded = $cache->load('product_101', [Product::class]);

if ($loaded instanceof Product) {
    echo "ID: {$loaded->id}\n";      // ID: 101
    echo "名前: {$loaded->name}\n"; // 名前: PHPマニュアル
    echo "価格: {$loaded->price}円\n"; // 価格: 2980円
}

$cache->clear();
echo "キャッシュをクリアしました\n";

実行結果:

ID: 101
名前: PHPマニュアル
価格: 2980円
キャッシュをクリアしました

解説: unserialize()allowed_classes オプションで許可するクラスを明示することが重要です。false(全禁止)または許可クラス名の配列を指定し、意図しないクラスが復元されることによるオブジェクトインジェクション攻撃を防ぎます。


⚠️ セキュリティ:unserialize() の危険性

serialize() 自体は安全ですが、unserialize() に外部入力を渡すことは非常に危険です。

オブジェクトインジェクション攻撃

// ❌ 絶対にやってはいけない
$data = unserialize($_GET['data']);     // GETパラメータをそのまま復元
$data = unserialize($_COOKIE['cart']); // クッキーをそのまま復元
$data = unserialize(file_get_contents('php://input')); // リクエストボディ

攻撃者が細工したシリアライズ文字列を送り込み、アプリケーション内に存在するクラスの __destruct()__wakeup() などのマジックメソッドを悪用してコード実行が可能になります(POP Chain攻撃)。

安全な代替策

// ✅ 外部データにはJSONを使う
$data = json_decode($_GET['data'], true);

// ✅ 不可避の場合はallowed_classesでクラスを制限
$data = unserialize($raw, ['allowed_classes' => false]);          // オブジェクト禁止
$data = unserialize($raw, ['allowed_classes' => [MyClass::class]]); // 特定クラスのみ

// ✅ HMACで署名・検証してから復元
function secureUnserialize(string $data, string $secret): mixed
{
    [$signature, $payload] = explode(':', $data, 2) + ['', ''];
    if (!hash_equals(hash_hmac('sha256', $payload, $secret), $signature)) {
        throw new \RuntimeException('データが改ざんされています');
    }
    return unserialize(base64_decode($payload), ['allowed_classes' => false]);
}

function secureSerialize(mixed $value, string $secret): string
{
    $payload   = base64_encode(serialize($value));
    $signature = hash_hmac('sha256', $payload, $secret);
    return $signature . ':' . $payload;
}

よくある落とし穴

① resource型はシリアライズできない

$fp = fopen('/tmp/test.txt', 'w');
$serialized = serialize($fp); // resource → null に変換される

$restored = unserialize($serialized);
var_dump($restored); // NULL

ファイルハンドル・DB接続・curl等は __sleep() で除外し、__wakeup() で再取得してください。

② 浮動小数点の精度に注意

$val = 1.0000000000001;
$restored = unserialize(serialize($val));
var_dump($val === $restored); // bool(true) ※精度による場合あり
// 高精度が必要なデータはBCMathや文字列で扱うこと

③ 循環参照は扱えるが注意が必要

$a = new stdClass();
$b = new stdClass();
$a->b = $b;
$b->a = $a; // 循環参照

$s = serialize($a); // 動作するが巨大になる可能性

④ クラス定義が存在しないと __PHP_Incomplete_Class になる

// FooClass が定義されていない状態でunserialize
$obj = unserialize('O:8:"FooClass":0:{}');
var_dump($obj instanceof \__PHP_Incomplete_Class); // bool(true)
// → autoloadが正しく設定されているか確認

まとめ

ポイント内容
主な用途セッション・ファイル・DBへのPHP値の永続化
オブジェクト対応private/protectedプロパティ含めクラス情報ごと保存
シリアライズ制御__sleep()/__wakeup()、または__serialize()/__unserialize()(PHP 7.4+推奨)
セキュリティunserialize()に外部入力を渡さない。allowed_classesを活用
外部連携他言語・フロントエンドとのやり取りにはjson_encode()を使う
resourceシリアライズ不可(nullに変換される)

serialize() はPHPのデータ永続化において今も現役の関数ですが、unserialize() の安全な使い方を正しく理解することが必須です。外部からのデータには使わず、allowed_classes やHMAC署名を組み合わせた防御設計を心がけましょう。


PHP 8.x / 執筆時点の最新安定版にて動作確認済み

タイトルとURLをコピーしました