【初心者向け】PHPの例外処理について

## はじめに
Qiitaにて新人プログラマ応援イベントを開催中とのことなので、乗っかってみます。

https://qiita.com/official-events/63aa63fd921659fb3b5d

## 例外処理とは
例外処理について調べてみるとさまざまな定義がされているので一概に言うのは難しいですが、
ここでは「**プログラムの実行時に発生した問題に対応するための処理**」と定義します。
プログラムの実行時に発生した問題とは、下記のようなものを想定しています。

- データベースに接続できなかった
- ファイル読み込み時にファイルが存在しなかった
- 数値が渡されるはずなのに文字列が渡された
- 0で割り算を行った
- etc...

こういった問題が発生した場合、例外処理が実装されていなければプログラムはそこで異常終了していまいますが、
例外処理を実装していれば処理を止めることなくプログラムをそのまま動かすことができます。

## 例外処理の必要性
例外処理が実装されていないと何が困るでしょうか？
もちろんプログラムを実行して全ての処理が正常に終了すればそれが一番望ましいですが、現実問題なかなか難しいです。問題が起きる可能性がある以上、きちんと対応しておく必要があります。もし例外処理が実装されていなかったら下記のような問題が発生します。

- ユーザが処理を継続できない
- 想定外の事象が発生した場合の原因調査、究明が難しい。

これらの問題を未然に防ぐためにも、例外処理は適切に実装する必要があります。

## 例外処理の実装方法
### 例外処理が実装されていない場合
まずサンプルコードを用意しました。

```Calculate.php
<?php
    // 分子の数字を入力させる
    echo "分子の数字を入力してください\n";
    $bunshi = trim(fgets(STDIN));

    // 分母の数字を入力してください
    echo "分母の数字を入力してください\n";
    $bunbo = trim(fgets(STDIN));

    $answer = 0;
    $answer = divide($bunshi, $bunbo);

    echo "答えは{$answer}です\n";
    echo "\n";
    echo "これにて処理を終了します\n";

    /**
     * 引数として渡された数字を使って割り算をおこない、計算結果を返す
     * (本来引数には型を指定しておくべきですが今回はサンプルのため省いています)
     *
     * @param  string  $bunshi 分子
     * @param  string  $bunbo  分母
     * @return int   $answer 計算結果
     */
    function divide($bunshi, $bunbo): int
    {
        $result = 0;

        $result = $bunshi / $bunbo;

        return $result;
    }
?>

```

ユーザにコンソール上で分子と分母の数字を入力してもらい、その計算結果を返すだけのプログラムです。
これを実行すると下記のような結果になります。

```zsh
$ php Caluculate.php
分子の数字を入力してください
6
分母の数字を入力してください
2
答えは3です

これにて処理を終了します
```

次に、例外処理が実装されていない場合どうなるか検証するため、分母の数字に誤って文字列を入力してみます。

```zsh
$ php Caluculate.php
分子の数字を入力してください
6
分母の数字を入力してください
a
PHP Fatal error:  Uncaught TypeError: Unsupported operand types: string / string in /~中略~/Caluculate.php:29
Stack trace:
#0 /~中略~/Caluculate.php(11): divide('6', 'a')
#1 {main}
  thrown in /~中略~/Caluculate.php on line 29
```

関数`divide()`の第二引数には本来数字が入り、割り算が実施されるはずですが、文字列が入力されたため異常終了してしまいました。これを処理を継続できるように、例外処理を実装していきます。

### 例外処理が実装されている場合
例外処理が実装されている場合のために、公式リファレンスの[例外](https://www.php.net/manual/ja/language.exceptions.php)を参考にしながら先ほどのサンプルコードを少し書き換えます。

```Calculate.php
<?php
    // ユーザに分子数字を入力させる
    echo "分子の数字を入力してください\n";
    $bunshi = trim(fgets(STDIN));

    echo "分母の数字を入力してください\n";
    $bunbo = trim(fgets(STDIN));
    
    $answer = 0;

    try {
        // 例外が発生しそうな処理
        $answer = divide($bunshi, $bunbo);
        echo "答えは{$answer}です\n";
    } catch(Exception $e) {
        // 例外を捕捉した場合に実行される処理
        echo "例外発生！\n";
    }

    echo "\n";
    echo "これにて処理を終了します\n";


    /**
     * 引数として渡された数字を使って割り算をおこない、計算結果を返す
     * (本来引数には型を指定しておくべきですが今回はサンプルのため省いています)
     *
     * @param  string  $bunshi 分子
     * @param  string  $bunbo  分母
     * @return int     $answer 計算結果
     */
    function divide($bunshi, $bunbo): int
    {
        if(!is_numeric($bunshi)) {
            throw new Exception("分子が数字ではありません\n");
        }

        if(!is_numeric($bunbo)) {
            throw new Exception("分母が数字ではありません\n");
        }

        $result = 0;

        $result = $bunshi / $bunbo;

        return $result;
    }
?>
```

例外が発生する可能性がある部分、今回であれば割り算を行う関数の呼び出し元をtry文で囲み、そのtry文の直後に例外が発生した場合に実行される処理をcatch文で囲んで記載しています。
また、関数内の冒頭で引数が数字または数値形式の文字列かどうかの判定文を入れており、もし数字または数値形式の文字列でなければ例外を投げ(スローし)ます。try文内でスローされた例外をcatch文で捕捉することによって例外処理が動作します。
実際に試してみます。まず数字が入力された場合ですが

```zsh
$ php Caluculate.php
分子の数字を入力してください
6
分母の数字を入力してください
2
答えは3です

これにて処理を終了します
```

先ほどと同様に正常に計算結果が返ってくることがわかります。
では再び分母に文字列が入力された場合はどうなるでしょうか？

```zsh
$ php Caluculate.php
分子の数字を入力してください
6
分母の数字を入力してください
a
例外発生！

これにて処理を終了します
```

呼び出した関数内で例外が発生したため、catch文の中へ入り、その後終了していることがわかります。try/catchで囲っていなかった場合は一番最後の「これにて処理を終了します」という文字列は出力されてませんでしたが、今回はされていると思います。
このようにtry/catchで囲んでいる部分で例外が発生した場合はcatch文の中の処理が実行され、その後try/catchを抜けた後の処理が実行されます。

### 例外メッセージの取得
上記のサンプル例では「例外発生！」と出力されるだけで、具体的にどういった問題が起きたのかわかりませんでした。
実際にcatch文内でログ出力などを行う場合は[Exceptionクラス内で定義されているメソッド](https://www.php.net/manual/ja/class.exception.php)を使うことがおおいです。
今回はよく使われる`getMessage()`を使ってどういった問題が起きたのかを確認します。
上記のコードのcatch文を以下のように書き換えます。

```Calculate.php
/*
   略
*/
    try {
        // 例外が発生しそうな処理
        $answer = divide($bunshi, $bunbo);
        echo "答えは{$answer}です\n";
    } catch(Exception $e) {
        // 例外を捕捉した場合に実行される処理
        echo $e->getMessage();
    }
/*
    略
*/
```

書き換えた上で再度実行してみます。

```zsh
$ php Caluculate.php
分子の数字を入力してください
6
分母の数字を入力してください
a
分母が数字ではありません

これにて処理を終了します
```

`throw new Exception("分子が数字ではありません\n");`と例外をスローした際に定義したエラーメッセージが出力されていることがわかると思います。このようにして定義したメッセージを取得することもできます。

### 独自例外の作成
上記のように例外をスローする際にエラーメッセージを定義しておけばcatch文の中で取得できますが、もし同じ例外をcatchさせたい場合何度もエラーメッセージを入力するのは面倒です。
そういった場合のために、例外クラスを継承して独自の例外を作ることができます。
実際にサンプルコードを用意してみます。

```Calculate.php
<?php
    // ユーザに分子数字を入力させる
    echo "分子の数字を入力してください\n";
    $bunshi = trim(fgets(STDIN));

    echo "分母の数字を入力してください\n";
    $bunbo = trim(fgets(STDIN));

    $answer = 0;

    try {
        // 例外が発生しそうな処理
        $answer = divide($bunshi, $bunbo);
        echo "答えは{$answer}です\n";
    } catch(Exception $e) {
        // 例外を捕捉した場合に実行される処理
        echo $e->getMessage();
    }

    echo "\n";
    echo "これにて処理を終了します\n";


    /**
     * 引数として渡された数字を使って割り算をおこない、計算結果を返す
     * (本来引数には型を指定しておくべきですが今回はサンプルのため省いています)
     *
     * @param  string  $bunshi 分子
     * @param  string  $bunbo  分母
     * @return int     $answer 計算結果
     */
    function divide($bunshi, $bunbo): int
    {
        if(!is_numeric($bunshi)) {
            throw new Exception("分子が数字ではありません\n");
        }

        if(!is_numeric($bunbo)) {
            throw new Exception("分母が数字ではありません\n");
        }

        if($bunbo === '0') {
            throw new DivideZeroException();
        }

        $result = 0;

        $result = $bunshi / $bunbo;

        return $result;
    }

    class DivideZeroException extends Exception {
        public function __construct() {
            parent::__construct("分母に0が設定されました\n", 1);
        }
    }
?>
```

まず、`divide()`の中に分母が0だった場合、新しい例外クラスをスローするようにします。
次に末尾に`Exception`クラスを継承した`DivideZeroException`クラスを宣言します。
実は0で割り算を行おうとした場合の例外クラスはすでに[DivisionByZeroErrorクラス](https://www.php.net/manual/ja/class.divisionbyzeroerror.php)が定義されているのですが、今回はあくまでサンプルなので自作していきます。
公式ドキュメントの[例外を拡張する](https://www.php.net/manual/ja/language.exceptions.extending.php)より、Exceptionクラスを継承したクラスのコンストラクタでは親クラスのコンストラクタも宣言することが推奨されているため親クラスのコンストラクタを宣言し、エラーメッセージとコード値を指定します。
これによって、数字以外が渡された場合や分母に0が来た場合など、例外の種別を区別したい場合に自作例外を呼ぶことによって原因の切り分けがしやすくなります。

```zsh
$ php Caluculate.php
分子の数字を入力してください
6
分母の数字を入力してください
a
分母が数字ではありません

これにて処理を終了します

$ php Caluculate.php
分子の数字を入力してください
6
分母の数字を入力してください
0
分子に0が設定されました

これにて処理を終了します
```

## まとめ
今回はPHPの例外処理について記載しました。
記載した内容はあくまで個人の解釈ですので、もし誤った解釈などがあった場合はお手数ですがコメントにてご指摘いただけると幸いです。
最後まで読んでいただきありがとうございました。

## 参考
- [例外(exceptions)](https://www.php.net/manual/ja/language.exceptions.php)
- [例外を拡張する](https://www.php.net/manual/ja/language.exceptions.extending.php)
- [定義済みの例外](https://www.php.net/manual/ja/reserved.exceptions.php)
- [SPLの例外](https://www.php.net/manual/ja/spl.exceptions.php)
- [プログラミング初心者がつまずきやすい「例外処理」の基本をC#で解説](https://paiza.hatenablog.com/entry/2019/12/09/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E5%88%9D%E5%BF%83%E8%80%85%E3%81%8C%E3%81%A4%E3%81%BE%E3%81%9A%E3%81%8D%E3%82%84%E3%81%99%E3%81%84%E3%80%8C%E4%BE%8B%E5%A4%96%E5%87%A6)
- [【PHP入門⑫】例外処理 ~初心者でも安心！基礎から学べるプログラミング講座~](https://snome.jp/programming/php-intro-12/#:~:text=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E5%AE%9F%E8%A1%8C%E4%B8%AD%E3%81%AB%E7%99%BA%E7%94%9F,%E3%82%92%E4%BE%8B%E5%A4%96%E3%81%A8%E8%A8%80%E3%81%84%E3%81%BE%E3%81%99%E3%80%82&text=%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E7%9A%84%E3%81%AB%E7%95%B0%E5%B8%B8%E7%B5%82%E4%BA%86,%E4%BD%9C%E3%82%8B%E3%81%93%E3%81%A8%E3%81%8C%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%80%82)