Laravel 核心开发解读:实用的异常处理流程
异常处理是编程中非常重要但又最容易被忽视的语言特性。它为开发人员提供了一种在程序运行时处理错误的机制。对于程序设计来说,适当的异常处理可以防止程序细节泄露给用户,为开发人员提供完整的错误跟踪堆栈,同时也可以提高程序的健壮性。
在本文中,我们将简要回顾 Laravel 提供的异常处理能力,然后讨论在开发中使用异常处理的一些最佳实践、如何使用自定义异常以及如何扩展 Laravel 的异常处理能力。
下面没什么好说的,我们看详细介绍
注册异常处理程序
这里我们需要回到内核处理之前多次说过的请求之前的引导阶段。在 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方法作为全局处理器的方法程序:
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报告方法报告异常。这里,异常被记录到 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函数是用来注册错误处理函数的,因为在一些旧的代码或者类库中,trigger_error PHP函数大多是用来扔。错误。异常处理程序只能处理异常,不能处理错误。因此,为了兼容旧的类库,通常使用set_error_handler来注册全局错误。 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 对于常见的程序异常会引发相应的异常实例,这使得开发者可以在运行时捕获这些异常,并根据自己的需要进行后处理(例如: v 调用另一个补救方法 v 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 规则的验证时抛出此异常
- Symfony\Component\Routing\Exception\MethodNotAllowedException\Invalid HTTP method‶请求的 HTTPRouting 方法不正确⓶ \Exceptions\HttpResponseException 当 HTTP 请求处理失败时,Laravel 会抛出此异常
Laravel 扩展异常处理
如上所述,Laravel 已成功将 \App\Exceptions\Handler 注册为全局异常处理程序,因此代码中未捕获的异常最终会被\App\Exceptions\Handler捕获。处理器首先抛出异常并将其记录在日志文件中,然后呈现异常响应,然后将响应发送到客户端。然而,内置的异常处理方法并不好用。很多时候我们希望通过电子邮件或错误日志系统报告异常情况。以下示例用于向Sentry系统报告异常。 Sentry是一个非常好的bug收集服务。用法:
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) { ....... } } }
除了上面存储库列出的情况外,大多数时候我们捕获的是通用的对于上面列出的异常,更详细的业务相关的异常实例都被扔到了代码块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接口的类的实例。在本例中,我们抛出一个 UserManagerException 实例,因为我们捕获了一个 QueryException 实例,然后通过此参数将 QueryException 实例发送到 PHP 异常堆栈。这使我们能够回溯整个异常以获取更多上下文信息,而不仅仅是当前抛出的异常实例的上下文信息。错误收集系统可以使用类似于以下的代码来检索有关所有异常的信息。
while($e instanceof \Exception) { echo $e->getMessage(); $e = $e->getPrevious(); }
异常处理是PHP一个非常重要的特性,但很容易被开发者忽视。本文简要介绍了 Laravel 内部的异常处理机制以及扩展 Laravel 异常处理的方法。一些站点专注于分享一些异常处理的编程实践。这些正是我希望每个读者能够理解和实践的编程习惯,包括之前分享的接口的应用。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。