Creating a dojo.store.JsonRest backend server using ZendFramework (PHP)

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:
Dojo & Zend Framework logos

  • 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:

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.

This entry was posted in JavaScript and tagged , , , , , . Bookmark the permalink.
  • http://www.google.com/ Rusty

    I’m out of lauege here. Too much brain power on display!

  • Billy Bob

    Excellent post, exactly what I was looking for!

    Many thanks!

  • Taneeda

    Hi,

    “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.”

    Could you give an example how to do this in this tutorial?

    Greets

  • Taneeda

    sorry, i was too fast…

  • Greg

    This is a fantastic article and exactly what I was looking for. Unfortunately I have never used Zend before (or PHP for that matter). Could you show how to ‘configure the default database adapter ‘? I added

    resources.db.adapter = “PDO_MYSQL”
    resources.db.params.host = “LOCALHOST”
    resources.db.params.username = “root”
    resources.db.params.password = “”
    resources.db.params.dbname = “blog”

    to the application.ini and updated the table name in the Item.php to an existing table in the blog datbase with an id field, but all my calls get 500 server errors.

  • http://blog.respondify.se/author/coa/ Christian O. Andersson

    What does the error message say? Is the server outputting a stack trace? (if not, try add “SetEnv APPLICATION_ENV development” to your .htaccess file in the /public folder)

    Is your Zend project setup correctly, i.e. can you see the default landing site?

  • Greg

    I could get the default landing site to show up ok so it seemed setup correctly in that respect. I will add the SetEnv APPLICATION_ENV development parameter and run again.

    Thanks for your help!

  • Swann

    Thank you for this post, it help me a lot.
    I’ve one problem, everything was working fine but I needed to use all my others controllers normaly. So I changed the bootstrap code into that :

    / Method in /application/Bootstrap.php
    protected function _initRest()
    {
    $front = Zend_Controller_Front::getInstance();
    $router = $front->getRouter();
    $restRoute = new Zend_Rest_Route($front, array(), array('default' => array('restdatacontroller')));
    $router->addRoute('rest', $restRoute);
    }

    And now I get this error :
    Fatal error: Uncaught exception ‘Zend_Controller_Action_Exception’ with message ‘Action “1″ does not exist and was not trapped in __call()’
    when I type this Url “http://localhost/restdata/1″

    Do you have any idea ? thank you in advance..

  • http://blog.respondify.se/author/coa/ Christian O. Andersson

    Try removing the ‘controller’ suffix.

    $restRoute = new Zend_Rest_Route($front, array(), array('default' => array('restdata')));

  • Swann

    it works thank you again :)

  • a1oCL3s5OfmT

    I discovered your blog website on google and examine numerous of your early posts. Continue to preserve up the superb operate. I merely extra up your RSS feed to my MSN News Reader. In search of forward to reading much more from you later on!