Monday, November 24, 2008

Advanced Cake Routing: Dynamic Routes...

I've been working on integrating Wildflower CMS with a number of my sites, however there has been one issue that I've had with how cake routing was setup.
Router::connect('(?!' . $admin . '|' . $prefix . '|login|contact)(.*)', array('controller' => 'wild_pages', 'action' => 'view', 'plugin' => 'wildflower'), array('$2'));
If you notice this route will basically connect everything that is not admin, prefix, login, or contact with the generic Wildflower view controller.

Advantage:

The advantage of having a connect string like the one above is so that we can have dynamic content directly after the URL in the site. Normally the dynamic content would point to something like /pages/name-of-the-article, so the link would look like:
http://localhost/pages/name-of-the-article
However with the route above, everything that is not defined will point to the pages controller, allowing you to skip the /pages/ in the URL. For example, the url above can now be written:
http://localhost/name-of-the-article
This makes it nicer when dealing with dynamic content...

The Problem:

Since Wildflower is a plugin, it is an addition to my normal application. Let's say my normal application has 2 controllers: foo_controller and bar_controller.

Normally in CakePHP, the routes for foo and bar would be automagic. This means that by default if I point my browser to http://localhost/foo/index it will automatically load the controllers/foo_controller.php, and look for the function index() definition. Otherwise it will throw an error.

With the route above, my "foo" controller will not load unless I specifically define a "foo" route to point to the foo controller before the Wildflower route:

Router::connect('/foo/*', array('controller'=>'foo', 'action'=>'index');

Router::connect('(?!' . $admin . '|' . $prefix . '|login|contact)(.*)', array('controller' => 'wild_pages', 'action' => 'view', 'plugin' => 'wildflower'), array('$2'));
Well I don't want to have to define 1 or more routes for every controller I build in my application. I want the automagic to still work.

Solution:

There is a solution for this problem. It is a bit tricky, but Cake PHP is flexable enough to handle it. The solution is to grab only a list of the pages that you want available for the routing, and only assign those routes. This will remove the "catch-all" route from above and allow us to create the normal automagic routes for my application.

But how do you get a list of the pages from the database when the routes have no access to the databse when they're called?

The answer is the object method: requestAction(). requestAction can be called anywhere in the system and it can call any current route. The results of the requestAction call is an array of returned results. Since the router extends object, it has access to call it, and therefore can gain access to dynamic data.

Here's an example:
// Set a temporary route
Router::connect('/pages/get_root_pages', array('controller' => 'wild_pages', 'action' => 'get_root_pages', 'plugin' => 'wildflower'));
// Now request that temporary route.
$root_pages = Router::requestAction('/pages/get_root_pages');
// Loop through the root pages and manually define the pages.
foreach($root_pages as $i => $page) {
Router::connect('/' . $page['WildPage']['slug'], array('controller' => 'wild_pages', 'action' => 'view', 'plugin' => 'wildflower'));
}
The next step is simply to add function get_root_pages() in your pages controller. It would simply find all the active pages with a parent == 0.

The disadvantage of this method is that the Cake system is called 2 times for every request, however this can be alieviated with Caching. If you store the results of root_pages in the /tmp folder, you can build a caching system that will check to see if the file is outdated, and only load the requestAction() on such an occasion. Of course then you would store the file again.

Summary:

With this solution, you will not only have root level slugs (i.e. http://localhost/this-is-my-page) but also you will not need to modify routes every time you add a new controller to your system. The only disadvantage would be if a slug is called the same thing as your controller. In that case you simply decide ahead of time which has the priority, and the route will hit the first one that is decalred.

Links:

Learn more about Wildflower CMS.

New Direction for the Blog (CakePHP)

For the past year I have been developing nearly exclusively in CakePHP. If you haven't heard of it yet, there are still a few seats on the bandwagon. Feel free to hop on board.

As with any large scale application framework, learning the tricks of the trade is important. While there are plenty of blog sites out there, everyone comes across different problems and solutions.

Therefore, I am resurrecting this blog and I'm going to use it for CakePHP. Hopefully this blog will become a valuable resource for anyone wanting to learn Cake PHP, get tips on the best practices, etc.

I hope this is helpful for you.

Sunday, November 23, 2008

Understanding Cake Routes

Cake Routes

What is a Route? Put simply, a route is a path from one point to another. In the world of networking, routes are built in routers to tell packets where to go to get to their destination. In the same way routing in Cake PHP is a set of rules that help a web browser display the right web page.

How do Cake Routes Work? Without exploring the 1000+ lines of code in the routes.php class, routes work by parsing the URL of the web page that you just requested. Your web browser sends that URL to the CakePHP Application. The Cake PHP application will then load your list of routes that you've defined.

Routes work by going Top-Down and looking for the first / best matching path. When it finds a match, the router ignores any additional routes that exist and it returns the routing information that decides which part of your application to load.

Why should we use routes? Let's say you have a giant application with tons of features, addons and tables. Now you need to load a single page. How does your application know just what files to load for this page without loading the entire application into memory? You could have an index.php file that contains a giant switch case with all the possible combinations of actions you have available in your system, however management of this becomes a nightmare. How will you determine just which classes and which files to load in a neat, managable, and expandable way?

Cake Routing is the answer. A Cake route connects a url path to a MVC Controller. That's it. Complete management in a few single lines of code. When the connection to the route is made, it will only ever load 1 controller. That controller decides which models (database tables) that it will be accessing along with which templates and layouts will be displayed. You never load anything that you don't need in that specific controller, which cuts down on memory consumption and helps the environment. Well not the environment so much, but it does cut down on needless hair loss.

Let's look at some examples:

Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home'));

The first route above does the following:
If the first thing I find after the URL is simply a "/" (which is usually always the home page) then we want to open up the pages_controller.php file in the app/controllers/ directory, and then look for a function called "display". Because "home" is stuck at the end of the array, it will pass the word "home" to that controller.

If it does not find /app/controllers/pages_controller.php, it will look in the cake core for the pages_controller (which is in cake/console/libs/templates/skel/controllers). If it can not find it there, cake will display an error.

Cake Routes also allow wildcards. This means that it will match everything. Here is an example:

Router::connect('/pages/*', array('controller' => 'pages', 'action' => 'display'));

This route does the following:
Any time there is a /pages/ after the URL, this route will kick in. It expects to have something after the pages/, and that something can be anything. For instance, if you had (http://url.com/pages/this-is-my-favorite-page), this route will kick in and it will send you to the pages controller, using the function display().
Once you get the hang of routes, there is quite a bit you can do with them. They can be customized in many ways.

Tips
  • Remember that routes work from top to bottom. Place your most exact matching routes at the top, then less exact routes farther down.
  • You can test routes by creating a file called "app/app_controller.php". Inside of it add the following lines:
  1. function __construct() {
  2. $route = Router::currentRoute();
  3. pr($route);
  4. parent::__construct();
  5. }
With this, you can test different URLs, and this code will print out the route that it the URL matches.

Note: the pr() should be commented out when you are not testing routes.
  • You can loop Route::connect commands in a foreach() statement to shorten the amount of code that you have. This helps for large administration pages.
Links:

You can read more about routes here.

Example routes can be found here.