はじめに
これまでの記事で、プロパティオーバーロードに関わる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()より古い仕組み | デフォルトのプロパティ復元後に追加処理を行う形式 |
JsonSerializable | json_encode()時の出力を制御するインターフェース | シリアライズ形式がJSONである点が異なる、別の仕組み |
よくある落とし穴(注意点)
__serialize()と__unserialize()はセットで実装する どちらか一方だけを実装すると、正しくシリアライズ・復元のサイクルが機能しません。両方を必ずペアで実装しましょう(すべての例で両方を実装しています)。__sleep()/__wakeup()と__serialize()/__unserialize()が共存する場合の優先順位 同じクラスに両方の仕組みが定義されている場合、PHP 7.4以降では__serialize()/__unserialize()が優先されます。新規開発では、より自由度の高い__serialize()/__unserialize()を使うことが推奨されます。- 戻り値のキー名は
__unserialize()側と一致させる必要がある__serialize()が返す配列のキー名と、__unserialize()が読み取るキー名が一致していないと、正しくデータが復元されません。キー名の変更時は両方のメソッドを同時に更新しましょう。 - シリアライズされたデータに機密情報が含まれないよう注意する デフォルトの挙動(マジックメソッド未実装)では全プロパティがシリアライズされるため、意図せずパスワードやAPIキーなどの機密情報がシリアライズデータに含まれてしまうことがあります。
__serialize()を使って、必要なデータだけを明示的に選別することが推奨されます(例3を参照)。 - セキュリティ上のリスクは
serialize()/unserialize()自体にもあることを忘れない__serialize()/__unserialize()はシリアライズ内容を制御する仕組みですが、unserialize()自体に信頼できない入力を渡すことの危険性(PHPオブジェクトインジェクション)は別途存在します。この点はunserialize()に関する別の記事で詳しく解説していますので、併せて確認することをおすすめします。
まとめ
| 観点 | まとめ |
|---|---|
| 何をするメソッドか | serialize()によるオブジェクトの文字列化時に、保存するデータを自由形式の配列で定義する |
| 主な用途 | リソースの除外、機密情報のマスキング、キャッシュデータの軽量化、バージョン管理 |
| 対になるメソッド | __unserialize()(必ずセットで実装する) |
| 旧来の仕組みとの関係 | __sleep()/__wakeup()より新しく、より自由度の高いPHP 7.4以降の標準 |
| 注意点 | キー名の一貫性、機密情報の除外、unserialize()自体のセキュリティリスクとは別問題であること |
__serialize() は、オブジェクトの永続化やキャッシュ保存において、「何を保存し、何を保存しないか」を明示的にコントロールできる、PHP 7.4以降の洗練された仕組みです。__unserialize() と対で実装し、シリアライズ不可能なリソースの除外や機密情報の保護に積極的に活用していきましょう。
