あっぽログ
← 記事一覧に戻る

PHPのSplObjectStorageを使いこなす:オブジェクトをキーにしたコレクション管理を実現する

SplObjectStorageとは

PHPの標準ライブラリ(SPL)には、さまざまなデータ構造が用意されています。その中の一つ、SplObjectStorageオブジェクトをキーとして使えるコレクションです。

通常のPHP配列ではオブジェクトをキーに使えません。文字列や整数しかキーとして扱えないため、「このオブジェクトに関連するデータを紐づけたい」というケースで工夫が必要になります。SplObjectStorageはその課題をエレガントに解決します。

主な特徴は以下のとおりです。

  • オブジェクトをキーとして扱える
  • 各オブジェクトに付加データ(info)を関連付けられる
  • オブジェクトの重複を自動排除する(同一オブジェクトは1つしか保持しない)
  • CountableIteratorArrayAccessインターフェースを実装しており、使いやすい

基本的な使い方

オブジェクトの追加・削除・確認

<?php

$storage = new SplObjectStorage();

$obj1 = new stdClass();
$obj2 = new stdClass();
$obj3 = new stdClass();

// オブジェクトを追加
$storage->attach($obj1);
$storage->attach($obj2);

// 含まれているか確認
var_dump($storage->contains($obj1)); // bool(true)
var_dump($storage->contains($obj3)); // bool(false)

// 要素数
echo $storage->count(); // 2

// オブジェクトを削除
$storage->detach($obj1);
echo $storage->count(); // 1

attach()で追加、detach()で削除、contains()で存在確認と、シンプルなAPIで操作できます。

付加データ(info)を使う

SplObjectStorageの強みは、オブジェクトに追加情報を紐づけられる点です。

<?php

$storage = new SplObjectStorage();

$userA = new stdClass();
$userB = new stdClass();

// 第2引数にinfoを指定
$storage->attach($userA, ['name' => '田中', 'role' => 'admin']);
$storage->attach($userB, ['name' => '佐藤', 'role' => 'editor']);

// infoを取得するにはオブジェクトを現在位置に設定してから
$storage->rewind();
while ($storage->valid()) {
    $obj  = $storage->current();
    $info = $storage->getInfo();
    echo $info['name'] . ' は ' . $info['role'] . "\n";
    $storage->next();
}
// 出力:
// 田中 は admin
// 佐藤 は editor

getInfo()で現在のオブジェクトに紐づいたデータを取得できます。イテレーション中に両方まとめて取り出せるのが便利です。

ArrayAccessとして使う

SplObjectStorageArrayAccessを実装しているため、配列のような記法でも操作できます。

<?php

$storage = new SplObjectStorage();

$task = new stdClass();

// 配列記法でinfoをセット
$storage[$task] = ['title' => 'レポート作成', 'done' => false];

// 配列記法でinfoを取得
$info = $storage[$task];
echo $info['title']; // レポート作成

// 存在確認
var_dump(isset($storage[$task])); // bool(true)

// 削除
unset($storage[$task]);
var_dump(isset($storage[$task])); // bool(false)

直感的な記法で読みやすいコードになります。

実践的な活用例

イベントリスナー管理

SplObjectStorageはリスナーオブジェクトの管理に活用できます。同一オブジェクトを重複登録しないという性質が特に役立ちます。

<?php

interface Listener
{
    public function handle(string $event): void;
}

class LogListener implements Listener
{
    public function handle(string $event): void
    {
        echo "[LOG] イベント受信: {$event}\n";
    }
}

class EventEmitter
{
    private SplObjectStorage $listeners;

    public function __construct()
    {
        $this->listeners = new SplObjectStorage();
    }

    public function addListener(Listener $listener): void
    {
        // 重複していても1つしか登録されない
        $this->listeners->attach($listener);
    }

    public function emit(string $event): void
    {
        foreach ($this->listeners as $listener) {
            $listener->handle($event);
        }
    }

    public function removeListener(Listener $listener): void
    {
        $this->listeners->detach($listener);
    }
}

$emitter  = new EventEmitter();
$listener = new LogListener();

$emitter->addListener($listener);
$emitter->addListener($listener); // 重複しても問題なし

$emitter->emit('user.created');
// [LOG] イベント受信: user.created (1回だけ出力)

配列で管理する場合はin_array()による重複チェックが必要ですが、SplObjectStorageならそのままattach()するだけで重複を排除できます。

オブジェクトへのメタデータ付与

グラフ探索やゲームの状態管理など、「オブジェクトに訪問済みフラグや重みを付けたい」という場面でも活躍します。

<?php

class Node
{
    public function __construct(public readonly string $name) {}
}

$visited = new SplObjectStorage();

$nodeA = new Node('A');
$nodeB = new Node('B');
$nodeC = new Node('C');

// 訪問済みとコストを記録
$visited->attach($nodeA, ['cost' => 0]);
$visited->attach($nodeB, ['cost' => 5]);

// Aは訪問済みか?
if ($visited->contains($nodeA)) {
    $visited->rewind();
    // ArrayAccess記法でinfoを確認
    echo 'コスト: ' . $visited[$nodeA]['cost'] . "\n"; // コスト: 0
}

// Cは未訪問
var_dump($visited->contains($nodeC)); // bool(false)

まとめ

SplObjectStorageはシンプルながらも実用的なデータ構造です。

操作メソッド
追加attach($obj, $info)
削除detach($obj)
存在確認contains($obj)
info取得getInfo()
要素数count()

「オブジェクトの集合を管理したい」「オブジェクトにデータを紐づけたい」「重複を自動的に排除したい」という要件があるとき、まずSplObjectStorageを検討してみてください。PHPの標準ライブラリには、知るだけで設計がスッキリするクラスが多く揃っています。ぜひ活用してみてください。

← 記事一覧に戻る