【问题标题】:How to inject a group of services that implement the same interface, without declaring the wiring for each service?如何注入一组实现相同接口的服务,而不声明每个服务的接线?
【发布时间】:2019-09-19 14:04:46
【问题描述】:

我正在开发一个应用程序,其中有一些处理程序作为我希望能够调用的服务。他们都实现了ItemHandlerInterface。

我希望能够在控制器中检索所有ItemHandlerInterface 服务集合,而无需手动连接它们。

到目前为止,我专门标记了它们:

services.yaml

_instanceof:
    App\Model\ItemHandlerInterface:
        tags: [!php/const App\DependencyInjection\ItemHandlersCompilerPass::ITEM_HANDLER_TAG]
        lazy: true

并尝试在控制器中检索我的服务集合。如果只有一个服务实现ItemHandlerInterface,它就可以工作,但是一旦我创建了几个服务(比如下面的TestHandler 和Test2Handler,我最终会得到一个The service "service_locator.03wqafw.App\Controller\ItemUpdateController" has a dependency on a non-existent service "App\Model\ItemHandlerInterface".

如何动态检索实现我的接口的所有服务?

一个肮脏的解决方案是用public: true 强制所有ItemHandlerInterface 并将Container 传递给我的控制器构造函数。但这很丑陋,我想找到一种更优雅的方式。

ItemUpdateController

namespace App\Controller;

use App\Model\ItemHandlerInterface;
use App\Service\ItemFinder;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Debug\Exception\ClassNotFoundException;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use App\Model\Item;
use Psr\Container\ContainerInterface;

/**
 * Class ItemUpdateController
 *
 * @package App\Controller
 */
class ItemUpdateController extends AbstractController
{
    /**
     * @var ContainerInterface
     */
    protected $locator;

    public function __construct(ContainerInterface $locator)
    {
        $this->locator = $locator;
    }

    public static function getSubscribedServices()
    {
        // Try to subscribe to all ItemHandlerInterface services
        return array_merge(
                parent::getSubscribedServices(),
                ['item_handler' => ItemHandlerInterface::class]
        );
    }

    /**
     * @param string $id
     * @param RequestStack $requestStack
     * @param ItemFinder $itemFinder
     *
     * @return Item
     * @throws \Symfony\Component\Debug\Exception\ClassNotFoundException
     */
    public function __invoke(
        string $id,
        RequestStack $requestStack,
        ItemFinder $itemFinder
    ) {
        // Find item
        $item = $itemFinder->findById($id);

        // Extract and create handler instance
        $handlerName = $item->getHandlerName();

        if($this->locator->has($handlerName)) {

            $handler = $this->locator->get($handlerName);
            $request = $requestStack->getCurrentRequest();
            $payload = json_decode($request->getContent());

            call_user_func($handler, $payload, $request);

            return $item;
        }
    }
}

src/ItemHandler/TestHandler.php

namespace App\ItemHandler;

use App\Model\ItemHandlerInterface;
use Doctrine\ORM\EntityManagerInterface;

class TestHandler implements ItemHandlerInterface
{
// implementation
}

src/ItemHandler/Test2Handler.php

namespace App\ItemHandler;

use App\Model\ItemHandlerInterface;
use Doctrine\ORM\EntityManagerInterface;

class Test2Handler implements ItemHandlerInterface
{
// implementation
}

【问题讨论】:

  • 不确定如果没有某种 ItemHandlers 定位器类,这将如何工作。 Symfony 如何知道将哪个定位器注入您的控制器?也许尝试按照这里的答案,看看会发生什么:stackoverflow.com/questions/54946647/…
  • 您想在您的控制器中注入ItemHandlerInterface 的所有实现,是这样吗?但是您说“无需手动连接它们”……例如,您不想将代码添加到 services.yaml 中?或者这样可以吗?
  • @yivi 这正是我的意思,至少不是一一。这个想法是发布应用程序,让开发人员仅添加 ItemHandlerInterface 实现,并让应用程序在运行时选择与控制器调用时给出的参数有关的应用程序。

标签: php symfony dependency-injection


【解决方案1】:

您可以一口气注入所有标记的服务,而无需使用编译器通道。

配置

由于您已经在进行标记,如问题所示,只需声明注入即可:

_instanceof:
    App\Model\ItemHandlerInterface:
        tags: ['item_handler']
        lazy: true

services:
    App\Controller\ItemUpdateController:
        arguments: !tagged 'item_handler'

实施

您需要更改控制器的构造函数,使其接受iterable:

public function __construct(iterable $itemHandlers)
{
    $this->handlers = $itemHandlers;
}

在您的班级中,RewindableGenerator 将被注入您的服务。您可以简单地对其进行迭代以获取其中的每一个。

这已提供since 3.4;它是still supported。


额外

从 4.3 开始,您可以为此使用标记的服务定位器。配置同样简单,但您可以获得能够延迟实例化服务的优势,而不必从一开始就实例化所有服务。

您可以阅读更多here。

【讨论】:

  • 我只是提到在 !tagged 之后不断使用 PHP 似乎对我不起作用。我现在将其删除。
  • @yivi 您省略了将处理程序视为数组所需的 iterator_to_array() 调用。我不得不将处理程序的类型提示从 iterable 更改为 Traversable 以保持 IDE 满意。实际上,您最终会得到一个不允许数组访问的 $handlers 的 Symfony RewindableGenerator。这一切都很令人困惑。当然,使用 iterator_to_array 函数会实例化所有单独的处理程序,这有点令人难过。
  • 另外,我无法让静态方法密钥工作。我必须在服务文件中为每个处理程序显式声明一个键,然后使用 index_by 属性。
  • 你是对的,@Cerad。我不仅忘记了复制array_to_iterator,而且这似乎并没有完全按预期工作。不过,它确实适用于tagged_locator,所以我在您链接的问题中添加了一个答案。
【解决方案2】:

当我输入这个时,我刚刚看到一个答案被接受了。很公平。无论如何,这是可行的,我现在将其作为参考:

services:
   _instanceof:
        # Tag all your item handlers
        App\Model\ItemHandlerInterface:
            tags: [app.item_handler]

    # inject as an iterable into the controller
    App\Controller\IndexController:
        arguments: [!tagged app.item_handler]

与接受的答案相同的参考:https://symfony.com/blog/new-in-symfony-3-4-simpler-injection-of-tagged-services

我还想指出,这种方法只支持可迭代。如果您想随机访问特定的项目处理程序(可能通过类名)而不实例化其余部分,那么您需要创建 own locator 类,这需要更多的努力。

【讨论】:

    【解决方案3】:

    这样做的一个好方法是使用CompilerPass 来收集所有标记的服务并将结果作为控制器的参数注入。
    借助 ContainerBuilder 类(例如使用 findTaggedServiceIds),您可以从那里访问查找服务所需的所有方法

    Sylius 在内部经常使用这个技巧,甚至有一个预先制作的编译器通过以抽象的方式执行此操作(因此您可以检查它是如何在内部完成的)。
    要使用它,我们只需要创建一个新的,扩展这个,并使用正确的参数调用父级__construct()。 (an example here)

    在那里检查:

    https://github.com/diimpp/Sylius/blob/master/src/Sylius/Bundle/ResourceBundle/DependencyInjection/Compiler/PrioritizedCompositeServicePass.php

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-02-09
      • 1970-01-01
      • 2022-11-18
      • 2014-07-01
      • 1970-01-01
      • 2019-11-26
      • 1970-01-01
      • 2015-05-20
      相关资源
      最近更新 更多