Hello everyone!

We have been investing plenty of personal time and energy for many years to share our knowledge with you all. However, we now need your help to keep this blog running. All you have to do is just click one of the adverts on the site, otherwise it will sadly be taken down due to hosting etc. costs. Thank you.

"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.


Without service locator


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

config/services.yaml


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

LocatorController


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

ServiceLocatorException


declare(strict_types=1);

namespace App\Exception;

use RuntimeException;

class ServiceLocatorException extends RuntimeException
{
}

ModelFactoryInterface


declare(strict_types=1);

namespace App\Factory;

use stdClass;

interface ModelFactoryInterface
{
public function create(string $name): stdClass;
}

ModelFactory


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

CalculatorUtilInterface


declare(strict_types=1);

namespace App\Util;

interface CalculatorUtilInterface
{
public function sum(int $a, int $b): int;
}

CalculatorUtil


declare(strict_types=1);

namespace App\Util;

class CalculatorUtil implements CalculatorUtilInterface
{
public function sum(int $a, int $b): int
{
return $a+$b;
}
}

ServiceLocatorInterface


declare(strict_types=1);

namespace App\Locator;

interface ServiceLocatorInterface
{
public function get(string $id);
}

ServiceLocator


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

Test


# 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.