Lazy tagged iterator in Symfony
Tagged iterator is powerful thing in Symfony and in Symfony 7 this feature has become better. You can inject list of lazy services in your classes with lazy service locator.
One interface for services
Let’s start from an interface for our lazy services:
<?php
declare(strict_types=1);
namespace App\Model\Car;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag]
interface CarInterface
{
public function drive(): void;
}
Pay attention at AutoconfigureTag. This attribute helps symfony autowire all services inside container.
Next, we will create a few implementations for our interface:
<?php
declare(strict_types=1);
namespace App\Model\Car;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
#[AsTaggedItem('bmw')]
class BmwCar implements CarInterface
{
public function drive(): void
{
dump('crash engine or etc.');
}
}
<?php
declare(strict_types=1);
namespace App\Model\Car;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
#[AsTaggedItem('audi')]
class AudiCar implements CarInterface
{
public function drive(): void
{
dump('Drive. Brrrrrr');
}
}
<?php
declare(strict_types=1);
namespace App\Model\Car;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
#[AsTaggedItem('mazda')]
class MazdaCar implements CarInterface
{
public function drive(): void
{
dump('Drive and drive.');
}
}
As you can see in each implementation we have AsTaggedItem attribute. Services can be injected inside container without this attribute, but naming will be like:
array:3 [▼
"App\Model\Car\AudiCar" => "App\Model\Car\AudiCar"
"App\Model\Car\BmwCar" => "App\Model\Car\BmwCar"
"App\Model\Car\MazdaCar" => "App\Model\Car\MazdaCar"
]
With AsTaggedItem attribute naming will be:
array:3 [▼
"audi" => "App\Model\Car\AudiCar"
"bmw" => "App\Model\Car\BmwCar"
"mazda" => "App\Model\Car\MazdaCar"
]
Service locator for our interface implementations
So, it is time to use our lazy service locator! Create class, that requires service locator:
<?php
declare(strict_types=1);
namespace App\Services\Provider;
use App\Model\Car\CarInterface;
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
use Symfony\Contracts\Service\ServiceCollectionInterface;
class CarProvider
{
public function __construct(
#[AutowireLocator(
services: CarInterface::class,
)]
private ServiceCollectionInterface $carServiceLocator,
) {
}
public function driveCar(string $carType): void
{
if (!$this->carServiceLocator->has($carType)) {
throw new \InvalidArgumentException(sprintf(
'Car for type "%s" not found!',
$carType,
));
}
/** @var CarInterface $car */
$car = $this->carServiceLocator->get($carType);
$car->drive();
}
/**
* @return array<string, string>
*/
public function getAvailableCars(): array
{
return $this->carServiceLocator->getProvidedServices();
}
}
where AutowireLocator is a convenient attribute which helps us show symfony what services we need. The first argument is name our CarInterface (it can also be a simple array of services). With AutowireLocator attribute, you can configure key and priority of your services. It also supports configuring via static methods.
Method below returns a list of key => service name for our locator and prevents initializing all services in locator.
/**
* @return array<string, string>
*/
public function getAvailableCars(): array
{
return $this->carServiceLocator->getProvidedServices();
}
You can use array keys for getting any service from locator.
Method driveCar shows an example of using locator in your application:
public function driveCar(string $carType): void
{
if (!$this->carServiceLocator->has($carType)) {
throw new \InvalidArgumentException(sprintf(
'Car for type "%s" not found!',
$carType,
));
}
/** @var CarInterface $car */
$car = $this->carServiceLocator->get($carType);
$car->drive();
}
You can check and get service by service key with service locator.
Using service locator in a real example
Below, you can see an example of using service locator in your app:
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Services\Provider\CarProvider;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
class ExampleController extends AbstractController
{
#[Route(
path: '/{carType?}',
defaults: [
'carType' => 'bmw',
]
)]
public function mainAction(
CarProvider $carProvider,
string $carType,
): JsonResponse {
$carProvider->driveCar($carType);
return new JsonResponse($carProvider->getAvailableCars());
}
}
Improvements
You can store service keys in static methods or in php enum and use them for fetching services from container and avoiding code duplication, but it is not the theme of this article.
See full example on https://github.com/no4ch/lazy-tagged-iterator-example
In the end, you can find more information about this feature in official documentation https://symfony.com/doc/7.1/service_container/service_subscribers_locators.html
Thanks for the reading!