The Dojo Object Store API is the emerging standard for supplying data to dojo widgets. Based on the W3C’s IndexedDB object store API, that we will start seeing native browser support for as HTML5 gets adopted, it provides a solid, uniform and light-weight data layer used to shuttle any kind of data to any kind of widget.
While there are many good resources on how to use the API client side, there is much less written on how to support a JsonRest Object Store server side. In fact, any server that can send JSON-encoded objects in a RESTful way would work as a backend for dojo.store.JsonRest. The SitePen Blog has an example using Spring/Java. This post will give an example of how to implement a dojo.store.JsonRest backend using PHP and the Zend Framework.
The JsonRest object store API
The JsonRest object store access and mutates data using the following RESTful API:
- GET /data/ to get all items belonging to data
- GET /data/345 to get the item with id=345
- POST /data/ to add a new item to the data. The request body contains the new JSON-encoded item.
- PUT /data/345 to change the item with id=345. The request body contains the updated JSON-encoded item.
- DELETE /data/345 to remove the item from the data
Setting up the server using PHP / ZendFramework
Assuming you’ve already created a Zend project, we’ll just add a single Controller, which we will name DataController. Normally, Controllers extend the Zend_Controller_Action class. To make a controller serve data in a RESTful way, we must do two things:
- Change the parent class and instead inherit from Zend_Rest_Controller
- Tell the bootstrap we want RESTful routing for that controller
Setting up RESTful routing in ZendFramework can be done by adding an instance of Zend_Rest_Route to the FrontController’s router. To do this, we add an _init-method to our Bootstrap class.
// Method in <project_dir>/application/Bootstrap.php protected function _initRest() { $front = Zend_Controller_Front::getInstance(); $router = $front->getRouter(); $restRoute = new Zend_Rest_Route($front); $router->addRoute('rest', $restRoute); }
This will change the way Zend routes requests into a more RESTful-friendly way. Incoming requests will now be routed to the appropriate action, depending on their HTTP method, rather than their URL:
Request | will be routed to |
---|---|
GET /data/ | indexAction() |
GET /data/345 | getAction() |
POST /data/ | postAction() |
PUT /data/345 | putAction() |
DELETE /data/345 | deleteAction() |
We will also have to disable Zend’s view renderer, as to only output JSON encoded data. Alternatively, you could output your data to the view object and automatically JSON-encode it using the ContextSwitch action helper.
<?php // Located at <project_dir>/application/controllers/DataController.php class DataController extends Zend_Rest_Controller { public function init() { $this->_helper->viewRenderer->setNoRender(true); } public function indexAction() { } public function getAction() { } public function postAction() { } public function putAction() { } public function deleteAction() { } }
After setting up the general REST structure, it’s just a matter of filling in the blanks.
Starting with the simplest, the getAction(), we’ll just extract the requested ID and ask our model for an object matching that ID, and then use the JSON helper to automatically encode and send it to the client.
public function getAction() { $id = $this->_getParam('id'); $item = Application_Model_Item::getItemById($id); $this->getHelper('json')->sendJson($item); }
The postAction() will have to de-serialize the new object and tell the model to store it. Before returning the newly created object we must set the HTTP response code to ”201 Created” and tell the client where the object can be found, using the ”Location” HTTP header.
public function postAction() { $item = Zend_Json::decode($this->getRequest()->getRawBody()); if (!$item) { throw new Exception("Must supply an item..."); } $item = Application_Model_Item::create($item); $location = "/".$this->getRequest()->getControllerName()."/".$item['id']; $this->getResponse()->setHttpResponseCode(201); $this->getResponse()->setHeader("Location", $location); $this->getHelper('json')->sendJson($item); }
The putAction() must ensure that we’ve supplied both an id AS WELL AS the new properties of the object we want to change. Then we’ll tell the model to update the storage.
public function putAction() { if (!$id = $this->_getParam('id', false)) { throw new Exception("Must update a specific item..."); } $item = Zend_Json::decode($this->getRequest()->getRawBody()); if (!$item) { throw new Exception("Must supply an item..."); } $item = Application_Model_Item::update($id, $item); $this->getHelper('json')->sendJson($item); }
The deleteAction() only needs the ID of the object we want to delete. Upon a successful delete, we must set a ”204 No Content” HTTP status code.
public function deleteAction() { if (!$id = $this->_getParam('id', false)) { throw new Exception("Must delete a specific item..."); } Application_Model_Item::delete($id); $this->getResponse()->setHttpResponseCode(204); }
Finally, the indexAction() should return all – or a filtered selection – of the items in the collection. Note how the JsonRest object store utilizes the ”Range” HTTP header to identify the subset of items it wants. If you want to support this header-based paging, you must also send an HTTP response header with information on how many items there are in the collection.
public function indexAction() { $range = $this->getRequest()->getHeader('Range'); sscanf($range, "items=%d-%d", $start, $end); $items = Application_Model_Item::getItems($this->getRequest()->getQuery(), $start, $end); if ($range) { $total = Application_Model_Item::getItemsCount(); $this->getResponse()->setHeader("Content-Range", 'items '.$start.'-'.$end.'/'.$total); } $this->getHelper('json')->sendJson($items); }
Designing a data model
What should Application_Model_Item look like? This obviously depends on how you want to store the data. The code snippet below illustrates what a simple SQL-backed model could look like.
<?php // Located at <project_dir>/application/models/Item.php class Application_Model_Item { public static function getItems($filter, $start, $end) { $db = Zend_Db_Table_Abstract::getDefaultAdapter(); $sql = $db->select()->from('db_table'); foreach($filter as $key => $val) { if ($val == "null") $sql->where($key." IS NULL"); else $sql->where($key." = ?", $val); } if ($start) $sql->where('id > ?',$start); if ($end) $sql->where('id < ?',$end); return $sql->query()->fetchAll(); } public static function getItemsCount() { $db = Zend_Db_Table_Abstract::getDefaultAdapter(); $rows = $db->select()->from('db_table', array('count(*) as total'))->query()->fetchAll(); return $rows[0]['total']; } public static function getItemById($id) { $db = Zend_Db_Table_Abstract::getDefaultAdapter(); $rows = $db->select()->from('db_table')->where("id = ?", $id)->query()->fetchAll(); return $rows[0]; } public static function create($data) { $db = Zend_Db_Table_Abstract::getDefaultAdapter(); $db->insert('db_table', $data); return Application_Model_Item::getItemById($db->lastInsertId()); } public static function update($id, $data) { $db = Zend_Db_Table_Abstract::getDefaultAdapter(); $db->update('db_table', $data, array('id = ?' => $id)); return Application_Model_Item::getItemById($id); } public static function delete($id) { $db = Zend_Db_Table_Abstract::getDefaultAdapter(); $db->delete('db_table', array('id = ?' => $id)); } }
Next steps
Given that you’ve configured the default database adapter – and of course also set up your database – you should now have a working RESTful server, capable of serving a JsonRest object store. You can verify your setup by pointing your browser to the same URL you’d point the object store to. For extensive testing of all the different HTTP methods, RESTClient (Firefox) and Simple REST client (Chrome) are two great open source tools at your disposal.
Obviously, there’s always room for improving your backend. Some of the things you might consider include:
- Better handling of missing objects (404s).
- Richer support of queries in the indexAction() for sorting and filtering items.
- In case you’re feeding a dijit.Tree, consider supporting the deferItemLoadingUntilExpand property to ensure efficient lazy loading.
You might also want to take a look at Kitson Kelly’s RESTful service tutorial on DojoCampus. While it is still work-in-progress, it goes into more detail on the underlying principles than we do here.