タイトル: 例外処理
SEOタイトル: PHP 例外処理 (try/catch) 完全ガイド (Throwable / 独自例外 / Laravel)
| この記事の要点 |
|
基本構文 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: Exception と RuntimeException どちらを継承?
A: 一般的なアプリ層は \RuntimeException 系、引数バリデーションは \InvalidArgumentException、論理矛盾は \LogicException がセオリー。
Q: 例外と戻り値、どちらでエラーを表現?
A: 異常系は例外 (例: DB 接続失敗)、業務ロジックの結果は戻り値 (例: ユーザー認証失敗) が定石。Result 型風に ['ok' => bool, 'data' => ...] を返す設計もアリ。