Menus are simple things, you have a root element, it has children, their children have children, and so on. All nice, clean and simple and utterly dull.

Add Knp Menu and bundle to the composer file, run update, go through the docs and set it up, and be on your way.

But what happens if you need to construct a menu from different bundles and you have no idea how many items there are and their order.

That is when things start to be just a bit tricky.

Lets assume there are three bundles, Alpha, Users and Scouts, Alpha bundle will be the one that generates the menu, and that following is the menu tree we need to end up with

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 root dashboard <- comes from the alpha bundle pages alpha pages <- comes from alpha bundle users pages <- comes from the users bundle users admins list <- comes from the users bundle add new <- comes from the users bundle users list <- comes from the users bundle add new <- comes from the users bundle scout users <- comes from scout bundle search scouts <- comes from scout bundle scout jobs <- comes from scout bundle logout

Yes, you could just go in and add all the values into your Builder file and be done with it

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <?php $menu = $factory -> createItem ( 'root' ); // Pages $pages = $factory -> createItem ( 'Pages' , array ()); $alphaPages = $factory -> createItem ( 'Alpha Pages' , array ( 'route' => 'list_alpha_pages' )); $pages -> addChild ($menu); $usersPages = $factory -> createItem ( 'Users Pages' , array ( 'route' => 'list_users_pages' )); $pages -> addChild ($menu); $menu -> addChild ($pages); // Users $usersRoot = $factory -> createItem ( 'Users' , array ()); $admins = $factory -> createItem ( 'Admins' , array ()); // ... add more children $usersRoot -> addChild ($admins); $users = $factory -> createItem ( 'Users' , array ()); // ... add more children $usersRoot -> addChild ($users); $menu -> addChild ($usersRoot); // ... add more children

This particular approach would get out of hand pretty fast, if you have lots of menu items, from different parts of the system, and especially if their appearance depends on other factors (if the bundle is used at all for example). Soon you would be injecting all kinds of services and doing all kinds of checks and end up with a Megamoth with Jenga issues. Yes you could split the code into smaller methods and compose it all, but still it is bad.

Better thing do to is to extent the menu with events, the how to is explained pretty good in the docs, but in my use case it had a shortcoming with ordering, to be perfectly clear, this is not a shortcoming of the bundle/lib itself, execution of all the registered event listeners can be guaranteed, but NOT the order of their execution.

My approach differs slightly from the one described in the docs. I opted to have a separate menu event type classes per bundle, so they could be as small(or as big) as possible. The event object class on the other hand is same and is defined once (in Alpha bundle) and used everywhere.

First Thing is to create a menu event types class for Alpha bundle, as mentioned previously the Alpha bundle will have the menu builder, so it will need the CONFIGURE_ROOT event type defined in it, as well as CONFIGURE_PAGES event type (it was developed first, and only pages were inside of it, things changed…)

src/AlphaBundle/Event/MenuEvents.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php namespace AlphaBundle\Event ; final class MenuEvents { /** * @var string */ const CONFIGURE_ROOT = 'alpha.menuConfigureRoot' ; /** * @var string */ const CONFIGURE_PAGES = 'alpha.menuConfigurePages' ; }

For Users bundle it would look like

src/UsersBundle/Event/MenuEvents.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php namespace UsersBundle\Event ; final class MenuEvents { /** * @var string */ const CONFIGURE_USERS = 'users.menuConfigureUsers' ; /** * @var string */ const CONFIGURE_ADMINS = 'users.menuConfigureAdmins' ; }

The Scouts bundle does not need its menu event type class, because the menu items from it will not have any children items. If that changes in future it would be easy to add the class and set things up.

Next up is the Event object class, aptly named ConfigureMenuEvent

src/AlphaBundle/Event/ConfigureMenuEvent.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <?php namespace AlphaBundle\Event ; use Knp\Menu\FactoryInterface ; use Knp\Menu\ItemInterface ; use Symfony\Component\EventDispatcher\Event ; class ConfigureMenuEvent extends Event { private $factory; private $menu; /** * @param \Knp\Menu\FactoryInterface $factory * @param \Knp\Menu\ItemInterface $menu */ public function __construct( FactoryInterface $factory, ItemInterface $menu) { $this -> factory = $factory; $this -> menu = $menu; } /** * @return \Knp\Menu\FactoryInterface */ public function getFactory () { return $this -> factory ; } /** * @return \Knp\Menu\ItemInterface */ public function getMenu () { return $this -> menu ; } }

As you can see pretty identical to the way docs describe it, sans the menu event type constant.

To start the ball rolling and build the menu, you would need to trigger the CONFIGURE_ROOT event from your menu builder class.

src/AlphaBundle/Menu/AdminMenuBuilder.php 1 2 3 4 5 6 7 8 9 10 11 12 13 <?php private function buildMenu () { $menu = $this -> factory -> createItem ( 'root' ); $this -> eventDispatcher -> dispatch ( MenuEvents :: CONFIGURE_ROOT , new ConfigureMenuEvent ($this -> factory , $menu)); $this -> reorderMenuItems ($menu); return $menu; }

With this you now have a menu builder emitting events that will need some listeners to pick up. As described in the docs it would look something like this for the Alpha bundle

src/AlphaBundle/Resources/config/services.yml 1 2 3 4 5 6 7 services: alpha.MenuEventsListener: class: %alpha.MenuEventsListener.Class% arguments: [@event_dispatcher] tags: - { name: kernel.event_listener, event: alpha.menuConfigureRoot, method: onMenuConfigureRoot } - { name: kernel.event_listener, event: alpha.menuConfigurePages, method: onMenuConfigurePages }

for the Users bundle

src/UsersBundle/Resources/config/services.yml 1 2 3 4 5 6 7 8 9 services: users.MenuEventsListener: class: %users.MenuEventsListener.Class% arguments: [@event_dispatcher] tags: - { name: kernel.event_listener, event: alpha.menuConfigureRoot, method: onMenuConfigureRoot } - { name: kernel.event_listener, event: users.menuConfigureUsers', method: onMenuConfigureUsers } - { name: kernel.event_listener, event: users.menuConfigureAdmins, method: onMenuConfigureAdmins } - { name: kernel.event_listener, event: alpha.menuConfigurePages, method: onMenuConfigurePages }

and finally for the Scouts bundle

src/ScoutsBundle/Resources/config/services.yml 1 2 3 4 5 6 services: scouts.MenuEventsListener: class: %scouts.MenuEventsListener.Class% arguments: [@event_dispatcher] tags: - { name: kernel.event_listener, event: users.menuConfigureUsers', method: onMenuConfigureUsers }

The listeners are now registered in the services, the listener class for the Alpha bundle is pretty much what you would expect

src/AlphaBundle/EventsListener/MenuEventsListener.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 <?php namespace AlphaBundle\EventsListener ; use AlphaBundle\Event\MenuEvents ; use AlphaBundle\Event\ConfigureMenuEvent ; class MenuEventsListener { protected $eventDispatcher; /** * @param FactoryInterface @factory */ public function __construct($eventDispatcher) { $this -> eventDispatcher = $eventDispatcher; } /** * @param AlphaBundle\Events\ConfigureMenuEvent $event */ public function onMenuConfigureRoot ($event) { $parentMenu = $event -> getMenu (); $factory = $event -> getFactory (); $menu = $factory -> createItem ( 'Dashboard' , array ( 'route' => 'admin' )); $menu -> setExtra ( 'orderNumber' , 1 ); $parentMenu -> addChild ($menu); $menu = $factory -> createItem ( 'Pages' , array ( 'route' => 'list_pages' )); $menu -> setAttribute ( 'id' , 'menu_admin_pages' ); $menu -> setAttribute ( 'class' , 'submenu' ); $this -> eventDispatcher -> dispatch ( MenuEvents :: CONFIGURE_PAGES , new ConfigureMenuEvent ($factory, $menu)); $menu -> setExtra ( 'orderNumber' , 10 ); $parentMenu -> addChild ($menu); $menu = $factory -> createItem ( 'Logout' , array ( 'route' => '_account_logout' )); $menu -> setExtra ( 'orderNumber' , 100 ); $parentMenu -> addChild ($menu); } /** * @param AlphaBundle\Events\ConfigureMenuEvent $event */ public function onMenuConfigurePages ($event) { $parentMenu = $event -> getMenu (); $factory = $event -> getFactory (); $menuType = $event -> getMenuType (); $menu = $factory -> createItem ( 'Alpha Pages' , array ( 'route' => 'list_pages' )); $menu -> setExtra ( 'orderNumber' , 10 ); $parentMenu -> addChild ($menu); } }

OK, so we got this far, events are being fired, listened to, and handled, this looks pretty much plain vanilla setup as in the docs, so why all this scribbling the dribble then, you might ask, rightly so. Well, note the lines 28, 35 and 37.

While looking around for a possible solution for the ordering of elements, I came across useful functions like moveToPosition($position), as useful as it is, I could not use it because I would have to execute it only after all the elements of the menu were assembled. Digging around some more I got to the reorderChildren($order) which would take the array of the item names and reorder the menu item in relation to the order of the names in the array. This was promising, but still the problem of passing around the order number remained.

I discovered you can pass extra info with each menu item in Knp Menu. That, there, and then was my A-HA moment. I could use this extra info to pass around the order number I need for each menu item relative to its parent, and when it is all said and done then call the reorderChildren($order) which would sort things out properly.

The implementation is on lines 28 and 37. That extra info will be used later to sort the menu items.

Line 35 triggers an event that would collect all the child items for the pages menu item, you can see the setup of those items for this bundle on line 49 in method onMenuConfigurePages .

The listener class for the Users bundle would look like this

src/UsersBundle/EventsListener/MenuEventsListener.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 <?php namespace UsersBundle\EventsListener; use UsersBundle\Event\MenuEvents; use AlphaBundle\Event\ConfigureMenuEvent; class MenuEventsListener { protected $eventDispatcher; /** * @param FactoryInterface @factory */ public function __construct($eventDispatcher) { $this->eventDispatcher = $eventDispatcher; } /** * @param AlphaBundle\Events\ConfigureMenuEvent $event */ public function onMenuConfigureRoot($event) { $parentMenu = $event->getMenu(); $factory = $event->getFactory(); $menu = $factory->createItem('Users', array('route' => 'list_users')); $menu->setAttribute('id', 'menu_admin_users'); $menu->setAttribute('class', 'submenu'); $submenuAdmins = $factory->createItem('Admins', array('route' => 'list_admins')); $submenuAdmins->setAttribute('id', 'menu_admin_users_admins'); $submenuAdmins->setAttribute('class', 'submenu'); $this->eventDispatcher->dispatch(MenuEvents::CONFIGURE_ADMINS, new ConfigureMenuEvent($factory, $submenuAdmins)); $submenuAdmins->setExtra('orderNumber', 10); $menu->addChild($submenuAdmins); $submenuUsers = $factory->createItem('Users', array('route' => 'list_users')); $submenuUsers->setAttribute('id', 'menu_admin_users_users'); $submenuUsers->setAttribute('class', 'submenu'); $this->eventDispatcher->dispatch(MenuEvents::CONFIGURE_USERS, new ConfigureMenuEvent($factory, $submenuUsers)); $submenuUsers->setExtra('orderNumber', 20); $menu->addChild($submenuUsers); $menu->setExtra('orderNumber', 20); $parentMenu->addChild($menu); } /** * @param AlphaBundle\Events\ConfigureMenuEvent $event */ public function onMenuConfigureUsers($event) { $parentMenu = $event->getMenu(); $factory = $event->getFactory(); $menuType = $event->getMenuType(); $menu = $factory->createItem('List Users', array('route' => 'list_users')); $menu->setExtra('orderNumber', 10); $parentMenu->addChild($menu); $menu = $factory->createItem('Add New User', array('route' => 'add_user')); $menu->setExtra('orderNumber', 20); $parentMenu->addChild($menu); } /** * @param AlphaBundle\Events\ConfigureMenuEvent $event */ public function onMenuConfigureAdmins($event) { $parentMenu = $event->getMenu(); $factory = $event->getFactory(); $menuType = $event->getMenuType(); $menu = $factory->createItem('List Admins', array('route' => 'list_admins')); $menu->setExtra('orderNumber', 10); $parentMenu->addChild($menu); $menu = $factory->createItem('Add New Admin', array('route' => 'add_admin')); $menu->setExtra('orderNumber', 20); $parentMenu->addChild($menu); } /** * @param AlphaBundle\Events\ConfigureMenuEvent $event */ public function onMenuConfigurePages($event) { $parentMenu = $event->getMenu(); $factory = $event->getFactory(); $menuType = $event->getMenuType(); $menu = $factory->createItem('User Pages', array('route' => 'list_pages_user')); $menu->setExtra('orderNumber', 20); $parentMenu->addChild($menu); } }

On lines 34 and 44 events are triggered to collect the Admin and User sub menu items, and those are added in the onMenuConfigureAdmins and onMenuConfigureUsers methods respectively.

On line 49 in method onMenuConfigurePages page menu item children are set.

And finally the listener for the Scouts bundle would look like this

src/ScoutsBundle/EventsListener/MenuEventsListener.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <?php namespace ScoutsBundle\EventsListener; use UsersBundle\Event\MenuEvents; use AlphaBundle\Event\ConfigureMenuEvent; class MenuEventsListener { protected $eventDispatcher; /** * @param FactoryInterface @factory */ public function __construct($eventDispatcher) { $this->eventDispatcher = $eventDispatcher; } /** * @param AlphaBundle\Events\ConfigureMenuEvent $event */ public function onMenuConfigureUsers($event) { $parentMenu = $event->getMenu(); $factory = $event->getFactory(); $menuType = $event->getMenuType(); $menu = $factory->createItem('Scout Users', array('route' => 'scouts_list_users')); $menu->setExtra('orderNumber', 30); $parentMenu->addChild($menu); $menu = $factory->createItem('Search Scouts', array('route' => 'scouts_search_users')); $menu->setExtra('orderNumber', 40); $parentMenu->addChild($menu); $menu = $factory->createItem('Scout Jobs', array('route' => 'scouts_jobs')); $menu->setExtra('orderNumber', 50); $parentMenu->addChild($menu); } }

In method onMenuConfigureUsers the children of the users menu item are added, and their order number is set.

You might have noted that I am using increments of 10 for the order number, reasoning is simple, if you need to add a new item in between the existing ones you should have room to, with least surprises in ordering.

With all this in place your menu would be constructed, the items you wanted would be present but the ordering would be questionable for now, because, as previously stated, the only thing you can be sure of is that the events will finish before line 7 in the snippet below, but you still have no idea on their order of execution.

src/AlphaBundle/Menu/AdminMenuBuilder.php 1 2 3 4 5 6 7 8 9 10 11 12 13 <?php private function buildMenu() { $menu = $this->factory->createItem('root'); $this->eventDispatcher->dispatch(MenuEvents::CONFIGURE_ROOT, new ConfigureMenuEvent($this->factory, $menu)); $this->reorderMenuItems($menu); return $menu; }

And with that we have reached the climax of this writeup, the reorderMenuItems function executed on line 7, the code of the method would look like this

src/AlphaBundle/Menu/AdminMenuBuilder.php 1 2 3 4 5 6 7 8 9 10 11 12 13 <?php private function buildMenu () { $menu = $this -> factory -> createItem ( 'root' ); $this -> eventDispatcher -> dispatch ( MenuEvents :: CONFIGURE_ROOT , new ConfigureMenuEvent ($this -> factory , $menu)); $this -> reorderMenuItems ($menu); return $menu; }

And with that we have reached the climax of this writeup, the reorderMenuItems function executed on line 7, the code of the method would look like this

src/AlphaBundle/Menu/AdminMenuBuilder.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 <?php public function reorderMenuItems ($menu) { $menuOrderArray = array (); $addLast = array (); $alreadyTaken = array (); foreach ($menu -> getChildren () as $key => $menuItem) { if ($menuItem -> hasChildren ()) { $this -> reorderMenuItems ($menuItem); } $orderNumber = $menuItem -> getExtra ( 'orderNumber' ); if ($orderNumber != null ) { if ( ! isset ($menuOrderArray[$orderNumber])) { $menuOrderArray[$orderNumber] = $menuItem -> getName (); } else { $alreadyTaken[$orderNumber] = $menuItem -> getName (); // $alreadyTaken[] = array('orderNumber' => $orderNumber, 'name' => $menuItem->getName()); } } else { $addLast[] = $menuItem -> getName (); } } // sort them after first pass ksort ($menuOrderArray); // handle position duplicates if ( count ($alreadyTaken)) { foreach ($alreadyTaken as $key => $value) { // the ever shifting target $keysArray = array_keys ($menuOrderArray); $position = array_search ($key, $keysArray); if ($position === false ) { continue ; } $menuOrderArray = array_merge ( array_slice ($menuOrderArray, 0 , $position), array ($value), array_slice ($menuOrderArray, $position)); } } // sort them after second pass ksort ($menuOrderArray); // add items without ordernumber to the end if ( count ($addLast)) { foreach ($addLast as $key => $value) { $menuOrderArray[] = $value; } } if ( count ($menuOrderArray)) { $menu -> reorderChildren ($menuOrderArray); } }

Line 6 addLast array will hold all the menu items that have no order number, for my needs those would be added to the end of the menu tree, as usual YMMV.

Line 8 alreadyTaken array will hold the items that need to be added, but their spot is already taken. The idea is that in first pass all the menu items without position collision would be setup, and in second pass the position collisions would be resolved.

Line 12 if the menu item has children do a recursion and sort out the child items first before proceeding with ordering.

Lines 18-27 either add to addLast array, alreadyTaken array or set the order position of the element.

Line 37 when the execution gets to this point, you should have the menu items with their order number setup, and possibly have elements in the addLast and alreadyTaken arrays to be processed

Lines 34-47 process the already taken and insert them at their appropriate place in the array by slicing and merging.

Lines 53-57 will process the menu elements that have no order numbers and add them to the end of the tree.

And finally on line 60 the new order array is passed to the Knp Menu to reorder each the items.

This kind of events based menu will allow for greater flexibility as your application grows, and easier menu ordering if/when needed.

For example if the Scout bundle got its own variant of pages, and they needed to appear under the pages menu item, the changes in service.yml would be the following

src/ScoutsBundle/Resources/config/services.yml 1 2 3 4 5 6 7 services: scouts.MenuEventsListener: class: %scouts.MenuEventsListener.Class% arguments: [@event_dispatcher] tags: - { name: kernel.event_listener, event: users.menuConfigureUsers', method: onMenuConfigureUsers } - { name: kernel.event_listener, event: alpha.menuConfigurePages, method: onMenuConfigurePages }

And the changes for listener are listener for the Scouts bundle would be

src/ScoutsBundle/EventsListener/MenuEventsListener.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 <?php namespace ScoutsBundle\EventsListener ; use UsersBundle\Event\MenuEvents ; use AlphaBundle\Event\ConfigureMenuEvent ; class MenuEventsListener { protected $eventDispatcher; /** * @param FactoryInterface @factory */ public function __construct($eventDispatcher) { $this -> eventDispatcher = $eventDispatcher; } /** * @param AlphaBundle\Events\ConfigureMenuEvent $event */ public function onMenuConfigureUsers ($event) { $parentMenu = $event -> getMenu (); $factory = $event -> getFactory (); $menuType = $event -> getMenuType (); $menu = $factory -> createItem ( 'Scout Users' , array ( 'route' => 'scouts_list_users' )); $menu -> setExtra ( 'orderNumber' , 30 ); $parentMenu -> addChild ($menu); $menu = $factory -> createItem ( 'Search Scouts' , array ( 'route' => 'scouts_search_users' )); $menu -> setExtra ( 'orderNumber' , 40 ); $parentMenu -> addChild ($menu); $menu = $factory -> createItem ( 'Scout Jobs' , array ( 'route' => 'scouts_jobs' )); $menu -> setExtra ( 'orderNumber' , 50 ); $parentMenu -> addChild ($menu); } /** * @param AlphaBundle\Events\ConfigureMenuEvent $event */ public function onMenuConfigurePages ($event) { $parentMenu = $event -> getMenu (); $factory = $event -> getFactory (); $menuType = $event -> getMenuType (); $menu = $factory -> createItem ( 'Scout Pages' , array ( 'route' => 'list_pages_scout' )); $menu -> setExtra ( 'orderNumber' , 30 ); $parentMenu -> addChild ($menu); } }

Boom you are done.