28/06/2018 - SYMFONY
"Service locator" services help us to fetch other services by passing service names to it. This approach has two main benefits. First, the main class only knows about the "service locator" service and the second one is that amount of injected services into main class is minimised. For more information read Service Subscribers & Locators page.
This is what our controller would look like if we didn't have a service locator class.
class LocatorController
{
public function __construct(
ServiceA $serviceA,
ServiceB $serviceB,
ServiceC $serviceC,
ServiceD $serviceD,
...
) {
}
}
This is what we are going to have instead.
class LocatorController
{
public function __construct(
ServiceLocator $serviceLocator
) {
}
}
parameters:
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App\:
resource: '../src/*'
exclude: '../src/{Entity,Repository,Service,Kernel.php}'
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
App\Factory\ModelFactory:
arguments:
$id: 1
This is just a demonstration purposes!
declare(strict_types=1);
namespace App\Controller;
use App\Factory\ModelFactoryInterface;
use App\Locator\ServiceLocatorInterface;
use App\Service\CountryServiceInterface;
use App\Util\CalculatorUtilInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
/**
* @Route("/locators")
*/
class LocatorController
{
private $serviceLocator;
public function __construct(
ServiceLocatorInterface $serviceLocator
) {
$this->serviceLocator = $serviceLocator;
}
/**
* @Route("/{type}")
* @Method({"GET"})
*/
public function getService(string $type): Response
{
if ('util' === $type) {
/** @var CalculatorUtilInterface $service */
$service = $this->serviceLocator->get(CalculatorUtilInterface::class);
$result = $service->sum(1, 2);
} elseif ('factory' === $type) {
/** @var ModelFactoryInterface $service */
$service = $this->serviceLocator->get(ModelFactoryInterface::class);
$result = $service->create('inanzzz');
} else {
$this->serviceLocator->get(CountryServiceInterface::class);
// This would throw an exception because it is not registered in ServiceLocator class
}
return new JsonResponse($result);
}
}
declare(strict_types=1);
namespace App\Exception;
use RuntimeException;
class ServiceLocatorException extends RuntimeException
{
}
declare(strict_types=1);
namespace App\Factory;
use stdClass;
interface ModelFactoryInterface
{
public function create(string $name): stdClass;
}
declare(strict_types=1);
namespace App\Factory;
use stdClass;
class ModelFactory implements ModelFactoryInterface
{
private $id;
public function __construct(int $id)
{
$this->id = $id;
}
public function create(string $name): stdClass
{
$model = new stdClass();
$model->id = $this->id;
$model->name = $name;
return $model;
}
}
declare(strict_types=1);
namespace App\Util;
interface CalculatorUtilInterface
{
public function sum(int $a, int $b): int;
}
declare(strict_types=1);
namespace App\Util;
class CalculatorUtil implements CalculatorUtilInterface
{
public function sum(int $a, int $b): int
{
return $a+$b;
}
}
declare(strict_types=1);
namespace App\Locator;
interface ServiceLocatorInterface
{
public function get(string $id);
}
declare(strict_types=1);
namespace App\Locator;
use App\Exception\ServiceLocatorException;
use App\Factory\ModelFactoryInterface;
use App\Util\CalculatorUtilInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
class ServiceLocator implements ServiceLocatorInterface, ServiceSubscriberInterface
{
private $locator;
public function __construct(ContainerInterface $locator)
{
$this->locator = $locator;
}
public static function getSubscribedServices()
{
return [
'App\Factory\ModelFactoryInterface' => ModelFactoryInterface::class,
'App\Util\CalculatorUtilInterface' => CalculatorUtilInterface::class,
];
}
public function get(string $id)
{
if (!$this->locator->has($id)) {
throw new ServiceLocatorException(sprintf(
'The entry for the given "%s" identifier was not found.',
$id
));
}
try {
return $this->locator->get($id);
} catch (ContainerExceptionInterface $e) {
throw new ServiceLocatorException(sprintf(
'Failed to fetch the entry for the given "%s" identifier.',
$id
));
}
}
}
# GET /locators/factory
{"id":1,"name":"inanzzz"}
# GET /locators/util
3
# GET /locators/invalid
ServiceLocatorException
The entry for the given "App\Service\CountryServiceInterface" identifier was not found.