RESTful services with Zend Framework (Part 2)

Previously, I covered the basic steps to create a RESTful Application with Zend Framework.

Today, we’re going to look into context switching, which will allow us to server different response formats based on the “Accept” HTTP header

I’ve seen many wasy of doing this, like using view scripts to generate JSON or XML, or creating the response DOM document manually, etc … but I found the simplest way is to stick to what we know, and what we know best in ZF is the setting the result variables through the view object, which we can easily serialize into any format we want using Zend’s own Zend_Serializer.

Zend Framework comes with a ContextSwitch action helper, but its only good to do JSON, so lets extend it and add more formats, I’m going to add AMF3, XML, PHP serialization, but you can add your own using either standard Zend_Serializer Adapters or building your own like we will for XML serialization

class REST_Controller_Action_Helper_ContextSwitch extends Zend_Controller_Action_Helper_ContextSwitch
{
    protected $_autoSerialization = true;

    protected $_availableAdapters = array(
        'amf'   => 'Zend_Serializer_Adapter_Amf3',
        'json'  => 'Zend_Serializer_Adapter_Json',
        'xml'   => 'REST_Serializer_Adapter_Xml',
        'php'   => 'Zend_Serializer_Adapter_PhpSerialize'
    );

    public function __construct($options = null)
    {
        if ($options instanceof Zend_Config)
        {
            $this->setConfig($options);
        }
        elseif (is_array($options))
        {
            $this->setOptions($options);
        }

        if (empty($this->_contexts))
        {
            $this->addContexts(
                array(
                    'amf' => array(
                        'suffix'    => 'json',
                        'headers'   => array(
                            'Content-Type' => 'application/octet-stream'
                        ),
                        'callbacks' => array(
                            'init' => 'initAbstractContext',
                            'post' => 'restContext'
                        ),
                    ),

                    'json' => array(
                        'suffix'    => 'json',
                        'headers'   => array(
                            'Content-Type' => 'application/json'
                        ),
                        'callbacks' => array(
                            'init' => 'initAbstractContext',
                            'post' => 'restContext'
                        ),
                    ),

                    'xml' => array(
                        'suffix'    => 'xml',
                        'headers'   => array(
                            'Content-Type' => 'application/xml'
                        ),
                        'callbacks' => array(
                            'init' => 'initAbstractContext',
                            'post' => 'restContext'
                        ),
                    ),

                    'php' => array(
                        'suffix'    => 'php',
                        'headers'   => array(
                            'Content-Type' => 'text/php'
                        ),
                        'callbacks' => array(
                            'init' => 'initAbstractContext',
                            'post' => 'restContext'
                        )
                    )
                )
            );
        }

        $this->init();
    }

    public function initAbstractContext()
    {
        if (!$this->getAutoSerialization())
        {
            return;
        }

        $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer');
        $view = $viewRenderer->view;

        if ($view instanceof Zend_View_Interface)
        {
            $viewRenderer->setNoRender(true);
        }
    }

    public function restContext()
    {
        if (!$this->getAutoSerialization())
        {
            return;
        }

        $view = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer')->view;

        if ($view instanceof Zend_View_Interface)
        {
            if (method_exists($view, 'getVars'))
            {
                $data = $view->getVars();

                if (count($data) !== 0)
                {
                    $serializer = new $this->_availableAdapters[$this->_currentContext];
                    $body = $serializer->serialize($view->getVars());

                    if ($this->_currentContext == 'xml')
                    {
                        $stylesheet = $this->getRequest()->getHeader('X-XSL-Stylesheet');

                        if ($stylesheet !== false and !empty($stylesheet))
                        {
                            $body = str_replace('', sprintf('', $stylesheet), $body);
                        }
                    }

                    if ($this->_currentContext == 'json')
                    {
                        $callback = $this->getRequest()->getParam('jsonp-callback', false);

                        if ($callback !== false and !empty($callback))
                        {
                            $body = sprintf('%s(%s)', $callback, $body);
                        }
                    }

                    $this->getResponse()->setBody($body);
                }
            }
        }

    }

    public function setAutoSerialization($flag)
    {
        $this->_autoSerialization = (bool) $flag;
        return $this;
    }

    public function getAutoSerialization()
    {
        return $this->_autoSerialization;
    }

}

for the most part, there is nothing special about this Class, it relies mostly on Zend_Controller_Action_Helper_ContextSwitch and mimics its logic:
The constructor adds the new contexts and defines the Content-Type header to send with the response.
initAbstractContext disables the view renderer completely (this replaces the method we used in Part 1)
restContext is where the magic happens!

The Magic!

first we grab the viewRenderer object through Zend_Controller_Action_HelperBroker and grab all the variables set from the Controller logic.
Next we run those variables through our serializer and set the serialized value as the response body!

You’ll notice there is some extra magic happening in there around XML and XSL Stylesheets. this is something I added to facilitate the usage of XSL Stylesheets in an XML Response, all it does is expect an optional X-XSL-Stylesheet header from the client request and inserts the xml-stylesheet declaration to the XML response.
That way if you are viewing this XML in a modern browser, the browser will automatically render the page into a more readable format using the specified XSL stylesheet.

We also look in the request for a jsonp-callback parameter to wrap around the body JSON response, this is most helpful when building web clients.

Note: I’m using a custom Zend_Serializer_Adapter: REST_Serializer_Adapter_Xml, which is a custom XML serialization class I’ve created, there is no point in describing it in this article since its irrelevant to the main topic, you can find the source in the repository.

Now we need to apply the ContextSwitch to each of our controllers actions, the easiest way to do so is with an Action Helper:

class REST_Controller_Action_Helper_RestContexts extends Zend_Controller_Action_Helper_Abstract
{
    protected $_contexts = array(
        'php',
        'xml',
        'json',
        'amf'
    );

    protected $_actions = array(
        'options',
        'head',
        'index',
        'get',
        'post',
        'put',
        'delete',
        'error'
    );

    public function preDispatch()
    {
        $controller = $this->getActionController();

        if (!$controller instanceof Zend_Rest_Controller)
        {
            return;
        }

        $this->_initContexts();
    }

    protected function _initContexts()
    {
        $contextSwitch = $this->getActionController()->getHelper('contextSwitch');

        $contextSwitch->setAutoSerialization(true);

        foreach ($this->_contexts as $context)
        {
            foreach ($this->_actions as $action)
            {
                $contextSwitch->addActionContext($action, $context);
            }
        }

        $contextSwitch->initContext();
    }
}

What this helper does is execute on preDispatch and set the all contexts on all actions of the current controller.

Now we need to initialize those two helpers, that can be done in Bootstrap.php:

    protected function _initActionHelpers()
    {
        $contextSwitch = new REST_Controller_Action_Helper_ContextSwitch();
        Zend_Controller_Action_HelperBroker::addHelper($contextSwitch);

        $restContexts = new REST_Controller_Action_Helper_RestContexts();
        Zend_Controller_Action_HelperBroker::addHelper($restContexts);
    }

But how is the context set for each request? by default, ZF uses the ?format= query parameter to define the context, but that is not very RESTful! the most elegant way is to rely on HTTP Accept headers to tell us what kind of response the client expects, to accomplish this, we need a plugin:

class REST_Controller_Plugin_RestHandler extends Zend_Controller_Plugin_Abstract
{
    public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
    {
        $this->getResponse()->setHeader('Vary', 'Accept');

        $mimeType = $this->getMimeType($request->getHeader('Accept'));

        switch ($mimeType) {
            case 'text/xml':
            case 'application/xml':
                $request->setParam('format', 'xml');
                break;

            case 'application/octet-stream':
                $request->setParam('format', 'amf');
                break;

            case 'text/php':
                $request->setParam('format', 'php');
                break;

            case 'application/json':
            default:
                $request->setParam('format', 'json');
                break;
        }
    }

    private function getMimeType($mimeTypes = null)
    {
        // Values will be stored in this array
        $AcceptTypes = Array ();

        // Accept header is case insensitive, and whitespace isn't important
        $accept = strtolower(str_replace(' ', '', $mimeTypes));

        // divide it into parts in the place of a ","
        $accept = explode(',', $accept);

        foreach ($accept as $a)
        {
            // the default quality is 1.
            $q = 1;

            // check if there is a different quality
            if (strpos($a, ';q='))
            {
                // divide "mime/type;q=X" into two parts: "mime/type" i "X"
                list($a, $q) = explode(';q=', $a);
            }

            // mime-type $a is accepted with the quality $q
            // WARNING: $q == 0 means, that mime-type isn't supported!
            $AcceptTypes[$a] = $q;
        }

        arsort($AcceptTypes);

        // let's check our supported types:
        foreach ($AcceptTypes as $mime => $q)
        {
            if ($q && in_array($mime, $this->availableMimeTypes))
            {
                return $mime;
            }
        }
        // no mime-type found
        return null;
    }

The plugin does two things: first it sets the response “Accept” header to “Vary” (this tells HTTP clients that we are able to serve multiple formats) then it sets the “format” parameter based on the best mime-type described in the request’s Accept header (the utility method “getMimeType” extracts and sorts the differnt values)

Here you can set any number of mime-types you want to support and match them with Serializers we’ve created previously.

Next, enabled this plugin in your application.ini

resources.frontController.plugins[] = "REST_Controller_Plugin_RestParams"

Going back to the FooController we created in Part 1, lets first get rid of the init method we created and modify the action methods to something like this:

    public function optionsAction()
    {
        $this->view->message = 'Resource Options';
        $this->getResponse()->setHttpResponseCode(200);
    }

    public function indexAction()
    {
        $this->view->resources = array();
        $this->getResponse()->setHttpResponseCode(200);
    }

    public function headAction()
    {
        $this->getResponse()->setHttpResponseCode(200);
    }

    public function getAction()
    {
        $this->view->id = $this->_getParam('id');
        $this->view->resource = new stdClass;
        $this->getResponse()->setHttpResponseCode(200);
    }

    public function postAction()
    {
        $this->view->message = 'Resource Created';
        $this->getResponse()->setHttpResponseCode(201);
    }

    public function putAction()
    {
        $this->view->message = sprintf('Resource #%s Updated', $this->_getParam('id'));
        $this->getResponse()->setHttpResponseCode(201);
    }

    public function deleteAction()
    {
        $this->view->message = sprintf('Resource #%s Deleted', $this->_getParam('id'));
        $this->getResponse()->setHttpResponseCode(200);
    }

Test it in the command line:

curl -v -H "Accept: application/json" http://localhost/v1/foo
curl -v -H "Accept: application/json" -X HEAD http://localhost/v1/foo
curl -v -H "Accept: application/json" -X OPTIONS http://localhost/v1/foo/1
curl -v -H "Accept: application/json" -X GET http://localhost/v1/foo/1
curl -v -H "Accept: application/json" -X POST http://localhost/v1/foo
curl -v -H "Accept: application/json" -X PUT -d '' http://localhost/v1/foo/1
curl -v -H "Accept: application/json" -X DELETE http://localhost/v1/foo/1

curl -v -H "Accept: application/xml" http://localhost/v1/foo
curl -v -H "Accept: application/xml" -X HEAD http://localhost/v1/foo
curl -v -H "Accept: application/xml" -X OPTIONS http://localhost/v1/foo/1
curl -v -H "Accept: application/xml" -X GET http://localhost/v1/foo/1
curl -v -H "Accept: application/xml" -X POST http://localhost/v1/foo
curl -v -H "Accept: application/xml" -X PUT -d '' http://localhost/v1/foo/1
curl -v -H "Accept: application/xml" -X DELETE http://localhost/v1/foo/1

You’ll notice that we are no longer setting the response body text directly, but rather setting the view object variables which will be serialized according to the Accept header we are sending in the request!
like I said, MAGIC!

All this is great, we got all sorts of magic happening in the output, but what about the input? shouldn’t we accept multiple formats as well?…
hellz yeah! lets do it!

Lets go back and edit REST_Controller_Plugin_RestHandler and add the following:

    private $availableMimeTypes = array(
        'php'           => 'text/php',
        'xml'           => 'application/xml',
        'json'          => 'application/json',
        'amf'           => 'application/octet-stream',
        'urlencoded'    => 'application/x-www-form-urlencoded'
    );

    private $methods = array('OPTIONS', 'HEAD', 'INDEX', 'GET', 'POST', 'PUT', 'DELETE');

    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
        if (!in_array(strtoupper($request->getMethod()), $this->methods))
        {
            $request->setActionName('options');
            $request->setDispatched(true);

            $this->getResponse()->setHttpResponseCode(405);

            return;
        }
        else
        {
            $contentType = $this->getMimeType($request->getHeader('Content-Type'));
            $rawBody = $request->getRawBody();

            if (!empty($rawBody))
            {
                try
                {
                    switch ($contentType)
                    {
                        case 'application/json':
                            $params = Zend_Json::decode($rawBody);
                            break;

                        case 'text/xml':
                        case 'application/xml':
                            $json = Zend_Json::fromXml($rawBody);
                            $params = Zend_Json::decode($json, Zend_Json::TYPE_OBJECT)->request;
                            break;

                        case 'application/octet-stream':
                            $serializer = new Zend_Serializer_Adapter_Amf3();
                            $params = $serializer->unserialize($rawBody);
                            break;

                        case 'text/php':
                            $params = unserialize($rawBody);
                            break;

                        case 'application/x-www-form-urlencoded':
                            $params = array();
                            parse_str($rawBody, $params);
                            break;

                        default:
                            $params = $rawBody;
                            break;
                    }

                    $request->setParams((array) $params);
                }
                catch (Exception $e)
                {
                    this->view->message = $e->getMessage();;
                    $this->getResponse()->setHttpResponseCode(400);
        
                    $request->setControllerName('error');
                    $request->setActionName('error');
                    $request->setParam('error', $error);

                    $request->setDispatched(true);

                    return;
                }
            }
        }
    }

The preDispatch runs just before any Controller method is initiated and does two things:
first, it checks if the requested HTTP Method is acceptable, and sends an OPTIONS response if its not (in accordance with the HTTP spec)
Then it looks for the request’s “Content-Type” header and un-serialze the raw body with a matching Zend_Serializer

and just like MAGIC our application can now parse different request formats!

Update [Monday, 07 March 2011]: added JSONP support.

Update [Sunday, 01 January 2012]: This article is out of date, please refer to the GitHub repo for updated instructions, the same principles still apply, but with some changes and slight modifications.

Note: all examples used are part of RESTful Zend Framework on GitHub