官方 有提到自定義錯誤頁可以如何簡單達成。遇到的問題是,想自定義錯誤頁,並在 debug 模式下,當隨意丟例外的時候,要在頁面某個地方列出 call stack trace。
從今天開始,會開始換來分享實作功能中遇到問題,而去追原始碼的過程。不知道能持續多久,就繼續寫吧!
結論先講:這無法單純使用自定義錯誤頁實作出來的,需要客製化某些程式才有辦法做。因為只是 debug 要用,所以就立馬放棄了。
文件有提到自定義錯誤頁會接到 abort()
函式產生的 HttpException 並注入頁面的 $exception 變數。
function abort ($code , $message = '' , array $headers = [] ) { if ($code instanceof Response) { throw new HttpResponseException ($code ); } elseif ($code instanceof Responsable) { throw new HttpResponseException ($code ->toResponse (request ())); } app ()->abort ($code , $message , $headers ); } public function abort ($code , $message = '' , array $headers = [] ) { if ($code == 404 ) { throw new NotFoundHttpException ($message ); } throw new HttpException ($code , $message , null , $headers ); }
從上面可以了解 abort()
的任務都是丟例外,因此我們首先要關注的應該是錯誤處理。
Error Handler 在 Pipeline 曾提到,Routing\Pipeline 繼承 Pipeline 後有覆寫一段程式,正是在做錯誤處理:
try { return $destination ($passable ); } catch (Exception $e ) { return $this ->handleException ($passable , $e ); } catch (Throwable $e ) { return $this ->handleException ($passable , new FatalThrowableError ($e )); }
而在分析 bootstrap 流程 也提過,從 request 產出 response 的 sendRequestThroughRouter()
方法裡面,是最一開始呼叫 Pipeline 的地方:
return (new Pipeline ($this ->app)) ->send ($request ) ->through ($this ->app->shouldSkipMiddleware () ? [] : $this ->middleware) ->then ($this ->dispatchToRouter ());
從以上兩點可以得知,Routing\Pipeline 所做的錯誤處理的有效範圍,從進全域的 middleware 開始,到全域的 middleware 回傳最後的 response 之後結束。
handleException()
回顧如下:
protected function handleException ($passable , Exception $e ) { if (! $this ->container->bound (ExceptionHandler ::class ) || ! $passable instanceof Request) { throw $e ; } $handler = $this ->container->make (ExceptionHandler ::class ); $handler ->report ($e ); $response = $handler ->render ($passable , $e ); if (method_exists ($response , 'withException' )) { $response ->withException ($e ); } return $response ; }
這段程式碼會看到,它使用了 ExceptionHandler::render()
方法,產生錯誤的 response,接著才回傳出去給 Http Kernel 處理。Laravel 預設會另外建一個 Handler.php
檔繼承 ExceptionHandler,然後覆寫 render()
:
public function render ($request , Exception $exception ) { return parent ::render ($request , $exception ); }
如果有要自定義處理 Exception,可以在這裡做。預設的 render()
實作如下:
public function render ($request , Exception $e ) { if (method_exists ($e , 'render' ) && $response = $e ->render ($request )) { return Router ::toResponse ($request , $response ); } elseif ($e instanceof Responsable) { return $e ->toResponse ($request ); } $e = $this ->prepareException ($e ); if ($e instanceof HttpResponseException) { return $e ->getResponse (); } elseif ($e instanceof AuthenticationException) { return $this ->unauthenticated ($request , $e ); } elseif ($e instanceof ValidationException) { return $this ->convertValidationExceptionToResponse ($e , $request ); } return $request ->expectsJson () ? $this ->prepareJsonResponse ($request , $e ) : $this ->prepareResponse ($request , $e ); }
普通的 Exception 與 HttpException 都不符合上面判斷的條件,因此會到最下面。因為是錯誤頁,所以 expectsJson()
將會回傳 false,而回傳 prepareResponse()
的結果
protected function prepareResponse ($request , Exception $e ) { if (! $this ->isHttpException ($e ) && config ('app.debug' )) { return $this ->toIlluminateResponse ($this ->convertExceptionToResponse ($e ), $e ); } if (! $this ->isHttpException ($e )) { $e = new HttpException (500 , $e ->getMessage ()); } return $this ->toIlluminateResponse ( $this ->renderHttpException ($e ), $e ); }
第一個判斷裡面輸出的結果,事實上就是平常看到的 call stack trace 頁,主要在裡面找到的 renderExceptionContent()
方法
protected function convertExceptionToResponse (Exception $e ) { return SymfonyResponse ::create ( $this ->renderExceptionContent ($e ), $this ->isHttpException ($e ) ? $e ->getStatusCode () : 500 , $this ->isHttpException ($e ) ? $e ->getHeaders () : [] ); } protected function renderExceptionContent (Exception $e ) { try { return config ('app.debug' ) && class_exists (Whoops ::class ) ? $this ->renderExceptionWithWhoops ($e ) : $this ->renderExceptionWithSymfony ($e , config ('app.debug' )); } catch (Exception $e ) { return $this ->renderExceptionWithSymfony ($e , config ('app.debug' )); } }
renderExceptionWithSymfony()
將會依照 debug 參數的開或關,而定輸出的內容有沒有 call stack trace。
而自定義錯誤頁的實作在 renderHttpException()
裡:
protected function renderHttpException (HttpException $e ) { $this ->registerErrorViewPaths (); if (view ()->exists ($view = "errors::{$e->getStatusCode()} " )) { return response ()->view ($view , [ 'errors' => new ViewErrorBag , 'exception' => $e , ], $e ->getStatusCode (), $e ->getHeaders ()); } return $this ->convertExceptionToResponse ($e ); }
從上面的分析可以知道,當 debug 模式開啟的時候,隨意丟例外是會符合 prepareResponse()
第一個判斷,並輸出預設的 call stack trace 頁面;丟 HttpException 才有辦法進到自定義錯誤頁。
了解了 Laravel 錯誤處理機制之後可以發現,大部分可預期的狀況丟 HttpException 或使用 abort()
處理錯誤比較適合,這也是 Laravel 預期的。自定義例外可以實作 render()
讓 Handler 自動處理。遇到的狀況則是第三方 library 使用過程中丟例外,使用 try catch 配合 abort()
來處理錯誤即可。