Permanent appended named parameters in routes with CakePHP

For a project i’m working on i needed a bit of custom routing.
Basically the project is a shop where you can edit multiple events and i stored the current event in a session.
But the request came if the client could edit multiple events at the same time.
Long story short, i needed the event_id somewhere in the url and this is where the router comes in to play.

There are a couple of ways to get the event_id in the url with custom routing.

Option one

Use a named parameter

this method will append event_id:[UUID] to the url.
for example: http://www.example.com/products/index/event_id:490ed5ab-ed6c-4034-8205-0264d5453078

in /app/config/routes.php

Router::connectNamed(array('event_id'=>$UUID), array('default' => true, 'greedy' => false));

This will let the router know that we have a named part called event_id.
It will also check if event_id is an UUID (other possibilities are $ID, $day, $month and $year, or a regular expression).

Helper

This was the easy part, because you need to tell all the helpers that create an url to use ‘event_id’=>’[UUID]‘.
In my case i already wrote an app and i didn’t feel like rewriting all links and forms, so i created a file called app_helper.php in the app folder like so:

/app/app_helper.php

<?php
class AppHelper extends Helper { 
        function url($url = null, $full = false) { 
                if (is_array($url)) { 
                        if (empty($url['event_id']) && isset($this->params['named]['event_id'])) { 
                                $url['event_id'] = $this->params['named]['event_id']; 
                        }
                } 
                return parent::url($url, $full); 
        } 
} 
?>

What this does is append the event_id to all url arrays and then calls the parent url function (in /cake/lib/helper/helper.php).
Because all helpers extend AppHelper, all url calls will go tough the above function.

Redirect

Now we only need to address one more issue, namely the $this->redirect(); function in the controllers, because we want the event_id appended to those url’s too.

/app/app_controller.php

function redirect($url, $status = null, $exit = true) { 
 	if (is_array($url)) { 
        if (empty($url['event_id']) && isset($this->params['named]['event_id'])) { 
        	$url['event_id'] = $this->params['named']['event_id']; 
        }
 	}
    return parent::redirect($url, $status, $exit); 
}

All controllers extend app_controller so they will all use this new redirect function.

So once you add event_id:[UUID] to an url, it will always be appended.
the event id can be retrieved in all controllers by calling $this->params['named']['event_id'] or $this->passedArgs['event_id']

Option two

The other way to do this is by changing a lot of routes in the routes.php file, but the url will be prettier (like: http://www.example.com/event_id/controller/action/params)

(/app/config/routes.php)

/*
	 * The following section is to add the event_id to each backend url
	 * The event_id is added in the url method of app_helper.php and redirect method of app_model.php
	 */
	Router::connect('/:event_id/:controller', 
			array('controller'=>':controller'), array('event_id'=>$UUID));

	Router::connect('/:event_id/:controller/:action', 
			array('controller'=>':controller', 'action'=>':action'), array('event_id'=>$UUID));

	Router::connect('/:event_id/:controller/:action/:id', 
			array('controller'=>':controller', 'action'=>':action', 'id'=>':id'), array('pass'=>array('id'),'event_id'=>$UUID));

	Router::connect('/:event_id/:controller/:action/:id/*', 
			array('controller'=>':controller', 'action'=>':action', 'id'=>':id'), array('pass'=>array('id'), 'event_id'=>$UUID));

	Router::connect('/:event_id/:controller/:action/*', 
			array('controller'=>':controller', 'action'=>':action'), array('event_id'=>$UUID));

I’m not shure if this is the most efficient way to cover all routes, but if you replace the code from the first block with this code, you get the nice url (http://www.example.com/event_id/controller/action/params).

The last array makes shure only uuid’s are used by the router.

Filed Under: News - read on

Cakephp routing: prefixes and forms

Prefixes are a nice way to define routes for groups of actions, (see the cookbook).

All admin_ actions go with the /admin// url
For my project i needed both an admin route and a dashboard route, so i made prefixes for them like:

(/app/config/routes.php)

Router::connect('/dashboard/:controller/:action/*', array('prefix' => 'dashboard'));

Now in the view you can use the following:

echo $html->link('Profile', array('controller'=>'users', 'action'=>'dashboard_profile'), array('title'=>'Edit profile'))

But when it comes to forms things start to break.

For example the following:

echo $form->create('User', array('action'=>'dashboard_profile'));

or this one:

echo $form->create('User', array('url'=>array('controller'=>'users', 'action'=>'dashboard_edit')));

generate the following (wrong) link: /users/dashboard_profile/1

While it does point to the right action (dashboard_profile) you get an error message explaining that this is a private action. The only way to use this private action is to use the prefix method.

The reason for this is that the form generate part in the cake code doesn’t do anything with the routing, thus ignoring the prefix we’ve set.

After some digging in the api i found a cake-way to generate the right url:

echo $form->create('User', array('url'=>$html->url(array('action'=>'dashboard_profile'))));

We use the html helper, that does parse the prefix part and generates a nice correct url, namely: /dashboard/user/profile

The html helper does check for any prefixes in the routing and generates the right url.
Combining the form->create and the html->url we get the right url.

Filed Under: CakePHP, Code, English - read on