Adjacency Model

I am looking for a tutorial or some help in making a menu using the Adjacency Model. I have been looking for hours now and found a lot of articles and discussions about the subject, but with most of them the styling was/is done within the function like:

function makeList($par_id = 0) {
    //your sql code here
    $subsql("SELECT * FROM pages WHERE pag_parent = $par_id");
    $pages = $dbch->fetchAll();

    if (count($pages)) {
        echo '<ul>';
        foreach ($pages as $page) {
            echo '<li>', $page['pag_title'];
            makeList($page['pag_id']);
            echo '</li>';
        }
        echo '</ul>';
    }
}

Where I’m working in a MVC structure and I really don’t have an idea how to translate it into my situation. This is my table:

CREATE TABLE `menus` (
  `id` int(11) NOT NULL,
  `menu_name` varchar(32) NOT NULL,
  `parent_id` int(11) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Edit: In the example above they are calling the function again within the function it self like this:

makeList($page['pag_id']);

I on the other hand send the result from the query from the Controller to the View.

$menu_items = $this->page->get_menu();

And in the View I use a foreach loop to go over the results fromthe variable $menu_items

<?php foreach($menu_items as $menu_item): ?>
   ......
<?php endforeach; ?> 

So should I do or adjust to get the same result.

Thank you in advance

So you want to first fetch the menu into a variable and display it in the view which doesn’t have direct access to the database?

First, change the makeList function to your getMenu and replace echos with an assignment of each menu item into an array - of course, you don’t assign any html here, only data like name or id. The function should return a single array item corresponding to a single menu item. You should leave the function recursive so that you will end up with multi-level multidimensional array. Assign the deeper level menu item into an array key of your choice, for example submenu (in the recursive bit in the foreach).

In your view you will need to iterate over the variable recursively, too. So you need a similar recursive function in your view but this time it needs to read data from the array and output html. You will recognize the submenu items by checking for the existence of the submenu key - if it exists then the function should call itself with the value as the parameter.

Hi Lemon_Juice. Sorry for a late reaction, I was gone for a short holiday. Thank you for the reply. Sorry for my ignorance, but can you show me waht you mean with an example. I am not sure what you mean. Thank you in advance

For some reason I still can’t get this to work. Right now I have the following two queries:

	public function get_navigation($lang)
	{
		$sql = "SELECT SN.page_id
					 , SN.navigation_name
		             , SN.navigation_class
		             , SN.navigation_title
					 , SN.navigation_url
					 , SP.parent_id
				  FROM site_navigation SN
				  JOIN site_pages SP ON SN.page_id = SP.page_id 
				 WHERE SN.language_abbr = ?
				   AND SP.parent_id = 0";
        $stmt = $this->pdo->prepare($sql);
		$stmt->execute(array($lang));
		return $stmt->fetchAll();			 
	}
	
	public function get_subs($lang)
	{
		$sql = "SELECT SN.navigation_name
		             , SN.navigation_class
		             , SN.navigation_title
					 , SN.navigation_url
					 , SP.parent_id
				  FROM site_navigation SN
				  JOIN site_pages SP ON SN.page_id = SP.page_id 
				 WHERE SN.language_abbr = ?
				   AND SP.parent_id > 0";
				   
        $stmt = $this->pdo->prepare($sql);
		$stmt->execute(array($lang));
		$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
		
		$sub_items = array();
		foreach ($result as $row) {
			if (isset($sub_items[$row['parent_id']]) && ($sub_items[$row['parent_id']] > 0 )) {
				$sub_items[$row['parent_id']] = array();
			}
    		$sub_items[$row['parent_id']][] = $row;
		}
		return $sub_items;			 
	}

These are send to the View from the controller:

 'menu_items'      => $this->page->get_navigation($this->lang),
'sub_items'          => $this->page->get_subs($this->lang),		 

And this is what I have in the View for now:

<?php foreach ($menu_items as $item): ?>
<li><a href="<?= $item['navigation_url']; ?>" class="<?= $item['navigation_class']; ?>" ><?= $item['navigation_name']; ?></a>
	<?php if ($item['page_id'] == 2 || $item['page_id'] == 3): ?>
	<ul>
		<?php foreach($sub_items[$item['parent_id']] as $value): ?>
		<li></li>
		<?php endforeach; ?>
	</ul>
	<? endif; ?>
</li>
<?php endforeach; ?>

But I get the following errors:

Undefined offset: 0 in .....

and

Invalid argument supplied for foreach() in ....

I know this is not the right way to handle the adjacency model and that actually everything should be in just one query, but i really have no idea how to do it differently.

Can someone please help me in the right direction. Thank you in advance

Ok. I am a step further I think. I was able to change the two queries into one the following way:

	public function get_menu_items($lang)
	{
		$sql = "SELECT SN.page_id
					 , SN.navigation_name
		             , SN.navigation_class
		             , SN.navigation_title
					 , SN.navigation_url
					 , SP.parent_id
				  FROM site_navigation SN
				  JOIN site_pages SP ON SN.page_id = SP.page_id 
				 WHERE SN.language_abbr = ?";
				 
        $items = $this->pdo->prepare($sql);
		$items->execute(array($lang));
		
		$menu_items = array();
		foreach ($items as $item) {
			if (!isset($menu_items[$item['parent_id']])) {
				$menu_items[$item['parent_id']] = array();	
			}
			$menu_items[$item['parent_id']][] = $item;	
		}
		return $menu_items;		
	}

In the front Controller I send this to the View:

$menu_items      = $this->page->get_menu_items($this->lang);

and this in the View:

<ul>	
	<?php $parent_id = 0; foreach ($menu_items[$parent_id] as $item):?>
	<li><a href="#"><?= $item['navigation_name']; ?></a></li>
    <?php endforeach; ?>
</ul>

So far so good. I dont get any errors and it shows me the menu_items with parent_id = 0. But I am not sure how to continue so I get the sub menus for page_id 2 and 3 as well. I think I am very near but have no idea how to continue. Any help from this point forward would be highly appreciated!

First of all, do you have submenu items in your $menu_items variables? If you posted here a result of print_r($menu_items) it would clear up a lot what structure it has.

Ideally, you need to make it a multi-dimensional array with a structure like this:

Array
(
    [0] => Array
        (
            [name] => Home
            [link] => /
        )

    [1] => Array
        (
            [name] => Information
            [link] => /info
            [subitems] => Array
                (
                    [0] => Array
                        (
                            [name] => About Us
                            [link] => /about-us
                        )

                    [1] => Array
                        (
                            [name] => Contact Info
                            [link] => /contact
                        )

                    [2] => Array
                        (
                            [name] => Our shops
                            [link] => /shops
                        )

                )

        )

    [2] => Array
        (
            [name] => Products
            [link] => /products
            [subitems] => Array
                (
                    [0] => Array
                        (
                            [name] => Electric Equipment
                            [link] => /electric
                            [subitems] => Array
                                (
                                    [0] => Array
                                        (
                                            [name] => Light Bulbs
                                            [link] => /electric/light-bulbs
                                        )

                                    [1] => Array
                                        (
                                            [name] => Mowers
                                            [link] => /electric/mowers
                                        )

                                    [2] => Array
                                        (
                                            [name] => Fridges
                                            [link] => /electric/fridges
                                        )

                                )

                        )

                    [1] => Array
                        (
                            [name] => Electronic Equipment
                            [link] => /electronic
                            [subitems] => Array
                                (
                                    [0] => Array
                                        (
                                            [name] => Computers
                                            [link] => /electronic/computers
                                        )

                                    [1] => Array
                                        (
                                            [name] => Laptops
                                            [link] => /electronic/laptops
                                        )

                                )

                        )

                )

        )

)

Each menu item has two mandatory keys: name and link. Optionally, there can be subitems, which is an array of items deeper under the branch. As you can see the nesting level can go deeper and deeper indefinitely. In order to achieve this structure you need to query your database table recursively - search for “recursive functions in PHP” and you will know what I mean.

Then in order to display this kind of array in html you need another recursive function - in your template - that will iterate over it and all its branches and output a properly nested set of <ul> and <li> tags.

This may seem too convoluted for some cases and reading recursively from the database is not very efficient because you will have many SELECTs - unless your database supports recursion (MySQL doesn’t). So you can take some shortcuts - to get rid of recursion you can code for, say, maximum of 2 levels deep - then you have a simple non-recursive function but you have 3 foreach loops - each one nested in another. This might be sufficient - however, still it’s inefficient with SELECTs.

There are some workarounds for this problem, one of them is adding another column to your table and apart from parent_id store a full path of row IDs to every category, for example a category Products > Electronic Equipment > Computers will have path 0003/0005/0010/ - supposing each number is an ID of the corresponding category in the path. Fixed-length zero-padded numbers allow you to easily calculate the depth level of each category by dividing the length by 5 in this case.

Then certain queries become much simpler. To get all subitems of Products you can use WHERE path LIKE '0003/%'. The only problem now is correct order. You can do this to select all your menu:

SELECT * FROM site_navigation ORDER BY path

and you’ll get a properly nested result but in the order of ID numbers, which doesn’t make sense. Therefore, you can add another column global_position - which is a number denoting position of each item menu within the whole tree regardless of its depth. So then you can quickly get all your menu like this:

SELECT *, FLOOR(LENGTH(path)/5) AS depth
FROM site_navigation ORDER BY global_postion

and depth will tell you how many levels deep is each menu item, which you can use to properly load this data into your menu variable.

The downside is that your table will become denormalized and on each update you will need to update additional columns: path and global_position, which can be resource hungry but in most ordinary scenarios pretty acceptable for occasional site updates. You can calculate path and global_position at any time (with a recursive function) according to the parent_id column, which is the one that truly defines your tree structure and must be regarded as source data for the denormalized columns.

I implemented a similar solution many years ago in an online shop for the category tree (about 400 items) and it works pretty well and is fast. But it adds some complexity to the system so I would certainly prefer to avoid such workarounds if only possible.

Hi Lemon_Juice, Thanks for the reply. The print_r on menu_items gives me:

Array 
( 
	[] => Array 
		( 
			[0] => Array 
				( 
					[navigation_id] => 1 [0] => 1 
					[page_id] => 1 [1] => 1 
					[parent_id] => [2] => 
					[language_abbr] => en [3] => en 
					[navigation_name] => Home [4] => Home  
				) 
				
			[1] => Array 
				( 
					[navigation_id] => 2 [0] => 2 
					[page_id] => 2 [1] => 2 [parent_id] => [2] => 
					[language_abbr] => en [3] => en 
					[navigation_name] => Flying [4] => Flying 
				) 
				
			[2] => Array 
				( 
					[navigation_id] => 3 [0] => 3 
					[page_id] => 3 [1] => 3 
					[parent_id] => [2] => 
					[language_abbr] => en [3] => en 
					[navigation_name] => About [4] => About 
				) 
				
			[3] => Array 
				( 
					[navigation_id] => 4 [0] => 4 
					[page_id] => 4 [1] => 4 
					[parent_id] => [2] => 
					[language_abbr] => en [3] => en 
					[navigation_name] => Contact [4] => Contact 
				)) 
				
				[2] => Array 
					( 
						[0] => Array 
							( 
								[navigation_id] => 5 [0] => 5 
								[page_id] => 5 [1] => 5 
								[parent_id] => 2 [2] => 2 
								[language_abbr] => en [3] => en 
								[navigation_name] => Paragliding articles [4] => Paragliding articles 
							) 
							
						[1] => Array 
							( 
								[navigation_id] => 6 [0] => 6 
								[page_id] => 6 [1] => 6 
								[parent_id] => 2 [2] => 2 
								[language_abbr] => en [3] => en 
								[navigation_name] => A typical day [4] => A typical day 
							) 
							
						[2] => Array 
							( 
								[navigation_id] => 7 [0] => 7 
								[page_id] => 7 [1] => 7 
								[parent_id] => 2 [2] => 2 
								[language_abbr] => en [3] => en 
								[navigation_name] => Prices & Levels [4] => Prices & Levels 
							) 
							
						[3] => Array 
							( 
								[navigation_id] => 8 [0] => 8 
								[page_id] => 8 [1] => 8 
								[parent_id] => 2 [2] => 2 
								[language_abbr] => en [3] => en 
								[navigation_name] => Non-Flyers [4] => Non-Flyers 
							) 
						[4] => Array 
							( 
								[navigation_id] => 9 [0] => 9 
								[page_id] => 9 [1] => 9 
								[parent_id] => 2 [2] => 2 
								[language_abbr] => en [3] => en 
								[navigation_name] => Important information [4] => Important information 
							)
							
						[5] => Array 
							( 
								[navigation_id] => 10 [0] => 10 
								[page_id] => 10 [1] => 10 
								[parent_id] => 2 [2] => 2 
								[language_abbr] => en [3] => en 
								[navigation_name] => Travel & Transfers [4] => Travel & Transfers 
							) 
			) 
						
)

Which ia based on this query:

	public function get_menu_items($lang)
	{
		$sql = "SELECT *
				  FROM site_navigation 
				 WHERE language_abbr = ?";
				 
        $items = $this->pdo->prepare($sql);
		$items->execute(array($lang));
		
		$menu_items = array();
		foreach ($items as $item) {
			if (!isset($menu_items[$item['parent_id']])) {
				$menu_items[$item['parent_id']] = array();	
			}
			$menu_items[$item['parent_id']][] = $item;	
		}
		return $menu_items;		
	}

Your array seems to be a bit messed up since it does not contain items nested in other items and you have some items in an element with an empty key - probably coming from null parent_id. It’s hard to iterate over such an array.

You have two options:

  1. Change your get_menu_items() so that you get a properly nested multidimensional array like the example I posted above. You don’t need to have exactly the same keys and data like I posted but the structure is important.

  2. De-normalize your table and get the whole menu in a single query like I posted above so that you end up with a one-dimensional array but each item will contain the depth value, which you can then use in your template to know how deep each item should be nested.

Sorry, I can’t write code for you because it is quite a complex subject and I don’t have time at the moment to spend on this. But I’ve given you some pointers and by experimenting you should be able to work out the solution.

And there are some good articles on how to retrieve a tree of items from the database , if you haven’t read them yet I highly recommend:

Categories and Subcategories
Managing Hierarchical Data in MySQL
Manage hierarchical data with MySQL stored procedures

And for nearest future MySQL 8:
Managing Hierarchical Data in MySQL Using the Adjacency List Model

Hi lemon_Juice… Thanks again for your replies. Maybe for now, this goes a step to far for me, I had it working but not, I think, in the right way. Using three different queries instead of one. But I need to deliver on friday so for now I will step back to my old system :(, but will for sure read more into this matter. Because I really would like to understand this. Thank you for your patience and explanations.

Cheers donboe

1 Like

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.