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 |
|
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 | namespace Cribbb\Exceptions; |
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 | /** |
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 | /** |
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 | /** |
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 | namespace Cribbb\Exceptions; |
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 | namespace Cribbb\Users\Exceptions; |
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 | /** |
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 | /** |
And finally we can convert the Exception into a JsonResponse in the handle() method:
1 | /** |
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.