CHAPTER 10

Routes

Out of the box, Cake intercepts all URLs and maps them to their appropriate controllers and actions. This is a wonderful aspect of the framework that improves the speed of developing applications. Rather than having long strings of passed variables in the URL or creating dozens of individual scripts to handle every function in the application, Cake's routing system manages all the requests, as you saw in previous chapters. But what about customizing the routing scheme? Or what if you want to generate non-HTML files such as PDFs, RSS feeds, or some kind of XML output? And what about search engine optimization? For these purposes, Cake's routing features will remove the headache of mapping URLs, handling dynamic requests, and customizing the paths and URL structure of the application.

Almost all the main routing action will take place in the app/config/routes.php file. All the routes and their configurations are stored in this file and use a specific syntax. A few default routes, which serve to handle the main URLs, are already written to the routes.php file. By default, Cake parses the passed values listed between slashes and derives a path to controllers and actions as well as passing parameters to those actions. By using magic variables, arguments, extensions, parameters, and other features, you can fully manipulate the routes to fit your application and still maintain the MVC structure.


Caution Remember that the routes.php page is cascading, meaning that the order of routes matters. If Cake resolves a URL with one route, it will immediate end there and not proceed to check other routes down the page. Although this generally doesn't affect the overall application, it can make a difference if you are working with more complex routes that occur in the same controller or action.


The Basic Route

As you can see in the default routes.php configuration file, the basic route is called as the Router::connect() function with a path as well as an array storing route parameters:

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

The previous line is the base route for the application. It connects URL elements to the Pages controller's Display action and sends the value home as a parameter to be used in the action. By specifying another controller and action, you can change the base route so that, by default, the home page is other than the Pages controller's Display action.

The first value in the Router::connect() function is the path that the routing engine has received. If it checks out, then the parameters array will be called, and the user will be taken to whatever is specified there. Another way of looking at this path is like an if-then statement; if the URL entered has no parameters but the base URL only, then pass the value home to the Display action in the Pages controller. You can specify the value by hand or use placeholders to accept all possible values for that URL parameter:

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

The previous line shows a new method for connecting with the Posts controller in your blog application. By using the asterisk in the path, you're essentially telling the router to accept and pass along anything it finds following the word articles. As in the base path example, the path will be tested like an if-then statement: if the supplied URL begins with the word articles, then pass along whatever else follows in the URL to the Posts controller. By default, the Index action will be called. You've more or less constructed an alias for the Posts controller. Now, if you wanted, you could enter all your links that point to the Posts controller as if they pointed to an Articles controller.

Arguments

Traditionally, assigning values to request variables is done in PHP by constructing a serialized string with arguments:

http://localhost/script.php?variable=value&another_var=another_val

In the previous code, the arguments in the string are variable and another_var. When a controller action interacts with a variety of arguments and combinations of possible arguments, then it may be necessary to construct the URL string differently than Cake's default route pattern.

Arguments in the router appear in the URL with a colon followed by a value:

http://localhost/blog/posts/view/id:5/set:2

In the previous code, the arguments are id and set, and their values are 5 and 2. Usually, this example would be better managed without the arguments in view, namely, with just the values. But when the action uses components such as the Paginator, arguments may need to be passed along for the component to work properly. For instance, in the case of the Paginator, normal Cake URLs are defaulted to the first page of the data set. But by adding arguments to the string, the Paginator is able to retrieve another page in the data set:

http://localhost/blog/posts/index/page:2/sort:id/direction:asc

Passed arguments are contained in the passedArgs array. The previous string would store the values like so:

$this->passedArgs = Array(
    'page'=>2,
    'sort'=>'id',
    'direction'=>'asc'
);

Arguments are passed to the controller as keys and values in this array unless it is bypassed in the routes.php configuration file.

One important use for arguments is as Cake's way of passing variables to the actions. Notice that because the router has found arguments in the URL string, it does not pass them as variables in the action:

http://localhost/blog/posts/view/5/comments:false

The previous URL would not result in the second parameter (comments:false) being passed in the action, like the first parameter:

function view($id=null) {
    $displayComments = $this->passedArgs['comments'];
...

This distinction between typical action parameters and passed arguments can make a difference when constructing methods in the controller. The action can still execute without any passed arguments, which can be an important option depending on the specific needs of the action.

Reverse Routing

Web applications have begun to prefer friendly URLs to convoluted URL strings. URLs like %www.site.com/cart/item/race_car work better, for example, than %www.site.com/index.php?page=cart&item=race_car. This is happening not just for search engine optimization purposes or to make the application more accessible for users but to facilitate simple changes to the overall routing of the application. Consider the difficulty, if a web application were built on GET variables, in revamping its entire routing structure. New problems would present themselves in maintaining legacy URLs and at the same time in implementing new ones. Much would depend on the application itself, but in general, it would take some added functions to reverse URLs.

Lookups

By default, URL lookups run through the router in one direction, meaning that when a link is clicked, the router performs a lookup and maps the appropriate controller, action, and parameters. Entering /posts/view/17 will cause the router to look up where the Posts controller's View action is in the application and pass the parameter to it. But what about lookups going the other way? Suppose you wanted to write a link and have the router look up how to construct the link. This would be a reverse lookup, or a lookup in the reverse direction.

Rewriting URLs in the Router

In the blog application, suppose that at some future date you needed to stop using the Posts controller name in the URL. This would mean going through the entire application changing every instance of a link to the Posts controller to the new route. That would obviously slow down development and make it harder to change the structure of the site. Or, you could use Cake's reverse routing mechanism to rewrite all URLs pointing to the Posts controller.

Verbose Linking

To reverse a route, you first must use the verbose method for writing links. Instead of writing a path string in the $html->link() function, you include some URL parameters in an array like this:

$html->link('View Post',array('controller'=>'posts','action'=>'view',$postimage
['Post']['id']));

This array does not tell the function what the URL path is but rather gives it the necessary parameters to construct the path dynamically. Although this array does result in more characters being entered to put a link together, it allows you to use the router to intercept any links in the entire application in one place, the routes.php configuration file, and not have to go back and find links that needed changing.

To complete the reverse route, you need to enter only a new connection string in the routes.php configuration that changes the route:

Router::connect('/articles/*', array('controller'=>'posts','action'=>'view'));

Without using the verbose array in the $html->link() function, the router would still try to access the Posts controller and the word posts would still appear in the URL. However, now that you've specified that you want the word articles to link up with the Posts controller, the router will construct the paths in the application for you. Wherever links call for both the Posts controller and the View action, the router will substitute articles in the path. For example, the link that would normally point here:

/posts/view/25

will be dynamically changed by the router to point here:

/articles/25

Remember that for reverse routing to work across the whole application, you will need to use verbose linking consistently.

Admin Routing

Many applications require some kind of administrative area to manage web site functions with a user interface. Making entirely different controllers to manage these administrative features would contravene Cake's conventions. But how do you distinguish between front-end users and site administrators in building the application? The router can dynamically manage admin routing for you, which means you can make some actions available only to an administrator while the URLs still follow the Cake standard structure.

Rather than build your own controller to manage administrator actions, like so:

posts_admin_controller.php

you can point all links to the following to the Posts controller:

http://localhost/blog/admin/posts

First you must choose an admin prefix and then name your actions and view files accordingly.

Choosing an Admin Prefix

The admin prefix can be any one word you like. Most use admin (as in the previous example), but you could make it webmaster or superuser if you like. The admin routing must be turned on in the core configuration, and the prefix must also be provided there. Open the app/config/core.php file, and scroll down to the admin routing preference:

Configure::write('Routing.admin', 'admin'),

Around line 69, you should see the preference, commented out; uncomment it to activate admin routing. Notice that by default, the prefix is set to admin. You can enter any string here that meets standard HTTP URL syntax as a prefix. The router will map this prefix to the appropriate admin actions.

Linking Admin Actions and Views

Once the prefix is defined in the core configuration, you can begin to create administrator actions. These actions may appear in any controller; they only need to be named with the prefix included. Suppose you wanted to change the Edit and Add actions in your Posts controller to be accessible via an admin route. You would need to rename the actions in the controller to admin_edit and admin_add, respectively.

The prefix is attached to the name of the action with an underscore. So, if the prefix were defined in the core configuration as superuser, then these Edit and Add actions would need to have the superuser prefix added to the name:

function superuser_edit($id=null) { ...

When some actions are changed to be accessed through the admin route, these actions' views will need to be renamed as well. The view files need to be named to link with the corresponding actions. So, the admin_edit action will need its view file to be named admin_edit.ctp.

These admin actions are not called in the URL with the underscore. For example, the Admin Edit action is not executed by typing this:

http://localhost/blog/posts/admin_edit/16

but by using the admin prefix before the controller name:

http://localhost/blog/admin/posts/edit/16

To restrict these admin routes, an authentication system must be built to check users against the database and grant them privileges. At least with this routing mechanism you can create the necessary paths to differentiate between administrators and front-end users.

Baking Admin Routes

The Bake script automates the process of building admin routes. When creating controllers or views with Bake and using the "build interactively" feature, you can enter options to build the actions and views with admin routing. In other words, simply tell Bake when it asks if you want admin routing what the admin prefix is, and it will not only build the typical CRUD actions and views but will do so with the admin prefix added to action names and view files.

Route Parameters

You may need to specify which sections of the URL correspond to parameters in the controller. With route parameters, you can tell the router how to parse the URL. Consider the Posts controller. You may want a particular route to be called only when this controller is entered in the URL. But what about routes that are called for certain actions without using the controller name? In this scenario, you would need to specify a route parameter for the controller; in other words, the router will need to know that the first parameter in the path represents the controller and the other parameters represent other elements. For example, if you wanted to affect the View action for all controllers, you could enter a route along these lines:

Router::connect('/:controller/view',array('action'=>'read'));

Here, whenever a View action is called, it will point to the Read action instead. You've used the route parameter :controller to tell the router that the first part of the path represents the controller to map.

Route parameters may be anything as long as they are one word, are preceded by a colon, and are defined in the parameters array. They can be checked by magic variables or regular expressions for the route to be activated. Route parameters do not need to be separated by slashes. Symbols such as slashes, dashes, colons, and ampersands can be used to separate parameters since the router can detect the route parameter.

Magic Variables

What about passing variables through the routing engine? You can accomplish this in part with magic variables. These include common action names, date elements, and ID numbers. In other words, the router will automatically detect months and years in the URL if you want and will determine where to map those types of requests.

For instance, most blog applications manage their posts by date. To do this, you'd need to use magic variables in the Router::connect() function:

Router::connect('/:controller/:month-:day-:year', array('action'=>'view',array(image
'month'=>$Month,'day'=>$Day,'year'=>$Year)));

You can see that in the path you've entered some placeholders—specifically a controller, month, day, and year placeholder between each set of slashes. These are "magic variables," and they always begin with a colon. In other words, you've defined the slots in the URL and have given them a name. The first one will contain the controller name, the second will contain a month value, and so forth. The router will check these slots against the magic variables used in the parameters array, and if they return true, it will pass them along to the specified controller's View action.

Values entered in the place of magic variable slots in the URL are passed to the action through the $this->params array. In the previous example, the given month would be available in the array as this:

$this->params['month']

and so forth. The $this->params array may be checked or called in the controller and the view.

The available magic functions include the following:

  • $Action
  • $Day
  • $Month
  • $Year
  • $ID

Each will automatically determine whether the supplied value checks out. For example, the $Action magic variable checks for common action names such as View, Delete, Edit, and so on. The $Year magic variable checks for a typical year value, and $ID checks for a valid whole number.

Using this route in the blog application, the following URL:

http://localhost/blog/posts/12-24-2008

would format the $this->params array like so:

$this->params = Array(
    'month' => 12,
    'day' => 24,
    'year' => 2008,
...
);

Custom Expressions

Using regular expressions in conjunction with route parameters, custom logic tests can be performed before a route is mapped. These are specified in the same array where magic variables are used.

Router::connect('/:controller/:id/:month-:day-:year',array('action'=>'view'),image
array('month'=>$Month,'day'=>$Day,'year'=>$Year,'id'=>'[d]+'));

Here the ID is tested in the route by use of the :id route parameter and the regular expression definition in the parameters array. If the ID slot contains a value that passes the regular expression test [d]+ (and the other slots pass as well), then the route will be mapped. If the ID slot does not contain a valid integer, then the route will be bypassed, and the router will move on to the next definition in the routes.php configuration file.

The Pass Key

So far, any values in the URL that are passed by a route that uses a nondefault method are placed in the $this->params array; arguments are placed in the $this->passedArgs array. But what about mapping values so that they are passed to the action as a typical parameter? The Pass key allows you to accomplish this. In the mapping array (which contains the controller and action definitions), add the Pass key with an assigned array. Then, provide values in the array that correspond to the route parameters you are using. For instance, the route with parameters title and id can have their values appear in the action like normal Cake parameters like so:

Router::connect('/posts/:title/:id',array('controller'=>'posts','action'=>'view',image
'pass'=>array('title','id')));

The router will now pass the values contained in the :title and :id slots to the action in the function definition:

function view($title=null,$id=null) { ...

This is beneficial because it can save time; you won't have to work through the $this->params array to work with passed values.

Parsing Files with Extensions Other Than .php

As of yet you have mapped only routes to controllers and actions in Cake. What about other file types such as RSS or XML files? Because of how Cake's .htaccess files are constructed, the server will supply files when they are directly referenced and available on the server. So, you can feasibly write static files such as RSS feeds, PDF files, or XML files and place these files in the app/webroot directory, and they can be accessed directly by entering the file name in the URL. But the router can also parse extensions, which makes it possible to use Cake to dynamically create and render non-HTML file types.

The Process

Understanding the process of how the router parses extensions is key to building the right route configurations and other Cake elements. First, you need to tell the router to look out for any URLs that point to a specific extension. Let's say you want to dynamically build an RSS feed for the blog application. Then you would need to use the Router::parseExtensions() function for the router to detect when the .rss extension is called in the URL:

Router::parseExtensions('rss'),

Next, you need to tell Cake what kind of MIME type this extension is. Some extensions already get recognized by default, but others need to be defined. In the case of the RSS extension, Cake won't need the MIME type to be defined, but to see how this is done, open the App controller and add these lines:

var $components = array('RequestHandler'),
function beforeFilter() {
    $this->RequestHandler->setContent('rss','application/rss+xml'),
}

Now Cake can respond to the browser and the HTTP request with the correct MIME type definition. But the last step is to create the layout to wrap the standard file contents around the view. Once the layout is defined correctly, the controller can include actions that output RSS in the view rather than HTML, and the browser will recognize the route as a file even though Cake is dynamically creating it.

Creating the RSS Feed

Knowing the process for the router to parse the RSS extension, let's build an RSS feed into the blog. First, write the parse extensions function in the routes.php configuration file near the top:

Router::parseExtensions('rss'),

Next, run the RequestHandler component in the App controller to filter HTTP requests through the controller.

Listing 10-1 contains all the contents of the App controller. Notice that this uses the beforeFilter() function to run the RequestHandler component's setContent() function before every controller action is called.

Listing 10-1. The App Controller with the RequestHandler Component in Use

1    <?
2    class AppController extends Controller {
3        var $helpers = array('Html','Form','Ajax','Javascript','Blog'),
4        var $components = array('RequestHandler'),
5
6        function beforeFilter() {
7            $this->RequestHandler->setContent('rss','application/rss+xml'),
8        }
9    }
10    ?>

Now that the router and the App controller can handle the HTTP requests that contain RSS file extensions, you need only to create the necessary layouts and views to generate the feed.

Extension Layouts

For the RSS extension, the router will look to the app/layouts/rss directory to find the default RSS layout. Create the default.ctp layout file, and place it in this directory. Paste into this new file the contents of Listing 10-2.

Listing 10-2. The File Contents of app/layouts/rss/default.ctp

<?=$rss->document(null,$content_for_layout);?>

With the help of the built-in $rss->document() function, you don't have to worry about knowing the format of a standard RSS file. In the event that you use a file type not included in Cake's helpers, you would have to supply the proper headers yourself; of course, this will depend on the appropriate syntax and specifications of the file type in question.

The Controller Action

Now, all you have to do is create an action to act as the file name for the parsed extension. For the blog, you will have the RSS feed be accessible as follows:

http://localhost/blog/posts/feed.rss

So, let's create the Feed action in the Posts controller (see Listing 10-3). You should not append the extension in the function definition. This action will be like the Index action in that you want to provide only the most recent five posts for the feed.

Listing 10-3. The Feed Action in the Posts Controller

1    function feed() {
2        $this->set('posts',$this->Post->feed());
3    }

Notice that line 2 of Listing 10-3 references a model function named feed(). Since the RSS output will require an array formatted with keys and values matching a specific syntax, you ought to let the model handle the data. In the Post model, paste in the feed() function shown in Listing 10-4.

Listing 10-4. The Feed Function in the Post Model

1    function feed() {
2        $posts = $this->find('all',array('order'=>'date DESC','limit'=>5));
3        $out = array();
4        foreach ($posts as $post) {
5            foreach ($post as $key=>$val) {
6                if ($key == 'Post') {
7                    $out[$val['id']]['pubDate'] = date('D, d M Y H:i:s +O',image
strtotime($val['date']));
8                    $out[$val['id']]['title'] = $val['name'];
9                    $out[$val['id']]['description'] = $val['content'];
10                    $out[$val['id']]['content'] = $val['content'];
11                }
12            }
13        }
14        return $out;
15    }

Line 2 of Listing 10-4 retrieves the five most recent posts and places them in the $posts array. The rest of the function cycles through this array and pulls out only the parts you want to appear in the feed. Notice how lines 7–10 name the key in the output array after the actual tag names that will appear in the RSS feed. This is because once you code the Feed view, you'll use the RSS helper to render the tags. The RSS helper will need the array to be formatted this way to fetch the right data for each tag.

Now that the data is handled correctly and the layout is working right, all you have left is the Feed view.

The Feed View

Finally, create the Feed view to match the action (see Listing 10-5). It will need to use the RSS helper to render the various tags, so be sure to include this helper in the Posts controller or App controller (see Chapter 9 for more information on including helpers in the controller). Notice that the Feed view file is stored in a folder in the Posts views folder named rss.

Listing 10-5. The app/views/posts/rss/feed.ctp File Contents

<?=$rss->channel(null,array('title'=>'Extensive Blog','description'=>'My Blog',image
'language'=>'en-us'));?>
<? foreach($posts as $post): ?>
<?=$rss->item(null,$post);?>
<? endforeach;?>
</channel>

The RSS helper here takes care of building the channel and the item tags automatically. You've passed the $post array that has been formatted correctly in the Post model, and now the process is complete. Open the new feed in any news aggregator, or use your browser to subscribe to the feed. You should end up with a valid RSS feed instead of an HTML page thanks to the router parsing the extension and the controller handling the request properly.

Custom Routes Exercise

This chapter explored some of the methods for building custom routes in Cake. For this exercise, construct a route that will display blog posts by their date and ID. Providing the correct route configuration is only half the problem; be sure to rebuild the View action to run the lookup correctly. Also build the admin route to handle editing and adding blog posts. You have completed the exercise correctly if posts are editable by accessing http://localhost/blog/admin/posts/edit/{id} rather than with the standard path to edit and add.


Summary

Rewriting URLs for the application is made much easier by editing Cake's routing configuration file. Not only can you make URLs friendlier with the app/config/routes.php file, but you can even set up reverse routes, or routes that are dynamically mapped by the dispatcher; just use verbose linking syntax when creating links, and Cake will automatically change all URL strings for any controllers and actions you specify. The router also includes an admin routing feature with which you can attach a prefix to any action name and view file, and Cake will map URLs containing the prefix to these files. This is useful for distinguishing between CRUD actions that should be accessed only by an administrator and those that are available to a public user. For applications that create output as non-HTML files, the dispatcher can parse extensions. For instance, in this chapter I described how to use Cake to generate an RSS feed as an .rss file. When a URL containing the .rss extension is entered, the router detects this extension and maps the request accordingly. Now that you understand how to customize URLs and other paths in Cake, it's time to explore more of Cake's advanced features. In Chapter 11, I'll explain components and utilities, which can dramatically enhance your Cake application and cut down on several web-related tasks.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.39.93