Laravel核心解读之异常处理的实践过程


Posted in PHP onFebruary 24, 2019

前言

异常处理是编程中十分重要但也最容易被人忽视的语言特性,它为开发者提供了处理程序运行时错误的机制,对于程序设计来说正确的异常处理能够防止泄露程序自身细节给用户,给开发者提供完整的错误回溯堆栈,同时也能提高程序的健壮性。

这篇文章我们来简单梳理一下Laravel中提供的异常处理能力,然后讲一些在开发中使用异常处理的实践,如何使用自定义异常、如何扩展Laravel的异常处理能力。

下面话不多说了,来一起看看详细的介绍吧

注册异常Handler

这里又要回到我们说过很多次的Kernel处理请求前的bootstrap阶段,在bootstrap阶段的Illuminate\Foundation\Bootstrap\HandleExceptions 部分中Laravel设置了系统异常处理行为并注册了全局的异常处理器:

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方法注册为程序的全局处理器方法:

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异常实例

Laravel中针对常见的程序异常情况抛出了相应的异常实例,这让开发者能够捕获这些运行时异常并根据自己的需要来做后续处理(比如:在catch中调用另外一个补救方法、记录异常到日志文件、发送报警邮件、短信)

在这里我列一些开发中常遇到异常,并说明他们是在什么情况下被抛出的,平时编码中一定要注意在程序里捕获这些异常做好异常处理才能让程序更健壮。

  • 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的异常处理器

上面说了Laravel把\App\Exceptions\Handler 注册成功了全局的异常处理器,代码中没有被catch到的异常,最后都会被\App\Exceptions\Handler捕获到,处理器先上报异常记录到日志文件里然后渲染异常响应再发送响应给客户端。但是自带的异常处理器的方法并不好用,很多时候我们想把异常上报到邮件或者是错误日志系统中,下面的例子是将异常上报到Sentry系统中,Sentry是一个错误收集服务非常好用:

public function report(Exception $exception)
{
 if (app()->bound('sentry') && $this->shouldReport($exception)) {
  app('sentry')->captureException($exception);
 }

 parent::report($exception);
}

还有默认的渲染方法在表单验证时生成响应的JSON格式往往跟我们项目里统一的JOSN格式不一样这就需要我们自定义渲染方法的行为。

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的应用也是一样。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

PHP 相关文章推荐
php mssql 分页SQL语句优化 持续影响
Apr 26 PHP
PHP中extract()函数的定义和用法
Aug 17 PHP
PHP return语句的另一个作用
Jul 30 PHP
wamp服务器访问php非常缓慢的解决过程
Jul 01 PHP
PHP环境搭建(php+Apache+mysql)
Nov 14 PHP
php获取POST数据的三种方法实例详解
Dec 20 PHP
ThinkPHP 在阿里云上的nginx.config配置实例详解
Oct 11 PHP
PHP实现浏览器格式化显示XML的方法示例
Jan 22 PHP
Laravel 已登陆用户再次查看登陆页面的自动跳转设置方法
Sep 30 PHP
在Laravel中使用GuzzleHttp调用第三方服务的API接口代码
Oct 15 PHP
php下的原生ajax请求用法实例分析
Feb 28 PHP
PHP论坛实现积分系统的思路代码详解
Jun 01 PHP
PHP通过GD库实现验证码功能示例
Feb 23 #PHP
php中file_get_contents()函数用法实例
Feb 21 #PHP
PHP基于mcript扩展实现对称加密功能示例
Feb 21 #PHP
PHP+jQuery实现双击修改table表格功能示例
Feb 21 #PHP
PHP+jQuery实现即点即改功能示例
Feb 21 #PHP
PHP hebrev()函数用法讲解
Feb 21 #PHP
Yii2.0框架实现带分页的多条件搜索功能示例
Feb 20 #PHP
You might like
推荐10个提供免费PHP脚本下载的网站
2014/12/31 PHP
php中get_cfg_var()和ini_get()的用法及区别
2015/03/04 PHP
js防止表单重复提交实现代码
2012/09/05 Javascript
jQuery Ajax提交表单查询获得数据实例代码
2012/09/19 Javascript
jquery 关于event.target使用的几点说明介绍
2013/04/26 Javascript
JavaScript实现QueryString获取GET参数的方法
2013/07/02 Javascript
js操作iframe父子窗体示例
2014/05/22 Javascript
JS创建类和对象的两种不同方式
2014/08/08 Javascript
jquery得到iframe src属性值的方法
2014/09/25 Javascript
JS简单实现表格排序功能示例
2016/12/20 Javascript
jQuery滑动到底部加载下一页数据的实例代码
2017/05/22 jQuery
Angular.js中上传指令ng-upload的基本使用教程
2017/07/30 Javascript
Angular浏览器插件Batarang介绍及使用
2018/02/07 Javascript
jQuery实现动态加载select下拉列表项功能示例
2018/05/31 jQuery
JavaScript实现的DOM绘制柱状图效果示例
2018/08/08 Javascript
Vue axios与Go Frame后端框架的Options请求跨域问题详解
2020/03/03 Javascript
解决Vue router-link绑定事件不生效的问题
2020/07/22 Javascript
python实现提取百度搜索结果的方法
2015/05/19 Python
菜鸟使用python实现正则检测密码合法性
2016/01/05 Python
ubuntu系统下使用pm2设置nodejs开机自启动的方法
2018/05/12 NodeJs
python dataframe向下向上填充,fillna和ffill的方法
2018/11/28 Python
python实现一个简单的udp通信的示例代码
2019/02/01 Python
python 实现return返回多个值
2019/11/19 Python
Python爬虫库BeautifulSoup获取对象(标签)名,属性,内容,注释
2020/01/25 Python
python实现打砖块游戏
2020/02/25 Python
函授毕业生的自我鉴定
2013/11/26 职场文书
小学教师听课制度
2014/02/01 职场文书
关于环保的建议书
2014/05/12 职场文书
服务口号大全
2014/06/11 职场文书
酒店财务经理岗位职责
2015/04/08 职场文书
党支部培养考察意见
2015/06/02 职场文书
上帝也疯狂观后感
2015/06/09 职场文书
竞聘演讲报告:基本写作有哪些?附开头范文
2019/10/16 职场文书
Java生成读取条形码和二维码的简单示例
2021/07/09 Java/Android
SQL之各种join小结详细讲解
2021/08/04 MySQL
简述Java中throw-throws异常抛出
2021/08/07 Java/Android