Thursday, November 5, 2009

Easy trees with Tree Behavior in CakePHP

This tutorial shows how to use the Tree Behaviour in CakePHP.

The only requirement is a working CakePHP application. You can refer to this post to help you get one.

Step 1: Create the table in your database

First we need to create a table to store our tree. To keep things a bit generic we create a simple table called 'Nodes'. There are various ways to extend this table with other functionality or data, but that is beyond the scope of this tutorial.

Run the following SQL code to create a table called Nodes:

CREATE TABLE `nodes` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `parent_id` int(11) unsigned DEFAULT NULL,
  `lft` int(11) unsigned DEFAULT NULL,
  `rght` int(11) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

The Tree Behaviour needs at least the fields parent_id, lft and rght, and they should all use type int().

Step 2: Create the model

To use table in the application we have to create the model. In this case the file will be app/models/node.php.

<?php

class Node extends AppModel {
    var $name = 'Node';
    var $actsAs = array('Tree');
}

?>

The model Node is defined as any other model in CakePHP. With the line var $actsAs = array('Tree'); we tell CakePHP to use this model as a tree.

Step 3: Create the controller

The controllers task is to take care of handling the data (adding, showing, updating, deleting, and more). At first we will create a controller with 2 functions, one called 'index' to display the tree and the other called 'add' to add items to the tree. The controller will be app/controllers/nodes_controller.php.

<?php

class NodesController extends AppController {

    function index() {
        $nodelist = $this->Node->generatetreelist(null,null,null," - ");
        $this->set(compact('nodelist'));
    }

    function add() {
        if (!empty($this->data)) {
            $this->Node->save($this->data);
            $this->redirect(array('action'=>'index'));
        } else {
            $parents[0] = "[ No Parent ]";
            $nodelist = $this->Node->generatetreelist(null,null,null," - ");
            if($nodelist) {
                foreach ($nodelist as $key=>$value)
                    $parents[$key] = $value;
            }
            $this->set(compact('parents'));
        }
    }
}

?>

The index function is pretty straightforward, it uses the generatetreelist function to generate a formatted tree, and return it in an array.

The Add function is a bit more complex. It first checks if it received any data. If it did, it saves the data to the database and then redirects to the index again. If the add function did not receive any data, it will create the array needed to populate the 'Parent' select box in our Add screen (see below).

Step 4: Create the views

We now create 2 view files in the folder app/views/nodes/, which needs to be created first.

The file app/views/nodes/index.ctp is used to display the tree:

<?php

echo $html->link("Add Node",array('action'=>'add'));

echo "<ul>";
foreach($nodelist as $key=>$value){
    echo "<li>$value</li>";
}
echo "</ul>";

?>

In the above code we first create a link to our 'add' functionality. After that we start a new Unordered list and fill it with the data from the tree.

For adding new items to the tree we will use the file app/views/nodes/add.ctp

<?php

echo $html->link('Back',array('action'=>'index'));

echo $form->create('Node');
echo $form->input('name',array('label'=>'Name'));
echo $form->input('parent_id',array('label'=>'Parent'));
echo $form->end('Add');

?>

First there is a link back to the index. After that a new HTML Form of the type Node is create. It then add two fields, one for the Node name, the other to select the parent of the new node. The last function add the submit button with the text 'Add'.


You should now be able to show your tree, and add nodes to it. Actually you first need to add a node because there is nothing to display yet. In the next step we will add some more functionality to it, but this seems a good moment to check if it all works so far :) ...


Step 5: Adding more functionality

In this step we will add the posibility to edit and delete the nodes, as wel as move them up and down. We do this by adding some links to the Index page, add the edit view and add some functions to the controller.

First we update the file app/views/nodes/index.ctp so it looks like this:

<?php

echo $html->link("Add Node",array('action'=>'add'));

echo "<ul>";
foreach($nodelist as $key=>$value){
    $editurl = $html->link("Edit", array('action'=>'edit', $key));
    $upurl = $html->link("Up", array('action'=>'moveup', $key));
    $downurl = $html->link("Down", array('action'=>'movedown', $key));
    $deleteurl = $html->link("Delete", array('action'=>'delete', $key));
    echo "<li>[$editurl|$upurl|$downurl|$deleteurl] $value</li>";
}
echo "</ul>";

?>

The part that loops trough the nodelist is changed here. We define 4 URL's for the needed actions, and put them all in front of the Node name, seperated by a pipeline character. This is not the most elegant solution, but it will get the job done.

Next we add a view for the edit functionality by creating the file app/views/nodes/edit.ctp:

<?php

echo $html->link('Back',array('action'=>'index'));

echo $form->create('Node');
echo $form->hidden('id');
echo $form->input('name');
echo $form->input('parent_id', array('selected'=>$this->data['Node']['parent_id']));
echo $form->end('Update');

?>

This view is mostly the same as add.ctp. Two differences: the edit view needs a hidden field called 'id', and the parent_id selectbox has a 'selected' parameter, which selects the right parent when in Edit mode.

In the last step we add four functions (edit, delete, moveup, movedown) to the Controller, app/controllers/nodes_controller.php, so it will look like this:

<?php

class NodesController extends AppController {

    function index() {
        $nodelist = $this->Node->generatetreelist(null,null,null," - ");
        $this->set(compact('nodelist'));
    }

    function add() {
        if (!empty($this->data)) {
            $this->Node->save($this->data);
            $this->redirect(array('action'=>'index'));
        } else {
            $parents[0] = "[ No Parent ]";
            $nodelist = $this->Node->generatetreelist(null,null,null," - ");
            if($nodelist)
                foreach ($nodelist as $key=>$value)
                    $parents[$key] = $value;
            $this->set(compact('parents'));
        }
    }

    function edit($id=null) {
        if (!empty($this->data)) {
            if($this->Node->save($this->data)==false)
                $this->Session->setFlash('Error saving Node.');
            $this->redirect(array('action'=>'index'));
        } else {
            if($id==null) die("No ID received");
            $this->data = $this->Node->read(null, $id);
            $parents[0] = "[ No Parent ]";
            $nodelist = $this->Node->generatetreelist(null,null,null," - ");
            if($nodelist) 
                foreach ($nodelist as $key=>$value)
                    $parents[$key] = $value;
            $this->set(compact('parents'));
        }
    }

    function delete($id=null) {
        if($id==null)
            die("No ID received");
        $this->Node->id=$id;
        if($this->Node->delete()==false)
            $this->Session->setFlash('The Node could not be deleted.');
        $this->redirect(array('action'=>'index'));
    }

    function moveup($id=null) {
        if($id==null)
            die("No ID received");
        $this->Node->id=$id;
        if($this->Node->moveup()==false)
            $this->Session->setFlash('The Node could not be moved up.');
        $this->redirect(array('action'=>'index'));
    }

    function movedown($id=null) {
        if($id==null)
            die("No ID received");
        $this->Node->id=$id;
        if($this->Node->movedown()==false)
            $this->Session->setFlash('The Node could not be moved down.');
        $this->redirect(array('action'=>'index'));
    }
}

?>

The first part of the edit function works much like the add function, it checks on received data and tries to save it. When there is no data received, it checks if there is a paramater called ID. If not it dies with an error message. If the parameter is given it fetches the node data, and gets the data needed to populate the 'Parent' select box, just like in the Add screen

The rest of the three functions are almost identical, and don't need any matching views as the code will redirect to the index page anyway. All these functions check if the parameter ID is passed, and dies with an error if not. If the parameter is given it selects the right node and runs the action.

Done!

This should be it, you should now have a tree created in CakePHP, with complete Create, Read, Update and Delete functionality (and even more)...

Good luck using this code, and please use the comments if you have any questions about it.


32 comments:

Nilz said...

Thanks for this excellent post - the best one on 'Tree Behavior'. I just preferred to use removeFromTree() in stead of delete().

my-CakePHP

Jaro said...

This is the best tutorial for Tree Behavior ever. I really appreciate it. Thank you.

Bram said...

Thanks! I'm glad you like it. :)

Raheel Dharolia said...

Bram... This was too helpful. Keep up the good work man.

One quick question though. Can you update the index action so that we can also get the name of the parent instead of parent_id.

Current Index:
id title parent_id
1 Computers 0
2 Printers 1

Required Index:
Current Index:
id title parent_id
1 Computers None
2 Printers Computers


Thanks alot.

Light said...

Very nice :)
But you might want to change your sql statement and make all int fields unsigned. They're not going to be negative anyway.
Also you could use the character set UTF-8 instead of Latin 1. CakePHP uses UTF-8 by default.

Bram said...

Thanks Light, I updated the SQL statement :)

Zetha said...

thank you very much.
I was a little confuse about how this behavior work,but you just have saved my day :)
Excellent post

Holger said...

Thank you so much for that! Helped me a lot to improve my cake app

Michel said...

Thanks a ton. It saved lots of time.
I will add that you can use $this->Model->recover() to have the lft and rght values automatically computed based on the parent_id.

Frank said...

Hi, very nice ! 1 Question, when I want to use it with another table-name then I unfortunately get lost ... although I took care to change all model names, I get errors

in controller :

function index() {
$nodelist = $this->Companycategory->generatetreelist(null,null,null," - ");
$this->set(compact('nodelist'));
}

in view :
foreach($nodelist as $key=>$value){

Error Message :
Invalid argument supplied for foreach() [APP\views\companycategories\index.ctp, line 6]


For newbies a hint how to change model-name correctly throughout the files would be a wonderwall.

Regards,
Frank

Anonymous said...

I actually found out now ... my bad the model.php was not setup right.

Thx, Frank

Anonymous said...

Thanks for the excellent tutorial, great help was struggling with Tree behavior.

Anonymous said...

How would I go about displaying additional table fields? This works perfectly minus that.

amit said...

Nice tutorial....thanks for this post.....
Please visit this link for learn more script.
http://amitmondal.wordpress.com/

knilz said...

This good Sir, is the most awesome tutorial about using Cakes Tree Behavior, ever. Thank you so much for sharing!

Paul said...

Very helpful thanks!

I agree with your fist poster, I also prefer removeFromTree() to delete(). Then my child groups, that still contain many users, are not deleted.

Dimo said...

Thanks mate!

Dee Wilcox said...

Thanks for the great tutorial! However, I'm using it with CakePHP 2.0 and needed to update my views. Where you have $html and $form, I needed to instead use $this->Html-> and $this->Form->. Hope that helps someone!

M Zubair Haroon said...

Nice tutorial I'm implementing it using drag n drop functionality, when I drag a node to different node I just change the parent id and when I also want to place the order where its dragged, can you provide the link u got for moveup n movedown func.

normally when I change the parent it put the that child in the last I want it to be placed where its dragged.

Thanks

tihanyilaci said...

Hi Bram,

that is a great tutorial. It was easy to implement it to cake2.0 also.

aaricevans said...

I have seen a very informative blog. Really I like this blog. This blog gives us very good knowledge about cake php development.

Anonymous said...

Very nice Treebehaviour article. Now, how about a tutorial on how to make the tree useful; for example if the tree was about recipe categories, how to organize a recipe system under it (as RecipeCetegories HABTM Recipes).

Unknown said...

Many thanks for your post... I save a lot of time!

kevin said...

Hello -- and thank you, in advance, for your help (time and expertise).

I am having trouble getting this to work with my own table and CakePHP 2.2. Could someone point me to the main things I need to look for in order to adapt this to CakePHP 2.2.

Thank you!
Kevin

Bram said...

Hello Kevin,

Could you post your table structure? I'll see if I can figure out what the problem is.

kevin said...

Hi Bram

Thank you for your help. I did finally get things working for CakePHP 2.2.0. I'll summarize the changes from 1.3 that made to get things working with 2.2.0:

// STEP 2: CREATE THE MODEL
// var becomes public, so...
class Node extends AppModel {
public $name = 'Node';
public $actsAs = array('Tree');
}

// STEP 3: CREATE THE CONTROLLER
function becomes public function

generatetreelist becomes generateTreeList

$this->data becomes $this->request->data

// STEP 4/5: CREATE THE VIEWS / ADDING MORE FUNCTIONALITY
$html-> becomes $this.Html->
$form-> becomes $this.Form->

For some reason I could not determine (being new to PHP and CakePHP) the setFlash statements in movedown and moveup functions caused an error. Just as a temporary fix, I commented them out. Someone will have to help me with why they did not work in CakePHP 2.+.

I think that summarizes all the changes I made to get things working.

Kevin

Bram said...

Hi Kevin,

Thanks for your reaction and steps you took to get it working on the 2.2 version!

In the future I will update the tutorial to reflect this CakePHP version.

At the moment I don't know what causes the setFlash methods to fail but I'll look into that too.

Cheers, Bram

kevin said...

Bram:

Do you know if it's possible to have the generateTreeList function display more than just the "name" field? I see in the documentation for TreeBehavior that you can specify an alternative valuePath:

string $valuePath optional NULL
A string path to the value, i.e. "{n}.Post.title"

I'm not sure what the {n} refers to. I was hoping to have two concatenated fields show in the tree, so that the resulting tree would look like "Title (reference)" -- e.g., Psalm 1 (1.1-6)

If you can help, thank you! OK: even if you can't :)

Kevin

kevin said...

PS: OK I discovered I have to literally add {n}.Node.reference if I wanted my reference field to show up in the generateTreeList. {n} refers to a numeric key for a DataSource (see https://groups.google.com/forum/?fromgroups#!topic/cake-php/6bK1XKgJnts).

What I still do not know is if this can be concatenated with another field(s). I tried using "{n}.Node.title . ' (' . {n}.Node.reference . ')'" -- but that did not work. I'm new to PHP, so I don't know if this is a PHP or a CakePHP issue.

Bram said...

Hi Kevin,

I haven't had the time to look into this issue very long, but CakePHP offers the option to create 'Virtual Fields' ( http://book.cakephp.org/2.0/en/models/virtual-fields.html ). This might help you get the right information in the 'name' field.

If this is not what you want you might have to look at another way (beyond the generateTreeList method) to display the information.

By the way, I tend to go to the irc channel #cakephp on Freenode if I'm stuck at some point, most of the time you can get a quick response to your questions there.

kevin said...

Hi Bram:

Another reader had asked a similar question, above, so I wanted to share that the virtualFields worked perfectly. In the Model I specified:

public $virtualFields = array(
'title_reference' => 'CONCAT(Node.title, " (", Node.reference, ")")'
);

Then I just used "title_reference" in the generateTreeList statement:

generateTreeList(null,null,"{n}.Node.title_reference","_",null)

Thank you, Bram, for all your help! ... for volunteering your time and expertise. Your answers have moved me along at a time when I was just about to give up on CakePHP altogether. I don't know quite how to describe it, but after weeks of work I had found the documentation to be very helpful on one level, but, on another, once I got beyond the tutorials, lacking in the information I needed to put the pieces together. So, I want you to know that your tutorial and your responses have helped me bridge that gap in a critical way. I had found the CakePHP IRC to be inactive when I visited several days ago, but I'll certainly give it another try. Thank you for encouraging that.

Kevin

Bram said...

Hi Kevin,

Thanks for your positive words, I am very happy that my knowledge helped you along the way!

When I started using CakePHP I had my doubts as well at some points, but in the end I think that the learning curve is well worth it.

One needs to learn how to read the book and the API, and think in a CakePHP way. (But, then again, never forget it's still just PHP...)

Reading the replies on this post really motivate me to update the tutorials on my site in the near future, so it can help even more people working with this nice framework.

Good luck on your project and don't hesitate to contact me if you have any other issues with it!

Cheers, Bram