[PHP]spl_autoload完全解説|クラスのオートロード仕組みと実践的な使い方

PHP

はじめに

PHPでクラスを使うとき、ファイルを一つひとつ require するのは手間がかかります。オートロードはクラスが初めて参照された瞬間に自動でファイルを読み込む仕組みで、現代のPHPアプリケーションには欠かせません。

spl_autoload() はSPL(Standard PHP Library)が提供するデフォルトのオートロード関数で、spl_autoload_register() にハンドラとして登録することで使用します。クラス名からファイルパスを自動解決する組み込みの変換ルールを持っており、シンプルなプロジェクトではほぼ設定なしで動作します。

本記事では spl_autoload() の仕様から、周辺関数群(spl_autoload_registerspl_autoload_unregisterspl_autoload_functionsspl_autoload_extensions)との連携まで丁寧に解説します。


関数の基本情報

項目内容
関数名spl_autoload()
利用可能バージョンPHP 5.1以降
所属SPL(Standard PHP Library)
戻り値void
拡張機能SPL(デフォルトで有効)

シグネチャ

spl_autoload(string $class, ?string $file_extensions = null): void

パラメータ

パラメータ説明
$classstring読み込むクラス名(オートロード時はPHPが自動で渡す)
$file_extensionsstring|null検索するファイル拡張子(カンマ区切り)。nullの場合はspl_autoload_extensions()の設定を使用

デフォルトの変換ルール

spl_autoload() はクラス名を以下のルールでファイルパスに変換します。

1. クラス名を小文字に変換
2. 名前空間の区切り文字 \ をディレクトリ区切り / に変換
3. include_path の各ディレクトリを検索
4. 設定された拡張子(デフォルト: .inc, .php)のファイルを探す
// 例:以下のクラスがオートロードされるとき
App\Controller\UserController

// 変換後のファイルパス候補
app/controller/usercontroller.inc
app/controller/usercontroller.php

SPLオートロード関連関数の全体像

spl_autoload()             → デフォルトのオートロードハンドラ(直接登録して使う)
spl_autoload_register()    → オートロードハンドラをキューに追加
spl_autoload_unregister()  → 登録済みハンドラをキューから削除
spl_autoload_functions()   → 登録済みハンドラ一覧を取得
spl_autoload_extensions()  → 検索対象のファイル拡張子を取得・設定
spl_autoload_call()        → 指定クラスのオートロードを手動で実行

ハンドラキューの動作

クラス参照が発生
      ↓
spl_autoload_register() で登録されたハンドラを順番に実行
      ↓
いずれかのハンドラでクラスが定義されたら完了
      ↓
すべて試みてもクラスが見つからない → Fatal Error

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

サンプル1:spl_autoload をそのまま登録する

最もシンプルな使い方。spl_autoload 自体をハンドラとして登録します。

<?php
declare(strict_types=1);

// spl_autoload をオートロードハンドラとして登録
spl_autoload_register('spl_autoload');

// 検索する拡張子を設定(デフォルトは .inc,.php)
spl_autoload_extensions('.php');

// include_path にクラスファイルのあるディレクトリを追加
set_include_path(
    get_include_path() . PATH_SEPARATOR . __DIR__ . '/src'
);

// ここで App\Model\User クラスを使うと...
// → src/app/model/user.php を検索して自動読み込み
// $user = new App\Model\User();

echo "登録済みハンドラ:\n";
foreach (spl_autoload_functions() as $fn) {
    echo "  " . (is_string($fn) ? $fn : (is_array($fn) ? implode('::', $fn) : 'closure')) . "\n";
}
// 登録済みハンドラ:
//   spl_autoload

解説: spl_autoload_register('spl_autoload') とするだけで、PHPのデフォルトオートロードが有効になります。include_path と拡張子の設定だけで動くため、シンプルなプロジェクトに向いています。


サンプル2:PSR-4準拠のカスタムオートローダー

現在の主流であるPSR-4規約に対応したオートローダーです。

<?php
declare(strict_types=1);

/**
 * PSR-4準拠のオートローダー
 */
class Psr4Autoloader
{
    /** @var array<string, list<string>> 名前空間プレフィックス → ディレクトリリスト */
    private array $prefixes = [];

    /**
     * 名前空間プレフィックスとベースディレクトリを登録
     */
    public function addNamespace(string $prefix, string $baseDir, bool $prepend = false): void
    {
        $prefix  = trim($prefix, '\\') . '\\';
        $baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;

        if (!isset($this->prefixes[$prefix])) {
            $this->prefixes[$prefix] = [];
        }

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

    /**
     * オートロードハンドラとして登録
     */
    public function register(): void
    {
        spl_autoload_register([$this, 'loadClass']);
    }

    /**
     * オートロードを解除
     */
    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 ($this->requireFile($file)) {
                return true;
            }
        }

        return false;
    }

    private function requireFile(string $file): bool
    {
        if (file_exists($file)) {
            require $file;
            return true;
        }
        return false;
    }
}

// --- 登録例 ---
$loader = new Psr4Autoloader();
$loader->addNamespace('App\\',  __DIR__ . '/src');
$loader->addNamespace('Tests\\', __DIR__ . '/tests');
$loader->register();

// 以降は以下のように自動でファイルが読み込まれる
// new App\Controller\UserController()
//   → src/Controller/UserController.php
// new App\Model\User()
//   → src/Model/User.php
// new Tests\Unit\UserTest()
//   → tests/Unit/UserTest.php

echo "PSR-4ローダー登録完了\n";
echo "登録ハンドラ数: " . count(spl_autoload_functions()) . "\n";

解説: spl_autoload_register() にクロージャやオブジェクトメソッドを渡せるため、PSR-4のような複雑なマッピングロジックをクラスにカプセル化できます。Composerが内部的に採用しているアプローチもこのパターンです。


サンプル3:複数ハンドラを優先度付きで管理

複数のオートローダーをキューで管理し、優先度を制御するパターンです。

<?php
declare(strict_types=1);

class AutoloadManager
{
    /** @var array<string, callable> 名前 → ハンドラ */
    private static array $handlers = [];

    /**
     * 名前付きハンドラを登録(prepend=trueで先頭挿入)
     */
    public static function register(
        string $name,
        callable $handler,
        bool $prepend = false
    ): void {
        if (isset(self::$handlers[$name])) {
            self::unregister($name);
        }

        self::$handlers[$name] = $handler;
        spl_autoload_register($handler, true, $prepend);
        echo "[AutoloadManager] 登録: {$name}" . ($prepend ? ' (先頭)' : '') . "\n";
    }

    /**
     * 名前指定で解除
     */
    public static function unregister(string $name): bool
    {
        if (!isset(self::$handlers[$name])) {
            return false;
        }

        $result = spl_autoload_unregister(self::$handlers[$name]);
        unset(self::$handlers[$name]);
        echo "[AutoloadManager] 解除: {$name}\n";
        return $result;
    }

    /**
     * 登録済みハンドラの名前一覧
     */
    public static function registeredNames(): array
    {
        return array_keys(self::$handlers);
    }

    /**
     * すべて解除
     */
    public static function unregisterAll(): void
    {
        foreach (array_keys(self::$handlers) as $name) {
            self::unregister($name);
        }
    }
}

// --- 登録 ---
AutoloadManager::register('vendor', function (string $class): void {
    $file = __DIR__ . '/vendor/' . str_replace('\\', '/', $class) . '.php';
    if (file_exists($file)) {
        require $file;
    }
});

AutoloadManager::register('app', function (string $class): void {
    if (!str_starts_with($class, 'App\\')) {
        return;
    }
    $file = __DIR__ . '/src/' . str_replace(['App\\', '\\'], ['', '/'], $class) . '.php';
    if (file_exists($file)) {
        require $file;
    }
});

// デバッグ用を先頭に挿入(最優先)
AutoloadManager::register('debug', function (string $class): void {
    echo "[DEBUG] オートロード試行: {$class}\n";
    // このハンドラ自体はファイルを読まず、次のハンドラに委ねる
}, prepend: true);

echo "\n登録済み: " . implode(', ', AutoloadManager::registeredNames()) . "\n";

// デバッグハンドラだけ解除
AutoloadManager::unregister('debug');

echo "解除後: " . implode(', ', AutoloadManager::registeredNames()) . "\n";

実行結果:

[AutoloadManager] 登録: vendor
[AutoloadManager] 登録: app
[AutoloadManager] 登録: debug (先頭)

登録済み: vendor, app, debug
[AutoloadManager] 解除: debug
解除後: vendor, app

解説: spl_autoload_register() の第3引数 $prependtrue にすると、キューの先頭に挿入できます。デバッグ用ハンドラを一時的に先頭挿入し、後で spl_autoload_unregister() で取り除くパターンは開発時に便利です。


サンプル4:拡張子の管理と spl_autoload_extensions()

spl_autoload() が検索するファイル拡張子を設定・確認するパターンです。

<?php
declare(strict_types=1);

// デフォルトの拡張子を確認
$default = spl_autoload_extensions();
echo "デフォルト拡張子: {$default}\n"; // .inc,.php

// 拡張子を変更(.phpのみに絞る)
spl_autoload_extensions('.php');
echo "変更後: " . spl_autoload_extensions() . "\n"; // .php

// 複数拡張子を設定(カンマ区切り)
spl_autoload_extensions('.php,.class.php');
echo "複数設定: " . spl_autoload_extensions() . "\n"; // .php,.class.php

// spl_autoload を登録してデフォルトハンドラとして使う
spl_autoload_extensions('.php');
spl_autoload_register('spl_autoload');

// include_path に検索ディレクトリを追加
$dirs = [
    __DIR__ . '/src',
    __DIR__ . '/lib',
    __DIR__ . '/vendor/classes',
];

$extraPath = implode(PATH_SEPARATOR, $dirs);
set_include_path(get_include_path() . PATH_SEPARATOR . $extraPath);

echo "\ninclude_path:\n";
foreach (explode(PATH_SEPARATOR, get_include_path()) as $path) {
    echo "  {$path}\n";
}

// 例:spl_autoload は以下の変換を自動で行う
// クラス名: MyApp\Utils\StringHelper
// 変換後:   myapp/utils/stringhelper.php (include_path 内を検索)
echo "\nspl_autoload の変換例:\n";
$className = 'MyApp\\Utils\\StringHelper';
$converted = strtolower(str_replace('\\', DIRECTORY_SEPARATOR, $className)) . '.php';
echo "  {$className}\n  → {$converted}\n";

実行結果:

デフォルト拡張子: .inc,.php
変更後: .php
複数設定: .php,.class.php

include_path:
  .
  /path/to/src
  /path/to/lib
  /path/to/vendor/classes

spl_autoload の変換例:
  MyApp\Utils\StringHelper
  → myapp/utils/stringhelper.php

解説: spl_autoload_extensions() は引数なしで呼ぶと現在の設定を返し、文字列を渡すと変更します。.inc はレガシーなPHP拡張子なので、現代のプロジェクトでは .php のみに絞ることを推奨します。


サンプル5:テスト時のオートローダー差し替え

テスト環境でオートローダーを差し替え、終了後に元に戻すパターンです。

<?php
declare(strict_types=1);

/**
 * テスト用オートローダーを一時的に差し込むスコープクラス
 */
class AutoloadScope
{
    /** @var list<callable> 差し込む前のハンドラ一覧 */
    private array $previous = [];
    /** @var list<callable> このスコープで登録したハンドラ */
    private array $registered = [];

    public function __construct()
    {
        // 現在のハンドラを退避
        $this->previous = spl_autoload_functions() ?: [];
    }

    public function add(callable $handler, bool $prepend = false): self
    {
        spl_autoload_register($handler, true, $prepend);
        $this->registered[] = $handler;
        return $this;
    }

    /**
     * スコープを終了して元に戻す
     */
    public function restore(): void
    {
        // このスコープで追加したハンドラを解除
        foreach ($this->registered as $handler) {
            spl_autoload_unregister($handler);
        }
        $this->registered = [];
    }

    public function __destruct()
    {
        $this->restore();
    }
}

// --- テスト用クラスマップ(実際にはテスト用モッククラスなど) ---
$testClassMap = [
    'App\Service\MailService'   => __DIR__ . '/tests/mocks/MockMailService.php',
    'App\Repository\UserRepo'  => __DIR__ . '/tests/mocks/MockUserRepo.php',
];

// テスト用スコープ開始
$scope = new AutoloadScope();
$scope->add(function (string $class) use ($testClassMap): void {
    if (isset($testClassMap[$class]) && file_exists($testClassMap[$class])) {
        require $testClassMap[$class];
        echo "[TEST LOADER] {$class} をモックで読み込み\n";
    }
}, prepend: true); // 先頭に差し込んでテスト用モックを優先

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

// テスト終了
$scope->restore();
echo "\nスコープ終了後のハンドラ数: " . count(spl_autoload_functions()) . "\n";

実行結果:

テスト実行中...
登録ハンドラ数: 1
スコープ終了後のハンドラ数: 0

解説: AutoloadScope のデストラクタで確実に spl_autoload_unregister() が呼ばれるため、テスト間でオートローダーの汚染が起きません。モックの差し込みと後片付けを構造的に管理できます。


サンプル6:クラスマップ型オートローダー

クラス名とファイルパスのマッピングを事前に構築し、高速に解決するパターンです(Composerの classmap オプションに相当)。

<?php
declare(strict_types=1);

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

    /**
     * クラスマップを追加
     *
     * @param array<string, string> $map
     */
    public function addMap(array $map): self
    {
        $this->classMap = array_merge($this->classMap, $map);
        return $this;
    }

    /**
     * ディレクトリを再帰スキャンしてクラスマップを自動生成
     */
    public function scanDirectory(string $dir, string $extension = '.php'): self
    {
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS)
        );

        foreach ($iterator as $file) {
            if (!$file->isFile() || !str_ends_with($file->getFilename(), $extension)) {
                continue;
            }

            $className = $this->extractClassName($file->getPathname());
            if ($className !== null) {
                $this->classMap[$className] = $file->getPathname();
            }
        }

        return $this;
    }

    /**
     * PHPファイルからクラス/インターフェース/トレイト名を抽出
     */
    private function extractClassName(string $filePath): ?string
    {
        $content   = file_get_contents($filePath) ?: '';
        $namespace = '';

        if (preg_match('/^namespace\s+([^;{]+)/m', $content, $m)) {
            $namespace = trim($m[1]) . '\\';
        }

        if (preg_match('/^(?:class|interface|trait|enum)\s+(\w+)/m', $content, $m)) {
            return $namespace . $m[1];
        }

        return null;
    }

    public function register(bool $prepend = false): self
    {
        if (!$this->registered) {
            spl_autoload_register([$this, 'loadClass'], true, $prepend);
            $this->registered = true;
        }
        return $this;
    }

    public function unregister(): void
    {
        spl_autoload_unregister([$this, 'loadClass']);
        $this->registered = false;
    }

    public function loadClass(string $class): bool
    {
        if (!isset($this->classMap[$class])) {
            return false;
        }

        $file = $this->classMap[$class];
        if (!file_exists($file)) {
            return false;
        }

        require_once $file;
        return true;
    }

    public function getMap(): array
    {
        return $this->classMap;
    }

    public function dump(): void
    {
        echo "クラスマップ (" . count($this->classMap) . " エントリ):\n";
        foreach ($this->classMap as $class => $file) {
            echo "  {$class}\n    → {$file}\n";
        }
    }
}

// --- 使用例 ---
$loader = new ClassMapAutoloader();

// 手動でマッピングを追加
$loader->addMap([
    'App\Controller\HomeController' => __DIR__ . '/src/Controller/HomeController.php',
    'App\Controller\UserController' => __DIR__ . '/src/Controller/UserController.php',
    'App\Model\User'                => __DIR__ . '/src/Model/User.php',
]);

// またはディレクトリをスキャンして自動生成
// $loader->scanDirectory(__DIR__ . '/src');

$loader->register();
$loader->dump();

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

実行結果:

クラスマップ (3 エントリ):
  App\Controller\HomeController
    → /path/to/src/Controller/HomeController.php
  App\Controller\UserController
    → /path/to/src/Controller/UserController.php
  App\Model\User
    → /path/to/src/Model/User.php

登録ハンドラ数: 1

解説: クラスマップ方式はファイルシステムを検索しないため、PSR-4方式より高速です。本番環境では scanDirectory() でキャッシュファイルを生成しておき、リクエストごとにそれを読み込むパターンがComposerの --optimize-autoloader オプションと同等の効果をもたらします。


spl_autoload_functions() で登録状態を確認する

$functions = spl_autoload_functions();

if ($functions === false || $functions === []) {
    echo "オートローダー未登録\n";
} else {
    foreach ($functions as $i => $fn) {
        $label = match (true) {
            is_string($fn)  => $fn,
            is_array($fn)   => (is_object($fn[0]) ? get_class($fn[0]) : $fn[0]) . '::' . $fn[1],
            $fn instanceof \Closure => 'Closure',
            default         => '(不明)',
        };
        echo "[{$i}] {$label}\n";
    }
}

よくある落とし穴

① spl_autoload() のデフォルト変換はすべて小文字

// クラス名: MyApp\Model\UserProfile
// spl_autoload が探すファイル: myapp/model/userprofile.php(全小文字!)

// ✅ PSR-4では大文字小文字を区別するためカスタムローダーを使う
spl_autoload_register(function (string $class): void {
    $file = __DIR__ . '/src/' . str_replace('\\', '/', $class) . '.php';
    if (file_exists($file)) {
        require $file;
    }
});

② 例外をスローするとFatal Errorになる場合がある

// ❌ オートローダー内で例外をスローすると、
//    クラス参照箇所でFatal Errorになる場合がある(PHP 7.4以前)
spl_autoload_register(function (string $class): void {
    throw new RuntimeException("クラスが見つかりません: {$class}");
});

// ✅ ログに記録してreturnするか、PHP 8.0+でのみ例外を使う
spl_autoload_register(function (string $class): void {
    $file = __DIR__ . '/src/' . str_replace('\\', '/', $class) . '.php';
    if (!file_exists($file)) {
        return; // 次のハンドラに委ねる
    }
    require $file;
});

③ spl_autoload_unregister() に渡すのは登録時と同じcallable

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

// ❌ 新しいクロージャを作っても解除できない
spl_autoload_unregister(function (string $class): void { /* ... */ }); // 失敗

// ✅ 登録時と同じ変数を使う
spl_autoload_unregister($handler); // 成功

④ Composerと共存させるときは prepend を考慮する

// Composerのオートローダーが先に登録されている場合
require 'vendor/autoload.php'; // Composerを先に読み込む

// 独自ローダーをその後に登録(後続)
spl_autoload_register($myLoader);

// または独自ローダーを優先させたい場合
spl_autoload_register($myLoader, true, true); // prepend=true

まとめ

ポイント内容
spl_autoload() の役割小文字化+include_path検索というデフォルトのオートロードハンドラ
現代の実務PSR-4準拠のカスタムハンドラを spl_autoload_register() で登録
複数ハンドラキュー順に実行。prepend=true で先頭挿入可能
解除spl_autoload_unregister() に登録時と同じcallableを渡す
拡張子spl_autoload_extensions() で設定(現代は .php のみ推奨)
Composerとの関係Composerのオートローダーは内部的に spl_autoload_register() を使用

spl_autoload() 単体はレガシー感がありますが、その土台となる spl_autoload_register() / spl_autoload_unregister() の仕組みはComposerも含め現代のPHPオートロードのすべての基盤です。仕組みを理解しておくことで、Composerの設定や複雑なオートロード要件にも自信を持って対応できます。


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

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