異常處理是編程中十分重要但也最容易被人忽視的語言特性,它爲開發者提供了處理程序運行時錯誤的機制,對於程序設計來講正確的異常處理可以防止泄露程序自身細節給用戶,給開發者提供完整的錯誤回溯堆棧,同時也能提升程序的健壯性。php
這篇文章咱們來簡單梳理一下Laravel中提供的異常處理能力,而後講一些在開發中使用異常處理的實踐,如何使用自定義異常、如何擴展Laravel的異常處理能力。laravel
這裏又要回到咱們說過不少次的Kernel處理請求前的bootstrap階段,在bootstrap階段的Illuminate\Foundation\Bootstrap\HandleExceptions
部分中Laravel設置了系統異常處理行爲並註冊了全局的異常處理器:git
class HandleExceptions
{
public function bootstrap(Application $app)
{
$this->app = $app;
error_reporting(-1);
set_error_handler([$this, 'handleError']);
set_exception_handler([$this, 'handleException']);
register_shutdown_function([$this, 'handleShutdown']);
if (! $app->environment('testing')) {
ini_set('display_errors', 'Off');
}
}
public function handleError($level, $message, $file = '', $line = 0, $context = [])
{
if (error_reporting() & $level) {
throw new ErrorException($message, 0, $level, $file, $line);
}
}
}
複製代碼
set_exception_handler([$this, 'handleException'])
將HandleExceptions
的handleException
方法註冊爲程序的全局處理器方法:github
public function handleException($e)
{
if (! $e instanceof Exception) {
$e = new FatalThrowableError($e);
}
$this->getExceptionHandler()->report($e);
if ($this->app->runningInConsole()) {
$this->renderForConsole($e);
} else {
$this->renderHttpResponse($e);
}
}
protected function getExceptionHandler()
{
return $this->app->make(ExceptionHandler::class);
}
// 渲染CLI請求的異常響應
protected function renderForConsole(Exception $e)
{
$this->getExceptionHandler()->renderForConsole(new ConsoleOutput, $e);
}
// 渲染HTTP請求的異常響應
protected function renderHttpResponse(Exception $e)
{
$this->getExceptionHandler()->render($this->app['request'], $e)->send();
}
複製代碼
在處理器裏主要經過ExceptionHandler
的report
方法上報異常、這裏是記錄異常到storage/laravel.log
文件中,而後根據請求類型渲染異常的響應生成輸出給到客戶端。這裏的ExceptionHandler就是\App\Exceptions\Handler
類的實例,它是在項目最開始註冊到服務容器中的:數據庫
// bootstrap/app.php
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
*/
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);
/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
*/
......
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
複製代碼
這裏再順便說一下set_error_handler
函數,它的做用是註冊錯誤處理器函數,由於在一些年代久遠的代碼或者類庫中大可能是採用PHP那件函數trigger_error
函數來拋出錯誤的,異常處理器只能處理Exception不能處理Error,因此爲了可以兼容老類庫一般都會使用set_error_handler
註冊全局的錯誤處理器方法,在方法中捕獲到錯誤後將錯誤轉化成異常再從新拋出,這樣項目中全部的代碼沒有被正確執行時都能拋出異常實例了。編程
/**
* Convert PHP errors to ErrorException instances.
*
* @param int $level
* @param string $message
* @param string $file
* @param int $line
* @param array $context
* @return void
*
* @throws \ErrorException
*/
public function handleError($level, $message, $file = '', $line = 0, $context = [])
{
if (error_reporting() & $level) {
throw new ErrorException($message, 0, $level, $file, $line);
}
}
複製代碼
Laravel
中針對常見的程序異常狀況拋出了相應的異常實例,這讓開發者可以捕獲這些運行時異常並根據本身的須要來作後續處理(好比:在catch中調用另一個補救方法、記錄異常到日誌文件、發送報警郵件、短信)bootstrap
在這裏我列一些開發中常遇到異常,並說明他們是在什麼狀況下被拋出的,平時編碼中必定要注意在程序裏捕獲這些異常作好異常處理才能讓程序更健壯。數組
Illuminate\Database\QueryException
Laravel中執行SQL語句發生錯誤時會拋出此異常,它也是使用率最高的異常,用來捕獲SQL執行錯誤,比方執行Update語句時不少人喜歡判斷SQL執行後判斷被修改的行數來判斷UPDATE是否成功,但有的情景裏執行的UPDATE語句並無修改記錄值,這種狀況就無法經過被修改函數來判斷UPDATE是否成功了,另外在事務執行中若是捕獲到QueryException 能夠在catch代碼塊中回滾事務。Illuminate\Database\Eloquent\ModelNotFoundException
經過模型的findOrFail
和firstOrFail
方法獲取單條記錄時若是沒有找到會拋出這個異常(find
和first
找不到數據時會返回NULL)。Illuminate\Validation\ValidationException
請求未經過Laravel的FormValidator驗證時會拋出此異常。Illuminate\Auth\Access\AuthorizationException
用戶請求未經過Laravel的策略(Policy)驗證時拋出此異常Symfony\Component\Routing\Exception\MethodNotAllowedException
請求路由時HTTP Method不正確Illuminate\Http\Exceptions\HttpResponseException
Laravel的處理HTTP請求不成功時拋出此異常上面說了Laravel把\App\Exceptions\Handler
註冊成功了全局的異常處理器,代碼中沒有被catch
到的異常,最後都會被\App\Exceptions\Handler
捕獲到,處理器先上報異常記錄到日誌文件裏而後渲染異常響應再發送響應給客戶端。可是自帶的異常處理器的方法並很差用,不少時候咱們想把異常上報到郵件或者是錯誤日誌系統中,下面的例子是將異常上報到Sentry系統中,Sentry是一個錯誤收集服務很是好用:bash
public function report(Exception $exception)
{
if (app()->bound('sentry') && $this->shouldReport($exception)) {
app('sentry')->captureException($exception);
}
parent::report($exception);
}
複製代碼
還有默認的渲染方法在表單驗證時生成響應的JSON格式每每跟咱們項目裏統一的JOSN
格式不同這就須要咱們自定義渲染方法的行爲。app
public function render($request, Exception $exception)
{
//若是客戶端預期的是JSON響應, 在API請求未經過Validator驗證拋出ValidationException後
//這裏來定製返回給客戶端的響應.
if ($exception instanceof ValidationException && $request->expectsJson()) {
return $this->error(422, $exception->errors());
}
if ($exception instanceof ModelNotFoundException && $request->expectsJson()) {
//捕獲路由模型綁定在數據庫中找不到模型後拋出的NotFoundHttpException
return $this->error(424, 'resource not found.');
}
if ($exception instanceof AuthorizationException) {
//捕獲不符合權限時拋出的 AuthorizationException
return $this->error(403, "Permission does not exist.");
}
return parent::render($request, $exception);
}
複製代碼
自定義後,在請求未經過FormValidator
驗證時會拋出ValidationException
, 以後異常處理器捕獲到異常後會把錯誤提示格式化爲項目統一的JSON響應格式並輸出給客戶端。這樣在咱們的控制器中就徹底省略了判斷表單驗證是否經過若是不經過再輸出錯誤響應給客戶端的邏輯了,將這部分邏輯交給了統一的異常處理器來執行能讓控制器方法瘦身很多。
這部份內容其實不是針對Laravel
框架自定義異常,在任何項目中均可以應用我這裏說的自定義異常。
我見過不少人在Repository
或者Service
類的方法中會根據不一樣錯誤返回不一樣的數組,裏面包含着響應的錯誤碼和錯誤信息,這麼作固然是能夠知足開發需求的,可是並不能記錄發生異常時的應用的運行時上下文,發生錯誤時沒辦法記錄到上下文信息就很是不利於開發者進行問題定位。
下面的是一個自定義的異常類
namespace App\Exceptions\;
use RuntimeException;
use Throwable;
class UserManageException extends RuntimeException
{
/**
* The primitive arguments that triggered this exception
*
* @var array
*/
public $primitives;
/**
* QueueManageException constructor.
* @param array $primitives
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(array $primitives, $message = "", $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->primitives = $primitives;
}
/**
* get the primitive arguments that triggered this exception
*/
public function getPrimitives()
{
return $this->primitives;
}
}
複製代碼
定義完異常類咱們就能在代碼邏輯中拋出異常實例了
class UserRepository
{
public function updateUserFavorites(User $user, $favoriteData)
{
......
if (!$executionOne) {
throw new UserManageException(func_get_args(), 'Update user favorites error', '501');
}
......
if (!$executionTwo) {
throw new UserManageException(func_get_args(), 'Another Error', '502');
}
return true;
}
}
class UserController extends ...
{
public function updateFavorites(User $user, Request $request)
{
.......
$favoriteData = $request->input('favorites');
try {
$this->userRepo->updateUserFavorites($user, $favoritesData);
} catch (UserManageException $ex) {
.......
}
}
}
複製代碼
除了上面Repository
列出的狀況更多的時候咱們是在捕獲到上面列舉的通用異常後在catch
代碼塊中拋出與業務相關的更細化的異常實例方便開發者定位問題,咱們將上面的updateUserFavorites
按照這種策略修改一下
public function updateUserFavorites(User $user, $favoriteData)
{
try {
// database execution
// database execution
} catch (QueryException $queryException) {
throw new UserManageException(func_get_args(), 'Error Message', '501' , $queryException);
}
return true;
}
複製代碼
在上面定義UserMangeException
類的時候第四個參數$previous
是一個實現了Throwable
接口類實例,在這種情景下咱們由於捕獲到了QueryException
的異常實例而拋出了UserManagerException
的實例,而後經過這個參數將QueryException
實例傳遞給PHP
異常的堆棧,這提供給咱們回溯整個異常的能力來獲取更多上下文信息,而不是僅僅只是當前拋出的異常實例的上下文信息, 在錯誤收集系統可使用相似下面的代碼來獲取全部異常的信息。
while($e instanceof \Exception) {
echo $e->getMessage();
$e = $e->getPrevious();
}
複製代碼
異常處理是PHP
很是重要但又容易讓開發者忽略的功能,這篇文章簡單解釋了Laravel
內部異常處理的機制以及擴展Laravel
異常處理的方式方法。更多的篇幅着重分享了一些異常處理的編程實踐,這些正是我但願每一個讀者都能看明白並實踐下去的一些編程習慣,包括以前分享的Interface
的應用也是同樣。
本文已經整理髮布到系列文章Laravel核心代碼學習中,歡迎訪問閱讀,多多交流。