官方有提到自定義錯誤頁可以如何簡單達成。遇到的問題是,想自定義錯誤頁,並在 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()
來處理錯誤即可。