
Custom route class
Although the base routing system provided with CakePHP will cover almost all cases, there may be times when you need to handle things which are slightly more complex. For this reason, the framework provides the option to define a custom route class, which processes more complicated scenarios.
In this recipe, we'll create a custom route class to handle news headlines for given year and month.
Getting ready
For this recipe, we'll need to create a custom route class to use in our routing. Therefore, create a file named HeadlineRoute.php
in app/Routing/Route/
with the following content:
<?php App::uses('CakeRoute', 'Routing/Route'); App::uses('ClassRegistry', 'Utility'); class HeadlineRoute extends CakeRoute { }
We'll then need some data to work with. So, create a table named headlines
using the following SQL statement:
CREATE TABLE headlines ( id INT NOT NULL AUTO_INCREMENT, title VARCHAR(50), year SMALLINT(2) UNSIGNED NOT NULL, month SMALLINT(2) UNSIGNED NOT NULL, PRIMARY KEY(id) );
After creating the table, run the following SQL statement to insert some headlines:
INSERT INTO headlines (title, year, month) VALUES ('CakePHP on top', '2013', '11'), ('CakeFest 2014', '2014', '08'), ('CakePHP going strong', '2014', '08');
We'll also need to create a file named HeadlinesController.php
in app/Controller/
and add the following content to it:
<?php App::uses('AppController', 'Controller'); class HeadlinesController extends AppController { }
Finally, create a listing.ctp
file in app/View/Headlines/
for our view.
How to do it...
Perform the following steps:
- Add the following code to the
routes.php
file inapp/Config/
:App::uses('HeadlineRoute', 'Routing/Route'); Router::connect('/:year/:month', array( 'controller' => 'headlines', 'action' => 'listing' ), array( 'year' => '[0-9]{4}', 'month' => '[0-1]{0,1}[0-9]{1}', 'routeClass' => 'HeadlineRoute' ));
- Edit the
HeadlineRoute.php
file inapp/Routing/Route/
and add the followingparse()
method to it:public function parse($url) { $params = parent::parse($url); if (empty($params)) { return false; } $cacheKey = $params['year'] . '-' . $params['month']; $headlines = Cache::remember($cacheKey, function () use ($params) { return ClassRegistry::init('Headline')->find('all', array( 'conditions' => array( 'Headline.year' => (int)$params['year'], 'Headline.month' => (int)$params['month'] ) )); }); if (!empty($headlines)) { return $params; } return false; }
- Define the following
listing()
method in theHeadlinesController.php
file:public function listing() { $year = $this->request->params['year']; $month = $this->request->params['month']; $headlines = Cache::read($year . '-' . $month); $this->set(compact('year', 'month', 'headlines')); }
- Add the following content to the
listing.ctp
file:<h2><?php echo __('Headlines for %s/%s', $year, $month); ?></h2> <?php $list = Hash::extract($headlines, '{n}.Headline.title'); echo $this->Html->nestedList($list);
- Finally, navigate to
/2014/08
in your browser to see the headlines for that month. This is shown in the following screenshot:
How it works...
For this recipe, we first created a custom route class to handle our routing logic. This class was created in a file named HeadlineRoute.php
, which is located in app/Routing/Route/
. There is no strict convention for the naming of route classes and they don't need to align with any controller or model, but it's best to name them wisely so that it's evident what their function entails. In these calls, we defined a parse()
method, which processes the route. Note that the HeadlineRoute
is only actioned when the /:year/:month
template we defined matches. In this case, we also added some validation to our route, making sure that the year and month are valid for our request. The routeClass
then defines that we want to use our custom route class for these routes. This is where our parse()
method comes into play.
For our routing logic, we first resolve the parent::parse()
call to obtain the underlying routing, and then check that we're in a valid route. We then build a cache key from the year
and month
values passed from our template in the request. Then, we use this key to call Cache::remember()
, passing it an anonymous function that initializes the Headline
model by calling the ClassRegistry::init()
method and returns the headlines based on the values provided from our route. This greatly improves our application performance, as we will have the results cached for subsequent requests for the same year and month. If there are headlines, we return the $params
array to signal that our route class has matched successfully. To the contrary, we return false
, which means the route class did not resolve a possible resource.
When the route is successfully matched, we had defined in our configuration array for the request to be routed to the listing()
action of the HeadlinesController
. In this method, we only used the year
and month
values provided in the CakeRequest::$params
array to construct our cache key and read the headlines our custom route class previously stored for us. Then, we just set those values as view variables using the set()
method of our controller and used the compact()
function to generate the key => value
array from our variables.
Our view is just as simple. We created a header by using the $year
and $month
values passed via the URL, and we also processed the headlines to generate a list. For this, we first used the Hash::extract()
method to parse our array of records to return an array of headline titles, and then we used the nestedList()
method of the Html
helper to build our HTML list.
There's more...
So far, we've successfully set up the logic to resolve the URL to a collection of headlines. However, it would now be ideal to also define the logic for reverse routing. This is where we generate a link based on the values of a headline.
For this, we want to be able to simply create a URL from the unique ID of a headline to generate the required path for the month's headlines. For this, we'll add a match()
method to our HeadlineRoute
class with the following logic:
public function match($url) { if ($url['controller'] === 'headlines' && $url['action'] === 'listing' && !empty($url[0])) { $headline = ClassRegistry::init('Headline')->find('first', array( 'conditions' => array( 'id' => $url[0] ) )); if (!empty($headline)) { $url['year'] = $headline['Headline']['year']; $url['month'] = $headline['Headline']['month']; unset($url[0]); return parent::match($url); } } return false; }
In the preceding code, we've first checked that the URL configuration specified the controller
as Headline
and the action
as listing
, with the ID of the headline. We then instantiate the Headline
model via ClassRegistry::init()
method, using the ID provided. If a record is found, we set the year
and month
keys in the URL configuration and remove the ID. Finally, we call the parent::match()
method to complete the translation of the route. To see how it works, add the following line to the app/View/Headlines/listing.ctp
file:
debug(Router::url(array('controller' => 'headlines', 'action' => 'listing', 1)));
It will output matching the /2013/11
URL.
See also
- The View caching recipe from Chapter 10, View Templates.