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编程效率 引入缓存机制提升性能
Feb 15 PHP
关于PHP堆栈与列队的学习
Jun 21 PHP
30个php操作redis常用方法代码例子
Jul 05 PHP
php中使用session防止用户非法登录后台的方法
Jan 27 PHP
php使用递归函数实现数字累加的方法
Mar 16 PHP
PHP使用内置函数生成图片的方法详解
May 09 PHP
[原创]解决wincache不支持64位PHP5.5/5.6的问题(提供64位wincache下载)
Jun 22 PHP
php结合mysql与mysqli扩展处理事务的方法
Jun 29 PHP
php、java、android、ios通用的3des方法(推荐)
Sep 09 PHP
php打开本地exe程序,js打开本地exe应用程序,并传递相关参数方法
Feb 06 PHP
PHP+jQuery实现双击修改table表格功能示例
Feb 21 PHP
tp5.1 实现setInc字段自动加1
Oct 18 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
使用PHP和HTML5 FormData实现无刷新文件上传教程
2014/09/06 PHP
PHP中判断文件存在使用is_file还是file_exists?
2015/04/03 PHP
php实现可逆加密的方法
2015/08/11 PHP
php修改数组键名的方法示例
2017/04/15 PHP
PHP code 验证码生成类定义和简单使用示例
2020/05/27 PHP
js和as的稳定传值问题解决
2013/07/14 Javascript
探讨JQUERY JSON的反序列化类 using问题的解决方法
2013/12/19 Javascript
Javascript的&&和||的另类用法
2014/07/23 Javascript
jQuery-1.9.1源码分析系列(十)事件系统之事件包装
2015/11/20 Javascript
学习掌握JavaScript中this的使用技巧
2016/08/29 Javascript
Angular.js中用ng-repeat-start实现自定义显示
2016/10/18 Javascript
Jquery on绑定的事件 触发多次实例代码
2016/12/08 Javascript
vue 2.0封装model组件的方法
2017/08/03 Javascript
React教程之Props验证的具体用法(Props Validation)
2017/09/04 Javascript
js判断节假日实例代码
2017/12/27 Javascript
NodeJS安装图文教程
2018/04/19 NodeJs
详解node字体压缩插件font-spider的用法
2018/09/28 Javascript
[55:26]DOTA2-DPC中国联赛 正赛 Aster vs LBZS BO3 第一场 2月23日
2021/03/11 DOTA
python获取网页状态码示例
2014/03/30 Python
Python实现查找系统盘中需要找的字符
2015/07/14 Python
Python2随机数列生成器简单实例
2017/09/04 Python
Python配置虚拟环境图文步骤
2019/05/20 Python
Python Django实现layui风格+django分页功能的例子
2019/08/29 Python
Python缓存技术实现过程详解
2019/09/25 Python
python创建n行m列数组示例
2019/12/02 Python
Python3搭建http服务器的实现代码
2020/02/11 Python
Pytorch .pth权重文件的使用解析
2020/02/14 Python
用python发送微信消息
2020/12/21 Python
处理HTML5新标签的浏览器兼容版问题
2017/03/13 HTML / CSS
Ralph Lauren拉夫·劳伦美国官网:带有浓郁美国气息的高品味时装品牌
2017/11/01 全球购物
Desigual德国官网:在线购买原创服装
2018/03/27 全球购物
历史学专业推荐信
2013/11/06 职场文书
社区五一劳动节活动总结
2015/02/09 职场文书
你为什么是穷人?可能是这5个缺点造成
2019/07/11 职场文书
PHP 技巧 * SVG 保存为图片(分享图生成)
2021/04/02 PHP
KVM基础命令详解
2022/04/30 Servers