diff --git a/composer.json b/composer.json index 10bb5a6e..9364a7b1 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ ], "require": { "php": "^8.1", - "knplabs/knp-menu": "^3.6", + "knplabs/knp-menu": "^3.8", "symfony/config": "^5.4 | ^6.0 | ^7.0", "symfony/dependency-injection": "^5.4 | ^6.0 | ^7.0", "symfony/deprecation-contracts": "^2.5 | ^3.3", diff --git a/docs/events.rst b/docs/events.rst index eed35cd9..aea639cb 100644 --- a/docs/events.rst +++ b/docs/events.rst @@ -13,39 +13,39 @@ to allow other parts of your application to add more stuff to it. .. code-block:: php - // src/Menu/MainBuilder.php + // src/Menu/MainBuilder.php - namespace App\Menu; + namespace App\Menu; - use App\Event\ConfigureMenuEvent; - use Knp\Menu\FactoryInterface; - use Symfony\Component\DependencyInjection\ContainerAwareInterface; - use Symfony\Component\DependencyInjection\ContainerAwareTrait; - - class MainBuilder implements ContainerAwareInterface - { - use ContainerAwareTrait; + use App\Event\ConfigureMenuEvent; + use Knp\Menu\FactoryInterface; + use Symfony\Contracts\EventDispatcher\EventDispatcherInterface: - public function build(FactoryInterface $factory) - { - $menu = $factory->createItem('root'); + class MainBuilder + { + private $factory; + private $eventDispatcher - $menu->addChild('Dashboard', ['route' => '_acp_dashboard']); + public function __construct(FactoryInterface $factory, EventDispatcherInterface $eventDispatcher) + { + $this->factory = $factory; + $this->eventDispatcher = $eventDispatcher; + } - $this->container->get('event_dispatcher')->dispatch( - new ConfigureMenuEvent($factory, $menu), - ConfigureMenuEvent::CONFIGURE - ); + public function build(array $options) + { + $menu = $this->factory->createItem('root'); - return $menu; - } - } + $menu->addChild('Dashboard', ['route' => '_acp_dashboard']); -.. note:: + $this->eventDispatcher->dispatch( + new ConfigureMenuEvent($this->factory, $menu), + ConfigureMenuEvent::CONFIGURE + ); - This implementation assumes you use the ``BuilderAliasProvider`` (getting - your menu as ``App:MainBuilder:build``) but you could also define - it as a service and inject the ``event_dispatcher`` service as a dependency. + return $menu; + } + } Create the Event object ----------------------- diff --git a/docs/index.rst b/docs/index.rst index 41befe31..693a270e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -113,114 +113,24 @@ Create your first menu! There are two ways to create a menu: the "easy" way, and the more flexible method of creating a menu as a service. -Method a) The Easy Way (yay)! -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To create a menu, first create a new class in the ``Menu`` directory of one -of your bundles. This class - called ``Builder`` in our example - will have -one method for each menu that you need to build. - -An example builder class would look like this: - -.. code-block:: php - - // src/Menu/Builder.php - namespace App\Menu; - - use App\Entity\Blog; - use Knp\Menu\FactoryInterface; - use Knp\Menu\ItemInterface; - - final class Builder - { - public function __construct( - private EntityManagerInterface $em, - ) { - } - - public function mainMenu(FactoryInterface $factory, array $options): ItemInterface - { - $menu = $factory->createItem('root'); - - $menu->addChild('Home', ['route' => 'homepage']); - - // findMostRecent and Blog are just imaginary examples - $blog = $this->em->getRepository(Blog::class)->findMostRecent(); - - $menu->addChild('Latest Blog Post', [ - 'route' => 'blog_show', - 'routeParameters' => ['id' => $blog->getId()] - ]); - - // create another menu item - $menu->addChild('About Me', ['route' => 'about']); - // you can also add sub levels to your menus as follows - $menu['About Me']->addChild('Edit profile', ['route' => 'edit_profile']); - - // ... add more children - - return $menu; - } - } - -With the standard ``knp_menu.html.twig`` template and your current page being -'Home', your menu would render with the following markup: - -.. code-block:: html - - - -.. note:: - - The menu builder can be overwritten using the bundle inheritance. - -To actually render the menu, just do the following from anywhere in any template: - -.. configuration-block:: - - .. code-block:: html+jinja - - {{ knp_menu_render('App:Builder:mainMenu') }} - - .. code-block:: html+php - - render('App:Builder:mainMenu') ?> - -With this method, you refer to the menu using a three-part string: -**bundle**:**class**:**method**. - -If you needed to create a second menu, you'd simply add another method to -the ``Builder`` class (e.g. ``sidebarMenu``), build and return the new menu, -then render it via ``App:Builder:sidebarMenu``. - -That's it! The menu is *very* configurable. For more details, see the -`KnpMenu documentation`_. - -Method b) A menu builder as a service +Method a) 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 `. - -Method c) A menu as a service +Method b) 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 `. +Method c) A menu discovered by convention +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For information on how to use menu based on the bundle alias convention, +read :doc:`Creating Menu via Naming Convention `. + .. note:: To improve performances, you can :doc:`disable providers you don't need `. @@ -228,18 +138,17 @@ For information on how to register a service and tag it as a menu, read Rendering Menus --------------- -Once you've set up your menu, rendering it is easy. If you've used the "easy" -way, then do the following: +Once you've set up your menu, rendering it is easy. .. configuration-block:: .. code-block:: html+jinja - {{ knp_menu_render('App:Builder:mainMenu') }} + {{ knp_menu_render('my_main_menu') }} .. code-block:: html+php - render('App:Builder:mainMenu') ?> + render('my_main_menu') ?> Additionally, you can pass some options to the renderer: @@ -247,11 +156,11 @@ Additionally, you can pass some options to the renderer: .. code-block:: html+jinja - {{ knp_menu_render('App:Builder:mainMenu', {'depth': 2, 'currentAsLink': false}) }} + {{ knp_menu_render('my_main_menu', {'depth': 2, 'currentAsLink': false}) }} .. code-block:: html+php - render('App:Builder:mainMenu', [ + render('my_main_menu', [ 'depth' => 2, 'currentAsLink' => false, ]) ?> @@ -265,12 +174,12 @@ You can also "get" a menu, which you can use to render later: .. code-block:: html+jinja - {% set menuItem = knp_menu_get('App:Builder:mainMenu') %} + {% set menuItem = knp_menu_get('my_main_menu') %} {{ knp_menu_render(menuItem) }} .. code-block:: html+php - get('App:Builder:mainMenu') ?> + get('my_main_menu') ?> render($menuItem) ?> If you want to only retrieve a certain branch of the menu, you can do the @@ -281,13 +190,13 @@ beneath it. .. code-block:: html+jinja - {% set menuItem = knp_menu_get('App:Builder:mainMenu', ['Contact']) %} - {{ knp_menu_render(['App:Builder:mainMenu', 'Contact']) }} + {% set menuItem = knp_menu_get('my_main_menu', ['Contact']) %} + {{ knp_menu_render(['my_main_menu', 'Contact']) }} .. code-block:: html+php - get('App:Builder:mainMenu', ['Contact']) ?> - render(['App:Builder:mainMenu', 'Contact']) ?> + get('my_main_menu', ['Contact']) ?> + render(['my_main_menu', 'Contact']) ?> If you want to pass some options to the builder, you can use the third parameter of the ``knp_menu_get`` function: @@ -296,12 +205,12 @@ of the ``knp_menu_get`` function: .. code-block:: html+jinja - {% set menuItem = knp_menu_get('App:Builder:mainMenu', [], {'some_option': 'my_value'}) %} + {% set menuItem = knp_menu_get('my_main_menu', [], {'some_option': 'my_value'}) %} {{ knp_menu_render(menuItem) }} .. code-block:: html+php - get('App:Builder:mainMenu', [], [ + get('my_main_menu', [], [ 'some_option' => 'my_value' ]) ?> render($menuItem) ?> @@ -314,6 +223,7 @@ More Advanced Stuff menu_service menu_builder_service + menu_convention i18n events custom_renderer diff --git a/docs/menu_builder_service.rst b/docs/menu_builder_service.rst index 146fc904..a7cd3bc2 100644 --- a/docs/menu_builder_service.rst +++ b/docs/menu_builder_service.rst @@ -1,15 +1,6 @@ Creating Menu Builders as Services ================================== -This bundle gives you a really convenient way to create menus by following -a convention. - -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. -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: @@ -20,6 +11,7 @@ builder classes in your application: namespace App\Menu; + use Knp\Menu\Attribute\AsMenuBuilder; use Knp\Menu\FactoryInterface; use Knp\Menu\ItemInterface; @@ -35,6 +27,7 @@ builder classes in your application: $this->factory = $factory; } + #[AsMenuBuilder(name: 'main')] // The name is what is used to retrieve the menu public function createMainMenu(array $options): ItemInterface { $menu = $this->factory->createItem('root'); @@ -46,7 +39,13 @@ builder classes in your application: } } -Next, register your menu builder as service and register its ``createMainMenu`` method as a menu builder: +That's it! The menu is *very* configurable. For more details, see the +`KnpMenu documentation`_. + +Next, register your menu builder as service and register its ``createMainMenu`` +method as a menu builder. When using autoconfiguration, the ``#[AsMenuBuilder]`` +attribute takes care of it. When not using autoconfiguration, you need to +register the menu builder by add the ``knp_menu.menu_builder`` tag: .. code-block:: yaml @@ -60,8 +59,7 @@ Next, register your menu builder as service and register its ``createMainMenu`` # ... -You can now render the menu directly in a template via the name given in the -``alias`` key above: +You can now render the menu directly in a template via the its name: .. code-block:: html+jinja @@ -80,6 +78,7 @@ is simple! Start by adding a new method to your builder: { // ... + #[AsMenuBuilder(name: 'sidebar')] public function createSidebarMenu(array $options): ItemInterface { $menu = $this->factory->createItem('sidebar'); @@ -94,25 +93,11 @@ is simple! Start by adding a new method to your builder: } } -Now, create a service for *just* your new menu, giving it a new name, like -``sidebar``: - -.. code-block:: yaml - - # config/services.yaml - services: - app.menu_builder: - class: App\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) }} + +.. _`KnpMenu documentation`: https://github.com/KnpLabs/KnpMenu/blob/master/doc/01-Basic-Menus.md diff --git a/docs/menu_convention.rst b/docs/menu_convention.rst new file mode 100644 index 00000000..f71ec479 --- /dev/null +++ b/docs/menu_convention.rst @@ -0,0 +1,73 @@ +Creating Menu via Naming Convention +=================================== + +KnpMenuBundle supports building menus using a naming convention to find +the class building the menu. + +.. warning:: + + The naming convention is relying on bundles. Project keeping their + code outside a bundle (like in the Flex skeleton) cannot use this + way of building menus. + +To create a menu, first create a new class in the ``Menu`` directory of one +of your bundles. This class - called ``Builder`` in our example - will have +one method for each menu that you need to build. +The builder methods will receive the ``Knp\Menu\FactoryInterface`` as first +argument and an array of options as second argument. + +An example builder class would look like this: + +.. code-block:: php + + // src/AppBundle/Menu/Builder.php + namespace AppBundle\Menu; + + use App\Entity\Blog; + use Knp\Menu\FactoryInterface; + use Knp\Menu\ItemInterface; + + final class Builder + { + public function __construct( + private EntityManagerInterface $em, + ) { + } + + public function mainMenu(FactoryInterface $factory, array $options): ItemInterface + { + $menu = $factory->createItem('root'); + + $menu->addChild('Home', ['route' => 'homepage']); + + // findMostRecent and Blog are just imaginary examples + $blog = $this->em->getRepository(Blog::class)->findMostRecent(); + + $menu->addChild('Latest Blog Post', [ + 'route' => 'blog_show', + 'routeParameters' => ['id' => $blog->getId()] + ]); + + // create another menu item + $menu->addChild('About Me', ['route' => 'about']); + // you can also add sub levels to your menus as follows + $menu['About Me']->addChild('Edit profile', ['route' => 'edit_profile']); + + // ... add more children + + return $menu; + } + } + +With this method, you refer to the menu using a three-part string: +**bundle**:**class**:**method**. + +If you needed to create a second menu, you'd simply add another method to +the ``Builder`` class (e.g. ``sidebarMenu``), build and return the new menu, +then render it via ``App:Builder:sidebarMenu``. + +You can now render the menu directly in a template via the its name: + +.. code-block:: html+jinja + + {{ knp_menu_render('App:Builder:mainMenu') }} diff --git a/docs/menu_service.rst b/docs/menu_service.rst index d2c8a622..e9ecb058 100644 --- a/docs/menu_service.rst +++ b/docs/menu_service.rst @@ -12,15 +12,6 @@ Creating Menus as Services It is recommended to register only :doc:`menu builders as services ` instead. -This bundle gives you a really convenient way to create menus by following -a convention. - -However, if you want to, you can instead choose to create a service for your -menu object. The advantage of this method is that you can inject the exact -dependencies that your menu needs. -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: diff --git a/src/DependencyInjection/KnpMenuExtension.php b/src/DependencyInjection/KnpMenuExtension.php index 339ff3aa..ef8cc347 100644 --- a/src/DependencyInjection/KnpMenuExtension.php +++ b/src/DependencyInjection/KnpMenuExtension.php @@ -2,10 +2,12 @@ namespace Knp\Bundle\MenuBundle\DependencyInjection; +use Knp\Menu\Attribute\AsMenuBuilder; use Knp\Menu\Factory\ExtensionInterface; use Knp\Menu\ItemInterface; use Knp\Menu\Matcher\Voter\VoterInterface; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; @@ -42,6 +44,12 @@ public function load(array $configs, ContainerBuilder $container): void ->addTag('knp_menu.voter'); $container->registerForAutoconfiguration(ExtensionInterface::class) ->addTag('knp_menu.factory_extension'); + $container->registerAttributeForAutoconfiguration(AsMenuBuilder::class, static function (ChildDefinition $definition, AsMenuBuilder $attribute, \ReflectionMethod $reflectionMethod): void { + $definition->addTag('knp_menu.menu_builder', [ + 'alias' => $attribute->name, + 'method' => $reflectionMethod->getName(), + ]); + }); } public function getNamespace(): string