CakePHP 2 Application Cookbook
上QQ阅读APP看书,第一时间看更新

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:

  1. Add the following code to the routes.php file in app/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'
    ));
  2. Edit the HeadlineRoute.php file in app/Routing/Route/ and add the following parse() 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;
    }
  3. Define the following listing() method in the HeadlinesController.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'));
    }
  4. 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);
  5. Finally, navigate to /2014/08 in your browser to see the headlines for that month. This is shown in the following screenshot:
    How to do it...

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.