Laravel异常处理构建

现在努力的你,未来的事情谁说得清楚呢? 但你不努力,未来是怎样大概都知道了。

Exceptions are a very important method for controlling the execution flow of an application. When an application request diverges from the happy path, it’s often important that you halt execution immediately and take another course of action.

Dealing with problems in an API is especially important. The response from the API is the user interface and so you need to ensure you give a detailed and descriptive explanation of what went wrong.

This includes the HTTP Status code, and error code that is linked to your documentation, as well as a human readable description of what went wrong.

In today’s tutorial I’m going to show you how I structure my Laravel API applications to use Exceptions. This structure will make it very easy to return detailed and descriptive error responses from your API, as well as make testing your code a lot easier.

After a brief hiatus I’m returning to writing about Laravel. After writing about PHP week after week for a long time I was really burned out and needed to take a break.

However, over the last couple of months I’ve been doing some of my best work focusing on Laravel API applications. This has given me a renewed focus with lots of new ideas and techniques I want to share with you.

Instead of starting a new project I’m just going to reboot the existing Cribbb series. All of the code from the previous tutorials can still be found within the Git history.

Understanding HTTP Status Codes

The first important thing to understand when building an API is that the Internet is built upon standards and protocols. Your API is an “interface” into your application and so it is very important that you adhere to these standards.

When a web service returns a response from a request, it should include a Status Code. The Status Code describes the response, whether the request was successful or if an error has occurred.

For example, when a request is successful, your API should return a Status Code of 200, when the client makes a bad request, you should return a Status Code of 400, and if there is an internal server error, you should return a Status Code of 500.

By sticking to these Status Codes and using them under the correct conditions, we can make our API easier to consume for third-party developers and applications.

If you are unfamiliar with the standard HTTP Status Codes I would recommend bookmarking the Wikipedia page and referring to it often. You will find you only ever really use a small handful of the status codes, but it is a good idea to be familiar with them.

How this Exception foundation will work

Whenever we return a response from the API it must use one of the standard HTTP status codes. We must also use the correct status code to describe what happened.

Using the incorrect status code is a really bad thing to do because you are giving the consumer of the API bad information. For example, if you return a 200 Status Code instead of a 400 Status Code, the consumer won’t know they are making an invalid request.

Therefore, we should be able to categorise anything that could possible go wrong in the application as one of the standard HTTP Status Codes.

For example, if the client requests a resource that doesn’t exist we should return a 404 Not Found response.

To trigger this response from our code, we can throw a matching NotFoundException Exception.

To do this we can create base Exception classes for each HTTP status code.

Next, in our application code we can create specific Exceptions that extend the base HTTP Exceptions to provide a more granular understanding of what went wrong. For example, we might have a UserNotFound Exception that extends the NotFoundException base Exception class.

This means under an exceptional circumstance we can throw a specific Exception for that problem and let it bubble up to the surface.

The application will automatically return the correct HTTP response from the base Exception class.

Finally we also need a way of providing a descriptive explanation of what went wrong. We can achieve this by defining error messages that will be injected when the exception class is thrown.

Hopefully that makes sense. But even if it doesn’t, continue reading as I think it will all fall into place as we look at some code.

Creating the Errors configuration file

It’s very important that you provide your API responses with a descriptive explanation of what went wrong.

If you don’t provide details of exactly what went wrong the consumers of your API are going to struggle to fix the issue.

To keep all of the possible error responses in one place I’m going to create an errors.php configuration file under the config directory.

This will mean we have all of the possible errors in one place which will make creating documentation a lot easier.

It should also make it easy to provide language translations for the errors too, rather than trying to dig through the code to find every single error!

To begin with I’ve created some errors for a couple of the standard HTTP error responses:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

return [

/*
|—————————————————————————————————————
| Default Errors
|—————————————————————————————————————
*/

'bad_request' => [
'title' => 'The server cannot or will not process the request due to something that is perceived to be a client error.',
'detail' => 'Your request had an error. Please try again.'
],

'forbidden' => [
'title' => 'The request was a valid request, but the server is refusing to respond to it.',
'detail' => 'Your request was valid, but you are not authorised to perform that action.'
],

'not_found' => [
'title' => 'The requested resource could not be found but may be available again in the future. Subsequent requests by the client are permissible.',
'detail' => 'The resource you were looking for was not found.'
],

'precondition_failed' => [
'title' => 'The server does not meet one of the preconditions that the requester put on the request.',
'detail' => 'Your request did not satisfy the required preconditions.'
]

];

As the application is developed I can add to this list. It’s also often a good idea to provide a link to the relevant documentation page. At some point in the future I can simply add this into each error.

Creating the abstract Exception

Next I want to create an abstract Exception class that all of my application specific exceptions will extend from.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php namespace Cribbb\Exceptions;

use Exception;

abstract class CribbbException extends Exception
{
/**
* @var string
*/
protected $id;

/**
* @var string
*/
protected $status;

/**
* @var string
*/
protected $title;

/**
* @var string
*/
protected $detail;

/**
* @param @string $message
* @return void
*/
public function __construct($message)
{
parent::__construct($message);
}
}

This will make it easy to catch all of the application specific exceptions and provides a clean separation from the other potential exceptions that may be thrown during the application’s execution.

For each exception I will provide an id, status, title and detail.

This is to stay close to the JSON API specification.

I will also provide a getStatus method

1
2
3
4
5
6
7
8
9
/**  
* Get the status
*
* @return int
*/
public function getStatus()
{
return (int) $this->status;
}

The JSON API specification states that the status code should be a string. I’m casting it as an int in this method so I can provide the correct response code to Laravel.

I will also provide a toArray() method to return the Exception as an array. This is just for convenience:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**  
* Return the Exception as an array
*
* @return array
*/
public function toArray()
{
return [
'id' => $this->id,
'status' => $this->status,
'title' => $this->title,
'detail' => $this->detail
];
}

Finally I need to get the title and detail for each specific error from the errors.php file.

To do this I will accept the exception id when a new Exception is instantiated.

I will then use this id to get the title and detail from the errors.php file.

So throwing an Exception will look like this:

1
throw new UserNotFound('user_not_found');  

However, I will also want to provide specific details of the Exception under certain circumstances.

For example I might want to provide the user’s id in the exception detail.

To do this I will allow the exception to accept an arbitrary number of arguments:

1
throw new UserNotFound('user_not_found', $id);  

To set up the Exception with the correct message I will use the following method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**  
* Build the Exception
*
* @param array $args
* @return string
*/
protected function build(array $args)
{
$this->id = array_shift($args);

$error = config(sprintf('errors.%s', $this->id));

$this->title = $error['title'];
$this->detail = vsprintf($error['detail'], $args);

return $this->detail;
}

In this method I first pop off the first argument as this will be the id.

Next I get the title and detail from the errors.php configuration file using the id.

Next I vsprintf the remaining arguments into the detail string if they have been passed into the exception.

Finally I can return the detail to be used as the default Exception message.

Creating the base Exceptions

With the abstract Exception in place I can now create the base Exceptions.

For example, here is the NotFoundException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php namespace Cribbb\Exceptions;

class NotFoundException extends CribbbException
{
/**
* @var string
*/
protected $status = '404’;

/**
* @return void
*/
public function __construct()
{
$message = $this->build(func_get_args());

parent::__construct($message);
}
}

Each base foundation Exception simply needs to provide the status code and a call to the __construct() method that will call the build() method and pass the message to the parent.

You can now create these simple Exception classes to represent each HTTP status code your application will be using.

If you need to add a new HTTP status code response, it’s very easy to just create a new child class.

Creating the application Exceptions

Finally we can use these base HTTP Exceptions within our application code to provide more specific Exceptions.

For example you might have a UserNotFound Exception:

1
2
3
4
5
6
7
8
<?php namespace Cribbb\Users\Exceptions;

use Cribbb\Exceptions\NotFoundException;

class UserNotFound extends NotFoundException
{

}

Now whenever you attempt to find a user, but the user is not found you can throw this Exception.

The Exception will bubble up to the surface and the correct HTTP Response will be automatically returned with an appropriate error message.

This means if an Exception is throw, you can just let it go, you don’t have to catch it because the consumer needs to be informed that the user was not found.

And in your tests you can assert a UserNotFound exception was thrown, rather than just a generic NotFound exception. This means you can write tests where you are confident the test is failing for the correct reason. This makes reading your tests very easy to understand.

Dealing with Exceptions and returning the correct response

Laravel allows you to handle Exceptions and return a response in the Handler.php file under the Exceptions namespace.

The first thing I’m going to do is to add the base CribbbException class to the $dontReport array.

1
2
3
4
5
6
7
8
9
/**  
* A list of the exception types that should not be reported
*
* @var array
*/
protected $dontReport = [
HttpException::class,
CribbbException::class
];

I don’t need to be told that an application specific Exception has been thrown because this is to be expected. By extending from the base CribbbException class we’ve made it very easy to capture all of the application specific exceptions.

Next I’m going to update the render() method to only render the Exception if we’ve got the app.debug config setting to true, otherwise we can deal with the Exception in the handle() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**  
* Render an exception into an HTTP response
*
* @param Request $request
* @param Exception $e
* @return Response
*/
public function render($request, Exception $e)
{
if (config('app.debug')) {
return parent::render($request, $e);
}

return $this->handle($request, $e);
}

And finally we can convert the Exception into a JsonResponse in the handle() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**  
* Convert the Exception into a JSON HTTP Response
*
* @param Request $request
* @param Exception $e
* @return JSONResponse
*/
private function handle($request, Exception $e) {
if ($e instanceOf CribbbException) {
$data = $e->toArray();
$status = $e->getStatus();
}

if ($e instanceOf NotFoundHttpException) {
$data = array_merge([
'id' => 'not_found',
'status' => '404'
], config('errors.not_found'));

$status = 404;
}

if ($e instanceOf MethodNotAllowedHttpException) {
$data = array_merge([
'id' => 'method_not_allowed',
'status' => '405'
], config('errors.method_not_allowed'));

$status = 405;
}

return response()->json($data, $status);
}

For the CribbbException classes we can simply call the toArray() method to return the Exception into an array as well as the getStatus() method to return the HTTP Status Code.

We can also deal with any other Exception classes in this method. As you can see I’m catching the NotFoundHttpException and MethodNotAllowedHttpException Exceptions in this example so I can return the correct response.

Finally we can return a JsonResponse by using the json() method on the response() helper function method with the $data and $status included.

Conclusion

Exceptions are a very important aspect of application development and they are an excellent tool in controlling the execution flow of the application.

Under exceptional circumstances you need to halt the application and return an error rather than continuing on with execution. Exceptions make this very easy to achieve.

It’s important that an API always returns the correct HTTP status code. The API is the interface to your application and so it is very important that you follow the recognised standards and protocols.

You also need to return a human readable error message as well as provide up-to-date documentation of the problem and how it can be resolved.

In today’s tutorial we’ve created a foundation for using Exceptions in the application by creating base classes for each HTTP status code.

Whenever a problem arrises in the application we have no reason not to return the specific HTTP status code for that problem.

We’ve also put in place an easy way to list detailed error messages for every possible thing that could go wrong.

This will be easy to keep up-to-date because it’s all in one place.

And finally we’ve created an easy way to use Exceptions in the application. By extending these base Exceptions with application specific exceptions we can create a very granular layer of Exceptions within our application code.

This make it very easy to write tests where you can assert that the correct Exception is being thrown under the specific circumstances.

And it also make it really easy to deal with exceptions because 9 times out of 10 you can just let the exception bubble up to the surface.

When the exception reaches the surface, the correct HTTP status code and error message will automatically be returned to the client.