コンテンツにスキップ

エラーとエラーハンドリング@PHP

はじめに

本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。


01. エラーとエラーハンドリング

エラーとは

プログラムの実行が強制的に停止されるランタイムエラー (実行時エラー) 、停止せずに続行される非ランタイムエラー、に分類される。


エラーハンドリングとは

エラーハンドリングは以下の4ステップからなる。

(1)

エラー検出

(2)

例外スロー

(3)

例外キャッチ

(4)

ロギング


01-02. エラーハンドリングの意義

DBにとって

DB更新系の処理の途中にエラーが発生すると、DBが中途半端な更新状態になってしまう。

そのため、メソッドコールしたクラスでエラーを検出し、これをきっかけにロールバック処理を実行する必要がある。

注意点として、下層クラスのエラーの内容自体は握りつぶさずに、スタックトレースとしてメソッドコールしたクラスでロギングしておく。


ソフトウェア開発者にとって

エラーが画面上に返却されたとしても、これはソフトウェア開発者にとってわかりにくい。

そのため、エラーをメソッドコールしたクラスで検出し、ソフトウェア開発者にわかる言葉に変換した例外としてスローする必要がある。

注意点として、下層クラスのエラー自体は握りつぶさずに、スタックトレースとしてメソッドコールしたクラスでロギングしておく。


ユーザーにとって

エラーが画面上に返却されたとしても、ユーザーにとっては何が起きているのかわからない。

また、エラーをメソッドコールしたクラスで検出し、例外としてスローしたとしても、ソフトウェア開発者にとっては理解できるが、ユーザーにとっては理解できない。

そのため、例外スローを別の識別子 (例:bool値) に変えてメソッドコールしたクラスに持ち上げ、最終的には、これをポップアップなどでわかりやすく通知する必要がある。

これらは、サーバーサイドのtry-catch-finally文や、フロントエンドのポップアップ処理で実現する。

注意点として、子クラスのエラー自体は握りつぶさずに、スタックトレースとしてメソッドコールしたクラスでロギングしておく。


01-03. 例外の定義

標準例外クラス

いずれもThrowableインターフェースを実装している。

以下リンクを参考にせよ。


自前例外クラス

▼ 定義

エラーの種類に合わせて、Exceptionクラスを継承した自前例外クラスを実装し、使い分けると良い。

__constructメソッドに、メッセージやエラーコード (例外コード) などを渡せる。

エラーコードのデフォルト値はゼロである。

エラーコードはステータスコードと異なり、例外を識別するためのものである。

異常系レスポンスのエラーコードデータとして使用される。

混乱を避けるため、例外クラスのエラーコード値にステータスコードを割り当てないようにする。

ステータスコードはコントローラーにおけるレスポンス処理で割り当てる。

*実装例*

『Foo変数が見つからない』というエラーに対応する例外クラスを定義する。

<?php

class FooNotFoundException extends Exception
{
    // 基本的に何も実装しない。

    // エラーコードとステータスコードは異なるもののため、以下の様にしないこと。
    // protected $code = 400
}
<?php

use Exception\FooNotFound;

function foo(string $foo) {

    if (empty($foo)) {
        throw new FooNotFoundException("foo is not found.");
    }

    return "これは ${foo} です。";
}

▼ 命名規則

何のエラーが発生したかを判断できるように、名前は『<エラー名>Exception』とする。

また、開発者にとって詳しく理解できるように、コンストラクタの引数にメッセージを渡す。

(例)

  • InvalidArgumentException


02. エラー検出と例外スロー

エラー検出と例外スローの種類

▼ フレームワークの標準機能

多くのフレームワークでは、ランタイムエラーや非ランタイムエラーが発生すると、それを検知して、例外をスローしてくれる。

▼ 自前定義

if-throw文を使用して、エラー検出と例外スローを実行する。

ランタイムエラーは検出できないことに注意する。特定の処理の中に、想定できる例外があり、それを例外クラスとしてするために使用する。

ここでは、全ての例外クラスの親クラスであるExceptionクラスのインスタンスを投げている。

*実装例*

<?php

function value(int $value) {

    if (empty($value)) {
        // 例外クラスを返却
        throw new Exception("Value is empty");
    }

    return "これは ${value} です。";
}

ただし、if-throwでは、都度例外を検証するがあり、様々な可能性を考慮しなければいけなくなる。

<?php

function value() {

    if (...) {
        throw new ExternalApiException();
    }

    if (...) {
        throw new FooInvalidArgumentException();
    }

    return "成功です。"
}


03. 例外キャッチ

例外キャッチの方法

▼ try-catch-finally文とは

try-catch-finally文では、特定の処理の中で起こる想定できない例外を捉えられる。定義されたエラー文は、デバック画面に表示される。

*実装例*

<?php

use \Exception\ExternalApiErrorException;
use \Exception\HttpRequestErrorException;

class Foo
{
    public function sendMessage(Message $message)
    {
        try {

            // ExternalApiErrorException、HttpRequestErrorException、Exceptionが起こる

        } catch (ExternalApiErrorException $exception) {

            // ExternalApiErrorExceptionが発生した時の処理

        } catch (HttpRequestErrorException $exception) {

            // HttpRequestErrorExceptionが発生した時の処理

        } catch (Exception $exception) {

            // その他 (例:自社ソフトウェアなど) のExceptionが起こっ時の処理

        } finally {

            // 例外を補足したか否かに関わらず実行する。
            // try句やcatch句の返却処理や終了処理が行われる直前に実行される。

        }
    }
}

finally句は、try句やcatch句の返却処理が行われる直前に実行されるため、finally句では、returncontinueを使用しないようにする。

<?php

use Exception\ExternalApiErrorException;
use Exception\HttpRequestErrorException;

class Foo
{
    public function sendMessage(Message $message)
    {
        try {

            // `(1)`

:
            echo "Aの直前です";
            return "Aです。";

        } catch (ExternalApiErrorException $exception) {

            // `(2)`

:
            echo "Bの直前です";
            return "Bです。";

        } catch (HttpRequestErrorException $exception) {

            // `(3)`

:
            echo "Cの直前です";
            return "Cです。";

        } catch (Exception $exception) {

            // `(4)`

:
            echo "Dの直前です";
            return "Dです。";

        } finally {

            // returnやcontinueを使用しない
            echo "Eです。";

        }
    }
}
(1)

(4)

のいずれかで返却される時、返却の直前にfinally句が実行されることがわかる。

// `(1)`

:    の場合
// Aの直前です。
// Eです。
// Aです。

// `(2)`

:    の場合
// Bの直前です。
// Eです。
// Bです。

// `(3)`

:    の場合
// Cの直前です。
// Eです。
// Cです。

// `(4)`

:    の場合
// Dの直前です。
// Eです。
// Dです。


新たな例外のスローし直し

例外をtry-catch文でキャッチした後、別の新しい例外をスローしても良い。

その場合は、例外のコンストラクタの第三引数 (previous) を使用して、キャッチされていた元の例外も検知できるようにする。

補足として、この例外をロギングする場合、スタックトレースログとして出力される。


例外キャッチのレイヤー

▼ コントローラー/ミドルウェア派

想定外のエラーも含めて、全てのエラーを検出できるように、コントローラーまたはミドルウェアにtry-catch文を実装する。

▼ ユースケース派

コントローラーの実装をより単純にするべく、より下位のユースケースにtry-catch文を実装する。


04. ロギング

ロギング関数

error_log関数

error_log(
    "<エラーメッセージ>",
    "<メッセージの出力先 (3の場合にファイル出力) >",
    "<ログファイルへのパス>"
)

*実装例*

<?php

class Notification
{
    public function sendMessage()
    {
        try {

            // 子クラスによる例外スローを含む処理

        } catch (\exception $exception) {

            error_log(
                sprintf(
                    "ERROR: %s at %s line %s",
                    $exception->getMessage(),
                    $exception->getFile(),
                    $exception->getLine()
                ),
                3,
                __DIR__ . "/error.log"
            );
        }
    }
}

他に、Loggerインターフェースを使用することも多い。

<?php

use Psr\Log\LoggerInterface;

class Notification
{
    private $logger;

    public function __construct(LoggerInterface $logger = null)
    {
        $this->logger = $logger;
    }

    public function sendMessage()
    {
        try {

            // 子クラスによる例外スローを含む処理

        } catch (\exception $exception) {

            $this->logger->error(sprintf(
                "ERROR: %s at %s line %s",
                $exception->getMessage(),
                $exception->getFile(),
                $exception->getLine()
            ));
        }
    }
}


ロギングのレイヤー

try-catch文に伴うロギングの場合、catch句の中でこれを実行する。そのため、ロギングを実行するレイヤーはtray-catch文のレイヤーと同じになる。


ログの種類別の振り分け

▼ 例外ごとのロギング

例えば、メッセージアプリケーションのAPIに対してメッセージ作成のリクエストを送信する時、例外の種類に合わせて、外部APIとの接続失敗によるエラーログを作成と、自社ソフトウェアなどその他原因によるエラーログを作成を行う必要がある。

*実装例*

<?php

use Exception\ExternalApiErrorException;
use Exception\HttpRequestErrorException;

class Foo
{
    public function sendMessage(Message $message)
    {
        try {

            // 外部APIのURL、送信方法、トークンなどのパラメーターが存在するかを検証。
            // 外部APIのためのリクエストを作成。
            // 外部APIのURL、送信方法、トークンなどのパラメーターを設定。

        } catch (\HttpRequestErrorException $exception) {

            // 子クラスによる例外スローを含む処理

            // 外部APIとの接続失敗によるエラーをロギング
            $this->logger->error(sprintf(
                "ERROR: %s at %s line %s",
                $exception->getMessage(),
                $exception->getFile(),
                $exception->getLine()
            ));

        } catch (\ExternalApiErrorException $exception) {

            // 子クラスによる例外スローを含む処理

            // 外部APIのソフトウェアエラーをロギング
            $this->logger->error(sprintf(
                "ERROR: %s at %s line %s",
                $exception->getMessage(),
                $exception->getFile(),
                $exception->getLine()
            ));

        } catch (\Exception $exception) {

            // 子クラスによる例外スローを含む処理

            // その他 (例:自社ソフトウェアなど) によるエラーをロギング
            $this->logger->error(sprintf(
                "ERROR: %s at %s line %s",
                $exception->getMessage(),
                $exception->getFile(),
                $exception->getLine()
            ));
        }

        // 問題なければTRUEを返却。
        return true;
    }
}