Я изо всех сил пытаюсь получить конкретную услугу через имя класса из группы внедренных помеченных услуг.
Вот пример:
Я помечаю все сервисы, реализующие 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 через них, но тогда все сервисы будут инстанцированы.
Есть ли другой способ внедрить эти службы, чтобы получить от них конкретную службу через имя класса без создания других?
Я знаю, что есть возможность сделать эти драйверы общедоступными и вместо этого использовать контейнер, но я хотел бы избежать внедрения контейнера в службы, если это возможно сделать каким-то другим способом.
В этом поможет новая функция, называемая индексированными службами: symfony.com/blog/… К сожалению, я думаю, что прямо сейчас либо нужно создать CompilerPass, если вы хотите решить эту проблему программно, либо добавить несколько тегов, например. в зависимости от папки, в которой хранятся службы, или вручную пометив каждую службу.






СервисЛокатор позволит получить доступ к сервису по имени без создания экземпляра остальных. Это требует прохождения компилятора, но его не так уж сложно настроить.
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');
}
Вам больше не нужно (начиная с 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'
Если вместо использования имени класса вы хотите определить свои собственные имена служб, вы можете использовать статический метод для определения имени службы. Конфигурация изменится на:
App\Test\Bar:
arguments:
- !tagged_locator
tag: 'test_foo_tag'
default_index_method: 'fooIndex'
где fooIndex — общедоступный статический метод, определенный для каждой из служб, который возвращает строку. Внимание: если вы используете этот метод, вы не сможете получить сервисы по именам их классов.
Это элегантное решение, но следует отметить, что оно работает только в Symfony >=4. Для Symfony 3 вам все равно придется использовать проход компилятора
Ты прав. «Больше не нужно» подразумевало это, но я должен сделать это явным. Установок Symfony <4 с каждым днем становится все меньше и меньше, но об этом стоит упомянуть.
Я работаю над устаревшей кодовой базой (3.4) и на самом деле пытаюсь сделать именно это, на полпути к вашему решению, прежде чем я понял, что это функция 4+. Грустное лицо.
Сделайте каждый драйвер ленивым сервисом.