Practice of Exception Handling in Laravel Core Interpretation

  • 2021-11-29 06:15:37
  • OfStack

Preface

Exception handling is one of the 10 most important but easily overlooked language features in programming. It provides a mechanism for developers to handle runtime errors. For program design, correct exception handling can prevent the disclosure of program details to users, provide developers with a complete error backtracking stack, and at the same time improve the robustness of the program.

In this article, we will briefly sort out the exception handling capabilities provided in Laravel, and then talk about the practice of using exception handling in development, how to use custom exceptions, and how to extend the exception handling capabilities of Laravel.

The following words are not much to say, let's take a look at the detailed introduction

Register Exception Handler

Here we go back to the bootstrap phase before Kernel handles the request, which we have said many times. In the Illuminate\ Foundation\ Bootstrap\ HandleExceptions section of bootstrap phase, Laravel sets up 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']) Register the handleException method of HandleExceptions as the program's global processor method:


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

//  Render CLI Exceptional response to a request 
protected function renderForConsole(Exception $e)
{
 $this->getExceptionHandler()->renderForConsole(new ConsoleOutput, $e);
}

//  Render HTTP Exceptional response to a request 
protected function renderHttpResponse(Exception $e)
{
 $this->getExceptionHandler()->render($this->app['request'], $e)->send();
}

In the processor mainly through the report ExceptionHandler method to report exceptions, here is the record of exceptions to storage/laravel. log file, and then according to the request type rendering abnormal response generated output to the client. Here, ExceptionHandler is an instance of the\ App\ Exceptions\ Handler class, which was 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 function set_error_handler under 1, Its function is to register the error handler function, Because most of the old codes or class libraries use PHP function trigger_error function to throw errors, Exception handler can only handle Exception but not Error, so in order to be compatible with the old class library, set_error_handler is usually used to register the global error handler method. After the error is captured in the method, the error is converted into an exception and then thrown again, so that all the code in the project can throw an exception instance when it is 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);
 }
}

Common Laravel exception instances

In Laravel, the corresponding exception instances are thrown for common program exceptions, which enables developers to catch these runtime exceptions and do subsequent processing according to their own needs (for example, calling another remedial method in catch, recording exceptions to log files, sending alarm emails and short messages)

Here I list some development often encountered exceptions, and explain in what circumstances they are thrown, usually code 1 must pay attention to capture these exceptions in the program to handle exceptions in order to make the program more robust.

This exception is thrown when an error occurs in executing an SQL statement in Illuminate\ Database\ QueryException Laravel, It is also the most frequently used exception, Used to catch SQL execution errors, For example, when executing Update statement, many people like to judge whether UPDATE is successful by judging the number of modified lines after SQL is executed. However, in some scenarios, the UPDATE statement executed does not modify the record value, so it is impossible to judge whether UPDATE is successful through the modified function. In addition, if QueryException is captured during transaction execution, the transaction can be rolled back in catch code block. Illuminate\ Database\ Eloquent\ ModelNotFoundException throws this exception if a single record is not found through the model's findOrFail and firstOrFail methods (NULL is returned when no data is found in find and first). This exception is thrown when an Illuminate\ Validation\ ValidationException request fails FormValidator authentication for Laravel. Illuminate\ Auth\ Access\ AuthorizationException This exception is thrown when a user request fails Laravel policy (Policy) authentication HTTP Method incorrect when Symfony\ Component\ Routing\ Exception\ MethodNotAllowedException request route This exception is thrown when the processing of an HTTP request for Illuminate\ Http\ Exceptions\ HttpResponseException Laravel is unsuccessful

Exception Handler for Extended Laravel

As mentioned above, Laravel successfully registered\ App\ Exceptions\ Handler as a global exception handler. Exceptions that are not received by catch in the code will be captured by\ App\ Exceptions\ Handler at last. The processor first reports the exception record to the log file and then renders the exception response and then sends the response to the client. However, the method of self-contained exception handler is not easy to use. Most of the time, we want to report exceptions to mail or error log system. The following example is to report exceptions to Sentry system. Sentry is an error collection service that is very easy to use:


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

 parent::report($exception);
}

There is also the default rendering method. The JSON format that generates the response during form validation is often different from the JOSN format in our project, which requires us to customize the behavior of the rendering method.


public function render($request, Exception $exception)
{
 // If the client expects JSON Response ,  In API Request failed Validator Validate the throw ValidationException Posterior 
 // Here to customize the response returned to the client .
 if ($exception instanceof ValidationException && $request->expectsJson()) {
  return $this->error(422, $exception->errors());
 }

 if ($exception instanceof ModelNotFoundException && $request->expectsJson()) {
  // Object thrown after the routing model binding cannot find the model in the database NotFoundHttpException
  return $this->error(424, 'resource not found.');
 }


 if ($exception instanceof AuthorizationException) {
  // Object thrown when capturing a non-compliant permission  AuthorizationException
  return $this->error(403, "Permission does not exist.");
 }

 return parent::render($request, $exception);
}

After customization, ValidationException will be thrown when the request fails FormValidator verification, and then the exception handler will format the error prompt into JSON response format of Project 1 and output it to the client after catching the exception. In this way, the logic of judging whether the form verification passes or not and outputting the error response to the client is completely omitted in our controller. Giving this part of logic to the exception handler of Unification 1 for execution can make the controller method much slimmer.

Using custom exceptions

This section is not really a custom exception for the Laravel framework, but the custom exception I mentioned here can be applied in any project.

I've seen a lot of people return different arrays for different errors in Repository or Service methods, It contains the response error code and error information, which can of course meet the development requirements, but it can't record the runtime context of the application when an exception occurs, and it is very unfavorable for developers to locate the problem if they can't record the context information when an error occurs.

The following is a custom exception class


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

After defining the exception class, we can throw exception instances in the code logic


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) {
   .......
  }
 }
}

In addition to the cases listed above in Repository, more often than not, we throw more detailed exception instances related to business in catch code block after catching the general exceptions listed above, which is convenient for developers to locate problems. We modify the above updateUserFavorites according to this strategy.


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

When defining the UserMangeException class above, the fourth parameter $previous is an instance of a class that implements the Throwable interface, In this scenario we throw an instance of UserManagerException because we caught an exception instance of QueryException, This parameter is then used to pass the QueryException instance to the PHP exception stack, which gives us the ability to trace back the entire exception to get more context information than just the currently thrown exception instance. In the error collection system, you can use code like the following to get information about all exceptions.


while($e instanceof \Exception) {
 echo $e->getMessage();
 $e = $e->getPrevious();
}

Exception handling is a very important function of PHP, but it is easy for developers to ignore it. This article briefly explains the internal exception handling mechanism of Laravel and the ways and means to extend Laravel exception handling. More space focuses on sharing some programming practices of exception handling, which is exactly one programming habit that I hope every reader can understand and practice, including the application of Interface shared earlier.

Summarize


Related articles: