Restful Controllers with Zend Framework

I just read Bradley Holt’s post on using HTML 5 forms with REST and HTTP methods and he mentioned the REST router in the recently released version 1.9 of the Zend Framework. The good news is we don’t have to wait for mainstream support for HTML 5.
Zend Framework REST controller supports PUT and DELETE HTTP methods right now through overloading the POST method (ok, it’s a bit of hack – but it’s worked in Rails for ages). Although the ZF 1.9 announcement mentions using Zend_Rest_Route for public APIs, building any application (or parts of it) restfully has a couple of advantages:

  • Consistency and Convention
    REST implies that the application is broken up into resources and these resources are manipulated using the HTTP interface methods (GET, POST, PUT and DELETE). If each controller corresponds to a resource, the controller methods stay consistent throughout the app and that’s a good thing.
  • Clarity
    Using the restful actions force a limit on the number of controller methods preventing the controllers from becoming bloated. This makes them both easier to read and debug.
  • Safety
    A typical restful controller has just two url endpoints eg. a books controller typically has ‘books’ and ‘books/:id’ as endpoints. Non destructive actions (listing collections and listing a single resource) are triggered on a GET request and the destructive actions get called on the other requests (or just POST if method overloading is used). This prevents non-idempotent actions from using a GET request so mistakes like putting a delete button behind a get request are impossible.
    It also removes the need for HTTP method checking in the code so
    if ($this->_request->isPost()) isn’t needed.
  • Reuse
    When a public API is eventually needed the changes will be minimal.

The code snippet below is for an example restful BooksController extending the Zend_Rest_Controller. The Zend_Rest_Controller stubs out 5 abstract methods but if all the methods aren’t needed, the code still works if Zend_Controller_Action is extended.
The summary is that two resource endpoints are used:

  • ‘/books’ for a collection – a GET request lists the collection and a POST request adds to the collection)
  • ‘/books/:id’ for a single resource – a GET request lists the details for a single resource, a PUT request updates the resource and a DELETE request deletes the resource)

I have also added two helper methods to the controller – a newAction method to render the new book form and an editAction method to render the edit book form.

You can grab the full demo on github.

class BooksController extends Zend_Rest_Controller
{

	private $_booksTable;
	private $_form;

    public function init()
    {
        $bootstrap = $this->getInvokeArg('bootstrap');
        $db = $bootstrap->getResource('db');

		$options = $bootstrap->getOption('resources');
		$dbFile  = $options['db']['params']['dbname'];
		if (!file_exists($dbFile)) {
		    $createTable = "CREATE TABLE IF NOT EXISTS books (
    					id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    					name VARCHAR(32) NOT NULL,
					    price DECIMAL(5,2) NOT NULL
					)";
			$db->query($createTable);

			$insert1 = "INSERT INTO books (name, price) VALUES ('jQuery in Action', 39.99)";
			$insert2 = "INSERT INTO books (name, price) VALUES ('PHP in Action', 45.99)";
			$db->query($insert1);
	        $db->query($insert2);
		}

		$this->_booksTable = new Zend_Db_Table('books');
		$this->_form = new Default_Form_Book();
    }

    /**
     * The index action handles index/list requests; it should respond with a
     * list of the requested resources.
     */
    public function indexAction()
    {
        $this->view->books = $this->_booksTable->fetchAll();
    }

 	/**
     * The list action is the default for the rest controller
     * Forward to index
     */
    public function listAction()
    {
        $this->_forward('index');
    }

    /**
     * The get action handles GET requests and receives an 'id' parameter; it
     * should respond with the server resource state of the resource identified
     * by the 'id' value.
     */
    public function getAction()
    {
    	$this->view->book = $this->_booksTable->find($this->_getParam('id'))->current();
    }

	/**
     * Show the new book form
     */
    public function newAction() {
    	$this->view->form = $this->_form;
    }

    /**
     * The post action handles POST requests; it should accept and digest a
     * POSTed resource representation and persist the resource state.
     */
    public function postAction() {
    	if ($this->_form->isValid($this->_request->getParams())) {
    		$this->_booksTable->createRow($this->_form->getValues())->save();
       		$this->_redirect('books');
    	} else {
    		$this->view->form = $this->_form;
    		$this->render('new');
    	}
    }

 	/**
     * Show the edit book form. Url format: /books/edit/2
     */
    public function editAction() {
    	$book = $this->_booksTable->find($this->_getParam('edit'))->current();
    	$this->_form->populate($book->toArray());
    	$this->view->form = $this->_form;
    	$this->view->book = $book;
    }

    /**
     * The put action handles PUT requests and receives an 'id' parameter; it
     * should update the server resource state of the resource identified by
     * the 'id' value.
     */
    public function putAction() {
    	$book = $this->_booksTable->find($this->_getParam('id'))->current();
    	if ($this->_form->isValid($this->_request->getParams())) {
    		$book->setFromArray($this->_form->getValues())->save();
       		$this->_redirect('books');
    	} else {
    		$this->view->book = $book;
    		$this->view->form = $this->_form;
    		$this->render('edit');
    	}
    }

    /**
     * The delete action handles DELETE requests and receives an 'id'
     * parameter; it should update the server resource state of the resource
     * identified by the 'id' value.
     */
    public function deleteAction() {
    	$book = $this->_booksTable->find($this->_getParam('id'))->current();
    	$book->delete();
    	$this->_redirect('books');
    }

}

To get this working the following resource methods need to be added to the Bootstrap file.

    protected function _initRestRoute()
	{
		$this->bootstrap('Request');
		$front = $this->getResource('FrontController');
		$restRoute = new Zend_Rest_Route($front, array(), array(
		    'default' => array('books')
		));
		$front->getRouter()->addRoute('rest', $restRoute);
	}

    protected function _initRequest()
    {
        $this->bootstrap('FrontController');
        $front = $this->getResource('FrontController');
        $request = $front->getRequest();
    	if (null === $front->getRequest()) {
            $request = new Zend_Controller_Request_Http();
            $front->setRequest($request);
        }
    	return $request;
    }

Restful controllers do add some much-needed consistency to Zend framework apps and hopefully we’ll start seeing them widely used real soon.

Related posts:

  1. Restful authentication with Rails 2: Usage
  2. Zend Framework titbits
  3. Jara Base – a base Zend Framework app
This entry was posted in Zend Framework. Bookmark the permalink.

2 Responses to Restful Controllers with Zend Framework

  1. Really informative article post.Much thanks again. Really Cool.

  2. Mpie says:

    Best ZF Rest article so far. Keep up the good work!

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>