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 相关文章推荐
杏林同学录(一)
Oct 09 PHP
如何使用PHP往windows中添加用户
Dec 06 PHP
从网上搜到的phpwind 0day的代码
Dec 07 PHP
php学习笔记 [预定义数组(超全局数组)]
Jun 09 PHP
第七章 php自定义函数实现代码
Dec 30 PHP
php读取txt文件组成SQL并插入数据库的代码(原创自Zjmainstay)
Jul 31 PHP
PHP实现设计模式中的抽象工厂模式详解
Oct 11 PHP
php调用mysql存储过程实例分析
Dec 29 PHP
详解PHP的Yii框架中组件行为的属性注入和方法注入
Mar 18 PHP
php实现自定义中奖项数和概率的抽奖函数示例
May 26 PHP
PHP实现求两个字符串最长公共子串的方法示例
Nov 17 PHP
实例介绍PHP删除数组中的重复元素
Mar 03 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实现的汉字拼音转换和公历农历转换类及使用示例
2014/07/01 PHP
WordPress中获取所使用的模板的页面ID的简单方法
2015/12/31 PHP
php实现数字补零的方法总结
2018/09/12 PHP
PHP使用DOM对XML解析处理操作示例
2019/07/04 PHP
PHP实现的多进程控制demo示例
2019/07/22 PHP
Javascript Global对象
2009/08/13 Javascript
jquery attr 设定src中含有&(宏)符号问题的解决方法
2011/07/26 Javascript
使用js判断当前时区TimeZone是否是夏令时
2014/02/23 Javascript
jquery仿百度百科底部浮动导航特效
2015/08/08 Javascript
javascript求日期差的方法
2016/03/02 Javascript
jquery获取复选框checkbox的值实现方法
2016/05/30 Javascript
使用bootstrap typeahead插件实现输入框自动补全之问题及解决办法
2016/07/07 Javascript
Angular网络请求的封装方法
2018/05/22 Javascript
vue实现吸顶、锚点和滚动高亮按钮效果
2019/10/21 Javascript
v-slot和slot、slot-scope之间相互替换实例
2020/09/04 Javascript
基于JavaScript实现简单抽奖功能代码实例
2020/10/20 Javascript
[03:15]DOTA2-DPC中国联赛1月22日Recap集锦
2021/03/11 DOTA
浅谈python中截取字符函数strip,lstrip,rstrip
2015/07/17 Python
在Django框架中运行Python应用全攻略
2015/07/17 Python
Python处理命令行参数模块optpars用法实例分析
2018/05/31 Python
Python实现图片转字符画的代码实例
2019/02/22 Python
tensorflow pb to tflite 精度下降详解
2020/05/25 Python
python调用私有属性的方法总结
2020/07/24 Python
纽约JewelryAffairs珠宝店:精细金银时尚首饰
2017/02/05 全球购物
升职自荐书范文
2013/11/28 职场文书
学校食堂采购员岗位职责
2013/12/05 职场文书
电子专业毕业生自我鉴定
2014/01/22 职场文书
学前班评语大全
2014/05/04 职场文书
大学生就业求职信
2014/06/12 职场文书
2015年计生工作总结范文
2015/04/24 职场文书
2016年小学党支部创先争优活动总结
2016/04/05 职场文书
golang 实现时间戳和时间的转化
2021/05/07 Golang
python中的None与NULL用法说明
2021/05/25 Python
【海涛dota解说】海涛小满开黑4v5被破两路翻盘潮汐第一视角解说
2022/04/01 DOTA
Android Flutter实现3D动画效果示例详解
2022/04/07 Java/Android
SQL Server删除表中的重复数据
2022/05/25 SQL Server