Threaded Lists
Ever needed a tree of sections with an unlimited depth? Here's a quick guide to findAllThreaded().
Why would I need this?
Let's say you wanted your website organised into sections like this:- [li] Art
- [li] Film [li] Music
- [li] Jazz [li] Pop
- [li] Archaeology [li] War
- [li] Biology [li] Chemistry [li] Physics
- [li] Computing
- [li] Hardware [li] Software
- [li] Film [li] Music
Partial table example
id name parent_id
----------------------------------------
1 Art 0
2 Music 1
3 Pop 2
Notice how top level sections have a parent_id of 0.That's just what I need! What do I do now?
I thought you'd never ask. First you need to make a table in your database. I'm going to call this 'sections':SQL:
CREATE TABLE `sections` (
`id` int(11) NOT NULL auto_increment,
`name` varchar(255) NOT NULL,
`parent_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `sections` (`id`, `name`, `parent_id`) VALUES
(1, 'Art', 0),
(2, 'Film', 1),
(3, 'Music', 1),
(4, 'Jazz', 3),
(5, 'Pop', 3),
(6, 'History', 0),
(7, 'Archaeology', 6),
(8, 'War', 6),
(9, 'Science', 0),
(10, 'Biology', 9),
(11, 'Chemistry', 9),
(12, 'Physics', 9),
(13, 'Technology', 0),
(14, 'Computing', 13),
(15, 'Hardware', 14),
(16, 'Software', 14),
(17, 'Engineering', 13);
Now, we need to make the model for this table. Save this as app/models/section.php:Model Class:
<?php
class Section extends AppModel
{
var $name = 'Section';
}
?>
Next, we need to pull data out of the database using our controller. Save this as app/controllers/sections_controller.php:
Controller Class:
<?php
class SectionsController extends AppController
{
var $name = 'Sections';
function index()
{
$this->set('data', $this->Section->findAllThreaded(null, null, 'name'));
}
}
?>
We need a view for the index action. Save this as app/views/sections/index.thtml:
View Template:
<h1>List of sections</h1>
<pre><?php print_r($data); ?></pre>
You should see an array with everything in your sections table organised with the right children. Looks a bit scary though...
Ok, so I have an array.. how do I make this into a nice HTML list?
I've written a simple helper that will convert the array into a lovely list. Save this as app/views/helpers/tree.php:Helper Class:
<?php
class TreeHelper extends Helper
{
var $tab = " ";
function show($name, $data)
{
list($modelName, $fieldName) = explode('/', $name);
$output = $this->list_element($data, $modelName, $fieldName, 0);
return $this->output($output);
}
function list_element($data, $modelName, $fieldName, $level)
{
$tabs = "\n" . str_repeat($this->tab, $level * 2);
$li_tabs = $tabs . $this->tab;
$output = $tabs. "<ul>";
foreach ($data as $key=>$val)
{
$output .= $li_tabs . "<li>".$val[$modelName][$fieldName];
if(isset($val['children'][0]))
{
$output .= $this->list_element($val['children'], $modelName, $fieldName, $level+1);
$output .= $li_tabs . "</li>";
}
else
{
$output .= "</li>";
}
}
$output .= $tabs . "</ul>";
return $output;
}
}
?>
Now change your controller so it includes this helper:
app/controllers/sections_controller.php:
Controller Class:
<?php
class SectionsController extends AppController
{
var $name = 'Sections';
var $helpers = array('Html', 'Tree');
function index()
{
$this->set('data', $this->Section->findAllThreaded(null, null, 'name'));
}
}
?>
And change your view so it uses this helper, instead of just dumping the array:
app/views/sections/index.thtml:
View Template:
<h1>List of sections</h1>
<?php echo $tree->show('Section/name', $data); ?>
'Section/name' is in the format 'Model/fieldname' just like with the HTML input helpers. You should now have a nice list of sections from your database!How do I add new sections to this list then?
You can simply add rows to the database, or make an add action. Here's one I baked earlier:app/controllers/sections_controller.php:
Controller Class:
<?php
class SectionsController extends AppController
{
var $name = 'Sections';
var $helpers = array('Html', 'Tree');
function index()
{
$this->set('data', $this->Section->findAllThreaded(null, null, 'name'));
}
function add()
{
$sectionArray = $this->Section->generateList(null, 'name');
$this->set('sectionArray', $sectionArray);
if(empty($this->data))
{
$this->render();
}
else
{
$this->cleanUpFields();
if($this->Section->save($this->data))
{
$this->Session->setFlash('The Section has been saved');
$this->redirect('/sections/index');
}
else
{
$this->Session->setFlash('Please correct errors below.');
}
}
}
}
?>
app/views/sections/index.thtml:
View Template:
<h1>List of sections</h1>
<?php echo $tree->show('Section/name', $data); ?>
<?php echo $html->link('Add Section', '/sections/add');?>
app/views/sections/add.thtml:
View Template:
<h2>New Section</h2>
<form action="<?php echo $html->url('/sections/add'); ?>" method="post">
<?php if(is_array($sectionArray)) { ?>
<div class="optional">
<label for="SectionParentId">Parent Section</label>
<?php echo $html->selectTag('Section/parent_id', $sectionArray);?>
</div>
<?php } ?>
<div class="required">
<label for="SectionName">Section</label>
<?php echo $html->input('Section/name', array('size' => '60'));?>
<?php echo $html->tagErrorMsg('Section/name', 'Please enter the name.');?>
</div>
<div class="submit">
<?php echo $html->submit('Add');?>
</div>
</form>
What?! You mean I didn't have to copy and paste all that code?
Click here to download full source code (6 KB)Hope you enjoyed the article and happy baking!








I've also extended this helper to do 1 more thing. I needed it to display the sections in hierarchical order in the drop down select menu.
Here's what I came up including controller, view and helper examples of how to use it.
Section Controller
Controller Class:
<?php
class SectionsController extends AppController
{
var $name = 'Sections';
var $helpers = array('Html','Tree');
function index()
{
$this->set('data', $this->Section->findAllThreaded(null, null, 'name'));
}
}
?>
sections/views/index.thtml
View Template:
<!-- This will give you a <select> with <option> values displayed in a hierarchical manner -->
<h3>Drop down hierarchical select menu</h3>
<?php echo $html->selectTag('Section/parent_id', $tree->show('Section/name', $data, 'options'));?>
<!-- This will give you a basic list as per the original tutorial -->
<h3>Basic hierarchical list</h3>
<?php echo $tree->show('Section/name', $data); ?>
<!-- This will give you a list of sections and edit/delete links next to
section name as per tutorial comment posted Mon, Apr 2nd 2007, 15:49 by sam -->
<h3>Basic hierarchical list with admin links</h3>
<?php echo $tree->show('Section/name', $data, 'admin'); ?>
<!-- This will turn the section name into a link
as per tutorial comment posted Mon, Apr 2nd 2007, 15:49 by sam -->
<h3>Basic hierarchical list with name as link</h3>
<?php echo $tree->show('Section/name', $data, 'link'); ?>
views/helpers/tree.php
Helper Class:
<?php
class TreeHelper extends Helper
{
var $tab = " ";
var $helpers = array('Html');
// Main Function
function show($name, $data, $style='')
{
list($modelName, $fieldName) = explode('/', $name);
if ($style=='options') {
$output = $this->selecttag_options_array($data, $modelName, $fieldName, $style, 0);
} else {
//$style='';
$output = $this->list_element($data, $modelName, $fieldName, $style, 0);
}
return $this->output($output);
}
// This creates a list with optional links attached to it
function list_element($data, $modelName, $fieldName, $style, $level)
{
$tabs = "\n" . str_repeat($this->tab, $level * 2);
$li_tabs = $tabs . $this->tab;
$output = $tabs. "<ul>";
foreach ($data as $key=>$val)
{
$output .= $li_tabs . "<li>".$this->style_print_item($val[$modelName], $modelName, $style);
if(isset($val['children'][0]))
{
$output .= $this->list_element($val['children'], $modelName, $fieldName, $style, $level+1);
$output .= $li_tabs . "</li>";
}
else
{
$output .= "</li>";
}
}
$output .= $tabs . "</ul>";
return $output;
}
// this handles the formatting of the links if there necessary
function style_print_item($item, $modelName, $style='')
{
switch ($style)
{
case "link":
$output = $this->Html->link($item['name'], "view/".$item['id']);
break;
case "admin":
$output = $item['name'];
$output .= $this->Html->link(" edit", "edit/".$item['id']);
$output .= " ";
$output .= $this->Html->link(" del", "delete/".$item['id']);
break;
default:
$output = $item['name'];
}
return $output;
}
// recursively reduces deep arrays to single-dimensional arrays
// $preserve_keys: (0=>never, 1=>strings, 2=>always)
// Source: http://php.net/manual/en/function.array-values.php#77671
function array_flatten($array, $preserve_keys = 1, &$newArray = Array())
{
foreach ($array as $key => $child)
{
if (is_array($child))
{
$newArray =& $this->array_flatten($child, $preserve_keys, $newArray);
}
elseif ($preserve_keys + is_string($key) > 1)
{
$newArray[$key] = $child;
}
else
{
$newArray[] = $child;
}
}
return $newArray;
}
// for formatting selecttag options into an associative array (id, name)
function selecttag_options_array($data, $modelName, $fieldName, $style, $level)
{
// html code does not work here
// tried using " " and it didn't work
$tabs = "-";
foreach ($data as $key=>$val)
{
$output[] = array($val[$modelName]['id'] => str_repeat($tabs, $level*2) . ' ' . $val[$modelName]['name']);
if(isset($val['children'][0]))
{
$output[] = $this->selecttag_options_array($val['children'], $modelName, $fieldName, $style, $level+1);
}
}
$output = $this->array_flatten($output, 2);
return $output;
}
}
?>
Now you can customize your output even more!
Disclaimers: I'm just learning CakePhp so use this at your own risk.
Problem:
I did have 1 problem I couldn't figure out. I had to use dashes "-" to create the effect of the hierarchical drop down menu. I tried using & n b s p ; to create the indents but any HTML code that I used would be shown in the output. Therefore, I had to use the dashes.
If you know how to override the html being output, I'd love to know!
Hope this helps.
Eric
class TreeHelper extends Helper
{
var $tab = " ";
var $helpers = array('Html');
function show($name, $data)
{
list($modelName, $style) = explode('/', $name);
$output = $this->list_element($data, $modelName, $style, 0);
return $this->output($output);
}
function list_element($data, $modelName, $style, $level)
{
$tabs = "\n" . str_repeat($this->tab, $level * 2);
$li_tabs = $tabs . $this->tab;
$output = $tabs. "
";- ".$this->style_print_item($val[$modelName],$style);
";
";foreach ($data as $key=>$val)
{
$output .= $li_tabs . "
if(isset($val['children'][0]))
{
$output .= $this->list_element($val['children'], $modelName, $style, $level+1);
$output .= $li_tabs . "
}
else
{
$output .= "";
}
}
$output .= $tabs . "
return $output;
}
function style_print_item($item, $style)
{
switch ($style)
{
case "link":
$output = $this->Html->link($item['title'],"/webpages/view/".$item['id']);
break;
case "admin":
$output = $item['title'];
$output .= $this->Html->link(" edit","/webpages/edit/".$item['id']);
$output .= $this->Html->link(" del","/webpages/del/".$item['id']);
break;
default:
$output = $item[$style];
}
return $output;
}
}
?>
A couple of notes, tagErrorMsg in your Add view won't do anything without var $validate = ... in your model.
Also, setFlash in your controller won't do anything without a matching flash call in your index view.
(I'm fairly new, and this was the first article I found, so those couple of points confused me for a while.)
THanks for the Tree Helper! Now if only it could output more than one database field at a time, I'd be very happy...
Art
1. Film
2. Music
1. Jazz
2. Pop
I want to implement urls for any category like:
/art/film
/art/music
/art/music/jazz
/art/music/pop
Did you think of this? And is it posible with regexp routes ...
I need to figure out this, all this will be good solution for some direcotory web app.
Thanks, and James Hall this is great tutorial!
I think you actually can. Any parameter passed after the method is treated as an argument to the controller class' function, therefore I think it is safe to assume you can use func_num_args, get the last arg, and do a filter from there.
Check http://pt.php.net/func_num_args
What if I would like to gather the tree from different tables?
Usually you have a table with 'sections' and then another with 'articles' for example.
Is it possible to use your TreeHelper to render this?
function showList($name, $RawData){
list($modelName, $fieldName) = explode('/', $name);
$output = $this->tableList($RawData, $modelName, $fieldName, 0);
return $output;
}
function tableList($RawData, $modelName, $fieldName, $level){
$td_tabs = NULL;
$output = NULL;
if($level >=1){
$tabs = "-" . str_repeat($this->tab, $level * 2);
$td_tabs = $tabs . $this->tab;
}
foreach ($RawData as $key=>$val) {
// Inject Space
$val[$modelName][$fieldName] = $td_tabs.$val[$modelName][$fieldName];
$tr = $val[$modelName];
// Inject controls - Move this out so we can add whatever we want
$tr['Actions'] = $this->Html->link("Edit", "/admin/Users/edit/{$val[$modelName]['id']}");
// Could check for level here and add different syles etc..
$output .= $this->Html->tableCells($tr, array('class' => 'trOff','onmouseover'=>"this.className='trOn';",'onMouseOut'=>"this.className='trOff';"));
if(isset($val['children'][0])){
$output .= $this->tableList($val['children'], $modelName, $fieldName, $level+1);
}
}
return $output;
}
Comments are closed for articles over a year old