Diving into Horde_Routes

<p><a target="_blank" href="http://dev.horde.org/routes">
Horde_Routes</a> is a new Horde library that is derived from the <a target="_blank" href="http://routes.groovie.org/">Python Routes</a> project.&nbsp; I've been meaning to give it a look for some time now, and a recent rewrite / cleanup of an Ansel powered <a title="My father-in-law's artwork" target="_blank" href="http://theabramsgallery.com">gallery site</a> gave me the perfect opportunity to dive in.</p>
  <p>In previous articles, I've outlined the basics of using Ansel to power an external gallery site.&nbsp; In this article, we'll look at using Horde_Routes to map 'pretty' URLs to the PHP code. </p>
  <p>The site is simple.&nbsp; It is basically nothing more than a thin wrapper around some of Ansel's views, with an 'About Us' and 'Home' page thrown in for good measure.&nbsp; I decided to implement the URLs like so:</p>
  <p> <code>
/ &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;             - The home, or default route <br /> /galleries  - The top level, paged gallery list.<br />/x&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;            - A gallery view where x represents the gallery id.<br />/x/y&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;         - An image view where y represents the image id.
</code> </p>
  <p>In all cases, paging is done with a 'page' URL parameter tacked on.&nbsp; For purely static pages, such as the About Us page, I have a path such as:</p>
  <p> <code>
    /content/about
</code> </p>
  <p>With the paths hashed out, it's time to look at the code. The first thing you need to do to enable Routes is to set up a rewrite rule on your webserver to pass all requests for your site to your controller script.&nbsp; On my site, I decided to name my controller script <em>dispatcher.php</em> since that pretty accurately represents it's responsibilities.&nbsp; How to go about setting up the rewrite rules will differ depending on your web server.&nbsp; I use <em>lighttpd</em> for my sites, and, as I found out, this has a particular 'gotcha' when dealing with a Routes enabled site. </p>
  <p>Apache has a switch that allows it to ignore any rewrite rules when the requested file already exists.&nbsp; This makes dealing with things like stylesheets, images and script files easy.&nbsp; With lighttpd, it's not so easy. Consider the following rewrite rule:</p>
  <p> <code>
    &quot;^(.*)$&quot; =&gt; &quot;/dispatcher.php?url=$1&quot;
</code> </p>
  <p>This basically takes all requests for your site (I'm assuming the Routes site is at the root of your site) and forwards it to <em>displatcher.php</em> and tacks on the requested path as a URL parameter.&nbsp; See the problem?&nbsp; Lighttpd does not ignore rewrite rules for existing files, so a request for a stylesheet, <em>/themes/default.css</em> will fail. The same for images, javascript files etc...&nbsp; To overcome this in lighttpd, you need to add a rewrite rule such as:</p>
  <p><code> </code></p>
  <p>&quot;^/(css|files|img|js)/.*$&quot; =&gt; &quot;$0&quot;</p>
  <p>Which, as you might guess, basically causes lighttpd to not rewrite the URLs that match the pattern given.&nbsp; With that in mind, and a rewrite rule to make sure that the default route of '/' is properly dealt with, my rewrite rules for this site look like this:</p>
  <p><code>&nbsp;$HTTP[&quot;host&quot;] =~ &quot;^(www.)?theabramsgallery\.com$&quot; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; url.rewrite-once += (
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;^/?$&quot; =&gt; &quot;/dispatcher.php?url=/&quot;,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;^/(css|files|img|js)/.*$&quot; =&gt; &quot;$0&quot;,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &quot;^(.*)$&quot; =&gt; &quot;/dispatcher.php?url=$1&quot;)
}
</code> </p>
  <p>The next step is to set up Routes and tell it about our desired mappings.&nbsp; This should be done in either some sort of config file, or a base include file for your site.&nbsp; First the code, then the explanation:<br /></p>
  <p><code> <span style="color: #007700;">* </span><span style="color: #0000bb;">Set up the Routes </span><span style="color: #007700;">*/
</span><span style="color: #0000bb;">$m </span><span style="color: #007700;">= new </span><span style="color: #0000bb;">Horde_Routes_Mapper</span><span style="color: #007700;">();

</span><span style="color: #ff8000;">/* 'Home' route */
</span><span style="color: #0000bb;">$m</span><span style="color: #007700;">-&gt;</span><span style="color: #0000bb;">connect</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'home'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">''</span><span style="color: #007700;">, array(</span><span style="color: #dd0000;">'controller' </span><span style="color: #007700;">=&gt; </span><span style="color: #dd0000;">'index'</span><span style="color: #007700;">));

</span><span style="color: #ff8000;">/* General content Pages */
</span><span style="color: #0000bb;">$m</span><span style="color: #007700;">-&gt;</span><span style="color: #0000bb;">connect</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'content'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">'/content/:content'</span><span style="color: #007700;">,
            array(</span><span style="color: #dd0000;">'controller' </span><span style="color: #007700;">=&gt; </span><span style="color: #dd0000;">'content'</span><span style="color: #007700;">,
                  </span><span style="color: #dd0000;">'action' </span><span style="color: #007700;">=&gt; </span><span style="color: #dd0000;">'view'</span><span style="color: #007700;">));

</span><span style="color: #ff8000;">/* Gallery List */
</span><span style="color: #0000bb;">$m</span><span style="color: #007700;">-&gt;</span><span style="color: #0000bb;">connect</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'list'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">'galleries'</span><span style="color: #007700;">, array(</span><span style="color: #dd0000;">'controller' </span><span style="color: #007700;">=&gt; </span><span style="color: #dd0000;">'galleries'</span><span style="color: #007700;">,
                                       </span><span style="color: #dd0000;">'action' </span><span style="color: #007700;">=&gt; </span><span style="color: #dd0000;">'index'</span><span style="color: #007700;">));
</span><span style="color: #ff8000;">/* Gallery View */
</span><span style="color: #0000bb;">$m</span><span style="color: #007700;">-&gt;</span><span style="color: #0000bb;">connect</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'gallery'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">'/:id'</span><span style="color: #007700;">, array(</span><span style="color: #dd0000;">'controller' </span><span style="color: #007700;">=&gt; </span><span style="color: #dd0000;">'galleries'</span><span style="color: #007700;">,
                                     </span><span style="color: #dd0000;">'action' </span><span style="color: #007700;">=&gt; </span><span style="color: #dd0000;">'view'</span><span style="color: #007700;">));

</span><span style="color: #ff8000;">/* Image View */
</span><span style="color: #0000bb;">$m</span><span style="color: #007700;">-&gt;</span><span style="color: #0000bb;">connect</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'image'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">'/:id/:image'</span><span style="color: #007700;">, array(</span><span style="color: #dd0000;">'controller' </span><span style="color: #007700;">=&gt; </span><span style="color: #dd0000;">'images'</span><span style="color: #007700;">,
                                          </span><span style="color: #dd0000;">'action' </span><span style="color: #007700;">=&gt; </span><span style="color: #dd0000;">'view'</span><span style="color: #007700;">));

</span><span style="color: #ff8000;">/* Advertise our controllers */
</span><span style="color: #0000bb;">$m</span><span style="color: #007700;">-&gt;</span><span style="color: #0000bb;">createRegs</span><span style="color: #007700;">(array(</span><span style="color: #dd0000;">'index'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">'galleries'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">'images'</span><span style="color: #007700;">, </span><span style="color: #dd0000;">'content'</span><span style="color: #007700;">));</span></code></p>
  <p>The first line creates a new instance of the Mapper object.&nbsp; With it, we 'connect' new mappings with the <em>connect()</em> method.&nbsp; Each <em>connect() </em>call as called above, takes 3 arguments (it can actually take a variable number of arguments - see the documentation for details).&nbsp; The first is the name of the route. It is not used at all when mapping a URL to an action, but it makes it easier when generating a URL within your site (see below).&nbsp; The second argument is the <em>Route Path</em> and can be composed of both <em>static </em>and <em>dynamic</em> parts.&nbsp; Static parts of the path are not preceded by a ':' , dynamic parts are. For example, the <em>list</em> route contains only a static path - <em>galleries. </em>This means that only the URL /galleries will match this route. The <em>gallery</em> route contains only a dynamic part, /:id.&nbsp; So a URL such as /10 will match this route.&nbsp; The third parameter is what actually determines what controller will be responsible for this action.&nbsp; As you can see, it does not have to mirror the paths...for example, you can see that I use the <em>galleries</em> controller for both the <em>list</em> and the <em>gallery</em> routes.<br /></p>
  <p> </p>
  <p>OK. So, now we know what controllers are responsible for what routes. Great. Now what?&nbsp; Well, now it's time to write the code that will handle the requests and pass off to the correct controller.&nbsp; For this, as stated above, I used a file named <em>dispatcher.php</em>.&nbsp; In that file is:</p>
  <p><code> <span style="color: #007700;">require_once </span><span style="color: #0000bb;">dirname</span><span style="color: #007700;">(</span><span style="color: #0000bb;">__FILE__</span><span style="color: #007700;">) . </span><span style="color: #dd0000;">'/lib/base.php'</span><span style="color: #007700;">;

</span><span style="color: #ff8000;">/* Grab, and hopefully match, the URL */
</span><span style="color: #0000bb;">$url </span><span style="color: #007700;">= </span><span style="color: #0000bb;">Util</span><span style="color: #007700;">::</span><span style="color: #0000bb;">getFormData</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'url'</span><span style="color: #007700;">);

</span><span style="color: #ff8000;">/* Get rid of any query args */
</span><span style="color: #007700;">if ((</span><span style="color: #0000bb;">$pos </span><span style="color: #007700;">= </span><span style="color: #0000bb;">strpos</span><span style="color: #007700;">(</span><span style="color: #0000bb;">$url</span><span style="color: #007700;">, </span><span style="color: #dd0000;">'?'</span><span style="color: #007700;">)) !== </span><span style="color: #0000bb;">false</span><span style="color: #007700;">) {
    list(</span><span style="color: #0000bb;">$url</span><span style="color: #007700;">, </span><span style="color: #0000bb;">$query</span><span style="color: #007700;">) = </span><span style="color: #0000bb;">explode</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'?'</span><span style="color: #007700;">, </span><span style="color: #0000bb;">$url</span><span style="color: #007700;">, </span><span style="color: #0000bb;">2</span><span style="color: #007700;">);
    </span><span style="color: #0000bb;">parse_str</span><span style="color: #007700;">(</span><span style="color: #0000bb;">$query</span><span style="color: #007700;">, </span><span style="color: #0000bb;">$args</span><span style="color: #007700;">);
} else {
    </span><span style="color: #0000bb;">$args </span><span style="color: #007700;">= array();
}
</span><span style="color: #0000bb;">$match </span><span style="color: #007700;">= </span><span style="color: #0000bb;">$m</span><span style="color: #007700;">-&gt;</span><span style="color: #0000bb;">match</span><span style="color: #007700;">(</span><span style="color: #0000bb;">$url</span><span style="color: #007700;">);</span> <span style="color: #007700;">.</span> <span style="color: #007700;">. // Do stuff</span> <span style="color: #007700;">.</span> <span style="color: #007700;"></span><span style="color: #007700;"> </span><span style="color: #ff8000;">/* Hand off to the proper controller */
</span><span style="color: #0000bb;">$action </span><span style="color: #007700;">= </span><span style="color: #0000bb;">$match</span><span style="color: #007700;">[</span><span style="color: #dd0000;">'action'</span><span style="color: #007700;">];
include </span><span style="color: #0000bb;">dirname</span><span style="color: #007700;">(</span><span style="color: #0000bb;">__FILE__</span><span style="color: #007700;">) . </span><span style="color: #dd0000;">'/' </span><span style="color: #007700;">. </span><span style="color: #0000bb;">$match</span><span style="color: #007700;">[</span><span style="color: #dd0000;">'controller'</span><span style="color: #007700;">] . </span><span style="color: #dd0000;">'.php'</span><span style="color: #007700;">;</span> </code></p>
  <p>In the first section of the code, we get the requested path from the query parameter.&nbsp; We then have to strip off any query parameters that were passed in with the path. Routes will only match URLs with no query arguments. Then, we call the <em>match()</em> method of our <em>Mapper</em> object and are passed back an array representing the matched route.&nbsp; This is a fairly simple site, I use a separate PHP file for each controller. I've omitted code from my dispatcher that doesn't relate to Routes, mainly I also set up a Horde_View object that I use in all my controllers to handle the displaying of the view template.</p>
  <p>The only thing left really, for a basic Routes driven site is generating the URLs for the site. That's done with the <em>Horde_Routes_Utils#urlFor </em>method like so:</p>
  <p><code> <span style="color: #0000bb;">$url = $m</span><span style="color: #007700;">-&gt;</span><span style="color: #0000bb;">utils</span><span style="color: #007700;">-&gt;</span><span style="color: #0000bb;">urlFor</span><span style="color: #007700;">(</span><span style="color: #dd0000;">'image'</span><span style="color: #007700;">, array(</span><span style="color: #007700;"> </span><span style="color: #dd0000;">'id' </span><span style="color: #007700;">=&gt; </span><span style="color: #dd0000;">'5'</span><span style="color: #007700;">,
                    </span><span style="color: #dd0000;">'image' </span><span style="color: #007700;">=&gt; </span><span style="color: #dd0000;">'10'</span><span style="color: #007700;">));</span> </code></p>
  <p>&nbsp;This line would generate a URL for an image view like /5/10 where 5 is the gallery id and 10 is the image id. In the above code, you see that the array keys match the <em>dynamic</em> parts of the route path you defined with the <em>connect()</em> method.</p>
  <p>I plan on refactoring all the websites under my control to use Horde_Routes, and I'd encourage you to take a look at the documentation at <a href="http://dev.horde.org/routes">http://dev.horde.org/routes</a>&nbsp; to learn more!</p>
  <p>Many thanks to Chuck who helped me sort out some things while working with Routes.<br /></p>