Laravelのエラーハンドラのカスタマイズ
前節で、Laravelのエラー表示の切り替えとエラー画面のカスタマイズの方法がわかってもらえたと思います。そのエラーのカスタマイズをもう少し掘り下げていきます。
Handlerクラス
Laravelのエラーハンドラの中心となるクラスは、\App\Exceptions\Handlerです。このクラスは、プロジェクトを作成した時点で、あらかじめ作成されています。そのコードをリスト2に抜粋します。
<?php namespace App\Exceptions; use Exception; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; class Handler extends ExceptionHandler // (1) { protected $dontReport = [ // (2) ]; protected $dontFlash = [ // (3) 〜省略〜 ]; public function report(Exception $exception) // (4) { parent::report($exception); // (5) } public function render($request, Exception $exception) // (6) { return parent::render($request, $exception); // (7) } }
Handlerクラスは、(1)にあるように、\Illuminate\Foundation\Exceptions\Handlerクラスを継承して作られています。
メソッドとしては、(4)のreport()と(6)のrender()の2個が定義されています。
先述のように、アプリケーション内で発生したすべての例外は、Laravelが処理します。その際に、このHandlerクラスのreport()メソッドとrender()メソッドが呼び出される仕組みとなっています。そして、その両方とも、プロジェクトが作成された時点でのコード(初期コード)では、親クラスの同名メソッドを呼び出しているにすぎません。ということは、この両メソッドを適切にカスタマイズすることで、エラーハンドラをカスタマイズできるようになります。
では、この両メソッドにはどのような働きがあり、どのようにカスタマイズすればいいのでしょうか。次はそれを見ていきます。
ログへの書き出し処理を行うreport()メソッド
リスト2の(4)のreport()メソッドは、例外が発生した際の記録処理を行うメソッドとして呼び出されます。初期コードである(5)では、親クラスのreport()メソッドを呼び出しています。このコードのおかげで、その時点で設定されたログの書き出し方式を利用して、すべての例外内容がログに記録されます。
なお、Laravelのデフォルトのログの書き出し方式は、プロジェクトディレクトリ内のstorage/logsディレクトリに、laravel-2020-02-10.logのように「laravel-日付.log」のファイルが日次で作られるようになっています。
このreport()メソッドに処理を記述することで、ある特定の例外が発生した場合に対して、その例外の記録方式をカスタマイズすることができます。例えば、独自に作成した例外クラスであるFatalProcessExceptionがアプリケーション内で発生するとして、この例外が発生した場合は、ログに記録するだけでなくメールも送信する、といったカスタマイズが考えられます。その場合は、次のようなコードを記述します。なお、実際のメール送信コードは割愛し、コメントのみの記載としています。
public function report(Exception $exception) { if($exception instanceof FatalProcessException) { // * // メール送信処理 } parent::report($exception); }
report()メソッドの引数であるException型の$exceptionは、発生した例外そのものです。したがって、上記コード中の*の行にあるように、この引数の型を、instanceof演算子を使って特定の例外クラスと比較した条件分岐を記述することで、その例外が発生した際の記録処理を独自にカスタマイズできます。
上記コードでは、FatalProcessExceptionの場合のみを記述していますが、次のようにelseifブロックを積み重ねることで、様々な例外に対して独自の処理を記述できます。
public function report(Exception $exception) { if($exception instanceof FatalProcessException) { // メール送信処理 } elseif($exception instanceof TooManyException) { // 何かの記録処理 } elseif($exception instanceof TooMuchException) { // 何かの記録処理 } parent::report($exception); }
上記コードでは、どの例外の場合でも、最終的に親クラスのreport()が呼び出されます。ということは、デフォルトの記録処理が行われた上で、FatalProcessExceptionやTooManyException、TooMuchExceptionの場合は、追加の記録処理が行われることを意味します。
一方、次のように、parent::report()をelseブロックに入れることで、デフォルトの記録処理が行われる場合を限定することもできます。
public function report(Exception $exception) { if($exception instanceof FatalProcessException) { // メール送信処理 } elseif($exception instanceof TooManyException) { // 何かの記録処理 } elseif($exception instanceof TooMuchException) { // 何かの記録処理 } else { parent::report($exception); } }
このように、発生する例外それぞれに応じて、様々な処理が記述できるようになります。
記録処理から除外する$dontReportプロパティ
前節での例は、特定の例外の場合に特別な記録処理を行うカスタマイズでした。これとは逆に、記録処理を全く行わないようにしたい例外クラスもあるでしょう。その場合は、report()メソッド内にコードを記述する必要はなく、$dontReportプロパティを使います。$dontReportプロパティは配列となっていますので、次のコードのように除外する例外クラスの完全修飾名を追加しておけば、その例外が発生した場合は、デフォルトの記録処理、つまり、parent::report()内の処理が実行されず、ログへの記録が行われなくなります。
protected $dontReport = [ \Illuminate\Auth\AuthenticationException::class, \App\Exceptions\DebugProcessException::class ];
上記コードでは、Laravelで用意されている認証機能によって、未認証の場合に発生する例外であるAuthenticationExceptionと、独自例外クラスであるDebugProcessExceptionが発生した場合は、ログへの記録が行われなくなります。
なお、リスト2のHandlerクラスには、プロパティとしてもうひとつ$dontFlashというのが記述されています。こちらのプロパティは、Laravelのバリデーション機能で利用されるものです。本稿の内容とは直接関係ないものですので、解説を省略します。
画面表示を行うrender()メソッド
report()メソッドと同じく、例外発生時に呼び出されるメソッドが、リスト2の(6)のrender()メソッドです。こちらも、report()メソッド同様に、初期コードとしては(7)の親クラスのrender()メソッドの呼び出し処理が記述されているだけです。
このメソッドでは、例外が発生した時に、そのクライアントに返すHTTPレスポンスを生成する処理を行います。通常は、クライアントはブラウザであり、HTTPレスポンスとしてはそのブラウザに表示するHTMLデータです。実は、前節で紹介した図1や図2のLaravelのエラー画面が表示されたり、独自に用意した500.blade.phpなどのエラー画面テンプレートが表示されたりするのは、リスト2の(7)のparent::render()の処理のおかげなのです。
そして、特定の例外が発生した場合に、デフォルトのエラー画面表示処理とは別の処理を行いたい場合は、report()メソッド同様に、発生した例外クラスを表す引数$exceptionの型を元に条件分岐を記述すれば可能です。
例えば、アプリケーション内でデータベース処理に失敗した際に、独自例外としてDataAccessExceptionが発生するとします。しかも、その際にDataAccessExceptionインスタンス内にどのようにデータベース処理に失敗したかのメッセージが格納されているとします。それを表示する独自エラー画面テンプレートとして、resources/views/errorsディレクトリ内にcustom.blade.phpがあるとします。それらを使って、次のようなコードが記述できます。
public function render($request, Exception $exception) { if($exception instanceof DataAccessException) { // (1) $data["errorMsg"] = $exception->getMessage(); // (2) return response()->view("errors.custom", $data); // (3) } return parent::render($request, $exception); }
report()で行なった条件分岐と同じ考え方で、発生した例外の型がDataAccessExceptionかどうかの条件分岐が、上記コードの(1)です。このifブロック内で独自テンプレートであるcustom.blade.phpを表示させる処理を行なっています。それが(3)です。Laravelのヘルパー関数であるresponse()を利用して、レスポンスオブジェクトを生成します。そのレスポンスオブジェクトに対してview()メソッドを実行することで、テンプレートファイルを元にしたレスポンスデータを生成できます。このview()メソッドは、コントローラクラスなどで、テンプレートファイルを元に画面表示を行うview()関数と同じです。その第2引数として渡すテンプレート変数用連想配列を、例外インスタンスに格納されたメッセージを元に生成しているのが(2)です。
このようなコードを記述することで、特定の例外に対して特定のエラー画面を表示させることが可能となります。
なお、render()メソッドの第1引数である$requestは、リクエストオブジェクトです。この引数を使うと、現在のリクエストに関係するセッションやパラメータなどのデータを取り出すこともできます。