[PHP]__serialize()とは?オブジェクトのシリアライズ内容を自在に制御する方法を徹底解説

PHP

はじめに

これまでの記事で、プロパティオーバーロードに関わる4つのマジックメソッド(__get()、__set()、__isset()、__unset())を解説してきました。今回取り上げる __serialize() は少し毛色が異なり、PHPの組み込み関数 serialize() によってオブジェクトが文字列化される際に、そのシリアライズ内容を自分で制御するためのマジックメソッドです。

デフォルトでは、serialize() はオブジェクトの全プロパティ(private/protectedを含む)をそのまま文字列化しますが、これではデータベース接続やファイルハンドルのようなシリアライズ不可能なリソースを含むオブジェクトを扱えません。__serialize() を実装することで、「どのデータをシリアライズ対象にするか」を開発者が明示的に制御できるようになります。本記事では、PHP 7.4で導入されたこのモダンな仕組みの基本的な使い方から、対になる __unserialize() との連携、実践的な活用例まで詳しく解説します。


メソッド概要

項目内容
メソッド名__serialize()
種別マジックメソッド
シグネチャpublic function __serialize(): array
引数なし
呼び出しタイミングserialize() 関数がそのオブジェクトを文字列化する際
戻り値シリアライズ対象とするデータを表す配列
対応バージョンPHP 7.4.0以降
対になるメソッド__unserialize()(復元時に呼ばれる)
旧来の仕組み__sleep() / __wakeup()(PHP 7.4より前から存在)

シリアライズの流れ(イメージ図)

  $user = new User('太郎', $dbConnection);
              │
              ▼
        serialize($user)
              │
              ▼
  ┌───────────────────────────┐
  │ __serialize() が自動的に呼ばれる      │
  │ → シリアライズしたいデータだけを       │
  │   配列として明示的に返す               │
  │   (dbConnectionのような               │
  │    シリアライズ不可能なものは除外)      │
  └───────────┬───────────────┘
              ▼
  戻り値の配列がシリアライズされ、
  文字列として保存される
              │
              ▼ (後日、復元時)
        unserialize($string)
              │
              ▼
  __unserialize($array) が呼ばれ、
  保存されていたデータからオブジェクトを再構築する

ポイントは、__serialize() が**「このオブジェクトの状態を保存するなら、どのデータが必要か」を開発者自身が明示的に定義できる**という点です。これにより、不要なデータの除外、機密情報のマスキング、シリアライズ不可能なリソースの除去などを、シリアライズ処理に組み込むことができます。


実践サンプル7選

例1:基本的な使い方

<?php

class BasicSerializeDemo
{
    public function __construct(
        private string $name,
        private int $age
    ) {
    }

    /**
     * シリアライズしたいプロパティだけを配列として返す
     */
    public function __serialize(): array
    {
        return [
            'name' => $this->name,
            'age'  => $this->age,
        ];
    }

    /**
     * __serialize()とセットで実装するのが基本
     */
    public function __unserialize(array $data): void
    {
        $this->name = $data['name'];
        $this->age = $data['age'];
    }
}

$demo = new BasicSerializeDemo('太郎', 30);
$serialized = serialize($demo);
echo $serialized . PHP_EOL;

$restored = unserialize($serialized);
echo "{$restored->__serialize()['name']}さん" . PHP_EOL;

例2:データベース接続のようなリソースを除外するクラス

<?php

class DatabaseAwareModel
{
    private array $attributes;
    private ?PDO $connection;

    public function __construct(array $attributes, ?PDO $connection = null)
    {
        $this->attributes = $attributes;
        $this->connection = $connection;
    }

    /**
     * PDO接続のようなシリアライズ不可能なリソースを
     * 保存対象から明示的に除外する
     */
    public function __serialize(): array
    {
        return ['attributes' => $this->attributes];
        // $this->connectionは意図的に含めない
    }

    public function __unserialize(array $data): void
    {
        $this->attributes = $data['attributes'];
        $this->connection = null; // 復元後、必要なら再接続する
    }
}

$model = new DatabaseAwareModel(['id' => 1, 'name' => 'サンプル'], null);
$serialized = serialize($model);
echo $serialized . PHP_EOL;

例3:機密情報をシリアライズ対象から除外するセキュリティ対策

<?php

class SecureUserSession
{
    public function __construct(
        private string $username,
        private string $sessionToken,
        private string $internalApiKey
    ) {
    }

    /**
     * セッションをキャッシュやログに保存する際、
     * 機密性の高いAPIキーを含めないようにする
     */
    public function __serialize(): array
    {
        return [
            'username'      => $this->username,
            'sessionToken'  => $this->sessionToken,
            // internalApiKeyは意図的に除外し、情報漏洩を防ぐ
        ];
    }

    public function __unserialize(array $data): void
    {
        $this->username = $data['username'];
        $this->sessionToken = $data['sessionToken'];
        $this->internalApiKey = ''; // 復元後、別途安全な方法で再取得する想定
    }
}

$session = new SecureUserSession('taro', 'tok_abc123', 'secret_key_xyz');
echo serialize($session) . PHP_EOL;
// 出力にinternalApiKeyの値は含まれない

例4:計算済みプロパティを保存せず復元時に再計算するクラス

<?php

class CachedCalculation
{
    private array $rawValues;
    private ?float $cachedSum = null;

    public function __construct(array $rawValues)
    {
        $this->rawValues = $rawValues;
    }

    public function getSum(): float
    {
        $this->cachedSum ??= array_sum($this->rawValues);
        return $this->cachedSum;
    }

    /**
     * キャッシュされた計算結果は保存せず、
     * 元データのみをシリアライズすることでサイズを削減する
     */
    public function __serialize(): array
    {
        return ['rawValues' => $this->rawValues];
    }

    public function __unserialize(array $data): void
    {
        $this->rawValues = $data['rawValues'];
        $this->cachedSum = null; // 復元後、必要になった時点で再計算される
    }
}

$calc = new CachedCalculation([10, 20, 30]);
$calc->getSum(); // キャッシュが作られる
$serialized = serialize($calc);
echo $serialized . PHP_EOL; // cachedSumは含まれていない

例5:バージョン情報を含めて将来的な互換性に備えるクラス

<?php

class VersionedData
{
    private const CURRENT_VERSION = 2;

    public function __construct(private string $value)
    {
    }

    /**
     * シリアライズ時にバージョン番号を含めておくことで、
     * 将来のデータ構造変更時に移行処理を判定できるようにする
     */
    public function __serialize(): array
    {
        return [
            'version' => self::CURRENT_VERSION,
            'value'   => $this->value,
        ];
    }

    public function __unserialize(array $data): void
    {
        $version = $data['version'] ?? 1;

        if ($version < 2) {
            // 旧バージョンのデータ構造からの移行処理(例)
            $this->value = strtoupper($data['value'] ?? '');
        } else {
            $this->value = $data['value'];
        }
    }
}

$versioned = new VersionedData('sample');
echo serialize($versioned) . PHP_EOL;

例6:継承関係における親子クラスのシリアライズ連携

<?php

class BaseEntity
{
    public function __construct(protected int $id)
    {
    }

    protected function baseSerializeData(): array
    {
        return ['id' => $this->id];
    }

    protected function baseUnserializeData(array $data): void
    {
        $this->id = $data['id'];
    }
}

class ExtendedEntity extends BaseEntity
{
    public function __construct(int $id, private string $label)
    {
        parent::__construct($id);
    }

    /**
     * 親クラスのデータ構築処理を再利用しつつ、
     * 子クラス独自のプロパティを追加する
     */
    public function __serialize(): array
    {
        return $this->baseSerializeData() + ['label' => $this->label];
    }

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

$entity = new ExtendedEntity(1, 'サンプルラベル');
echo serialize($entity) . PHP_EOL;

例7:__sleep()との違いを比較するデモ

<?php

class OldStyleSleep
{
    public function __construct(private string $name, private $resource = null)
    {
    }

    /**
     * __sleep()は"シリアライズしたいプロパティ名の配列"を返す
     * (PHP 7.4より前からある古いスタイル)
     */
    public function __sleep(): array
    {
        return ['name']; // resourceは除外
    }
}

class ModernStyleSerialize
{
    public function __construct(private string $name, private $resource = null)
    {
    }

    /**
     * __serialize()は"シリアライズしたいデータそのもの"を
     * 自由な形式の配列として返す
     */
    public function __serialize(): array
    {
        return ['custom_key' => $this->name]; // キー名も自由に変更できる
    }

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

echo serialize(new OldStyleSleep('太郎')) . PHP_EOL;
echo serialize(new ModernStyleSerialize('太郎')) . PHP_EOL;

関連機能との比較

機能役割__serialize()との違い
__serialize()シリアライズするデータを自由形式の配列で定義本記事の対象。PHP 7.4以降の推奨される仕組み
__unserialize()シリアライズされたデータからオブジェクトを復元__serialize()と対になる、復元処理を担うメソッド
__sleep()シリアライズ対象のプロパティ「名」の配列を返すより古い仕組み。プロパティ名の配列しか返せず自由度が低い
__wakeup()復元時に呼ばれる、__unserialize()より古い仕組みデフォルトのプロパティ復元後に追加処理を行う形式
JsonSerializablejson_encode()時の出力を制御するインターフェースシリアライズ形式がJSONである点が異なる、別の仕組み

よくある落とし穴(注意点)

  1. __serialize()と__unserialize()はセットで実装する どちらか一方だけを実装すると、正しくシリアライズ・復元のサイクルが機能しません。両方を必ずペアで実装しましょう(すべての例で両方を実装しています)。
  2. __sleep()/__wakeup()と__serialize()/__unserialize()が共存する場合の優先順位 同じクラスに両方の仕組みが定義されている場合、PHP 7.4以降では __serialize()/__unserialize() が優先されます。新規開発では、より自由度の高い __serialize()/__unserialize() を使うことが推奨されます。
  3. 戻り値のキー名は__unserialize()側と一致させる必要がある __serialize() が返す配列のキー名と、__unserialize() が読み取るキー名が一致していないと、正しくデータが復元されません。キー名の変更時は両方のメソッドを同時に更新しましょう。
  4. シリアライズされたデータに機密情報が含まれないよう注意する デフォルトの挙動(マジックメソッド未実装)では全プロパティがシリアライズされるため、意図せずパスワードやAPIキーなどの機密情報がシリアライズデータに含まれてしまうことがあります。__serialize() を使って、必要なデータだけを明示的に選別することが推奨されます(例3を参照)。
  5. セキュリティ上のリスクはserialize()/unserialize()自体にもあることを忘れない __serialize()/__unserialize() はシリアライズ内容を制御する仕組みですが、unserialize() 自体に信頼できない入力を渡すことの危険性(PHPオブジェクトインジェクション)は別途存在します。この点は unserialize() に関する別の記事で詳しく解説していますので、併せて確認することをおすすめします。

まとめ

観点まとめ
何をするメソッドかserialize()によるオブジェクトの文字列化時に、保存するデータを自由形式の配列で定義する
主な用途リソースの除外、機密情報のマスキング、キャッシュデータの軽量化、バージョン管理
対になるメソッド__unserialize()(必ずセットで実装する)
旧来の仕組みとの関係__sleep()/__wakeup()より新しく、より自由度の高いPHP 7.4以降の標準
注意点キー名の一貫性、機密情報の除外、unserialize()自体のセキュリティリスクとは別問題であること

__serialize() は、オブジェクトの永続化やキャッシュ保存において、「何を保存し、何を保存しないか」を明示的にコントロールできる、PHP 7.4以降の洗練された仕組みです。__unserialize() と対で実装し、シリアライズ不可能なリソースの除外や機密情報の保護に積極的に活用していきましょう。

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