プログラムでは、ファイルが見つからない、データベースへ接続できない、不正な値が渡された、といった問題が起こります。PHPの例外処理を使うと、問題を検知し、利用者向けの表示と開発者向けの記録を分けられます。
この記事では、throw、try、catch、finallyの役割を、割り算とファイル保存の例で学びます。
エラーと例外の違い
PHPには構文エラー、警告、例外など複数の問題通知があります。セミコロンや括弧の書き間違いは、先によくあるPHPエラーで確認してください。
例外は、プログラムからthrowでき、catchで受け止められるオブジェクトです。適切に処理すれば、エラー画面をそのまま利用者へ見せず、安全な案内へ切り替えられます。
throwで例外を発生させる
処理を続けられない条件を見つけたら、throwで例外を投げます。
<?php
function divide(float $left, float $right): float
{
if ($right === 0.0) {
throw new InvalidArgumentException("0では割れません");
}
return $left / $right;
}
例外が投げられると、その位置より後の処理は実行されず、対応するcatchが探されます。
tryとcatchで例外を処理する
<?php
function divide(float $left, float $right): float
{
if ($right === 0.0) {
throw new InvalidArgumentException("0では割れません");
}
return $left / $right;
}
try {
$answer = divide(10, 0);
echo $answer;
} catch (InvalidArgumentException $exception) {
echo "計算できませんでした: " . $exception->getMessage();
}
echo PHP_EOL . "処理を続けます";
tryには例外が起こる可能性のある処理を書き、catchには例外が起きた場合の処理を書きます。上の例では、例外を処理したあともプログラムを続けられます。
finallyは成功・失敗にかかわらず実行される
finallyは、例外が発生した場合も、発生しなかった場合も実行されます。ファイルや接続など、最後に必ず片付けたい処理に使います。
<?php
$handle = null;
try {
$handle = fopen(__DIR__ . "/sample.txt", "rb");
if ($handle === false) {
throw new RuntimeException("ファイルを開けませんでした");
}
echo stream_get_contents($handle);
} catch (RuntimeException $exception) {
error_log($exception->getMessage());
echo "ファイルを読み込めませんでした";
} finally {
if (is_resource($handle)) {
fclose($handle);
}
}
複数の例外を分けて処理する
例外の種類によって対応を変えたい場合は、catchを複数書きます。上から順に一致する型が選ばれるため、具体的な例外を先に、広い例外を後に書きます。
<?php
try {
throw new InvalidArgumentException("入力値が不正です");
} catch (InvalidArgumentException $exception) {
echo "入力内容を確認してください";
} catch (RuntimeException $exception) {
echo "一時的な問題が発生しました";
} catch (Throwable $exception) {
echo "予期しない問題が発生しました";
}
Throwableは、ExceptionとPHPの多くのErrorに共通するインターフェースです。最上位で予期しない問題を記録するときに使えますが、すべてを握りつぶさないようにしてください。
独自例外で問題の意味を表す
アプリ独自の例外クラスを作ると、何が問題だったかを型で表現できます。
<?php
class InsufficientStockException extends RuntimeException
{
}
function purchase(int $stock, int $quantity): int
{
if ($quantity < 1) {
throw new InvalidArgumentException("購入数は1以上にしてください");
}
if ($stock < $quantity) {
throw new InsufficientStockException("在庫が不足しています");
}
return $stock - $quantity;
}
try {
echo purchase(3, 5);
} catch (InsufficientStockException $exception) {
echo "在庫数を確認してください";
}
利用者への表示とログを分ける
本番環境で例外メッセージやスタックトレースをそのまま画面へ表示すると、ファイルパスや設定情報が漏れる可能性があります。
<?php
try {
throw new RuntimeException("Database host: internal-db.example");
} catch (Throwable $exception) {
// 開発者向け:詳細をログへ残す
error_log((string) $exception);
// 利用者向け:内部情報を含まない表示
echo "現在処理を完了できません。時間をおいて再度お試しください。";
}
開発環境では問題を見つけるためerror_reporting(E_ALL)を有効にし、本番環境では画面表示を無効にしてログへ記録するのが基本です。
例外を使う場面
- データベースや外部サービスへの接続に失敗した
- 必要なファイルや設定を読み込めない
- 関数が前提とする値を受け取れない
- 在庫不足など、通常の戻り値では表しにくい失敗が起きた
入力欄の未記入のように、利用者が修正できる通常の検証エラーは、必ずしも例外にする必要はありません。戻り値やエラー配列で扱う方が読みやすい場合もあります。
よくある間違い
tryの範囲を広げすぎ、どの処理が失敗したか分からなくするcatch (Throwable)で受けたあと、ログを残さず無視する- 秘密情報を含む例外メッセージをそのまま画面へ表示する
finallyの中で再び例外を起こし、元の例外を分かりにくくする
練習問題
正の整数だけを受け取るcalculateSquare()関数を作ってください。0以下ならInvalidArgumentExceptionを投げ、呼び出し側でメッセージを表示します。
解答例を見る
<?php
function calculateSquare(int $number): int
{
if ($number <= 0) {
throw new InvalidArgumentException("正の整数を指定してください");
}
return $number ** 2;
}
try {
echo calculateSquare(-3);
} catch (InvalidArgumentException $exception) {
echo $exception->getMessage();
}
まとめ
throwで処理を続けられない問題を通知するtry / catchで例外を受け止めるfinallyで成功・失敗にかかわらず必要な後処理を行う- 本番では詳細をログへ残し、画面には安全なメッセージを表示する
例外処理は、PDOとMySQLによるCRUDでも実際に使用します。
コメント