Skip to content

Commit

Permalink
Add a menu provider registering builders as services
Browse files Browse the repository at this point in the history
  • Loading branch information
stof committed Jun 16, 2015
1 parent 802e7f6 commit c0766ad
Show file tree
Hide file tree
Showing 11 changed files with 386 additions and 23 deletions.
33 changes: 33 additions & 0 deletions DependencyInjection/Compiler/MenuBuilderPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Knp\Bundle\MenuBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;

/**
* This compiler pass registers the menu builders in the BuilderServiceProvider.
*
* @author Christophe Coevoet <stof@notk.org>
*/
class MenuBuilderPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('knp_menu.menu_provider.builder_service');

$menuBuilders = array();
foreach ($container->findTaggedServiceIds('knp_menu.menu_builder') as $id => $tags) {
foreach ($tags as $attributes) {
if (empty($attributes['alias'])) {
throw new \InvalidArgumentException(sprintf('The alias is not defined in the "knp_menu.menu_builder" tag for the service "%s"', $id));
}
if (empty($attributes['method'])) {
throw new \InvalidArgumentException(sprintf('The method is not defined in the "knp_menu.menu_builder" tag for the service "%s"', $id));
}
$menuBuilders[$attributes['alias']] = array($id, $attributes['method']);
}
}
$definition->replaceArgument(1, $menuBuilders);
}
}
1 change: 1 addition & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public function getConfigTreeBuilder()
->children()
->booleanNode('builder_alias')->defaultTrue()->end()
->booleanNode('container_aware')->defaultTrue()->end()
->booleanNode('builder_service')->defaultTrue()->end()
->end()
->end()
->arrayNode('twig')
Expand Down
2 changes: 2 additions & 0 deletions KnpMenuBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Knp\Bundle\MenuBundle\DependencyInjection\Compiler\AddRenderersPass;
use Knp\Bundle\MenuBundle\DependencyInjection\Compiler\AddTemplatePathPass;
use Knp\Bundle\MenuBundle\DependencyInjection\Compiler\AddVotersPass;
use Knp\Bundle\MenuBundle\DependencyInjection\Compiler\MenuBuilderPass;
use Knp\Bundle\MenuBundle\DependencyInjection\Compiler\MenuPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
Expand All @@ -18,6 +19,7 @@ public function build(ContainerBuilder $container)
parent::build($container);

$container->addCompilerPass(new MenuPass());
$container->addCompilerPass(new MenuBuilderPass());
$container->addCompilerPass(new AddExtensionsPass());
$container->addCompilerPass(new AddProvidersPass());
$container->addCompilerPass(new AddRenderersPass());
Expand Down
43 changes: 43 additions & 0 deletions Provider/BuilderServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Knp\Bundle\MenuBundle\Provider;

use Knp\Menu\Provider\MenuProviderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* This provider uses methods of services to build menus.
*
* @author Christophe Coevoet <stof@notk.org>
*/
class BuilderServiceProvider implements MenuProviderInterface
{
private $container;
private $menuBuilders;

public function __construct(ContainerInterface $container, array $menuBuilders = array())
{
$this->container = $container;
$this->menuBuilders = $menuBuilders;
}

public function get($name, array $options = array())
{
if (!isset($this->menuBuilders[$name])) {
throw new \InvalidArgumentException(sprintf('The menu "%s" is not defined.', $name));
}

if (!is_array($this->menuBuilders[$name]) || 2 !== count($this->menuBuilders[$name])) {
throw new \InvalidArgumentException(sprintf('The menu builder definition for the menu "%s" is invalid. It should be an array (serviceId, method)', $name));
}

list($id, $method) = $this->menuBuilders[$name];

return $this->container->get($id)->$method($options);
}

public function has($name, array $options = array())
{
return isset($this->menuBuilders[$name]);
}
}
5 changes: 5 additions & 0 deletions Resources/config/menu.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@
<argument type="collection" />
</service>

<service id="knp_menu.menu_provider.builder_service" class="Knp\Bundle\MenuBundle\Provider\BuilderServiceProvider" public="false">
<argument type="service" id="service_container" />
<argument type="collection" />
</service>

<service id="knp_menu.menu_provider.builder_alias" class="%knp_menu.menu_provider.builder_alias.class%" public="false">
<argument type="service" id="kernel" />
<argument type="service" id="service_container" />
Expand Down
22 changes: 22 additions & 0 deletions Resources/doc/disabling_providers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Disabling the Core Menu Providers
=================================

To be able to use different menu providers together (the builder-service-based
one, the container-based one and the convention-based one for instance),
a chain provider is used. However, it is not used when only one provider
is enabled to increase performance by getting rid of the wrapping. If you
don't want to use the built-in providers, you can disable them through the
configuration:

.. code-block:: yaml
#app/config/config.yml
knp_menu:
providers:
builder_alias: false # disable the builder-alias-based provider
builder_service: false
container_aware: true # keep this one enabled. Can be omitted as it is the default
.. note::

All providers are enabled by default.
15 changes: 13 additions & 2 deletions Resources/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ If you skip this step, these defaults will be used.
<?xml version="1.0" charset="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:knp-menu="http://knplabs.com/schema/dic/menu">

<!--
templating: if true, enabled the helper for PHP templates
default-renderer: the renderer to use, list is also available by default
Expand Down Expand Up @@ -207,12 +207,23 @@ then render it via ``AppBundle:Builder:sidebarMenu``.
That's it! The menu is *very* configurable. For more details, see the
`KnpMenu documentation`_.

Method b) A menu as a service
Method b) A menu builder as a service
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For information on how to register a menu builder as a service, read
:doc:`Creating Menu Builders as Services <menu_builder_service>`.


Method c) A menu as a service
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For information on how to register a service and tag it as a menu, read
:doc:`Creating Menus as Services <menu_service>`.

.. note::

To improve performances, you can :doc:`disable providers you don't need <disabling_providers>`.

Rendering Menus
---------------

Expand Down
125 changes: 125 additions & 0 deletions Resources/doc/menu_builder_service.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
Creating Menu Builders as Services
==================================

This bundle gives you a really convenient way to create menus by following
a convention and - if needed - injecting the entire container.

However, if you want to, you can instead choose to create a service for your
menu builder. The advantage of this method is that you can inject the exact
dependencies that your menu builder needs, instead of injecting the entire
service container. This can lead to code that is more testable and also potentially
more reusable. The disadvantage is that it needs just a little more setup.

Start by creating a builder for your menu. You can stick as many menus into
a builder as you want, so you may only have one (or just a few) of these
builder classes in your application:

.. code-block:: php
// src/AppBundle/Menu/MenuBuilder.php
namespace AppBundle\Menu;
use Knp\Menu\FactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class MenuBuilder
{
private $factory;
/**
* @param FactoryInterface $factory
*
* Add any other dependency you need
*/
public function __construct(FactoryInterface $factory)
{
$this->factory = $factory;
}
public function createMainMenu(array $options)
{
$menu = $this->factory->createItem('root');
$menu->addChild('Home', array('route' => 'homepage'));
// ... add more children
return $menu;
}
}
Next, register your menu builder as service and register its ``createMainMenu`` method as a menu builder:

.. code-block:: yaml
# app/config/services.yml
services:
app.menu_builder:
class: AppBundle\Menu\MenuBuilder
arguments: ["@knp_menu.factory"]
tags:
- { name: knp_menu.menu_builder, method: createMainMenu, alias: main } # The alias is what is used to retrieve the menu
# ...
.. note::

The menu service must be public as it will be retrieved at runtime to keep
it lazy-loaded.

You can now render the menu directly in a template via the name given in the
``alias`` key above:

.. code-block:: html+jinja

{{ knp_menu_render('main') }}

Suppose now we need to create a second menu for the sidebar. The process
is simple! Start by adding a new method to your builder:

.. code-block:: php
// src/AppBundle/Menu/MenuBuilder.php
// ...
class MenuBuilder
{
// ...
public function createSidebarMenu(array $options)
{
$menu = $this->factory->createItem('sidebar');
if (isset($options['include_homepage']) && $options['include_homepage']) {
$menu->addChild('Home', array('route' => 'homepage'));
}
// ... add more children
return $menu;
}
}
Now, create a service for *just* your new menu, giving it a new name, like
``sidebar``:

.. code-block:: yaml
# app/config/services.yml
services:
app.menu_builder:
class: AppBundle\Menu\MenuBuilder
arguments: ["@knp_menu.factory"]
tags:
- { name: knp_menu.menu_builder, method: createMainMenu, alias: main } # the previous menu
- { name: knp_menu.menu_builder, method: createSidebarMenu, alias: sidebar } # Named "sidebar" this time
# ...
It can now be rendered, just like the other menu:

.. code-block:: html+jinja

{% set menu = knp_menu_get('sidebar', {include_homepage: false}) %}
{{ knp_menu_render(menu) }}
32 changes: 11 additions & 21 deletions Resources/doc/menu_service.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Creating Menus as Services
==========================

.. note::

Registering a menu as service comes with several limitations:

- it does not allow to use builder options
- it reuses the same instance several times in case you render the same
menu several times, which can has weird side-effects.

It is recommended to register only :doc:`menu builders as services <menu_builder_service>`
instead.

This bundle gives you a really convenient way to create menus by following
a convention and - if needed - injecting the entire container.

Expand Down Expand Up @@ -125,24 +136,3 @@ It can now be rendered, just like the other menu:
.. code-block:: html+jinja

{{ knp_menu_render('sidebar') }}

Disabling the core menu providers
---------------------------------

To be able to use different menu providers together (the container-based
one and the builder-based one for instance), a chain provider is used.
However, it is not used when only one provider is enabled to increase performance
by getting rid of the wrapping. If you don't want to use the built-in providers,
you can disable them through the configuration:

.. code-block:: yaml
#app/config/config.yml
knp_menu:
providers:
builder_alias: false # disable the builder-based provider
container_aware: true # keep this one enabled. Can be omitted as it is the default
.. note::

Both providers are enabled by default.
Loading

0 comments on commit c0766ad

Please sign in to comment.