Recursive function for CMenu

I have a database with the following structure:




ID   |   Title    |    Parent ID     |     URL

------------------------------------------------------

1    |   Home     |        0         |    /home

2    |   About    |        0         |    /about

3    |   Contact  |        2         |    /contact



I’ve been having an extremely hard time getting a recursive function to work that can hook into CMenu. I can get some arrays to build, but it’s never the right format. Can someone please share their code? Or help me figure out a function that will work with this?

I found a similar article in the wiki (with dropdowns) but that unfortunately did not help me.

Thank you!

This is not totally comnplete (for me) but will give you a good idea on how you can go recursively through menu items

         if (isset($childmenu->menus))


           $subsubmenu  = self::findChildMenus($childmenu);

$childmenu->menus where menus is a relation like this in my model:

		'menus' => array(self::HAS_MANY, 'Menu', 'menu_id'),

menu_id points to the parent menu item in your case it is Parent ID




class SiteMenu  {


    /*

     * @return array of items prepared for CMenu

     * @param model $item

     * Recursively digs in a model to find child menus

     */

    public static function findChildMenus($item)

    {

        $submenu  = array();

        $subsubmenu = array();


        if (isset($item->menus)) {

          foreach ($item->menus as $childmenu) {


             /*

              * Again here to add a down arrow span

              */

                

              /*

               * If a child has more child items

               */

             if (isset($childmenu->menus))

               $subsubmenu  = self::findChildMenus($childmenu);


               $linkOption = array();

               if ($childmenu->newwindow === '1')

                  $linkOption['target'] = '_blank';


                /*

                 * Check if we have more submenus from here

                 * if so add the class fly to the <a>

                 */

                if (!empty($childmenu->menus)) {

                  $linkOption['class'] = 'fly';

                }


                $submenu[]  = array('label' => $childmenu->name,

                                  'url' => self::checkUrl($childmenu),

                                  'items' => $subsubmenu,

                                  'linkOptions' => $linkOption,

                          );

           }

        }

        return $submenu;

    }

    /*

     * @return valid url

     * @param array $item, which is the actual model passed here

     */

    public static function checkUrl($item)

    {

        $url = '';

        if (!empty($item->permalink)) {

          if (preg_match('/^http:\/\//', $item->permalink))

            $url  = $item->permalink;

          else

             $url  = "http://$item->permalink";

        } else {

          if (!empty($item->page->permalink))

            $url  = array('site/index', 'permalink' => $item->page->permalink);

          else

              $url  = array('site/index', 'id' => $item->page_id);

        }

          return $url;

    }


    public static function top()

    {

      $top[] = array();

      $submenu  = array();

      

      $model  = Menu::model()->findAll(array(

                'condition' => 'enabled = :enabled AND top = 1',

                'params' => array(':enabled' => 1),

                'order' => 't.order',

            ));

        

      if (empty($model))

        return $top;




      foreach ($model as $item) {




            $url  = self::checkUrl($item);


            $linkOption = array();


            if ($item->top && $item->menu_id == 0) {

                $linkOption['class'] = 'top_link';

                $submenu = self::findChildMenus($item);

                

               if ($item->newwindow==="1")

                  $linkOption['target'] = '_blank';


                //This is to add the down class to the span element

                $hasSubMenu = false;

                if (!empty($item->menus)) {

                  $hasSubMenu = true;

                }


                //Top Item Options has the class top

                $itemOption = array('class' => 'top');


                $top[] = array('label' => $item->name,

                                      'url' => $url,

                                      'linkOptions' => $linkOption,

                                      'itemOptions' => $itemOption,

                                      'hasSubMenu' => $hasSubMenu,

                                      'items' => $submenu,

                                      'submenuOptions' => array('class' => 'sub'),

                                     

                                );

            }//If ($item->top)

      }


        return $top;

    }


}


In your view here's how its used:


        <?php

          $this->widget('TopMenu', array(

                  'items' => SiteMenu::top(),

                  'id' => 'nav',

                  'linkLabelWrapper' => 'span',

               ));

        ?>




rookie84, this is great but I’m still having a hard time understanding it. I’d imagine the code I need, at its very basic level could be very short. Having that then building off from it will be the best way for me to get a better understanding of this.

This works and pulls in the correct values, however I’m just displaying them as an example. How do I modify this to put this into arrays that can be used in CMenu?




        /**

	 * Builds the menu into an array

	 * @param integer $menu_id the menu id

	 * @return array $menu the menu with all items

	 */

	public function buildMenu($menu_id)

	{

		echo '<ul>';

		$menu = $this->findChildren(0,$menu_id);

		echo '</ul>';

	}

	

	/**

	 * Recursive function that finds the children of a parent

	 * @param integer $parent_id the parent id

	 * @param integer $menu_id the menu id to search in

	 * @return array $children the array of children

	 */

	public function findChildren($parent_id,$menu_id)

	{

		$children = MenuItem::model()->findAll(array(

			'condition'=>'parent_id=:parent_id AND menu_id=:menu_id',

			'params'=>array(':parent_id'=>$parent_id,':menu_id'=>$menu_id)

		));

		

		foreach($children as $child)

		{

			echo '<li>label: '.$child->title.' url: '.$child->url.'</li>';

			

			$this->findChildren($child->id,$menu_id);

		}

	}



Thank you!

How about following approach?

Have a class which generated items array that can be directly passed to CMenu.

Here, getMenuItemsRecursive method scans table and generates a complete array which can be passed to CMenu directly.

getMenuItems method can be programmed to include various logics and also have cache implemented for performance boost.

[PHP]

class MyMenu extends CActiveRecord

{

public &#036;id;


public &#036;title;


public &#036;parentId;


public &#036;url;





public function tableName()


{


	return 'test.tbl_menu';


}





public static function model(&#036;className = __CLASS__)


{


	return parent::model(&#036;className);


}





public function relations()


{


	return array(


		'childMenu' =&gt; array(self::HAS_MANY, 'MyMenu', 'parentId'),


	);


}





public function getMenuItems()


{


	&#036;result = array(


		'label' =&gt; &#036;this-&gt;title,


		'url' =&gt; &#036;this-&gt;url,


	);


	


	if(isset(&#036;this-&gt;childMenu))


	{


		&#036;childrens = array();


		foreach(&#036;this-&gt;childMenu as &#036;cm)


		{


			&#036;childrens[] = &#036;cm-&gt;getMenuItems();


		}


		


		if(&#33;empty(&#036;childrens))


		{


			&#036;result['items'] = &#036;childrens;


		}


	}


	


	return &#036;result;


}





public static function getMenuItemsRecursive()


{


	&#036;result = array();


	&#036;models = self::model()-&gt;findAllByAttributes(array('parentId' =&gt; 0));


	


	if(&#036;models)


	{


		foreach(&#036;models as &#036;menu)


		{


			&#036;result[] = &#036;menu-&gt;getMenuItems();


		}


	}


	


	return &#036;result;


	


}





public static function doTest()


{


	&#036;menuItems = self::getMenuItemsRecursive();


	


	echo '&lt;pre&gt;';


	print_r(&#036;menuItems);


	echo '&lt;/pre&gt;';


	exit;


}

}

[/PHP]

Below is dump of table I used as reference.






-- ----------------------------


-- Table structure for `tbl_menu`


-- ----------------------------


DROP TABLE IF EXISTS `tbl_menu`;


CREATE TABLE `tbl_menu` (


  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,


  `title` varchar(255) DEFAULT NULL,


  `parentId` int(11) unsigned NOT NULL DEFAULT '0',


  `url` varchar(255) DEFAULT NULL,


  PRIMARY KEY (`id`)


) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1;





-- ----------------------------


-- Records of tbl_menu


-- ----------------------------


INSERT INTO `tbl_menu` VALUES ('1', 'Home', '0', '/home');


INSERT INTO `tbl_menu` VALUES ('2', 'About', '0', '/about');


INSERT INTO `tbl_menu` VALUES ('3', 'Contact', '2', '/contact');


INSERT INTO `tbl_menu` VALUES ('4', 'Services', '0', '/services');


INSERT INTO `tbl_menu` VALUES ('5', 'Programming', '4', '/programming');


INSERT INTO `tbl_menu` VALUES ('6', 'Designing', '4', '/designing');


INSERT INTO `tbl_menu` VALUES ('7', 'PHP', '5', '/php');


INSERT INTO `tbl_menu` VALUES ('8', 'Java', '5', '/java');




Thank you mastermunj and rookie84! I was able to put something together that works great.

Glad you figured it out :)

How rewrite it in DAO?