Introduction;

The current way that the Zend Framework handles URI structure is simple enough. It serves a logical purpose and is easy break apart into it's components;

Zend Framework's URI structure:
/controller/action/key1/value1/key2/value/

Ok, so that pretty much makes sense. After a quick read of the framework manual, it should be clear to you what controllers, actions and key-pairs are. The structure is frozen like that however, and you can't make one URI node look a parent of another without cheating.

Example 1 (Semantically correct URI, with key-pair):
/tag/view/name/framework/

Example 2 (Only key specified, no value):
/tag/view/framework/

Example 3 (Using __call to catch tag-name):
/tag/framework/

The above (example 3) is how Technorati specified the URI for their view-tag page. After all, it's not preferable to waste URI segments, like the previous examples did. Also the web address for any given page should allow for the user to easily self-navigate to a parent node without difficulty.

Technorati breaks from this slightly in that /tag/ is used as the parent instead of the literal parent node /tags/. If /tag/ is accessed directly, you get the same content as /tags/. Personally I opt for a /tag/ controller to exist with a single index action, which would then use _redirect to forward the user to the literal parent node.

Another real-world example:
/article/*name*/comments/

In the above example, /article/ is a controller, but so is /comments/. To implement this in the current Zend Framework route procedure, you have to completely break the semantic rules. /article/ becomes the controller, __call becomes a dynamic view action, and /comments/ effectively becomes a GET value.

Key-Pair Problem;

When writing the patch I came across a problem. It becomes nessesary to choose between the following;

Method A)

  • Rely on __call to deal with dynamic pages (still partially cheating).
  • Transfers key-pairs from redirecting to to target URI
  • Redirection rules of higher level nodes (eg. /article/) are still activated for lower level children (eh. /article/view/).
Method B)
  • Enables semantically correct structure (/controller/action/key/val/)
  • Is not reliant on __call.
  • Doesn't transfer key-pairs.
  • URI with keypairs are treated as seperate addresses

The reason for having to choose between tradeoffs is based on the fact that in the ZF structure, a key-pair looks exactly the same as an action or a controller. I propose a comprimise on this issue and include an equals sign in the key-pair URI syntax.

Proposed key-pair syntax improvement:
/controller/action/key1=value1/key2=value/

As well as providing a clearer distinction, and solving the above problem, the modification would have usability benefits. The ability for the user to self navigate to a higher level node is hindered in the current system. A user might just a key-pair value and nothing more. The ?key=value syntax is ugly sure, but at least a user is aware that's a parameter rather than a node.

In the config part of the patch, you can specify a flag to decide which or the above methods you want. Any comments specifically regarding this are very welcome.

The Patch;

The framework's plugin architecture exists now, but there is no documentation. If there is a simple way to currently make a plugin, and someone tells me, I will make this hack into one. Also, it is possible to subclass framework components to extend, but given that other from inserted code, the class stays exactly the same, i'm using a direct patch to the framework's Router class rather than subclassing.

Given this fact, the business code part of the patch should be inserted directly into the Framework's Zend_Controller_Router class, so you will need access to it's library directories.

Config Code:

(Inserted after Zend classfile included, and before Controller_Front Instantiation.)
// A couple of example rules:	
$uriMap = array(
'/people/index/' => '/people/',
'/person/*uname*/' => '/company/*uname*/'
);
Zend::register('uri-map', (object) $uriMap);
// 1 = Method A, 0 = Method B:
Zend::register('keep-pairs', (object) 1);

Business Code:

(Inserted at the start of route($dispatch) method, on line 49, in Zend/Controller/Router.php)
/**
* PATCH - Hierarchical URI Pre-Processing
* @version 1.0
* @author Daniel Morris
*/
$uriMap = (array) Zend::registry('uri-map');
$uriMapKeys = array_keys($uriMap);
$uriSegments = explode('/', $_SERVER['REQUEST_URI']);
$transferVars = null;
$transferPairs = Zend::registry('keep-pairs')->scalar;
$uriTarget = null;
$uriWithoutKeyPairs = null;
$matchFound = false;
for($i = 0; $i < count($uriMap); $i++) {
reset($uriSegments);
$transferVars = array();
$countMatches = 0;
$countSegments = 0;
$uriMapSegments = explode('/', current($uriMapKeys));
for($j = 1; $j < count($uriMapSegments); $j++) {
$currentMapSegment = current($uriMapSegments);
if ($currentMapSegment) {
$countSegments++;
}
if (($currentMapSegment[0] == '*') && ($currentMapSegment[strlen($currentMapSegment)-1] == '*')) {
$transferVars[$currentMapSegment] = current($uriSegments);
} else {
if ($currentMapSegment == current($uriSegments)) {
$countMatches++;
}
}
next($uriSegments);
next($uriMapSegments);
}
if ($countMatches >= $countSegments) {
$matchFound = true;
$uriTarget = current($uriMap);
$uriWithoutKeyPairs = current($uriMapKeys);
break;
}
next($uriMap);
next($uriMapKeys);
}
for($i = 0; $i < count($transferVars); $i++) {
$uriTranslated = str_replace(key($transferVars), current($transferVars), $uriTarget);
$uriWithoutKeyPairs = str_replace(key($transferVars), current($transferVars), $uriWithoutKeyPairs);
next($transferVars);
}
if ($uriWithoutKeyPairs) {
$keyPairs = explode($uriWithoutKeyPairs, $_SERVER['REQUEST_URI']);
}
if ($transferPairs) {
$uriTranslated .= $keyPairs[1];
}
if (($transferPairs) || ($uriWithoutKeyPairs == $_SERVER['REQUEST_URI'])) {
$_SERVER['REQUEST_URI'] = $uriTranslated;
}
/** END-PATCH */

Credit;

The version of the Zend framework used in this article was 0.1.2

Author Dan Morris