Thursday, March 4. 2010Responding to Different Content Types in RESTful ZF AppsIn 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 DetectionThe 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:
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
Responding to the client: Context SwitchingSo, 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 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: ViewsRecall 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 pointsThis 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:
What are you waiting for? Don't you have an API to expose? 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 Good point about the application/x-www-form-urlencoded content-type... my bad! Easy enough to detect, though.
check out the mimeparse implementation for PHP here: http://code.google.com/p/mimeparse/
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. 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. 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.
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. 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.
Nice article.
I think the indicating the Content-Type in the URI itself is more elegant. For example. example.com/customer/1.json This is actually possible with route chaining in ZF, but I like the simplicity of having fewer routes I need to track.
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. 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?
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.
This could be prevented by using setPost() in the helper and getPost() in controllers.
Unfortunately, we don't have a setPost() method in the standard request object -- though it's planned for 2.0.
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. 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. wha ... ?!?!?! surely ALL *web* applications benefit from RESTful architecture.
seriously, though - what's the downside to enabling RESTful MVC by default? 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).
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? 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!
Add Comment
|
Calendar
QuicksearchLinksArchivesCategoriesSyndicate This BlogShow tagged entries |
|||||||||||||||||||||||||||||||||||||||||||||||||





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?
Tracked: Mar 05, 04:45