ExpressionEngine CMS
Open, Free, Amazing

Thread

This is an archived forum and the content is probably no longer relevant, but is provided here for posterity.

The active forums are here.

MPTtree, Hieararchical trees in a database table

March 13, 2008 5:15pm

Subscribe [45]
  • #1 / Mar 13, 2008 5:15pm

    m4rw3r's avatar

    m4rw3r

    647 posts

    Current Version: 0.1.6-fix2


    MPTtree is a model to handle trees in a Nested Sets structure in a database table.

    Features:
    xpath and paths to nodes
    ORM (currently without the relationship part), will be somewhat integrated with IgnitedRecord in the future
    Iterators (Only for PHP 5)
    Multiple instances with help of custom loader (optional)
    A simple admin interface, using Ext JS for drag n drop (a separate zip file, in wiki)
    Insertion, Moving, Deletion and Validation methods


    Now I have released version 0.1.6 which has got a few bug fixes, a new tree2array method and get_descendants have been modified so they additionally can return a depth column (only non ORM).

    0.1.6-fix contains some additional bug fixes.

    MPTtree have currently only been tested with MySQL 5, and the queries are currently only written for MySQL (if someone wants to help me to port the SQL to other databases, or is it needed?).

    Included in the zip file is a (good?) manual which describes most of the methods (if not all you need to know).


    You can find the zip file here

    If you have any comments or suggestions, feel free to reply (I’d like to have some feedback on this).

  • #2 / Mar 14, 2008 4:35pm

    louis w's avatar

    louis w

    450 posts

    Wow this looks cool! Reading over the manual now.

    Any way you could set up some demos to show it in use?

  • #3 / Mar 22, 2008 3:17pm

    m4rw3r's avatar

    m4rw3r

    647 posts

    Have been ill for some time, but now I’m alright. I have now updated MPTtree to version 0.1.6 (see top post) and
    I have also made a simple wiki as an example (quite hastily done, can contain errors):

    controllers/wiki.php:

    <?php
    class Wiki extends Controller{
        function Wiki(){
            parent::Controller();
            $this->load->database();
            $this->load->MPTT('wiki_tree', 'pages');
            $this->load->helper('url');
        }
        
        function _remap($method){
            switch ($method) {
                case 'add':
                    $this->add();
                    break;
                
                case 'edit':
                    $this->edit();
                    break;
                case 'del':
                    $this->del();
                    break;
                
                default:
                    $this->index();
                    break;
            }
        }
        
        function index(){
            // load the segments and remove the "wiki" one
            $segs = $this->uri->segment_array();
            array_shift($segs);
            
            // find the page with xpath
            $page = $this->pages->xpath($segs);
            
            if($page != false){
                $this->_show_page($page,$segs);
            }
            else{
                if(count($segs) == 0){
                    // no root, add one
                    $new_node->set('title','Default title');
                    $new_node->set('contents','Default Text');
                    $new_node->set('date',time());
                    $new_node->insert_as_root();
                    $this->_show_page($parent->get_all(),$segs);
                    return;
                }
                // page does not exists
                $data['title'] = '404 Error: Page does not exist';
                $data['contents'] = 'The page you requested cannot be found';
                $this->load->view('wiki',$data);
            }
        }
        
        function edit(){
            if($this->input->post('edit') == true && $this->input->post('page') !== false){
                $data['contents'] = $this->input->post('contents');
                $data['title'] = $this->input->post('title');
                $data['date'] = time();
                // commit changes
                $this->db->where('id',$this->input->post('page'));
                $this->db->update('wiki_tree',$data);
                // load data to display edited page
                $page = $this->pages->get_ORM_byid($this->input->post('page'));
                $this->_show_page($page->get_all(),$page->path());
            }
            else{
                // display edit form
                $page = $this->pages->get_node($this->uri->segment(3));
                $page['edit'] = true;
                $this->load->view('wiki',$page);
            }
        }
        
        function add(){
            $parent = $this->pages->get_ORM($this->uri->segment(3));
            $new_node = $this->pages->new_ORM(); // creates a new empty object
            // save data
            $new_node->set('title','Default title');
            $new_node->set('contents','Default Text');
            $new_node->set('date',time());
            $new_node->insert_as_first_child_of($parent);
            
            // load data to display parent page
            $this->_show_page($parent->get_all(),$parent->path());
        }
        
        function del(){
            $page = $this->pages->get_ORM($this->uri->segment(3));
            if($page){
                $page->delete();
            }
            redirect('/wiki');
        }
        
        function _show_page($page,$segs = array()){
            $page['edit_link'] = array('wiki','edit',$page['lft']);
            $page['del_link'] = array('wiki','del',$page['lft']);
            $page['children'] = $this->pages->get_children($page['lft'], $page['rgt']);
            $page['path'] = array_merge(array('wiki'),$segs);
            $page['deleteable'] = true;
            // just pass the result to the view
            $this->load->view('wiki',$page);
        }
    }
    ?>

    views/wiki.php:

    <html>
    <head>
    <title>Wiki: <?php echo htmlentities($title) ?></title>
    </head>
    <body>
    <h1><?php echo htmlentities($title) ?></h1>
    
    <?php if (isset($children) && count($children)): ?>
        Children:<?php foreach ($children as $child): ?>
        <?php echo anchor(array_merge($path,array($child['title'])),$child['title']) ?>
    <?php endforeach ?>
    <?php endif ?>
    <?php echo anchor(array('wiki','add',$lft),'Add Child') ?>
    <hr >
    <?php if (isset($edit) && $edit == true): ?>
        <form method="POST" action="<?php echo site_url(array('wiki','edit')) ?>">
        <input type="hidden" name="edit" value="true" />
        <input type="hidden" name="page" value="<?php echo $id ?>" />
        Title:
    
        <input type="text" name="title" value="<?php echo $title ?>">
    
        Content: 
    
        <textarea name="contents" rows="20" cols="60"><?php echo htmlentities($contents) ?></textarea>
        
    
        <input type="submit" value="Save" />
    </form>
    <?php else: ?>
        <?php echo $contents; ?>
    <?php endif ?>
    <hr >
    <?php if(isset($date)): ?>
    <em>Last Modified: <?php echo date('Y-m-d H:i:s',$date) ?></em>
    <?php endif ?>
    <?php echo isset($edit_link) ? ' - ' . anchor($edit_link,'Edit Page') : '' ?>
     - <?php echo isset($deleteable) && $deleteable == true ? anchor($del_link,'Delete Page') : '' ?> - <a href="http://&lt?php">Home</a>
    </body>
    </html>

    Database table:
    wiki_tree

    columns:
    id unsigned int auto increment
    lft unsigned int
    rgt unsigned int
    title varchar 45
    contents text
    date unsigned int

  • #4 / Mar 27, 2008 4:25pm

    m4rw3r's avatar

    m4rw3r

    647 posts

    I have now made some bug fixes (0.1.6-fix) to MPTtree, see change log for details.
    Thanks for the bug reports!
    The wiki above is an example (forgot to mention that earlier).

    Any suggestions for new features?

  • #5 / Apr 06, 2008 7:11am

    Tony Nash

    42 posts

    Sorry, But I wonder what purpose I can use MPTtree for? Can I use this for a CI based CMS development? or shopping cart?

  • #6 / Apr 06, 2008 8:15am

    nmweb

    206 posts

    Anything that involves trees. Pine trees, etc. 😉

    Say you have a list of locations:
    <br /> World<br /> -Europe<br /> -Asia<br /> -North-America<br />—USA<br />—-California<br />——Los Angeles<br />
    With MPTtree you can store the hierarchy of this in a database. Same goes for menus, product categories etc.

  • #7 / Apr 06, 2008 9:27am

    m4rw3r's avatar

    m4rw3r

    647 posts

    Another good thing with nested sets is that they can easily be paced in a certain order (not bound to sorting by columns).

    If you want to make a CMS, look at the wiki example above. The example has a lot of flaws (Not able of having two pages with the same names as children to the same node, for example (has nothing to do with MPTtree, it’s how I implemented the easy searching by title in the controller)), but it is something that might point you in the right direction.

    I’m also making a CMS with MPTtree, something reminding of Radiant CMS (I like the simplicity in the Radius tags), so don’t hesitate to ask 😊.

  • #8 / May 04, 2008 2:30pm

    taewoo's avatar

    taewoo

    212 posts

    @m4rw3r

    I am fascinated by your example wiki ...
    Is there a “fuller”, more complete (i.e. more documented) version of your work that you might’ve posted somewhere?

  • #9 / May 04, 2008 2:54pm

    m4rw3r's avatar

    m4rw3r

    647 posts

    @taewoo

    No, not for the moment, the CMS I’m developing is currently on hold (haven’t got enough time and interest, need to rebuild the data representation of the pages (again :-( ) because I have got a better idea on how to organize them and the db (will still use MPTtree, though)).

    But you can sit and experiment with and improve the example wiki if you want to have something to start from.

  • #10 / May 11, 2008 8:49am

    MaxPar

    6 posts

    m4rw3r,

    I’ve been trying to get your module to work, but without much success.  Using the wiki.php example, you posted, I’m getting the error:

    Fatal error: Cannot pass parameter 1 by reference in /mywebserver’s_path/system/application/models/mpttree.php on line 405

    This is after I setup the table as you described in your example.  I’m running PHP 5.25 with MySQL     4.1.22-standard.  I believe that I’m successfully connecting to the database without any problems (since loading the database() routines doesn’t result in any complaining). 

    I’m also having the same problem when I’m running my own code examples. 

    Is there supposed to be a default setting for lft, rgt or ID?  Right now I have them all set to 0, but fiddling around with the default values hasn’t solved the error.

    Thanks for all your work on this code.  It looks really great - exactly what I was looking for, actually - and I’m excited to use it once I get things working.

  • #11 / May 11, 2008 9:01am

    m4rw3r's avatar

    m4rw3r

    647 posts

    Ok, seems like it’s complaining on the debug_message() function.

    Remove the ampersand in the definition of debug_message():
    function debug_message(&$message){
    to
    function debug_message($message){

    The default settings are covered in the manual.

  • #12 / May 11, 2008 9:11am

    MaxPar

    6 posts

    Wow, talk about a fast response!  Now the error has changed to:

    A PHP Error was encountered

    Severity: Notice

    Message: Only variable references should be returned by reference

    Filename: MPTtree/MPTtree_ORM.php

    Line Number: 199
    A PHP Error was encountered

    Severity: Notice

    Message: Undefined variable: parent

    Filename: controllers/wiki.php

    Line Number: 48

    Fatal error: Call to a member function get_all() on a non-object in /webserver/system/application/controllers/wiki.php on line 48

    Do you think this could have anything to do with my version of MySQL?  Also, I neglected to mention in my previous post that I’m running the latest version of CodeIgniter.

  • #13 / May 11, 2008 9:35am

    MaxPar

    6 posts

    I deleted and re-entered the database in MySQL, and now it seems to be working fine.  I guess I must have typod something as I was entering it :\ :\

    Anyway, thanks so much for your (extremely fast!) help and work on this code.  This is a really great plugin!

  • #14 / May 16, 2008 6:19pm

    TheLoops's avatar

    TheLoops

    61 posts

    After installing MPTree in an application that uses Matchbox I get this conflict with Matchbox:

    A PHP Error was encountered
    Severity: Notice
    Message: Undefined property: Tree_admin::$matchbox
    Filename: libraries/MY_Config.php
    Line Number: 81

    Fatal error: Call to a member function argument() on a non-object in …/application/libraries/MY_Config.php on line 81

    It would be great if this could be fixed.

    Matchbox uses a library called “Loader.php” that replaces CI’s native CI_Loader with an enhanced one.
    It looks like there is a loading conflict with the loading order between MPTree and Matchbox.

    I tried the following fix (rather a dirty hack):
    I cut the class from MY_Loader.php and pasted it into Matchbox’s Loader.php right after the CI_Loader class declaration.
    Then I deleted MY_Loader.php. Works like a charme. But way too dirty to keep it that way, imho.

  • #15 / May 16, 2008 8:44pm

    TheLoops's avatar

    TheLoops

    61 posts

    I myself have have written (or rather adapted) a nested tree model for CI.
    I actually just turned this class to a CI model that uses CI’s native Active Record.

    But unfortunately it does not have any useful editing functionality implemented.
    Hence I’ll most likely have to drop it in favor of MPTree.

    Nevertheless there are some features in my model wich I’d like to see in MPTree.

    (bool)  isDescendantOf(…)
    Would check if a given node is found in MPTree’s get_descendants()

    (bool) isChildOf(…)
    Would check if a given node is found in MPTree’s get_parents()

    (array) getFamilyBranch(…)
    Would fetch the immediately family of a node. More specifically, fetch a node’s siblings, parents, parents’ siblings, and its own direct children.

    (int) getDescendantsCounts(…)
    Would return MPTree’s get_descendants() with an additional field holding the node’s descendants count. (This is handy if want to print the count of descendants next to each node of a tree)

    (int) getChildrenCounts(…)
    Would return MPTree’s get_children() with an additional field holding the node’s children count. (This is handy if want to print the count of children next to each node of a tree)

    Furthermore I’m curious why you did not add a parent_id and a nlevel field to MPTree.
    Using such fields would make many of MPTree’s so more simple. Both: to understand for the user and also to maintenance.

    Just take thes simple query as example:

    MPTree’s approach WITHOUT a parent_id field:

    function count_children($lft,$rgt){
            $result = $this->db->query(
    "SELECT COUNT(*) as num FROM
    (SELECT node.*, (COUNT(parent.{$this->id_col}) - (sub_tree.depth + 1)) AS depth
    FROM {$this->tree_table} AS node,
        {$this->tree_table} AS parent,
        {$this->tree_table} AS sub_parent,
        (
            SELECT node.{$this->id_col}, (COUNT(parent.{$this->id_col}) - 1) AS depth
            FROM {$this->tree_table} AS node,
            {$this->tree_table} AS parent
            WHERE node.{$this->left_col} BETWEEN parent.{$this->left_col} AND parent.{$this->right_col}
        AND node.{$this->left_col} = {$lft}
            GROUP BY node.{$this->id_col}
            ORDER BY node.{$this->left_col}
        )AS sub_tree
    WHERE node.{$this->left_col} BETWEEN parent.{$this->left_col} AND parent.{$this->right_col}
        AND node.{$this->left_col} BETWEEN sub_parent.{$this->left_col} AND sub_parent.{$this->right_col}
        AND sub_parent.{$this->id_col} = sub_tree.{$this->id_col}
    GROUP BY node.{$this->id_col}
    HAVING depth = 1
    ORDER BY node.{$this->left_col}) as a");
            $result = $result->row_array();
            return $result['num'];
        }

    Versus an approach WITH a parent_id field:

    function count_children($lft,$rgt,$id = NULL){
            if($rgt - $lft < 3) // leaf node, 3 here because of the possibility of a gap (4 = have children)
                return array();
            
            $parent_col = $this->parent_col;
            if($id == NULL) {
                $current_node = $this->get_node($lft);
                $id = $current_node->$parent_col;
            }
                
            $this->db->select("COUNT({$this->id_col}) AS num",FALSE);
            $this->db->where($this->parent_col, $id);
            $this->db->limit(1);
    
            $query = $this->db->get($this->table);
            $result = $query->result();
            
            $result = $result->row_array();
            return $result['num'];
        }

    Or another MPTree’s approach WITHOUT a parent_id field:

    function AR_from_children_of($lft,$rgt){
            // Circumvent the db escaping to enable a subquery
            $this->db->ar_from[] = "(SELECT node.*, (COUNT(parent.{$this->id_col}) - (sub_tree.depth + 1)) AS depth
    FROM {$this->tree_table} AS node,
        {$this->tree_table} AS parent,
        {$this->tree_table} AS sub_parent,
        (
            SELECT node.{$this->id_col}, (COUNT(parent.{$this->id_col}) - 1) AS depth
            FROM {$this->tree_table} AS node,
            {$this->tree_table} AS parent
            WHERE node.{$this->left_col} BETWEEN parent.{$this->left_col} AND parent.{$this->right_col}
        AND node.{$this->left_col} = {$lft}
            GROUP BY node.{$this->id_col}
            ORDER BY node.{$this->left_col}
        )AS sub_tree
    WHERE node.{$this->left_col} BETWEEN parent.{$this->left_col} AND parent.{$this->right_col}
        AND node.{$this->left_col} BETWEEN sub_parent.{$this->left_col} AND sub_parent.{$this->right_col}
        AND sub_parent.{$this->id_col} = sub_tree.{$this->id_col}
    GROUP BY node.{$this->id_col}
    HAVING depth = 1
    ORDER BY node.{$this->left_col}) as child";
        }

    Versus an approach with parent_id field:

    function AR_from_children_of($lft,$rgt,$id = NULL){
        
            $parent_col = $this->parent_col;
            if($id == NULL) {
                $current_node = $this->get_node($lft);
                $id = $current_node->$id_col;
            }
            // Circumvent the db escaping to enable a subquery
            $this->db->ar_from[] = "(SELECT * FROM {$this->tree_table}
    WHERE {$this->parent_col} > $id AND {$this->left_col} > $lft AND {$this->right_col} < $rgt
    ORDER BY {$this->left_col} ASC) as descendant";
        }

    I highly recommend to use a parent_id field. It just makes things much easier.
    And the MySQL queries should also get a significant performance boost in some cases.

.(JavaScript must be enabled to view this email address)

ExpressionEngine News!

#eecms, #events, #releases