异常处理是编程中一个非常重要但最容易被忽视的语言特性。它为开发者提供了处理程序运行时错误的机制,对程序设计的异常进行正确处理,可以防止将程序本身的细节泄露给用户,为开发者提供完整的错误回溯栈,同时也提高了程序的健壮性。在这篇文章中,我们先简单回顾一下Laravel提供的异常处理能力,然后谈谈在开发中使用异常处理的一些实践,如何使用自定义异常,以及如何扩展Laravel的异常处理能力。RegisterexceptionHandler这里又要回到我们说过多次的Kernel处理请求之前的bootstrap阶段。在引导阶段的Illuminate\Foundation\Bootstrap\HandleExceptions部分,Laravel设置系统异常处理行为并注册全局异常处理程序。:classHandleExceptions{publicfunctionbootstrap(Application$app){$this->app=$app;错误报告(-1);set_error_handler([$this,'handleError']);set_exception_handler([$this,'handleException']);register_shutdown_function([$this,'handleShutdown']);如果(!$app->environment('testing')){ini_set('display_errors','Off');}}publicfunctionhandleError($level,$message,$file='',$line=0,$context=[]){if(error_reporting()&$level){thrownewErrorException($message,0,$级别,$文件,$行);}}}set_exception_handler([$this,'handleException'])将HandleExceptions的handleException方法注册为程序的全局handler方法:publicfunctionhandleException($e){if(!$einstanceofException){$e=newFatalThrowableError($e);}$this->getExceptionHandler()->report($e);如果($this->app->runningInConsole()){$this->renderForConsole($e);}else{$this->renderHttpResponse($e);}}protectedfunctiongetExceptionHandler(){return$this->app->make(ExceptionHandler::class);}//对CLI请求渲染异常响应protectedfunctionrenderForConsole(Exception$e){$this->getExceptionHandler()->renderForConsole(newConsoleOutput,$e);}//渲染HTTP请求保护函数的异常响应renderHttpResponse(Exception$e){$this->getExceptionHandler()->render($this->app['request'],$e)->send();}在处理器中,主要通过ExceptionHandler的report方法上报异常。这里将异常记录在storage/laravel.log文件中,然后根据请求类型Render异常响应,生成输出给客户端这里的ExceptionHandler是\App\Exceptions\Handler类的一个实例,在项目开始时注册到服务容器中://bootstrap/app.php/*|----------------------------------------------------------------------|创建应用程序|--------------------------------------------------------------------------*/$app=newIlluminate\Foundation\Application(realpath(__DIR__.'/../'));/*|--------------------------------------------------------------------------|绑定重要接口|---------------------------------------------------------------------*/......$app->singleton(Illuminate\Contracts\Debug\ExceptionHandler::类,应用\异常\处理程序::类);这里顺便提一下set_error_handler函数用来注册错误处理函数,因为在一些老代码或者类库中,PHP的trigger_error函数多用于抛出错误。异常处理程序只能处理Exception而不能处理Error,所以为了兼容老类库,通常使用set_error_handler来注册全局错误处理方法,在方法中捕获到错误后,将错误转化为exception然后重新抛出,这样当所有代码都没有正确执行时,项目Exception实例现在可以被抛出。/***将PHP错误转换为ErrorException实例。**@paramint$level*@paramstring$message*@paramstring$file*@paramint$line*@paramarray$context*@returnvoid**@throws\ErrorException*/publicfunctionhandleError($level,$message,$file='',$line=0,$context=[]){if(error_reporting()&$level){thrownewErrorException($message,0,$level,$file,$line);}}常用的Laravel异常实例Laravel针对常见的程序异常抛出对应的异常实例,可以让开发者捕获这些运行时异常,并根据需要进行后续处理(例如:在catch中调用另一个补救方法,记录异常日志文件,发送告警邮件,短信)这里我列举一些开发中经常遇到的异常,并说明它们是在什么情况下抛出的,在平时的编码中,一定要注意在程序中捕获这些异常,并进行异常处理使程序更健壮。Illuminate\Database\QueryException当在Laravel中执行SQL语句发生错误时会抛出该异常。也是使用率最高的异常,用于捕获SQL执行错误。比如在执行Update语句的时候,很多人喜欢判断SQL语句被修改了。行数用于判断UPDATE是否成功,但在某些场景下,执行的UPDATE语句并没有修改记录值。这种情况下,无法通过修改后的函数来判断UPDATE是否成功。另外,如果在事务执行过程中捕获了QueryException,可以在catch代码块中Rollback事务。Illuminate\Database\Eloquent\ModelNotFoundException当通过model的findOrFail和firstOrFail方法获取单条记录时,如果没有找到数据会抛出这个异常(find和first如果没有找到数据会返回NULL)。Illuminate\Validation\ValidationException当请求未通过Laravel的FormValidator验证时抛出。Illuminate\Auth\Access\AuthorizationException当用户请求没有通过Laravel的策略(Policy)验证时抛出该异常Laravel的处理HTTP请求不成功时抛出这个异常。扩展Laravel的异常处理程序。如上所述,Laravel已经成功注册了\App\Exceptions\Handler作为全局异常处理程序。任何在代码中没有捕获到的异常最终都会被\App\Exceptions捕获到\Handler中捕获,处理器先上报异常并记录在日志文件中,然后渲染异常响应再将响应发送给客户端.但是内置的异常处理方法并不好用。很多时候我们希望将异常报告给电子邮件或错误日志系统。下面的例子是向哨兵系统报告异常。Sentry是一个非常好的错误收集服务。使用:publicfunctionreport(Exception$exception){if(app()->bound('sentry')&&$this->shouldReport($exception)){app('sentry')->captureException($exception);}parent::report($exception);}还有一个默认的渲染方法。表单验证时生成的响应的JSON格式往往与我们项目中统一的JSON格式不同,这就需要我们自定义渲染方法的行为。publicfunctionrender($request,Exception$exception){//如果客户端期望JSON响应,API请求未通过Validator验证后抛出ValidationException//此处自定义返回给客户端的响应。如果($exceptioninstanceofValidationException&&$request->expectsJson()){return$this->error(422,$exception->errors());}if($exceptioninstanceofModelNotFoundException&&$request->expectsJson()){//捕获模型绑定后抛出的路由NotFoundHttpException在数据库中找不到模型return$this->error(424,'resourcenotfound.');}if($exceptioninstanceofAuthorizationException){//不满足权限时捕获AuthorizationExceptionreturn$this->error(403,"权限不存在。");}returnparent::render($request,$exception);}自定义后,当请求FormValidator验证失败时会抛出ValidationException,异常处理器捕获到异常后会将错误信息格式化成项目统一的JSON响应格式并输出给客户端。这样,在我们的controller中,就完全省略了判断表单验证是否通过,然后向客户端输出错误响应的逻辑,而将这部分逻辑交给统一的异常处理程序来执行,可以使控制器方法更苗条。很少。使用自定义异常的内容其实并不是Laravel框架的自定义异常。我在这里提到的自定义异常可以应用于任何项目。我看到很多人在Repository或者Service类的方法中根据不同的错误返回不同的数组,里面包含了响应的错误码和错误信息。当然这样可以满足开发需求,但是无法记录异常发生时应用的运行时上下文,没有办法记录错误发生时的上下文信息,非常不利于开发者定位问题。下面是一个自定义的异常类命名空间App\Exceptions\;useRuntimeException;useThrowable;classUserManageExceptionextendsRuntimeException{/***触发这个异常的原始参数**@vararray*/public$primitives;/***QueueManageException构造函数。*@paramarray$primitives*@paramstring$message*@paramint$code*@paramThrowable|null$previous*/publicfunction__construct(array$primitives,$message="",$code=0,Throwable$previous=null){parent::__construct($message,$code,$previous);$this->primitives=$primitives;}/***获取触发此异常的原始参数*/publicfunctiongetPrimitives(){return$this->primitives;}}定义异常类后,我们可以在代码逻辑类UserRepository中抛出一个异常实例{publicfunctionupdateUserFavorites(User$user,$favoriteData){...if(!$executionOne){thrownewUserManageException(func_get_args(),'更新用户收藏错误','501');}......if(!$executionTwo){thrownewUserManageException(func_get_args(),'AnotherError','502');}返回真;}}classUserControllerextends...{publicfunctionupdateFavorites(User$user,Request$request){......$favoriteData=$request->input('favorites');尝试{$this->userRepo->updateUserFavorites($user,$favoritesData);}catch(UserManageException$ex){...。}}}Repository中除了上面列举的情况,我们更多时候是在捕获到上面列举的一般异常后,在catch代码块中抛出更详细的业务相关的异常实例,方便开发者定位问题。我们将根据这个策略修改上面的updateUserFavorites。PublicfunctionupdateUserFavorites(User$user,$favoriteData){try{//数据库执行//数据库执行}catch(QueryException$queryException){thrownewUserManageException(func_get_args(),'错误信息','501',$queryException);}returntrue;}上面定义UserMangeException类时,第四个参数$previous是一个实现了Throwable接口的类的实例。这种情况下我们会抛出UserManagerException的一个实例,因为QueryException的异常实例被捕获,然后通过这个参数将QueryException实例传递给PHP异常栈,这为我们提供了追溯整个异常的能力,从而获得更多上下文信息,而不仅仅是当前信息。抛出的异常实例的上下文信息。在错误收集系统中,可以使用如下代码获取所有的异常信息while($einstanceof\Exception){echo$e->getMessage();$e=$e->getPrevious();}异常处理是PHP的一个非常重要的功能,但是很容易被开发者忽视。本文简要介绍了Laravel内部异常处理的机制以及扩展Laravel异常处理的方式。更多篇幅着重分享一些异常处理编程实践。以上是一些编程习惯,希望每个读者都能理解和实践,包括之前分享的Interface的应用。
