ZF-8584: Add format to rest routes

Description

The goal is to be able to request a specific content type in a REST URL:

/controller.json or /controller/index.json /controller/22.json

This patch will remove the extension after the period and place it in params['format'].

Code assumes that actions don't use periods (i.e. index.xml mapping to indexXmlAction).

 
--- Route.php   2009-11-19 16:36:49.000000000 -0800
+++ Route2.php  2009-12-18 11:50:26.000000000 -0800
@@ -67,6 +67,12 @@ class Zend_Rest_Route extends Zend_Contr
     protected $_front;
 
     /**
+     * Array keys to use for format. Should be taken out of request.
+     * @var string
+     */
+    protected $_formatKey = 'format';
+   
+    /**
      * Constructor
      *
      * @param Zend_Controller_Front $front Front Controller object
@@ -129,10 +135,14 @@ class Zend_Rest_Route extends Zend_Contr
             // Determine Controller
             $controllerName = $this->_defaults[$this->_controllerKey];
             if (count($path) && !empty($path[0])) {
-                if ($this->_checkRestfulController($moduleName, $path[0])) {
-                    $controllerName = $path[0];
-                    $values[$this->_controllerKey] = array_shift($path);
+               preg_match('/([^\.]+)(\.(.+))?/', $path[0], $matches);
+                if ($this->_checkRestfulController($moduleName, $matches[1])) {
+                    $controllerName = $matches[1];
+                    $values[$this->_controllerKey] = $matches[1];
+                   if (!empty($matches[3]))
+                       $values[$this->_formatKey] = $matches[3];
                     $values[$this->_actionKey] = 'get';
+                   array_shift($path);
                 } else {
                     // If Controller in URI is not found to be a RESTful
                     // Controller, return false to fall back to other routes
@@ -145,13 +155,23 @@ class Zend_Rest_Route extends Zend_Contr
 
             // Check for leading "special get" URI's
             $specialGetTarget = false;
-            if ($pathElementCount && array_search($path[0], array('index', 'new')) > -1) {
-                $specialGetTarget = array_shift($path);
+           preg_match('/([^\.]+)(\.(.+))?/', $path[0], $matches);
+            if ($pathElementCount && array_search($matches[1], array('index', 'new')) > -1) {
+                $specialGetTarget = $matches[1];
+               if (!empty($matches[3]))
+                   $values[$this->_formatKey] = $matches[3];
+                array_shift($path);
             } elseif ($pathElementCount && $path[$pathElementCount-1] == 'edit') {
                 $specialGetTarget = 'edit';
-                $params['id'] = $path[$pathElementCount-2];
+                preg_match('/([^\.]+)(\.(.+))?/', $path[$pathElementCount-2], $matches);
+                $params['id'] = $matches[1];
+               if (!empty($matches[3]))
+                   $values[$this->_formatKey] = $matches[3];
             } elseif ($pathElementCount == 1) {
-                $params['id'] = array_shift($path);
+                preg_match('/([^\.]+)(\.(.+))?/', array_shift($path), $matches);
+                $params['id'] = $matches[1];
+               if (!empty($matches[3]))
+                   $values[$this->_formatKey] = $matches[3];
             } elseif ($pathElementCount == 0 || $pathElementCount > 1) {
                 $specialGetTarget = 'index';
             }
@@ -199,13 +219,14 @@ class Zend_Rest_Route extends Zend_Contr
         $this->_values = $values + $params;
 
         $result = $this->_values + $this->_defaults;
-        
+
         if ($partial && $result)
            $this->setMatchedPath($request->getPathInfo());
-           
+
         return $result;
     }
 
+
     /**
      * Assembles user submitted parameters forming a URL path defined by this route
      *

Comments

Hmm... typically, this sort of thing should be specified in the Accept header, not as part of the URI. As such, you can determine the format dynamically, and dispatch to the appropriate view. This also can be done on-demand in plugins, without further complicating the route logic.

Accept headers are nice but so is a tidy URI with a format specifier which simplifies API usage and eases debug. Rails routes support this by default.

This can already be easily done with ContextSwitch in the controller and a format parameter on the URI:


class NewsController
{
    public function init()
    {
        parent::init();
        $this->_contextSwitch->addActionContexts(array('index' => array('json', 'xml')));
        $this->_contextSwitch->initContext();
    }
}

/news/index/format/xml /news/index/?format=xml /news/index/format/json /news/index/?format=json

The suggested workaround ideas are appreciated, but are not quite the same. There are good reasons for mapping URI's using file type extensions.

Not sure but it appears there might be a general resistance to extend the framework. The rest route appears to not yet have many important features which I suppose we'll implement in our own proprietary libraries. Nested resources, implicit controller specification, file type extensions, and overall the general ability to control what URI's look like are important.

"There are good reasons for mapping URI's using file type extensions."

Without knowing what these "good reasons" are, yes - I am still resisting this change. It has to do with the whole "URI opacity" axiom - http://www.w3.org/DesignIssues/Axioms.html#opaque I.e., according to REST architectural principles, there's no difference between /news/index.json and /news/index/?format=json

Furthermore, this route is just one method of implementing RESTful architecture in ZF - specifically, it's a method that leverages Zend_Controller_Router_Rewrite. We can easily use static or regexp routing to implement more URI control like RoR or django.

To implement features like nested resources, implicit controllers, etc. we should use the proposal process and not a single JIRA ticket. I've been working on a proposal to enhance the 'Resource' part of Zend_Rest here:

http://framework.zend.com/wiki/display/…

Finally, RESTful implementation could be going thru a renovation as we work on ZF 2.0 so we may want to make a new 'Zend_Rest_2.0' proposal.

In either case, the feature request of this ticket is more appropriate for a proposal I think.

I really appreciate Doug's enhancement, although I've not yet screened the patch.

Currently we're using the Accept-header approach but when working with people that are not really familiar with HTTP headers it sometimes gets a bit confusing. For example, {{file_get_contents}} does not send any {{Accept}} headers (without defining a context).

@Luke: You're right that "there's no difference between /news/index.json and /news/index/?format=json" but I myself would prefer the former so that REST API users won't have to mess with additional parameters. It's just convenience and the way Rails is doing it.

+1 for this issue/enhancement.

@Luke: No objections over standards such as URI opacity. However I haven't convinced myself there's no place for what I'm doing here, at least in development mode.

As I use Rails as my model for best practices, I'll have to research their justification for this with respect to the same.

@Moritz: thanks for the support.