KnpLabs / KnpMenuBundle

Object Oriented menus for your Symfony project.
http://knplabs.com
MIT License
1.4k stars 203 forks source link

Missing documentation about autowiring #368

Open garak opened 6 years ago

garak commented 6 years ago

Current docs are still showing the old approach (e.g. the one working with Symfony < 3.4). We need to cover also the new autowiring features of Symfony 3.4/4.0

stof commented 6 years ago

Well, this doc is perfectly valid today. The only change with autowiring is that you can remove the definition of the constructor arguments (the FactoryInterface implementation can be injected by the autowiring).

I have a new feature in my TODO list which will make it much easier to use the library in alongside autoconfiguration. But it is not there yet

garak commented 6 years ago

I know the docs are still valid, but I think it's a pity to miss showing the new features. I think docs should be expanded to cover both options, the old ones and the new ones. Other doubts: is there currently a way to avoid declaring a menu? For example, this is my current declaration:

    app.menu.main:
        class: Knp\Menu\MenuItem
        factory: ['@App\Menu\Builder', createMenu]
        tags:
            - { name: knp_menu.menu, alias: main }

Can I exploit somehow the autowiring/autoconfigure?

poolerMF commented 6 years ago

+1

stof commented 6 years ago

@garak only in my head. I know exactly how I want this to work for autoconfiguration, but I have not written the code yet (I need a few hours available for that but I'm quite busy these days)

garak commented 6 years ago

Sorry for bumping (I know it's not really nice), but I was wondering if there are some news here.

vctls commented 5 years ago

This works, apparently:

services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

  AppBundle\Menu\:
    resource: '../../src/AppBundle/Menu/*'

  AppBundle\Menu\Builder:
    tags:
      - { name: knp_menu.menu_builder, method: mainMenu, alias: main }
{{ knp_menu_render('main', { 'template': 'knpmenu/knp_menu.html.twig' }) }}

But I couldn't get it working without the tag. If I try to render the menu like this

{{ knp_menu_render('AppBundle:Builder:mainMenu', { 'template': 'knpmenu/knp_menu.html.twig' }) }}

BuilderAliasProvider tries to instanciate it without any of its dependencies.

garak commented 3 years ago

This is my idea for autowiring menu builders: we can create an interface and add tag/alias to all classes implementing it. Something like:

<?php
namespace Knp\Bundle\MenuBundle;
use Knp\Menu\ItemInterface;

interface MenuBuilderInterface
{
    public function build(): ItemInterface;
    public function getAlias(): string;
}
wryk commented 2 years ago

I added support of autoconfigurable menu builder services in my apps using an interface, I think people following this issue might be interested in the code :

// Here the usage
namespace App\Menu;

use Knp\Menu\FactoryInterface;
use Knp\Menu\ItemInterface;

class SidebarBuilder implements MenuBuilderInterface
{
    private FactoryInterface $factory;

    public function __construct(FactoryInterface $factory)
    {
        $this->factory = $factory;
    }

    static public function getAlias(): string
    {
        return 'sidebar';
    }

    public function build(array $options): ItemInterface
    {
        $menu = $this->factory->createItem('root');

        $menu->addChild('Home', ['route' => 'hompage']);

        return $menu;
    }
}

The interface is inspired by the autoconfigurable Symfony 4.2 FormTypeExtensionInterface. I don't use multiple builders in a service but an interface like EventSubscriberInterface could be provided instead of the one I used to allow this usage.

This works with a compiler pass adding menu builder services to those managed by the bundle (tagged by knp_menu.menu_builder and knp_menu.menu) in the lazy provider used by the bundle. I chose to use the existing provider because I couldn't find a good reason to register a dedicated provider. If you fear the friction with the bundle you can instead register a separated provider.

If the maintainers are ok with an autoconfigurable interface in the bundle I can make a merge request (not with this solution of course)

How to setup (click here to show code) *This is just how I did it, feel free to adapt the code to your use case if necessary.* ```php namespace App\Menu; use Knp\Menu\ItemInterface; // The interface enforce a statically available alias and a build method interface MenuBuilderInterface { /** * Statically available alias (like alias property in knp tags) */ static public function getAlias(): string; /** * Build method (like method property in knp tags) */ public function build(array $options): ItemInterface; } ``` ```php namespace App\Compiler; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Reference; class RegisterMenuBuildersPass implements CompilerPassInterface { private string $tagName; public function __construct(string $tagName) { $this->tagName = $tagName; } public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('knp_menu.menu_provider.lazy')) { return; } $providerDefinition = $container->getDefinition('knp_menu.menu_provider.lazy'); $builders = $providerDefinition->getArgument(0); foreach ($container->findTaggedServiceIds($this->tagName, true) as $id => $tag) { $alias = $container->getDefinition($id)->getClass()::getAlias(); $builders[$alias] = [new ServiceClosureArgument(new Reference($id)), 'build']; } $providerDefinition->replaceArgument(0, $builders); } } ``` ```php namespace App; use App\Compiler\RegisterMenuBuildersPass; use App\Menu\MenuBuilderInterface; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel as BaseKernel; class Kernel extends BaseKernel { use MicroKernelTrait; public function build(ContainerBuilder $container): void { // Tag name used to discover services using the menu builder interface $menuBuilderTagName = 'app.menu_builder'; // Autoconfigure the discovery tag so you don't need to do it manually $container->registerForAutoconfiguration(MenuBuilderInterface::class) ->addTag($menuBuilderTagName); // Add the compiler pass to handle menu builders using our interface $container->addCompilerPass(new RegisterMenuBuildersPass($menuBuilderTagName)); } } ```
garak commented 2 years ago

@wryk yes please!

stof commented 2 years ago

I started the work a few years ago in #392, but I never finished it