Populating Select Boxes with Tree Paths

By Ben Snider (stupergenius)
If you populate select boxes with a tree, and your tree has many nodes, which can be named the same, you are often left with an unusable select box. The following methods will help alleviate this problem by displaying the nodes in the select box as a path from the root of the tree.
To begin, your model must behave like a tree, so at the very least your model should look like:

Model Class:

Download code <?php 
class Category extends AppModel {
    var 
$name 'Category';
    var 
$actsAs = array('Tree');
?>

Next we will add two functions that will do the work of finding each tree node's path.

Model Class:

Download code <?php 
class Category extends AppModel {
    var 
$name 'Category';
    var 
$actsAs = array('Tree');
    
    function 
setTreePath(&$data$path='tree_path'$label='name') {
        if (!
is_array($data) || !in_array('Tree'$this->actsAs)) {
            return 
$data;
        }
        if (
is_array($data) && is_int(array_shift(array_keys($data)))) {
            foreach (
$data as $i=>$item) {
                
$this->_setTreePath($data[$i], $path$label);
            }
        } else {
            
$this->_setTreePath($data$path$label);
        }
    }
    
    function 
_setTreePath(&$data$pathField$label) {
        
$cats $this->getpath($data[$this->name][$this->primaryKey]);
        
$path = array();
        foreach (
$cats as $cat) {
            
array_push($path$cat[$this->name][$label]);
        }
        
$data[$this->name][$pathField] = implode('/'$path);
    }
    
}
?>

Then in our controller we can do something like the following to get a list of tree paths in the same format as find('list').

Controller Class:

Download code <?php 
function showSelect() {
    
$allCategories $this->Category->find('all', array('fields'=>'id''name'));
    
$this->Category->setTreePath($allCategories);
    
$categories = array();
    foreach (
$allCategories as $cat) {
        
$categories[$cat['Category']['id']] = $cat['Category']['tree_path'];
    }
    
$this->set(compact('categories'));
}
?>

And simply in our show_select.ctp view we write:

View Template:

Download code <?php
    
echo $form->create('Category', array('action'=>'/not/an/action'));
    echo 
$form->input('Category.Category');
    echo 
$form->end('Submit');
?>

And we should get a select box filled with our category names as paths. So, for instance, we could get 'Electronics/Televisions/LCD' and 'Electronics/Monitors/LCD', whereas before we would get multiple LCD options.

 

Comments 754

CakePHP Team Comments Author Comments
 

Comment

1 Order field

You say "at the very least" the model should include $order = 'lft ASC'; This is not true. The tree behavior does this by default, so it's not needed.

By the by, instead of doing a find('all') and passing the result to a setTreePath, why not do a find('treePath') or similar?

Thanks for sharing
Posted Jul 30, 2008 by Alexander Morland
 

Comment

2 thanks

You say "at the very least" the model should include $order = 'lft ASC'; This is not true. The tree behavior does this by default, so it's not needed.
So it does, changed that, must have been reading some old articles. :)
By the by, instead of doing a find('all') and passing the result to a setTreePath, why not do a find('treePath') or similar?
Hmm interesting. So to do that all I would do is override find() in the model, and if type=='treePath' then do my logic, else parent::find()? But, if it ain't broke, don't fix it.
Posted Jul 30, 2008 by Ben Snider
 

Comment

3 Suggested Modifications

So I took Alexander's advice and implemented the model methods as a special find(). The results are the following code.

Model Class:

<?php 
class Category extends AppModel {
    
// model stuffs
    
    
function find($type$options = array()) {
        if (
in_array($type$this->customFinds)) {
            return 
$this->{'_find' ucfirst($type)}($options);
        } else {
            return 
parent::find($type$options);
        }
    }

    function 
_findTreepath($options) {
        
// set some default options and get some from the parameters if set
        
$pathField 'tree_path';
        
$labelField 'name';
        if (isset(
$options['pathField'])) {
            
$pathField $options['pathField'];
            unset(
$options['pathField']);
        }
        if (isset(
$options['labelField'])) {
            
$labelField $options['labelField'];
            unset(
$options['labelField']);
        }
    
        
// find the specified rows and return something like find('list') does
        
$results $this->find('all'$options);
        
$return = array();
            foreach (
$results as $i=>$result) {
            
$this->_setTreePath($result$pathField$labelField);
            
$return[$result[$this->name][$this->primaryKey]] = $result[$this->name][$pathField];
        }
        
        return 
$return;
    }
    
    function 
_setTreePath(&$data$pathField$label) {
        
$cats $this->getpath($data[$this->name][$this->primaryKey]);
        
$path = array();
        foreach (
$cats as $cat) {
            
array_push($path$cat[$this->name][$label]);
        }
        
$data[$this->name][$pathField] = implode('/'$path);
    }
}
?>

And then we can use it like the following to get an array of category ids and slugs.

Controller Class:

<?php 
    $categories 
$this->Category->find('treepath', array('pathField'=>'tree_path''labelField'=>'slug');
?>
Posted Aug 26, 2008 by Ben Snider
 

Comment

4 cant edit

Can't edit my comment at the moment, but put the following in the Category model to register the findTreepath as a custom find method in the current class.

var $customFinds = array('treepath');
Posted Aug 26, 2008 by Ben Snider