はじめに
前回の記事では、serialize() によるシリアライズ内容を自由に制御する __serialize() を解説しました。今回取り上げる __unserialize() は、その対となる、unserialize() によってオブジェクトが復元される際の処理を制御するマジックメソッドです。
__serialize() が「保存する内容」を定義する役割だとすれば、__unserialize() は「保存されていたデータから、どうやってオブジェクトの状態を再構築するか」を定義する役割を担います。この2つは常にペアで実装されるべきものであり、どちらか一方だけでは正しく機能しません。本記事では、__unserialize() の基本的な使い方から、データの検証、後方互換性の確保、セキュリティ上の考慮点まで、実践的なコード例とともに詳しく解説します。
メソッド概要
| 項目 | 内容 |
|---|---|
| メソッド名 | __unserialize() |
| 種別 | マジックメソッド |
| シグネチャ | public function __unserialize(array $data): void |
| 引数 | $data — __serialize() が返した配列(復元の元データ) |
| 呼び出しタイミング | unserialize() 関数がそのクラスのオブジェクトを復元する際 |
| 戻り値 | なし(void) |
| 対応バージョン | PHP 7.4.0以降 |
| 対になるメソッド | __serialize()(シリアライズ時に呼ばれる) |
| 重要な前提 | コンストラクタを経由せずにオブジェクトが生成される |
復元の流れ(イメージ図)
シリアライズされた文字列
'O:4:"User":2:{s:4:"name";s:2:"太郎";s:3:"age";i:30;}'
│
▼
unserialize($string)
│
▼
┌───────────────────────────┐
│ 1. __construct()を呼ばずに、 │
│ 空のUserオブジェクトを生成 │
│ 2. __unserialize($data)を呼び出し、 │
│ $dataには保存されていた配列が渡される │
│ ★この記事の対象 │
│ 3. __unserialize()内でプロパティを │
│ 自分で設定し、オブジェクトを完成させる │
└───────────────────────────┘
▼
完全に復元されたUserオブジェクトが返る
ポイントは、unserialize() によるオブジェクト復元では通常のコンストラクタ(__construct())が呼び出されないという点です。代わりに __unserialize() が、コンストラクタの代役としてプロパティの設定を担うことになります。この前提を正しく理解しておくことが重要です。
実践サンプル7選
例1:基本的な使い方
<?php
class BasicUnserializeDemo
{
public function __construct(
private string $name,
private int $age
) {
echo 'コンストラクタが呼ばれました' . PHP_EOL;
}
public function __serialize(): array
{
return ['name' => $this->name, 'age' => $this->age];
}
/**
* 復元時、__construct()の代わりにこのメソッドで
* プロパティを設定する
*/
public function __unserialize(array $data): void
{
$this->name = $data['name'];
$this->age = $data['age'];
}
public function describe(): string
{
return "{$this->name}({$this->age}歳)";
}
}
$original = new BasicUnserializeDemo('太郎', 30); // コンストラクタが呼ばれる
$serialized = serialize($original);
$restored = unserialize($serialized); // コンストラクタは呼ばれず、__unserialize()が呼ばれる
echo $restored->describe() . PHP_EOL;
例2:復元時にデータの妥当性を検証するクラス
<?php
class ValidatedRestoration
{
private string $email;
public function __construct(string $email)
{
$this->setEmail($email);
}
private function setEmail(string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('不正なメールアドレスです');
}
$this->email = $email;
}
public function __serialize(): array
{
return ['email' => $this->email];
}
/**
* 復元時も、改めて検証ロジックを通すことで、
* 改ざんされたデータからの不正な復元を防ぐ
*/
public function __unserialize(array $data): void
{
$this->setEmail($data['email']);
}
}
$user = new ValidatedRestoration('taro@example.com');
$serialized = serialize($user);
$restored = unserialize($serialized);
echo '復元成功' . PHP_EOL;
例3:外部から受け取った疑わしいシリアライズデータへの防御
<?php
class SafeDataRestorer
{
private array $allowedKeys = ['id', 'name', 'status'];
private array $data = [];
public function __serialize(): array
{
return $this->data;
}
/**
* 復元時、許可されたキーのみを受け入れることで、
* 想定外のデータが紛れ込むことを防ぐ
*/
public function __unserialize(array $data): void
{
foreach ($data as $key => $value) {
if (in_array($key, $this->allowedKeys, true)) {
$this->data[$key] = $value;
}
}
}
public function toArray(): array
{
return $this->data;
}
}
$obj = new SafeDataRestorer();
// allowed_classesをfalseにして、自身のクラスのみ安全に復元する例
$serialized = 'O:16:"SafeDataRestorer":2:{s:2:"id";i:1;s:7:"unknown";s:3:"bad";}';
$restored = unserialize($serialized, ['allowed_classes' => [SafeDataRestorer::class]]);
print_r($restored->toArray()); // "unknown"キーは除外される
例4:旧バージョンのデータ構造からのマイグレーション処理
<?php
class MigratableEntity
{
private string $fullName;
public function __construct(string $fullName)
{
$this->fullName = $fullName;
}
public function __serialize(): array
{
return ['version' => 2, 'fullName' => $this->fullName];
}
/**
* 古いバージョンのデータ構造(firstName/lastNameに分かれていた形式)を
* 新しいデータ構造(fullName)に変換してから復元する
*/
public function __unserialize(array $data): void
{
$version = $data['version'] ?? 1;
if ($version === 1) {
// v1形式: firstName/lastNameに分かれていた
$this->fullName = trim(($data['firstName'] ?? '') . ' ' . ($data['lastName'] ?? ''));
} else {
$this->fullName = $data['fullName'];
}
}
}
// 旧バージョン形式のシリアライズデータを模したサンプル
$oldFormat = 'O:17:"MigratableEntity":2:{s:9:"firstName";s:6:"太郎";s:8:"lastName";s:6:"山田";}';
$restored = unserialize($oldFormat, ['allowed_classes' => [MigratableEntity::class]]);
例5:復元後にリソースを再接続するクラス
<?php
class ReconnectableConnection
{
private array $connectionParams;
private ?PDO $pdo = null;
public function __construct(array $connectionParams)
{
$this->connectionParams = $connectionParams;
}
public function __serialize(): array
{
return ['connectionParams' => $this->connectionParams];
// $this->pdo(リソース)はシリアライズ対象から除外
}
/**
* 復元時は接続パラメータだけを受け取り、
* 実際のPDO接続は遅延して必要時に再確立する
*/
public function __unserialize(array $data): void
{
$this->connectionParams = $data['connectionParams'];
$this->pdo = null; // 接続は復元せず、使用時に再接続する
}
public function getConnection(): PDO
{
$this->pdo ??= new PDO(...$this->connectionParams);
return $this->pdo;
}
}
例6:継承関係における親子クラスの復元連携
<?php
class Shape
{
public function __construct(protected string $color)
{
}
protected function restoreShapeData(array $data): void
{
$this->color = $data['color'];
}
}
class Circle extends Shape
{
public function __construct(string $color, private float $radius)
{
parent::__construct($color);
}
public function __serialize(): array
{
return ['color' => $this->color, 'radius' => $this->radius];
}
/**
* 親クラスの復元ロジックを再利用しつつ、
* 子クラス固有のプロパティも復元する
*/
public function __unserialize(array $data): void
{
$this->restoreShapeData($data);
$this->radius = $data['radius'];
}
}
$circle = new Circle('赤', 5.0);
$serialized = serialize($circle);
$restored = unserialize($serialized);
echo get_class($restored) . PHP_EOL;
例7:__wakeup()との違いを比較するデモ
<?php
class OldStyleWakeup
{
private $resource;
public function __construct(private string $name)
{
}
/**
* __wakeup()はデフォルトのプロパティ復元処理の「後」に
* 追加の初期化処理だけを行う、より限定的な仕組み
*/
public function __wakeup(): void
{
$this->resource = null; // リソースの再初期化のみ
}
}
class ModernStyleUnserialize
{
private string $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function __serialize(): array
{
return ['custom_name_key' => $this->name];
}
/**
* __unserialize()はプロパティの復元自体を
* 完全にコントロールできる、より柔軟な仕組み
*/
public function __unserialize(array $data): void
{
$this->name = strtoupper($data['custom_name_key']); // 復元時に変換も可能
}
}
$modern = new ModernStyleUnserialize('太郎');
$restored = unserialize(serialize($modern));
echo '復元完了(__unserialize()はデータ変換も自由に行える)' . PHP_EOL;
関連機能との比較
| 機能 | 役割 | __unserialize()との違い |
|---|---|---|
__unserialize() | シリアライズされたデータからオブジェクトを復元 | 本記事の対象。PHP 7.4以降の推奨される仕組み |
__serialize() | シリアライズするデータを自由形式の配列で定義 | __unserialize()と対になる、保存処理を担うメソッド(前回記事を参照) |
__wakeup() | デフォルト復元後に追加の初期化処理を行う、古い仕組み | プロパティの復元自体は制御できない、より限定的な役割 |
__construct() | 通常のオブジェクト生成時に呼ばれる | unserialize()による復元時には呼ばれない点が重要な違い |
unserialize()のallowed_classesオプション | 復元可能なクラスを制限するセキュリティ機構 | __unserialize()自体とは別の、呼び出し側で設定する安全対策 |
よくある落とし穴(注意点)
- コンストラクタが呼ばれないことを前提に設計する これが最も重要な注意点です。
__unserialize()が呼ばれる際、通常の__construct()は実行されません。コンストラクタ内で行っていた必須の初期化処理(デフォルト値の設定など)は、__unserialize()内でも改めて行う必要があります。 __serialize()とキー名を一致させる__unserialize()が読み取るキー名は、対になる__serialize()が返す配列のキー名と一致している必要があります。片方だけを変更すると、復元に失敗したり、undefined array keyの警告が発生したりします。- 外部から受け取ったデータの信頼性を疑う
unserialize()全般に言えることですが、信頼できない入力元からのシリアライズデータを復元する場合、__unserialize()内で想定外のキーや型が渡される可能性があります。必要に応じてバリデーションを行いましょう(例2・例3を参照)。 - シリアライズされていなかったリソースの再構築を忘れる
__serialize()側でPDO接続などのリソースを除外していた場合、__unserialize()側でそれをどう扱うか(即座に再接続するか、遅延初期化にするか)を明確に設計しておく必要があります(例5を参照)。 unserialize()呼び出し元でのallowed_classes設定も重要__unserialize()自体はあくまでクラス内部での復元ロジックですが、unserialize()を呼び出す側でallowed_classesオプションを適切に設定しないと、PHPオブジェクトインジェクションのリスクが依然として残ります。この点は、unserialize()に関する別記事で解説しているセキュリティ対策と合わせて理解しておくことが重要です。
まとめ
| 観点 | まとめ |
|---|---|
| 何をするメソッドか | unserialize()によるオブジェクト復元時に、保存されていたデータからプロパティを再構築する |
| 主な用途 | 復元データの検証、旧バージョンデータからのマイグレーション、リソースの再構築 |
| 対になるメソッド | __serialize()(必ずセットで実装する) |
| 最重要の前提 | 復元時には__construct()が呼ばれない |
| 注意点 | キー名の一貫性、信頼できない入力への備え、リソースの再構築方法の明確化 |
__unserialize() は、__serialize() とペアで、オブジェクトの永続化とその復元を安全かつ柔軟に制御するための仕組みです。コンストラクタが呼ばれないという重要な前提を理解した上で、データの検証やマイグレーション処理を適切に組み込むことで、堅牢なシリアライズ・デシリアライズのサイクルを実装できます。
