[PHP]restore_exception_handler関数の使い方を徹底解説!例外ハンドラーの復元

PHP

はじめに

PHPで例外処理をカスタマイズする際、一時的に例外ハンドラーを変更してから元に戻したいことがよくあります。そんな時に使うのがrestore_exception_handler関数です。

restore_exception_handler関数は、set_exception_handler()で設定したカスタム例外ハンドラーを解除して、以前のハンドラーに戻す機能を提供します。この記事では、restore_exception_handler関数の基本から実践的な使い方まで、詳しく解説していきます。

restore_exception_handler関数とは?

restore_exception_handler関数は、以前の例外ハンドラーを復元する関数です。set_exception_handler()でカスタムハンドラーを設定した後、元の状態に戻すために使用します。

基本的な構文

<?php
restore_exception_handler(): bool
?>
  • 引数: なし
  • 戻り値: 常にtrue

最もシンプルな使用例

<?php
// カスタム例外ハンドラーを設定
set_exception_handler(function($exception) {
    echo "カスタムハンドラー: " . $exception->getMessage() . "\n";
});

throw new Exception("例外1");  // カスタムハンドラーが処理

// 例外ハンドラーを元に戻す
restore_exception_handler();

throw new Exception("例外2");  // デフォルトハンドラーが処理(Fatal error)
?>

例外ハンドラースタックの仕組み

PHPは例外ハンドラーもスタック構造で管理しています:

<?php
// ハンドラー1を設定
set_exception_handler(function($exception) {
    echo "ハンドラー1: " . $exception->getMessage() . "\n";
});

// ハンドラー2を設定(ハンドラー1の上に積まれる)
set_exception_handler(function($exception) {
    echo "ハンドラー2: " . $exception->getMessage() . "\n";
});

throw new Exception("テスト");  // ハンドラー2が処理

// 最新のハンドラー(ハンドラー2)を削除
restore_exception_handler();

throw new Exception("テスト2");  // ハンドラー1が処理
?>

実践的な使用例

1. 特定の処理だけカスタムハンドラーを使用

<?php
/**
 * 一時的に例外ハンドラーを変更して処理を実行
 */
function executeWithCustomHandler($callback, $handler) {
    // カスタムハンドラーを設定
    set_exception_handler($handler);

    try {
        $result = $callback();
        return $result;
    } catch (Throwable $e) {
        // ハンドラー内でキャッチされなかった場合
        throw $e;
    } finally {
        // 必ず元に戻す
        restore_exception_handler();
    }
}

// グローバルハンドラー
set_exception_handler(function($exception) {
    echo "[グローバル] " . $exception->getMessage() . "\n";
});

echo "=== 通常の例外 ===\n";
throw new Exception("グローバルハンドラーで処理");

echo "\n=== カスタムハンドラー内 ===\n";
executeWithCustomHandler(
    function() {
        throw new Exception("カスタムハンドラーで処理");
    },
    function($exception) {
        echo "[カスタム] " . $exception->getMessage() . "\n";
        echo "  詳細情報を表示\n";
        echo "  ファイル: " . $exception->getFile() . "\n";
        echo "  行: " . $exception->getLine() . "\n";
    }
);

echo "\n=== 復元後の例外 ===\n";
throw new Exception("再びグローバルハンドラーで処理");
?>

2. 例外の詳細ログ記録

<?php
/**
 * 例外を詳細にログ記録するクラス
 */
class DetailedExceptionLogger {
    private $logFile;
    private $exceptions = [];

    public function __construct($logFile = null) {
        $this->logFile = $logFile ?? sys_get_temp_dir() . '/exceptions.log';
    }

    /**
     * 詳細ログを有効にして処理を実行
     */
    public function execute($callback) {
        $this->exceptions = [];

        // カスタム例外ハンドラーを設定
        set_exception_handler(function($exception) {
            $this->logException($exception);
        });

        try {
            $result = $callback();
            return $result;
        } finally {
            // ハンドラーを復元
            restore_exception_handler();
        }
    }

    /**
     * 例外を詳細にログ記録
     */
    private function logException($exception) {
        $details = [
            'time' => date('Y-m-d H:i:s'),
            'class' => get_class($exception),
            'message' => $exception->getMessage(),
            'code' => $exception->getCode(),
            'file' => $exception->getFile(),
            'line' => $exception->getLine(),
            'trace' => $exception->getTraceAsString()
        ];

        $this->exceptions[] = $details;

        // ログファイルに記録
        $logEntry = sprintf(
            "[%s] %s: %s\nFile: %s:%d\nTrace:\n%s\n%s\n",
            $details['time'],
            $details['class'],
            $details['message'],
            $details['file'],
            $details['line'],
            $details['trace'],
            str_repeat('-', 80)
        );

        file_put_contents($this->logFile, $logEntry, FILE_APPEND);

        // 画面にも表示
        echo "╔══════════════════════════════════════╗\n";
        echo "║       例外が発生しました             ║\n";
        echo "╠══════════════════════════════════════╣\n";
        printf("║ クラス: %-28s║\n", $details['class']);
        printf("║ メッセージ: %-24s║\n", substr($details['message'], 0, 24));
        printf("║ ファイル: %-26s║\n", basename($details['file']));
        printf("║ 行番号: %-28d║\n", $details['line']);
        echo "╚══════════════════════════════════════╝\n";
    }

    /**
     * 記録された例外を取得
     */
    public function getExceptions() {
        return $this->exceptions;
    }

    /**
     * ログファイルパスを取得
     */
    public function getLogFile() {
        return $this->logFile;
    }
}

// 使用例
$logger = new DetailedExceptionLogger('./exceptions.log');

try {
    $logger->execute(function() {
        // 何か処理
        throw new RuntimeException("データベース接続エラー");
    });
} catch (Throwable $e) {
    // ハンドラーで処理済み
}

echo "\nログファイル: " . $logger->getLogFile() . "\n";
?>

3. 環境別の例外処理

<?php
/**
 * 環境に応じた例外ハンドラーの切り替え
 */
class EnvironmentExceptionHandler {
    private $environment;

    const ENV_PRODUCTION = 'production';
    const ENV_DEVELOPMENT = 'development';
    const ENV_TESTING = 'testing';

    public function __construct($environment = self::ENV_PRODUCTION) {
        $this->environment = $environment;
    }

    /**
     * 環境に応じたハンドラーを有効化
     */
    public function enable() {
        switch ($this->environment) {
            case self::ENV_DEVELOPMENT:
                $handler = [$this, 'handleDevelopment'];
                break;
            case self::ENV_TESTING:
                $handler = [$this, 'handleTesting'];
                break;
            case self::ENV_PRODUCTION:
            default:
                $handler = [$this, 'handleProduction'];
                break;
        }

        set_exception_handler($handler);
    }

    /**
     * ハンドラーを無効化
     */
    public function disable() {
        restore_exception_handler();
    }

    /**
     * 本番環境用ハンドラー(詳細を隠す)
     */
    private function handleProduction($exception) {
        // ログに詳細を記録
        error_log(sprintf(
            "[EXCEPTION] %s: %s in %s:%d",
            get_class($exception),
            $exception->getMessage(),
            $exception->getFile(),
            $exception->getLine()
        ));

        // ユーザーには簡潔なメッセージのみ
        http_response_code(500);
        echo "申し訳ございません。システムエラーが発生しました。\n";
        echo "エラーID: " . uniqid() . "\n";
    }

    /**
     * 開発環境用ハンドラー(詳細を表示)
     */
    private function handleDevelopment($exception) {
        echo "╔══════════════════════════════════════════════════╗\n";
        echo "║              開発環境エラー情報                  ║\n";
        echo "╠══════════════════════════════════════════════════╣\n";
        echo "║ クラス: " . get_class($exception) . "\n";
        echo "║ メッセージ: " . $exception->getMessage() . "\n";
        echo "║ ファイル: " . $exception->getFile() . ":" . $exception->getLine() . "\n";
        echo "╠══════════════════════════════════════════════════╣\n";
        echo "║ スタックトレース:\n";
        echo $exception->getTraceAsString() . "\n";
        echo "╚══════════════════════════════════════════════════╝\n";
    }

    /**
     * テスト環境用ハンドラー(例外を記録)
     */
    private function handleTesting($exception) {
        // テスト用ログに記録
        $logEntry = json_encode([
            'class' => get_class($exception),
            'message' => $exception->getMessage(),
            'file' => $exception->getFile(),
            'line' => $exception->getLine()
        ]);

        file_put_contents('/tmp/test_exceptions.log', $logEntry . "\n", FILE_APPEND);

        echo "[TEST] Exception logged: " . $exception->getMessage() . "\n";
    }
}

// 使用例
echo "=== 本番環境モード ===\n";
$prodHandler = new EnvironmentExceptionHandler(EnvironmentExceptionHandler::ENV_PRODUCTION);
$prodHandler->enable();
throw new Exception("本番環境エラー");

$prodHandler->disable();

echo "\n=== 開発環境モード ===\n";
$devHandler = new EnvironmentExceptionHandler(EnvironmentExceptionHandler::ENV_DEVELOPMENT);
$devHandler->enable();
throw new Exception("開発環境エラー");
?>

4. 例外の変換と再スロー

<?php
/**
 * 例外を別の型に変換
 */
class ExceptionTransformer {
    private $transformMap = [];

    /**
     * 変換ルールを追加
     */
    public function addTransform($fromClass, $toClass, $messagePrefix = '') {
        $this->transformMap[$fromClass] = [
            'class' => $toClass,
            'prefix' => $messagePrefix
        ];
    }

    /**
     * 変換ハンドラーを有効にして処理を実行
     */
    public function execute($callback) {
        set_exception_handler(function($exception) {
            $this->handleException($exception);
        });

        try {
            $result = $callback();
            restore_exception_handler();
            return $result;
        } catch (Throwable $e) {
            restore_exception_handler();
            throw $e;
        }
    }

    /**
     * 例外を処理(変換または再スロー)
     */
    private function handleException($exception) {
        $exceptionClass = get_class($exception);

        if (isset($this->transformMap[$exceptionClass])) {
            $transform = $this->transformMap[$exceptionClass];
            $newClass = $transform['class'];
            $prefix = $transform['prefix'];

            $message = $prefix . $exception->getMessage();
            $newException = new $newClass($message, $exception->getCode(), $exception);

            echo "例外を変換: {$exceptionClass} → {$newClass}\n";
            echo "メッセージ: {$message}\n";
        } else {
            echo "変換なし: {$exceptionClass}\n";
            echo "メッセージ: " . $exception->getMessage() . "\n";
        }
    }
}

// カスタム例外クラス
class DatabaseException extends Exception {}
class ValidationException extends Exception {}
class ApplicationException extends Exception {}

// 使用例
$transformer = new ExceptionTransformer();
$transformer->addTransform(
    RuntimeException::class,
    ApplicationException::class,
    '[APP] '
);
$transformer->addTransform(
    InvalidArgumentException::class,
    ValidationException::class,
    '[VALIDATION] '
);

$transformer->execute(function() {
    throw new RuntimeException("データベースエラー");
});

echo "\n";

$transformer->execute(function() {
    throw new InvalidArgumentException("無効な引数");
});
?>

5. 例外のキャプチャとテスト

<?php
/**
 * テスト用に例外をキャプチャ
 */
class ExceptionCapture {
    private $captured = [];
    private $active = false;

    /**
     * キャプチャを開始
     */
    public function start() {
        $this->captured = [];
        $this->active = true;

        set_exception_handler(function($exception) {
            $this->captured[] = [
                'class' => get_class($exception),
                'message' => $exception->getMessage(),
                'code' => $exception->getCode(),
                'file' => $exception->getFile(),
                'line' => $exception->getLine(),
                'time' => microtime(true)
            ];

            // 例外の伝播を止めずに記録のみ
            echo "[CAPTURED] " . get_class($exception) . ": " . $exception->getMessage() . "\n";
        });
    }

    /**
     * キャプチャを停止
     */
    public function stop() {
        if ($this->active) {
            restore_exception_handler();
            $this->active = false;
        }
    }

    /**
     * キャプチャされた例外を取得
     */
    public function getExceptions() {
        return $this->captured;
    }

    /**
     * 例外がキャプチャされたかチェック
     */
    public function hasExceptions() {
        return !empty($this->captured);
    }

    /**
     * 特定のクラスの例外があるかチェック
     */
    public function hasExceptionOfType($className) {
        foreach ($this->captured as $exception) {
            if ($exception['class'] === $className) {
                return true;
            }
        }
        return false;
    }

    /**
     * 特定のメッセージを含む例外があるかチェック
     */
    public function hasExceptionWithMessage($message) {
        foreach ($this->captured as $exception) {
            if (strpos($exception['message'], $message) !== false) {
                return true;
            }
        }
        return false;
    }

    /**
     * 例外数を取得
     */
    public function count() {
        return count($this->captured);
    }
}

// テスト例
function testExceptionHandling() {
    $capture = new ExceptionCapture();

    echo "=== テスト開始 ===\n";
    $capture->start();

    // テスト1: RuntimeException
    try {
        throw new RuntimeException("テストエラー1");
    } catch (Throwable $e) {
        // キャプチャされる
    }

    // テスト2: InvalidArgumentException
    try {
        throw new InvalidArgumentException("テストエラー2");
    } catch (Throwable $e) {
        // キャプチャされる
    }

    $capture->stop();

    // アサーション
    echo "\n=== テスト結果 ===\n";
    echo "例外数: " . $capture->count() . "\n";
    echo "RuntimeExceptionあり: " . 
         ($capture->hasExceptionOfType(RuntimeException::class) ? "✓" : "✗") . "\n";
    echo "'テストエラー1'を含む: " . 
         ($capture->hasExceptionWithMessage("テストエラー1") ? "✓" : "✗") . "\n";

    // 詳細表示
    echo "\n=== キャプチャされた例外 ===\n";
    foreach ($capture->getExceptions() as $i => $ex) {
        echo ($i + 1) . ". {$ex['class']}: {$ex['message']}\n";
    }
}

testExceptionHandling();
?>

6. チェーン化された例外ハンドラー

<?php
/**
 * 複数のハンドラーをチェーン実行
 */
class ExceptionHandlerChain {
    private $handlers = [];

    /**
     * ハンドラーを追加
     */
    public function addHandler(callable $handler) {
        $this->handlers[] = $handler;
        return $this;
    }

    /**
     * チェーンを有効化
     */
    public function enable() {
        set_exception_handler(function($exception) {
            foreach ($this->handlers as $handler) {
                try {
                    $handler($exception);
                } catch (Throwable $e) {
                    // ハンドラー内のエラーは無視
                    error_log("Handler error: " . $e->getMessage());
                }
            }
        });
    }

    /**
     * チェーンを無効化
     */
    public function disable() {
        restore_exception_handler();
    }
}

// 使用例
$chain = new ExceptionHandlerChain();

// ハンドラー1: ログ記録
$chain->addHandler(function($exception) {
    echo "[LOG] " . get_class($exception) . ": " . $exception->getMessage() . "\n";
    file_put_contents('/tmp/exceptions.log', 
        date('Y-m-d H:i:s') . " - " . $exception->getMessage() . "\n", 
        FILE_APPEND);
});

// ハンドラー2: メール通知(例)
$chain->addHandler(function($exception) {
    echo "[MAIL] 管理者にメール送信(例)\n";
    // mail('admin@example.com', 'Exception', $exception->getMessage());
});

// ハンドラー3: 統計記録
$chain->addHandler(function($exception) {
    echo "[STATS] 統計情報を更新\n";
});

$chain->enable();

throw new Exception("チェーンテスト例外");
?>

7. グレースフルシャットダウン

<?php
/**
 * 例外発生時にクリーンアップを実行
 */
class GracefulShutdownHandler {
    private $cleanupCallbacks = [];

    /**
     * クリーンアップコールバックを追加
     */
    public function addCleanup(callable $callback, $description = '') {
        $this->cleanupCallbacks[] = [
            'callback' => $callback,
            'description' => $description
        ];
    }

    /**
     * ハンドラーを有効化
     */
    public function enable() {
        set_exception_handler(function($exception) {
            echo "╔══════════════════════════════════════╗\n";
            echo "║   致命的エラーが発生しました         ║\n";
            echo "╠══════════════════════════════════════╣\n";
            echo "║ " . $exception->getMessage() . "\n";
            echo "╠══════════════════════════════════════╣\n";
            echo "║   クリーンアップを実行中...          ║\n";
            echo "╚══════════════════════════════════════╝\n\n";

            $this->performCleanup();

            echo "\nクリーンアップ完了。プログラムを終了します。\n";
        });
    }

    /**
     * ハンドラーを無効化
     */
    public function disable() {
        restore_exception_handler();
    }

    /**
     * クリーンアップを実行
     */
    private function performCleanup() {
        foreach ($this->cleanupCallbacks as $cleanup) {
            $desc = $cleanup['description'] ?: 'クリーンアップ処理';
            echo "実行中: {$desc}...";

            try {
                $cleanup['callback']();
                echo " ✓\n";
            } catch (Throwable $e) {
                echo " ✗ (エラー: {$e->getMessage()})\n";
            }
        }
    }
}

// 使用例
$shutdown = new GracefulShutdownHandler();

// クリーンアップタスクを登録
$shutdown->addCleanup(function() {
    // データベース接続を閉じる
    echo "データベース接続をクローズ";
}, 'データベース接続のクローズ');

$shutdown->addCleanup(function() {
    // 一時ファイルを削除
    echo "一時ファイルを削除";
}, '一時ファイルの削除');

$shutdown->addCleanup(function() {
    // ロックを解放
    echo "ロックファイルを解放";
}, 'ロックの解放');

$shutdown->enable();

// 処理中に例外が発生
throw new Exception("予期しないエラーが発生しました");
?>

よくある問題と解決策

問題1: restore忘れによるハンドラーのリーク

<?php
// ❌ 悪い例
function badExample() {
    set_exception_handler(function($e) {
        echo "カスタムハンドラー\n";
    });

    // restore_exception_handler()を呼び忘れ!
}

// ✅ 良い例: finallyで確実に復元
function goodExample() {
    set_exception_handler(function($e) {
        echo "カスタムハンドラー\n";
    });

    try {
        // 処理
    } finally {
        restore_exception_handler();
    }
}
?>

問題2: 例外ハンドラー内での例外

<?php
// ハンドラー内でエラーが発生すると致命的
set_exception_handler(function($exception) {
    // ✅ try-catchで保護
    try {
        // ログ記録などの処理
        throw new Exception("ハンドラー内エラー");
    } catch (Throwable $e) {
        error_log("Handler error: " . $e->getMessage());
    }
});
?>

まとめ

restore_exception_handler関数のポイントをおさらいしましょう:

  1. set_exception_handler()で設定したハンドラーを削除
  2. スタック構造で管理されている
  3. finallyブロックで確実に復元するのがベストプラクティス
  4. 環境別の例外処理に便利
  5. 複数回呼び出しても問題ない
  6. テスト用の例外キャプチャに最適
  7. 必ず対応するset_exception_handler()とペアで使用

restore_exception_handler関数を適切に使用することで、柔軟で保守性の高い例外処理を実現できます。特にfinallyブロックと組み合わせることで、確実にハンドラーを復元できます!

参考リンク


この記事が役に立ったら、ぜひシェアしてください!PHPに関する他の記事もお楽しみに。

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