In this situation, I always think of the interface first, then write PHP code to support it.
- It's a REST API, so meaningful HTTP status codes are a must.
- You want consistent, flexible data structures being sent to and from the client.
Let's think of all the things that could go wrong and their HTTP status codes:
- The server throws an error (500)
- Authentication failure (401)
- The resource requested was not found (404)
- The data you are modifying has been changed since you loaded it (409)
- Validation errors when saving data (422)
- The client has exceeded their request rate (429)
- Unsupported file type (415)
Note, there are others which you can research later.
For most of the failure conditions, there is only one error message to be returned. The 422 Unprocessable Entity
response, which I've used for "validation errors" could return more than one error --- One or more errors per form field.
We need a flexible data structure for error responses.
Take as an example, the 500 Internal Server Error
:
HTTP/1.1 500 Internal Server Error
Content-Type: text/json
Date: Fri, 16 Jan 2015 17:44:25 GMT
... other headers omitted ...
{
"errors": {
"general": [
"Something went catastrophically wrong on the server! BWOOP! BWOOP! BWOOP!"
]
}
}
Contrast that with simple validation errors when trying to POST something to the server:
HTTP/1.1 422 Unprocessable Entity
Content-Type: text/json
Date: Fri, 16 Jan 2015 17:44:25 GMT
... other headers omitted ...
{
"errors": {
"first_name": [
"is required"
],
"telephone": [
"should not exceed 12 characters",
"is not in the correct format"
]
}
}
They key here is the content type being text/json
. This tells client applications that they can decode the response body with a JSON decoder. If, say, an internal server error isn't caught and your generic "Something went wrong" web page gets delivered instead, the content type should be text/html; charset=utf-8
so client applications won't attempt to decode the response body as JSON.
This looks all find and dandy, until you need to support JSONP responses. You must return a 200 OK
response, even for failures. In this case you'll have to detect that the client is requesting a JSONP response (usually by detecting a URL request parameter called callback
) and change up the data structure a bit:
(GET /posts/123?callback=displayBlogPost)
<script type="text/javascript" src="/posts/123?callback=displayBlogPost"></script>
HTTP/1.1 200 OK
Content-Type: text/javascript
Date: Fri, 16 Jan 2015 17:44:25 GMT
... other headers omitted ...
displayBlogPost({
"status": 500,
"data": {
"errors": {
"general": [
"Something went catastrophically wrong on the server! BWOOP! BWOOP! BWOOP!"
]
}
}
});
Then the response handler on the client (in a web browser) should have a global JavaScript function called displayBlogPost
which accepts a single argument. This function would have to determine if the response was successful:
function displayBlogPost(response) {
if (response.status == 500) {
alert(response.data.errors.general[0]);
}
}
So we've taken care of the client. Now, let's take care of the server.
<?php
class ResponseError
{
const STATUS_INTERNAL_SERVER_ERROR = 500;
const STATUS_UNPROCESSABLE_ENTITY = 422;
private $status;
private $messages;
public function ResponseError($status, $message = null)
{
$this->status = $status;
if (isset($message)) {
$this->messages = array(
'general' => array($message)
);
} else {
$this->messages = array();
}
}
public function addMessage($key, $message)
{
if (!isset($message)) {
$message = $key;
$key = 'general';
}
if (!isset($this->messages[$key])) {
$this->messages[$key] = array();
}
$this->messages[$key][] = $message;
}
public function getMessages()
{
return $this->messages;
}
public function getStatus()
{
return $this->status;
}
}
And to use this in the case of a server error:
try {
// some code that throws an exception
}
catch (Exception $ex) {
return new ResponseError(ResponseError::STATUS_INTERNAL_SERVER_ERROR, $ex->message);
}
Or when validating user input:
// Validate some input from the user, and it is invalid:
$response = new ResponseError(ResponseError::STATUS_UNPROCESSABLE_ENTITY);
$response->addMessage('first_name', 'is required');
$response->addMessage('telephone', 'should not exceed 12 characters');
$response->addMessage('telephone', 'is not in the correct format');
return $response;
After that, you just need something that takes the returned response object and converts it into JSON and sends the response on its merry way.