通过可迭代的类名获取服务-注入标记的服务

p.m*_*mar 5 php autowired symfony symfony-dependency-injection

我正在努力通过类别名称从注入的已标记服务组中获取特定服务。

这是一个示例:我标记实现DriverInterface为的所有服务app.driver并将其绑定到$drivers变量。

在其他一些服务中,我需要获取所有已标记app.driver并实例化的驱动程序,并且仅使用其中一些驱动程序。但是需要的驱动程序是动态的。

services.yml

_defaults:
        autowire: true
        autoconfigure: true
        public: false
        bind:
            $drivers: [!tagged app.driver]

_instanceof:
        DriverInterface:
            tags: ['app.driver']
Run Code Online (Sandbox Code Playgroud)

其他一些服务:

/**
 * @var iterable
 */
private $drivers;

/**
 * @param iterable $drivers
 */
public function __construct(iterable $drivers) 
{
    $this->drivers = $drivers;
}

public function getDriverByClassName(string $className): DriverInterface
{
    ????????
}
Run Code Online (Sandbox Code Playgroud)

因此,将实现的服务DriverInterface注入到$this->driversparam中作为可迭代的结果。我只能foreach通过它们,但是所有服务都将被实例化。

是否有其他方法可以将这些服务注入,以通过类名从它们获得特定服务而无需实例化其他服务?

我知道有可能将那些驱动程序公开并改为使用容器,但是我想避免以其他方式将容器注入服务中。

yiv*_*ivi 10

您不再(自 Symfony 4 起)需要创建编译器通道来配置服务定位器。

可以通过配置做任何事情,让 Symfony 发挥“魔力”。

您可以在配置中添加以下内容:

services:
  _instanceof:
    DriverInterface:
      tags: ['app.driver']
      lazy: true

  DriverConsumer:
    arguments:
      - !tagged_locator
        tag: 'app.driver'
Run Code Online (Sandbox Code Playgroud)

需要访问这些而不是接收 的服务iterable接收ServiceLocatorInterface

class DriverConsumer
{
    private $drivers;
    
    public function __construct(ServiceLocatorInterface $locator) 
    {
        $this->locator = $locator;
    }
    
    public function foo() {
        $driver = $this->locator->get(Driver::class);
        // where Driver is a concrete implementation of DriverInterface
    }
}
Run Code Online (Sandbox Code Playgroud)

就是这样。你不需要其他任何东西,它只是有效tm


完整示例

包含所有类的完整示例。

我们有:

FooInterface

class DriverConsumer
{
    private $drivers;
    
    public function __construct(ServiceLocatorInterface $locator) 
    {
        $this->locator = $locator;
    }
    
    public function foo() {
        $driver = $this->locator->get(Driver::class);
        // where Driver is a concrete implementation of DriverInterface
    }
}
Run Code Online (Sandbox Code Playgroud)

AbstractFoo

为了简化实现,我们将在具体服务中扩展一个抽象类:

abstract class AbstractFoo implements FooInterface
{
    public function whoAmI(): string {
        return get_class($this);
    }   
}
Run Code Online (Sandbox Code Playgroud)

服务实现

几个实现的服务 FooInterface

class FooOneService extends AbstractFoo { }
class FooTwoService extends AbstractFoo { }
Run Code Online (Sandbox Code Playgroud)

服务消费者

另一个需要服务定位器来使用我们刚刚定义的这两个服务的服务:

class Bar
{
    /**
     * @var \Symfony\Component\DependencyInjection\ServiceLocator
     */
    private $service_locator;

    public function __construct(ServiceLocator $service_locator) {
        $this->service_locator = $service_locator;
    }

    public function handle(): string {
        /** @var \App\Test\FooInterface $service */
        $service = $this->service_locator->get(FooOneService::class);

        return $service->whoAmI();
    }
}
Run Code Online (Sandbox Code Playgroud)

配置

唯一需要的配置是:

interface FooInterface
{
    public function whoAmI(): string;
}
Run Code Online (Sandbox Code Playgroud)

服务名称的 FQCN 替代方案

如果您想定义自己的服务名称而不是使用类名称,则可以使用静态方法来定义服务名称。配置将更改为:

App\Test\Bar:
        arguments:
          - !tagged_locator
            tag: 'test_foo_tag'
            default_index_method: 'fooIndex'
Run Code Online (Sandbox Code Playgroud)

其中fooIndex是在每个返回字符串的服务上定义的公共静态方法。注意:如果您使用此方法,您将无法通过类名获取服务。


Cer*_*rad 5

ServiceLocator将允许按名称访问服务而无需实例化其余服务。它确实需要编译器通过,但设置起来并不难。

use Symfony\Component\DependencyInjection\ServiceLocator;
class DriverLocator extends ServiceLocator
{
    // Leave empty
}
# Some Service
public function __construct(DriverLocator $driverLocator) 
{
    $this->driverLocator = $driverLocator;
}

public function getDriverByClassName(string $className): DriverInterface
{
    return $this->driverLocator->get($fullyQualifiedClassName);
}
Run Code Online (Sandbox Code Playgroud)

现在魔法来了:

# src/Kernel.php
# Make your kernel a compiler pass
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
class Kernel extends BaseKernel implements CompilerPassInterface {
...
# Dynamically add all drivers to the locator using a compiler pass
public function process(ContainerBuilder $container)
{
    $driverIds = [];
    foreach ($container->findTaggedServiceIds('app.driver') as $id => $tags) {
        $driverIds[$id] = new Reference($id);
    }
    $driverLocator = $container->getDefinition(DriverLocator::class);
    $driverLocator->setArguments([$driverIds]);
}
Run Code Online (Sandbox Code Playgroud)

和急速。假设您修复了我可能引入的任何语法错误或拼写错误,它应该可以工作。

为了获得额外的积分,您可以自动注册您的驱动程序类并删除服务文件中的该实例条目。

# Kernel.php
protected function build(ContainerBuilder $container)
{
    $container->registerForAutoconfiguration(DriverInterface::class)
        ->addTag('app.driver');
}
Run Code Online (Sandbox Code Playgroud)