PHPの例外処理を完全攻略:try/catchからカスタム例外設計まで

目次

全体像(例外の役割と境界)

例外は「通常フローでは回復できない状態」を呼び出し元へ即時に伝播させ、
適切な境界でロールバック・ログ・ユーザー向けレスポンスに変換するための仕組みです。
PHP 7以降はThrowableが階層の最上位になり、その配下に Exception(ユーザーランド・ライブラリ例外)と Error(型エラー、パース時の致命的エラーなど)が並びます(Throwable, Exception, Error)。
「どの層で握るか(catchするか)」はアプリ境界の設計そのもので、
Webならミドルウェア/フロントコントローラ層、CLIならアプリのエントリポイントが典型です。

1. 基本:try / catch / finally とスローの要点

1.1 基本の骨格

try {
    // 例:DB書き込み、外部API呼び出し…
    $id = $repo->create($payload);
    echo "created: {$id}";
} catch (\DomainException $e) {
    // 業務ルール違反(ユーザー入力の矛盾など)
    http_response_code(400);
    echo "Bad Request: {$e->getMessage()}";
} catch (\Throwable $e) {
    // 想定外:ログして汎用メッセージ
    error_log($e);
    http_response_code(500);
    echo "Internal Server Error";
} finally {
    // ここは常に実行(コネクション解放・一時ファイル削除など)
    $locker->release();
}
  • try/catch/finally の構文は公式にまとまっています(try/catch, finally)。
  • finally は「成功・失敗を問わず走るクリーンアップ」の専用。リソース管理の最後の砦に。

1.2 スローの基本と再スロー

function storeUser(array $input): int {
    if (!filter_var($input['email'] ?? '', FILTER_VALIDATE_EMAIL)) {
        throw new \DomainException('メール形式が不正です');
    }
  
  // 例:throwが式として使える
  $value = $input['key'] ?? throw new \InvalidArgumentException('キーが必要です');

    try {
        return $this->gateway->insert($input);
    } catch (\PDOException $e) {
        // インフラ例外 → ドメインに近い例外へ変換して上へ伝える
        throw new \RuntimeException('永続化に失敗', previous: $e);
    }
}
  • 例外に元例外を連鎖(previousさせると解析が楽になります(Exception::getPrevious)。
  • PHP 8以降は throwとして使えます(条件演算子内など)。

2. 応用:複数 catch、型階層、再試行、トランザクション連携

2.1 複数 catch とユニオンキャッチ

try {
    $client->postJson($url, $body);
} catch (\InvalidArgumentException|\LengthException $e) {
    // 入力バリデーション系をまとめて 400 に
    respondBadRequest($e->getMessage());
} catch (\RuntimeException $e) {
    // 実行時エラーは 500 に
    respondServerError('処理に失敗しました');
}
  • PHP 8+ はユニオン型の catch が使えます(例外の処理)。

2.2 リトライ戦略(再試行)

function withRetry(callable $op, int $max = 3, int $backoffMs = 200) {
    for ($i = 1; $i <= $max; $i++) {
        try { return $op(); }
        catch (\Throwable $e) {
            if ($i === $max) throw $e;
            usleep($backoffMs * 1000);
        }
    }
}
  • ネットワーク系は一時的失敗があり得るので、上位で再試行を包むのが定石。
  • ただし非冪等操作(重複実行で副作用が累積)には要注意。

2.3 トランザクションと例外

$db->beginTransaction();
try {
    $orderId = $orders->create($payload);
    $ledger->record($orderId);
    $db->commit();
} catch (\Throwable $e) {
    $db->rollBack();
    throw $e;
}
  • 例外が「ロールバックのトリガ」になるのは実務の定番。
  • DB例外は PDOExceptionPDOException)で受け止め、再スローで層をまたぐのがよくある変換パターン。

3. カスタム例外設計:ドメイン・アプリ・インフラの分離

3.1 例外の命名と粒度

  • ドメイン層DomainException を基底に、業務ルール違反を表現(例:InsufficientBalance)。
  • アプリ層:ユースケースの前提違反を InvalidOperation 等で。
  • インフラ層StorageUnavailable, ThirdPartyTimeout など「依存外部要因」。
namespace App\Domain;

class InsufficientBalance extends \DomainException {
    public function __construct(public readonly int $shortage) {
        parent::__construct("残高不足: {$shortage}円不足");
    }
}
  • 例外にドメイン情報(不足額など)をプロパティで持たせると、メッセージ生成や監視で再利用できます。

3.2 変換(Wrapping)と境界でのマッピング

  • インフラ例外 → アプリ/ドメイン例外へ変換
  • Web境界 → HTTPステータス/エラーレスポンスへマッピング
  • CLI境界 → 終了コード標準エラー出力
try {
    $useCase->execute($cmd);
} catch (\App\Domain\InsufficientBalance $e) {
    respondJson(400, ['code' => 'INSUFFICIENT_BALANCE', 'shortage' => $e->shortage]);
} catch (\App\Infra\ThirdPartyTimeout $e) {
    respondJson(504, ['code' => 'GATEWAY_TIMEOUT']);
}

4. SPL例外・標準例外の使い分け

  • 代表例:InvalidArgumentException, LengthException, OutOfBoundsException, RuntimeExceptionSPL例外の一覧)。
  • 入力がおかしいInvalidArgumentException
  • 状態がおかしい(前提が崩れた)→ LogicException
  • 実行時に起きた避けにくい問題RuntimeException
  • ライブラリ側はSPL例外で十分なことが多く、アプリ側で自前例外に包んで扱いやすい語彙へ寄せるのが実務的。

5. 例外とエラーの違い・ハンドラ設計

  • Error は型ミスマッチ・未定義関数などプログラミングエラーで、基本は修正対象Error)。
  • アプリのエントリポイントではThrowable 全面キャッチで落ちないようにしつつ、Errorログ+失敗レスポンスに留め、回復を試みないのが原則。
  • グローバルのハンドラは set_exception_handler()公式)で定義可能。ただしフレームワークのハンドラに委ねるのが一般的。

6. ロギング・観測性:PSR-3/構造化ログ/相関ID

  • PSR-3 ロガー(LoggerInterface)で例外・メタ情報を構造化して出力(contextexception を渡す)。
  • 相関ID(correlationId) を1リクエストに1つ発行してログに含めると、分散環境でも追跡容易
  • 例外はスタックトレース量が多いため、メッセージはユーザー向けとログ向けを分離(UXとセキュリティのため)。

7. 代案:戻り値エラー・Result 型・ガード節

7.1 戻り値でのエラー表現(慎重に)

// 戻り値で成功/失敗(PHPらしいが、取りこぼしやすい)
[$ok, $valueOrError] = $service->tryFetch($id);
if (!$ok) { return respondNotFound(); }
  • 取りこぼしリスクが高く、深いネストになりがち。本当に例外が重い箇所に限定を。

7.2 Result 風ラッパ(成功/失敗を型で表現)

final class Result {
    private function __construct(private ?mixed $ok, private ?\Throwable $err) {}
    public static function ok(mixed $v): self { return new self($v, null); }
    public static function err(\Throwable $e): self { return new self(null, $e); }
    public function unwrap(): mixed { if ($this->err) throw $this->err; return $this->ok; }
}
  • 例外を捕捉・保持して後段で処理したい場合に有効。パイプライン処理で便利。

7.3 ガード節とバリデーション

  • バリデーションは例外ではなく「早期return」で返すほうが読みやすい場面も多い。
  • 「本当に異常か?」を見極めて、制御フローを例外で“組まない”のも上級者の判断。

8. パフォーマンスの実際:計測と指針

  • 例外スローは高コスト(トレース構築・オブジェクト生成)。
  • 通常フローで例外を使わない(例:ループ内の分岐に例外を多用しない)。
  • コストが支配的なのは I/O であり、例外の有無よりDB/ネットワークの設計が遥かに影響大。
  • 実測は hrtime()公式)+代表ケースでベンチを取る。例外をベンチ対象にしないのが本筋(異常系は頻発させない)。

9. テスト戦略:例外の期待・契約テスト

  • PHPUnitなら expectException() / expectExceptionMessage()契約を明示
  • 層をまたぐ変換PDOExceptionRuntimeException)はユニットテストで固定。
  • エンドツーエンド(E2E)ではHTTPステータス/エラーコードが一致することを検証。
public function testInsufficientBalance(): void {
    $this->expectException(\App\Domain\InsufficientBalance::class);
    $this->service->withdraw($userId, 999999);
}

10. アンチパターン集(やりがちな落とし穴)

  1. なんでも catch (Throwable) して黙殺:バグを隠す最悪パターン。ログ+可観測性を。
  2. finally で戻り値を上書き:可読性が落ちる。finally はクリーンアップ専用に。
  3. 例外メッセージに機密情報:SQLやトークンを露出しない。ユーザー向けとログ向けを分離。
  4. Error は境界で捕捉し、ログ記録後にクリーンにプロセスを終了させるのが原則
  5. 例外を制御フローの代用に濫用:通常分岐で書けるものは書く。異常時のみ例外。

11. 現場向けチェックリスト

  • 例外の境界(握る場所)はフロントコントローラ/ミドルウェアに集約したか
  • カスタム例外階層で語彙を整理したか(Domain/App/Infra)
  • 再スロー時に previous を連鎖させているか
  • ロールバックtry/catch の近傍で必ず行うか
  • PSR-3 で構造化ログ+相関IDを残しているか
  • ユーザー向けメッセージから機密情報を排除したか
  • 例外を濫用していないか(通常分岐で書ける箇所の見直し)

参考リンク(文中で参照した主な公式)

まとめ

  • 例外は設計の道具:境界で握り、適切に変換し、ユーザー文脈へマッピング。
  • 階層設計と語彙の整備で、運用・監視・テストが一気に楽になる。
  • 性能は I/O と設計が支配:例外のコストは異常時だけ。通常フローで使わない。
  • “握る場所を決める” ことが、現場品質(可用性・可観測性)を左右する。

あわせて読みたい ~徹底解説シリーズ~

おすすめ本を紹介!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

神奈川県出身
どこにでもいる現役ゲーム会社サーバーエンジニア。大学ではひたすらゲーム!ゲーム!という人生を送ってました。

このブログは「ゲームを作ってみたい!」「プログラミングに関する知識をつけたい!」そんな皆さんの少しでもお役になれば嬉しいなと思い開設しました。

趣味
YouTube鑑賞、ストリートな格闘ゲーム、キャンプ

最近の出来事
・痔主になってしまいました....
・Google Cloud Professional Cloud Architect取得しました。

コメント

コメントする

目次