こんにちは。開発部の北村です!
カルテットに入社し、5月1日でちょうど1年になりました。 私がプログラミングを始めたのが2021年11月頃だったので、すでにPHPのバージョンは8.0でしたが、例外について調べると記事の書かれた時期により色々内容が変わっており(特にPHP7系~)混乱したので、例外についてまとめてみました。

このブログは、そんな私のような初心者エンジニアから例外についてやんわり理解していてどんな種類があるか知りたい!と思っているエンジニアの方々向けに書いていきます。お役に立てれば嬉しいです。

1. はじめに

PHPに最初から備わっている例外のことを、「組み込み例外」(以下,「例外」)と呼びますが、本ブログでは「例外の全体像」、「例外クラスの説明」、「例外処理の方法」についてご紹介していきたいと思います。
※例外処理については各人の考え方が異なる分野であり、このブログでは主観的な意見が含まれていますが、参考までに読んでいただければと思います。
※このブログでの「例外」はThrowableインターフェイスを満たしているクラス(Errorクラスも含む)のことを指しています。

2. 例外とは?

PHPマニュアルでは例外とは、

PHP は、他のプログラミング言語に似た例外モデルを持っています。 PHP 内で例外がスローされ (throwされ)、それが 捕捉され (catchされ) ます。発生した例外を 捕捉するには、コードをtryブロックで囲みます。 各tryブロックには、対応するcatchブロックあるいはfinallyブロックが存在する必要があります。

との記述がありますが、これでは「例外」を知っている前提での例外処理方法についての説明になっている気がします。

ChatGPTによると、

PHPにおける例外は、実行時に発生するエラーや異常状態を表現するための仕組みです。例外は予期しない状況やエラーが発生した場合に使用され、プログラムの正常なフローを中断し、例外処理のための特定のコードブロックに制御を移します。

だそうです。
私はこちらの説明の方がしっくりきましたが、私自身の理解としては「プログラム実行中に発生する問題」を例外というのかなと思っています。

冒頭で

※このブログでの「例外」はThrowableインターフェイスを満たしているクラスのことを指しています。

と記述しましたが、Throwableインターフェイスとは例外(「プログラム実行中に発生する問題」)が発生した時の行動指針(振る舞いや特性)と考えていただければいいかなと思います。

なので、「例外」を噛み砕くと、 「プログラム実行中に発生した行動指針が決まっている問題」 なのかなと思います。

3. 例外全体の構造

では実際の例外(「プログラム実行中に発生する問題」)にはどんな例外クラス(具体的な問題)があるのかThrowableインターフェイスを満たしている例外クラスをPHP8.2.1で確認してみます。

$exceptionClasses = get_declared_classes();
foreach ($exceptionClasses as $class) {
    $reflectionClass = new ReflectionClass($class);
    if ($reflectionClass->isSubclassOf(Throwable::class)) {
        echo $class . "\n";
    }
}

//出力結果
Exception
ErrorException
Error
CompileError
ParseError
TypeError
ArgumentCountError
ValueError
ArithmeticError
DivisionByZeroError
UnhandledMatchError
ClosedGeneratorException
FiberError
DOMException
JsonException
IntlException
LogicException
BadFunctionCallException
BadMethodCallException
DomainException
InvalidArgumentException
LengthException
OutOfRangeException
RuntimeException
OutOfBoundsException
OverflowException
RangeException
UnderflowException
UnexpectedValueException
AssertionError
PDOException
PharException
Random\RandomError
Random\BrokenRandomEngineError
Random\RandomException
ReflectionException
mysqli_sql_exception
SoapFault
SodiumException

これではまとまりがなくわかりにくいので、出力結果の中の主要なクラスを階層ごとにまとめました。

- Throwableインターフェース

    - Errorクラス
        - ArithmeticErrorクラス
            - DivisionByZeroErrorクラス
        - AssertionErrorクラス
        - CompileErrorクラス
            - ParseErrorクラス
        - FiberErrorクラス
        - Random\RandomErrorクラス
        - Random\BrokenRandomEngineErrorクラス
        - TypeErrorクラス
            - ArgumentCountErrorクラス
        - UnhandledMatchErrorクラス
        - ValueErrorクラス
    
    
    - Exceptionクラス
        - ErrorExceptionクラス
        - ClosedGeneratorExceptionクラス
        - DOMExceptionクラス
        - JsonExceptionクラス
        - IntlExceptionクラス
        - PharExceptionクラス
        - Random\RandomExceptionクラス
        - ReflectionExceptionクラス
        - SodiumExceptionクラス
    
        - LogicExceptionクラス
            - BadFunctionCallExceptionクラス
                - BadMethodCallExceptionクラス
            - DomainExceptionクラス
            - InvalidArgumentExceptionクラス
            - LengthExceptionクラス
            - OutOfRangeExceptionクラス
        
        - RuntimeExceptionクラス
            - OutOfBoundsExceptionクラス
            - OverflowExceptionクラス
            - RangeExceptionクラス
            - UnderflowExceptionクラス
            - UnexpectedValueExceptionクラス
            - PDOExceptionクラス

先程、私自身の理解としては「例外」を、

「プログラム実行中に発生した行動指針が決まっている問題」

と説明しましたが、この「プログラム実行中に発生した行動指針が決まっている問題」には様々な理由があり、その理由がThrowableインターフェイスを満たしている各例外クラスにあたります。

詳しく見ていきます。

4. 例外クラス

例外クラスは大きく分けて、Throwableインターフェイスを実装(implemets)(以下, 「実装」)しているErrorクラスとExceptionクラスに分けられます。

- Throwableインターフェイス
    - Errorクラス
    - Exceptionクラス

またExceptionクラスを継承(extends)(以下, 「継承」)しているクラスはさらに、RuntimeExceptionクラスとLogicExceptionクラスとそれ以外の各例外クラスに分けられます。

- Exceptionクラス
	- RuntimeExceptionクラス
	- LogicExceptionクラス
	- その他例外クラス

ここでは大きく分類したErrorクラスとExceptionクラス、RuntimeExceptionクラスとLogicExceptionクラスの役割の説明と代表的な例外クラスの紹介をしていきます。

4-1. Errorクラスとは?

PHPマニュアルには、

Errorは、PHP のすべての内部エラーの基底クラスです。

と記述されています。 ErrorクラスはPHP7.0から実装されたクラスで、従来のPHPエンジン自体のエラーを一部例外クラスとして扱えるようにしたクラスです。 Errorクラスを継承している例外クラスの一部を紹介します。

TypeError

string型の変数にint型の値を代入しようとした際などに発生するエラーを表す例外クラスです。

function sum(int $one, int $two)
{
    echo $one + $two;
}

sum('一', '二');

//出力結果
PHP Fatal error:  Uncaught TypeError: sum(): Argument #1 ($one) must be of type int, string given, called

ArithmeticErrorクラス

Errorクラスを継承しているクラスで、算術演算の実行中に発生するエラーを表す例外クラスです。

DivisionByZeroErrorクラス

ArithmeticErrorクラスを継承しているクラスで、0で除算を行った際に発生するエラーを表す例外クラスです。

$result = 10 / 0;
echo $result;

//出力結果
PHP Fatal error:  Uncaught DivisionByZeroError: Division by zero

ComplieErrorクラス

Errorクラスを継承しているクラスで、プログラムの文法的な間違いや構造上の問題がある場合に発生するエラーを表す例外クラスです。

ParseErrorクラス

ComplieErrorクラスを継承しているクラスで、CompileErrorとは異なりコンパイル(ソースコードの変換)の前段階であるパース(プログラムの解析)の段階でエラーが検出されます。

if ($array = [] {
    echo "空配列です";
}

//出力結果
PHP Parse error:  syntax error, unexpected token "echo"

4-2. Exceptionクラスとは?

PHPマニュアルには、

Exception は、 すべてのユーザー例外の基底クラスです。

と記述されています。
Exceptionクラス とExceptionクラスを継承している例外クラスは、Errorクラス(PHPエンジン自体のエラーの一部)とは違いプログラマーが明示的に発生させる事ができる例外です。
Exceptionクラスを継承している例外クラスを一部紹介します。

ErrorExceptionクラス

Exceptionクラスを継承しているクラスで、PHPのエラーを例外として処理するため例外クラスです。
※エラーハンドリングについて今回は説明しませんが、通常エラーハンドラによって生成されたエラーがこのクラスのインスタンスとしてthrowされます。

DOMExceptionクラス

Exceptionクラスを継承しているクラスで、DOM(Document Object Model)操作中に発生するさまざまなエラーを表す例外クラスです。
※DOM:HTMLなどを表現するための標準的なインターフェース。

JsonExceptionクラス

Exceptionクラスを継承しているクラスで、JSONデータの操作中に発生する例外クラスです。

LogicExceptionクラス

Exceptionクラスを継承しているクラスで、プログラマーのミスやロジック(ソースコード)に問題がある時に発生するエラーを表す例外クラスです。

BadFunctionCallExceptionクラス

LogicExceptionクラスを継承しているクラスで、存在しない関数を呼び出した場合や、引数の数が間違っている場合に発生する例外クラスです。

DomainExceptionクラス

LogicExceptionクラスを継承しているクラスで、範囲外の値や、無効なファイル名を指定した場合など特定のドメインに関連する例外クラスです。

InvalidArgumentExceptionクラス

LogicExceptionクラスを継承しているクラスで、無効な引数が渡された場合(型も対象)に発生する例外クラスです。
 

RuntimeExceptionクラス

Exceptionクラスを継承しているクラスで、プログラムを実行時に予期しない事が起こった場合に発生する例外です。

PDOExceptionクラス

RuntimeExceptionクラスを継承しているクラスで、PDO (PHP Data Objects)を使用してデータベースにアクセスする際(接続できないやクエリ文が実行できないなど)に発生する例外クラスです。

OutOfBoundsExceptionクラス

RuntimeExceptionクラスを継承しているクラスで、配列やオブジェクトに対して範囲外(存在しない)のkeyやindexを指定した場合に発生する例外クラスです。

UnderflowExceptionクラス

RuntimeExceptionクラスを継承しているクラスで、メソッドの引数などに渡せる値が期待される範囲より小さい場合に発生する例外クラスです。

5. 例外処理の方法

一部の例外クラスを紹介しましたが、これらの例外が発生した場合にログを残したり、回復処理をするなど、例外を処理をすることを「例外処理」と言い、例外を発生させることを例外をthrowする(throwされる)と言います。
PHPではtry-catch文を使って例外処理します。

5-1. 基本構文

try {
    // 例外が発生する可能性のある処理を書く
} catch (Exception $e) {
    // 例外をキャッチして処理する
}

tryブロックでは、例外が発生する可能性のある処理を記述します。
※例外がthrowされた時点でtryブロック内の処理は中断されます。
catchブロックでは、tryブロックで発生した例外を捕捉(catch)し、その後回復処理などを記述します。

5-2. 実例

下記は「4-2. Exceptionクラスとは?」で紹介したOutOfBoundsExceptionクラスthrowする時のサンプルコードです。

try {
    $array = array('apple', 'banana', 'orange');
    if (!array_key_exists(3, $array)) {
        throw new OutOfBoundsException('$array[3]にはindexが存在しません');
    }
    echo $array[3];
} catch (OutOfBoundsException $e) {
    echo 'OutOfBoundsException: ' . $e->getMessage();
}

//出力結果
OutOfBoundsException: $array[3]にはindexが存在しません

サンプルコードではわかりやすく具体的な例外クラスをthrowしてcatchしましたが、実際に例外処理を実装する際にはどの例外をcatchすべきなのかは様々な考え方があります。
しかし、一般的な例外処理においてcatchしなくていい例外というものは考え方次第ですが、存在するのでご紹介します。

5-3. catchしなくていい例外

「3. 例外全体の構造」で挙げたThrowableインターフェイスを実装(implements)している上位のクラスで考えていきます。

- Throwableインターフェイス
	- Errorクラス
	- Exceptionクラス

まずErrorクラスですが、

従来のPHPエンジン自体のエラーを一部例外クラスとして扱えるようにしたクラス

なので、catchしてしまうとPHPエンジン自体のエラーが発生した場合でも処理が継続されてしまい、思わぬ問題が発生する可能性があるため推奨はされていません。
そのため、一般的な例外処理ではcatchしなくていい例外と言えます。

次にExceptionクラスですが、

プログラマーが明示的に発生させる事ができる例外

であり、すべてのユーザー例外の基底クラスであるため、catchした後に処理する必要のある例外と考えられます。
特定の例外に対応するために具体的な例外クラスをcatchすることも可能ですが、少なくともExceptionクラスをcatchすれば、複数の例外クラスをまとめて処理する場合や、予期せぬ例外が発生した場合に対応が可能になります。

6. まとめ

例外の基礎的な部分について整理するためにブログを書きましたが、例外が発生した場合の処理方法やcatchする例外クラス、ログの記録方法など、人により考え方が違い、調べていて面白かったです。
例外の全体像や各クラスの役割を把握し、適切な例外処理を行うことでプログラムの品質を高めていきたいと思います。