【PHP】例外処理(try-catch-finally)の使い方|カスタム例外クラスの作り方も解説

PHP でプログラムを書いていると、ファイル読み込みや外部 API 呼び出しなど「失敗する可能性のある処理」を扱う場面が必ずあります。こうした処理を安全に扱うための仕組みが例外(Exception)であり、trycatchfinally を使った例外処理です。本記事では基本的な構文から実務でよく使うパターンまでを整理します。

動作の細部は PHP のバージョンによって異なる場合があります。コード例は執筆時点で PHP 8.x を主な参照先とし、手元の環境に合わせて PHP 公式マニュアルの例外処理の節 を並行して参照することをおすすめします。

はじめに

「外部 API を呼んだら接続タイムアウトになった」「ファイルを開こうとしたら存在しなかった」——こうしたエラーは事前にすべてを防ぐことはできません。エラーが発生したとき、プログラムが突然停止するのではなく「想定済みの異常として処理を引き継ぐ」ために例外処理を使います。

PHP の例外処理は trycatchfinally の 3 ブロックで構成されます。

  • try ブロックに「例外が発生し得る処理」を書く
  • catch ブロックに「例外が発生したときの処理」を書く
  • finally ブロックに「例外の有無にかかわらず必ず実行したい処理」を書く

この記事では次の範囲を扱います。

  • trycatchfinally の基本構文
  • 複数の例外クラスを使い分ける方法
  • 独自例外クラスの定義
  • finally の使いどころと注意点
  • よくある失敗例と対処

背景:PHP の例外クラス階層

PHP の例外は Throwable インターフェースを頂点とした継承ツリーになっています。

Throwable
├── Error(PHP エンジンレベルのエラー)
│   ├── TypeError
│   ├── ValueError
│   └── ...
└── Exception(アプリケーションレベルの例外)
    ├── RuntimeException
    ├── InvalidArgumentException
    ├── LogicException
    └── ...

catch で捕捉できるのは Throwable を実装したオブジェクトです。通常のアプリケーションコードでは Exception またはそのサブクラスを扱います。Error は PHP エンジン自身が投げるエラーで、TypeError(型の不一致)などが該当します。次の「実装例」では、この階層を踏まえた具体的なコードを示します。

実装例

基本構文

最小限の trycatch の例です。

try {
    $result = riskyOperation();
} catch (Exception $e) {
    echo 'エラーが発生しました: ' . $e->getMessage();
}

throw で意図的に例外を発生させることもできます。

function divide(int $a, int $b): float
{
    if ($b === 0) {
        throw new InvalidArgumentException('除数に 0 は指定できません。');
    }
    return $a / $b;
}

try {
    $result = divide(10, 0);
} catch (InvalidArgumentException $e) {
    echo $e->getMessage(); // 除数に 0 は指定できません。
}

例外オブジェクトからは次のメソッドで情報を取得できます。

メソッド内容
getMessage()エラーメッセージ
getCode()エラーコード(throw new Exception('msg', 100) で指定)
getFile()例外が発生したファイルパス
getLine()例外が発生した行番号
getPrevious()前の例外(チェーンしている場合)

複数の catch ブロック

例外の種類ごとに処理を分けたい場合は、catch を複数並べます。上から順に評価されるため、より具体的なクラスを先に書くのが基本です。

try {
    $data = fetchDataFromApi($url);
} catch (ConnectionException $e) {
    // 接続失敗:リトライやフォールバックを検討
    logger()->error('API 接続エラー', ['message' => $e->getMessage()]);
} catch (InvalidArgumentException $e) {
    // 引数誤り:呼び出し側のバグの可能性が高い
    logger()->error('引数エラー', ['message' => $e->getMessage()]);
} catch (Exception $e) {
    // その他の例外
    logger()->error('予期しないエラー', ['message' => $e->getMessage()]);
}

PHP 8.0 以降では、1 つの catch ブロックで複数の例外型を | でまとめられます(Union Catch)。

catch (ConnectionException | TimeoutException $e) {
    // 接続系エラーを一括処理
}

finally ブロック

finally に書いた処理は、例外の有無にかかわらず try / catch の後に必ず実行されます。ファイルハンドルやデータベース接続のクローズなど、「後始末」に向いています。

$file = fopen('data.txt', 'r');

try {
    $content = processFile($file);
} catch (RuntimeException $e) {
    logger()->error($e->getMessage());
} finally {
    fclose($file); // 例外が発生しても必ず閉じる
}

try の中で return しても finally は実行されます。

function readConfig(string $path): array
{
    $file = fopen($path, 'r');
    try {
        return parseFile($file); // return しても finally は動く
    } finally {
        fclose($file);
    }
}

独自例外クラスの定義

アプリケーション固有のエラーを表すために、Exception を継承した独自クラスを定義します。

class UserNotFoundException extends RuntimeException
{
    public function __construct(int $userId)
    {
        parent::__construct("ユーザー ID {$userId} が見つかりません。", 404);
    }
}

function findUser(int $id): User
{
    $user = User::find($id);
    if ($user === null) {
        throw new UserNotFoundException($id);
    }
    return $user;
}

独自例外クラスを使うと、catch で型を指定して捕捉できるため、処理の分岐が明確になります。

// 以下は Laravel フレームワークを使用した例
try {
    $user = findUser(999);
} catch (UserNotFoundException $e) {
    // 404 相当の対処
    return response()->json(['error' => $e->getMessage()], $e->getCode());
}

例外チェーン(原因の保持)

例外を別の例外に包んで再スローするときは、第 3 引数に元の例外を渡します。こうすると getPrevious() で原因を遒れます。

// PDO(PHP Data Objects)はデータベース操作の標準的な PHP 拡張
try {
    $pdo->query($sql);
} catch (PDOException $e) {
    throw new DatabaseException('クエリに失敗しました。', 0, $e);
}

実装時の注意点

catch する例外は具体的に絞る

catch (Exception $e) で何でも捕捉すると、意図しない例外まで握りつぶす恐れがあります。捕捉する例外クラスは処理の意図に合わせて具体的に指定します。

finally の中での return や throw には注意する

finally ブロックで return または throw すると、trycatch ブロックの return / throw を上書きします。finally は原則として後始末のみに使い、値を返したり例外を投げたりするのは避けた方が無難です。

例外メッセージにユーザー入力をそのまま含めない

ログや外部レスポンスに例外メッセージをそのまま出力すると、内部情報の漏洎につながる場合があります。ユーザーへの表示用メッセージと開発者向けログは分けて管理します。

失敗例:catch で例外を握りつぶして原因が追えなくなる

// 問題のある書き方
try {
    $result = expensiveOperation();
} catch (Exception $e) {
    // 何もしない(握りつぶし)
}

例外を捕捉しても何もしないと、処理失敗の事実が隠れ、後からデバッグできなくなります。少なくともログに記録するか、上位に再スローします。

// 改善例
try {
    $result = expensiveOperation();
} catch (RuntimeException $e) {
    logger()->error('処理に失敗しました。', ['exception' => $e]);
    throw $e; // 上位で判断させる場合は再スロー
}

学び:例外処理で設計が整理されること

trycatchfinally を正しく使うと、次の点でコードが整理されます。

正常系と異常系の分離

メインの処理を try に集め、エラー対応を catch に分けることで、正常系のフローが読みやすくなります。

後始末の確実な実行

finally を使うことで、ファイルやネットワーク接続のクローズを例外の有無にかかわらず確実に行えます。

エラーの種類に応じた対処

独自例外クラスと複数の catch を組み合わせることで、エラーの種類に応じた処理(リトライ・ログ・レスポンスコードの変更など)を明確に書けます。

まとめ

  • try に例外が発生しうる処理、catch に例外への対処、finally に後始末を書く。
  • catch は具体的な例外クラスを指定し、握りつぶしを避ける。
  • Exception を継承した独自クラスを使うと、エラーの種類ごとに処理を分けやすくなる。
  • finally の中での return / throwtry / catch の結果を上書きするため注意が必要である。

次に試せること

  1. 自分のプロジェクトで「エラーが返ったら何もしない」箇所を探し、ログ記録または再スローに改善する。
  2. ドメイン固有の操作(ユーザー検索・外部 API 呼び出しなど)に対して独自例外クラスを 1 つ定義してみる。
  3. PHP 公式マニュアルの例外処理 を読み、set_exception_handler によるグローバルな例外ハンドラの設定を確認する。

Source

PHPPHP

Posted by 千原 耕司