Building RESTful Services with Zend Framework
As a followup to my previous post, I now turn to RESTful web services. I originally encountered the term when attending php|tropics in 2005, where George Schlossnaggle likened it to simple GET and POST requests. Since then, the architectural style — and developer understanding of the architectural style — has improved a bit, and a more solid definition can be made.
At its heart, REST simply dictates that a given resource have a unique address, and that you interact with that resource using HTTP verbs. The standard verbs utilized are:
- GET: retrieve a list of resources, or, if an identifier is present, view a single resource
- POST: create a new resource with the data provided in the POST
- PUT: update an existing resource as specified by an identifier, using the PUT data
- DELETE: delete an existing resource as specified by an identifier
The standard URL structure used is as follows:
-
/resource
- GET (list) and POST operations -
/resource/{identifier}
- GET (view), PUT, and DELETE operations
What the REST paradigm provides you is a simple, standard way to structure your CRUD (Create-Read-Update-Delete) applications. Due to the large number of REST clients available, it also means that if you follow the rules, you get a ton of interoperability with those clients.
As of Zend Framework 1.9.0, it's trivially easy to create RESTful routes for your MVC application, as well as to handle the various REST actions via action controllers.
Zend_Rest_Route allows you to define RESTful controllers at several levels:
- You can make it the default route, meaning that unless you have additional routes, all controllers will be considered REST controllers.
- You can specify modules that contain RESTful controllers.
- You can specify specific controllers per module that are RESTful
As examples:
$front = Zend_Controller_Front::getInstance();
$router = $front->getRouter();
// Specifying all controllers as RESTful:
$restRoute = new Zend_Rest_Route($front);
$router->addRoute('default', $restRoute);
// Specifying the "api" module only as RESTful:
$restRoute = new Zend_Rest_Route($front, array(), array(
'api',
));
$router->addRoute('rest', $restRoute);
// Specifying the "api" module as RESTful, and the "task" controller of the
// "backlog" module as RESTful:
$restRoute = new Zend_Rest_Route($front, array(), array(
'api',
'backlog' => array('task'),
));
$router->addRoute('rest', $restRoute);
To define a RESTful action controller, you can either extend
Zend_Rest_Controller
, or simply define the following methods in a standard
controller extending Zend_Controller_Action
(you'll need to define them
regardless):
// Or extend Zend_Rest_Controller
class RestController extends Zend_Controller_Action
{
// Handle GET and return a list of resources
public function indexAction() {}
// Handle GET and return a specific resource item
public function getAction() {}
// Handle POST requests to create a new resource item
public function postAction() {}
// Handle PUT requests to update a specific resource item
public function putAction() {}
// Handle DELETE requests to delete a specific item
public function deleteAction() {}
}
For those methods that operate on individual resources (getAction()
,
putAction()
, and deleteAction()
), you can test for the identifier using the
following:
if (!$id = $this->_getParam('id', false)) {
// report error, redirect, etc.
}
Responding is an art
Many developers are either unaware of or ignore the part of the specification that dictates what the response should look like.
For instance, in classic REST, after performing a POST to create a new item, you should do the following:
- Set the HTTP response code to 201, indicating "Created"
- Set the Location header to point to the canonical URI for the newly created item:
/team/31
- Provide a representation of the newly created item
Note that there's no redirect, which flies in the face of standard web development (where GET-POST-Redirect is the typical format). This is a common "gotcha" moment.
Similarly, with PUT requests, you simply indicate an HTTP 200 status when successful, and show a representation of the updated item. DELETE requests should return an HTTP 204 status (indicating success - no content), with no body content.
Note: when building RESTful HTML applications, you may want to still do GET-POST-Redirect to prevent caching issues. The above applies to RESTful web services, which typically use XML or JSON for transactions, and have smart clients for interacting with the service.
I'll be writing another article soon showing some tips and tricks for
interacting with HTTP headers, both from the request and for the response, as
it's a subject lengthy enough for a post of its own. In the meantime, start
playing with Zend_Rest_Route
and standardizing on it for your CRUD operations!