Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Missing documentation about autowiring #368

Open
garak opened this issue Dec 1, 2017 · 10 comments
Open

Missing documentation about autowiring #368

garak opened this issue Dec 1, 2017 · 10 comments

Comments

@garak
Copy link
Collaborator

garak commented Dec 1, 2017

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
Copy link
Collaborator

stof commented Dec 1, 2017

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
Copy link
Collaborator Author

garak commented Dec 1, 2017

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
Copy link

poolerMF commented Dec 8, 2017

+1

@stof
Copy link
Collaborator

stof commented Dec 13, 2017

@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
Copy link
Collaborator Author

garak commented Sep 3, 2018

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

@vctls
Copy link

vctls commented Sep 6, 2019

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
Copy link
Collaborator Author

garak commented Apr 8, 2021

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
Copy link

wryk commented Jan 13, 2022

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.

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;
}
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);
    }
}
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
Copy link
Collaborator Author

garak commented Jan 13, 2022

@wryk yes please!

@stof
Copy link
Collaborator

stof commented Jan 14, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants