Miles' Blog

天涯何處無幹話,何必要講實務話

來看看 Collection 吧!

Laravel 用來處理 array 的小幫手,至今已知道有三種類型:第一種就是 [helpers.php][] 所提供的函式,第二種則是 Arr 類別所提供的靜態方法,今天要講的是第三種--Collection

Collection 與原生 PHP 函式之間的關係

這三種方法之間都有類似的處理方法。不過,我們先來看 Collection 哪些方法跟原生 PHP 提供的函式很像,如 array_map() 函式與 Collection::map() 方法:

$data = [1, 2, 3];

array_map(function($value) {
return $value * 2;
}, $data);

// [2, 4, 6]

collect($data)->map(function($value) {
return $value * 2;
});

// [2, 4, 6]

原生 PHP 函式被垢病最大的缺點就是:參數位置不統一,一下 array 在前,一下在後。Collection 帶來的好處就是參數容易猜,比較不容易出錯。

map()

今天先來看看很常用的 map() 方法:

public function map(callable $callback)
{
$keys = array_keys($this->items);

$items = array_map($callback, $this->items, $keys);

return new static(array_combine($keys, $items));
}

它也會呼叫原生的 array_map(),但因 map 意義比較像用同個一方法來轉換所有元素,並產生另一個新的元素。以此條件來看 array_map() 就顯得有點不好用,因為它的問題如下:

// 沒辦法拿到 $key
array_map(function($value, $key) {

}, $items);

// 可以拿到 $key,可是產生的新 array 並非是以 $key 為 key
$newItems = array_map(function($value, $key) {

}, $items, array_keys($items));

// 必須要使用 array_combine() 來重組 array
array_combine($keys, $newItems);

不過 array_map() 也是有它奇妙的用法:

$data1 = [1, 2, 3];
$data2 = [4, 5, 6];
$data3 = [7, 8, 9];

array_map(function($data1, $data2, $data3) {
return $data1 + $data2 + $data3;
}, $data1, $data2, $data3);

// [12, 15, 18]

這三個都是 Laravel 所提供的 helpers 函式。剛好今天聽到有人提到這個問題,所以就來翻看看。

單就註解與介面來看這三個函式:

/**
* Get an item from an array using "dot" notation.
*/
function array_get($array, $key, $default = null);

/**
* Get an item from an array or object using "dot" notation.
*/
function data_get($target, $key, $default = null);

/**
* Get an item from an object using "dot" notation.
*/
function object_get($object, $key, $default = null);

可以約略知道 array_get()object_get() 分別在處理 array 與 object,而 data_get() 則是混合的。

array_get()

接著來看 array_get() 原始碼:

function array_get($array, $key, $default = null)
{
return Arr::get($array, $key, $default);
}

它是把任務交付給 Arr 類別處理的

會這樣做有兩種可能:一種是因為 4.1 版以前,並沒有 Arr 類別,這是為了符合過去的習慣所留下來的,但這個可能性較小。較有可能是因為 array 處理越來越複雜,為了讓檔案可以 SRP,所以就另外寫了一個靜態類別來處理。

public static function get($array, $key, $default = null)
{
// 如果不是一個可用的 array,就回傳預設值
if (! static::accessible($array)) {
return value($default);
}

// key 是 null 的話,就回傳整個 array 回去
if (is_null($key)) {
return $array;
}

// 當存在就回傳
if (static::exists($array, $key)) {
return $array[$key];
}

// 如果找不到 `.` 的話,而直接存取也沒值,那就回傳 default 值
if (strpos($key, '.') === false) {
return $array[$key] ?? value($default);
}

// 如果有 `.` 的話,就一階一階找看看。
foreach (explode('.', $key) as $segment) {
if (static::accessible($array) && static::exists($array, $segment)) {
$array = $array[$segment];
} else {
return value($default);
}
}

// 把最後找到的結果回傳
return $array;
}

public static function accessible($value)
{
// 可用的 array,要嘛是原生 array,不然就是實作了 ArrayAccess
return is_array($value) || $value instanceof ArrayAccess;
}

public static function exists($array, $key)
{
// ArrayAccess 實例
if ($array instanceof ArrayAccess) {
return $array->offsetExists($key);
}

// 原生 array
return array_key_exists($key, $array);
}

這裡面不意外的,都是使用 array 的方法在取內容。

object_get()

類似地,object_get() 則是對 object 型態的變數取資料:

function object_get($object, $key, $default = null)
{
// 如果 key 是空的,就把整個 object 回傳
if (is_null($key) || trim($key) == '') {
return $object;
}

// 依續把每個階層的 key,用來取得 object 的屬性
foreach (explode('.', $key) as $segment) {
// 如果不是 object 或屬性不存在的時候,就回傳預設值
if (! is_object($object) || ! isset($object->{$segment})) {
return value($default);
}

// 重設 object 為下一個階層
$object = $object->{$segment};
}

// 最後取得的 property 即結果
return $object;
}

整個過程都是使用 object 的取屬性方法(->)。

data_get()

array_get() 必須要所有階層都是 array 或 ArrayAccess 實例,才能正常地使用。object_get() 則要每一階層都是 object。

如果是 array 包 object 或相反,就需要靠 data_get() 了。因為它除了同時支援兩種取資料方法,還另外實作了 * 的取資料方法,所以原始碼也比較複雜的:

function data_get($target, $key, $default = null)
{
// key 是空的就直接回傳
if (is_null($key)) {
return $target;
}

// key 除了用 `.` 區隔外,也可以使用 array
$key = is_array($key) ? $key : explode('.', $key);

// 將 key 一個一個拿出來跑
while (! is_null($segment = array_shift($key))) {
// 如果是 * 的話
if ($segment === '*') {
if ($target instanceof Collection) {
// 是 Collection 的話,把裡面的 item 拿出來
$target = $target->all();
} elseif (! is_array($target)) {
// 如果不是 Collection,也不是 array 的話,代表取資料的 key 有問題,直接回預設值
return value($default);
}

// 依 key 取得裡面的 value,然後重組成 array
$result = Arr::pluck($target, $key);

// 如果剩下的 key 還有 * 的話,就使用 collapse 把結果打平成一維 array,如果沒有 * 的話就直接回傳
return in_array('*', $key) ? Arr::collapse($result) : $result;
}

if (Arr::accessible($target) && Arr::exists($target, $segment)) {
// 如果 $target 是個 array 且有資料的話,就使用 array 方法取資料
$target = $target[$segment];
} elseif (is_object($target) && isset($target->{$segment})) {
// 如果 $target 是個 object 且有資料的話,就使用 object 方法取資料
$target = $target->{$segment};
} else {
都不是的話,key 是有問題的,只好回傳預設值
return value($default);
}
}

// 回傳最後取到的 target
return $target;
}

* 的用法,比方說在分析 Routing(5)曾提到 addToCollections() 會建立查詢表:

$domainAndUri = $route->getDomain().$route->uri();

foreach ($route->methods() as $method) {
$this->routes[$method][$domainAndUri] = $route;
}

下面是應用方法:

// 取得所有 GET 方法的 route,並轉換成一維陣列
data_get($this->routes, 'get.*');

// 取得所有 URI 是 `/user` 的 route,並轉換成一維陣列
data_get($this->routes, '*./user');

三種方法都可以像 Javascript 使用 . 來取得多維陣列或 object 的取,像 config() 在取設定的時候,正是使用這些方法。

平常如果要對一堆資料操作的話,還是都會選擇使用 Collection,未來有機會再來翻翻它的原始碼。

不同公司所制定的系統環境規範都有所不同。

比方說環境變數的設定,敝公司的規範如下:

  1. 不設定系統環境變數 /etc/environment,而是使用檔案載入
  2. 系統人員將會把檔案放在 /path/to/.env
  3. 部分的環境設定系統人員不清楚,會由開發人員設定。換句話說,還有另一個 env 是跟著專案跑的。

上述會有幾個要點要注意:

  1. Laravel 預設只有載入 /path/to/project/.env,而上面的規範,還會多載 /path/to/.env
  2. 系統人員設定的 .env 開發人員不能隨意覆蓋

很直接的 Workaround

最一開始,使用最蠢的 workaround,直接修改 bootstrap/app.php

// 在 return Application 前,載入該載的 env 即可
if (file_exists('/path/to/.env')) {
(new Dotenv\Dotenv('/path/to'))->load();
}

return $app;

簡單,也很有效。只是有點不明顯,在重建環境時,很容易忘了這件事。

自定義新的 Bootstrapper

分析 bootstrap 流程有提到原本的 .env 如何載入,我們想辦法來客製化這個流程,讓載 env 也成為 Laravel Bootstrap 的流程之一。

一個直接做法是,自己寫一個 bootstrap:

<?php
// app/Bootstrap/CustomizeLoadEnvironmentVariables.php
namespace App\Bootstrap;

use Dotenv\Dotenv;
use Illuminate\Contracts\Foundation\Application;

class CustomizeLoadEnvironmentVariables
{
public function bootstrap(Application $app)
{
if (file_exists('/path/to/.env')) {
(new Dotenv('/path/to'))->load();
}
}
}

然後在 Http Kernel 覆寫 bootstrappers 屬性即可:

protected $bootstrappers = [
App\Bootstrap\CustomizeLoadEnvironmentVariables::class,
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class,
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];

覆寫 Bootstrapper

自定義新的 Bootstrapper 的缺點是,會需要覆寫一個修改內容不多的 bootstrappers,某種程度這也算是 copy & paste 的產物,這是不符合 DRY 原則的。

這裡還有另一個做法則是拿原有的來覆寫:

<?php

namespace App\Bootstrap;

use Dotenv\Dotenv;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables as BaseLoadEnvironmentVariables;

class LoadEnvironmentVariables extends BaseLoadEnvironmentVariables
{
public function bootstrap(Application $app)
{
if (file_exists('/path/to/.env')) {
(new Dotenv('/path/to'))->load();
}

parent::bootstrap($app);
}
}

接著在 bootstrap/app.php 綁定這個實作即可:

// ======== Customize Binding ========

$app->singleton(
Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
App\Bootstrap\LoadEnvironmentVariables::class
);

可以參考分析 bootstrap 流程提到的如何產生 bootstrapper 實例,以及 Containerbind() 如何實作,即可了解為何這個做法是可行的。

而 Http Kernel 就不需要覆寫了。這也是目前的做法。

續昨天,先來列一下 Http Kernel 預設有哪些 middleware:

protected $middleware = [
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class,
];

// 為簡化問題,我們來看 web 的就好
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];

但查過原始碼,雖然有的 middleware 有調整過 response 內容,但並沒有任何一個 middleware 去改過 header,理論上是不會受影響的。但我們應該用驗證來證實程式是如我們所預期的,因此跟朋友確認程式內容,並實際做了一點實驗。

測試驗證

首先得知版本為 Laravel v5.7.0,我們先打開專案,來寫個 Feature 測試如下:

public function testBasicTest()
{
$routeMiddleware = function ($request, $next) {
return $next($request)
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
};

/** @var Router $router */
$router = $this->app->make('router');
$router->middleware(['web', $routeMiddleware])->get('/foo', function () {
return '';
});

$this->get('/foo')
->assertStatus(200)
->assertHeader('Access-Control-Allow-Origin', '*')
->assertHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
}

對於設定 Router 的方法有疑問的話,可以參考之前分析 Routing 文章。

這個測試是通過的,但畢竟這只是程式上運行,跟啟用 web 服務或許會有落差,所以實際照著朋友的做法做一次:

建立 Cors middleware:

<?php

namespace App\Http\Middleware;

class Cors
{
public function handle($request, $next)
{
return $next($request)
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
}
}

Kernel 加上 alias:

protected $routeMiddleware = [
// 略
'cors' => \App\Http\Middleware\Cors::class,
];

routes/api.php 加上測試用 route:

Route::middleware(['api', 'cors'])->group(function() {
Route::get('/foo', function () {
return '';
});
});

接著使用 Artisan 指令啟動服務:

php artisan serve

然後打開 http://localhost/api/foo 即可看到剛剛建立的測試 route。

驗證一切正常,該有的 header 是有的,所以可以推測是使用或測試的方法出錯導致錯誤的。


這篇記錄並不是責怪朋友,主要是想分享:這是從發現問題,到確認問題在或不在某個範圍內的過程。可以看到我們一開始從原始碼開始說明,但原始碼畢竟只是紙上談兵,所以也使用單元測試驗證,也做了實際整合程式的驗證。

開發程式的過程中,也會遇到許多奇怪的問題,我們可以像一開始一樣,直接翻原始碼確認,但時間容易拖很長,而且最後的結果依然不是可靠的;或許直接寫單元測試會是個可行的做法,寫測試即可馬上驗證想法是否正確,而且真的是一翻兩瞪眼啊!

前一陣子,朋友在社群分享小知識。

$routeMiddleware 裡面的 middleware 加上 $header 的話會有問題,要在 $middleware 宣告才能保證生效

因為剛好正在研究 Laravel 原始碼,所以就認真翻了一下。

Response header 是在分析 bootstrap 流程有提到 index.php 的這行程式碼才會送出:

$response->send();

因此只要了解 $response 如何建構,以及 Pipeline 經過了哪些關卡、被做了哪些修改,就能知道真正問題點在哪了。

Response 如何被建構出來的

在 middleware 裡,我們會預期 $next($request) 回來的結果會是 [Response][] 物件。這可以在 Router 裡找到蛛絲馬跡。在分析 Routing 時,有提到 runRouteWithinStack() 的實作,裡面在執行 Pipeline 的程式碼如下:

return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then(function ($request) use ($route) {
// 實際執行 route 的地方在這裡:$route->run()
return $this->prepareResponse(
$request, $route->run()
);
});

根據 Pipeline 的分析,$next($request) 拿到的結果,正是 prepareResponse() 的回傳結果。換句話說,它就是建構 Response 的方法。

回顧一下 prepareResponse() 的實作,事實上有機會對 Response 做處理的,只有下面這兩個方法:

$response->setNotModified();

$response->prepare($request);

解析 Middleware 的實作細節的時候,有提到 Pipeline 會被使用兩次,一次是上面所說的,也就是朋友所提到的 $routeMiddleware;另一次則是 $middleware,也就是在 Http Kernel 的這段程式碼:

protected function sendRequestThroughRouter($request)
{
// 略

return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}

dispatchToRouter() 一直到 runRouteWithinStack() 的 call stack 如下:

Kernel::dispatchToRouter();
Router::dispatch();
Router::dispatchToRoute();
Router::runRoute();
Router::runRouteWithinStack();

這過程中,還有另一次 prepareResponse(),但如同上面 runRouteWithinStack() 所說,其實對 header 並沒有特別修改什麼。

因此可以知道建構與 Pipeline 過程,理論上不會影響 response header。


今天先看建構過程與 Pipeline 流程是否有問題,明天再來看預設的 middleware 是否有偷偷對 header 做什麼處理。

假設 routes 定義了以下路由:

Route::get('/', 'IndexController@welcome')->name('welcome');

我們可以使用下面的方法取得 URL:

route('welcome');

先大概了解這個實作方法:

function route($name, $parameters = [], $absolute = true)
{
return app('url')->route($name, $parameters, $absolute);
}

這個 url 曾在一開始分析 Routing 的時候提到過,它所對應的類別是 UrlGenerator,註冊方法如下:

$this->app->singleton('url', function ($app) {
$routes = $app['router']->getRoutes();

$app->instance('routes', $routes);

// UrlGenerator 建構子需要 RouteCollection 與 Request
$url = new UrlGenerator(
$routes, $app->rebinding(
'request', $this->requestRebinder()
)
);

// Session 解析器
$url->setSessionResolver(function () {
return $this->app['session'];
});

// APP_KEY 解析器
$url->setKeyResolver(function () {
return $this->app->make('config')->get('app.key');
});

// 當 RouteCollection 觸發 rebind 事件時,就重新設定給 UrlGenerator
$app->rebinding('routes', function ($app, $routes) {
$app['url']->setRoutes($routes);
});

return $url;
});

route() 函式,實際上是呼叫 UrlGenerator 的 route() 方法

public function route($name, $parameters = [], $absolute = true)
{
// 看看 RouteCollection 用名稱能不能找到
if (! is_null($route = $this->routes->getByName($name))) {
// 找到將會使用 RouteUrlGenerator 產生 url
return $this->toRoute($route, $parameters, $absolute);
}

// 找不到就讓它爆
throw new InvalidArgumentException("Route [{$name}] not defined.");
}

接著來看看 Redirector。Laravel 也有提供 helper 函式:

function redirect($to = null, $status = 302, $headers = [], $secure = null)
{
if (is_null($to)) {
return app('redirect');
}

return app('redirect')->to($to, $status, $headers, $secure);
}

這個函式提供兩種用法:

redirect(); // 取得 Redirector
redirect('/path'); // 取得 RedirectResponse

註冊方法如下:

$this->app->singleton('redirect', function ($app) {
$redirector = new Redirector($app['url']);

// 如果有 session 的話就設定
if (isset($app['session.store'])) {
$redirector->setSession($app['session.store']);
}

return $redirector;
});

Redirector 建構需要有 UrlGenerator。也因此兩個 helper 函式搭配後,下面的寫法都是通的:

redirect(route('welcome'));
redirect()->to(route('welcome'));
redirect()->route('welcome');
redirect()->action('IndexController@welcome');

這樣的設計是很有趣且可以學習的,Redirector 本身提供了基本的 to()away() 方法,可以導頁到指定的地方外,配合 UrlGenerator 與 Session 還可以做到 route()action()back() 等,與 Application 本身或狀態相關的導頁。

這是一個使用物件組合功能的好例子。

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

// Illuminate\Foundation\Application::abort()

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)) {
// 如果 Exception 有實作 render() 方法,就呼叫它並回傳
return Router::toResponse($request, $response);
} elseif ($e instanceof Responsable) {
// 如果 Exception 可以轉換成 response 就直接轉
return $e->toResponse($request);
}

// 這裡 Laravel 會把某幾個特定的 Exception 換成合適的 HttpException
$e = $this->prepareException($e);

if ($e instanceof HttpResponseException) {
// 有另外包一個 Response 的 Exception
return $e->getResponse();
} elseif ($e instanceof AuthenticationException) {
// 未認證 Exception
return $this->unauthenticated($request, $e);
} elseif ($e instanceof ValidationException) {
// 未驗證 Exception
return $this->convertValidationExceptionToResponse($e, $request);
}

// 如果 request 預期要 JSON 則準備 JSON response,反正準備一般的 response
return $request->expectsJson()
? $this->prepareJsonResponse($request, $e)
: $this->prepareResponse($request, $e);
}

普通的 Exception 與 HttpException 都不符合上面判斷的條件,因此會到最下面。因為是錯誤頁,所以 expectsJson() 將會回傳 false,而回傳 prepareResponse() 的結果

protected function prepareResponse($request, Exception $e)
{
// 如果不是 HttpException,且 debug 模式開啟的時候
if (! $this->isHttpException($e) && config('app.debug')) {
// 使用 convertExceptionToResponse() 方法,產生 exception 專用的 response
return $this->toIlluminateResponse($this->convertExceptionToResponse($e), $e);
}

// 如果不是 isHttpException,且 debug 模式「關閉」的時候,把 exception 轉成 HttpException
if (! $this->isHttpException($e)) {
$e = new HttpException(500, $e->getMessage());
}

// 將 HttpException 轉換成 response
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)
{
// 註冊 errors namespace,也就是文件裡提到的 resources/views/errors/*
$this->registerErrorViewPaths();

// 如果 errors 裡面有找到對應 status code 的樣版,就輸出它。
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() 來處理錯誤即可。

這麼多天以來,看了很多 Laravel 的程式,其實可以發現它有一些常見的模式。沒有好壞,只是一種可參考的寫法。

閱讀全文 »
0%