17.

PHP 例外処理 (try/catch) 完全ガイド (Throwable / 独自例外 / Laravel)

編集
この記事の要点
  • PHP の例外処理try / catch / finally + throw
  • PHP 7+ で Error クラスが追加 → 共通インターフェース \Throwable を catch すれば両方拾える
  • PHP 8+ で複数 catch catch (E1|E2 $e)、PHP 8+ で $e 省略可
  • 独自例外は class MyException extends Exception
  • Laravel は app/Exceptions/Handler.php でグローバルにハンドリング

基本構文 try / catch / finally

try {
    $result = riskyOperation();
    process($result);
} catch (FileNotFoundException $e) {
    Log::warning("ファイルなし: " . $e->getMessage());
} catch (DatabaseException $e) {
    Log::error("DB エラー: " . $e->getMessage());
    throw $e;   // 再 throw
} catch (Exception $e) {
    Log::error("予期せぬエラー", ['exception' => $e]);
} finally {
    cleanup();  // 例外発生有無に関わらず必ず実行
}

throw 文と Exception クラス

// 基本
throw new Exception("エラーメッセージ");

// メッセージ + コード + 前の例外
throw new Exception("ユーザー保存失敗", 500, $previous);

// よく使う SPL 例外
throw new InvalidArgumentException("name は必須");
throw new RuntimeException("接続に失敗しました");
throw new LogicException("プログラムの論理エラー");
throw new OutOfBoundsException("配列の範囲外");
throw new UnexpectedValueException("予期しない値: $x");
throw new DomainException("値が定義域外");

// PHP 8+ は throw が「式」になり一行で書ける
$user = User::find($id) ?? throw new NotFoundException("user $id");

Exception の階層

Throwable (interface)              ★ PHP 7+
├── Exception                      ユーザー例外の基底
│   ├── RuntimeException           実行時エラー
│   │   ├── OutOfBoundsException
│   │   ├── UnexpectedValueException
│   │   └── PDOException           DB エラー
│   ├── LogicException             プログラムの論理エラー
│   │   ├── InvalidArgumentException
│   │   ├── DomainException
│   │   ├── LengthException
│   │   └── OutOfRangeException
│   ├── ErrorException             set_error_handler で変換
│   └── (ユーザー定義例外)
└── Error                          ★ PHP 7+ システムエラー
    ├── TypeError                  型エラー
    ├── ValueError                 ★ PHP 8+ 値エラー
    ├── ArgumentCountError         引数不足
    ├── ArithmeticError
    │   └── DivisionByZeroError
    ├── AssertionError
    └── ParseError                 構文エラー

Throwable で全部キャッチ (PHP 7+)

PHP 7 以前はパースエラーや TypeError は catch できませんでしたが、PHP 7+ では \Throwable インターフェースで Exception と Error を共通キャッチできます。

try {
    $result = mightFail();
} catch (\Throwable $t) {
    // Exception も Error も両方拾う
    Log::critical("最終手段", [
        'class' => get_class($t),
        'message' => $t->getMessage(),
        'file' => $t->getFile(),
        'line' => $t->getLine(),
        'trace' => $t->getTraceAsString(),
    ]);
    throw $t;
}

複数の例外を 1 つの catch (PHP 8+)

try {
    sendEmail();
} catch (NetworkException | TimeoutException | SmtpException $e) {
    // ネットワーク系を一括ハンドリング
    retry();
}

// PHP 8+ は $e 不要なら省略可
try {
    cleanup();
} catch (Exception) {
    // 黙って無視 (基本やめましょう)
}

独自例外クラス

namespace App\Exceptions;

class UserNotFoundException extends \RuntimeException
{
    private int $userId;

    public function __construct(int $userId, ?\Throwable $previous = null)
    {
        $this->userId = $userId;
        parent::__construct("User #$userId not found", 404, $previous);
    }

    public function getUserId(): int
    {
        return $this->userId;
    }
}

// 使用
try {
    $user = User::find($id);
    if (!$user) {
        throw new UserNotFoundException($id);
    }
} catch (UserNotFoundException $e) {
    abort(404, "User {$e->getUserId()} not found");
}

エラーから例外への変換

PHP の警告 (E_WARNING) や通知 (E_NOTICE) は通常 catch できませんが、set_error_handler で変換できます。

// アプリ起動時に登録
set_error_handler(function($severity, $message, $file, $line) {
    if (!(error_reporting() & $severity)) {
        return false;
    }
    throw new \ErrorException($message, 0, $severity, $file, $line);
});

// これで file_get_contents の警告も例外として扱える
try {
    $data = file_get_contents("does-not-exist.txt");
} catch (\ErrorException $e) {
    Log::warning("ファイル取得失敗: " . $e->getMessage());
    $data = "";
}

finally の使いどころ

$fh = fopen("data.txt", "r");
try {
    process(fread($fh, 1024));
} finally {
    fclose($fh);   // 例外発生しても必ず閉じる
}

// DB トランザクション
$db->beginTransaction();
try {
    $db->insert(...);
    $db->insert(...);
    $db->commit();
} catch (\Throwable $e) {
    $db->rollback();
    throw $e;
}

// Laravel ヘルパ (上記の簡略版)
DB::transaction(function () {
    User::create(...);
    Order::create(...);
});

Laravel での例外ハンドリング

// app/Exceptions/Handler.php (Laravel 10 以前)
namespace App\Exceptions;

class Handler extends ExceptionHandler
{
    public function register(): void
    {
        // 特定例外をログから除外
        $this->dontReport([
            UserNotFoundException::class,
        ]);

        // カスタムレンダリング
        $this->renderable(function (UserNotFoundException $e, $request) {
            if ($request->expectsJson()) {
                return response()->json(['error' => $e->getMessage()], 404);
            }
            return response()->view('errors.user-not-found', [], 404);
        });

        // 共通のロギング
        $this->reportable(function (\Throwable $e) {
            Sentry::captureException($e);
        });
    }
}

// Laravel 11+ は bootstrap/app.php で
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (UserNotFoundException $e) {
        return response()->json(['error' => 'not found'], 404);
    });
})

アンチパターン: グローバル try/catch (Pokemon catch)

// ❌ 全部捕まえて無視
try {
    doEverything();
} catch (\Throwable $e) {
    // 何もしない or echo "Error"; だけ
}
// → バグが隠れる、デバッグ不能

// ❌ catch して即 echo
try {
    saveUser($data);
} catch (\Exception $e) {
    echo $e->getMessage();   // ユーザーに内部エラー暴露 + ログに残らない
}

// ✅ ログを出す + 上位に再 throw
try {
    saveUser($data);
} catch (\Throwable $e) {
    Log::error("ユーザー保存失敗", ['exception' => $e, 'data' => $data]);
    throw $e;  // 上位 (グローバルハンドラ) で適切に処理
}

例外設計のベストプラクティス

  • 「想定済の異常系」だけ例外。普通の制御フローに使わない (性能劣化)
  • 適切な粒度の独自例外を定義し、catch する側で分岐できるように
  • 例外の元情報を失わない → 再 throw するときは throw new Wrapper($msg, 0, $previous)
  • finally でリソース解放 (DB / ファイル / ロック)
  • 本番のレスポンスに内部エラー詳細を出さない (情報漏洩)

FAQ

Q: @ で警告を抑制したい
A: @file_get_contents(...) は警告を抑制しますが、デバッグ困難 + 性能劣化。try/catch + set_error_handler 推奨。

Q: ExceptionRuntimeException どちらを継承?
A: 一般的なアプリ層は \RuntimeException 系、引数バリデーションは \InvalidArgumentException、論理矛盾は \LogicException がセオリー。

Q: 例外と戻り値、どちらでエラーを表現?
A: 異常系は例外 (例: DB 接続失敗)、業務ロジックの結果は戻り値 (例: ユーザー認証失敗) が定石。Result 型風に ['ok' => bool, 'data' => ...] を返す設計もアリ。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 基本的なルール
  2. 変数
  3. 演算子
  4. 標準ライブラリ
  5. 外部ライブラリ
  6. 制御構文
  7. リスト(配列)
  8. タプル
  9. セット
  10. 辞書(dict)
  11. クラスとメソッド
  12. 継承の概念と必要性
  13. 継承の構文
  14. コンストラクタ
  15. cookieの値の設定と取得
  16. 例外処理
  17. 例外を文字列で出力する方法
  18. httpリクエスト(curl)をする方法
  19. Responseオブジェクトの中身の確認
  20. 変数が空かどうか判定する方法
  21. タイムゾーンの設定と現在日時の取得と文字列化
  22. シングルクォーテーションとダブルクォーテーションの違い

最近更新/作成されたページ