Not signed in (Sign In)

SkillShare - A place to discuss Web Standards and Web Design topics

Categories

Vanilla 1.1.9 is a product of Lussumo. More Information: Documentation, Community Support.

    • CommentAuthorlatinomigs
    • CommentTimeFeb 28th 2007
     permalink
    Hello,

    My overall goal is to build a website generated by PHP with a MySQL database storing content. From experimenting I have a good idea of how to do most of it. Where I'm confused is with the organization and generation of site navigation.

    My site nav is split into two sections:

    - A primary nav bar with 6 links
    - A left column with any number of secondary and tertiary navigation

    An example of the output markup for the primary nav is:

    <div id="priNav">
    <ul>
    <li><a href="#">Primary Link One</a></li>
    <li><a href="#">Primary Link Two</a></li>
    <li><a href="#">Primary Link Three</a></li>
    <li><a href="#">Primary Link Four</a></li>
    </ul>
    </div>

    And an example of the output markup for the secondary/tertiary nav is:

    <div id="secNav">
    <ul>
    <li><a href="#">Secondary Link One</a></li>
    <li><a href="#">Secondary Link Two</a>
    <ul>
    <li><a href="#">Tertiary Link One</a></li>
    <li><a href="#">Tertiary Link Two</a></li>
    </ul>
    </li>
    <li><a href="#">Secondary Link Three</a></li>
    </ul>
    </div>


    My first attempt at organizing the db tables for navigation resulted in the following table structures:

    dbt_nav_root
    --------------------
    id = The primary key for this table
    cont_id = A foreign key that links to the primary key of the content table
    text = The anchor tag text
    is_parent = a boolean designating whether this table has child links


    dbt_nav_child1,
    dbt_nav_child2
    --------------------
    id = The primary key for this table
    cont_id = A foreign key that links to the primary key of the content table
    parent_id = A foreign key that links to the primary key of it's parent link
    text = The anchor tag text
    is_parent = a boolean designating whether this table has child links


    I'm not sure how to measure the success of this structure because I can't find anything to compare it to. I guess it's been successful because I was able to make it work, but I'm anxious to validate and improve my attempt through comparison and your advice.

    So how do you guys handle this sort of thing?
    • CommentAuthordhayes
    • CommentTimeMar 1st 2007
     permalink
    you could get away with using a single table.. the dbt_nav_childx table for instance.. if the parent id is zero, it's the top, 1 secondary..etc..etc. i'd probably make the cont_id the primary as well, assuming it's unique from the other table.. it'll make more sense and provide a better index if you ever query against it. just a few off hand observations.. good luck with it.
    • CommentAuthorlatinomigs
    • CommentTimeMar 12th 2007
     permalink
    Ok, i've come further in my quest here and i've learned a lot about how to deal with hierarchies that come from a single database table. I'm focusing on two options:

    1. Recursion
    2. Modified Preorder Tree Traversal

    Both are great for taking data from a db table and organizing it into its appropriate hierarchical form... Unless you're using nested lists. And this is where my problem is. I'm having a lot of trouble coming up with an efficient solution for translating a list of hierarchical data into an XHTML nested list.

    Any ideas?
    • CommentAuthorPettyRider
    • CommentTimeMar 12th 2007 edited
     permalink
    Workin on something...

    ... any minute now...
    • CommentAuthorPettyRider
    • CommentTimeMar 13th 2007
     permalink
    ... oh well. Worked in this for a while and only came up with something that works for two levels. I'm not a master on recursion yet. The function expects an array of row objects from the database:

    function _build_menu_tree($list)
    {
    // If an array wasn't submitted there's nothing to do...
    if (empty($list))
    {
    return;
    }

    static $_used = array();

    $out = '';

    // Write the opening list tag
    $out .= "<ul>\n";

    foreach ($list as $link)
    {
    if (in_array($link->id, $_used))
    {
    continue;
    }
    $out .= '<li><a href="/function/'?phpMyAdmin=4594f30712f4fabaff6997416810f3f2 . $link->id . '">' . $link->text . '</a>';

    foreach ($list as $_link)
    {
    if ($link->id == $_link->parent_id)
    {
    $sub_list[] = $_link;
    }
    }

    if (!empty($sub_list))
    {
    $out .= _build_menu_tree($sub_list);
    }
    $out .= "</li>\n";

    $_used[] = $link->id;
    }


    // Write the closing list tag
    $out .= "</ul>\n";

    return $out;
    }
  1.  permalink
    Building a recursive nested xhtml list from scratch was insanely difficult, if I remember correctly. I might have a solution for you (if I manage to find it), but it tends to lose efficiency if you traverse too deep. Then again, that's recursion for you.
    • CommentAuthorlatinomigs
    • CommentTimeMar 14th 2007
     permalink
    I found a solution. Using recursion or the modified preorder tree traversal methods alone are both not very efficient for creating nested lists from hierarchies. But, put em together and you can make it work real nice. I have an example I'm working on that I will post in a new thread soon!
    • CommentAuthorPettyRider
    • CommentTimeMar 14th 2007
     permalink
    If you recursively build your hierarchy, then again recursively build your list, no problem. I think I was trying to hard to create one elegant function that did both, but again, it's only semi-recursive. This was my first stab at recursion, and yeah, new kind of ball game, but manageable once you figure out the concept
    •  
      CommentAuthortsk
    • CommentTimeMar 16th 2007
     permalink
    Related to this table structure with parent and descendant elements can you suggest an efficient delete function that will delete a parent and all its direct and indirect descendants ? I think it has to do with recursive programming but I can't get it to work more than 2 levels down.
    • CommentAuthorlatinomigs
    • CommentTimeMar 16th 2007
     permalink
    Answer:

    Let's assume you have a table called "food". In this table you have title column and a parent column. The title will act as the primary key or 'id' for this example. You have the following members in the table:

    title|parent
    food|
    fruit|food
    green|fruit
    pear|green
    red|fruit
    cherry|red
    yellow|fruit
    banana|yellow
    meat|food
    beef|meat
    pork|meat

    So you get the following hierarchy:

    food
     fruit
       green
        pear
       red
        cherry
       yellow
        banana
     meat
       beef
       pork

    Let's say you want to delete "fruit" and all of it's children. Your logic may be something like this:

    target fruit, delete all descendents
    delete banana
    delete yellow
    delete cherry
    delete red
    delete pear
    delete green
    delete fruit

    Here's the function (actually it's 2 functions)

    // The delete_children() function recieves 4 variables
    // 1. The id of the parent of all the child nodes you want to delete
    // 2. The name of the 'id' column in the table
    // 3. The name of the 'parent_id' column in the table
    // 4. The name of the table to delete from
    function delete_children($parentNode, $idCol, $parentCol, $table)
    {
    // Query all records, selecting the ID and Parent_id columns in the table
    $qSelect = mysql_query("SELECT $idCol, $parentCol FROM $table;");

    // Create an empty array and populate it as a 2D array with all the records from your query
    $aTree = array();
    while( $aRow = mysql_fetch_assoc($qSelect) )
    {
    $aTree[] = array
    (
    'id' => $aRow["$idCol"],
    'parent' => $aRow["$parentCol"]
    );
    }

    // The get_children() function will find all the children related to the parent variable
    // It receives the 2D array created above, and the parent variable received from the parent function
    function get_children($array, $parent)
    {
    // Iterate through and cast each member of the 2D array as a 1D array
    foreach($array AS $node)
    {
    // Check if the parent property of the current node matches the parent variable received by this function
    if($node['parent'] == $parent)
    {
    // If parents match,
    // concatinate the title property of the node to a string with a pipeline symbol
    // and initiate the function again to check for children of this node
    $aDelete .= $node['id'] . "|";
    $aDelete .= get_children($array, $node['id']);
    }
    }
    // Return the string with all the node members we will delete from the database table
    return $aDelete;
    }

    // Initiate the recursive function above and cast its output to a string variable
    $sChildren = get_children($aTree, $parentNode);

    // Attach the parent to the string with a pipeline symbol
    $sChildren = $parentNode . "|" . $sChildren;

    // Explode the string by the pipeline and cast to an array
    $aChildren = explode("|", $sChildren);

    // Reverse the array
    $aChildren = array_reverse($aChildren);

    // Array member $aFood[0] will have an empty value, so shift it (delete)
    array_shift($aChildren);

    // Loop through each member of our final array and concatinate it to a DELETE query
    foreach($aChildren AS $key => $value)
    {
    $qDelete = mysql_query("DELETE FROM $table WHERE $idCol = '" . $value . "' LIMIT 1;");
    }
    }

    delete_children('fruit', 'title', 'parent', 'food');


    For the database example above, youll end up with the following hierarchy if you were to query the remaining members:

    food
      meat
       beef
       pork


    Not sure if this is absolutely the most efficient way, but it works :)
    •  
      CommentAuthortsk
    • CommentTimeMar 18th 2007
     permalink
    thanks latinomigs for the solution. I haven't tried it yet but I also did some extra reseach.
    I found this powerpoint presentation on more advanced uses for MySQL instructions in both hierarchic table and nested lists.

    A certain slide presents a delete method that uses strictly MySQL and seems at least more clean (nested list usage). I don't know if it's more efficient but I intend on trying both methods, yours and the one presented and time them.

    Thank you for sharing the info.
  2.  permalink
    Huzzah! I think I finally figured this out. You can most definately build an XHTML nested list from a numbered tree in a database, no matter how deep the branches go. And you can do so without using recursive functions. You'll need a couple of loops, though.

    This would have been a huge time-saver on a earlier projects, if only I'd had the tenacity to see this through before, since It only took a couple of hours to code from scratch. Most of that time went into figuring out the nested list output in XHTML.

    A single database table, from which you can print out everything from menus to breadcrumb paths or sitemaps makes everything so much simpler. It's quite simple to insert or delete branches, also.

    I'll have to do some tweaking and testing before I share the code (English variable names and such might be a bit easier to understand than Finnish ones).
    • CommentAuthoracopic
    • CommentTimeMay 1st 2007
     permalink
    Recursion is really pretty easy. It's easier to create 2 functions. The public function that is called to begin the recursion and a helper function. An example is listed below. Note: This example uses an Active Record database pattern to create a multi-dimensional array - but the fundamentals are exactly the same.

    /**
    * Load all categories in nested format
    */
    public function loadCategoryTree() {
    $categories = array();
    // Call helper function and specify 0 as parent ID (because this is the root level)
    $categories = $this->loadCategoryTree_Helper(0);
    return $categories;
    }

    /**
    * Load all categories in nested format
    */
    public function loadCategoryTree_Helper($parent_id) {
    $categories = array();
    $counter = 0;
    // Load matching categories
    $rows = $this->query("SELECT * FROM " . $this->_table . " WHERE parent_id = " . $parent_id . " ORDER BY position ASC");
    foreach($rows as $row) {
    $categories[$counter]['id'] = $row->id;
    $categories[$counter]['name'] = $row->name;
    $children = $this->loadCategoryTree_Helper($row->id);
    if(count($children) > 0) {
    $categories[$counter]['children'] = $children;
    }
    $counter ++;
    }
    return $categories;
    }
    • CommentAuthoracopic
    • CommentTimeMay 1st 2007
     permalink
    In response to kari.patila - off the top of my head it's impossible to create a nested list (to unlimited depth) without recursion. If you were simply using loops without recursion you'd need to physically code a loop for every possible depth/node. You basically need a loop for every node to check whether it has a child node, and then to see if that child node has a child node etc etc. Without recursion this is impossible (although if you knew that there was only going to be 2 depths etc it would be achievable).
    •  
      CommentAuthorkari.patila
    • CommentTimeMay 1st 2007 edited
     permalink
    Sorry, I meant without any recursive php loops. It just occurred to me too, that I might have left that out. As I understand it, it's the MySQL that does the recursive part in this case. The tree is represented as a nested set in the database table. The resulting php array is then looped through and the xhtml printed out after going through a couple of conditional clauses.
    • CommentAuthorPettyRider
    • CommentTimeMay 1st 2007 edited
     permalink
    I've finally solved this with a lot of help from the guy with an actual Computer Science degree at work. It really only took a few lines of code and didn't require more than one call to the database. And it required only one function. Took us a good hour to hammer out.
  3.  permalink
    I did it with two calls to the database, inside one function. Before this thread gets sidetracked any further from the point, let me ask you this: did anyone solve the problem of actually generating valid, nested xhtml list structure from the database? I did that, and sounds like PettyRider did also.
    • CommentAuthorPettyRider
    • CommentTimeMay 1st 2007 edited
     permalink
    I guess the amount of calls to the database really depends more on how your database is structured. In our case, we ended up with an array of objects like:

    Array
    {
    [stdClass Object] =>
    [id] => 2
    [pid] => 1
    }

    and then just passed the array and a parent to a function _build_tree($array, $pid = FALSE) and recursively hunted for children (or passed it no $pid and let it figure out the parent on the first go). The initial function call and each recursive call were run through a theming function that built the nested list.
    • CommentAuthorvarland
    • CommentTimeMay 2nd 2007 edited
     permalink

    I read this thread, and am wondering what I'm missing that makes this so difficult. I read the description of the problem, and here's what I came up with in 10 minutes:

    <?php

    // Store starting time.
    $start = microtime(true);

    // Made up array of items from database.
    $items = array(
    (object) array('id' => 1,
    'parent_id' => false,
    'position' => 1,
    'url' => 'http://www.google.com',
    'title' => 'Item #1'),
    (object) array('id' => 2,
    'parent_id' => 1,
    'position' => 3,
    'url' => 'http://www.google.com',
    'title' => 'Item #2'),
    (object) array('id' => 3,
    'parent_id' => 1,
    'position' => 1,
    'url' => 'http://www.google.com',
    'title' => 'Item #3'),
    (object) array('id' => 4,
    'parent_id' => 3,
    'position' => 1,
    'url' => 'http://www.google.com',
    'title' => 'Item #4'),
    (object) array('id' => 5,
    'parent_id' => 1,
    'position' => 2,
    'url' => 'http://www.google.com',
    'title' => 'Item #5'),
    (object) array('id' => 6,
    'parent_id' => false,
    'position' => 3,
    'url' => 'http://www.google.com',
    'title' => 'Item #6'),
    (object) array('id' => 7,
    'parent_id' => false,
    'position' => 2,
    'url' => 'http://www.google.com',
    'title' => 'Item #7')
    );

    // Method for use with usort to sort items by position.
    function sort_by_position($item_a, $item_b) {
    if ($item_a->position == $item_b->position) return 0;
    return ($item_a->position < $item_b->position) ? -1 : 1;
    }

    // Method for use with usort to sort items by parent_id.
    function sort_by_parent_id_and_position($item_a, $item_b) {
    if ($item_a->parent_id == $item_b->parent_id) {
    if ($item_a->position == $item_b->position) return 0;
    return ($item_a->position < $item_b->position) ? -1 : 1;
    }
    return ($item_a->parent_id < $item_b->parent_id) ? -1 : 1;
    }

    // Recursive function to create <ul>-based dropdown menu.
    function menu($items, $parent_id = false) {

    // Find all items at this level.
    $items_at_this_level = array();
    foreach ($items as $i) {
    if ($i->parent_id == $parent_id) $items_at_this_level[] = $i;
    elseif ($i->parent_id > $parent_id) continue;
    }

    // Return false if no items were found.
    if (count($items_at_this_level) == 0) return false;

    // Sort the items.
    usort($items_at_this_level, 'sort_by_position');

    // Initialize a string to store the HTML for this level.
    $this_level = '<ul>';

    // For each item at this level...
    foreach ($items_at_this_level as $item) {

    // Create the hyperlink.
    $link = '<a href="'?phpMyAdmin=4594f30712f4fabaff6997416810f3f2 . $item->url . '">' . $item->title . '</a>';

    // Create the list item for this item.
    $this_level .= '<li>' . $link . menu($items, $item->id) . '</li>';
    }

    // Return the markup for this level.
    return "$this_level</ul>";
    }

    // Prepare the array of menu items.
    usort($items, 'sort_by_parent_id_and_position');

    // Print the menu markup.
    echo(menu($items));

    // Store ending time.
    $end = microtime(true);

    // Print elapsed time.
    echo($end - $start . ' seconds to complete function.');

    ?>

    As far as I can tell, it does absolutely everything you're looking for (and more) except for actually make the database call. Any comments?

    If you're wondering what I meant by "and more", I added a Rails-style position column for sorting the menu items at each level. I haven't seen any of the other solutions to this problem on this thread address that issue, but I think it's pretty important. I also added the stupid microtime() stuff to (very basically) test the efficiency of this script, and it seems pretty fast to me (knowing that the biggest bottleneck, database access, isn't there). Finally, I also presorted the main array to increase efficiency (although I don't know that the efficiency gain makes up for the time spent sorting the array).

    P.S. For those of you who've seen my other posts, you know that I prefer Rails to PHP… this is a great illustration of why. This would take about 5 code statements with Rails.

    • CommentAuthorPettyRider
    • CommentTimeMay 2nd 2007 edited
     permalink
    I know it can be done in a third of that code. As far as the Rails thing goes, Rails is a framework for Ruby. PHP is PHP. I'm sure a PHP framework could handle this in a few lines too. So, how many lines of Ruby code does it take for Rails to do this? That's a proper comparison. And then, which one runs faster.
    • CommentAuthorvarland
    • CommentTimeMay 2nd 2007
     permalink
    PettyRider... I agree. Comparing the "Rails way" and the "PHP way" isn't really fair. The difference, as I've found it, is this: Rails feels like a programming language, even though it's a framework for the Ruby language. The PHP frameworks I've tried, and I've tried a lot, feel clunky. I don't doubt that the Ruby way involves more lines of pure Ruby code and isn't quite as efficient (Ruby simply does not execute very quickly), but, in most situations, developer efficiency is worth much more than the cost of a little bit of code inefficiency. That's why Ruby, PHP, and other dynamic programming languages are exploding. Dynamic, interpreted languages will never be as fast as compiled languages, but they don't need to be. A few milliseconds of execution time (if that) isn't worth hours of a developer's time. If your site goes big, and you're getting enough requests where you need those precious milliseconds (see all the recent talk about Twitter), then you should be able to afford a code guru who can work on squeezing time out of the code.
  4.  permalink
    The difficulty level seems to be related to the structure of the database, as the queries can get pretty complex, at least when dealing with a numbered tree. The efficiency of Varland's script seems to be the about the same as mine, when handling the same amount of data, even though the original data is represented differently in the database.
Add your comments
    Username Password
  • Format comments as (Help)