摘要:請(qǐng)求未通過(guò)的驗(yàn)證時(shí)會(huì)拋出此異常。異常處理是非常重要但又容易讓開發(fā)者忽略的功能,這篇文章簡(jiǎn)單解釋了內(nèi)部異常處理的機(jī)制以及擴(kuò)展異常處理的方式方法。
異常處理是編程中十分重要但也最容易被人忽視的語(yǔ)言特性,它為開發(fā)者提供了處理程序運(yùn)行時(shí)錯(cuò)誤的機(jī)制,對(duì)于程序設(shè)計(jì)來(lái)說(shuō)正確的異常處理能夠防止泄露程序自身細(xì)節(jié)給用戶,給開發(fā)者提供完整的錯(cuò)誤回溯堆棧,同時(shí)也能提高程序的健壯性。
這篇文章我們來(lái)簡(jiǎn)單梳理一下Laravel中提供的異常處理能力,然后講一些在開發(fā)中使用異常處理的實(shí)踐,如何使用自定義異常、如何擴(kuò)展Laravel的異常處理能力。
注冊(cè)異常Handler這里又要回到我們說(shuō)過(guò)很多次的Kernel處理請(qǐng)求前的bootstrap階段,在bootstrap階段的IlluminateFoundationBootstrapHandleExceptions 部分中Laravel設(shè)置了系統(tǒng)異常處理行為并注冊(cè)了全局的異常處理器:
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方法注冊(cè)為程序的全局處理器方法:
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請(qǐng)求的異常響應(yīng) protected function renderForConsole(Exception $e) { $this->getExceptionHandler()->renderForConsole(new ConsoleOutput, $e); } // 渲染HTTP請(qǐng)求的異常響應(yīng) protected function renderHttpResponse(Exception $e) { $this->getExceptionHandler()->render($this->app["request"], $e)->send(); }
在處理器里主要通過(guò)ExceptionHandler的report方法上報(bào)異常、這里是記錄異常到storage/laravel.log文件中,然后根據(jù)請(qǐng)求類型渲染異常的響應(yīng)生成輸出給到客戶端。這里的ExceptionHandler就是AppExceptionsHandler類的實(shí)例,它是在項(xiàng)目最開始注冊(cè)到服務(wù)容器中的:
// bootstrap/app.php /* |-------------------------------------------------------------------------- | Create The Application |-------------------------------------------------------------------------- */ $app = new IlluminateFoundationApplication( realpath(__DIR__."/../") ); /* |-------------------------------------------------------------------------- | Bind Important Interfaces |-------------------------------------------------------------------------- */ ...... $app->singleton( IlluminateContractsDebugExceptionHandler::class, AppExceptionsHandler::class );
這里再順便說(shuō)一下set_error_handler函數(shù),它的作用是注冊(cè)錯(cuò)誤處理器函數(shù),因?yàn)樵谝恍┠甏眠h(yuǎn)的代碼或者類庫(kù)中大多是采用PHP那件函數(shù)trigger_error函數(shù)來(lái)拋出錯(cuò)誤的,異常處理器只能處理Exception不能處理Error,所以為了能夠兼容老類庫(kù)通常都會(huì)使用set_error_handler注冊(cè)全局的錯(cuò)誤處理器方法,在方法中捕獲到錯(cuò)誤后將錯(cuò)誤轉(zhuǎn)化成異常再重新拋出,這樣項(xiàng)目中所有的代碼沒有被正確執(zhí)行時(shí)都能拋出異常實(shí)例了。
/** * 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異常實(shí)例
Laravel中針對(duì)常見的程序異常情況拋出了相應(yīng)的異常實(shí)例,這讓開發(fā)者能夠捕獲這些運(yùn)行時(shí)異常并根據(jù)自己的需要來(lái)做后續(xù)處理(比如:在catch中調(diào)用另外一個(gè)補(bǔ)救方法、記錄異常到日志文件、發(fā)送報(bào)警郵件、短信)
在這里我列一些開發(fā)中常遇到異常,并說(shuō)明他們是在什么情況下被拋出的,平時(shí)編碼中一定要注意在程序里捕獲這些異常做好異常處理才能讓程序更健壯。
IlluminateDatabaseQueryException Laravel中執(zhí)行SQL語(yǔ)句發(fā)生錯(cuò)誤時(shí)會(huì)拋出此異常,它也是使用率最高的異常,用來(lái)捕獲SQL執(zhí)行錯(cuò)誤,比方執(zhí)行Update語(yǔ)句時(shí)很多人喜歡判斷SQL執(zhí)行后判斷被修改的行數(shù)來(lái)判斷UPDATE是否成功,但有的情景里執(zhí)行的UPDATE語(yǔ)句并沒有修改記錄值,這種情況就沒法通過(guò)被修改函數(shù)來(lái)判斷UPDATE是否成功了,另外在事務(wù)執(zhí)行中如果捕獲到QueryException 可以在catch代碼塊中回滾事務(wù)。
IlluminateDatabaseEloquentModelNotFoundException 通過(guò)模型的findOrFail和firstOrFail方法獲取單條記錄時(shí)如果沒有找到會(huì)拋出這個(gè)異常(find和first找不到數(shù)據(jù)時(shí)會(huì)返回NULL)。
IlluminateValidationValidationException 請(qǐng)求未通過(guò)Laravel的FormValidator驗(yàn)證時(shí)會(huì)拋出此異常。
IlluminateAuthAccessAuthorizationException 用戶請(qǐng)求未通過(guò)Laravel的策略(Policy)驗(yàn)證時(shí)拋出此異常
SymfonyComponentRoutingExceptionMethodNotAllowedException 請(qǐng)求路由時(shí)HTTP Method不正確
IlluminateHttpExceptionsHttpResponseException Laravel的處理HTTP請(qǐng)求不成功時(shí)拋出此異常
擴(kuò)展Laravel的異常處理器上面說(shuō)了Laravel把AppExceptionsHandler 注冊(cè)成功了全局的異常處理器,代碼中沒有被catch到的異常,最后都會(huì)被AppExceptionsHandler捕獲到,處理器先上報(bào)異常記錄到日志文件里然后渲染異常響應(yīng)再發(fā)送響應(yīng)給客戶端。但是自帶的異常處理器的方法并不好用,很多時(shí)候我們想把異常上報(bào)到郵件或者是錯(cuò)誤日志系統(tǒng)中,下面的例子是將異常上報(bào)到Sentry系統(tǒng)中,Sentry是一個(gè)錯(cuò)誤收集服務(wù)非常好用:
public function report(Exception $exception) { if (app()->bound("sentry") && $this->shouldReport($exception)) { app("sentry")->captureException($exception); } parent::report($exception); }
還有默認(rèn)的渲染方法在表單驗(yàn)證時(shí)生成響應(yīng)的JSON格式往往跟我們項(xiàng)目里統(tǒng)一的JOSN格式不一樣這就需要我們自定義渲染方法的行為。
public function render($request, Exception $exception) { //如果客戶端預(yù)期的是JSON響應(yīng), 在API請(qǐng)求未通過(guò)Validator驗(yàn)證拋出ValidationException后 //這里來(lái)定制返回給客戶端的響應(yīng). if ($exception instanceof ValidationException && $request->expectsJson()) { return $this->error(422, $exception->errors()); } if ($exception instanceof ModelNotFoundException && $request->expectsJson()) { //捕獲路由模型綁定在數(shù)據(jù)庫(kù)中找不到模型后拋出的NotFoundHttpException return $this->error(424, "resource not found."); } if ($exception instanceof AuthorizationException) { //捕獲不符合權(quán)限時(shí)拋出的 AuthorizationException return $this->error(403, "Permission does not exist."); } return parent::render($request, $exception); }
自定義后,在請(qǐng)求未通過(guò)FormValidator驗(yàn)證時(shí)會(huì)拋出ValidationException, 之后異常處理器捕獲到異常后會(huì)把錯(cuò)誤提示格式化為項(xiàng)目統(tǒng)一的JSON響應(yīng)格式并輸出給客戶端。這樣在我們的控制器中就完全省略了判斷表單驗(yàn)證是否通過(guò)如果不通過(guò)再輸出錯(cuò)誤響應(yīng)給客戶端的邏輯了,將這部分邏輯交給了統(tǒng)一的異常處理器來(lái)執(zhí)行能讓控制器方法瘦身不少。
使用自定義異常這部分內(nèi)容其實(shí)不是針對(duì)Laravel框架自定義異常,在任何項(xiàng)目中都可以應(yīng)用我這里說(shuō)的自定義異常。
我見過(guò)很多人在Repository或者Service類的方法中會(huì)根據(jù)不同錯(cuò)誤返回不同的數(shù)組,里面包含著響應(yīng)的錯(cuò)誤碼和錯(cuò)誤信息,這么做當(dāng)然是可以滿足開發(fā)需求的,但是并不能記錄發(fā)生異常時(shí)的應(yīng)用的運(yùn)行時(shí)上下文,發(fā)生錯(cuò)誤時(shí)沒辦法記錄到上下文信息就非常不利于開發(fā)者進(jìn)行問題定位。
下面的是一個(gè)自定義的異常類
namespace AppExceptions; 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; } }
定義完異常類我們就能在代碼邏輯中拋出異常實(shí)例了
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列出的情況更多的時(shí)候我們是在捕獲到上面列舉的通用異常后在catch代碼塊中拋出與業(yè)務(wù)相關(guān)的更細(xì)化的異常實(shí)例方便開發(fā)者定位問題,我們將上面的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類的時(shí)候第四個(gè)參數(shù)$previous是一個(gè)實(shí)現(xiàn)了Throwable接口類實(shí)例,在這種情景下我們因?yàn)椴东@到了QueryException的異常實(shí)例而拋出了UserManagerException的實(shí)例,然后通過(guò)這個(gè)參數(shù)將QueryException實(shí)例傳遞給PHP異常的堆棧,這提供給我們回溯整個(gè)異常的能力來(lái)獲取更多上下文信息,而不是僅僅只是當(dāng)前拋出的異常實(shí)例的上下文信息, 在錯(cuò)誤收集系統(tǒng)可以使用類似下面的代碼來(lái)獲取所有異常的信息。
while($e instanceof Exception) { echo $e->getMessage(); $e = $e->getPrevious(); }
異常處理是PHP非常重要但又容易讓開發(fā)者忽略的功能,這篇文章簡(jiǎn)單解釋了Laravel內(nèi)部異常處理的機(jī)制以及擴(kuò)展Laravel異常處理的方式方法。更多的篇幅著重分享了一些異常處理的編程實(shí)踐,這些正是我希望每個(gè)讀者都能看明白并實(shí)踐下去的一些編程習(xí)慣,包括之前分享的Interface的應(yīng)用也是一樣。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/30090.html
摘要:過(guò)去一年時(shí)間寫了多篇文章來(lái)探討了我認(rèn)為的框架最核心部分的設(shè)計(jì)思路代碼實(shí)現(xiàn)。為了大家閱讀方便,我把這些源碼學(xué)習(xí)的文章匯總到這里。數(shù)據(jù)庫(kù)算法和數(shù)據(jù)結(jié)構(gòu)這些都是編程的內(nèi)功,只有內(nèi)功深厚了才能解決遇到的復(fù)雜問題。 過(guò)去一年時(shí)間寫了20多篇文章來(lái)探討了我認(rèn)為的Larave框架最核心部分的設(shè)計(jì)思路、代碼實(shí)現(xiàn)。通過(guò)更新文章自己在軟件設(shè)計(jì)、文字表達(dá)方面都有所提高,在剛開始決定寫Laravel源碼分析地...
摘要:如何做用戶認(rèn)證根據(jù)文檔描述,提供用戶認(rèn)證的接口,他的核心是看守器和提供器,看守器定義怎么認(rèn)證用戶,提供器定義怎么檢索用戶。 最近的一個(gè)PHP項(xiàng)目,上一個(gè)項(xiàng)目是采用ThinkPHP來(lái)弄的,因?yàn)楹茉缇吐犝f(shuō)過(guò)Laravel的大名,所以進(jìn)了Laravel的官網(wǎng),意外發(fā)現(xiàn)了Lumen,正好我項(xiàng)目是提供API的,所以選擇了Lumen,因?yàn)槭荓aravel的精簡(jiǎn)版,看了幾天的Laravel文檔,也總...
摘要:調(diào)用了的可以看出,所有服務(wù)提供器都在配置文件文件的數(shù)組中。啟動(dòng)的啟動(dòng)由類負(fù)責(zé)引導(dǎo)應(yīng)用的屬性中記錄的所有服務(wù)提供器,就是依次調(diào)用這些服務(wù)提供器的方法,引導(dǎo)完成后就代表應(yīng)用正式啟動(dòng)了,可以開始處理請(qǐng)求了。 服務(wù)提供器是所有 Laravel 應(yīng)用程序引導(dǎo)中心。你的應(yīng)用程序自定義的服務(wù)、第三方資源包提供的服務(wù)以及 Laravel 的所有核心服務(wù)都是通過(guò)服務(wù)提供器進(jìn)行注冊(cè)(register)和引...
摘要:其中設(shè)置請(qǐng)求是唯一區(qū)別于內(nèi)核的一個(gè)引導(dǎo)程序。和命令行腳本的規(guī)范一樣,如果執(zhí)行命令任務(wù)程序成功會(huì)返回拋出異常退出則返回。嚴(yán)格遵循了面向?qū)ο蟪绦蛟O(shè)計(jì)的原則。 Console內(nèi)核 上一篇文章我們介紹了Laravel的HTTP內(nèi)核,詳細(xì)概述了網(wǎng)絡(luò)請(qǐng)求從進(jìn)入應(yīng)用到應(yīng)用處理完請(qǐng)求返回HTTP響應(yīng)整個(gè)生命周期中HTTP內(nèi)核是如何調(diào)動(dòng)Laravel各個(gè)核心組件來(lái)完成任務(wù)的。除了處理HTTP請(qǐng)求一個(gè)健壯...
摘要:終止程序終止中間件內(nèi)核的方法會(huì)調(diào)用中間件的方法,調(diào)用完成后從請(qǐng)求進(jìn)來(lái)到返回響應(yīng)整個(gè)應(yīng)用程序的生命周期就結(jié)束了。 Http Kernel Http Kernel是Laravel中用來(lái)串聯(lián)框架的各個(gè)核心組件來(lái)網(wǎng)絡(luò)請(qǐng)求的,簡(jiǎn)單的說(shuō)只要是通過(guò)public/index.php來(lái)啟動(dòng)框架的都會(huì)用到Http Kernel,而另外的類似通過(guò)artisan命令、計(jì)劃任務(wù)、隊(duì)列啟動(dòng)框架進(jìn)行處理的都會(huì)用到C...
閱讀 1395·2021-11-25 09:43
閱讀 816·2021-11-18 10:02
閱讀 3026·2021-09-07 09:59
閱讀 2819·2021-08-30 09:44
閱讀 2976·2019-08-30 13:17
閱讀 2374·2019-08-29 12:17
閱讀 1731·2019-08-28 17:57
閱讀 1343·2019-08-26 14:04