Skip to end of metadata
Go to start of metadata

<ac:macro ac:name="section"><ac:parameter ac:name="border">true</ac:parameter><ac:rich-text-body>

<ac:macro ac:name="column"><ac:parameter ac:name="width">70%</ac:parameter><ac:rich-text-body>
<h2>Download <ac:link><ri:attachment ri:filename="PathRouter.zip" /></ac:link></h2>

<p>Latest changes 7-30-2006</p>
<ul>
<li>added support for an alternative "controllers in subdirs" syntax, dir1_dir2_controller, to MJS_Controller_Dispatcher</li>
<li>proposed by Christopher Thompson</li>
</ul>

<p>Latest changes 7-26-2006</p>
<ul>
<li>fixed a bug where if a var with a default followed a path that wasn't a <code>/</code>, the regex wasn't making the trailing <code>/</code> optional</li>
<li>thanks to Richard Ingham for the fix!</li>
</ul>

<p>Latest changes 7-25-2006</p>
<ul>
<li>moved <code>$path = trim($path, '/');</code> from MJS_Controller_PathRouter back into MJS_Controller_Router_PathRoute to maintain compatibility with RewriteRouter for now</li>
<li>simplified using controllers in subdirectories. No more overrides array (what was I thinking? <ac:emoticon ac:name="smile" />) Look below for updated instructions.</li>
</ul>
</ac:rich-text-body></ac:macro>

<ac:macro ac:name="column"><ac:parameter ac:name="width">30%</ac:parameter><ac:rich-text-body>
<h2>Download <ac:link><ri:attachment ri:filename="Stripper.php" /></ac:link></h2>

<p>Makes .php files in the Zend Framework on average 80% smaller by stripping out comments and whitespace. By reducing the filesize and making parsing easier, it makes your apps quicker. No original files are modified. See instructions in <ac:link><ri:attachment ri:filename="Stripper.php" /></ac:link> for details.</p></ac:rich-text-body></ac:macro>
</ac:rich-text-body></ac:macro>

<p><br class="atl-forced-newline" /></p>
<h2>Overview of the classes:</h2>
<hr />
<p><strong>MJS_Controller_PathRouter</strong> is an improved version of Zend_Controller_RewriteRouter.  I created it because RewriteRouter is not cross platform (does not work on IIS due to its reliance on REQUEST_URI), it doesn't generate a correct "RewriteBase", and I was annoyed that it forced me to use the default routes. MJS_Controller_PathRouter fixes all of these issues.
<br class="atl-forced-newline" />
<br class="atl-forced-newline" />
<strong>MJS_Controller_Router_PathRoute</strong> is a rewrite of Zend_Controller_Router_Route. It was written to address the shortcomings in Zend_Controller_Router_Route.  Mainly the fact that variables could only be separated by a '/' and parameters could not be captured as a string but only a key/value combination.  MJS_Controller_Router_PathRoute is 100% backwards compatible with the most recent Zend_Controller_Router_Route in SVN so you can use it with either MJS_Controller_PathRouter or Zend_Controller_RewriteRouter.
<br class="atl-forced-newline" />
<br class="atl-forced-newline" />
<strong>MJS_Controller_Dispatcher</strong> is an extension of Zend_Controller_Dispatcher that adds the ability to have controllers in subdirectories
<br class="atl-forced-newline" />
<br class="atl-forced-newline" />
<strong>MJS_View_Helper_Url</strong> is the same as Zend_View_Helper_Url with a change to call $router->getBasePath() instead of $router->getRewriteBase() for compatibility with MJS_Controller_PathRouter</p>

<p><br class="atl-forced-newline" /></p>
<h2>Features:</h2>
<hr />
<p><strong>MJS_Controller_PathRouter</strong></p>
<ul>
<li>compatible with Apache with and without mod_rewrite, running PHP as either a SAPI module or cgi/fastcgi</li>
<li>compatible with IIS with and without ISAPI_Rewrite, running PHP as either an ISAPI module or cgi/fastcgi</li>
<li>works correctly on 1and1 web hosting servers</li>
<li>automagic default routes. Does not force you to use the default routes if you define your own routes</li>
<li>accurately sets the BasePath (RewriteBase) and takes into account whether or not you are using mod_rewrite/ISAPI_Rewrite. PathRouter will set the BasePath to <code>/path/to/base</code> if you are using rewrite, or <code>/path/to/base/index.php</code> if you are not using rewrite</li>
<li>since BasePath is set correctly, PathRouter also processes the incoming REQUEST_URI and determines the route more reliably than Zend_Controller_RewriteRouter</li>
<li>most likely you will not need to call setBasePath() aka setRewriteBase() yourself because of the above improvements it will already be set correctly in the first place</li>
<li>on average, performance is equal to and sometimes better than Zend_Controller_RewriteRouter</li>
</ul>

<p><br class="atl-forced-newline" />
<strong>MJS_Controller_Router_PathRoute</strong></p>
<ul>
<li>:variables can be defined anywhere and in any combination in your route, not just in between <code>/</code>'s. This allows for Zend Framework to have the same advanced matching capabilities as mod_rewrite and the most advanced URL matching out of any PHP framework.</li>
<li>PathRoute has more advanced route matching than Ruby on Rails</li>
<li>:variables can be next to each other</li>
<li>wildcards can be defined anywhere in your route and you can use more than one wildcard as opposed to Zend_Controller_Router_Route which only allows a single wildcard at the end of the route with a hardcoded var1/value1/var2/value2 format</li>
<li>you can prefix or append text to a variable after it has been matched in your url in order to modify it before it gets to the next step in the routing process</li>
<li>because of the above feature, you can prefix a folder name to a <code>:controller</code> which will allow you to have controllers in subdirectories.</li>
<li>syntax and behavior is 100% backwards compatible with the latest Zend_Controller_Router_Route so none of your code has to be changed</li>
<li>can easily be used as an addRoute() plugin to Zend_Controller_RewriteRouter</li>
<li>less overhead when defining a route. Absolutely no processing occurs until match() or assemble() is called. This should potentially save CPU cycles and memory for sites with large numbers of routes. Just as a comparison, Zend_Controller_Router_Route on each call to __construct() performs 2 typecasts, explode, a foreach loop containing 2 if statements, and 2 calls to substr(). Let's say you have a site with 100 routes and the 90th route you defined is a match. Using PathRoute you will only parse and process 10 routes but with Zend_Controller_Router_Route you parse all 100 routes every request whether or not you used them in addition to processing 10 routes before the 90th is matched.</li>
</ul>

<p><br class="atl-forced-newline" /></p>
<h2>How To Install:</h2>
<hr />
<p>Download <ac:link><ri:attachment ri:filename="PathRouter.zip" /></ac:link> and unzip the <strong>MJS</strong> folder into your <strong>library</strong> folder. If you created the folders in the correct place your folder structure should look like:</p>
<ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
/path/to/library

– Zend
– MJS
]]></ac:plain-text-body></ac:macro>

<p><br class="atl-forced-newline" /></p>
<h2>Quick Start:</h2>
<hr />
<p>Your index.php (bootstrap) file should look something like this:</p>
<ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
<?php

// Add Zend Framework to the PHP include path
set_include_path(get_include_path() . PATH_SEPARATOR . 'path/to/trunk/library');

// If you don't have an __autoload() function defined you need to require the files below
require_once 'Zend.php';
require_once 'Zend/View.php';
require_once 'Zend/Controller/Front.php';
require_once 'MJS/Controller/PathRouter.php';
require_once 'MJS/Controller/Dispatcher.php';

// Create a Zend_View object
$view = new Zend_View;
$view->setScriptPath('path/to/views');
Zend::register('view', $view);

// Create a MJS_Controller_PathRouter object
$router = new MJS_Controller_PathRouter();

// If you add your own routes and still want to use the default routes too then you need to
// call addDefaultRoutes(). If you don't add any of your own routes you do not need this line
// because the default routes will be used automatically.
$router->addDefaultRoutes();

// PathRouter can use the same routes as RewriteRouter
$router->addRoute('news', new Zend_Controller_Router_Route('news/:controller/:action/:id'));

// PathRoute routes are backwards compatible with Zend_Controller_Router_Route so the same syntax works
$router->addRoute('news', new MJS_Controller_Router_PathRoute('news/:controller/:action/:id'));

// but now you can also add routes which require advanced matching
$router->addRoute('logViewer',
new MJS_Controller_Router_PathRoute(
':controller/:action/customer{:id}httpd-error.:ext',
array(),
array('id' => '\d+')
)
);

// Create a Zend_Controller_Front object
$controller = Zend_Controller_Front::getInstance();
$controller->setRouter($router);
// MJS_Controller_Dispatcher is only needed if you are using the controllers in subdirectories feature
$controller->setDispatcher(new MJS_Controller_Dispatcher());
$controller->run(path/to/application/controllers');
]]></ac:plain-text-body></ac:macro>
<ac:macro ac:name="info"><ac:parameter ac:name="title">:variable names follow the same rules as PHP</ac:parameter><ac:rich-text-body>
<p>When naming <code>:variables</code> in your PathRoutes, use the PHP variable naming conventions. Basicly the name can only consist of the following characters: <code>_, A-Z, 0-9, and ASCII characters from 127 through 255</code>.<br />
For more information please look at <a href="http://php.net/variables">http://php.net/variables</a></p></ac:rich-text-body></ac:macro>

<p><br class="atl-forced-newline" /></p>
<h2>Important notes about MJS_Controller_PathRouter and default routes:</h2>
<hr />
<p>If you do not set any routes with $router->addRoute(), PathRouter will use the built in default routes for '' and ':controller/:action/*'. If you do set your own routes, the built in default routes will not be used unless you call $router->addDefaultRoutes() before your other addRoute()'s</p>

<p><br class="atl-forced-newline" /></p>
<h2>Controllers in subdirectories:</h2>
<hr />
<p>There are only two things you have to know to use controllers in subdirectories:</p>
<ol>
<li>In your defaults array you have to specify a path to prefix to the <code>:controller</code> var by using the special prefix syntax: <code>'+controller' => 'mySubDir/'</code></li>
<li>You must call <code>$controller->setDispatcher(new MJS_Controller_Dispatcher())</code> before calling <code>$controller->run()</code> or <code>$controller->dispatch()</code> in order for the controller to locate controllers/actions in subdirectories. This is only temporary until the Zend Dispatcher supports subdirs.</li>
</ol>

<p><br class="atl-forced-newline" />
Let's say you have your controllers organized like this:</p>
<ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
/path/to/controllers

– admin
– IndexController.php
– UsersController.php
– IndexController.php
– FooController.php
– BarController.php
]]></ac:plain-text-body></ac:macro>
<p>You can access the actions in admin/AccountsController by defining your route like this:</p>
<ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
$router->addRoute('AdminUsers',
new MJS_Controller_Router_PathRoute(
'admincp/:controller/:action',
array('+controller' => 'admin' . DIRECTORY_SEPARATOR)
)
);
]]></ac:plain-text-body></ac:macro>
<p>When you type in a URL such as <code><a class="external-link" href="http://mysite.com/admincp/users/edit"> http://mysite.com/admincp/users/edit

</a></code> it will assign <code>'controller' => 'users'</code> and <code>'action' => 'edit'</code> but then the prefix default will change it to <code>'controller' => 'admin/</code><strong>users</strong><code>'</code></p>

<p><br class="atl-forced-newline" /></p>
<h2>Using the special Prefix and Append defaults:</h2>
<p>The above example used the <strong>prefix</strong> default to add a subdirectory to a controller. You are not limited to just the <code>:controller</code> variable. You can prefix any variable that is matched in your URL by putting <code>'variable' => 'myPrefix'</code> in your defaults array. You can also add text after your matched variables using the <strong>append</strong> default. By putting <code>'variable' => 'addedToEnd'</code> in your defaults array you can append to any variable that is matched in your URL.</p>

<p>For example:</p>
<ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
$router->addRoute('myRoute',
new MJS_Controller_Router_PathRoute(
':controller/:action',
array(
'+controller' => 'this_text_is_before_'),
'controller+' => '_and_this_is_after',
'action+' => '-appendedToAction'
)
)
);
]]></ac:plain-text-body></ac:macro>
<p>When you type in a URL such as <code><a class="external-link" href="http://mysite.com/ctrl/act">http://mysite.com/ctrl/act</a></code> it will assign <code>'controller' => 'ctrl'</code> and <code>'action' => 'act'</code> but then the the prefix and append defaults will change them to <code>'controller' => 'this_text_is_before_</code><strong>ctrl</strong><code>_and_this_is_after'</code> and <code>'action' => '</code><strong>act</strong><code>-appendedToAction'</code>.</p>

<p><br class="atl-forced-newline" /></p>
<h2>Examples:</h2>
<hr />
<p>In Zend_Controller_Router_Route, routes are limited to only one variable between each <code>'/'</code> in the url. Now you can define routes with variables anywhere such as:</p>
<ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
$router->addRoute(
'logViewer',
new MJS_Controller_Router_PathRoute(
':controller/:action/file:id.:ext',
array(),
array('id' => '\d+')
)
);

matches: http://mysite.com/log/view/file206.xml
Array
(
[controller] => log
[action] => view
[id] => 206
[ext] => xml
)
]]></ac:plain-text-body></ac:macro>
<p>If a variable in the route is ambiguous, you can escape it with {}'s:</p>
<ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
$router->addRoute(
'logViewer',
new MJS_Controller_Router_PathRoute(
':controller/:action/customer{:id}httpd-error.:ext',
array(),
array('id' => '\d+')
)
);

matches: http://mysite.com/log/view/customer206httpd-error.log
Array
(
[controller] => log
[action] => view
[id] => 206
[ext] => log
)
]]></ac:plain-text-body></ac:macro>
<p>You can use a wildcard at the end of your route to capture parameters in a key1/value1/key2/value2 format:</p>
<ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
$router->addRoute(
'books',
new MJS_Controller_Router_PathRoute(
'books/:action/*',
array('controller' => 'book')
)
);

matches: http://mysite.com/books/find/author/Hemingway/sort/title
Array
(
[controller] => book
[action] => find
[author] => Hemingway
[sort] => title
[*] => author/Hemingway/sort/title
)
]]></ac:plain-text-body></ac:macro>
<p>You can also use (multiple) regex wildcards to capture parameters as strings:</p>
<ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
$router->addRoute(
'directions',
new MJS_Controller_Router_PathRoute(
'search/:action/from/:from/to/:to',
array('controller' => 'mapit'),
array('from' => '.', 'to' => '.')
)
);

matches: http://mysite.com/search/getDirections/from/1866 Euclid Ave/Cleveland/OH/44315/to/Rome/Italy
Array
(
[controller] => mapit
[action] => getDirections
[from] => 1866%20Euclid%20Ave/Cleveland/OH/44315
[to] => Rome/Italy
)
]]></ac:plain-text-body></ac:macro>
<p>You can even put two variables next to eachother:</p>
<ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
$router->addRoute(
'onlineStore',
new MJS_Controller_Router_PathRoute(
':controller/:action/:model_id:modelnum',
array(),
array('model_id' => '[a-zA-Z]', 'modelnum' => '\d')
)
);

matches: http://mysite.com/product/info/MX1035347
Array
(
[controller] => product
[action] => info
[model_id] => MX
[modelnum] => 1035347
)
]]></ac:plain-text-body></ac:macro>

<p><br class="atl-forced-newline" /></p>
<h2>Benchmarks: (updated 7-25-2006 to reflect latest changes in code)</h2>
<hr />
<h3>About:</h3>
<p>I designed the benchmark based on finding a match out of 20 routes. A seperate bootstrap is created for each router. Apache is restarted before each script is tested and 10 runs of 1000 requests are made to each script. The command used to execute each test is:</p>
<ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
apachectl restart && for i in 1 2 3 4 5 6 7 8 9 10; do ab -n 1000 -c 10 http://127.0.0.1/mjs.php | grep mean; done
]]></ac:plain-text-body></ac:macro>
<p>The highest and lowest result from each script is discarded and then the average requests per second of the remaining 8 runs is recorded.</p>

<p><br class="atl-forced-newline" /></p>
<h3>Test machine:</h3>

<ul>
<li>2.4GHz Celeron, 1gig RAM</li>
<li>FreeBSD 6.1</li>
<li>Apache 2.2.2</li>
<li>PHP 5.1.4 FastCGI</li>
</ul>

<p><br class="atl-forced-newline" /></p>
<h3>Results:</h3>

<ul>
<li>MJS_Controller_PathRouter + MJS_Controller_Router_PathRoute
<ul>
<li>served <strong>42.2025</strong> pages in 1 second</li>
<li>took approximately <strong>23.69525 milliseconds</strong> to find the correct route</li>
</ul>
</li>
<li>Zend_Controller_RewriteRouter + Zend_Controller_Router_Route
<ul>
<li>served <strong>42.7475</strong> pages in 1 second</li>
<li>took approximately <strong>23.393 milliseconds</strong> to find the correct route</li>
</ul>
</li>
</ul>

<p><br class="atl-forced-newline" />
As you can see from the results there is only a <strong>0.54</strong> pages served each second difference and a <strong>0.3 millisecond</strong> time per request difference between the two routers. There no performance penalty for the features that PathRouter / PathRoute adds.</p>

<p><br class="atl-forced-newline" /></p>
<h3>Source code:</h3>
<p>config.php</p>
<ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
<?php

$zfLibraryDir = dirname(_FILE_) . '/../trunk/library';

$numberOfRoutes = 20;
$_SERVER['REQUEST_URI'] = $_SERVER['SCRIPT_NAME'] . '/' . rand(1, $numberOfRoutes) . '/benchmark/';
$controllerDir = dirname(_FILE_) . DIRECTORY_SEPARATOR . 'controllers';
]]></ac:plain-text-body></ac:macro>

<p>mjs.php</p>
<ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
<?php

require 'config.php';

set_include_path(get_include_path() . PATH_SEPARATOR . $zfLibraryDir);
require_once 'Zend.php';
require_once 'Zend/Controller/Front.php';
require_once 'MJS/Controller/PathRouter.php';

$router = new MJS_Controller_PathRouter();
$router->addDefaultRoutes();

for ($i=1; $i<=$numberOfRoutes; $i++) {
$router->addRoute($i, new MJS_Controller_Router_PathRoute("$i/:controller/:action", array('action' => 'index')));
}

$controller = Zend_Controller_Front::getInstance();
$controller->setRouter($router);
$controller->run($controllerDir);
]]></ac:plain-text-body></ac:macro>

<p>martel.php</p>
<ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
<?php

require 'config.php';

set_include_path(get_include_path() . PATH_SEPARATOR . $zfLibraryDir);
require_once 'Zend.php';
require_once 'Zend/Controller/Front.php';
require_once 'Zend/Controller/RewriteRouter.php';

$router = new Zend_Controller_RewriteRouter();

for ($i=1; $i<=$numberOfRoutes; $i++) {
$router->addRoute($i, new Zend_Controller_Router_Route("$i/:controller/:action", array('action' => 'index')));
}

$controller = Zend_Controller_Front::getInstance();
$controller->setRouter($router);
$controller->run($controllerDir);
]]></ac:plain-text-body></ac:macro>

Labels:
router router Delete
route route Delete
pathroute pathroute Delete
pathrouter pathrouter Delete
rewriterouter rewriterouter Delete
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.
  1. Jul 25, 2006

    <p>This seemed to error out a bit on me, it wasn't matching things with their defaults... <br />
    For example, if:</p>

    <p><code>$route = new MJS_Controller_Router_PathRoute('admin/news/:action/:id', array('controller' => 'admin_news', 'action' => 'index', 'id' => 0), array('id' => '\d+'));</code></p>

    <p>Then <code>$route->match('admin/news/');</code> returns false.</p>

    <p>I think this is to do with the slashes and what happens to them, I think I've sort of fixed it with the following:</p>

    <p><strong>in PathRouter::route():</strong><br />
    <code>$path = trim($path, '/'); }}->{{ $path = trim($path, '/') . '/';</code></p>

    <p><strong>in PathRoute::prepare():</strong><br />
    <code>$this->_regex .= self::REGEX_DELIMITER; }}<span style="text-decoration: line-through;">>{{ $this</span>>_regex .= '(?:/)?$' . self::REGEX_DELIMITER;</code></p>

    <p>And then it matches things with blanks where they should have defaults.</p>

    <p>(This is with the latest version; I just downloaded the attachment today.)</p>

    1. Jul 25, 2006

      <p>Oops, I messed up the formatting on that a bit, I pressed post instead of preview :\</p>

      <p>Try again:<br />
      in PathRouter::route():<br />
      <code>$path = trim($path, '/');</code> => <code>$path = trim($path, '/') . '/';</code></p>

      <p>in PathRoute::prepare():<br />
      <code>$this->_regex .= self::REGEX_DELIMITER;</code> => <code>$this->_regex .= '(?:/)?$' . self::REGEX_DELIMITER;</code></p>

    2. Jul 25, 2006

      <p>Hi Richard,</p>

      <p>You are correct. When working on PathRouter I moved <code>$path = trim($path, '/')</code> from PathRoute into PathRouter because I didn't see a point in trimming the path every single route you define when it could be done once in the router itself. For now I will move it back into the PathRoute class to maintain compatibility with RewriteRouter. Thanks!!</p>

      1. Jul 26, 2006

        <p>No problem, it's a great set of classes, and I think it should definitely be in ZF proper.</p>

        <p>I think the new overrides method is much neater too <ac:emoticon ac:name="smile" /> Can you prepend actions as well?</p>

        1. Jul 26, 2006

          <p>You sure can! You can change ANY variable with prefix and append.</p>

          <ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
          $router->addRoute('AdminCP',
          new MJS_Controller_Router_PathRoute(
          'admincp/:controller/:action/*',
          array(
          '+controller' => 'admin' . DIRECTORY_SEPARATOR),
          '+action' => 'my',
          '+userid' => 'foo'
          )
          )
          );
          ]]></ac:plain-text-body></ac:macro>

          <p>If you match the url: <code><a class="external-link" href="http://mysite.com/admincp/account/edit/userid/123">http://mysite.com/admincp/account/edit/userid/123</a></code></p>

          <p>The prefix default will change it to:</p>
          <ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
          Array
          (
          [controller] => admin/account
          [action] => myedit
          [userid] => foo123
          )
          ]]></ac:plain-text-body></ac:macro>

          <p>Which would then load:<br />
          <code>/path/to/controllers/admin/AccountController.php</code></p>

          <p>and run the <code>myeditAction()</code></p>

          1. Jul 26, 2006

            <p>Cool, that will work nicely <ac:emoticon ac:name="smile" /></p>

            <p>I think I must be doing something wrong though, because I had to make my changes again to make it match correctly. :\</p>

            1. Jul 26, 2006

              <p>Hmm are you sure you have the latest version from the download link at the top of the page?</p>

              <p>If you look in PathRoute.php it should have:</p>
              <ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
              113 public function match($path)
              114 {
              115 $path = trim($path, '/'); // Added for compatibility with RewriteRouter
              ]]></ac:plain-text-body></ac:macro>

              <p>If you are trying to use controllers in subdirectories make sure your controller code in the index.php bootstrap looks something like:</p>
              <ac:macro ac:name="noformat"><ac:plain-text-body><![CDATA[
              // Create a Zend_Controller_Front object
              $controller = Zend_Controller_Front::getInstance();
              $controller->setRouter($router);
              $controller->setDispatcher(new MJS_Controller_Dispatcher()); // needed for controllers in subdirs until Zend_Controller_Dispatcher supports it natively
              $controller->run(path/to/application/controllers');
              ]]></ac:plain-text-body></ac:macro>

              <p>If none of that works could you post some example code?</p>

              1. Jul 26, 2006

                <p>Sure: I'm playing with the following route:</p>

                <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
                'admin-news' => new MJS_Controller_Router_PathRoute(
                'admin/news/:action/:id',
                array (
                'controller' => 'admin_news',
                'action' => 'index',
                'id' => 0
                ),
                array(
                'id' => '\d+'
                )
                ),
                ]]></ac:plain-text-body></ac:macro>

                <p>The controllers are not in subdirectories at the moment. I may do that in the future.</p>

                <p>Then if I go to <a class="external-link" href="http://localhost/admin/news/">http://localhost/admin/news/</a> then I get no match, but if I try <a class="external-link" href="http://localhost/admin/news/someaction">http://localhost/admin/news/someaction</a> then it's fine.</p>

                <p>I can show what's happenning by putting in the following notifier:</p>

                <p><strong>PathRoute::match()</strong></p>
                <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
                $this->_values = $this->_defaults;

                if ($this->_regex === null) $this->prepare();

                echo ("<pre>Match [$path] to [{$this->_regex}]</pre>"); // <- Inserted

                if (preg_match_all($this->_regex, $path, $matches, PREG_SET_ORDER)) {
                ]]></ac:plain-text-body></ac:macro>

                <p>And then in the Dispatcher, I exit like so:</p>

                <p><strong>Dispatcher::_dispatch()</strong></p>
                <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
                $className = $this->formatControllerName($action->getControllerName());

                die($className); // <- Inserted
                ]]></ac:plain-text-body></ac:macro>

                <p>So now, if I run <a class="external-link" href="http://localhost/admin/news/">http://localhost/admin/news/</a> I get the following:</p>

                <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
                Match [admin/news] to [#^(admin/news/)([\w\-\.\~\!\*'\(\)\;\:\@\&\=\+\$\,\?\%\#\[\]])??(\d)?$#]

                Match [admin/news] to [#^([\w\-\.\~\!\*'\(\)\;\:\@\&\=\+\$\,\?\%\#\[\]])?([\w\-\.\~\!\*'\(\)\;\:\@\&\=\+\$\,\?\%\#\[\]])?/?(.*)$#]

                AdminController
                ]]></ac:plain-text-body></ac:macro>

                <p>Where the one that matches in this case is just the default route, ':controller/:action/*'.</p>

                <p>The fact that we've trimmed off the final slash from the $path means that it won't match the first regex, because that slash is inside the <code>#^(admin/news/)</code> part of the regex, so is compulsory.</p>

                <p>If I run <a class="external-link" href="http://localhost/admin/news/someaction">http://localhost/admin/news/someaction</a> I get the following:</p>

                <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
                Match [admin/news/someaction] to
                [#^(admin/news/)([\w\-\.\~\!\*'\(\)\;\:\@\&\=\+\$\,\?\%\#\[\]])??(\d)?$#]

                AdminNewsController
                ]]></ac:plain-text-body></ac:macro>

                <p>i.e, it's matching here, because it doesn't expect a slash on the end of 'someaction'.</p>

                <p>So what I did was put the slash back on the end, to make sure that it does have one, instead of trying to make sure that it doesn't have one:</p>

                <p><strong>PathRoute::match()</strong></p>
                <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
                public function match($path)
                {
                $path = trim($path, '/') . '/'; // Added for compatibility with RewriteRouter

                ...
                ]]></ac:plain-text-body></ac:macro>

                <p>Now there is always a trailing slash, whatever the input. It just needs a slash in the regex to match it. This has to be optional because otherwise an empty path won't match, and neither will one with no actions etc. And it can't be captured because otherwise you get an extra match which you didn't expect <ac:emoticon ac:name="smile" /> So:</p>

                <p><strong>PathRouter::prepare()</strong></p>
                <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
                ...

                $this->_regex .= '/?$' . self::REGEX_DELIMITER;
                }
                ]]></ac:plain-text-body></ac:macro>

                <p>And now, if I go to the same URL, my output is:</p>

                <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
                Match [admin/news/] to [#^(admin/news/)([\w\-\.\~\!\*'\(\)\;\:\@\&\=\+\$\,\?\%\#\[\]])??(\d)?/?$#]

                AdminNewsController
                ]]></ac:plain-text-body></ac:macro>

                <p>Which is a match. The /? in the regex ensures that if I go to <a class="external-link" href="http://localhost/admin/news/edit/5">http://localhost/admin/news/edit/5</a> then I get:</p>

                <ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
                Match [admin/news/edit/5/] to [#^(admin/news/)([\w\-\.\~\!\*'\(\)\;\:\@\&\=\+\$\,\?\%\#\[\]])??(\d)?/?$#]

                AdminNewsController
                ]]></ac:plain-text-body></ac:macro>

                <p>Which is also a match, and everything works perfectly.</p>

                <p>I think the problem comes from the fact that if a 'part' is a 'path' then the trailing slash at the end of it is not optional in the regex, but gets trimmed off when we try to match it.</p>

                <p>I hope that's a bit more clear that I managed last time!</p>

                <p>(Of course, I may be using an old version of MJS or have conflicting code here, but I'm pretty sure that I don't...)</p>

                1. Jul 26, 2006

                  <p>Thanks Richard that was a huge help! I fixed this issue in the latest download at the top of the page. Hope that works for you. Keep me updated!</p>

                  1. Jul 27, 2006

                    <p>You left my echo statement in the match() funtion <ac:emoticon ac:name="cheeky" /><br />
                    Otherwise it looks like a good fix, thanks!</p>

          2. Jan 28, 2009

            <p>Hi Michael,</p>

            <p>I'm triying to set "administrative" actions inside each controller for each module. Example:</p>

            <p>|-- modules<br />
                        |-- user</p>

            <p>                      |-- controller</p>

            <p>                                |-- IndexController.php     
            <br class="atl-forced-newline" /></p>

            <p>inside the IndexController i want to have normal actions and prefixed actions too, this way:</p>

            <p>public function indexAction()<br />
            public function admin_editAction()</p>

            <p>i want the "prefixed" actions (like admin_editAction()) to be called this way:</p>

            <p>/:prefix/:modulename/:controller(default->index)/:action</p>

            <p>example:<br />
            /admin/user/edit/</p>

            <p>Can your Rewriter do it? i saw you control especific prefixed actions... i want it to admit params too, this way:</p>

            <p>/admin/user/edit/id/1/name/Jhon</p>

            <p>it is possible?</p>

            <p>i was triying to do one, and searching i reach your rewriterouter, can i make it with it?</p>

  2. Jul 26, 2006

    <p>Results:</p>

    <ul>
    <li>MJS_Controller_PathRouter + MJS_Controller_Router_PathRoute: 42.2025 Requests Per Second</li>
    <li>Zend_Controller_RewriteRouter + Zend_Controller_Router_Route: 42.7475 Requests Per Second</li>
    </ul>

    <p>As you can see from the results there is only a 0.54 requests per second difference between the two routers. There no performance penalty for the features that PathRouter / PathRoute adds.</p>

    <p>I would have to inject that 1/2 a second is a large amount of time in a web application considering the suggested max loadtime last time I read anything on the subject is around 8 seconds, Your router is using an additional 5% of the time you have to display a page and its only a small portion of the whole picture.</p>

    <p>That being said I love the functionality your router providers and I can over anaylize things a bit but I think performance should be kepted in check and improved if possible which is why so many suggestions from me on the mailing list, I want this module to improve and be added to ZF</p>

    1. Jul 26, 2006

      <p>It isn't 1/2 second difference. It is 1/2 REQUEST per second difference. Basicly on my machine the RewriteRouter test can serve 42.74 pages in 1 second. PathRouter can serve 42.2 pages in 1 second.</p>

    2. Jul 26, 2006

      <p>Just to put it into the perspective of time, PathRouter takes 0.3 milliseconds longer to do its thing than RewriteRouter. So as you see the difference is only a few thousandths of a second, much less of an impact than 1/2 second <ac:emoticon ac:name="smile" /></p>

  3. Dec 04, 2006

    <p>Why isn't this a formal proposal? I think this could easily be approved and moved to the incubator.</p>

    1. Dec 04, 2006

      <p>All this things can be made with a Plugin for Zend_Controller_Front</p>

      1. Dec 04, 2006

        <p>Or, since most of the cool stuff is MJS_Controller_Router_PathRoute you could just use that with Zend_Controller_RewriteRouter::addRoute()</p>

        1. Dec 04, 2006

          <p>Yeah, at this point the only part worth taking is MJS_Controller_Router_PathRoute. I wrote this before the MVC revsion which address similar issues that MJS_Controller_PathRouter does. I'm not sure if the new MVC components support controllers in subdirs but that might also be worth merging from this code in some form.</p>

          <p>There is a small issue in the regex that affects Wikipedia style url's that contain a colon but that is an easy fix. Other than that it has been rock solid.</p>

          1. Dec 05, 2006

            <blockquote>
            <p>I'm not sure if the new MVC components support controllers in subdirs but that might also be worth merging from this code in some form.</p></blockquote>
            <p>You should take a look at <a href="http://framework.zend.com/issues/browse/ZF-614">ZF-614</a> and <a href="http://framework.zend.com/issues/browse/ZF-617">ZF-617</a>, which discuss this.</p>

          2. Jan 04, 2007

            <p>Great piece of work! Will you look at the "colon issue" eventually or should we try to fix it ourselfs?</p>