Friday 18 April 2008

CakePHP Tree Behaviour

Its often really useful to be able to represent data in a tree, or self-referential way - like product categories, music genres, organisational structures etc. Often, that takes quite a bit of code. However thanks to CakePHP, we can do it in far less lines of code, thanks to a behavior var $actsAs = array('Tree');.

First create a db table as normal with an id and title. Then add three special fields: 'parent_id', 'lft' and 'rght' - all Integers. Cake uses these to manage the hierarchical relationships between each row, and it does this all behind the scenes. Next, add the behaviour tag to your model. My example is for Genres. and its as easy as this... in genre.php...


var $actsAs = array('Tree');


Some of the clever stuff happens in the Controller to make nice URL's so lets look at that now.

What I want is a URL structure like this www.example.com/genres/genre1/genre2/genre3 etc where genre 3 is the child of genre 2 which is the child of genre 1 etc. To get this behaviour, we first add a line to our app/config/routes.php file.


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


Its a very simple line which routes any sub URL to the index action of the genre controller. Then we can do all the goodness there. So, in our genres controller, we need to get to work building a clever index action.

First, we get the current URL, split it into an array on every slash using explode. If there are no other URL paths, we assume we're at the route and list all top level genres accordingly. If there are other paths, then we can lookup current genre (the last element in the array) from the database.


function index(){
$url = $this->params['url']['url'];
$genreUrl = explode('/',$url);
if (count($genreUrl)>1){
$genreCurrent = $genreUrl[count($genreUrl)-1];
if($genre = $this->Genre->findByTitle($genreCurrent)){
pr($genre);
}else{
$this->Session->setFlash('There such genre as ' . $genreCurrent);
$this->redirect(array('action' => 'index'));
}
}else{
$genres = $this->Genre->findAll("parent_id IS NULL");
pr($genres);
}
}


There are some problems with this so far. It only works if all your genre names are unique, and worse, allows you to specify any path (like www.example.com/genres/blah/this/is/wrong/validGenreName) where validGenreName is valid at any level, but the path to it does not reflect it's true higherachy. To fix this, we're now going to parse every genre given in the URL, and check to see if the one before it, and the one after it in the DB match those in the URL. This way we can ensure the URL is a true reflection of the database structure.

I've got to go now, but I'll come back and finish this post when I get another chance.

There's more info on Tree Behaviour in the CakePHP 1.2 manual.

1 comment:

Parijatha Kumar said...

Please post the nest part also.