I’ve recently been faced with the challenge of setting up a REST-full interface using Zend and I found it to be not so straight forward as one would expect from ZF. At first I tried with Zend_Rest_Server but I gave up quickly for 2 reasons:
- I didn’t like that it was too much of a RPC style server and almost not at all REST;
- I was integrating this REST api into an existing web application and I wanted to reuse the routing and bootstraping of the app which meant I had to hack and slash to integrate it which I don’t particularily enjoy.
I turned to the old router and controller instead and it was easier than expected.
Set up a route for the REST api
I wanted to use endpoints that looks like this: /rest/<resource>/[id] so I’ve set up my route as follows:
In application.ini:
|
1 2 3 4 |
resources.router.routes.rest.route = "rest/:resource/:id"
resources.router.routes.rest.defaults.controller = "rest"
resources.router.routes.rest.defaults.action = "rest"
resources.router.routes.rest.defaults.id = "" |
You could also do this in bootstrap.php :
|
1 2 3 4 5 6 7 8 |
$router = Zend_Controller_Front::getInstance()->getRouter();
$router->addRoute(
'rest',
new Zend_Controller_Router_Route(
'rest/:resource/:id',
array('controller' => 'rest','action' => 'rest')
)
); |
Extend Zend_Controller_Action to handle REST requests
Here is how a basic rest controller might look like.
|
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
class RestController extends Zend_Controller_Action
{
// HTTP verb to action mapping
protected $_methodMapping = array(
'GET' => 'read',
'POST' => 'create',
'PUT' => 'update',
'DELETE' => 'delete'
);
public function preDispatch()
{
$request = $this->getRequest();
// we only do this pre-dispatching logic if the action is rest
// otherwise we will cause an infinite loop in the front controller
if ($request->getActionName() != 'rest') {
return;
}
// find the HTTP method used to make the request (GET, POST, PUT.. )
$httpMethod = $request->getMethod();
// map it to an action name
$action = $this->_methodMapping[$httpMethod];
// get the data sent by the client
$data = $this->_getData();
// save thid data into the request
$request->setParam('data', $data);
// finaly, forward to the appropriate action
$this->_forward($action);
}
// a method to send error responsed to clients
protected function _error($code, $message)
{
$response = $this->getResponse();
$response->setHttpResponseCode($code);
$response->setHeader('X-Application-Error-Code', $message);
}
// a method for extracting data from the request
// getting POST and GET values is easy, we can access them using $_GET or
// $_POST superglobals or $request->getParam();
// for PUT and delete we have to parse the raw body of the request
protected function _getData($request)
{
$rawBody = $request->getRawBody();
$contentType = $request->getHeader('Content-Type');
$data = array();
switch (true) {
case strpos($contentType, 'application/json') !== false:
$data = Zend_Json::decode($rawBody);
break;
case strpos($contentType, 'application/xml') !== false:
//$data = /* parse xml */;
break;
}
// if we have POST
if (count($_POST)) {
$data += $_POST;
}
return $data;
}
// helper method to send a response to clients
protected function _sendResponse($data)
{
$response= $this->getResponse();
$response->setHeader('Content-Type', 'application/json');
echo Zend_Json::encode($data);
}
// the init method in wich we disable any layout and view renderers
public function init()
{
$this->_helper->layout()->disableLayout();
$this->_helper->viewRenderer->setNoRender(true);
}
// the dummy action for our controller. this should never be called because
// we are forwarding from it in preDispatch()
public function restAction()
{
}
// GET method
public function readAction()
{
}
// POST method
public function createAction()
{
}
// PUT method
public function updateAction()
{
}
// DELETE method
public function deleteAction()
{
} |
Routing and dispatching
I’m being fancy here by using the preDispatch() and _forward to call the actual method. preDispatch() is called before an action is called in the action controller. By calling _forward() I’m basicaly setting the isDispatched flag in the request object to false which will cause the action to be skipped. The front controller action loop will reiterate and redo the process for the new action. To avoind an infinite loop we need a breaking condition, which in this case is $request->getActionName() != 'rest'.
Getting data from client
Another thing worth metioning is how we get the data from the client. PHP makes it easy to access GET and POST data using the superglobals and Zend makes it even easier: $request->getParam(). For some reason, neither PHP nor Zend don’t provide any easy acess to data sent via the other request methods, specificaly PUT, so we have to do this ourselves.
All data sent by client is in the raw http body wich is accessible with $request->getRawBody(). We can also take a look at the Content-Type request header to get a hint of what kind of data we have (JSON, XML, etc..). Headers are acessible with $request->getHeader($headerKey).
Response
To have a truly RESTfull app we should be making good use of the HTTP response codes and headers. A good REST response will include an appropriate reponse code and a helpfull user error message. To do this you can use the setHttpResponseCode and setHeader methods of the reponse object.
Wikipedia has a list of HTTP status codes and you can also take a look at some common REST error responses on MSDN.
Next steps
Depending on your app you might want to turn this into an abstract controller and extend it for each resource or you could also use it as a single entry point and delegate to a separate resource handling class.