Получить сервис через имя класса из итерабельных - внедренных помеченных сервисов

Я изо всех сил пытаюсь получить конкретную услугу через имя класса из группы внедренных тегированных сервисов.

Вот пример: я помечаю все службы, которые реализуют 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']

Другая услуга:

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

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

public function getDriverByClassName(string $className): DriverInterface
{
    ????????
}

Таким образом, сервисы, реализующие DriverInterface, вводятся в $this->drivers param как повторяемый результат. Я могу только foreach через них, но тогда будут созданы все службы.

Есть ли другой способ внедрить эти службы, чтобы получить от них конкретную службу через имя класса, не создавая экземпляры других?

Я знаю, что есть возможность сделать эти драйверы общедоступными и вместо этого использовать контейнер, но я бы хотел избежать внедрения контейнера в службы, если это возможно сделать другим способом.


person povs    schedule 01.03.2019    source источник
comment
Сделайте каждого водителя ленивым сервисом.   -  person emix    schedule 01.03.2019
comment
В этом вам поможет новая функция, называемая индексированными службами: symfony.com/blog/ К сожалению, я думаю, что прямо сейчас нужно либо создать CompilerPass, если вы хотите решить эту проблему программно, либо добавить несколько тегов, например в зависимости от папки, в которой хранятся службы, или путем добавления тегов к каждой службе вручную.   -  person dbrumann    schedule 01.03.2019


Ответы (2)


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);
}

Теперь начинается волшебство:

# 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]);
}

И престо. Он должен работать, если вы исправите какие-либо синтаксические ошибки или опечатки, которые я мог допустить.

А за дополнительную плату вы можете автоматически зарегистрировать свои классы драйверов и избавиться от записи instanceof в файле служб.

# Kernel.php
protected function build(ContainerBuilder $container)
{
    $container->registerForAutoconfiguration(DriverInterface::class)
        ->addTag('app.driver');
}
person Cerad    schedule 01.03.2019

Вам больше не нужно (начиная с Symfony 4) создавать проход компилятора для настройки локатора служб.

Можно сделать все через конфигурацию и позволить Symfony творить чудеса.

Вы можете обойтись следующими дополнениями к вашей конфигурации:

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

  DriverConsumer:
    arguments:
      - !tagged_locator
        tag: 'app.driver'

Служба, которой требуется доступ к ним, вместо получения 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
    }
}

И это все. Больше ничего не нужно, просто работает tm.


Полный пример

Полный пример со всеми задействованными классами.

У нас есть:

FooInterface:

interface FooInterface
{
    public function whoAmI(): string;
}

AbstractFoo

Чтобы упростить реализацию, абстрактный класс, который мы расширим в наших конкретных сервисах:

abstract class AbstractFoo implements FooInterface
{
    public function whoAmI(): string {
        return get_class($this);
    }   
}

Реализации сервисов

Пара сервисов, реализующих FooInterface

class FooOneService extends AbstractFoo { }
class FooTwoService extends AbstractFoo { }

Потребитель услуг

И еще один сервис, которому требуется локатор сервисов для использования этих двух, которые мы только что определили:

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();
    }
}

Конфигурация

Единственная необходимая конфигурация:

services:
  _instanceof:
    App\Test\FooInterface:
      tags: ['test_foo_tag']
      lazy: true
    
  App\Test\Bar:
      arguments:
        - !tagged_locator
          tag: 'test_foo_tag'
            

Альтернатива FQCN для названий сервисов

Если вместо использования имени класса вы хотите определить свои собственные имена служб, вы можете использовать статический метод для определения имени службы. Конфигурация изменится на:

App\Test\Bar:
        arguments:
          - !tagged_locator
            tag: 'test_foo_tag'
            default_index_method: 'fooIndex'

где fooIndex - это общедоступный статический метод, определенный для каждой из служб, возвращающих строку. Внимание: если вы воспользуетесь этим методом, вы не сможете получать сервисы по именам их классов.

person yivi    schedule 19.09.2019
comment
Это элегантное решение, но следует отметить, что оно работает только в Symfony ›= 4. Для Symfony 3 вам все равно придется использовать проход компилятора - person Sean; 14.02.2020
comment
Ты прав. Это подразумевало «больше не нужно», но я должен сделать это ясно. Установок Symfony ‹4 с каждым днем ​​становится все меньше и меньше, но об этом стоит упомянуть. - person yivi; 14.02.2020
comment
Я работаю над устаревшей (3.4) кодовой базой и на самом деле пытаюсь сделать именно это, прошел половину вашего решения, прежде чем понял, что это функция 4+. Грустное лицо. - person Sean; 14.02.2020