はじめに
spl_autoload_register() でオートロードハンドラを登録したあと、状況によってはそのハンドラを解除したい場面があります。テストのモック差し替え・プラグインのアンロード・特定フェーズでの無効化など、用途はさまざまです。
spl_autoload_unregister() は、登録済みのオートロードハンドラをキューから取り除く関数です。シンプルに見えますが、「登録時と完全に同一のcallableを渡す」 という制約が最大のポイントであり、よく落とし穴になります。
本記事では関数の仕様から、クロージャ・静的メソッド・インスタンスメソッドそれぞれの解除パターン、そして安全な管理設計まで詳しく解説します。
関数の基本情報
| 項目 | 内容 |
|---|---|
| 関数名 | spl_autoload_unregister() |
| 利用可能バージョン | PHP 5.1以降 |
| 所属 | SPL(Standard PHP Library) |
| 戻り値 | bool(成功時true、失敗時false) |
| 拡張機能 | SPL(デフォルトで有効) |
シグネチャ
spl_autoload_unregister(callable $callback): bool
パラメータ
| パラメータ | 型 | 説明 |
|---|---|---|
$callback | callable | 解除するハンドラ。登録時と完全に同一のcallableを渡す必要がある |
戻り値
| 状況 | 戻り値 |
|---|---|
| 解除成功 | true |
| 指定のcallableがキューに存在しない | false |
「同一のcallable」とは何か
これが spl_autoload_unregister() を使う上で最も重要な概念です。
// 文字列(関数名)→ 同じ文字列なら同一
spl_autoload_register('myLoader');
spl_autoload_unregister('myLoader'); // ✅ 同じ文字列
// 静的メソッド配列 → クラス名とメソッド名が同じなら同一
spl_autoload_register(['MyLoader', 'load']);
spl_autoload_unregister(['MyLoader', 'load']); // ✅ 同じ配列内容
// インスタンスメソッド配列 → 同じオブジェクトインスタンスでなければ不一致
$obj = new MyLoader();
spl_autoload_register([$obj, 'load']);
spl_autoload_unregister([$obj, 'load']); // ✅ 同じインスタンス
spl_autoload_unregister([new MyLoader(), 'load']); // ❌ 別インスタンス
// クロージャ → 同じ変数(同一インスタンス)でなければ不一致
$fn = function (string $class): void { /* ... */ };
spl_autoload_register($fn);
spl_autoload_unregister($fn); // ✅ 同じ変数
spl_autoload_unregister(function (string $class): void { /* ... */ }); // ❌ 別インスタンス
実践サンプル集(PHP 8.x対応)
サンプル1:各callable形式の登録と解除
4種類すべての登録形式での解除を確認します。
<?php
declare(strict_types=1);
function countHandlers(): int
{
return count(spl_autoload_functions() ?: []);
}
// --- ① 関数名文字列 ---
function namedLoader(string $class): void {}
spl_autoload_register('namedLoader');
echo "登録後: " . countHandlers() . "\n"; // 1
$result = spl_autoload_unregister('namedLoader');
echo "解除: " . ($result ? 'OK' : 'NG') . " / 残: " . countHandlers() . "\n"; // OK / 0
// --- ② 静的メソッド ---
class StaticLoader
{
public static function load(string $class): void {}
}
spl_autoload_register(['StaticLoader', 'load']);
echo "\n登録後: " . countHandlers() . "\n"; // 1
$result = spl_autoload_unregister(['StaticLoader', 'load']); // 同じ配列内容でOK
echo "解除: " . ($result ? 'OK' : 'NG') . " / 残: " . countHandlers() . "\n"; // OK / 0
// --- ③ インスタンスメソッド ---
class InstanceLoader
{
public function load(string $class): void {}
}
$loader = new InstanceLoader();
spl_autoload_register([$loader, 'load']);
echo "\n登録後: " . countHandlers() . "\n"; // 1
// 別インスタンスでは解除できない
$result = spl_autoload_unregister([new InstanceLoader(), 'load']);
echo "別インスタンスで解除: " . ($result ? 'OK' : 'NG') . " / 残: " . countHandlers() . "\n"; // NG / 1
// 同じインスタンスなら解除できる
$result = spl_autoload_unregister([$loader, 'load']);
echo "同インスタンスで解除: " . ($result ? 'OK' : 'NG') . " / 残: " . countHandlers() . "\n"; // OK / 0
// --- ④ クロージャ ---
$closure = function (string $class): void {};
spl_autoload_register($closure);
echo "\n登録後: " . countHandlers() . "\n"; // 1
// 新しいクロージャリテラルでは解除できない
$result = spl_autoload_unregister(function (string $class): void {});
echo "別クロージャで解除: " . ($result ? 'OK' : 'NG') . " / 残: " . countHandlers() . "\n"; // NG / 1
// 同じ変数なら解除できる
$result = spl_autoload_unregister($closure);
echo "同クロージャで解除: " . ($result ? 'OK' : 'NG') . " / 残: " . countHandlers() . "\n"; // OK / 0
実行結果:
登録後: 1
解除: OK / 残: 0
登録後: 1
解除: OK / 残: 0
登録後: 1
別インスタンスで解除: NG / 残: 1
同インスタンスで解除: OK / 残: 0
登録後: 1
別クロージャで解除: NG / 残: 1
同クロージャで解除: OK / 残: 0
解説: 最も注意が必要なのはクロージャとインスタンスメソッドです。どちらも同一のオブジェクトインスタンスでなければ解除できません。登録時の変数を必ず保持しておく必要があります。
サンプル2:ハンドラを名前で管理する登録・解除マネージャー
クロージャの同一性問題を根本から解決する管理クラスです。
<?php
declare(strict_types=1);
/**
* オートロードハンドラを名前付きで管理するクラス
* クロージャの同一性問題を内部で吸収する
*/
class NamedAutoloadManager
{
/** @var array<string, callable> 名前 → callable */
private array $handlers = [];
/**
* 名前付きでハンドラを登録
*/
public function register(
string $name,
callable $handler,
bool $prepend = false
): bool {
if (isset($this->handlers[$name])) {
// 既存の同名ハンドラを先に解除
$this->unregister($name);
}
$result = spl_autoload_register($handler, true, $prepend);
if ($result) {
$this->handlers[$name] = $handler;
echo "[Manager] 登録: {$name}\n";
}
return $result;
}
/**
* 名前でハンドラを解除
*/
public function unregister(string $name): bool
{
if (!isset($this->handlers[$name])) {
echo "[Manager] 未登録: {$name}\n";
return false;
}
$result = spl_autoload_unregister($this->handlers[$name]);
if ($result) {
unset($this->handlers[$name]);
echo "[Manager] 解除: {$name}\n";
}
return $result;
}
/**
* 登録済みハンドラ名一覧
*/
public function names(): array
{
return array_keys($this->handlers);
}
/**
* 全ハンドラを解除
*/
public function unregisterAll(): void
{
foreach (array_keys($this->handlers) as $name) {
$this->unregister($name);
}
}
public function isRegistered(string $name): bool
{
return isset($this->handlers[$name]);
}
public function count(): int
{
return count($this->handlers);
}
}
// --- 使用例 ---
$manager = new NamedAutoloadManager();
$manager->register('app', function (string $class): void {
$file = __DIR__ . '/src/' . str_replace('\\', '/', $class) . '.php';
if (is_file($file)) {
require $file;
}
});
$manager->register('vendor', function (string $class): void {
$file = __DIR__ . '/vendor/' . str_replace('\\', '/', $class) . '.php';
if (is_file($file)) {
require $file;
}
});
$manager->register('debug', function (string $class): void {
echo "[DEBUG] ロード試行: {$class}\n";
}, prepend: true);
echo "\n登録済み: " . implode(', ', $manager->names()) . "\n";
echo "SPLキュー: " . count(spl_autoload_functions() ?: []) . "件\n";
// 名前で解除(callableを保持する必要なし)
$manager->unregister('debug');
echo "\n解除後: " . implode(', ', $manager->names()) . "\n";
// 全解除
$manager->unregisterAll();
echo "全解除後のSPLキュー: " . count(spl_autoload_functions() ?: []) . "件\n";
実行結果:
[Manager] 登録: app
[Manager] 登録: vendor
[Manager] 登録: debug
登録済み: app, vendor, debug
SPLキュー: 3件
[Manager] 解除: debug
解除後: app, vendor
[Manager] 解除: app
[Manager] 解除: vendor
全解除後のSPLキュー: 0件
解説: 名前→callableのマッピングを内部で保持するため、呼び出し側はcallableの変数を保持する必要がありません。「名前で解除」という直感的なAPIになります。
サンプル3:テスト用モックの差し替えと後始末
テスト実行中だけハンドラを差し替え、終了後に確実に元に戻すパターンです。
<?php
declare(strict_types=1);
/**
* テスト中のオートロードをモックに差し替えるスコープクラス
*/
class AutoloadMockScope
{
/** @var list<callable> このスコープで登録したハンドラ */
private array $registered = [];
/** @var list<callable> このスコープで一時解除したハンドラ */
private array $suspended = [];
/**
* モックハンドラを先頭に追加
*/
public function addMock(callable $handler): self
{
spl_autoload_register($handler, true, true); // prepend
$this->registered[] = $handler;
return $this;
}
/**
* 指定ハンドラを一時停止(解除してリスト保持)
*/
public function suspend(callable $handler): self
{
if (spl_autoload_unregister($handler)) {
$this->suspended[] = $handler;
}
return $this;
}
/**
* スコープを終了して元に戻す
*/
public function teardown(): void
{
// モックハンドラを解除
foreach ($this->registered as $handler) {
spl_autoload_unregister($handler);
}
$this->registered = [];
// 一時停止したハンドラを復元
foreach ($this->suspended as $handler) {
spl_autoload_register($handler);
}
$this->suspended = [];
}
public function __destruct()
{
// フェイルセーフ:GCのタイミングでも後始末
$this->teardown();
}
/**
* クロージャスコープで自動後始末
*/
public static function run(callable $setup, callable $test): void
{
$scope = new self();
$setup($scope);
try {
$test();
} finally {
$scope->teardown();
}
}
}
// --- 本番用ローダー(事前登録済み想定)---
$productionLoader = function (string $class): void {
$file = __DIR__ . '/src/' . str_replace('\\', '/', $class) . '.php';
if (is_file($file)) {
require $file;
}
};
spl_autoload_register($productionLoader);
echo "テスト前 SPLキュー: " . count(spl_autoload_functions() ?: []) . "件\n";
// --- テスト実行 ---
AutoloadMockScope::run(
setup: function (AutoloadMockScope $scope) use ($productionLoader): void {
// 本番ローダーを一時停止
$scope->suspend($productionLoader);
// モックローダーを追加
$scope->addMock(function (string $class): void {
$file = __DIR__ . '/tests/mocks/' . str_replace('\\', '/', $class) . '.php';
if (is_file($file)) {
require $file;
echo "[MockLoader] {$class}\n";
}
});
},
test: function (): void {
echo "テスト中 SPLキュー: " . count(spl_autoload_functions() ?: []) . "件\n";
// new App\Service\SomeService(); // モックが使われる
}
);
echo "テスト後 SPLキュー: " . count(spl_autoload_functions() ?: []) . "件\n";
実行結果:
テスト前 SPLキュー: 1件
テスト中 SPLキュー: 1件
テスト後 SPLキュー: 1件
解説: suspend() で本番ローダーを一時解除し、モックを先頭に差し込んで、teardown() で確実に元に戻します。try-finally で囲むことで例外時も後始末が保証されます。
サンプル4:解除の成否を検証するガード関数
解除の結果を確認しながら安全に操作するユーティリティです。
<?php
declare(strict_types=1);
/**
* 解除操作を検証付きで行うユーティリティ
*/
final class AutoloadUnregisterGuard
{
/**
* ハンドラをキューから検索して解除する
* 静的メソッドはクラス名文字列で一致検索可能
*/
public static function unregisterByClass(
string $className,
string $methodName = 'load'
): bool {
$handlers = spl_autoload_functions() ?: [];
foreach ($handlers as $handler) {
if (!is_array($handler)) {
continue;
}
$handlerClass = is_object($handler[0])
? get_class($handler[0])
: $handler[0];
if ($handlerClass === $className && $handler[1] === $methodName) {
$result = spl_autoload_unregister($handler);
echo "[Guard] " . ($result ? '✅' : '❌')
. " {$className}::{$methodName} を解除\n";
return $result;
}
}
echo "[Guard] ❌ {$className}::{$methodName} はキューに存在しません\n";
return false;
}
/**
* すべてのクロージャを解除する
*/
public static function unregisterAllClosures(): int
{
$handlers = spl_autoload_functions() ?: [];
$count = 0;
foreach ($handlers as $handler) {
if ($handler instanceof \Closure) {
if (spl_autoload_unregister($handler)) {
$count++;
}
}
}
echo "[Guard] クロージャ {$count}件を解除\n";
return $count;
}
/**
* 関数名のハンドラを解除する
*/
public static function unregisterFunction(string $name): bool
{
$result = spl_autoload_unregister($name);
echo "[Guard] " . ($result ? '✅' : '❌') . " 関数 {$name} を解除\n";
return $result;
}
/**
* キューを完全にクリアする
*/
public static function unregisterAll(): int
{
$handlers = spl_autoload_functions() ?: [];
$count = 0;
foreach ($handlers as $handler) {
if (spl_autoload_unregister($handler)) {
$count++;
}
}
echo "[Guard] 全 {$count}件を解除\n";
return $count;
}
}
// --- セットアップ ---
function globalLoader(string $class): void {}
class AppLoader { public static function load(string $c): void {} }
class TestLoader { public function handle(string $c): void {} }
$closure1 = function (string $class): void {};
$closure2 = function (string $class): void {};
$testObj = new TestLoader();
spl_autoload_register('globalLoader');
spl_autoload_register(['AppLoader', 'load']);
spl_autoload_register([$testObj, 'handle']);
spl_autoload_register($closure1);
spl_autoload_register($closure2);
echo "登録数: " . count(spl_autoload_functions() ?: []) . "件\n\n";
// クラス名で静的メソッドを解除
AutoloadUnregisterGuard::unregisterByClass('AppLoader', 'load');
// 関数名で解除
AutoloadUnregisterGuard::unregisterFunction('globalLoader');
// クロージャを全解除
AutoloadUnregisterGuard::unregisterAllClosures();
echo "\n残: " . count(spl_autoload_functions() ?: []) . "件 (TestLoaderのみ)\n";
実行結果:
登録数: 5件
[Guard] ✅ AppLoader::load を解除
[Guard] ✅ 関数 globalLoader を解除
[Guard] クロージャ 2件を解除
残: 1件 (TestLoaderのみ)
解説: spl_autoload_functions() でキューを走査して条件に合うハンドラを探し、見つかったものを解除します。クロージャ変数を保持していなくても、タイプ別に一括解除できます。
サンプル5:解除をスタックで管理するUNDO機構
解除操作をスタックに積んで巻き戻せる仕組みです。
<?php
declare(strict_types=1);
/**
* オートロード操作をスタックに積んでUNDO可能にする
*/
class AutoloadOperationStack
{
/** @var list<array{op: string, handler: callable}> */
private array $stack = [];
/**
* ハンドラを登録してスタックに積む
*/
public function register(callable $handler, bool $prepend = false): self
{
spl_autoload_register($handler, true, $prepend);
$this->stack[] = ['op' => 'register', 'handler' => $handler];
echo "[Stack] PUSH register (depth=" . count($this->stack) . ")\n";
return $this;
}
/**
* ハンドラを解除してスタックに積む
*/
public function unregister(callable $handler): self
{
if (spl_autoload_unregister($handler)) {
$this->stack[] = ['op' => 'unregister', 'handler' => $handler];
echo "[Stack] PUSH unregister (depth=" . count($this->stack) . ")\n";
}
return $this;
}
/**
* 直前の操作を1つ取り消す
*/
public function undo(): bool
{
if (empty($this->stack)) {
echo "[Stack] スタックが空です\n";
return false;
}
$last = array_pop($this->stack);
if ($last['op'] === 'register') {
// 登録を取り消す → 解除
$result = spl_autoload_unregister($last['handler']);
echo "[Stack] UNDO register → " . ($result ? '解除OK' : '解除NG') . "\n";
return $result;
}
if ($last['op'] === 'unregister') {
// 解除を取り消す → 再登録
$result = spl_autoload_register($last['handler']);
echo "[Stack] UNDO unregister → " . ($result ? '再登録OK' : '再登録NG') . "\n";
return $result;
}
return false;
}
/**
* すべての操作を逆順に取り消す
*/
public function undoAll(): void
{
while (!empty($this->stack)) {
$this->undo();
}
}
public function depth(): int
{
return count($this->stack);
}
}
// --- 使用例 ---
$handlerA = function (string $class): void { /* A */ };
$handlerB = function (string $class): void { /* B */ };
$handlerC = function (string $class): void { /* C */ };
$stack = new AutoloadOperationStack();
echo "=== 操作開始 ===\n";
$stack->register($handlerA);
$stack->register($handlerB);
$stack->register($handlerC);
echo "\nSPLキュー: " . count(spl_autoload_functions() ?: []) . "件\n";
echo "\n=== UNDO ===\n";
$stack->undo(); // C の登録を取り消す
$stack->undo(); // B の登録を取り消す
echo "\nSPLキュー: " . count(spl_autoload_functions() ?: []) . "件\n";
echo "\n=== 残りをすべてUNDO ===\n";
$stack->undoAll();
echo "\nSPLキュー: " . count(spl_autoload_functions() ?: []) . "件\n";
実行結果:
=== 操作開始 ===
[Stack] PUSH register (depth=1)
[Stack] PUSH register (depth=2)
[Stack] PUSH register (depth=3)
SPLキュー: 3件
=== UNDO ===
[Stack] UNDO register → 解除OK
[Stack] UNDO register → 解除OK
SPLキュー: 1件
=== 残りをすべてUNDO ===
[Stack] UNDO register → 解除OK
SPLキュー: 0件
解説: 操作をスタックに積むことで「登録→解除→再登録→解除…」という複雑な操作の流れを追跡し、逆順に巻き戻せます。プラグインシステムや段階的な初期化処理の実装で役立つパターンです。
サンプル6:PHPUnitスタイルのテストクリーンアップトレイト
テスト間でオートロードキューが汚染されないことを保証するトレイトです。
<?php
declare(strict_types=1);
/**
* テストケースでオートロードハンドラを安全に管理するトレイト
*/
trait ManagesAutoloadHandlers
{
/** @var array<string, callable> 名前 → callable */
private array $testHandlers = [];
/** @var list<mixed> setUp時のキュースナップショット */
private array $originalHandlers = [];
/**
* setUp() の先頭で呼ぶ
*/
protected function captureHandlers(): void
{
$this->originalHandlers = spl_autoload_functions() ?: [];
$this->testHandlers = [];
}
/**
* テスト用ハンドラを名前付きで登録
*/
protected function registerTestHandler(
string $name,
callable $handler,
bool $prepend = false
): void {
spl_autoload_register($handler, true, $prepend);
$this->testHandlers[$name] = $handler;
}
/**
* テスト用ハンドラを名前で解除
*/
protected function unregisterTestHandler(string $name): bool
{
if (!isset($this->testHandlers[$name])) {
return false;
}
$result = spl_autoload_unregister($this->testHandlers[$name]);
if ($result) {
unset($this->testHandlers[$name]);
}
return $result;
}
/**
* tearDown() の末尾で呼ぶ
* テスト中に追加された全ハンドラを解除する
*/
protected function cleanupHandlers(): void
{
// 名前付きハンドラを解除
foreach (array_keys($this->testHandlers) as $name) {
$this->unregisterTestHandler($name);
}
// スナップショット後に追加された残りのハンドラも解除
$current = spl_autoload_functions() ?: [];
$origSize = count($this->originalHandlers);
$extra = array_slice($current, $origSize);
foreach ($extra as $handler) {
spl_autoload_unregister($handler);
}
$after = count(spl_autoload_functions() ?: []);
if ($after !== $origSize) {
trigger_error(
"テスト後のハンドラ数({$after})がスナップショット({$origSize})と一致しません",
E_USER_WARNING
);
}
}
}
// --- テストクラスのシミュレーション ---
class IntegrationTest
{
use ManagesAutoloadHandlers;
// 既存のプロダクションローダー(事前登録)
private static $prodHandler;
public static function setUpBeforeClass(): void
{
self::$prodHandler = function (string $class): void {
// 本番のローダーロジック
};
spl_autoload_register(self::$prodHandler);
}
public function setUp(): void
{
$this->captureHandlers(); // スナップショット取得
}
public function tearDown(): void
{
$this->cleanupHandlers(); // テスト中の追加分を解除
}
public function testScenarioA(): void
{
// テスト用モックハンドラを登録
$this->registerTestHandler('mock-a', function (string $class): void {
echo "[MockA] {$class}\n";
}, prepend: true);
echo "シナリオA実行中: " . count(spl_autoload_functions() ?: []) . "件\n";
// テスト処理...
}
public function testScenarioB(): void
{
// 別のテスト(前のテストの影響を受けない)
echo "シナリオB実行中: " . count(spl_autoload_functions() ?: []) . "件\n";
}
}
// --- 実行 ---
IntegrationTest::setUpBeforeClass();
$test = new IntegrationTest();
echo "=== testScenarioA ===\n";
$test->setUp();
$test->testScenarioA();
$test->tearDown();
echo "\n=== testScenarioB ===\n";
$test->setUp();
$test->testScenarioB();
$test->tearDown();
echo "\n最終SPLキュー: " . count(spl_autoload_functions() ?: []) . "件\n";
実行結果:
=== testScenarioA ===
シナリオA実行中: 2件
[mockハンドラ解除]
=== testScenarioB === シナリオB実行中: 1件 最終SPLキュー: 1件
解説: captureHandlers() でスナップショットを取り、cleanupHandlers() でテスト中に追加したハンドラをすべて解除します。各テストが独立したオートロード環境で動くことを保証できます。
よくある落とし穴
① 解除できていないことに気づかない
$fn = function (string $class): void {};
spl_autoload_register($fn);
// ❌ 戻り値を確認しないと解除失敗に気づかない
spl_autoload_unregister(function (string $class): void {}); // false(別インスタンス)
// ✅ 戻り値を確認する
$result = spl_autoload_unregister($fn);
if (!$result) {
throw new RuntimeException('ハンドラの解除に失敗しました');
}
② ループ中のキュー変更に注意
// ❌ ループ中にキューを変更するのは危険
foreach (spl_autoload_functions() ?: [] as $handler) {
spl_autoload_unregister($handler); // ループ中にキーがずれる可能性
}
// ✅ スナップショットを取ってからループする
$handlers = spl_autoload_functions() ?: [];
foreach ($handlers as $handler) {
spl_autoload_unregister($handler);
}
③ __invoke オブジェクトの解除
class InvokableLoader
{
public function __invoke(string $class): void {}
}
$obj = new InvokableLoader();
spl_autoload_register($obj);
// ✅ 同じオブジェクトを渡せば解除できる
spl_autoload_unregister($obj);
// ❌ 新しいインスタンスでは不可
spl_autoload_unregister(new InvokableLoader()); // false
④ spl_autoload 自体の解除
spl_autoload_register('spl_autoload');
// ✅ 文字列で解除できる
spl_autoload_unregister('spl_autoload');
まとめ
| ポイント | 内容 |
|---|---|
| 主な用途 | 登録済みオートロードハンドラの解除 |
| 最重要ルール | 解除には登録時と完全に同一のcallableが必要 |
| 文字列関数名 | 同じ文字列でOK |
| 静的メソッド配列 | 同じクラス名・メソッド名の配列でOK |
| インスタンスメソッド配列 | 同一オブジェクトインスタンスが必要 |
| クロージャ | **同一変数(インスタンス)**が必要 |
| 安全な設計 | 名前付き管理クラス・スナップショット・try-finally |
| 戻り値 | 必ず確認する(falseのとき解除失敗) |
spl_autoload_unregister() はシンプルな関数ですが、「同一のcallable」 という制約がクロージャやインスタンスメソッドで落とし穴になりがちです。登録時のcallableを変数に保持するか、本記事で紹介した名前付き管理クラスを使うことで、安全で確実な解除が実現できます。
PHP 8.x / 執筆時点の最新安定版にて動作確認済み
