LogoPhly, boy, phly
the weblog and site of Matthew Weier O'Phinney

Thursday, March 4. 2010

Responding to Different Content Types in RESTful ZF Apps

In previous articles, I've explored building service endpoints and RESTful services with Zend Framework. With RPC-style services, you get to cheat: the protocol dictates the content type (XML-RPC uses XML, JSON-RPC uses JSON, SOAP uses XML, etc.). With REST, however, you have to make choices: what serialization format will you support?

Why not support multiple formats?

There's no reason you can't re-use your RESTful web service to support multiple formats. Zend Framework and PHP have plenty of tools to assist you in responding to different format requests, so don't limit yourself. With a small amount of work, you can make your controllers format agnostic, and ensure that you respond appropriately to different requests.

Content-Type Detection

The first problem to solve is going to be how to retrieve passed parameters. When using XML or JSON as your serialization format, you aren't getting your standard POST variables -- you're getting a raw post instead, and you'll need to deserialize the payload. In fact, if you're getting a PUT request, you also have some work to do, as PHP doesn't do anything with PUT requests.

I do this via an action helper. The basic algorithm is:

  • Do we have a raw body in the request? If not, nothing more need be done.
  • Determine the Content-Type passed in the request headers, and decode appropriately:
    • If it was JSON, pass the raw request body to json_decode or Zend_Json::decode.
    • If it was XML, I pass the raw request body to the Zend_Config_XML constructor, and then serialize to an arrya using the toArray() method. Yes, it's a hack, but it's effective.
    • Otherwise, I assume I've got a regular PUT-style request, and I pass the data to parse_str().

I keep the values within the action helper, and then retrieve them on demand within my action controller. The helper looks like the following:


class Scrummer_Controller_Helper_Params
    extends Zend_Controller_Action_Helper_Abstract
{
    /**
     * @var array Parameters detected in raw content body
     */

    protected $_bodyParams = array();

    /**
     * Do detection of content type, and retrieve parameters from raw body if
     * present
     *
     * @return void
     */

    public function init()
    {
        $request     = $this->getRequest();
        $contentType = $request->getHeader('Content-Type');
        $rawBody     = $request->getRawBody();
        if (!$rawBody) {
            return;
        }
        switch (true) {
            case (strstr($contentType, 'application/json')):
                $this->setBodyParams(Zend_Json::decode($rawBody));
                break;
            case (strstr($contentType, 'application/xml')):
                $config = new Zend_Config_Xml($rawBody);
                $this->setBodyParams($config->toArray());
                break;
            default:
                if ($request->isPut()) {
                    parse_str($rawBody, $params);
                    $this->setBodyParams($params);
                }
                break;
        }
    }

    /**
     * Set body params
     *
     * @param  array $params
     * @return Scrummer_Controller_Action
     */

    public function setBodyParams(array $params)
    {
        $this->_bodyParams = $params;
        return $this;
    }

    /**
     * Retrieve body parameters
     *
     * @return array
     */

    public function getBodyParams()
    {
        return $this->_bodyParams;
    }

    /**
     * Get body parameter
     *
     * @param  string $name
     * @return mixed
     */

    public function getBodyParam($name)
    {
        if ($this->hasBodyParam($name)) {
            return $this->_bodyParams[$name];
        }
        return null;
    }

    /**
     * Is the given body parameter set?
     *
     * @param  string $name
     * @return bool
     */

    public function hasBodyParam($name)
    {
        if (isset($this->_bodyParams[$name])) {
            return true;
        }
        return false;
    }

    /**
     * Do we have any body parameters?
     *
     * @return bool
     */

    public function hasBodyParams()
    {
        if (!empty($this->_bodyParams)) {
            return true;
        }
        return false;
    }

    /**
     * Get submit parameters
     *
     * @return array
     */

    public function getSubmitParams()
    {
        if ($this->hasBodyParams()) {
            return $this->getBodyParams();
        }
        return $this->getRequest()->getPost();
    }

    public function direct()
    {
        return $this->getSubmitParams();
    }
}
 

This helper is intended to be run on each request, so I register it in my bootstrap:


class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
    // ...
    protected function _initActionHelpers()
    {
        // ...
        $params = new Scrummer_Controller_Helper_Params();
        Zend_Controller_Action_HelperBroker::addHelper($params);
        // ...
    }
    // ...
}
 

Within your action controller, all you need to do is call the helper:


$data = $this->params();
 

In a RESTful controller, you'll only need to use this with your postAction and putAction. The beauty is that your controller can remain ignorant of the Content-Type -- you write the same logic to retrieve your parameters regardless.

Responding to the client: Context Switching

So, the first half of the problem is taken care of: how to handle the request. The second half is responding appropriately.

Zend Framework has some built in tooling to help with this. The ContextSwitch and AjaxContext action helpers look for a particular parameter -- "format" by default -- and, if detected, will render an alternate view script named after the context. As an example, if an "XML" context is detected, it will render "<controller>/<action>.xml.phtml" -- note the ".xml" segment of the script name.

Both helpers work in the same basic way (the latter, AjaxContext, will only activate if the request is determined to originate from an XMLHttpRequest): you define which actions in the controller are context sensitive, and then if the context is detected, a new view script will be used.

So, the first trick is ensuring that the context is passed. As mentioned before, the helpers look for a "format" parameter in the request object. You can pass this using a query parameter -- "?format=xml" -- but I find that ugly. There's an HTTP header defined for this purpose already: "Accept".

Detecting the header and injecting the context into the request is absurdly simple, and can be done in a dispatchLoopStartup plugin:


class Scrummer_Controller_Plugin_AcceptHandler
    extends Zend_Controller_Plugin_Abstract
{
    public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
    {
        if (!$request instanceof Zend_Controller_Request_Http) {
            return;
        }

        $header = $request->getHeader('Accept');
        switch (true) {
            case (strstr($header, 'application/json')):
                $request->setParam('format', 'json');
                break;
            case (strstr($header, 'application/xml')
                  && (!strstr($header, 'html'))):
                $request->setParam('format', 'xml');
                break;
            default:
                break;
        }
    }
}
 

The above can be registered in your application configuration:


resources.frontController.plugins[] = "Scrummer_Controller_Plugin_AcceptHandler"
 

I like my RESTful controllers to automatically expose their methods as context-aware. To make this happen, I defined a marker interface, "Scrummer_Rest_Controller", and created an action helper that checks if the current controller implements it; if it does, I then automatically add contexts for the RESTful actions.


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

    public function preDispatch()
    {
        $controller = $this->getActionController();
        if (!$controller instanceof Scrummer_Rest_Controller) {
            return;
        }

        $this->_initContexts();

        // Set a Vary response header based on the Accept header
        $this->getResponse()->setHeader('Vary', 'Accept');
    }

    protected function _initContexts()
    {
        $cs = $this->getActionController()->contextSwitch;
        $cs->setAutoJsonSerialization(false);
        foreach ($this->_contexts as $context) {
            foreach (array('index', 'post', 'get', 'put', 'delete') as $action) {
                $cs->addActionContext($action, $context);
            }
        }
        $cs->initContext();
    }
}
 

Register this via the bootstrap as well:


class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
    // ...
    protected function _initActionHelpers()
    {
        // ...
        $params = new Scrummer_Controller_Helper_Params();
        Zend_Controller_Action_HelperBroker::addHelper($params);

        $contexts = new Scrummer_Controller_Helper_RestContexts();
        Zend_Controller_Action_HelperBroker::addHelper($contexts);
        // ...
    }
    // ...
}
 

There are two things to note about this helper. First, you'll see that I specify a "Vary" header. This is to ensure that if the client chooses to cache responses, it will cache separate responses based on the value sent in the "Accept" header.

Second, note that I turn off automatic JSON serialization in the ContextSwitch helper. I do this so that I can keep my controller context agnostic; this will require additional view scripts, but the ability to keep my controller logic simple will be worth it. More on that in a moment.

We now have the infrastructure in place to respond to different contexts based on the "Accept" header, and can retrieve parameters appropriately based on the "Content-Type" provided us. Now comes the actual response.

Responding to the client: Views

Recall that ContextSwitch will attach an additional prefix to the specified view script -- "<controller>/<action>.phtml" will become "<controller>/<action>.xml.phtml" or "<controller>/<action>.json.phtml". Basically, for each context we will respond to, we have an additional view script per action.


views/
|-- scripts/
|   `-- foo/
|      |-- delete.phtml
|      |-- delete.json.phtml
|      |-- delete.xml.phtml
|      |-- get.phtml
|      |-- get.json.phtml
|      |-- get.xml.phtml
|      |-- index.phtml
|      |-- index.json.phtml
|      |-- index.xml.phtml
|      |-- post.phtml
|      |-- post.json.phtml
|      |-- post.xml.phtml
|      |-- put.phtml
|      |-- put.json.phtml
|      `-- put.xml.phtml

This may seem like overkill, but consider the following representative method from my controller:


    public function postAction()
    {
        $data    = $this->params();
        $service = $this->getService();
        $result  = $service->add($data); 
        if (!$result) {
            $this->view->form = $service->getBacklogForm();
            return;
        }

        $this->view->success = true;
        $this->view->backlog = $result;
    }
 

You don't see anything in there about headers, redirects, or XHR requests. Just slinging data to services and views. Real simple.

The view scripts then take care of the appropriate display logic. Let's look at two view scripts for the above action, one for plain old HTML, the other for a JSON response:


<?php // backlog/post.phtml ?>
<?php
if ($this->success):
    $this->response->setRedirect($this->url(array(
        'controller' => 'backlog',
        'id'         => $this->backlog->id,
    ), 'rest', true));
else: ?>
<h2>Create new backlog</h2>
<?php
    $this->form->setAction($this->url())
               ->setMethod('post');
    echo $this->form;
endif ?>

<?php // backlog/post.json.phtml ?>
<?php
if ($this->success) {
    $url = $this->url(array(
        'controller' => 'backlog',
        'id'         => $this->backlog->id,
    ), 'rest', true);
    $this->response->setHeader('Location', $url)
                   ->setHttpResponseCode(201);
    echo $this->json($this->backlog->toArray());
    return;
}

$form = $this->form;
$form->setAction($this->url())
     ->setMethod('post');
echo $this->jsonFormErrors($form);
 

A few things to note: I inject my response object into the view. I feel HTTP headers are part of the view, and thus I deal with them there. That also serves the purpose of keeping my controllers thin and agnostic. Additionally, you'll note that I use different response codes for HTML versus JSON -- this allows my JSON-REST support to be RESTful, by returning a 201 status code indicating the resource was created; I also return a JSON representation of the object. Finally, you'll note that I have a special view helper for creating JSON representations of validation errors.

Closing points

This post is far from exhaustive, and I expect it will likely raise at least as many questions as it tries to answer.

My main point in this article is to get you, the reader and developer, thinking creatively about how to expose RESTful web services. Hopefully, you're taking the following away:

  1. Architect in such a way as to minimize the code in your controllers; keep that code as agnostic as possible in regards to where input comes from and what type of response is required.
  2. Use front controller plugins and action helpers to create scaffolding for your services; these are incredibly flexible and re-usable, and help make point 1 that much easier.
  3. Offload as much as possible to your views. This will allow you to isolate logic specific to given formats.

What are you waiting for? Don't you have an API to expose?

Posted by Matthew Weier O'Phinney in PHP at 15:28 | Comments (22) | Trackback (1)
Defined tags for this entry: php, rest, zend framework
Related entries by tags:
Autoloading Benchmarks
Applying FilterIterator to Directory Iteration
Running mod_php and FastCGI side-by-side
Creating Zend_Tool Providers
State of Zend Framework 2.0

Trackbacks
Trackback specific URI for this entry

Responding to Different Content Types in RESTful ZF Apps - phly, boy, phly
In previous articles, I've explored building service endpoints and RESTful services with Zend Framework. With RPC-style services, you get to cheat: the protocol dictates the content type (XML-RPC uses XML, JSON-RPC uses JSON, SOAP uses XML, etc.). With REST, however, you have to make choices: what serialization format will you support?
Weblog: abcphp.com
Tracked: Mar 05, 04:45

Comments
Display comments as (Linear | Threaded)

I really think you should only run parse_str() on application/x-www-form-urlencoded data though... treat anything else as an uploaded file or something.

Feel free to use http://trac.agavi.org/browser/tags/1.0.2/src/request/AgaviWebRequest.class.php#L436 as a reference ;-)
#1 David Zülke (Link) on 2010-03-04 16:01 (Reply)
Good point about the application/x-www-form-urlencoded content-type... my bad! Easy enough to detect, though.
#1.1 Matthew Weier O'Phinney (Link) on 2010-03-04 16:47 (Reply)
check out the mimeparse implementation for PHP here: http://code.google.com/p/mimeparse/
#2 Mike Amundsen (Link) on 2010-03-04 16:32 (Reply)
I think the context based views are a bad idea. If I have 100 controller actions and I want to add another content-type I have to create another 100 files. That just isn't maintainable.

I use an action helper that detects the content-type and formats the output accordingly. This means a small modification to the action helper for the new content-type and QA is testing.
#3 Herman Radtke (Link) on 2010-03-04 16:40 (Reply)
Depends on how you want to maintain the app.

Using the technique I outline here, I can very easily debug individual responses -- if there's a bug in only my XML responses, I know exactly where to look.

Additionally, I'm not going to have 100 controller actions -- that's just bad architecture (rule of thumb: if you have more than 7 actions in a controller, you probably need to refactor). With a RESTful app, I'll have typically 5 actions total (and potentially 2 more for HTML views for create/edit forms) -- this is definitely maintainable, and there isn't such a proliferation of views as to make the directory unwieldy.

Certainly you can go the action helper route; I just like to keep my areas of responsibility discrete -- and as such, I push the display logic (headers and response) to the views. YMMV, and the architecture will vary based on what your application needs.
#3.1 Matthew Weier O'Phinney (Link) on 2010-03-04 16:46 (Reply)
I never meant to imply a single controller has 100 actions. For all controllers, there are 100 actions total. If there are 100 total actions, I have 100 * content-types. Thus, if I have json, xml and yaml content-types, I have 300 view scripts to maintain.
#3.1.1 Herman Radtke (Link) on 2010-03-04 19:09 (Reply)
There's always a trade-off between number of files and having discrete responsibilities per file. Fewer view scripts often leads to more logic in the controllers (though, as you note, this can often be addressed via action helpers). Discrete view scripts allows me to address differing display/response needs per action and per context -- but with more files to keep track of. However, the framework provides a structure and a hierarchy for locating these files -- which should assist you when maintaining the application.

There are many ways to address web services in ZF -- I'm simply showing one way I've experimented with that was highly successful.
#3.1.1.1 Matthew Weier O'Phinney (Link) on 2010-03-05 07:59 (Reply)
I take the best of both worlds. I wrote a helper class that looks for a specific view template file in case I want to make a specific json or xml representation for a specific resource. If it doesn't find a template file, it falls into a generic json or xml serialization template.
#3.1.1.1.1 luke (Link) on 2010-03-08 15:32 (Reply)
Nice article.

I think the indicating the Content-Type in the URI itself is more elegant. For example. example.com/customer/1.json
#4 Sudheer (Link) on 2010-03-05 01:17 (Reply)
This is actually possible with route chaining in ZF, but I like the simplicity of having fewer routes I need to track.
#4.1 Matthew Weier O'Phinney (Link) on 2010-03-05 07:54 (Reply)
I'd love to see a post about how that is done.

My first attempt is not really cutting it, and i'm actually pretty confused about how a parameter would only be passed to the chained route, instead of both. (i would've thought parameters would somehow have been merged, but apparently they have not, or i'm doing it wrong).

https://gist.github.com/325092/0b4919bd139cfb4844380752ce414098e39b36a2

I'd also love how you would integrate it with the request, so a parameter 'extension', is always available to the current controller.

I guess some sort of plugin would be needed for that. But as long as i can't even get the correct route to assemble, i'm pretty stuck already.
#4.1.1 David Reuss (Link) on 2010-03-08 07:24 (Reply)
Regarding Scrummer_Controller_Helper_Params: Is there a reason why you just don't populate the params to the request object and use $this->_getAllParams() or similar in your controllers?
#5 Jan on 2010-03-08 04:34 (Reply)
Yes, actually -- it's so that I know where they originated. If they come from the helper, they came from a raw post (or simply a POST); otherwise, they could have come from the query string, which almost certainly is not what we want.
#5.1 Matthew Weier O'Phinney (Link) on 2010-03-08 07:01 (Reply)
This could be prevented by using setPost() in the helper and getPost() in controllers.
#5.1.1 Jan on 2010-03-08 07:15 (Reply)
Unfortunately, we don't have a setPost() method in the standard request object -- though it's planned for 2.0.
#5.1.1.1 Matthew Weier O'Phinney (Link) on 2010-03-08 07:25 (Reply)
Speaking of 2.0, I think we should make the Zend_Rest_Route behavior the default routing behavior in 2.0 ... or at least put it in the default route chain so you can do this RESTful stuff out-of-the-box without any special controller configuration.

But I haven't seen a good reference implementation of the new Controller 2.0 system. :-(
#5.1.1.1.1 luke (Link) on 2010-03-08 15:37 (Reply)
re: making Zend_Rest_Route the default behavior, I'm not completely sold on that idea; not all applications use CRUD or benefit from RESTful architectures. We _will_ do some refactoring, however, to make it easier to configure (well, to make it possible to configure!) via Zend_Application. More than that, well, we'll have to see what everyone things.

re: ZF MVC 2.0 proof of concept: http://github.com/weierophinney/phly/tree/mvcfsm/Phly_Mvc/ -- this has been available for literally months. :-)
#5.1.1.1.1.1 Matthew Weier O'Phinney (Link) on 2010-03-08 15:58 (Reply)
wha ... ?!?!?! surely ALL *web* applications benefit from RESTful architecture. ;-)

seriously, though - what's the downside to enabling RESTful MVC by default?
#5.1.1.1.1.1.1 luke (Link) on 2010-03-10 00:54 (Reply)
Zend_Controller_Request_Http has a setPost() method, or did i misunderstand you?
#5.1.1.1.2 Jan on 2010-03-10 07:53 (Reply)
nm -- I forgot we added that method sometime back. It did not prior to 1.6 (which is when we added Zend_Test_PHPUnit).
#5.1.1.1.2.1 Matthew Weier O'Phinney (Link) on 2010-03-10 09:28 (Reply)
Hi Matthew, excellent post! It has been a source of inspiration and reference in the architecture for my latest project. I'm exposing a RESTful api as a module named "api" and I have that module attached to Zend_Rest_Route.

My idea was to use the api module to house the REST controllers and JSON views, and the default module to house the HTML client that uses the API.

I agree with your approach of keeping the controllers thin by moving display logic to your views, but I'm uncomfortable with adding HTML to the API views. It seems more appropriate to keep the HTML views specific to the client, and therefore in the default module.

Am I worrying about that too much, or do you see any benefit in separating the HTML from the API? If you do, how would you suggest I go about that?
#6 @talentedmrjones (Link) on 2010-05-12 15:53 (Reply)
I think you'll do yourself a favor if you include the HTML views in the api module. The HTML views are really ad-hoc REST clients you send to the user's browser. If you think of it like this it also helps force you to use standard and clean HTML - the kind of HTML that works well with RESTful interfaces!
#6.1 luke (Link) on 2010-05-13 17:19 (Reply)

Add Comment

Standard emoticons like :-) and ;-) are converted to images.
E-Mail addresses will not be displayed and will only be used for E-Mail notifications

To prevent automated Bots from commentspamming, please enter the string you see in the image below in the appropriate input box. Your comment will only be submitted if the strings match. Please ensure that your browser supports and accepts cookies, or your comment cannot be verified correctly.
CAPTCHA

 
 
  • Home
  • Resume
  • Blog
  • Phly PEAR Channel
  • Twitter
  • Contact Me
  • About this site

ZCE

Zend Education Advisory Board Member

Add to Technorati Favorites

Calendar

Back September '10
Mon Tue Wed Thu Fri Sat Sun
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30      

Quicksearch

Links

  • PHLY - PHp LibrarY
  • Planet PHP
  • Zend Framework, where I'm project lead
  • Sebastian Bergmann
  • Cal Evans
  • Shahar Evron
  • Paul M. Jones
  • Bill Karwin
  • Mike Naberezny
  • Fabien Potencier
  • Ben Ramsey
  • Derick Rethans
  • Ralph Schindler
  • Marco Tabini

Archives

September 2010
August 2010
July 2010
Recent...
Older...

Categories

XML Linux
XML Personal
XML Aikido
XML Family
XML Programming
XML Dojo
XML Perl
XML PHP

All categories

Syndicate This Blog

XML RSS 0.91 feed
XML RSS 1.0 feed
XML RSS 2.0 feed
ATOM/XML ATOM 0.3 feed
ATOM/XML ATOM 1.0 feed
XML RSS 2.0 Comments

Show tagged entries

xml apache
xml best practices
xml books
xml conferences
xml cw09
xml decorators
xml dojo
xml dpc08
xml file_fortune
xml git
xml linux
xml mvc
xml oop
xml pear
xml perl
xml personal
xml php
xml phpworks08
xml programming
xml rest
xml ubuntu
xml vim
xml webinar
xml zendcon
xml zendcon08
xml zendcon09
xml zend framework
© 2004 - present, Matthew Weier O'Phinney
matthew-web <at> weierophinney.net