Zend Framework: Zend_Controller_Router_Route_Rest Component Proposal
| Proposed Component Name | Zend_Controller_Router_Route_Rest |
|---|---|
| Developer Notes | http://framework.zend.com/wiki/display/ZFDEV/Zend_Controller_Router_Route_Rest |
| Proposers | luke.crouch@gmail.com Matthew Weier O'Phinney - Zend Liaison |
| Revision | 1.0 - 23 Feb 2008: Started. 1.0 - 15 May 2008: changed from Route class to full-on Router class. 1.0 - 15 May 2008: changed back to Route class 2.0 - 01 Jan 2009: updated Route class for ZF 1.7 compatibility 2.0 - 30 Jun 2009: development started under ZF-7109 (wiki revision: 45) |
Table of Contents
1. Overview
Zend_Rest_Route is a route that uses URI to route the module & controller and HTTP request method to route to the action of a Zend_Controller_Action_Rest controller.
Zend_Rest_Controller is an abstract action controller designating the action methods to implement for use with Zend_Controller_Router_Route_Rest.
Zend_Rest_PutHandler is a controller plugin to improve PUT body handling - enables controllers to access x-www-form-urlencoded PUT bodies via getParams().
2. References
3. Component Requirements, Constraints, and Acceptance Criteria
- This component will operate as a Route class of the Rewrite Router.
- This component will route to Modules and Controllers based on URI tokens.
- This component will route to specific Actions based on HTTP request types.
- This component will support overloaded-POST-based routing for PUT and DELETE request types.
- This component will route to some special Actions based on URI tokens.
- This component will include an abstract Zend_Controller_Action class for implementation reference.
- This component will allow for designating specific REST Modules and/or Controllers
4. Dependencies on Other Framework Components
- Zend_Controller
5. Theory of Operation
A RESTful routing paradigm for Zend_Controller should behave as follows:
| Request method | URI | Module_Controller::action |
|---|---|---|
| GET | /product/ratings/ | Product_RatingsController::indexAction() |
| GET | /product/ratings/id/:id | Product_RatingsController::getAction() |
| GET | /product/ratings/new | Product_RatingsController::newAction() |
| GET | /product/ratings/id/:id/edit | Product_RatingsController::editAction() |
| POST | /product/ratings | Product_RatingsController::postAction() |
| PUT | /product/ratings/id/:id | Product_RatingsController::putAction() |
| DELETE | /product/ratings/id/:id | Product_RatingsController::deleteAction() |
| POST | /product/ratings/id/:id _method=PUT |
Product_RatingsController::putAction() |
| POST | /product/ratings/id/:id _method=DELETE |
Product_RatingsController::deleteAction() |
6. Milestones / Tasks
- Milestone 1: Working prototype checked into the incubator supporting use cases #1
- Milestone 2: Unit tests exist, work, and are checked into SVN.
- Milestone 3: Initial documentation exists.
7. Class Index
- Zend_Rest_Route
- Zend_Rest_Controller
- Zend_Rest_PutHandler
8. Use Cases
9. Class Skeletons
yeah, that's the best design, but RESTful routing needs the full request object and it's not currently available to Route classes, as you mentioned. I have this same code in a Route class as well, but it couldn't access the request object, so it's impossible to get the request method in a Route.
I'll just use this Router class in my project for now, but I will change this back into a Route and I think it will just have to wait until the request object is passed into Route instances. please keep me posted on how and when that advances. thanks.
Luke, pay attention to this issue:
http://framework.zend.com/issues/browse/ZF-777
As Matthew mentioned it in the last comment, I have a patch that keeps backwards compatibility intact. Basically this means you will just have to implement an additional interface (with setRequest method). And that's it - the request will be available for you on match().
As I have mentioned before, you now have a Request object passed as a first parameter to match() method on version 2 routes. Just extend abstract class to get this behavior by default.
What is the status of this class? What is the preferred method of implementing a RESTful Web Service?
I've updated the use-case and class code of this proposal to match Route behavior of ZF 1.7.2. What are next steps for moving it into incubator?
I'm not sure I agree with the URI/request method mappings you outlined under "Theory of Operation". Traditional REST would do the following:
| Request Method | URI | Module_Controller::action |
|---|---|---|
| GET | /product/ratings | Product_Ratings::listAction() |
| GET | /product/ratings/:id | Product_Ratings::getAction() |
| POST | /product/ratings | Product_Ratings::newAction() |
| PUT | /product/ratings/:id | Product_Ratings::updateAction() |
| DELETE | /product/ratings/:id | Product_Ratings::deleteAction() |
| POST | /product/ratings/:id | Product_Ratings::updateAction() |
| POST | /product/ratings/:id?delete | Product_Ratings::deleteAction() |
The differences are around how get/new/update/delete work. When you POST with an identifier, it acts as an update; without an identifier, it attempts to create a new item.
Otherwise, the proposed solution makes sense.
One other item: It seems like this should include an extension to Zend_Controller_Action – perhaps Zend_Controller_Action_Rest and/or Zend_Controller_Action_Rest_Interface? That would simplify the creation of REST controllers, and ensure that they would work with the route.
Hmm ... I was going by behavior described in RESTful Web Services by Leonard Richardson & Sam Ruby. The spec seems to support it:
"The fundamental difference between the POST and PUT requests is reflected in the different meaning of the Request-URI. The URI in a POST request identifies the resource that will handle the enclosed entity. That resource might be a data-accepting process, a gateway to some other protocol, or a separate entity that accepts annotations. In contrast, the URI in a PUT request identifies the entity enclosed with the request"
http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6
I don't mind changing the names of the action methods, but the reason I have a separate newAction vs. a postAction is to allow the route to choose a controller action that serves a representation that will instruct the user agent how to compose the POST request that creates a new resource - e.g., an HTML form so /product/ratings/new returns an HTML form that POSTs to /product/ratings
The reason I didn't include query params in the URI for over-loaded POSTs is because the request method for over-loaded POST is still POST which means the params will be in the request body, not in the URI like that. It's effectively the same but I think the POST body params are more indicative of what's really happening.
I actually have an abstract Zend_Controller_Action_Rest class as well, but I'm not as sure of it, and I thought I'd submit them as separate proposals so users could use them separately. I wouldn't mind writing a Zend_Controller_Action_Rest_Interface to include with this proposal so we have an idea what the controller implementation should be.
So, if I change:
indexAction => listAction
putAction => updateAction
Add Zend_Controller_Action_Rest_Interface
Think that will be good for this proposal to move forward?
I was going based on other literature I've read, not the spec. I'm fine with identifying the identity as part of the payload – but make sure it's well documented what the various schemes are. Bonus points if you can make it configurable.
That said, I really don't like using "new" to indicate a new record and "edit" to grab one to update; that's part of what REST tries to get around. If there is no entity identifier, it's a new record; POST updates the record.
Regarding the query parameters, it was unclear from your table that the _method=(PUT|DELETE) was a part of the payload; I thought it was part of the URI. Putting that in the payload is fine by me.
One last note on URIs: while the default ZF route uses key/value pairs, I really don't think it's necessary to do so with a REST route – and in fact makes the URLs look less clean. Let's go with traditional REST routes, and drop the id/:id in favor of just :id.
As for the action names, I think either index or list would work well for the collection of entities; maybe list could be an alias for index. I also do prefer using updateAction over putAction, as it identifies what's happening semantically.
Finally, regarding the Zend_Controller_Action_Rest class, I think abstract is fine (no need for an interface), and that it should be part of the proposal; it makes for a clean, full solution that users can develop on immediately.
Overall, very nice!
Re: edit and new ...
I agree that performing stateful operations via URI tokens is RESTless, but in this route's usage, 'edit' and 'new' denote resources/nouns - not operations:
'new' = the form (a resource) for creating a new resource
'edit' = the form (a resource) for editing an existing resource
This is basically to encourage "hypertext as the engine of application state." e.g., GET /resource/new delivers a representation (HTML page) that contains hypertext (<form method="post" action="/resource/">) that drives to another state of the application (creating a new resource). /resource/new DOES NOT perform any stateful operations, and does not respond to any requests other than GET.
With all that said, think it's fine to leave the 'new' and 'edit' actions in there as long as we make it explicit what their purpose is?
I'll work on dropping the key-value pairs for the identifier, but in using this route myself, I've found key-value params pretty useful. e.g., makes it easy to also support flexible identifiers like /name/:name or /modified_since/:timestamp.
I can also add a Zend_Controller_Action_Rest abstract class.
Aha - I was thinking of this more in terms of an XML/JSON REST service, not in terms of using it for HTML as well. With that in mind, I think it makes sense.
As for the /:id comment – you could think of it like Zend_Controller_Router_Route, where a /* means that anything following is key/value pairs. That would allow for urls like /wiki/SomePage/modified_since/2008-12-02 – which gives you the :id (SomePage) as well as extra key/value resources.
| Zend Acceptance This proposal is accepted for immediate development in the standard incubator. However, there are several changes/issues we would like to see addressed:
|
We can't put these classes into Zend_Rest. Frankly and simply, Zend_Rest has nothing to do with REST. It's just another XML-RPC implementation that ignores the uniform HTTP interface and other elements of RESTful design and architecture. I've made mention in the mailing lists before that it needs to be re-named and/or merged with Zend_XmlRpc.
REST is a different architectural style. These classes are Zend_Controller classes; they enhance functionality of Zend_Controller to enable REST architectural style and technical design. I'd rather keep them in the Zend_Controller package.
The request matching will never be format-specific, so there's no need for different sub-classes for different formats. In fact, I've used this exact route class really well with Zend_Controller_Action_Helper_ContextSwitch for building REST API that supports HTML, JSON, and XML simultaneously using only separate template files.
The injection of the dispatcher and the request into the constructor are inherited from Zend_Controller_Router_Route_Module. I'll alter this class to accept the front controller in the constructor, but that won't fix the problem in Module.
I'll remove the newAction and editAction from the Controller.
I'll also start work on designating specific module and controllers as RESTful via this route class.
Our plan is to deprecate Zend_Rest_Server, and potentially Zend_Rest_Client as well. We are well aware that they do not follow RESTful design, and this is one reason we are excited about your proposal, as it follows our desire to offer RESTful offerings. (The current Zend_Rest offerings were written well before we had a proposal process, and did not have sufficient review.)
The reason we would like these classes under Zend_Rest is two-fold. First, for developers looking for REST solutions in Zend Framework, they are unlikely to look in Zend_Controller; placing them under Zend_Rest makes finding them easier. Second, all other protocol and service specific classes in ZF have their own namespace: Zend_XmlRpc, Zend_Soap, Zend_Json, Zend_Dojo (form and view integration), etc – even if they have ties to other components. While your implementation is specific to the MVC, it allows us to group REST related components in a single location; documentation can then indicate how the various subcomponents are used and how they relate to other ZF components.
Thanks for pointing out the dispatcher/request injection in the module route; I'll create an issue to track that. Also, thank you for clarifying that the route is not format specific – this wasn't clear from the proposal, so it's good to know you've already taken care of that.
Okay, I feel better about that plan to deprecate the existing Zend_Rest classes. I agree it will be nice for users to find all the REST classes in a single place, and I think replacing the current Zend_Rest documentation with a guide on how to use these new RESTful classes will be a huge improvement.
Given that, should this proposal be re-named to Zend_Rest or something more like that? I should also mention that I also use a Controller plugin called 'RestHandler' for better support of PUT requests often used in RESTful HTML clients - i.e., to digest PUT request bodies of type 'x-www-form-urlencoded' and make the params available via $request->getParams(). Should I add this plugin class to the proposal as well?
No need to rename the proposal; the core of it is basically the same: a route class for RESTful requests.
Yes, do please include the RestHandler plugin; that way we can have a fairly complete REST offering for users. Please update the page to show the API of the plugin, as well as a sample use case.
Hi, Zend_Rest_Server will be deprecated. Does it mean that there will be some new Zend_Restful_Server/Client? If yes, will the server be consistent with the others soap/amf/xmlrpc server? (the server load a class/method and check the phpdoc)
No. Zend_Rest_Server currently follows that paradigm (introspection of class to determine method signatures), but that paradigm is not RESTful – it's simply another RPC-style service. This has caused confusion for many developers, and does not serve our users well. If you want to do RPC using XML... use XML-RPC.
Zend_Rest_Client will likely continue to live on. However, for most use cases when querying RESTful architectures, Zend_Http_Client is a better choice, as it gives more fine-grained control over the request headers and body, and in many cases is more intuitive when using RESTful architectures.
I assume if the route is configured for an entire module, all requests to the module should be routed RESTfully to all controllers in it?
Does this solution allows to submit identifier with Umlauts and Slashes?
Example Use Case:
It should be possible to update products with the following identifiers:
- ProductId: "Mü/120/abc"
- ProductId: "Aß/230/def"
In this Use Case the identifiers contains Slashes as well as Umlauts. Submitting identifiers with slashes (and different character sets) shouldn't lead to misinterpreted parameters.
Remi
I hadn't thought of this but will look at supporting it. I'm not sure how to support both the flexible key-value url param identifiers AND slashes in the identifiers, but I'll give it a try.
I've updated the proposal and code to meet the acceptance criteria. Couple of things discussed but not implemented ...
I didn't drop the key-value URI scheme because I have found it easier to implement a flexible 'index' action - i.e., one that accepts an assortment of filtering params like /new_since/:timestamp/changed_since/:timestamp - if each of the params retain names. As I consider this though, I could change the route to pass key-value URI params for the 'index' route only, and all other routes could operate from the /:module/:controller/:identifier scheme. How important is this feature?
I left the putAction as putAction, simply because the route code is getting a little complex already, and also because I think it helps to reinforce users' implementation of the HTTP uniform interface in their controllers. Again I could re-consider this if we think it's very important.
Also, I need a bit of coaching on my next steps. I think I have svn access, but can I simply check-out the standard incubator repos? How's the best way to set up a test project to use my Zend_Rest stuff from the incubator repos but use Zend_Controller from regular repos?
Could support for the X-HTTP-Method-Override header be included to supplement the _method parameter for clients that (for whatever reason) offer limited or no support for methods other than GET and POST?
Surely I'm a little lost, but I cannot find your proposal at the standard incubator repository. Where it is?
On the other hand, you said:
As I consider this though, I could change the route to pass key-value URI params for the 'index' route only, and all other routes could operate from the /:module/:controller/:identifier scheme. How important is this feature?
I think this is a very important feature because it will allow a better integration with dojox.data.JsonRestStore
ZF Home Page
Code Browser
Wiki Dashboard
Luke, I don't really see anything in there that couldn't be used in the route class instead of a router. While overriding the router you're basically removing support for chaining different types of routing. And you render the URL view helper useless. And that's all. That's what you get by doing it in the router instead of a route class.
If you leave automatic module detection out, you don't even need the dispatcher instance (and router in fact shouldn't be aware of the dispatcher). All you need is the request object which will be passed to route instances in a very near future.