Skip to content

RESTful services with Zend Framework 1.0

Building a fully RESTful API with Zend Framework 1.0.

Ahmad Nassri
Ahmad Nassri
13 min read
RESTful services with Zend Framework 1.0

This is a re-post of an old blog post that gets a lot of traffic, and people have asked me about it since my move to Sbvtle.

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.

I started an open-source project called: RESTful Zend Framework which is a custom Zend Framework Application built to act as a REST API.

The following describing how I built the REST implementation from scratch.

Some Assumptions:

I’m going to skip the initial project creation and assume you already know the basics, if not, spend some time on Google figuring it out first (I don’t recommend the ZF documentation since it’s more confusing than helpful!)

My entire application is meant to be a REST API, I don’t have a mixed mode of web controllers and REST controllers, though it is easily possible to have both, with very little modifications to fit your needs once you read through the steps.

I believe APIs should have maintainable revisions and as such I’ve used Zend Framework Modules to separate my API revisions, again, its easy enough to skip that and suit your own needs.

Configuration:

As always, the first thing you’d wanna do is customize your application.ini file, here are the lines I’ve added:

resources.frontController.defaultModule = "v1"
resources.frontController.moduleDirectory = APPLICATION_PATH "/modules"
resources.frontController.moduleControllerDirectoryName = "controllers"

resources.modules = ""

You should also remove the following line from your application.ini file, otherwise ZF will complain about missing Module Bootstrap calsses.

resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers"

This will enable Modules in your Application and setup the default module name as v1. (At this point you would want to re-organize your directory structure to accommodate modules.)

You can skip this step if you don’t want Modules as part of your Application.

Setting up the REST Route:

Now we need to setup routing, ZF comes with its own REST route, so lets make sure all our controllers are setup to go through it by adding the following to our application.ini

resources.router.routes.rest.type = Zend_Rest_Route

The REST route controls the URL scheme for our application, more on that later.

Note: this method will apply the REST route to ALL controllers If you are thinking of having a mix of REST controllers and regular Zend_Action_Controller you need to customize the route rules by initiating it in your Bootstrap.php instead.

At this point, some clean-up of your application directory is probably a good idea … depending on your set-up of the REST route, you might want to delete the views folder if you apply REST to all your controllers. I created a new folder named v1 under application/modulesto match my module setup, and moved the controllers & models folders into it.

Controllers vs. Resources

In a typical ZF Application you would want to have your controllers extend Zend_Controller_Action, but in this case, we want them to extend Zend_Rest_Controller.

<?php
class FooController extends Zend_Rest_Controller

Zend_Rest_Controller is just an abstract controller with a list of predefined Actions that we should implement in each of our Controllers:

<?php
class FooController extends Zend_Rest_Controller
{
  public function indexAction()
  {}

  public function getAction()
  {}

  public function postAction()
  {}

  public function putAction()
  {}

  public function deleteAction()
  {}
}

The first thing you’ll notice is that the Actions represent the most basic HTTP Methods: GET, POST, PUT and DELETE which are the core of our RESTful implementation. at this point you should start thinking of your Controllers as Resources (you might wanna read up on REST to clearify this bit) Also, we didn’t create any view scripts for this Controller, and as we are not going to need the view renderer anymore, lets disable it:

<?php
public function init()
{
  $this->_helper->viewRenderer->setNoRender(true);
}

this is a temporary solution and will only disable the view rendered for this controller, we eventually want to disable it for all our REST controllers (covered in later steps).

Now lets add some response output and give it a go!

<?php
public function indexAction()
{
  $this->getResponse()->setBody('Hello World');
  $this->getResponse()->setHttpResponseCode(200);
}

public function getAction()
{
  $this->getResponse()->setBody('Foo!');
  $this->getResponse()->setHttpResponseCode(200);
}

public function postAction()
{
  $this->getResponse()->setBody('resource created');
  $this->getResponse()->setHttpResponseCode(200);
}

public function putAction()
{
  $this->getResponse()->setBody('resource updated');
  $this->getResponse()->setHttpResponseCode(200);
}

public function deleteAction()
{
  $this->getResponse()->setBody('resource deleted');
  $this->getResponse()->setHttpResponseCode(200);
}

We test this by using CURL in the command line:

$ curl -v "http://localhost/v1/foo"
$ curl -v -X GET "http://localhost/v1/foo/id"
$ curl -v -X POST "http://localhost/v1/foo"
$ curl -v -X PUT "http://localhost/v1/foo/id" -d ""
$ curl -v -X DELETE "http://localhost/v1/foo/id"

you’ll notice that I’ve added /id to GET, PUT, DELETE. This is by design of course, remember the REST route you setup earlier? well this is where it comes to play! now that you are creating Resources not controllers, there are no need for any other actions other than to CREATE, READ, UPDATE & DELETE individual resources, otherwise known as CRUD and naturally: READ, UPDATE & DELETE require a resource identifier so Zend_Rest_Route takes care of mapping the URLs to match. here’s the breakdown of how the HTTP methods map to CRUD functions:

  • POST = CREATE
  • GET = READ
  • PUT = UPDATE
  • DELETE = that’s right, you guessed it! DELETE!

That leaves one: The index action handles index/list requests; it should respond with a list of the requested resources.

Now lets modify our logic to give a proper REST response:

<?php
public function indexAction()
{
  $this->getResponse()->setBody('List of Resources');
  $this->getResponse()->setHttpResponseCode(200);
}

public function getAction()
{
  $this->getResponse()->setBody(sprintf('Resource #%s', $this->_getParam('id')));
  $this->getResponse()->setHttpResponseCode(200);
}

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

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

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

Now lets test again:

$ curl -v "http://localhost/v1/foo"
$ curl -v -X GET "http://localhost/v1/foo/1"
$ curl -v -X POST "http://localhost/v1/foo"
$ curl -v -X PUT "http://localhost/v1/foo/1" -d ""
$ curl -v -X DELETE "http://localhost/v1/foo/1"

There, nice and clean RESTful response!

Now lets add some more HTTP Mehods beyond the basic GET, POST, PUT, DELETE. Two very popular HTTP methods are: HEAD and OPTIONS.

The OPTIONS method represents a request for information about the communication options available on the request/response chain identified by the Request-URI. This method allows the client to determine the options and/or requirements associated with a resource, or the capabilities of a server, without implying a resource action or initiating a resource retrieval.

The HEAD method is identical to GET except that the server MUST NOT return a message-body in the response. The metainformation contained in the HTTP headers in response to a HEAD request SHOULD be identical to the information sent in response to a GET request. This method can be used for obtaining metainformation about the entity implied by the request without transferring the entity-body itself. This method is often used for testing hypertext links for validity, accessibility, and recent modification (cache).

Zend_Rest_Controller does not force any methods other than ones we’ve already used, and since HEAD and OPTIONS in my example behave the same across all controllers, lets create a new Class REST_Controller and implement the optionsAction and headAction there.

First step is to add the custom REST_ namespace to our application.ini:

autoloaderNamespaces.rest = "REST_"

the REST_Controller class should look something like this:

<?php
abstract class REST_Controller extends Zend_Rest_Controller
{
  public function headAction()
  {
    // you should add your own logic here to check for cache headers from the request
    $this->getResponse()->setBody(null);
  }

  public function optionsAction()
  {
    $this->getResponse()->setBody(null);
    $this->getResponse()->setHeader('Allow', 'OPTIONS, HEAD, INDEX, GET, POST, PUT, DELETE');
  }
}

now lets make sure our FooController extends the newly created class:

<?php
class FooController extends REST_Controller

Now lets test the new actions:

$ curl -v -X HEAD "http://localhost/v1/foo"
$ curl -v -X OPTIONS "http://localhost/v1/foo/1"

Next, let’s look into context switching, which will allow us to server different response formats based on the Accept HTTP header

I’ve seen many ways 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

<?php
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('&lt;?xml version="1.0"?>', sprintf('&lt;?xml version="1.0"?>&lt;?xml-stylesheet type="text/xsl" href="%s"?>', $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 earlier) 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:

<?php
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:

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

<?php
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 earlier, lets first get rid of the init method we created and modify the action methods to something like this:

<?php
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 "http://localhost/v1/foo/1" -d ""
$ 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 "http://localhost/v1/foo/1" -d ""
$ 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:

<?php
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 [Friday, 04 March 2011]: using application.ini to setup the REST route, added OPTIONS, HEAD actions

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

Blog

Ahmad Nassri Twitter

Fractional CTO, Co-Founder of Cor, Developer Accelerator, Startup Advisor, Entrepreneur, Founder of REFACTOR Community. Previously: npm, TELUS, Kong, CBC/Radio-Canada, BlackBerry


Related Posts

The Modern Application Model

Most of today’s software development is occurring within the Application Layer as defined by the OSI Model. However that definition is a bit dated and does not exactly reflect today’s technology. We need a new model to visualize what we, Application Software Developers, are building.

The Modern Application Model

The New Normal — Scaling the challenges of a Modern CTO

Whether you’re a Founding CTO or an Enterprise CTO, you cannot go at it alone. You have to hire a team around you to help delegate and distribute a modern CTO’s responsibilities and overlapping technical domains, such as CIO, CDO, CMO, etc.

The New Normal — Scaling the challenges of a Modern CTO

Challenges of a Modern CTO

A CTO needs to be able to operate and be experienced in many areas beyond just the tactical. To be successful, they require Technical & People Leadership experience.

Challenges of a Modern CTO