Threaded Lists

This article is also available in the following languages:
By MrRio
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:

  1. [li] Art
    1. [li] Film [li] Music
      1. [li] Jazz [li] Pop
    [li] History
    1. [li] Archaeology [li] War
    [li] Science
    1. [li] Biology [li] Chemistry [li] Physics
    [li] Technology
    1. [li] Computing
      1. [li] Hardware [li] Software
      [li] Engineering
How would you store that in a relational database? Simple - each 'node' has a 'parent'. So, the Pop section would have a parent of Music and Music would have a parent of Art. You'd get something like this:

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(nullnull'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$fieldName0);
    
    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(nullnull'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(nullnull'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!

Comments

  • Posted 01/09/11 05:16:59 PM
    Thank you for your great article, it has saved me tons of lines of code...
  • Posted 10/28/07 01:53:00 PM
    Thanks for the great tutorial and the addition added by Sam in the previous comment.

    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(nullnull'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$style0);
            } else {
                
    //$style='';
                
    $output $this->list_element($data$modelName$fieldName$style0);
            }
            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($output2);
            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
  • Posted 04/02/07 03:49:41 PM
    an addition to the helper to allow multiple output styles to the tree view

    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. "
      ";
      foreach ($data as $key=>$val)
      {
      $output .= $li_tabs . "
    • ".$this->style_print_item($val[$modelName],$style);
      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;
    }

    }
    ?>
  • Posted 12/12/06 10:44:02 AM
    Nice work! It occurred to me that this code could be the foundation for displaying threaded comments of the likes of slashdot and digg.
  • Posted 10/31/06 09:07:29 PM
    Thanks for this article James!

    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...
  • Posted 10/06/06 03:22:41 AM
    I just was curios about threaded links. What i mean is if i have categories like:

    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!
    • Posted 10/24/06 02:02:02 PM
      I just was curios about threaded links. What i mean is if i have categories like:

      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
  • Posted 09/26/06 07:06:43 AM
    I really like your implementation. I have one question though.
    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?
  • Posted 11/30/99 12:00:00 AM
    Great Tutorial, helped me heaps, here is a dirty hack for those wanting to implement it using a table output.

    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