[PHP]spl_autoload_functions完全解説|登録済みオートロードハンドラを取得・検査する方法

PHP

はじめに

PHPのオートロードは spl_autoload_register() で複数のハンドラを**キュー(待ち行列)**として積み上げる仕組みです。しかし、実行中のプログラムで「今どんなハンドラが、どんな順番で登録されているのか」を確認する手段はないのでしょうか。

spl_autoload_functions() は、まさにそのために用意された関数です。現在オートロードキューに登録されているすべてのハンドラを配列で返します。デバッグ・診断・テスト・ハンドラの条件付き操作など、幅広い場面で活躍します。


関数の基本情報

項目内容
関数名spl_autoload_functions()
利用可能バージョンPHP 5.1以降
所属SPL(Standard PHP Library)
戻り値array(登録順のハンドラ配列)またはハンドラ未登録時は false
拡張機能SPL(デフォルトで有効)

シグネチャ

spl_autoload_functions(): array|false

パラメータ

なし。

戻り値の詳細

状況戻り値
1件以上登録されているarray(登録順)
1件も登録されていないfalse

注意: ハンドラが0件のときに [](空配列)ではなく false を返す点が独特です。empty() や厳密比較(=== false)での判定が必要です。


返される配列の要素形式

各ハンドラは登録時の型に応じた形式で返されます。

登録方法返される型
関数名文字列string"spl_autoload"
静的メソッド配列array["MyLoader", "load"]
オブジェクトメソッド配列array[$loaderObj, "load"]
クロージャClosure オブジェクトClosure {#1}

関連関数との位置づけ

spl_autoload_register()    → ハンドラをキューに追加
spl_autoload_unregister()  → ハンドラをキューから削除
spl_autoload_functions()   → キューの現在の中身を取得  ← 本関数
spl_autoload_call()        → キュー内のハンドラを手動実行
spl_autoload_extensions()  → spl_autoload() の拡張子設定

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

サンプル1:基本的な取得とハンドラ形式の確認

<?php
declare(strict_types=1);

// ハンドラが未登録の場合
$result = spl_autoload_functions();
var_dump($result); // bool(false)

// 関数名文字列で登録
spl_autoload_register('spl_autoload');

// クロージャで登録
$closureLoader = function (string $class): void {
    $file = __DIR__ . '/src/' . str_replace('\\', '/', $class) . '.php';
    if (file_exists($file)) {
        require $file;
    }
};
spl_autoload_register($closureLoader);

// クラスの静的メソッドで登録
class StaticLoader
{
    public static function load(string $class): void
    {
        $file = __DIR__ . '/lib/' . str_replace('\\', '/', $class) . '.php';
        if (file_exists($file)) {
            require $file;
        }
    }
}
spl_autoload_register(['StaticLoader', 'load']);

// オブジェクトメソッドで登録
class ObjectLoader
{
    public function __construct(private readonly string $dir) {}

    public function load(string $class): void
    {
        $file = $this->dir . '/' . str_replace('\\', '/', $class) . '.php';
        if (file_exists($file)) {
            require $file;
        }
    }
}
$objLoader = new ObjectLoader(__DIR__ . '/vendor');
spl_autoload_register([$objLoader, 'load']);

// 登録内容を取得・表示
$handlers = spl_autoload_functions();

echo "登録ハンドラ数: " . count($handlers) . "\n\n";

foreach ($handlers as $i => $handler) {
    $label = match (true) {
        is_string($handler)          => "関数名文字列: \"{$handler}\"",
        $handler instanceof \Closure => "クロージャ",
        is_array($handler) && is_string($handler[0])
                                     => "静的メソッド: {$handler[0]}::{$handler[1]}",
        is_array($handler) && is_object($handler[0])
                                     => "オブジェクトメソッド: " . get_class($handler[0]) . "->{$handler[1]}",
        default                      => "(不明)",
    };
    echo "[{$i}] {$label}\n";
}

実行結果:

登録ハンドラ数: 4

[0] 関数名文字列: "spl_autoload"
[1] クロージャ
[2] 静的メソッド: StaticLoader::load
[3] オブジェクトメソッド: ObjectLoader->load

解説: spl_autoload_functions() はハンドラを登録順に返します。オートロード時はこの順番通りに実行され、いずれかがクラスを定義した時点で後続のハンドラは呼ばれません。


サンプル2:ハンドラの種類を判別するユーティリティ

ハンドラの型を判別して人間が読める文字列に変換するユーティリティです。

<?php
declare(strict_types=1);

/**
 * オートロードハンドラの情報を表すValue Object
 */
final class HandlerInfo
{
    public readonly string $type;
    public readonly string $label;
    public readonly string $signature;

    public function __construct(public readonly mixed $handler)
    {
        [$this->type, $this->label, $this->signature] = $this->analyze($handler);
    }

    private function analyze(mixed $handler): array
    {
        if (is_string($handler)) {
            return ['function', 'Function', $handler . '()'];
        }

        if ($handler instanceof \Closure) {
            $ref  = new \ReflectionFunction($handler);
            $file = basename($ref->getFileName() ?? 'unknown');
            $line = $ref->getStartLine();
            return ['closure', 'Closure', "Closure@{$file}:{$line}"];
        }

        if (is_array($handler) && count($handler) === 2) {
            [$target, $method] = $handler;

            if (is_string($target)) {
                return ['static', 'Static Method', "{$target}::{$method}()"];
            }

            if (is_object($target)) {
                $class = get_class($target);
                return ['instance', 'Instance Method', "{$class}->{$method}()"];
            }
        }

        return ['unknown', 'Unknown', var_export($handler, true)];
    }

    public function __toString(): string
    {
        return "[{$this->type}] {$this->signature}";
    }
}

/**
 * 登録済みハンドラを分析して表示する
 */
function dumpAutoloadHandlers(): void
{
    $handlers = spl_autoload_functions();

    if ($handlers === false) {
        echo "オートロードハンドラは登録されていません\n";
        return;
    }

    echo "=== 登録済みオートロードハンドラ(" . count($handlers) . "件)===\n";
    foreach ($handlers as $index => $handler) {
        $info = new HandlerInfo($handler);
        printf(
            "  [%d] %-16s %s\n",
            $index,
            $info->label . ':',
            $info->signature
        );
    }
    echo "\n";
}

// --- 各種ハンドラを登録 ---
spl_autoload_register('spl_autoload');

spl_autoload_register(function (string $class): void {
    // クロージャ1
});

spl_autoload_register(function (string $class): void {
    // クロージャ2
});

spl_autoload_register(['StaticLoader', 'load']);

class InstanceLoader
{
    public function handle(string $class): void {}
}
spl_autoload_register([new InstanceLoader(), 'handle']);

dumpAutoloadHandlers();

実行結果:

=== 登録済みオートロードハンドラ(5件)===
  [0] Function:         spl_autoload()
  [1] Closure:          Closure@sample2.php:42
  [2] Closure:          Closure@sample2.php:46
  [3] Static Method:    StaticLoader::load()
  [4] Instance Method:  InstanceLoader->handle()

解説: ReflectionFunction を使うとクロージャの定義ファイルと行番号を取得できます。複数のクロージャが登録されている場合でも、それぞれがどこで定義されたかを追跡できるため、デバッグが格段に楽になります。


サンプル3:特定ハンドラが登録済みか確認する

<?php
declare(strict_types=1);

/**
 * ハンドラがすでに登録されているか確認する
 */
function isHandlerRegistered(callable $target): bool
{
    $handlers = spl_autoload_functions();
    if ($handlers === false) {
        return false;
    }

    foreach ($handlers as $handler) {
        // 関数名文字列
        if (is_string($handler) && is_string($target) && $handler === $target) {
            return true;
        }

        // クロージャ(同一インスタンスかどうか)
        if ($handler instanceof \Closure && $target instanceof \Closure) {
            return $handler === $target;
        }

        // 配列 [class/object, method]
        if (is_array($handler) && is_array($target)) {
            $hClass = is_object($handler[0]) ? get_class($handler[0]) : $handler[0];
            $tClass = is_object($target[0])  ? get_class($target[0])  : $target[0];
            if ($hClass === $tClass && $handler[1] === $target[1]) {
                return true;
            }
        }
    }

    return false;
}

/**
 * 重複登録を防いでハンドラを登録する
 */
function registerOnce(callable $handler, bool $prepend = false): bool
{
    if (isHandlerRegistered($handler)) {
        echo "[registerOnce] 既に登録済みのためスキップ\n";
        return false;
    }

    spl_autoload_register($handler, true, $prepend);
    echo "[registerOnce] 登録完了\n";
    return true;
}

// --- 使用例 ---
class MyLoader
{
    public static function load(string $class): void {}
}

// 1回目:登録される
registerOnce(['MyLoader', 'load']);

// 2回目:スキップされる
registerOnce(['MyLoader', 'load']);

// クロージャは同一インスタンスでないと別物扱い
$cl = function (string $class): void {};
registerOnce($cl); // 登録
registerOnce($cl); // スキップ(同一インスタンス)
registerOnce(function (string $class): void {}); // 登録(別インスタンス)

echo "\n最終的なハンドラ数: " . count(spl_autoload_functions()) . "\n";

実行結果:

[registerOnce] 登録完了
[registerOnce] 既に登録済みのためスキップ
[registerOnce] 登録完了
[registerOnce] 既に登録済みのためスキップ
[registerOnce] 登録完了

最終的なハンドラ数: 3

解説: クロージャは異なる function() {} リテラルが別インスタンスになるため、「同じ処理のクロージャ」でも別物として扱われます。重複登録を防ぐには、クロージャを変数に保持して同じ変数を使い回すことが重要です。


サンプル4:ハンドラのスナップショットと差分検出

テストや診断でオートロードキューの変化を追跡するパターンです。

<?php
declare(strict_types=1);

class AutoloadSnapshot
{
    /** @var list<mixed> */
    private array $snapshot;
    private string $label;

    public function __construct(string $label = 'snapshot')
    {
        $this->label    = $label;
        $this->snapshot = spl_autoload_functions() ?: [];
    }

    /**
     * 現在の状態と比較して差分を返す
     *
     * @return array{added: list<string>, removed: list<string>}
     */
    public function diff(): array
    {
        $current = spl_autoload_functions() ?: [];

        $toLabel = function (mixed $h): string {
            if (is_string($h)) {
                return "fn:{$h}";
            }
            if ($h instanceof \Closure) {
                $r = new \ReflectionFunction($h);
                return "closure@" . basename($r->getFileName() ?? '?') . ':' . $r->getStartLine();
            }
            if (is_array($h)) {
                $cls = is_object($h[0]) ? get_class($h[0]) : $h[0];
                return "method:{$cls}::{$h[1]}";
            }
            return 'unknown';
        };

        $snapLabels    = array_map($toLabel, $this->snapshot);
        $currentLabels = array_map($toLabel, $current);

        return [
            'added'   => array_values(array_diff($currentLabels, $snapLabels)),
            'removed' => array_values(array_diff($snapLabels, $currentLabels)),
        ];
    }

    public function report(): void
    {
        $diff = $this->diff();
        $currentCount = count(spl_autoload_functions() ?: []);

        echo "=== [{$this->label}] 差分レポート ===\n";
        echo "スナップショット時: " . count($this->snapshot) . "件\n";
        echo "現在:             {$currentCount}件\n";

        if (!empty($diff['added'])) {
            echo "追加されたハンドラ:\n";
            foreach ($diff['added'] as $h) {
                echo "  + {$h}\n";
            }
        }

        if (!empty($diff['removed'])) {
            echo "削除されたハンドラ:\n";
            foreach ($diff['removed'] as $h) {
                echo "  - {$h}\n";
            }
        }

        if (empty($diff['added']) && empty($diff['removed'])) {
            echo "変化なし\n";
        }
    }
}

// --- テスト用シナリオ ---

function vendorLoader(string $class): void {}
spl_autoload_register('vendorLoader');

// テスト前のスナップショット
$snap = new AutoloadSnapshot('テスト前');

// テスト中にハンドラが追加・削除される
$testHandler = function (string $class): void {};
spl_autoload_register($testHandler);

class DebugLoader { public static function load(string $c): void {} }
spl_autoload_register(['DebugLoader', 'load']);

// vendorLoader を解除
spl_autoload_unregister('vendorLoader');

// 差分を確認
$snap->report();

実行結果:

=== [テスト前] 差分レポート ===
スナップショット時: 1件
現在:             2件
追加されたハンドラ:
  + closure@sample4.php:62
  + method:DebugLoader::load
削除されたハンドラ:
  - fn:vendorLoader

解説: テストやプラグインシステムの開発でハンドラが意図せず増減していないかを検出するのに役立ちます。ReflectionFunction でクロージャの定義箇所まで追跡できるため、「どこで登録されたか」を突き止めやすくなります。


サンプル5:診断レポートジェネレーター

オートロード全体の状態を診断するレポートを生成するパターンです。

<?php
declare(strict_types=1);

class AutoloadDiagnostics
{
    public static function report(): string
    {
        $lines = [];
        $lines[] = str_repeat('=', 55);
        $lines[] = ' PHP Autoload Diagnostics';
        $lines[] = str_repeat('=', 55);

        // 1. 登録済みハンドラ
        $handlers = spl_autoload_functions();
        $count    = $handlers === false ? 0 : count($handlers);
        $lines[]  = "\n[1] 登録済みハンドラ: {$count}件";

        if ($handlers === false || $handlers === []) {
            $lines[] = "    (なし)";
        } else {
            foreach ($handlers as $i => $h) {
                $lines[] = sprintf(
                    "    [%d] %s",
                    $i,
                    self::describeHandler($h)
                );
            }
        }

        // 2. 拡張子設定
        $extensions = spl_autoload_extensions();
        $lines[]    = "\n[2] spl_autoload 拡張子: {$extensions}";
        $extList    = explode(',', $extensions);
        foreach ($extList as $ext) {
            $note = (trim($ext) === '.inc')
                ? ' ⚠️  レガシー拡張子(セキュリティリスクあり)'
                : '';
            $lines[] = "    " . trim($ext) . $note;
        }

        // 3. include_path
        $paths   = explode(PATH_SEPARATOR, get_include_path());
        $lines[] = "\n[3] include_path: " . count($paths) . "件";
        foreach ($paths as $path) {
            $exists  = is_dir($path) ? '✅' : '❌';
            $lines[] = "    {$exists} {$path}";
        }

        // 4. 推奨事項
        $lines[]       = "\n[4] チェック項目";
        $hasHandlers   = $count > 0;
        $hasIncInExt   = str_contains($extensions, '.inc');
        $hasSrcInPath  = count($paths) > 1;

        $lines[] = "    " . ($hasHandlers  ? '✅' : '❌') . " ハンドラが登録されている";
        $lines[] = "    " . (!$hasIncInExt ? '✅' : '⚠️ ') . " .inc 拡張子を使用していない";
        $lines[] = "    " . ($hasSrcInPath ? '✅' : '⚠️ ') . " include_path に追加ディレクトリが設定されている";

        $lines[] = str_repeat('=', 55);

        return implode("\n", $lines);
    }

    private static function describeHandler(mixed $h): string
    {
        if (is_string($h)) {
            return "function  : {$h}";
        }

        if ($h instanceof \Closure) {
            try {
                $r    = new \ReflectionFunction($h);
                $file = basename($r->getFileName() ?? 'unknown');
                $line = $r->getStartLine();
                return "closure   : defined at {$file}:{$line}";
            } catch (\ReflectionException) {
                return "closure   : (unknown location)";
            }
        }

        if (is_array($h) && count($h) === 2) {
            $cls = is_object($h[0]) ? get_class($h[0]) . ' (instance)' : $h[0] . ' (static)';
            return "method    : {$cls}::{$h[1]}";
        }

        return "unknown   : " . gettype($h);
    }
}

// --- 登録してから診断 ---
spl_autoload_register('spl_autoload');
spl_autoload_register(function (string $class): void { /* App loader */ });

class ComposerLoader { public static function loadClass(string $c): void {} }
spl_autoload_register(['ComposerLoader', 'loadClass']);

set_include_path(get_include_path() . PATH_SEPARATOR . __DIR__ . '/src');

echo AutoloadDiagnostics::report();

実行結果:

=======================================================
 PHP Autoload Diagnostics
=======================================================

[1] 登録済みハンドラ: 3件
    [0] function  : spl_autoload
    [1] closure   : defined at sample5.php:87
    [2] method    : ComposerLoader (static)::loadClass

[2] spl_autoload 拡張子: .inc,.php
    .inc ⚠️  レガシー拡張子(セキュリティリスクあり)
    .php

[3] include_path: 2件
    ✅ .
    ✅ /var/www/project/src

[4] チェック項目
    ✅ ハンドラが登録されている
    ⚠️  .inc 拡張子を使用していない
    ✅ include_path に追加ディレクトリが設定されている
=======================================================

解説: オートロードの現状を俯瞰できる診断レポートです。.inc の警告やパスの存在確認など、設定ミスを可視化できます。CIパイプラインや開発環境のヘルスチェックに組み込めます。


サンプル6:テスト前後のクリーンアップ検証トレイト

PHPUnitなどのテストフレームワークでオートロードキューの汚染を防ぐトレイトです。

<?php
declare(strict_types=1);

/**
 * テストケースでオートロードキューの汚染を検出・防止するトレイト
 */
trait AssertsAutoloadIntegrity
{
    /** @var array<mixed> テスト前のハンドラスナップショット */
    private array $autoloadSnapshot = [];

    /**
     * テスト開始前に呼ぶ(setUp相当)
     */
    protected function captureAutoloadState(): void
    {
        $this->autoloadSnapshot = spl_autoload_functions() ?: [];
        $count = count($this->autoloadSnapshot);
        echo "[AutoloadGuard] スナップショット取得: {$count}件\n";
    }

    /**
     * テスト終了後に呼ぶ(tearDown相当)
     * テスト中に追加されたハンドラを自動削除する
     */
    protected function restoreAutoloadState(): void
    {
        $current  = spl_autoload_functions() ?: [];
        $snapshot = $this->autoloadSnapshot;

        // スナップショット後に追加されたハンドラを特定して削除
        $added = array_slice($current, count($snapshot));
        foreach ($added as $handler) {
            spl_autoload_unregister($handler);
            echo "[AutoloadGuard] 追加ハンドラを削除: " . $this->handlerName($handler) . "\n";
        }

        $after = count(spl_autoload_functions() ?: []);
        echo "[AutoloadGuard] 復元完了: {$after}件\n";
    }

    /**
     * キューが汚染されていないか検証(アサーション)
     */
    protected function assertAutoloadNotContaminated(): void
    {
        $current  = spl_autoload_functions() ?: [];
        $snapshot = $this->autoloadSnapshot;

        if (count($current) !== count($snapshot)) {
            $diff = count($current) - count($snapshot);
            $sign = $diff > 0 ? "+{$diff}" : (string)$diff;
            throw new \RuntimeException(
                "オートロードキューが汚染されています(スナップショット比 {$sign}件)"
            );
        }

        echo "[AutoloadGuard] ✅ キューは汚染されていません\n";
    }

    private function handlerName(mixed $handler): string
    {
        if (is_string($handler)) return $handler;
        if ($handler instanceof \Closure) return 'Closure';
        if (is_array($handler)) {
            $cls = is_object($handler[0]) ? get_class($handler[0]) : $handler[0];
            return "{$cls}::{$handler[1]}";
        }
        return 'unknown';
    }
}

// --- テストクラスのシミュレーション ---
class SomeFeatureTest
{
    use AssertsAutoloadIntegrity;

    // テスト: ハンドラを追加して後始末するケース
    public function testWithCleanup(): void
    {
        $this->captureAutoloadState();

        // テスト中にハンドラを追加
        $tempLoader = function (string $class): void {};
        spl_autoload_register($tempLoader);

        echo "テスト実行中: ハンドラ数=" . count(spl_autoload_functions()) . "\n";

        // tearDown相当
        $this->restoreAutoloadState();
        $this->assertAutoloadNotContaminated();
    }

    // テスト: 汚染されたまま終わるケース(検出デモ)
    public function testWithContamination(): void
    {
        $this->captureAutoloadState();

        spl_autoload_register(function (string $c): void {});
        spl_autoload_register(function (string $c): void {});

        try {
            $this->assertAutoloadNotContaminated();
        } catch (\RuntimeException $e) {
            echo "[検出] " . $e->getMessage() . "\n";
        } finally {
            $this->restoreAutoloadState();
        }
    }
}

$test = new SomeFeatureTest();

echo "=== testWithCleanup ===\n";
$test->testWithCleanup();

echo "\n=== testWithContamination ===\n";
$test->testWithContamination();

実行結果:

=== testWithCleanup ===
[AutoloadGuard] スナップショット取得: 0件
テスト実行中: ハンドラ数=1
[AutoloadGuard] 追加ハンドラを削除: Closure
[AutoloadGuard] 復元完了: 0件
[AutoloadGuard] ✅ キューは汚染されていません

=== testWithContamination ===
[AutoloadGuard] スナップショット取得: 0件
[検出] オートロードキューが汚染されています(スナップショット比 +2件)
[AutoloadGuard] 追加ハンドラを削除: Closure
[AutoloadGuard] 追加ハンドラを削除: Closure
[AutoloadGuard] 復元完了: 0件

解説: spl_autoload_functions() でスナップショットと現在の状態を比較することで、テスト間のオートロードキュー汚染を自動検出・修復できます。テストスイートの独立性を保つ上で重要なパターンです。


よくある落とし穴

① 未登録時は false(空配列ではない)

// ❌ 空配列として扱うと count() でエラー
$handlers = spl_autoload_functions();
$count = count($handlers); // TypeError: count() on false

// ✅ false チェックを先に行う
$handlers = spl_autoload_functions();
$count = $handlers === false ? 0 : count($handlers);

// または
$handlers = spl_autoload_functions() ?: [];
$count = count($handlers);

② 返される配列は「参照」ではなく「コピー」

$handlers = spl_autoload_functions();
// この配列を変更してもキューには影響しない
// キューを変更したい場合は spl_autoload_register/unregister を使う

③ クロージャの同一性は厳密

$a = function (string $class): void {};
$b = function (string $class): void {};

spl_autoload_register($a);
spl_autoload_register($b);

$handlers = spl_autoload_functions();
// $a と $b は内容が同じでも別インスタンスなので別要素として返される
echo count($handlers); // 2

④ ハンドラの順序がオートロードの優先順位

// 先頭のハンドラほど優先される
$handlers = spl_autoload_functions();
// [0] が最優先、[n] が最後に試みられる
// prepend=true で登録したハンドラは先頭に来る

まとめ

ポイント内容
主な用途登録済みハンドラの確認・デバッグ・診断・テストの整合性チェック
戻り値登録順のハンドラ配列、未登録時は false(空配列ではない)
要素の型登録方法により string / array / Closure の3形式
配列の順序オートロードの実行優先順(先頭が最優先)
よくある誤り戻り値 false を空配列として扱う → ?: [] でガードする
活用パターンスナップショット比較・重複チェック・診断レポート・テスト後クリーンアップ

spl_autoload_functions() 自体は「現在の状態を取得するだけ」のシンプルな関数ですが、返される情報を活用することでオートロードシステムの可視化・診断・テスト保護を実現できます。spl_autoload_register()spl_autoload_unregister() と組み合わせて、オートロードキューを安全かつ透明に管理しましょう。


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

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