CakePHP Editable Table Based Dynamic Superfish JQuery Menus (<==mouthful)

Written by James McDonald

March 31, 2015

Below is one way to create dynamic menus in CakePHP. It uses an element, a custom helper and a database table setup for the Tree behavior.

It uses CakePHP 2.6.3.

Since posting the below I have made some improvements and posted them to github ==> https://github.com/jmcd73/superfish-db-menus

Caveats:

  1. The custom helper “MenuHelper.php” contains eval statements … used to parse a text array(‘controller’=>”, ‘action’=>”) into PHP. Evals are really a bad idea. If you are thinking of taking user input change the code to be safer. (I have tried to mitigate by checking that the input is an array)
  2. You would need some extra code to make things on the same menu level be sorted in the correct order
  3. Use the superfish samples to help you
  4. I’m really tired and I have stayed up way too late posting this.

Create a database that contains a table as follows

CREATE TABLE `menus` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT NULL,
  `controller_action_value` varchar(100) DEFAULT NULL,
  `title` varchar(100) DEFAULT NULL,
  `lft` int(11) DEFAULT NULL,
  `rght` int(11) DEFAULT NULL,
  `created` datetime DEFAULT NULL,
  `modified` datetime DEFAULT NULL,
  `sort` int(11) DEFAULT NULL,
  `active` tinyint(1) DEFAULT NULL,
  `parent_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Once you have created the database schema. Save it so you can restore it or move it to another app when needed

cd app/
Console/cake schema generate

# schema.php will be written to app/Config/Schema

cd Config/Schema

../../Console/cake schema dump --write filename.sql

# write an SQL file too so you can just go

mysql -u menudb -p mendbpw menudb < filename.sql

 

 

Use app/Console/cake to bake the MVC

cd app/
Console/cake bake all

Then create the custom helper in app/View/Helper/MenuHelper.php

<?php

App::uses('AppHelper', 'View/Helper');

class MenuHelper extends AppHelper {
	public $helpers = array('Html');
	public function buildMenu($menu_array, &$menu = null) {
	
	foreach($menu_array as $key => $value) {
         
      
          if ( is_array($value) && array_key_exists('Menu', $value)){
		$menu .= '<li>';
                
		$url = "#";
                
                // make sure it's a routing array and not malicious code
                // eval be bad
                $pattern = '/^\s*array\s*\(.*(\'|\")(controller|action)(\'|\").*\)\s*$/';
                if (preg_match($pattern, $value['Menu']['controller_action_value'])){
                    $url = eval( "return " . $value['Menu']['controller_action_value'] . ";" );
                }
                
                // attribute array
                $pattern = '/^\s*array\s*\(.*\)\s*$/';
                $options = array();
                if (preg_match($pattern, $value['Menu']['title'])){
                    $options = eval( "return " . $value['Menu']['title'] . ";" );
                }
                
		$menu .= $this->Html->link($value['Menu']['name'], $url, $options);

             # echo '<li><a href="#">' . $value['Menu']['name'] . '</a>';
          }
          
          if ( is_array($value) && array_key_exists('children', $value)){
               if (!empty($value['children'])) {
                $menu .= '<ul>';
                $this->buildMenu($value['children'], $menu);
                $menu .= '</ul>';
             
              }
          }
          
            $menu .= '</li>';
        }
           

	return $menu;
	}

}

In app/View/Element/Menu create the element

<?php
if (!isset($menu_tree) || empty($menu_tree)) :
    $menu_tree = $this->requestAction('/menus/build_menu');
endif;
?>

<ul class="sf-menu">
    <?php echo $this->Menu->buildMenu($menu_tree); ?>
</ul>

In app/Controller/MenusController.php add the following

 
         public function build_menu() {
		$this->Menu->recursive = -1;
                $options = array(
                    'conditions' => array(
                        'Menu.active' => 1
                    )
                );
		return $this->Menu->find('threaded', $options);
	}

Download Superfish from http://users.tpg.com.au/j_birch/plugins/superfish/download/

Unzip it and copy the superfish-master/src/js and superfish-master/src/css folders to app/webroot/ and merge them with the existing js and css folders.

Create a init file in app/webroot/js/superfish-init.js

(function ($) { //create closure so we can safely use $ as alias for jQuery

    $(document).ready(function () {

        // initialise plugin
        $('ul.sf-menu').superfish({
            //add options here if required
        });

    });

})(jQuery);

In app/View/Layouts/default.ctp add the superfish css and js along with the init file

<head>
       // ... snipped ... //
	<?php
		echo $this->Html->css(array(
                    'cake.generic',
                    'superfish',
                    ));

	        $this->Html->script( array(
                 'jquery.js',
		'hoverIntent',
		'superfish',
                'superfish-init'
                ), array(
                    'block' => 'script_bottom',
                    'inline' => false
                ))
	?>
</head>
<body>
         <div id="header">
               <!-- replace the heading -->
               <?php echo $this->Element('Menus/menu.ctp'); ?>
         </div>
	// ... snipped ... //
        <?php echo $this->fetch('script_bottom'); ?>
</body>
</html>

Navigate to menus/add and create a top level menu. The settings as shown will generate <a href=”#” title=”Top Level menu with # link”>Top</a>

1

 

Once you save the menu you should see a menu button appear

2

With the edit and add baked CRUD views you may have to add:

// app/Controller/MenusController.php
public function add() {
		if ($this->request->is('post')) {
			$this->Menu->create();
			if ($this->Menu->save($this->request->data)) {
				$this->Session->setFlash(__('The menu has been saved.'));
				return $this->redirect(array('action' => 'index'));
			} else {
				$this->Session->setFlash(__('The menu could not be saved. Please, try again.'));
			}
		}
                // add this
                $parentMenus = $this->Menu->ParentMenu->find('list');
                $this->set(compact('parentMenus'));
	}

public function edit($id = null) {
		if (!$this->Menu->exists($id)) {
			throw new NotFoundException(__('Invalid menu'));
		}
		if ($this->request->is(array('post', 'put'))) {
			if ($this->Menu->save($this->request->data)) {
				$this->Session->setFlash(__('The menu has been saved.'));
				return $this->redirect(array('action' => 'index'));
			} else {
				$this->Session->setFlash(__('The menu could not be saved. Please, try again.'));
			}
		} else {
			$options = array('conditions' => array('Menu.' . $this->Menu->primaryKey => $id));
			$this->request->data = $this->Menu->find('first', $options);
		}
                
                // add this
                $parentMenus = $this->Menu->ParentMenu->find('list');
		$this->set(compact('parentMenus'));
	}

// app/View/Menus/add.ctp and edit.ctp change the parent_id to pickup the values
// need empty so you can create multiple top level menus
echo $this->Form->input('parent_id' , array(
                                         'empty' => '(Choose a parent)',
                                         'options' => $parentMenus));

// app/Model/Menu.php 
// because otherwise it automagically picks 'title' as the displayField
public $displayField = 'name';

Add another menu…

3

And now you have multi level dynamic javascript menus

4

 

Adding a sub menu

 

5

 

With an argument… because of using array()

6

 

 

 

 

 

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

The reCAPTCHA verification period has expired. Please reload the page.

You May Also Like…

Squarespace Image Export

To gain continued access to your Squarespace website images after cancelling your subscription you have several...

MySQL 8.x GRANT ALL STATEMENT

-- CREATE CREATE USER 'tgnrestoreuser'@'localhost' IDENTIFIED BY 'AppleSauceLoveBird2024'; GRANT ALL PRIVILEGES ON...

Exetel Opt-Out of CGNAT

If your port forwards and inbound and/or outbound site-to-site VPN's have failed when switching to Exetel due to their...