Recently I’ve been putting together a Web application for a research project. I decided it was about time I really looked properly into REST so my Web interfaces are better structured. I won’t go into all the benefits here, you can read for yourself. Suffice to say it seems like a good approach to take.
This is quite a long article and you might only be interested in some of it so here are the sections:
- Getting access to request data
- Returning appropriate responses
- Handling different data formats
- Passing all requests to a single PHP script
- Determining the full requested URL
- A generic REST Service class
If you have an suggestions for improvement, please let me know – this was a first attempt!
Some credit for this article should go to lornajane for her PHP REST Server articles (Part 1, Part 2, Part 3) which I used as a good starting point.
Getting access to request data
Getting access to HTTP GET arguments or POST data is well known and easy. With a REST interface you are likely to support other HTTP verbs such as PUT and DELETE. Getting data from these requests is not obvious but luckily lornajane had worked this out for me:
parse_str(file_get_contents('php://input'), $arguments);
Returning appropriate responses
With REST you should really make use of the HTTP Status Codes for all requests (rather that using 200 OK for everything). In PHP, this can be done with the header function in a couple of ways:
// When setting just the status code
header('HTTP/1.1 405 Method Not Allowed');
// When you're setting another header at the same time
header('Allow: GET, HEAD, POST, DELETE', true, 405);
I haven’t found a way of sending just the status code number (e.g. 405) without specifying the exact text (‘Method Not Allowed’) but these are part of a fixed specification so it’s not a huge problem.
The following is a list of status codes that I’ve used and why:
- 200 OK: successful request when data is returned
- 201 Created: Successful request when something is created at another URL (specified by the value returned in the Location header)
- 204 No Content: Successful request when no data is returned
- 400 Bad Request: Incorrect parameters specified on request
- 404 Not Found: No resource at the specified URL
- 405 Method Not Allowed: when a client makes a request using an HTTP verb not supported at the requested URL (supported verbs are returned in the Allow header)
- 406 Not Acceptable: Requested data format not supported
- 500 Internal Server Error: An unexpected error occurred
- 501 Not Implemented: when a client makes a request using an unknown HTTP verb
Handling different data formats
As URLs are supposed to represent resources in REST, it is a feasible requirement that client applications would like to receive responses from the same URL in different formats. Clients can specify the data formats they are capable of understanding using the HTTP Accept header. In Javacript this can be set using the setRequestHeader method on the XMLHttpRequest object. Using jQuery as my current Javascript library of choice, the code for doing this is as follows:
$.ajax({
type: method, // GET, POST, PUT, DELETE etc.
url: url, // The actual URL to make the request
data: data, // Any data/parameters to send to the server
beforeSend: function(xmlHttpRequest) {
xmlHttpRequest.setRequestHeader('Accept', format); // MIME Type
}
});
This then needs to be handled by the server. In PHP this data can be retrieved using $_SERVER['HTTP_ACCEPT']. I also found a handy library for parsing this header according to the specification (not trivial), cleverly named ‘HTTP_ACCEPT‘. I then wanted a way of knowing the most appropriate format from a list of formats I would support so I threw the following code together:
$accept = new HTTP_Accept($_SERVER['HTTP_ACCEPT']);
foreach ($supportedFormats as $supportedFormat) {
$supportedFormatQuality = $accept->getQuality($supportedFormat);
if ((!isset($bestFormat) && $supportedFormatQuality > 0) ||
$bestQuality < $supportedFormatQuality) {
$bestFormat = $supportedFormat;
$bestQuality = $supportedFormatQuality;
}
}
The server also needs to specify the type of content it is returning; this can be done using the Content-Type header as follows:
header("Content-Type: $format");
Passing all requests to a single PHP script
I needed all requests with a certain URL prefix to be handled by a single script. This was easily solved using the following URL re-writing rule in a .htaccess file:
Options +FollowSymLinks
RewriteEngine on
RewriteRule ^.*$ index.php
Note that I had to enable the mod_rewrite module in Apache and ensure that directives could be overridden with the following:
<Directory /var/www/>
AllowOverride All
</Directory>
Determining the full requested URL
Another requirement of my application complicated by URL rewriting was that I needed access to the full URL. I achieved this with the following bit of code:
$protocol = $_SERVER['HTTPS'] == 'on' ? 'https' : 'http';
$location = $_SERVER['REQUEST_URI'];
if ($_SERVER['QUERY_STRING']) {
$location = substr($location, 0, strrpos($location, $_SERVER['QUERY_STRING']) - 1);
}
$url = $protocol.'://'.$_SERVER['HTTP_HOST'].$location;
To allow me to reuse some of the REST code I'd created, I put it into a generic REST Service class which I subclassed and replaced different methods for different means:
class RestService {
private $supportedMethods;
public function __construct($supportedMethods) {
$this->supportedMethods = $supportedMethods;
}
public function handleRawRequest($_SERVER, $_GET, $_POST) {
$url = $this->getFullUrl($_SERVER);
$method = $_SERVER['REQUEST_METHOD'];
switch ($method) {
case 'GET':
case 'HEAD':
$arguments = $_GET;
break;
case 'POST':
$arguments = $_POST;
break;
case 'PUT':
case 'DELETE':
parse_str(file_get_contents('php://input'), $arguments);
break;
}
$accept = $_SERVER['HTTP_ACCEPT'];
$this->handleRequest($url, $method, $arguments, $accept);
}
protected function getFullUrl($_SERVER) {
$protocol = $_SERVER['HTTPS'] == 'on' ? 'https' : 'http';
$location = $_SERVER['REQUEST_URI'];
if ($_SERVER['QUERY_STRING']) {
$location = substr($location, 0, strrpos($location, $_SERVER['QUERY_STRING']) - 1);
}
return $protocol.'://'.$_SERVER['HTTP_HOST'].$location;
}
public function handleRequest($url, $method, $arguments, $accept) {
switch($method) {
case 'GET':
$this->performGet($url, $arguments, $accept);
break;
case 'HEAD':
$this->performHead($url, $arguments, $accept);
break;
case 'POST':
$this->performPost($url, $arguments, $accept);
break;
case 'PUT':
$this->performPut($url, $arguments, $accept);
break;
case 'DELETE':
$this->performDelete($url, $arguments, $accept);
break;
default:
/* 501 (Not Implemented) for any unknown methods */
header('Allow: ' . $this->supportedMethods, true, 501);
}
}
protected function methodNotAllowedResponse() {
/* 405 (Method Not Allowed) */
header('Allow: ' . $this->supportedMethods, true, 405);
}
public function performGet($url, $arguments, $accept) {
$this->methodNotAllowedResponse();
}
public function performHead($url, $arguments, $accept) {
$this->methodNotAllowedResponse();
}
public function performPost($url, $arguments, $accept) {
$this->methodNotAllowedResponse();
}
public function performPut($url, $arguments, $accept) {
$this->methodNotAllowedResponse();
}
public function performDelete($url, $arguments, $accept) {
$this->methodNotAllowedResponse();
}
}