はじめに (対象読者・この記事でわかること)

この記事は、PHPで開発を行っている中級者以上のエンジニア、特に例外処理の実装でつまずいた経験がある方を対象としています。
「try‑catch を書いたのに例外が捕まらない」「独自例外クラスを作ったが throw した瞬間にスクリプトが停止する」などの症状が出たときに、原因の切り分け方と正しいコード例を学べます。この記事を読むことで、例外が意図した通りに発行されないケースを特定し、適切に例外を投げてハンドリングできるようになります。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。
- PHP の基本的な構文(if 文、関数定義、名前空間など)
- OOP の概念(クラス継承、インターフェース、アクセシビリティ)
- Composer でのパッケージ管理の経験(任意)

例外発行の基礎と注意点

PHP の例外は Throwable インターフェースを実装した ExceptionError の二つのクラス階層から構成されます。通常はビジネスロジックで起こりうるエラーを表すために Exception を継承した独自例外クラスを作成し、throw new MyException('メッセージ'); とします。ここで注意すべき点は次の三つです。

  1. 例外クラスは \Exception かそのサブクラスでなければならない
    Error 系統は PHP7 以降の致命的エラー用で、意図的に投げるとキャッチできないケースがあります。
  2. 名前空間の影響を受けやすい
    throw new Exception('msg'); と書くと現在の名前空間内に Exception が無いかどうかで解釈が変わります。必ず完全修飾名 \Exception か、use 文でインポートしてください。
  3. 例外が捕捉できない原因はスコープの問題が多い
    try ブロックが期待通りに囲んでいない、もしくは catch が正しい型を指定していないと例外は上位スタックへ伝搬します。

以上を踏まえたうえで、実際に例外を投げるまでの流れと、よくある落とし穴を次の章で具体的に示します。

例外発行実装とデバッグの手順

このパートでは、実務で遭遇しやすい「例外が発行できない」シナリオを再現しつつ、ステップごとに正しい実装とデバッグ手法を解説します。サンプルコードは PHP 8.2 を前提にしていますが、8.0 以降であれば同様に動作します。

ステップ1 基本的な例外クラスの作成

まずは最もシンプルな独自例外クラスを作ります。ファイル構成は次の通りです。

src/
 └─ Exception/
     └─ InvalidParameterException.php

src/Exception/InvalidParameterException.php

Php
<?php namespace App\Exception; use InvalidArgumentException; class InvalidParameterException extends InvalidArgumentException { // 必要に応じてカスタムプロパティやメソッドを追加 }

ポイントは InvalidArgumentException を継承している点です。InvalidArgumentException は PHP 標準で \LogicException\Exception の階層にあるので、throw が問題なく機能します。

ステップ2 例外を投げるロジックの実装

次に、入力検証を行う関数を書きます。ここで意図的に例外を投げます。

Php
<?php declare(strict_types=1); namespace App\Service; use App\Exception\InvalidParameterException; class UserService { public function createUser(array $data): void { if (empty($data['email']) || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { throw new InvalidParameterException('メールアドレスが無効です'); } // 他のロジック... } }

重要use App\Exception\InvalidParameterException; を忘れると、名前空間解決の結果 App\Service\InvalidParameterException が探され、クラスが見つからず Fatal error が発生します。

ステップ3 例外ハンドリング側の実装

例外が投げられたときにキャッチするコードです。try ブロックの範囲と catch の型が合致しているか確認しましょう。

Php
<?php require __DIR__ . '/vendor/autoload.php'; use App\Service\UserService; use App\Exception\InvalidParameterException; $service = new UserService(); try { $service->createUser(['email' => 'invalid-email']); } catch (InvalidParameterException $e) { // 予期した例外はここで処理 echo "バリデーションエラー: " . $e->getMessage(); } catch (\Throwable $e) { // 想定外の例外はログに出力して再スロー error_log($e); throw $e; }

ハマった点やエラー解決

1. Fatal error: Uncaught Exception が出た

  • 原因:例外クラスの use が抜けていた、もしくは throw new InvalidParameterException の前に名前空間が誤っていた。
  • 対策:IDE のインポート機能を利用し、必ず use 文を自動補完させる。throw new \App\Exception\InvalidParameterException のように完全修飾名で書くと確実。

2. catch が呼び出されない

  • 原因catch (InvalidParameterException $e) と書いたが、実際に投げた例外は InvalidArgumentException(継承関係の逆)だった。
  • 対策:継承チェーンを確認し、捕捉したい例外の最上位クラス(親クラス)でキャッチするか、catch (\Exception $e) のように広域に捕捉してから型判定する。

3. try が期待した範囲に入っていない

  • 原因:関数内部で throw が走っても、呼び出し元の try が別スレッドや別ファイルに切り離されているケース(例えば Laravel のジョブキュー)。
  • 対策:例外は同一呼び出しスタック上でしか捕捉できないことを認識し、ジョブやイベントリスナ内でも try-catch を配置するか、フレームワークの例外ハンドラに委譲する。

解決策まとめ

  • 名前空間と use を必ず確認し、IDE の自動補完を活用する。
  • 例外クラスの継承関係 を意識し、catch の型が投げた例外と合致しているか検証する。
  • try ブロックのスコープ を正しく設定し、例外が伝搬する経路を把握した上でハンドリングする。
  • デバッグ時は xdebugphp -d xdebug.mode=debug を利用し、スタックトレースを詳細に確認すると原因が特定しやすい。

まとめ

本記事では、PHP における例外発行が期待通りに動かない典型的なケースと、その原因・対策を体系的に整理しました。

  • 例外クラスは必ず \Exception 系列か \Error 系列のサブクラス であること。
  • 名前空間と use 文の抜け漏れが例外投げ失敗の最大の落とし穴 であること。
  • try‑catch のスコープとキャッチ型の整合性 を常に意識し、スタックトレースを活用してデバッグすること。

これらを正しく実装すれば、例外処理が安定し、エラーハンドリングの品質が大幅に向上します。次回は、例外のロギングとカスタムハンドラ(set_exception_handler)を活用した全体的なエラーマネジメント戦略について解説する予定です。

参考資料