【PHP】PHPのエラーハンドリング:try-catchで例外処理を書く方法

phpアイキャッチ PHP

こんにちは、かつコーチです。

前回は正規表現でパターンマッチングを行う方法を解説しました。

今回は、プログラムの実行中に想定外のエラーが起きたときに備える「例外処理」を扱います。

正規表現やファイル操作、後々学ぶデータベース接続など、外部の要因でエラーが起きやすい処理を書くようになると、この例外処理の重要性がぐっと増してきます。

なぜ例外処理が必要なのか

エラーが起きるとどうなるか

まず、例外処理を使わずにエラーが起きるとどうなるか見てみましょう。

<?php
function divide($a, $b) {
    return $a / $b;
}

echo divide(10, 0);
// Warning: Division by zero
// 出力: INF(無限大を表す特殊な値)
?>

このように、PHPはエラーが起きても処理を止めずに動き続けてしまうことがあります。

一見便利にも思えますが、「エラーが起きたのに気づかないまま処理が進んでしまう」というのは、実務では大きな事故につながりかねません。

私が最初に業務でPHPを触っていたとき、DBへの接続に失敗しているのに気づかず処理が最後まで進んでしまい、「保存できたはずのデータが実は保存されていなかった」というトラブルに遭遇したことがあります。

原因を調べてわかったのは、接続失敗時のエラーをきちんとキャッチして止めていなかったことでした。

この経験から、「起きるかもしれないエラーは、起きた時点で確実に検知して処理を止める(あるいは適切に対処する)」という発想の大切さを痛感しました。

例外(Exception)とは

例外(Exception)とは、プログラムの実行中に発生した「想定外の事態」を表すオブジェクトのことです。

PHPでは、「例外を投げる(throw)」「例外を受け止める(catch)」という仕組みで、エラーが起きたときの処理を整理して書くことができます。

try-catchの基本

手順1:基本の書き方

try ブロックの中で処理を実行し、エラーが起きたら catch ブロックでそれを受け止めます。

<?php
function divide($a, $b) {
    if ($b === 0) {
        throw new Exception("0で割ることはできません");
    }
    return $a / $b;
}

try {
    $result = divide(10, 0);
    echo $result;
} catch (Exception $e) {
    echo "エラーが発生しました:" . $e->getMessage();
}
// 出力: エラーが発生しました:0で割ることはできません
?>

throw new Exception("メッセージ") で例外を意図的に発生させ、それを catch (Exception $e) で受け止めています。

$e->getMessage() で、例外に付けたメッセージを取り出せます。

手順2:finallyで必ず実行する処理を書く

trycatch の結果に関わらず、必ず実行したい処理は finally に書きます。

<?php
function readConfig($path) {
    $handle = fopen($path, "r");

    try {
        if ($handle === false) {
            throw new Exception("ファイルを開けませんでした:" . $path);
        }
        return fread($handle, filesize($path));
    } finally {
        if ($handle !== false) {
            fclose($handle); // エラーが起きても必ずファイルを閉じる
        }
    }
}
?>

「ファイルを開いたら必ず閉じる」「DB接続を開いたら必ず切断する」といった後片付けの処理は、finally に書いておくと、途中でエラーが起きても確実に実行されるので安心です。

複数の例外の種類を使い分ける

catchを複数に分ける

エラーの種類によって対応を変えたい場合は、catch を複数書けます。

<?php
function fetchUser($id) {
    if (!is_int($id)) {
        throw new InvalidArgumentException("IDは整数で指定してください");
    }
    if ($id <= 0) {
        throw new OutOfRangeException("IDは1以上を指定してください");
    }
    // ここでDBからユーザーを取得する処理(省略)
    throw new RuntimeException("ユーザーの取得に失敗しました");
}

try {
    fetchUser(-1);
} catch (InvalidArgumentException $e) {
    echo "入力エラー:" . $e->getMessage();
} catch (OutOfRangeException $e) {
    echo "範囲エラー:" . $e->getMessage();
} catch (RuntimeException $e) {
    echo "実行時エラー:" . $e->getMessage();
}
// 出力: 範囲エラー:IDは1以上を指定してください
?>

InvalidArgumentException(不正な引数)、OutOfRangeException(範囲外の値)、RuntimeException(実行時のエラー)など、PHPには用途別の例外クラスがあらかじめ用意されています。

catchは上から順にチェックされるので、より具体的な例外クラスを先に書くのが基本です。

独自の例外クラスを作る

自分のアプリケーション独自のエラーを表現したい場合は、Exception を継承したクラスを作れます。

<?php
class InsufficientStockException extends Exception
{
    // Exceptionを継承するだけで独自の例外クラスになる
}

function purchaseItem($stock, $requestedCount) {
    if ($requestedCount > $stock) {
        throw new InsufficientStockException("在庫が不足しています(在庫:{$stock}個)");
    }
    return "購入が完了しました";
}

try {
    echo purchaseItem(3, 5);
} catch (InsufficientStockException $e) {
    echo "購入できません:" . $e->getMessage();
}
// 出力: 購入できません:在庫が不足しています(在庫:3個)
?>

「在庫不足」「権限不足」のように、業務上意味のある単位で例外クラスを分けておくと、catch の時点でエラーの種類が一目で分かるようになります。

つまずきやすいポイント:例外をcatchし忘れて真っ白画面になった話

Fatal errorで処理が止まってしまう

例外処理でよくあるつまずきが、例外をキャッチせずに放置してしまうケースです。

❌ Before:例外をキャッチせずそのまま放置する

<?php
function divide($a, $b) {
    if ($b === 0) {
        throw new Exception("0で割ることはできません");
    }
    return $a / $b;
}

echo "計算を開始します" . PHP_EOL;
echo divide(10, 0); // ここで例外が発生
echo "計算が完了しました" . PHP_EOL; // 実行されない
?>

このコードを実行すると、次のようなエラーが表示され、以降の処理が一切実行されなくなります。

Fatal error: Uncaught Exception: 0で割ることはできません

私が実際にこれをやってしまったのは、外部APIとの通信処理を書いていたときでした。

APIがまれにタイムアウトすることを想定できておらず、例外を投げる処理をtry-catchで囲んでいなかったため、本番環境で画面が真っ白(Fatal error)になるという事故を起こしてしまったことがあります。

✅ After:try-catchで確実にキャッチする

<?php
function divide($a, $b) {
    if ($b === 0) {
        throw new Exception("0で割ることはできません");
    }
    return $a / $b;
}

echo "計算を開始します" . PHP_EOL;

try {
    echo divide(10, 0);
} catch (Exception $e) {
    echo "計算中にエラーが発生しました:" . $e->getMessage() . PHP_EOL;
}

echo "計算が完了しました" . PHP_EOL; // ここまで確実に実行される
?>

「例外を投げる可能性がある処理は、必ずtry-catchで囲む」というのを徹底するようになってから、本番環境での予期せぬ画面停止はほとんど起きなくなりました。

外部との通信、ファイル操作、DB接続など、「自分のコードの外側に依存する処理」は特に例外処理を意識するようにしています。

応用:例外を握りつぶさない設計

空のcatchブロックは避ける

例外処理に慣れてきた頃にやりがちなのが、「エラーを消すためだけ」の空のcatchです。

<?php
try {
    $result = 10 / 0;
} catch (Exception $e) {
    // 何もしない(エラーを握りつぶしている)
}
?>

一見エラーが出なくなって解決したように見えますが、実際には問題が起きていることに誰も気づけない状態になっているだけです。

最低限、error_log() でログに記録するなど、後から追跡できる形にしておきましょう。

<?php
try {
    $result = 10 / 0;
} catch (Exception $e) {
    error_log("計算エラー: " . $e->getMessage());
    $result = 0; // 代替値を使うなど、明示的な対処をする
}
?>

Laravelでの例外処理の考え方

余談ですが、Laravelにも同じ try-catch の考え方が引き継がれており、さらに app/Exceptions に例外を一元管理する仕組みが用意されています。

素のPHPで「どんな例外が起きうるか」「catchした後どう対処するか」を意識しておくと、Laravelの例外ハンドリングの仕組みもすんなり理解できるようになります。

まとめ

この記事のポイント

  • try の中でエラーが起きそうな処理を実行し、catch で受け止める
  • finally は、成功・失敗に関わらず必ず実行したい後片付け処理に使う
  • 例外の種類ごとに catch を分けたり、独自の例外クラスを作ったりできる
  • 例外をキャッチし忘れると Fatal error で処理全体が止まってしまう
  • 空のcatchで例外を握りつぶさず、ログに残すなど明示的に対処する

次に読むべき記事

次回は、複数のファイルにコードを分割していく際に欠かせない「名前空間(namespace)とautoload」を解説します。

例外クラスを独自に増やしていくと、ファイルを分割したくなる場面が必ず出てくるので、続けて読んでみてください。

→ 次の記事:PHPの名前空間(namespace)とautoloadの基本

タイトルとURLをコピーしました