__serialize()と__unserialize()で直列化の中身を自分で決める

__serialize()と__unserialize()は、オブジェクトをserialize()する際にどの値を直列化し、unserialize()時にどう復元するかを自分で決められるマジックメソッドです。PHP 7.4以降で使えます。

DBコネクションやリソースを持つオブジェクトを何も考えずにserialize()すると、復元時にエラーになったり、パスワードのような値がそのまま文字列として残ってしまったりします。私も昔、セッションに突っ込んだオブジェクトの中にPDOのインスタンスが混ざっていて、ログイン処理が謎に落ちるという事故を踏んだことがあります。今日はその対処法としての__serialize()/__unserialize()の話です。

__serialize()と__unserialize()は何をするメソッドか

シグネチャはこうなっています。

public function __serialize(): array;
public function __unserialize(array $data): void;

serialize($obj)が呼ばれると、PHPはまずオブジェクトに__serialize()があるかを確認します。あれば、そのメソッドが返した連想配列を直列化対象として使います。戻り値が配列でない場合はTypeErrorになります。

逆にunserialize()するときは__unserialize()が呼ばれ、__serialize()が返したのと同じ形の配列が引数として渡ってきます。プロパティへの復元は自分で書く必要があります。

なぜ__sleep()/__wakeup()ではダメなのか

従来は__sleep()(直列化するプロパティ名の配列を返す)と__wakeup()(復元後の後処理をする)の組み合わせが使われてきました。これは今も動きますが、PHP 8.5でこの仕組みは非推奨(soft-deprecated)になっていて、公式マニュアルも新規・既存コードとも__serialize()/__unserialize()への移行を勧めています。

__sleep()には、親クラスのprivateプロパティ名を返せない(E_NOTICEになる)という制約もありました。__serialize()はプロパティ名の配列ではなく値そのものの連想配列を組み立てて返すので、この制約を受けません。両方定義した場合は__serialize()が優先され、__sleep()は無視されます。

DB接続を持つクラスをどう書くか

PDO接続を保持するクラスを例にします。接続オブジェクトそのものは直列化できないので、接続に必要な情報だけを渡し、復元時に接続を張り直します。

final class Connection
{
    private PDO $link;

    public function __construct(
        private readonly string $dsn,
        private readonly string $username,
        private readonly string $password,
    ) {
        $this->connect();
    }

    private function connect(): void
    {
        $this->link = new PDO($this->dsn, $this->username, $this->password);
    }

    public function __serialize(): array
    {
        return [
            'dsn' => $this->dsn,
            'user' => $this->username,
            'pass' => $this->password,
        ];
    }

    public function __unserialize(array $data): void
    {
        $this->dsn = $data['dsn'];
        $this->username = $data['user'];
        $this->password = $data['pass'];
        $this->connect();
    }
}

__serialize()が返す配列のキー名は、プロパティ名と一致している必要はありません。__unserialize()側で対応する形にさえなっていれば、内部で好きなキー名を使えます。$link(PDOインスタンス)は配列に含めていないので、直列化データには一切残りません。パスワードのような値をあえて出したくない場合は、ここで除外するかハッシュ化した形にすることもできます。

Serializableインターフェースとの関係はどうなる?

PHPには古くからSerializableインターフェース(serialize()/unserialize()という名前のメソッドを実装する)もありますが、オブジェクトが__serialize()を持っている場合、Serializableインターフェースのserialize()メソッドは無視され、__serialize()が使われます。これから書くコードでSerializableを新規に実装する理由は基本的にないと考えていいと思います。

まとめ

__serialize()/__unserialize()を使うと、直列化されるデータの中身を配列として明示的にコントロールできます。リソースや接続オブジェクトを持つクラス、あるいは秘密情報を安易に直列化データへ残したくないクラスでは、まずこの2つのメソッドを検討するのが今の書き方だと思います。__sleep()/__wakeup()しか使っていない既存クラスがあれば、書き換えの優先度は高めにしておいて損はなさそうです。

よくある質問

Q. __serialize()と__sleep()を両方定義したらどうなりますか?
A. __serialize()が使われ、__sleep()は無視されます。両方書く意味はほぼないので、新規に書くなら__serialize()側だけでよいです。

Q. __serialize()はいつから使えますか?
A. PHP 7.4.0以降です。それより前のバージョンをサポートする必要があるなら__sleep()/__wakeup()を使うことになります。

Q. __unserialize()の引数に渡ってくる配列の形は自由に決めていいのですか?
A. はい。__serialize()が返した配列がそのまま渡ってくるだけなので、キー名や構造は自分で設計して構いません。プロパティ名と一致させる必要もありません。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です