Creating a database-driven hierarchical Structure combined with CMenu and superfish

In this tutorial we will create a hierarchical Structure using the traditional adjacency list model. Yii's ActiveRecord paradigm makes it very easy to implement this structure via a join on itself. After this, we will use the new CMenu from yii 1.1 and implement it in conjunction with superfish, a jQuery plugin for creating menus.

The CDropDownMenu extension will help us to accomplish this task.

I will also add a tutorial using a Nested Set (using the wonderful nested set extension) soon.

At first, we create our SQL-Table containing our hierarchical data like this:

CREATE TABLE `Hierarchy` (
  `id` int(11) NOT NULL auto_increment,
  `sort` int(11) NOT NULL,
  `parent` int(11) default NULL,
  `title` varchar(255) default NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB

You may add additional Columns if you wish, and you can rename the table however you like. The 'parent' field contains the id of the direct parent, while 'sort' defines, where our row gets displayed in the menu.

After that, we generate the model and a C-R-U-D interface:

$ php protected/yiic shell
Yii Interactive Tool v1.1 (based on Yii v1.1.0)
Please type 'help' for help. Type 'exit' to quit.
>> model Hierarchy
>> crud Hierarchy

When this commands run without an error, we should add two relation rules to our new created model:

public function relations()
  {
    return array(
      'getparent' => array(self::BELONGS_TO, 'Hierarchy', 'parent'),
      'childs' => array(self::HAS_MANY, 'Hierarchy', 'parent', 'order' => 'sort ASC'),
    );

This reads as: the 'parent' of a row belongs to the parent-column of the same table, while we can gather the childs of a row by a relation of ourself with the HAS_MANY relation. We always want our childs get ordered by the column 'sort's. You can add additional relations if you want.

To test our newly created model, we need to insert some random test data:

insert into Hierarchy (id, sort, parent, title) values(1, 0, 0, 'root');
insert into Hierarchy (id, sort, parent, title) values(2, 0, 1, 'First Entry');
insert into Hierarchy (id, sort, parent, title) values(3, 0, 1, 'Second Entry withoud Childs');
insert into Hierarchy (id, sort, parent, title) values(4, 0, 1, 'Third Entry');
insert into Hierarchy (id, sort, parent, title) values(5, 0, 3, 'Child of the third Entry');
insert into Hierarchy (id, sort, parent, title) values(6, 0, 5, 'Child of the Child of the third Entry');
insert into Hierarchy (id, sort, parent, title) values(7, 0, 1, 'Child of the first Entry');

Now, in our yii console or in the Application we can use

$model = Hierarchy::model()->findByPk(7);
$parent = $model->parent;
echo $parent->title;
// returns 'First Entry'

to get the parent element, or

$model = Hierarchy::model()->findByPk(1);
print_r($model->childs);

to print out all existing models in our Database. As you may suggest, this output is not very nice. Since we want to use the CHtml module of yii 1.1, we need to format our Output in an array like this:

public function getListed() {
    $subitems = array();
    if($this->childs) foreach($this->childs as $child) {
      $subitems[] = $child->getListed();
    }
    $returnarray = array('label' => $this->headline, 'url' => array('Hierarchy/view', 'id' => $this->id));
    if($subitems != array()) $returnarray = array_merge($returnarray, array('items' => $subitems));
    return $returnarray;
  }

We place this function in models/Hierarchy.php

This is a recursive function (note how the function calls itself) that gathers all subchilds of an element that are available in the Database. Of course, when we run this function on our root node, we get all non-orphan childs.

After that, we can use the CDropDownMenu-Widget to generate our Menu:

$model = Hierarchy::model()->findByPk(1);
$items[] = $model->getListed(); // note that the [] is important, otherwise CMenu will crash.
 
$this->widget('zii.widgets.CDropDownMenu',array(
      'items'=>$items,
));

to render the menu with the content of our database.

Attention: please be sure to not use this Widget inside the div id="mainmenu" of standard-generated webapps because there seems to be some css inconsistency between yii's default css and superfish's default css. Maybe this will be fixed sometime.

Note the bunch of effects and options (like drop-shadow) you can configure with superfish in extensions/vendors/CDropDownMenu.js

After this, we want our Users to be able to easily move Menu entries around. To achieve this, we will use a Drop-Down List, in which we can choose the parent of our selected element. We write this code-snippet to views/Hierarchy/_form.php :

$data = Hierarchy::model()->findAll('parent=:parent', array('parent' => '0'));
 
  foreach($data as $child) {
    $subchilds = $child->childs;
    foreach($subchilds as $subchild) {
    $subchild->title = $subchild->getparent->title . "|" . $subchild->title;
    $data = array_merge($data, $child->childs);
    }
  }
 
  $rootobj = new Hierarchy;
  $rootobj->id = 0;
  $rootobj->title = "root level";
  $root = array($rootobj);
  $data = array_merge($root, $data);
 
  if(isset($model->id) && $model->id == 1) {
    echo "This is the root node and can't be moved.";
    $model->parent = 0; 
  }
  else {
    if(isset($_GET['hierarchyParent']))
    echo CHtml::DropDownList('Hierarchy[parent]', $_GET['HierarchyParent'], CHtml::listData($data, 'id', 'title'));
    else if($update)
    echo CHtml::DropDownList('Hierarchy[parent]', $model->parent, CHtml::listData($data, 'id', 'title'));
    else
    echo CHtml::DropDownList('Hierarchy[parent]', 1, CHtml::listData($data, 'id', 'title'));
  }

Note the lines

if(isset($_GET['hierarchyParent']))
    echo CHtml::DropDownList('Hierarchy[parent]', $_GET['HierarchyParent'], CHtml::listData($data, 'id', 'title'));

With this lines we will be able to create a "add entry to this element"-Button like this:

if(!Yii::app()->User->isguest)
 echo CHtml::link("Add a new element", array('Hierarchy/create', 'hierarchyParent' => $model->id));

Place this lines somewhere at views/Hierarchy/view.php .

I hope my small tutorial was helpful for you. There are some points that can be made even better, for example someone could change the admin CGridView to be collapsable, and the elements could be moved around by drag & drop. In the next version of this Tutorial i will use the nestedset extension to achieve the Hierarchy Structure. Thank you for reading & trying, and don't hesitate to ask me when you have Questions.

Total 14 comments:

#1007
Typos...
by wlepinski at 1:33am on January 17, 2010.

PHP code is broken... =/

#1009
result
by StErMi at 1:13pm on January 17, 2010.

good coockbook :)

can you show us an example of the result?

and please fix php code :)

#1012
Source of article OK but rendering with lots of errors/omisions
by mmezo at 1:18pm on January 18, 2010.

I've been struggling with this a bit, and I found some things not so clear (like how and where superfish should be installed. It seemed like it would be picked automagically from some location).

I also found some errors like "$_GET'hierarchyParent'" which should be "$_GET'hierarchyParent'" which I could fix myself, but in the end I couldn't make it work (now I have it working).

I was very surprised that a simple comment like "//<-- optional" in the layout/main.php could do the magic to replace the static menu by the superfish one.

Now I've found the revision control source for the article. There the code seems to be complete and without errors. For example instead of the "// <-- optional" there are 3 or for lines with includes for the html source. With that code I could finally make the example work and figure that I can install superfish wherever I want as long as I point the aforementioned includes to the right place.

So the article is ok, but the rendering has quite a lot of artifacts.

There is still a little bug in the parent id in the sample database entries, where the parent id's of some menu items are "off by one" and also I don't think that the "root" entry should be showed, but otherwise it is working nice.

#1013
Same error in comment and article.... :-(
by mmezo at 1:22pm on January 18, 2010.

My previous comment suffers from the same artifacts as the article. It seems you cannot write the square bracket (" [ ") directly. You have to write " \[ ".

#1015
markyp fixed
by jonah at 1:30pm on January 18, 2010.

markup fixed. To embed php code, use

~~~
[php]
//Code here
~~~

#1016
It was my fault...
by thyseus at 1:13am on January 19, 2010.

Hi, i wrote this tutorial and it was my fault, since i did not know how to mark up correctly.

Much thanks for jonas to repair my small cookbook entry :)

You can install superfish anywhere you want, for example you can make a project/js/superfish/ directory and extract superfish there.

Now i know how to do correct markup, so i will add a few sentences and an example on how to install superfish today.

#1017
CDropDownMenu is on the way...
by thyseus at 1:07am on January 19, 2010.

I've just decided to build a CDropDownMenu Widget, that automates the integration of superfish.

I think it will be available this week. The Tutorial will then be updated.

#1021
Ok, please test my initial release of the CDropDownMenu
by thyseus at 1:56am on January 20, 2010.

thank you...

#1145
All menu items sorted
by zillabyte at 2:05pm on February 20, 2010.

I've been trying to get this to work by first call findAll() from my controller.

the line $items[] = $model->getListed(); doesn't work. I get an error saying that I'm calling a method on a non-object. The example here shows getting only one item so perhaps I am using this on an array of objects.

Would someone know how to sort the entire list in the correct order or display it properly. I'm not really trying to get this to work with CMenu but I have a similar structure. My table structure is similar in that I have an id field and a parent field (although these are not integers in my case) and I want to retrieve the entire list and essentailly display a tree structure. A simple example:

Cars -- chevy -- ford ---- mustang ---- ranger -- toyota Food -- fruit ---- apples ---- oranges ------- sunny ------- green -- pasta -- veges

#1146
All menu items sorted
by zillabyte at 2:06pm on February 20, 2010.

should display


Cars
-- chevy
-- ford
---- mustang
---- ranger
-- toyota
Food
-- fruit
---- apples
---- oranges
------- sunny
------- green
-- pasta
-- veges

#1336
headline?
by 银河王子 at 3:31am on March 30, 2010.

$returnarray = array('label' => $this->headline, 'url' => array('Hierarchy/view', 'id' => $this->id));

$this->headline should be this->title;

change it and look.

#1470
Tree view ?
by yiiuser at 5:45pm on May 9, 2010.

Hi,

This example works very well, but i want to use the tree view instead of the dropdownmenu, so i tried something like this:

public function actionAjaxFillTree() { if (!Yii::app()->request->isAjaxRequest) { exit(); } $parentId = 1;

    if (isset($_GET['root'])) {
        $parentId = (int) $_GET['root'];
    }

    $model = hierarchy::model()->findByPk($parentId);
$items[] = $model->getListed(); 

    echo CTreeView::saveDataAsJson($items);
}


In the view i put this code:

<?php $this->widget( 'CTreeView', array('url'=>array('ajaxFillTree')); ?>

but the view show nothing with no error message.

Do you have an idea how can i fix this problem ?

Thanks in advance

#1471
Here is the error message
by yiiuser at 5:21pm on May 9, 2010.

The error message wasn't listed on the page view but has hidden in the javascript cause its was an Ajax request: You are not authorized to perform this action.

Why i have this error message ?

Thanks

#1687
Display all menu items
by bluecloudy at 7:27am on July 15, 2010.

Using loop to display all items:

$Hierarchy=Hierarchy::model()->findAll(array('condition'=>'parent = 0'));
        foreach ($Hierarchy as $Hierarchy){
            $models = Cat::model()->findBypk($Hierarchy->id);
            $items[] = $models->getListed();        
        }       
        if($items[0]['items'] != "0"){
            $items = array_merge(array('items'=>array('label'=>'Trang chủ', 'url'=>array('/site/index'))),
                                $items,array(array('label'=>'Liên hệ', 'url'=>array('/site/contact'))));    
 
        $this->widget('zii.widgets.CMenu',array(        
            'items'=>$items,    
        ));

Your Comment:

You may enter comment using Markdown syntax.

Please login with your forum account.
Note: you must have at least ONE forum post with your account.