Using events to allow a menu to be extended

Using events to allow a menu to be extended

If you want to let different parts of your system hook into the building of your menu, a good way is to use an approach based on the Symfony EventDispatcher component.

Create the menu builder

Your menu builder will create the base menu item and then dispatch an event to allow other parts of your application to add more stuff to 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
// src/AppBundle/Menu/MainBuilder.php

namespace AppBundle\Menu;

use AppBundle\Event\ConfigureMenuEvent;
use Knp\Menu\FactoryInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;

class MainBuilder implements ContainerAwareInterface
{
    use ContainerAwareTrait;

    public function build(FactoryInterface $factory)
    {
        $menu = $factory->createItem('root');

        $menu->addChild('Dashboard', ['route' => '_acp_dashboard']);

        $this->container->get('event_dispatcher')->dispatch(
            ConfigureMenuEvent::CONFIGURE,
            new ConfigureMenuEvent($factory, $menu)
        );

        return $menu;
    }
}

Note

This implementation assumes you use the BuilderAliasProvider (getting your menu as AppBundle:MainBuilder:build) but you could also define it as a service and inject the event_dispatcher service as a dependency.

Create the Event object

The event object allows to pass some data to the listener. In this case, it will hold the menu being created and the factory.

 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
// src/AppBundle/Event/ConfigureMenuEvent.php

namespace AppBundle\Event;

use Knp\Menu\FactoryInterface;
use Knp\Menu\ItemInterface;
use Symfony\Component\EventDispatcher\Event;

class ConfigureMenuEvent extends Event
{
    const CONFIGURE = 'app.menu_configure';

    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;
    }
}

Note

Following the Symfony best practices, the first segment of the event name will be the alias of the bundle, which allows avoiding conflicts.

That's it. Your builder now provides a hook. Let's see how you can use it!

Create a listener

You can register as many listeners as you want for the event. Let's add one.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// src/Acme/AdminBundle/EventListener/ConfigureMenuListener.php

namespace Acme\AdminBundle\EventListener;

use AppBundle\Event\ConfigureMenuEvent;

class ConfigureMenuListener
{
    /**
     * @param \AppBundle\Event\ConfigureMenuEvent $event
     */
    public function onMenuConfigure(ConfigureMenuEvent $event)
    {
        $menu = $event->getMenu();

        $menu->addChild('Matches', ['route' => 'versus_rankedmatch_acp_matches_index']);
        $menu->addChild('Participants', ['route' => 'versus_rankedmatch_acp_participants_index']);
    }
}

You can now register the listener.

1
2
3
4
5
6
# app/config/services.yml
services:
    app.admin_configure_menu_listener:
        class: Acme\AdminBundle\EventListener\ConfigureMenuListener
        tags:
          - { name: kernel.event_listener, event: app.menu_configure, method: onMenuConfigure }

You could also create your listener as a subscriber and use the kernel.event_subscriber tag, which does not have any additional attributes.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.