[PHP]spl_autoload_register完全解説|オートロードハンドラを登録してクラスを自動読み込みする方法

PHP

はじめに

PHPで規模の大きいアプリケーションを書くとき、ファイルを一つひとつ require で読み込むのは非現実的です。spl_autoload_register() を使えば、未定義クラスが参照された瞬間に自動でファイルを読み込む「オートロード」を実現できます。

Composerが内部的に使用しているのもこの関数です。現代のPHPアプリケーション開発において、オートロードの仕組みを正しく理解することは必須スキルといえます。

本記事では spl_autoload_register() の仕様から、PSR-4準拠の実装・複数ハンドラの制御・Composerとの共存まで、実践的に解説します。


関数の基本情報

項目内容
関数名spl_autoload_register()
利用可能バージョンPHP 5.1以降
所属SPL(Standard PHP Library)
戻り値bool(成功時true、失敗時false
拡張機能SPL(デフォルトで有効)

シグネチャ

spl_autoload_register(
    ?callable $callback = null,
    bool $throw = true,
    bool $prepend = false
): bool

パラメータ

パラメータデフォルト説明
$callbackcallable|nullnull登録するハンドラ。nullの場合はspl_autoload()を登録
$throwbooltrue登録失敗時に例外をスローするか(PHP 8.0以降は常にtrue相当)
$prependboolfalsetrueでキューの先頭に挿入(後から優先させたいときに使う)

ハンドラとして渡せるcallable

書き方
関数名(文字列)'spl_autoload'
クロージャfunction(string $class): void { ... }
静的メソッド配列[MyLoader::class, 'load']
インスタンスメソッド配列[$loaderObj, 'load']
呼び出し可能オブジェクト__invoke() を持つオブジェクト

オートロードキューの動作原理

未定義クラスへのアクセス(new / class_exists / instanceofなど)
          ↓
  登録済みハンドラをキュー順([0]→[1]→[2]…)に実行
          ↓
   ┌──────────────────────────────┐
   │ ハンドラがクラスを定義した? │
   └──────────────────────────────┘
        Yes ↓           No ↓
     処理完了       次のハンドラへ
          ↓
   全ハンドラで未解決
          ↓
      Fatal Error

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

サンプル1:最もシンプルなオートローダー

<?php
declare(strict_types=1);

// クロージャでオートローダーを登録
spl_autoload_register(function (string $class): void {
    // 名前空間の \ をディレクトリ区切り / に変換して .php を付ける
    $file = __DIR__ . '/src/' . str_replace('\\', '/', $class) . '.php';

    if (file_exists($file)) {
        require $file;
    }
});

// 以降、このディレクトリ構造でクラスが自動ロードされる
// src/
//   App/
//     Model/
//       User.php        → App\Model\User クラスを含む
//     Controller/
//       UserController.php → App\Controller\UserController を含む

// new App\Model\User();  // → src/App/Model/User.php が自動読み込みされる

echo "登録完了: " . count(spl_autoload_functions()) . " ハンドラ\n";

解説: わずか数行で基本的なオートローダーが完成します。str_replace('\\', '/', $class) が名前空間をディレクトリパスに変換するキーポイントです。


サンプル2:PSR-4準拠のオートローダークラス

現在の業界標準であるPSR-4規約に完全準拠したオートローダーです。

<?php
declare(strict_types=1);

/**
 * PSR-4準拠オートローダー
 *
 * 名前空間プレフィックスとベースディレクトリを1対多でマッピングし、
 * 最長一致アルゴリズムでファイルを解決する。
 */
class Psr4AutoloaderClass
{
    /** @var array<string, list<string>> プレフィックス → ベースディレクトリリスト */
    private array $prefixes = [];

    /**
     * 名前空間プレフィックスとベースディレクトリを登録
     *
     * @param string $prefix   "App\\" のように末尾に \\ を付ける
     * @param string $baseDir  対応するベースディレクトリの絶対パス
     * @param bool   $prepend  trueでそのプレフィックスのディレクトリ一覧の先頭に挿入
     */
    public function addNamespace(
        string $prefix,
        string $baseDir,
        bool $prepend = false
    ): self {
        // プレフィックスの末尾を \\ に統一
        $prefix  = trim($prefix, '\\') . '\\';
        // ベースディレクトリの末尾を / に統一
        $baseDir = rtrim($baseDir, '/\\') . DIRECTORY_SEPARATOR;

        $this->prefixes[$prefix] ??= [];

        if ($prepend) {
            array_unshift($this->prefixes[$prefix], $baseDir);
        } else {
            $this->prefixes[$prefix][] = $baseDir;
        }

        return $this;
    }

    /**
     * spl_autoload_register に登録する
     */
    public function register(bool $prepend = false): self
    {
        spl_autoload_register([$this, 'loadClass'], true, $prepend);
        return $this;
    }

    public function unregister(): void
    {
        spl_autoload_unregister([$this, 'loadClass']);
    }

    /**
     * クラス名からファイルを解決してロードする(ハンドラ本体)
     */
    public function loadClass(string $class): bool
    {
        // 名前空間プレフィックスを長い方から順に試す
        $prefix = $class;

        while (false !== ($pos = strrpos($prefix, '\\'))) {
            $prefix        = substr($class, 0, $pos + 1);
            $relativeClass = substr($class, $pos + 1);

            if ($this->loadMappedFile($prefix, $relativeClass)) {
                return true;
            }

            // 末尾の \ を除いて次の区切りへ
            $prefix = rtrim($prefix, '\\');
        }

        return false;
    }

    private function loadMappedFile(string $prefix, string $relativeClass): bool
    {
        if (!isset($this->prefixes[$prefix])) {
            return false;
        }

        foreach ($this->prefixes[$prefix] as $baseDir) {
            $file = $baseDir
                . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass)
                . '.php';

            if (is_file($file)) {
                require $file;
                return true;
            }
        }

        return false;
    }
}

// --- 登録例 ---
$loader = new Psr4AutoloaderClass();
$loader
    ->addNamespace('App\\',    __DIR__ . '/src')
    ->addNamespace('App\\',    __DIR__ . '/app')    // 同じプレフィックスで複数ディレクトリ
    ->addNamespace('Tests\\',  __DIR__ . '/tests')
    ->addNamespace('Vendor\\', __DIR__ . '/vendor/src')
    ->register();

echo "PSR-4 ローダー登録完了\n";

// App\Controller\UserController → src/Controller/UserController.php
// App\Model\User               → src/Model/User.php または app/Model/User.php
// Tests\Unit\UserTest          → tests/Unit/UserTest.php

解説: strrpos を使って末尾から順に名前空間プレフィックスを試す「最長一致アルゴリズム」がPSR-4の要です。App\Controller\UserController なら App\Controller\App\ の順に試行します。


サンプル3:複数ハンドラの優先順位制御

$prepend 引数を使ってハンドラの実行順序を制御するパターンです。

<?php
declare(strict_types=1);

// 通常の優先度でアプリケーションローダーを登録
spl_autoload_register(function (string $class): void {
    echo "[App]    {$class}\n";
    // 本来はファイルを読み込む
}, true, false);

// 通常の優先度でベンダーローダーを登録
spl_autoload_register(function (string $class): void {
    echo "[Vendor] {$class}\n";
}, true, false);

// 先頭に割り込む高優先度ローダー(prepend=true)
spl_autoload_register(function (string $class): void {
    echo "[Cache]  {$class} ← 最優先でキャッシュを確認\n";
    // キャッシュヒットしなければ次のハンドラへ
}, true, true); // prepend=true

// 現在のキュー順序を確認
echo "=== ハンドラ実行順序 ===\n";
foreach (spl_autoload_functions() as $i => $fn) {
    echo "[{$i}] " . ($fn instanceof \Closure ? 'Closure' : (string)$fn) . "\n";
}

/*
 * [0] Cache  ← prepend=true で先頭に来る
 * [1] App    ← 登録順
 * [2] Vendor ← 登録順
 */

// クラス参照時(実際には存在しないのでFatal Errorになるため here はコメント)
// new SomeClass(); → [Cache] → [App] → [Vendor] の順で実行される

実行結果:

=== ハンドラ実行順序 ===
[0] Closure  ← Cache(prepend=true)
[1] Closure  ← App
[2] Closure  ← Vendor

解説: prepend=true を使うと、後から登録したハンドラを先頭に割り込ませられます。デバッグ用ロガーやキャッシュレイヤーを他のハンドラより先に実行させたい場合に有効です。


サンプル4:クラスマップ型高速ローダーと動的ローダーの併用

Composerの --optimize-autoloader に相当する、クラスマップを優先して動的検索をフォールバックにするパターンです。

<?php
declare(strict_types=1);

class HybridAutoloader
{
    /** @var array<string, string> クラス名 → ファイルパス */
    private array $classMap = [];

    /** @var array<string, string> 名前空間プレフィックス → ベースディレクトリ */
    private array $namespaceMap = [];

    private int $cacheHits   = 0;
    private int $cacheMisses = 0;

    public function addClassMap(array $map): self
    {
        $this->classMap = array_merge($this->classMap, $map);
        return $this;
    }

    public function addNamespace(string $prefix, string $dir): self
    {
        $this->namespaceMap[trim($prefix, '\\') . '\\'] = rtrim($dir, '/\\');
        return $this;
    }

    public function register(): self
    {
        spl_autoload_register([$this, 'load']);
        return $this;
    }

    public function load(string $class): bool
    {
        // ① クラスマップで高速解決(O(1))
        if (isset($this->classMap[$class])) {
            $file = $this->classMap[$class];
            if (is_file($file)) {
                $this->cacheHits++;
                require $file;
                return true;
            }
        }

        // ② 名前空間マップで動的解決(フォールバック)
        foreach ($this->namespaceMap as $prefix => $dir) {
            if (!str_starts_with($class, $prefix)) {
                continue;
            }

            $relative = str_replace(
                '\\',
                DIRECTORY_SEPARATOR,
                substr($class, strlen($prefix))
            );
            $file = $dir . DIRECTORY_SEPARATOR . $relative . '.php';

            if (is_file($file)) {
                $this->cacheMisses++;
                require $file;
                // 次回のためにクラスマップへ追加
                $this->classMap[$class] = $file;
                return true;
            }
        }

        return false;
    }

    public function stats(): void
    {
        $total = $this->cacheHits + $this->cacheMisses;
        $rate  = $total > 0 ? round($this->cacheHits / $total * 100, 1) : 0;
        echo "クラスマップヒット: {$this->cacheHits}件 / ミス: {$this->cacheMisses}件 / ヒット率: {$rate}%\n";
    }
}

// --- 使用例 ---
$loader = new HybridAutoloader();
$loader
    ->addClassMap([
        // 事前にスキャンして生成したクラスマップ
        'App\Model\User'               => __DIR__ . '/src/Model/User.php',
        'App\Model\Post'               => __DIR__ . '/src/Model/Post.php',
        'App\Controller\HomeController' => __DIR__ . '/src/Controller/HomeController.php',
    ])
    ->addNamespace('App\\', __DIR__ . '/src')  // クラスマップにないものはここで探す
    ->register();

// クラスマップにあるクラスは即座に解決(高速)
// new App\Model\User();

// クラスマップにないクラスは名前空間マップで動的検索(やや遅いがキャッシュに追加)
// new App\Service\AuthService();

$loader->stats();

解説: ①クラスマップ(ハッシュ検索、O(1))で見つからなければ②名前空間マップ(ファイルシステム検索)に fallback し、見つかったら次回のためにクラスマップへ追記します。Composerのオートローダー最適化と同じ戦略です。


サンプル5:条件付き・環境別ローダー

開発・テスト・本番で異なるオートロード戦略を切り替えるパターンです。

<?php
declare(strict_types=1);

class EnvironmentAwareAutoloader
{
    private array $registeredHandlers = [];

    public function __construct(
        private readonly string $env,     // 'production' | 'development' | 'test'
        private readonly string $srcDir,
        private readonly string $cacheFile = '',
    ) {}

    public function setup(): void
    {
        match ($this->env) {
            'production'  => $this->setupProduction(),
            'test'        => $this->setupTest(),
            default       => $this->setupDevelopment(),
        };

        echo "[Autoloader] 環境: {$this->env} / ハンドラ: "
            . count(spl_autoload_functions()) . "件\n";
    }

    private function setupProduction(): void
    {
        // 本番:事前生成したクラスマップを使用(最速)
        if ($this->cacheFile && is_file($this->cacheFile)) {
            $classMap = require $this->cacheFile;
            $this->addHandler('classmap', function (string $class) use ($classMap): void {
                if (isset($classMap[$class]) && is_file($classMap[$class])) {
                    require $classMap[$class];
                }
            });
            echo "[Autoloader] クラスマップ読み込み: " . count($classMap) . "件\n";
        } else {
            // クラスマップがなければPSR-4にフォールバック
            $this->setupPsr4();
        }
    }

    private function setupDevelopment(): void
    {
        // 開発:デバッグログ付きPSR-4(毎回ファイルシステムを検索)
        $this->addHandler('debug', function (string $class): void {
            echo "[AutoloadDebug] 試行: {$class}\n";
        }, prepend: true);
        $this->setupPsr4();
    }

    private function setupTest(): void
    {
        // テスト:モックディレクトリを優先
        $this->addHandler('test-mock', function (string $class): void {
            $file = __DIR__ . '/tests/mocks/' . str_replace('\\', '/', $class) . '.php';
            if (is_file($file)) {
                require $file;
                echo "[TestLoader] モック読み込み: {$class}\n";
            }
        }, prepend: true);
        $this->setupPsr4();
    }

    private function setupPsr4(): void
    {
        $dir = $this->srcDir;
        $this->addHandler('psr4', function (string $class) use ($dir): void {
            $file = $dir . '/' . str_replace('\\', '/', $class) . '.php';
            if (is_file($file)) {
                require $file;
            }
        });
    }

    private function addHandler(
        string $name,
        callable $handler,
        bool $prepend = false
    ): void {
        spl_autoload_register($handler, true, $prepend);
        $this->registeredHandlers[$name] = $handler;
    }

    public function unregisterAll(): void
    {
        foreach ($this->registeredHandlers as $handler) {
            spl_autoload_unregister($handler);
        }
        $this->registeredHandlers = [];
    }
}

// --- 使用例 ---
$env    = getenv('APP_ENV') ?: 'development';
$loader = new EnvironmentAwareAutoloader(
    env:       $env,
    srcDir:    __DIR__ . '/src',
    cacheFile: __DIR__ . '/cache/classmap.php',
);
$loader->setup();

実行結果(development環境):

[Autoloader] 環境: development / ハンドラ: 2件

解説: APP_ENV 環境変数に応じてオートロード戦略を切り替えます。本番では事前生成クラスマップ(高速)、開発ではデバッグログ付きPSR-4、テストではモックディレクトリ優先という実際のフレームワークに近いパターンです。


サンプル6:__invoke() を使った呼び出し可能オブジェクトとライフサイクル管理

オートローダーをファーストクラスオブジェクトとして扱い、統計・ライフサイクルを管理するパターンです。

<?php
declare(strict_types=1);

/**
 * オートロードハンドラを呼び出し可能オブジェクトとして実装
 * __invoke() を持つオブジェクトはそのまま callable として渡せる
 */
class TrackedAutoloader
{
    private int $attempts  = 0;
    private int $successes = 0;
    private int $failures  = 0;

    /** @var list<array{class: string, success: bool, time: float}> */
    private array $history = [];

    private bool $active = false;

    public function __construct(
        private readonly string $baseDir,
        private readonly string $namespace = '',
    ) {}

    /**
     * callable として呼ばれるオートロードハンドラ本体
     */
    public function __invoke(string $class): void
    {
        if (!$this->active) {
            return;
        }

        // 担当する名前空間のみ処理
        if ($this->namespace !== '' && !str_starts_with($class, $this->namespace . '\\')) {
            return;
        }

        $start    = microtime(true);
        $relative = $this->namespace !== ''
            ? substr($class, strlen($this->namespace) + 1)
            : $class;

        $file = $this->baseDir
            . DIRECTORY_SEPARATOR
            . str_replace('\\', DIRECTORY_SEPARATOR, $relative)
            . '.php';

        $this->attempts++;
        $elapsed = round((microtime(true) - $start) * 1000, 3);

        if (is_file($file)) {
            require $file;
            $this->successes++;
            $this->history[] = ['class' => $class, 'success' => true,  'time' => $elapsed];
        } else {
            $this->failures++;
            $this->history[] = ['class' => $class, 'success' => false, 'time' => $elapsed];
        }
    }

    public function start(): self
    {
        if (!$this->active) {
            spl_autoload_register($this);
            $this->active = true;
        }
        return $this;
    }

    public function stop(): self
    {
        if ($this->active) {
            spl_autoload_unregister($this);
            $this->active = false;
        }
        return $this;
    }

    public function isActive(): bool
    {
        return $this->active;
    }

    public function stats(): array
    {
        return [
            'attempts'  => $this->attempts,
            'successes' => $this->successes,
            'failures'  => $this->failures,
            'rate'      => $this->attempts > 0
                ? round($this->successes / $this->attempts * 100, 1)
                : 0.0,
        ];
    }

    public function dumpHistory(int $limit = 10): void
    {
        echo "=== オートロード履歴(直近{$limit}件)===\n";
        foreach (array_slice($this->history, -$limit) as $entry) {
            $icon = $entry['success'] ? '✅' : '❌';
            printf("  %s %-50s %.3fms\n", $icon, $entry['class'], $entry['time']);
        }

        $s = $this->stats();
        echo "成功: {$s['successes']} / 失敗: {$s['failures']} / 成功率: {$s['rate']}%\n";
    }
}

// --- 使用例 ---
$appLoader = new TrackedAutoloader(
    baseDir:   __DIR__ . '/src',
    namespace: 'App'
);
$appLoader->start();

echo "稼働中: " . ($appLoader->isActive() ? 'yes' : 'no') . "\n";
echo "ハンドラ数: " . count(spl_autoload_functions()) . "\n";

// クラスを参照するとオートロード履歴に記録される
// new App\Model\User();
// new App\Service\AuthService();
// new App\NonExistent\Class();

// 統計確認
$appLoader->dumpHistory();

// ローダーを停止
$appLoader->stop();
echo "停止後ハンドラ数: " . count(spl_autoload_functions() ?: []) . "\n";

実行結果:

稼働中: yes
ハンドラ数: 1
=== オートロード履歴(直近10件)===
成功: 0 / 失敗: 0 / 成功率: 0%
停止後ハンドラ数: 0

解説: __invoke() を持つオブジェクトは spl_autoload_register($this) でそのまま登録できます。start()/stop() メソッドでライフサイクルを制御し、統計や履歴をオブジェクト内に蓄積できるためデバッグやモニタリングに便利です。


よくある落とし穴

① ハンドラ内で例外を投げるとクラスが未定義のまま伝播する

// ❌ 例外でクラス定義が飛ぶ
spl_autoload_register(function (string $class): void {
    $file = "/path/{$class}.php";
    if (!file_exists($file)) {
        throw new RuntimeException("Not found: {$class}"); // Fatalを招く場合がある
    }
    require $file;
});

// ✅ 見つからなければ return(次のハンドラに委ねる)
spl_autoload_register(function (string $class): void {
    $file = __DIR__ . '/src/' . str_replace('\\', '/', $class) . '.php';
    if (!is_file($file)) {
        return; // 次のハンドラへ
    }
    require $file;
});

② require と require_once の使い分け

// ❌ require_once は不要(同じクラスを2度ロードしようとする前に
//    class_exists でチェックされるためPHPが自動制御する)
spl_autoload_register(function (string $class): void {
    require_once __DIR__ . '/src/' . $class . '.php'; // 過剰
});

// ✅ require で十分(オートロードは未定義クラスのときのみ発火する)
spl_autoload_register(function (string $class): void {
    $file = __DIR__ . '/src/' . str_replace('\\', '/', $class) . '.php';
    if (is_file($file)) {
        require $file;
    }
});

③ unregister に渡すのは登録時と同一の callable

$handler = function (string $class): void { /* ... */ };
spl_autoload_register($handler);

// ❌ 新しいクロージャは別インスタンスなので解除できない
spl_autoload_unregister(function (string $class): void { /* ... */ });

// ✅ 同じ変数を使う
spl_autoload_unregister($handler);

④ __autoload() との共存(PHP 8.0で廃止)

// PHP 7.x まで
function __autoload(string $class): void { /* ... */ } // 非推奨

// PHP 8.0 以降は Fatal Error になる
// → spl_autoload_register() を使う

⑤ パフォーマンス:is_file() vs file_exists()

// ❌ file_exists() はディレクトリもヒットする
if (file_exists($file)) { require $file; }

// ✅ is_file() でファイルのみを確認(わずかに安全・正確)
if (is_file($file)) { require $file; }

Composerとの共存

// Composerのオートローダーを読み込む(内部でspl_autoload_registerを使用)
require __DIR__ . '/vendor/autoload.php';

// 独自ローダーをその後に追加(後続)
spl_autoload_register(function (string $class): void {
    // Composerに含まれないレガシークラスなどを処理
});

// または Composerより先に実行したい場合(prepend=true)
spl_autoload_register(function (string $class): void {
    // 独自プロトコルでの解決を先に試みる
}, true, true);

// 現在のキューを確認
echo count(spl_autoload_functions()) . " ハンドラ登録済み\n";

まとめ

ポイント内容
主な用途クラス・インターフェース・トレイトの自動ロード
登録方法クロージャ・関数名・静的/インスタンスメソッド・__invokeオブジェクト
複数登録キューとして積み上げ順番に実行。いずれかがクラスを定義したら完了
優先制御prepend=true でキュー先頭に挿入
解除spl_autoload_unregister() に登録時と同一callableを渡す
現代の実務ComposerがPSR-4でラップして使用。直接書く場合もPSR-4準拠が推奨
requireハンドラ内では requirerequire_once は過剰)

spl_autoload_register() はPHPオートロードの根幹をなす関数です。Composerを使っていても内部では必ずこの関数が呼ばれています。仕組みを理解しておくことで、Composerの設定・カスタムローダーの追加・テスト時のモック差し替えなど、あらゆるオートロード関連の課題に自信を持って対応できるようになります。


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

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