Laurence
Laurence

Reputation: 409

PHP: Loop through multidimensional array and establish parent-child relationships between array items

I am developing a content management system and I have run into an issue with child-parent relationships of items in the CMS.

Basically I have a system that can create pages and when a page is created you can select a parent page for sub-navigation. This is all fine and dandy until I try to generate the navigation from the DB.

I'm not sure if some sort of join would be better but I prefer to get all the data in an array and manipulate the array with php.

My array of results from the DB is like this:

Array
(
    [0] => Array
    (
        [id] => 27
        [name] => home
        [link] => home.html
        [parent] => 0
    )

    [1] => Array
    (
        [id] => 30
        [name] => about
        [link] => about.html
        [parent] => 27
    )
)

I need to loop through an array like this that can have any number of navigation and intelligently sort it into its parent child relationships. I was able to do it but only one level deep. It needs to manage children with children with children etc. with an infinite number of layers and output it to HTML unordered nested lists.

<ul>
  <li>
  <a>Nav</a>
     <ul>
        <li>
           <a>Subnav</a>
             <ul>
                 <li>
                    <a>Sub Sub nav</a>
                 </li>
             </ul>
        </li>
     </ul>
  <li>
<ul>

Etc. ...

Upvotes: 4

Views: 12610

Answers (4)

vortextangent
vortextangent

Reputation: 571

Saad Imran's solution works alright except for if you would like multiple navigation items in the same list. I had to change a few lines to get it to generate validated list of items. I also added indents to make generated code more readable. I'm using spaces but this can be easily changed to tabs if you prefer, just replace 4 spaces with "\t". (note you must use double quotes, not single quotes, otherwise php doesn't replace with a tab character but actually a \t)

I am using this in codeigniter 2.1.0 with superfish (PHP5)

function GenerateNavHTML($nav, $tabs = "")
{
    $tab="    ";
    $html = "\n$tabs<ul class=\"sf-menu\">\n";
    foreach($nav as $page)
    {
        //check if page is currently being viewed
        if($page['link'] == uri_string()) {
            $html .= "$tabs$tab<li class=\"current\">";
        } else {
            $html .= "$tabs$tab<li>";
        }
        $html .= "<a href=\"$page[link]\">$page[name]</a>";
        //Don't generate empty lists
        if(isset($page['sub'][0])) {
            $html .= $this->GenerateNavHTML($page['sub'], $tabs.$tab);
        }
        $html .= "</li>\n";
    }
    $html .= $tabs."</ul>\n";

    return $html;
}

I was getting (switched to ol for clarification)

1. Home  
1. About Us  
1. Products  
    1. sub-product 1  
    1. sub-product 2  
1. Contact  

now i get

1. Home  
2. About Us  
3. Products  
    1. sub-product 1  
    2. sub-product 2  
4. Contact  

Nicely generated HTML

<ul class="sf-menu">
    <li class="current"><a href="index.html">Home</a></li>
    <li><a href="about.html">About Us</a></li>
    <li><a href="products.html">Products</a>
        <ul class="sf-menu">
            <li><a href="products-sub1.html">sub-product 1</a></li>
            <li><a href="products-sub2.html">sub-product 2</a></li>
        </ul>
    </li>
    <li><a href="contact.html">Contact</a></li>
</ul>

Upvotes: 3

user569143
user569143

Reputation:

I am working on the same project, and I have not found found the need to use objects. The database pretty much takes care of the structure, and php functions can do the rest. My solution was have a parent field for pages that points to a section name, as you have, but also to add a parent field for the sections table, so that sections can point to other sections as parents. It is still very manageable because there are only 2 parent fields to keep track of, one for each table, and I can nest my structure as many levels as necessary in the future.

So for dynamic tree creation, I am going to recursively check for parents until I hit a null, which indicates to me that the current element sits on the doc root. This way we don't need to know any details of the current page structure in the function code, and we can focus on just adding and arranging pages in mysql. Since another poster showed some menu html, I thought I would add an example here for a dynamic breadcrumb path because the themes are so similar.

DATA

// Sample 3 x 2 page array (php / html pages)
// D1 key = page name
// D2 element 1 = page name for menu display
// D2 element 2 = parent name (section)
$pages = Array
(
  'sample-page-1.php' => Array ( 'M' => 'page 1', 'P' => null ),
  'sample-page-2.php' => Array ( 'M' => 'page 2', 'P' => 'hello' ),
  'sample-page-3.php' => Array ( 'M' => 'page 3', 'P' => 'world' )
);

// Sample 2 x 1 section array (parent directories)
// D1 key = section name
// D2 element = parent name (if null, assume root)
$sections = Array
( 
  'hello' => null,
  'world' => 'hello'
);

$sep = ' > ';       // Path seperator
$site = 'test.com'; // Home string

FUNCTIONS

// Echo paragraph to browser
function html_pp ( $text )
{
  echo PHP_EOL . '<p>' . sprintf ( $text ) . '</p>' . PHP_EOL;
}

// Get breadcrumb for given page
function breadcrumb ( $page )
{
  // Reference variables in parent scope
  global $pages;
  global $sections;
  global $sep;
  global $site;

  // Get page data from array
  $menu   = $pages [ $page ] [ 'M' ];
  $parent = $pages [ $page ] [ 'P' ];

  if  ( $parent == null )
  {
    $path = $site . $sep . $menu;
  }
  else
  {
    $path = $site . $sep . get_path ( $parent ) . $sep . $menu;
  }
  return $path;
}

// Trace ancestry back to root
function get_path ( $parent )
{
  // Reference variables in parent scope
  global $sections;
  global $sep;

  if ( $sections [ $parent ] == null )
  {
    // No more parents
    return $parent;
  }
  else
  {
    // Get next parent through recursive call
    return get_path ( $sections [ $parent ] ) . $sep . $parent;
  }
}

USAGE

// Get breadcrumbs by page name
$p1 = 'sample-page-1.php';
$p2 = 'sample-page-2.php';
$p3 = 'sample-page-3.php';

html_pp ( $p1 . ' || ' . breadcrumb ( $p1 ) );
html_pp ( $p2 . ' || ' . breadcrumb ( $p2 ) );
html_pp ( $p3 . ' || ' . breadcrumb ( $p3 ) );

// or use foreach to list all pages
foreach ( $pages as $page => $data)
{
  html_pp ( $page . ' || ' . breadcrumb ( $page ) );
}

OUTPUT

sample-page-1.php || test.com > page 1

sample-page-2.php || test.com > hello > page 2

sample-page-3.php || test.com > hello > world > page 3

Upvotes: 0

Qqwy
Qqwy

Reputation: 5649

The easiest way of doing this would probably be using objects. These can be easily manipulated and also created quite easily from the array you get from the database.

An example, every object has:

  1. an ID
  2. a name
  3. a link
  4. an object reference to the parent object
  5. a list of child objects

You will need a static function that is able to find out what object has a certain database ID. This is done during the creation of a new object, by putting a reference in a static list. This list can then be called and checked against at runtime.

The rest is quite straightforward:

$arr = get_array_from_database();
foreach($arr as $node){
    $parent_object = get_parent_object($node_id);
    $parent_object.subnodes[] = new NodeObject($node);
}

As for returning the objects in a list, this is best done recursively;

function return_in_list($objects)
{
    foreach($objects as $node)
    {
        echo '<li>';
        echo '<a>' + node.link + '</a>';

        if(node.subnodes.length > 0)
        {
            echo '<ul>';
            return_in_list($node.subnodes);
            echo '</ul>';
        }
    puts '</li>'
    }
}

Upvotes: 1

Saad Imran.
Saad Imran.

Reputation: 4530

I don't think you should get into objects. Plus I think it would just be extra work to generate objects and etc. In my opinion you should loop through the array and generate a multidimensional array that represents the navigational hierarchy and then loop the generated array recursively to generate your HTML. I've done a sample code for you, it works the way you want it to but you probably want to make some changes.

functions

// Generate your multidimensional array from the linear array
function GenerateNavArray($arr, $parent = 0)
{
    $pages = Array();
    foreach($arr as $page)
    {
        if($page['parent'] == $parent)
        {
            $page['sub'] = isset($page['sub']) ? $page['sub'] : GenerateNavArray($arr, $page['id']);
            $pages[] = $page;
        }
    }
    return $pages;
}

// loop the multidimensional array recursively to generate the HTML
function GenerateNavHTML($nav)
{
    $html = '';
    foreach($nav as $page)
    {
        $html .= '<ul><li>';
        $html .= '<a href="' . $page['link'] . '">' . $page['name'] . '</a>';
        $html .= GenerateNavHTML($page['sub']);
        $html .= '</li></ul>';
    }
    return $html;
}

** sample usage **

$nav = Array
(
    Array
    (
        'id' => 27,
        'name' => 'home',
        'link' => 'home.html',
        'parent' => 0
    ),
    Array
    (
        'id' => 30,
        'name' => 'about',
        'link' => 'about.html',
        'parent' => 27
    )
);

$navarray = GenerateNavArray($nav);
echo GenerateNavHTML($navarray);

You can probably do both things in one step but I think it's neater to generate the multidimensional array first. Goodluck!

Upvotes: 14

Related Questions