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

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

注册异常Handler

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


  1. class HandleExceptions
  2. {
  3. public function bootstrap(Application $app)
  4. {
  5. $this->app = $app;
  6. error_reporting(-1);
  7. set_error_handler([$this, 'handleError']);
  8. set_exception_handler([$this, 'handleException']);
  9. register_shutdown_function([$this, 'handleShutdown']);
  10. if (! $app->environment('testing')) {
  11. ini_set('display_errors', 'Off');
  12. }
  13. }
  14. public function handleError($level, $message, $file = '', $line = 0, $context = [])
  15. {
  16. if (error_reporting() & $level) {
  17. throw new ErrorException($message, 0, $level, $file, $line);
  18. }
  19. }
  20. }

set_exception_handler([$this, 'handleException'])HandleExceptionshandleException方法注册为程序的全局处理器方法:


  1. public function handleException($e)
  2. {
  3. if (! $e instanceof Exception) {
  4. $e = new FatalThrowableError($e);
  5. }
  6. $this->getExceptionHandler()->report($e);
  7. if ($this->app->runningInConsole()) {
  8. $this->renderForConsole($e);
  9. } else {
  10. $this->renderHttpResponse($e);
  11. }
  12. }
  13. protected function getExceptionHandler()
  14. {
  15. return $this->app->make(ExceptionHandler::class);
  16. }
  17. // 渲染CLI请求的异常响应
  18. protected function renderForConsole(Exception $e)
  19. {
  20. $this->getExceptionHandler()->renderForConsole(new ConsoleOutput, $e);
  21. }
  22. // 渲染HTTP请求的异常响应
  23. protected function renderHttpResponse(Exception $e)
  24. {
  25. $this->getExceptionHandler()->render($this->app['request'], $e)->send();
  26. }

在处理器里主要通过ExceptionHandlerreport方法上报异常、这里是记录异常到storage/laravel.log文件中,然后根据请求类型渲染异常的响应生成输出给到客户端。这里的ExceptionHandler就是\App\Exceptions\Handler类的实例,它是在项目最开始注册到服务容器中的:


  1. // bootstrap/app.php
  2. /*
  3. |--------------------------------------------------------------------------
  4. | Create The Application
  5. |--------------------------------------------------------------------------
  6. */
  7. $app = new Illuminate\Foundation\Application(
  8. realpath(__DIR__.'/../')
  9. );
  10. /*
  11. |--------------------------------------------------------------------------
  12. | Bind Important Interfaces
  13. |--------------------------------------------------------------------------
  14. */
  15. ......
  16. $app->singleton(
  17. Illuminate\Contracts\Debug\ExceptionHandler::class,
  18. App\Exceptions\Handler::class
  19. );

这里再顺便说一下set_error_handler函数,它的作用是注册错误处理器函数,因为在一些年代久远的代码或者类库中大多是采用PHP那件函数trigger_error函数来抛出错误的,异常处理器只能处理Exception不能处理Error,所以为了能够兼容老类库通常都会使用set_error_handler注册全局的错误处理器方法,在方法中捕获到错误后将错误转化成异常再重新抛出,这样项目中所有的代码没有被正确执行时都能抛出异常实例了。


  1. /**
  2. * Convert PHP errors to ErrorException instances.
  3. *
  4. * @param int $level
  5. * @param string $message
  6. * @param string $file
  7. * @param int $line
  8. * @param array $context
  9. * @return void
  10. *
  11. * @throws \ErrorException
  12. */
  13. public function handleError($level, $message, $file = '', $line = 0, $context = [])
  14. {
  15. if (error_reporting() & $level) {
  16. throw new ErrorException($message, 0, $level, $file, $line);
  17. }
  18. }

常用的Laravel异常实例

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

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

  • Illuminate\Database\QueryException Laravel中执行SQL语句发生错误时会抛出此异常,它也是使用率最高的异常,用来捕获SQL执行错误,比方执行Update语句时很多人喜欢判断SQL执行后判断被修改的行数来判断UPDATE是否成功,但有的情景里执行的UPDATE语句并没有修改记录值,这种情况就没法通过被修改函数来判断UPDATE是否成功了,另外在事务执行中如果捕获到QueryException 可以在catch代码块中回滚事务。
  • Illuminate\Database\Eloquent\ModelNotFoundException 通过模型的findOrFailfirstOrFail方法获取单条记录时如果没有找到会抛出这个异常(findfirst找不到数据时会返回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是一个错误收集服务非常好用:


  1. public function report(Exception $exception)
  2. {
  3. if (app()->bound('sentry') && $this->shouldReport($exception)) {
  4. app('sentry')->captureException($exception);
  5. }
  6. parent::report($exception);
  7. }

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


  1. public function render($request, Exception $exception)
  2. {
  3. //如果客户端预期的是JSON响应, 在API请求未通过Validator验证抛出ValidationException后
  4. //这里来定制返回给客户端的响应.
  5. if ($exception instanceof ValidationException && $request->expectsJson()) {
  6. return $this->error(422, $exception->errors());
  7. }
  8. if ($exception instanceof ModelNotFoundException && $request->expectsJson()) {
  9. //捕获路由模型绑定在数据库中找不到模型后抛出的NotFoundHttpException
  10. return $this->error(424, 'resource not found.');
  11. }
  12. if ($exception instanceof AuthorizationException) {
  13. //捕获不符合权限时抛出的 AuthorizationException
  14. return $this->error(403, "Permission does not exist.");
  15. }
  16. return parent::render($request, $exception);
  17. }

自定义后,在请求未通过FormValidator验证时会抛出ValidationException, 之后异常处理器捕获到异常后会把错误提示格式化为项目统一的JSON响应格式并输出给客户端。这样在我们的控制器中就完全省略了判断表单验证是否通过如果不通过再输出错误响应给客户端的逻辑了,将这部分逻辑交给了统一的异常处理器来执行能让控制器方法瘦身不少。

使用自定义异常

这部分内容其实不是针对Laravel框架自定义异常,在任何项目中都可以应用我这里说的自定义异常。

我见过很多人在Repository或者Service类的方法中会根据不同错误返回不同的数组,里面包含着响应的错误码和错误信息,这么做当然是可以满足开发需求的,但是并不能记录发生异常时的应用的运行时上下文,发生错误时没办法记录到上下文信息就非常不利于开发者进行问题定位。

下面的是一个自定义的异常类


  1. namespace App\Exceptions\;
  2. use RuntimeException;
  3. use Throwable;
  4. class UserManageException extends RuntimeException
  5. {
  6. /**
  7. * The primitive arguments that triggered this exception
  8. *
  9. * @var array
  10. */
  11. public $primitives;
  12. /**
  13. * QueueManageException constructor.
  14. * @param array $primitives
  15. * @param string $message
  16. * @param int $code
  17. * @param Throwable|null $previous
  18. */
  19. public function __construct(array $primitives, $message = "", $code = 0, Throwable $previous = null)
  20. {
  21. parent::__construct($message, $code, $previous);
  22. $this->primitives = $primitives;
  23. }
  24. /**
  25. * get the primitive arguments that triggered this exception
  26. */
  27. public function getPrimitives()
  28. {
  29. return $this->primitives;
  30. }
  31. }

定义完异常类我们就能在代码逻辑中抛出异常实例了


  1. class UserRepository
  2. {
  3. public function updateUserFavorites(User $user, $favoriteData)
  4. {
  5. ......
  6. if (!$executionOne) {
  7. throw new UserManageException(func_get_args(), 'Update user favorites error', '501');
  8. }
  9. ......
  10. if (!$executionTwo) {
  11. throw new UserManageException(func_get_args(), 'Another Error', '502');
  12. }
  13. return true;
  14. }
  15. }
  16. class UserController extends ...
  17. {
  18. public function updateFavorites(User $user, Request $request)
  19. {
  20. .......
  21. $favoriteData = $request->input('favorites');
  22. try {
  23. $this->userRepo->updateUserFavorites($user, $favoritesData);
  24. } catch (UserManageException $ex) {
  25. .......
  26. }
  27. }
  28. }

除了上面Repository列出的情况更多的时候我们是在捕获到上面列举的通用异常后在catch代码块中抛出与业务相关的更细化的异常实例方便开发者定位问题,我们将上面的updateUserFavorites 按照这种策略修改一下


  1. public function updateUserFavorites(User $user, $favoriteData)
  2. {
  3. try {
  4. // database execution
  5. // database execution
  6. } catch (QueryException $queryException) {
  7. throw new UserManageException(func_get_args(), 'Error Message', '501' , $queryException);
  8. }
  9. return true;
  10. }

在上面定义UserMangeException类的时候第四个参数$previous是一个实现了Throwable接口类实例,在这种情景下我们因为捕获到了QueryException的异常实例而抛出了UserManagerException的实例,然后通过这个参数将QueryException实例传递给PHP异常的堆栈,这提供给我们回溯整个异常的能力来获取更多上下文信息,而不是仅仅只是当前抛出的异常实例的上下文信息, 在错误收集系统可以使用类似下面的代码来获取所有异常的信息。


  1. while($e instanceof \Exception) {
  2. echo $e->getMessage();
  3. $e = $e->getPrevious();
  4. }

异常处理是PHP非常重要但又容易让开发者忽略的功能,这篇文章简单解释了Laravel内部异常处理的机制以及扩展Laravel异常处理的方式方法。更多的篇幅着重分享了一些异常处理的编程实践,这些正是我希望每个读者都能看明白并实践下去的一些编程习惯,包括之前分享的Interface的应用也是一样。

来源:https://segmentfault.com/a/1190000018105686

Laravel核心解读--异常处理的更多相关文章

  1. Laravel核心解读--HTTP内核

    Http Kernel Http Kernel是Laravel中用来串联框架的各个核心组件来网络请求的,简单的说只要是通过public/index.php来启动框架的都会用到Http Kernel,而 ...

  2. Laravel核心解读--ENV的加载和读取

    Laravel在启动时会加载项目中的.env文件.对于应用程序运行的环境来说,不同的环境有不同的配置通常是很有用的. 例如,你可能希望在本地使用测试的Mysql数据库而在上线后希望项目能够自动切换到生 ...

  3. Laravel核心解读--Console内核

    Console内核 上一篇文章我们介绍了Laravel的HTTP内核,详细概述了网络请求从进入应用到应用处理完请求返回HTTP响应整个生命周期中HTTP内核是如何调动Laravel各个核心组件来完成任 ...

  4. Laravel核心解读--Contracts契约

    Contracts Laravel 的契约是一组定义框架提供的核心服务的接口, 例如我们在介绍用户认证的章节中到的用户看守器契约IllumninateContractsAuthGuard 和用户提供器 ...

  5. Laravel核心解读 -- 扩展用户认证系统

    扩展用户认证系统 上一节我们介绍了Laravel Auth系统实现的一些细节知道了Laravel是如何应用看守器和用户提供器来进行用户认证的,但是针对我们自己开发的项目或多或少地我们都会需要在自带的看 ...

  6. Redis核心解读:集群管理工具(Redis-sentinel)

    Redis核心解读:集群管理工具(Redis-sentinel) - Redis - TechTarget数据库 Redis核心解读:集群管理工具(Redis-sentinel)

  7. Laravel 核心--Facades 门面

    Laravel 核心--Facades 门面 伊Summer 关注  0.1 2017.08.12 19:07* 字数 2017 阅读 1089评论 0喜欢 5 介绍 Facades 为应用的 IoC ...

  8. Laravel 核心概念

    工欲善其事,必先利其器.在开发Xblog的过程中,稍微领悟了一点Laravel的思想.确实如此,这篇文章读完你可能并不能从无到有写出一个博客,但知道Laravel的核心概念之后,当你再次写起Larav ...

  9. Laravel 中的异常处理

    这篇文章里,我们将研究 Laravel 框架中最重要也最少被讨论的功能 -- 异常处理. Laravel 自带了一个异常处理类,它能够让你以简单.优雅的方式 report 和 render 异常. 文 ...

随机推荐

  1. [黑金原创教程] FPGA那些事儿《设计篇 I》- 图像处理前夕

    简介 一本为入门图像处理的入门书,另外还教你徒手搭建平台(片上系统),内容请看目录. 注意 为了达到最好的实验的结果,请准备以下硬件. AX301开发板, OV7670摄像模块, VGA接口显示器, ...

  2. 比较好用的Opera 翻译工具 ddict

    http://ddict.me

  3. 170331、58到家MQ如何快速实现流量削峰填谷

    问:为什么会有本文? 答:上一篇文章<到底什么时候该使用MQ?>引起了广泛的讨论,有朋友回复说,MQ的还有一个典型应用场景是缓冲流量,削峰填谷,本文将简单介绍下,MQ要实现什么细节,才能缓 ...

  4. poj1179 Polygon【区间DP】

    Polygon Time Limit: 1000MS   Memory Limit: 10000K Total Submissions:6633   Accepted: 2834 Descriptio ...

  5. CH5201 数组组合【01背包】

    5201 数字组合 0x50「动态规划」例题 描述 在N个数中找出其和为M的若干个数.先读入正整数N(1<N<100)和M(1<M<10000), 再读入N个正数(可以有相同的 ...

  6. [iOS微博项目 - 4.6] - 微博配图

    github: https://github.com/hellovoidworld/HVWWeibo A.微博配图 1.需求 显示原创微博.转发微博的缩略图 4张图使用2x2布局,其他使用3x3布局, ...

  7. 不阻塞浏览器的解析,待外部js下载完成后异步执行

    网站统计中的数据收集原理及实现(js埋点实现) - lastwhisper - CSDN博客 https://blog.csdn.net/l1212xiao/article/details/80450 ...

  8. Python面象对象与类

    # -*- coding: utf-8 -*- # @Date: 2017-08-26 # @Original: from collections import namedtuple from col ...

  9. 【opencv】c++ 读取图片 & 绘制点 & 绘制文字 & 保存图片

    //read pic ]; sprintf(path, "%s%d/%s", image_dir.c_str(), cam_num, filename.c_str()); cv:: ...

  10. Java中重载和重写

    重载(Overloading) 方法重载是让类以统一的方式处理不同类型数据的一种手段.多个同名函数同时存在,具有不同的参数个数/类型.重载Overloading是一个类中多态性的一种表现. Java的 ...