Home  >  Article  >  PHP Framework  >  Laravel core analysis exception handling (code)

Laravel core analysis exception handling (code)

不言
不言forward
2019-02-11 10:22:422894browse

The content of this article is about exception handling (code) of Laravel core analysis. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.

Exception handling is a very important but most overlooked language feature in programming. It provides developers with a mechanism to handle program runtime errors. For program design, correct exception handling can prevent leaks. The details of the program itself are provided to users, providing developers with a complete error traceback stack, while also improving the robustness of the program.

In this article, we will briefly review the exception handling capabilities provided in Laravel, and then talk about some practices of using exception handling in development, how to use custom exceptions, and how to extend Laravel's exception handling capabilities.

Register Exception Handler

Here we have to go back to the bootstrap stage before Kernel processes the request that we have said many times. In the bootstrap stage, Illuminate\Foundation\Bootstrap\ In the HandleExceptions section, Laravel sets the system exception handling behavior and registers the global exception handler:

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']) registers the handleException method of HandleExceptions as the global handler method of the program :

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();
}

In the processor, exceptions are mainly reported through the report method of ExceptionHandler. Here, exceptions are recorded to the storage/laravel.log file, and then the exception response is rendered according to the request type to generate output to the client. The ExceptionHandler here is an instance of the \App\Exceptions\Handler class. It is registered in the service container at the beginning of the project:

// 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
);

Here, by the way, the set_error_handler function is used to register an error handler. function, because in some old codes or class libraries, PHP's trigger_error function is mostly used to throw errors. The exception handler can only handle Exception but not Error, so in order to be compatible with old class libraries, set_error_handler is usually used. Register a global error handler method, and after catching the error in the method, convert the error into an exception and then rethrow it, so that all codes in the project can throw exception instances when they are not executed correctly.

/**
 * 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);
    }
}

Commonly used Laravel exception examples

Laravel throws corresponding exception instances for common program exceptions, which allows developers to catch these runtime exceptions And do follow-up processing according to your own needs (for example: calling another remedy method in catch, recording exceptions to log files, sending alarm emails, text messages)

Here I will list some exceptions that are often encountered in development. And explain under what circumstances they are thrown. In normal coding, you must pay attention to catching these exceptions in the program and handle them well to make the program more robust.

Illuminate\Database\QueryException This exception will be thrown when an error occurs when executing a SQL statement in Laravel. It is also the most commonly used exception and is used to capture SQL execution errors. For example, many people like to judge SQL when executing an Update statement. After execution, the number of modified rows is judged to determine whether the UPDATE is successful. However, in some scenarios, the executed UPDATE statement does not modify the record value. In this case, it is impossible to determine whether the UPDATE is successful through the modified function. In addition, during transaction execution If a QueryException is caught, the transaction can be rolled back in the catch code block.

Illuminate\Database\Eloquent\ModelNotFoundException This exception will be thrown if a single record is not found through the findOrFail and firstOrFail methods of the model (find and first will return NULL when the data cannot be found).

Illuminate\Validation\ValidationException This exception is thrown when the request does not pass Laravel's FormValidator validation.

Illuminate\Auth\Access\AuthorizationException This exception is thrown when the user request does not pass Laravel's policy (Policy) verification

Symfony\Component\Routing\Exception\MethodNotAllowedException When requesting routing, the HTTP Method does not Correct

Illuminate\Http\Exceptions\HttpResponseException Laravel throws this exception when processing the HTTP request is unsuccessful

Extend Laravel's exception handler

As mentioned above, Laravel has successfully registered \App\Exceptions\Handler as a global exception handler. Exceptions that are not caught in the code will eventually be caught by \App\Exceptions\Handler. The processor first reports the exception and records it in the log file. Then render the exception response and send the response to the client. However, the built-in exception handler method is not easy to use. Many times we want to report exceptions to email or error log systems. The following example is to report exceptions to the Sentry system. Sentry is a very good error collection service. Use:

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

    parent::report($exception);
}

and the default rendering method. The JSON format of the response generated during form verification is often different from the unified JOSN format in our project, which requires us to customize the rendering method. Behavior.

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

The above is the detailed content of Laravel core analysis exception handling (code). For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:segmentfault.com. If there is any infringement, please contact admin@php.cn delete