PHPではthrow new Exception('message');で例外を投げます。実行はその行で止まり、例外の型を受け付けるいちばん近いcatchブロックへ飛び、そこで$e->getMessage()がメッセージを返します。呼び出し側が問題を区別できるよう、InvalidArgumentExceptionのような具体的なクラスを使いましょう。
例外はオブジェクトです。throwはそれをコールスタックの上へ、catchが一致する最初の呼び出し側まで渡し、その呼び出し側がどうするかを決めます。問題を見つけた関数がそれを知っている必要はありません。キャッチ、finally、複数の型のキャッチの仕組みはtry catchのページにあります。
例外オブジェクトが持っているもの
すべての例外は、メッセージ、整数のコード、作られたファイルと行、スタックトレースを持っています。コンストラクタは($message, $code, $previous)を受け取り、どれも任意です。
getLine()はcatchの行ではなく、throw newの行です。トレースにはそこに至るまでの呼び出しが並び(#0 /home/index.php(8): findOrder())、ログにエラーが出たときにたいてい最初に読むものです。例外を文字列にキャストする(echo $e;や(string) $e)と、PHPの標準の形式ですべてが出力されます。
組み込みの例外クラス
PHPには、標準PHPライブラリ(SPL)に例外クラスのファミリーが用意されています。問題を名前で表すものを投げれば、コードが読みやすくなり、呼び出し側は狭くキャッチできます。これらはすべてExceptionを継承しています。
| クラス | 投げる場面 |
|---|---|
InvalidArgumentException | 引数の形が間違っている:空の名前、未知のオプション |
DomainException | 値が意味をなす集合の外にある:負の価格 |
OutOfRangeException | 決して存在しえない添字をコードが求めた(呼び出し側のバグ) |
OutOfBoundsException | 実行時にしかわからないデータにキーや添字がない |
RangeException | 実行中に計算結果が有効な範囲外になった |
LengthException | 何かが長すぎるか短すぎる |
RuntimeException | 実行中にしか検出できない問題:ディスクがいっぱい、タイムアウト |
UnexpectedValueException | 関数が予想外の型の値を返したか受け取った |
LogicException | 不正な入力ではなく、コード自体が間違っている(バグ) |
JsonException | JSON_THROW_ON_ERRORを指定したjson_encode()/json_decode()が投げる |
InvalidArgumentException、DomainException、LengthException、OutOfRangeExceptionはLogicExceptionを、OutOfBoundsException、RangeException、UnexpectedValueExceptionはRuntimeExceptionを継承しています。どのクラスの親も自分で確認できます。
TypeErrorの親はExceptionではなくErrorです。下で紹介する2つ目のファミリーに属します。
PHP自身が投げるエラー
PHP 7以降、そしてPHP 8ではさらに多く、PHP自身の関数や演算子は、以前は警告だった問題に対してErrorのサブクラスを投げます。キャッチの仕方は同じですが、catch (Exception $e)はそれらに一致しません。
DivisionByZeroError、TypeError、ValueError、ArgumentCountError、UnhandledMatchError、素のErrorはすべてErrorを継承しています。どれも不正なデータより呼び出し側のコードのバグを示すことが多く、だからExceptionの木の外にあるのです。str_repeat(5, 2)をTypeErrorにしているのはdeclare(strict_types=1)で、それがなければPHPは5を"5"に変換します。
独自の例外クラスを書く
たいていは1行のサブクラスで十分です。クラス名自体が意味を伝え、呼び出し側はまさにその型をキャッチできます。キャッチする側がデータ(どの注文か、いくらか)を必要とするなら、プロパティを加え、parent::__construct()でメッセージを上へ渡します。
親は慎重に選びましょう。素のExceptionではなくRuntimeExceptionを継承すれば、RuntimeExceptionをキャッチするコードがあなたの例外も処理します。ライブラリやモジュールごとに1つの基底例外を置き、その下に具体的なサブクラスを並べる小さな階層を作れば、呼び出し側は「決済のものを何でもキャッチする」と「この1つのケースだけキャッチする」を選べます。
previousで例外を連鎖させる
低レベルの例外をキャッチして高レベルの例外を投げるときは、元の例外を3つ目の引数として渡します。何も失われず、getPrevious()で根本の原因までたどれます。
ユーザーには「customers.csv could not be imported」が表示され、連鎖をたどるログには「row 2 has 2 columns」も残ります。連鎖した例外が最後までキャッチされなければ、PHPの致命的エラーは連鎖全体を出力します。元の原因が最初で、そのあと各ラッパーがNext行の下に続きます。
式としてのthrow
PHP 8.0以降、throwは式なので、??のあと、三項演算子の中、アロー関数の中など、値が期待される場所ならどこにでも書けます。「これを取得する、できなければ失敗する」が1行になります。
例外を投げるべきでないとき
例外は、今の関数が処理できず、通常の流れが想定していない状況のためのものです。何も見つからない検索、空の任意の項目、ログインしていないユーザーは普通の結果です。null、false、空の配列を返して呼び出し側に確認させ、例外は処理を続けることが間違いになる場合のためにとっておきましょう。
例外のコードを読みやすく保つ習慣があと2つあります。例外を無視するためだけにキャッチしない(空のcatch (Exception $e) {}は次の本物のバグを隠します)ことと、素のExceptionではなく具体的なクラスを投げ、呼び出し側が1つのケースを処理するためにすべてをキャッチせざるをえない状況を作らないことです。
よくある質問
PHPで例外を投げるには?
例外オブジェクトを作って投げます:throw new InvalidArgumentException('Quantity must be positive');。実行はその行で止まり、いちばん近い一致するcatchブロックへ飛びます。なければ、スクリプトは「Uncaught」の致命的エラーで終わります。
PHPで独自の例外を作るには?
Exceptionかそのサブクラスを継承します:class PaymentFailedException extends RuntimeException {}。この1行だけで、独自の型で投げたりキャッチしたりできます。キャッチする側が追加のデータを必要とするならプロパティとコンストラクタを加え、parent::__construct($message, $code, $previous)を呼びます。
getMessageとgetCodeの違いは何ですか?
getMessage()は、コンストラクタの1つ目の引数として渡した、人やログ向けのテキストを返します。getCode()は2つ目の引数として渡した整数(デフォルトは0)を返し、エラーをHTTPのステータスコードに対応させるような機械的な判定に便利です。
PHPではどの例外を投げるべきですか?
問題に合った組み込みのSPLクラスを使います。不正な引数にはInvalidArgumentException、許される範囲外の値にはDomainException、実行時にしかわからない失敗(ディスクがいっぱい、タイムアウト)にはRuntimeException、プログラミングのミスにはLogicExceptionです。呼び出し側が区別する必要のあるエラーには、独自のサブクラスを作ります。
PHPの例外の連鎖とは何ですか?
新しい例外を投げるときに、元の例外をコンストラクタの3つ目の引数として渡すことです:throw new ImportException('Import failed', 0, $e);。新しい例外が文脈を伝え、その$e->getPrevious()が元の原因を返すので、詳細が失われません。