Lazy tagged iterator in Symfony
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('Drive. Brrrrrr'); } }<?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('crash engine or etc.'); } }<?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!
2 месяца назад