[PHP]spl_object_id完全解説|オブジェクトの一意な整数IDを高速取得する方法

PHP

はじめに

前回解説した spl_object_hash() は文字列のハッシュ値を返しますが、PHP 7.2で追加された spl_object_id() は、オブジェクトの同一性を表す整数(int)のIDを直接取得できる関数です。

文字列ハッシュ生成のオーバーヘッドがなく、PHPの内部オブジェクトハンドルをそのまま返すため、spl_object_hash() より高速でシンプルです。今では公式マニュアルでも spl_object_hash() の代替として推奨されています。

本記事では spl_object_id() の仕様・パフォーマンス特性・実践パターンを詳しく解説します。


関数の基本情報

項目内容
関数名spl_object_id()
利用可能バージョンPHP 7.2以降
所属SPL(Standard PHP Library)
戻り値int(オブジェクトの一意な識別子)
拡張機能SPL(デフォルトで有効)

シグネチャ

spl_object_id(object $object): int

パラメータ

パラメータ説明
$objectobjectIDを取得したいオブジェクトインスタンス

戻り値の特性

特性内容
int(PHPの内部オブジェクトハンドル番号)
同一インスタンス常に同じIDを返す
異なるインスタンス(生存中)必ず異なるIDを返す
ID再利用GCで回収後、新しいオブジェクトが同じIDを持つ場合がある
割り当て順序通常はオブジェクト生成順に小さい番号から割り当てられる

spl_object_hash() との違い

$obj = new stdClass();

var_dump(spl_object_id($obj));
// int(1) のような小さい整数

var_dump(spl_object_hash($obj));
// string(32) "0000000012a4e8d70000000012a4e8d7" のような32文字の16進文字列
観点spl_object_id()spl_object_hash()
戻り値の型intstring(32文字)
利用可能バージョンPHP 7.2以降PHP 5.2以降
パフォーマンス✅ 高速(直接ハンドル値を返す)△ 文字列生成コストあり
配列キーとしての利用✅ そのまま使える(int キー)✅ そのまま使える(string キー)
可読性✅ シンプルな数値△ 長い16進文字列
後方互換性が必要な場合PHP 7.2未満では使えないPHP 5.2から使える

方針: PHP 7.2以降をターゲットにするなら spl_object_id() が推奨されます。spl_object_hash() は内部的に spl_object_id() をラップして文字列化したものに近い実装です。


実践サンプル集(PHP 8.x対応)

サンプル1:基本動作の確認

<?php
declare(strict_types=1);

$objA = new stdClass();
$objB = new stdClass();
$objC = $objA; // 参照のコピー

echo "objA id: " . spl_object_id($objA) . "\n";
echo "objB id: " . spl_object_id($objB) . "\n";
echo "objC id: " . spl_object_id($objC) . "\n";

echo "\n";
echo "id(A) === id(B): " . (spl_object_id($objA) === spl_object_id($objB) ? 'true' : 'false') . "\n"; // false
echo "id(A) === id(C): " . (spl_object_id($objA) === spl_object_id($objC) ? 'true' : 'false') . "\n"; // true

// 複数オブジェクトを生成してIDの連番性を確認
echo "\n--- 生成順とID ---\n";
for ($i = 0; $i < 5; $i++) {
    $obj = new stdClass();
    echo "生成{$i}: id=" . spl_object_id($obj) . "\n";
    // unsetしないとIDが回収されず増え続ける
}

実行結果:

objA id: 1
objB id: 2
objC id: 1

id(A) === id(B): false
id(A) === id(C): true

--- 生成順とID ---
生成0: id=3
生成1: id=4
生成2: id=5
生成3: id=6
生成4: id=7

解説: IDは多くの場合、オブジェクト生成順に小さい整数から割り当てられます(PHPの内部実装に依存するため将来変わる可能性はあります)。spl_object_hash() の16進文字列に比べて、ログ出力やデバッグ表示が圧倒的に見やすくなります。


サンプル2:パフォーマンス比較

spl_object_id()spl_object_hash() の処理速度を比較します。

<?php
declare(strict_types=1);

function benchmark(string $label, callable $fn, int $iterations): float
{
    $start = microtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $fn();
    }
    $elapsed = (microtime(true) - $start) * 1000;
    printf("%-20s: %8.2f ms (%d回)\n", $label, $elapsed, $iterations);
    return $elapsed;
}

$objects = [];
for ($i = 0; $i < 1000; $i++) {
    $objects[] = new stdClass();
}

$iterations = 100_000;

$timeId = benchmark('spl_object_id', function () use ($objects) {
    static $i = 0;
    spl_object_id($objects[$i++ % count($objects)]);
}, $iterations);

$timeHash = benchmark('spl_object_hash', function () use ($objects) {
    static $i = 0;
    spl_object_hash($objects[$i++ % count($objects)]);
}, $iterations);

$diff = round((($timeHash - $timeId) / $timeId) * 100, 1);
echo "\nspl_object_hash は spl_object_id より約 {$diff}% 遅い傾向\n";
echo "(実際の差は環境・PHPバージョンにより変動します)\n";

実行結果例(環境により変動):

spl_object_id       :    12.34 ms (100000回)
spl_object_hash     :    28.91 ms (100000回)

spl_object_hash は spl_object_id より約 134.3% 遅い傾向
(実際の差は環境・PHPバージョンにより変動します)

解説: spl_object_id() は内部のオブジェクトハンドル番号を直接返すだけなので、文字列フォーマット処理が発生する spl_object_hash() より高速です。大量のオブジェクトを処理するホットパスでは差が顕著になります。


サンプル3:オブジェクトを配列キーとして使う(int版)

spl_object_hash() の文字列キー版と比較して、int配列キーで管理するパターンです。

<?php
declare(strict_types=1);

class ObjectRegistry
{
    /** @var array<int, object> ID → オブジェクト */
    private array $objects = [];

    /** @var array<int, mixed> ID → メタデータ */
    private array $metadata = [];

    /**
     * オブジェクトを登録
     */
    public function register(object $obj, mixed $meta = null): int
    {
        $id = spl_object_id($obj);

        $this->objects[$id]  = $obj; // 強参照を保持(GC防止)
        $this->metadata[$id] = $meta;

        return $id;
    }

    public function unregister(object $obj): bool
    {
        $id = spl_object_id($obj);
        if (!isset($this->objects[$id])) {
            return false;
        }
        unset($this->objects[$id], $this->metadata[$id]);
        return true;
    }

    public function getMetadata(object $obj): mixed
    {
        return $this->metadata[spl_object_id($obj)] ?? null;
    }

    public function isRegistered(object $obj): bool
    {
        return isset($this->objects[spl_object_id($obj)]);
    }

    public function count(): int
    {
        return count($this->objects);
    }

    /**
     * IDのリストを取得(ソートしやすい)
     */
    public function getIds(): array
    {
        $ids = array_keys($this->objects);
        sort($ids); // int同士なので自然順ソートが容易
        return $ids;
    }
}

class Connection
{
    public function __construct(public readonly string $host) {}
}

$registry = new ObjectRegistry();

$conn1 = new Connection('db1.example.com');
$conn2 = new Connection('db2.example.com');
$conn3 = new Connection('cache.example.com');

$id1 = $registry->register($conn1, ['type' => 'mysql', 'pool_size' => 10]);
$id2 = $registry->register($conn2, ['type' => 'mysql', 'pool_size' => 5]);
$id3 = $registry->register($conn3, ['type' => 'redis', 'pool_size' => 20]);

echo "登録数: " . $registry->count() . "\n";
echo "ID一覧: " . implode(', ', $registry->getIds()) . "\n\n";

$meta = $registry->getMetadata($conn1);
echo "conn1 type: {$meta['type']} / pool_size: {$meta['pool_size']}\n";

$registry->unregister($conn2);
echo "\nconn2解除後の登録数: " . $registry->count() . "\n";
echo "conn2は登録済み?: " . ($registry->isRegistered($conn2) ? 'yes' : 'no') . "\n";

実行結果:

登録数: 3
ID一覧: 8, 9, 10

conn1 type: mysql / pool_size: 10

conn2解除後の登録数: 2
conn2は登録済み?: no

解説: int型のIDは sort() での並び替えや数値比較が直感的に行えます。文字列ハッシュよりもメモリ効率もよく、PHP配列の内部実装(整数キーは文字列キーよりわずかに高速)の観点でも有利です。


サンプル4:デバッグ出力でのオブジェクト識別

ログやデバッグ出力で、どのオブジェクトインスタンスが処理されているかを簡潔に識別するパターンです。

<?php
declare(strict_types=1);

trait DebuggableObject
{
    public function debugId(): string
    {
        return sprintf('%s#%d', static::class, spl_object_id($this));
    }
}

class Order
{
    use DebuggableObject;

    public function __construct(
        public readonly int $orderId,
        public float $total
    ) {}
}

class OrderProcessor
{
    private array $log = [];

    public function process(Order $order): void
    {
        $this->log[] = sprintf(
            '[%s] 処理開始: orderId=%d, total=%.2f',
            $order->debugId(),
            $order->orderId,
            $order->total
        );

        // 何らかの処理
        $order->total *= 1.1; // 税金計算など

        $this->log[] = sprintf(
            '[%s] 処理完了: 新total=%.2f',
            $order->debugId(),
            $order->total
        );
    }

    public function dumpLog(): void
    {
        foreach ($this->log as $entry) {
            echo $entry . "\n";
        }
    }
}

// --- 使用例 ---
$processor = new OrderProcessor();

$order1 = new Order(1001, 100.0);
$order2 = new Order(1002, 250.0);

$processor->process($order1);
$processor->process($order2);
$processor->process($order1); // 同じオブジェクトを再処理

$processor->dumpLog();

実行結果:

[Order#11] 処理開始: orderId=1001, total=100.00
[Order#11] 処理完了: 新total=110.00
[Order#12] 処理開始: orderId=1002, total=250.00
[Order#12] 処理完了: 新total=275.00
[Order#11] 処理開始: orderId=1001, total=110.00
[Order#11] 処理完了: 新total=121.00

解説: クラス名#ID という形式のデバッグ識別子は、同じビジネスID(orderId)を持つオブジェクトでもインスタンスとして同一かどうかを一目で判別できます。ログから「同じオブジェクトが複数回処理されている」という不具合パターンを見つけやすくなります。


サンプル5:オブジェクトプールでの再利用管理

spl_object_id() を使ってオブジェクトプールの利用状況を管理するパターンです。

<?php
declare(strict_types=1);

class PooledConnection
{
    public bool $inUse = false;

    public function __construct(public readonly string $id) {}

    public function reset(): void
    {
        $this->inUse = false;
    }
}

class ConnectionPool
{
    /** @var array<int, PooledConnection> ID → コネクション */
    private array $pool = [];

    /** @var array<int, float> ID → 借用開始時刻 */
    private array $checkoutTime = [];

    public function __construct(int $size)
    {
        for ($i = 0; $i < $size; $i++) {
            $conn = new PooledConnection("conn-{$i}");
            $this->pool[spl_object_id($conn)] = $conn;
        }
        echo "プール初期化: {$size}個のコネクション\n";
    }

    /**
     * 利用可能なコネクションを取得
     */
    public function acquire(): ?PooledConnection
    {
        foreach ($this->pool as $id => $conn) {
            if (!$conn->inUse) {
                $conn->inUse = true;
                $this->checkoutTime[$id] = microtime(true);
                echo "取得: {$conn->id} (objId={$id})\n";
                return $conn;
            }
        }
        echo "⚠️  利用可能なコネクションがありません\n";
        return null;
    }

    /**
     * コネクションをプールに返却
     */
    public function release(PooledConnection $conn): void
    {
        $id = spl_object_id($conn);

        if (!isset($this->pool[$id])) {
            echo "⚠️  このコネクションはこのプールに属していません\n";
            return;
        }

        $duration = isset($this->checkoutTime[$id])
            ? round((microtime(true) - $this->checkoutTime[$id]) * 1000, 2)
            : 0;

        $conn->reset();
        unset($this->checkoutTime[$id]);
        echo "返却: {$conn->id} (使用時間: {$duration}ms)\n";
    }

    public function stats(): array
    {
        $inUse = 0;
        foreach ($this->pool as $conn) {
            if ($conn->inUse) {
                $inUse++;
            }
        }
        return ['total' => count($this->pool), 'in_use' => $inUse, 'available' => count($this->pool) - $inUse];
    }
}

// --- 使用例 ---
$pool = new ConnectionPool(3);

$c1 = $pool->acquire();
$c2 = $pool->acquire();

print_r($pool->stats());

$pool->release($c1);
print_r($pool->stats());

$c3 = $pool->acquire();
$c4 = $pool->acquire(); // 利用可能数を超える要求

実行結果:

プール初期化: 3個のコネクション
取得: conn-0 (objId=15)
取得: conn-1 (objId=16)
Array
(
    [total] => 3
    [in_use] => 2
    [available] => 1
)
返却: conn-0 (使用時間: 0.12ms)
Array
(
    [total] => 3
    [in_use] => 1
    [available] => 2
)
取得: conn-0 (objId=15)
取得: conn-2 (objId=17)

解説: プール内のコネクションをIDで管理することで、同じオブジェクトが正しいプールに返却されているかを検証できます(isset($this->pool[$id]) のチェック)。


サンプル6:等価性 vs 同一性の判定ユーティリティ

spl_object_id() を活用して「同一インスタンスか」と「同等の内容か」を区別するユーティリティです。

<?php
declare(strict_types=1);

final class ObjectComparator
{
    /**
     * 同一インスタンスかどうか(参照の同一性)
     */
    public static function isSameInstance(object $a, object $b): bool
    {
        return spl_object_id($a) === spl_object_id($b);
    }

    /**
     * 同じクラスで内容が等しいか(値の等価性)
     */
    public static function isEquivalent(object $a, object $b): bool
    {
        return $a == $b; // PHPの == はプロパティを比較
    }

    /**
     * オブジェクトリストから重複インスタンスを除去
     *
     * @param  list<object> $objects
     * @return list<object>
     */
    public static function uniqueInstances(array $objects): array
    {
        $seen   = [];
        $result = [];

        foreach ($objects as $obj) {
            $id = spl_object_id($obj);
            if (!isset($seen[$id])) {
                $seen[$id] = true;
                $result[]  = $obj;
            }
        }

        return $result;
    }

    /**
     * 2つのオブジェクトリストの差分(インスタンス単位)
     *
     * @param  list<object> $listA
     * @param  list<object> $listB
     * @return list<object> listAにのみ存在するオブジェクト
     */
    public static function diffInstances(array $listA, array $listB): array
    {
        $idsB = array_map('spl_object_id', $listB);
        return array_values(array_filter(
            $listA,
            fn($obj) => !in_array(spl_object_id($obj), $idsB, true)
        ));
    }
}

class Point
{
    public function __construct(public float $x, public float $y) {}
}

// --- 同一性 vs 等価性 ---
$p1 = new Point(1.0, 2.0);
$p2 = new Point(1.0, 2.0); // 内容は同じだが別インスタンス
$p3 = $p1;                  // 同一インスタンス

echo "p1 === p2 (同一性): " . (ObjectComparator::isSameInstance($p1, $p2) ? 'true' : 'false') . "\n"; // false
echo "p1 == p2 (等価性): "  . (ObjectComparator::isEquivalent($p1, $p2)  ? 'true' : 'false') . "\n"; // true
echo "p1 === p3 (同一性): " . (ObjectComparator::isSameInstance($p1, $p3) ? 'true' : 'false') . "\n"; // true

// --- 重複除去 ---
$points = [$p1, $p2, $p3, new Point(5.0, 5.0)];
$unique = ObjectComparator::uniqueInstances($points);
echo "\n元の数: " . count($points) . " / 重複除去後: " . count($unique) . "\n"; // 4 → 3(p1とp3は同一)

// --- 差分 ---
$listA = [$p1, $p2];
$listB = [$p2];
$diff  = ObjectComparator::diffInstances($listA, $listB);
echo "\n差分(listAにのみ存在): " . count($diff) . "件\n"; // 1件(p1)

実行結果:

p1 === p2 (同一性): false
p1 == p2 (等価性): true
p1 === p3 (同一性): true

元の数: 4 / 重複除去後: 3

差分(listAにのみ存在): 1件

解説: spl_object_id() を使うことで「同じインスタンスかどうか」を明示的に判定できます。==(等価性)と ===(同一性、実質的にspl_object_idが同じこと)の違いを意識した設計は、オブジェクトコレクションの重複処理で重要です。


ID再利用に関する注意

spl_object_hash() と同様、spl_object_id() のIDもGCで回収されたオブジェクトのものが再利用される可能性があります。

<?php
declare(strict_types=1);

$obj1 = new stdClass();
$id1 = spl_object_id($obj1);
echo "obj1 id: {$id1}\n";

unset($obj1); // GCで回収される

$obj2 = new stdClass();
$id2 = spl_object_id($obj2);
echo "obj2 id: {$id2}\n";

if ($id1 === $id2) {
    echo "⚠️  IDが再利用されました\n";
}

安全な設計パターン

// ✅ WeakMap を使う(PHP 8.0+、最も安全)
$map = new WeakMap();
$map[$obj] = $someValue;
// $obj がGCされると自動でエントリも削除される

// ✅ オブジェクトへの強参照を保持してID再利用を防ぐ
class SafeRegistry
{
    private array $objects = []; // 強参照を保持
    private array $data    = [];

    public function set(object $obj, mixed $value): void
    {
        $id = spl_object_id($obj);
        $this->objects[$id] = $obj; // GCされないようにする
        $this->data[$id]    = $value;
    }
}

よくある落とし穴

① 永続化には使えない

// ❌ リクエスト間・プロセス間でIDは保持されない
$id = spl_object_id($user);
// セッションやDBに保存しても、次のリクエストでは無意味な値になる

// ✅ 永続化が必要なら、オブジェクト固有のビジネスID(プライマリキー等)を使う
$persistentId = $user->getId(); // DB上のID

② IDの数値に意味を持たせない

// ❌ IDが小さいから「先に生成された」と断定しない
// (GCで再利用されると小さいIDが後から再割り当てされることがある)

// ✅ 生成順序が必要なら、独自のタイムスタンプやシーケンスを使う
class TrackedObject
{
    private static int $counter = 0;
    public readonly int $sequence;

    public function __construct()
    {
        $this->sequence = ++self::$counter; // 独自の連番
    }
}

③ === 演算子で十分な場合はそれを使う

// オブジェクト同士の同一性比較は通常 === で十分
if ($objA === $objB) { /* 同一インスタンス */ }

// spl_object_id() が必要なのは「配列キーにしたい」など
// IDを値として扱う必要がある場面のみ
$map = [];
$map[spl_object_id($objA)] = 'some data';

まとめ

ポイント内容
主な用途オブジェクトの同一性を表すint IDの取得・配列キー・デバッグ識別
戻り値int(PHPの内部オブジェクトハンドル)
spl_object_hash()高速・シンプル・PHP 7.2+限定
最大の注意点GC後にID再利用の可能性 → WeakMapまたは強参照で対策
適さない用途永続化・リクエスト間の識別子(プロセスローカルな値)
推奨バージョンPHP 7.2以降はspl_object_hash()より本関数を優先

spl_object_id()spl_object_hash() の現代的な代替として、パフォーマンスと可読性の両面で優れています。配列キー・デバッグ表示・オブジェクトレジストリなど、オブジェクトの同一性を扱うすべての場面で第一選択肢として検討する価値があります。


PHP 8.x / 執筆時点の最新安定版にて動作確認済み

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