Skip to content

Commit

Permalink
minor #18010 Improve description of ad hoc containers (nicolas-grekas)
Browse files Browse the repository at this point in the history
This PR was merged into the 6.2 branch.

Discussion
----------

Improve description of ad hoc containers

I think we shouldn't advertise `ServiceLocator` that much.

Here is a proposal to decouple a bit from DI and use more abstract concepts.

Commits
-------

9a6f38f Improve description of ad hoc containers
  • Loading branch information
OskarStark committed Mar 11, 2023
2 parents 12880b8 + 9a6f38f commit e3cfa6d
Showing 1 changed file with 45 additions and 53 deletions.
98 changes: 45 additions & 53 deletions service_container/service_subscribers_locators.rst
Expand Up @@ -30,24 +30,22 @@ to handle their respective command when it is asked for::
class CommandBus
{
/**
* @var CommandHandler[]
* @param CommandHandler[] $handlerMap
*/
private $handlerMap;

public function __construct(array $handlerMap)
{
$this->handlerMap = $handlerMap;
public function __construct(
private array $handlerMap,
) {
}

public function handle(Command $command)
{
$commandClass = get_class($command);

if (!isset($this->handlerMap[$commandClass])) {
if (!$handler = $this->handlerMap[$commandClass] ?? null) {
return;
}

return $this->handlerMap[$commandClass]->handle($command);
return $handler->handle($command);
}
}

Expand All @@ -72,8 +70,7 @@ Defining a Service Subscriber

First, turn ``CommandBus`` into an implementation of :class:`Symfony\\Contracts\\Service\\ServiceSubscriberInterface`.
Use its ``getSubscribedServices()`` method to include as many services as needed
in the service subscriber and change the type hint of the container to
a PSR-11 ``ContainerInterface``::
in the service subscriber::

// src/CommandBus.php
namespace App;
Expand All @@ -85,11 +82,9 @@ a PSR-11 ``ContainerInterface``::

class CommandBus implements ServiceSubscriberInterface
{
private $locator;

public function __construct(ContainerInterface $locator)
{
$this->locator = $locator;
public function __construct(
private ContainerInterface $locator,
) {
}

public static function getSubscribedServices(): array
Expand Down Expand Up @@ -119,8 +114,12 @@ a PSR-11 ``ContainerInterface``::
can also manually add the ``container.service_subscriber`` tag.

The injected service is an instance of :class:`Symfony\\Component\\DependencyInjection\\ServiceLocator`
which implements the PSR-11 ``ContainerInterface``, but it is also a callable::
which implements both the PSR-11 ``ContainerInterface`` and :class:`Symfony\\Contracts\\Service\\ServiceProviderInterface`.
It is also a callable and a countable::

// ...
$numberOfHandlers = count($this->locator);
$nameOfHandlers = array_keys($this->locator->getProvidedServices());
// ...
$handler = ($this->locator)($commandClass);

Expand Down Expand Up @@ -312,15 +311,16 @@ argument of type ``service_locator``.
Consider the following ``CommandBus`` class where you want to inject
some services into it via a service locator::

// src/HandlerCollection.php
// src/CommandBus.php
namespace App;

use Symfony\Component\DependencyInjection\ServiceLocator;
use Psr\Container\ContainerInterface;

class CommandBus
{
public function __construct(ServiceLocator $locator)
{
public function __construct(
private ContainerInterface $locator,
) {
}
}

Expand All @@ -334,14 +334,15 @@ or directly via PHP attributes:
// src/CommandBus.php
namespace App;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
use Symfony\Component\DependencyInjection\ServiceLocator;
class CommandBus
{
public function __construct(
// creates a service locator with all the services tagged with 'app.handler'
#[TaggedLocator('app.handler')] ServiceLocator $locator
#[TaggedLocator('app.handler')]
private ContainerInterface $locator,
) {
}
}
Expand Down Expand Up @@ -571,14 +572,14 @@ of the ``key`` tag attribute (as defined in the ``index_by`` locator option):
// src/CommandBus.php
namespace App;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
use Symfony\Component\DependencyInjection\ServiceLocator;
class CommandBus
{
public function __construct(
#[TaggedLocator('app.handler', indexAttribute: 'key')]
ServiceLocator $locator
private ContainerInterface $locator,
) {
}
}
Expand Down Expand Up @@ -652,13 +653,13 @@ Inside this locator you can retrieve services by index using the value of the
// src/Handler/HandlerCollection.php
namespace App\Handler;

use Symfony\Component\DependencyInjection\ServiceLocator;
use Psr\Container\ContainerInterface;

class HandlerCollection
{
public function __construct(ServiceLocator $locator)
public function getHandlerTwo(ContainerInterface $locator)
{
$handlerTwo = $locator->get('handler_two');
return $locator->get('handler_two');
}

// ...
Expand Down Expand Up @@ -691,14 +692,14 @@ attribute to the locator service defining the name of this custom method:
// src/CommandBus.php
namespace App;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
use Symfony\Component\DependencyInjection\ServiceLocator;
class CommandBus
{
public function __construct(
#[TaggedLocator('app.handler', 'key', defaultIndexMethod: 'myOwnMethodName')]
ServiceLocator $locator
private ContainerInterface $locator,
) {
}
}
Expand Down Expand Up @@ -758,7 +759,7 @@ The :class:`Symfony\\Contracts\\Service\\ServiceSubscriberTrait` provides an
implementation for :class:`Symfony\\Contracts\\Service\\ServiceSubscriberInterface`
that looks through all methods in your class that are marked with the
:class:`Symfony\\Contracts\\Service\\Attribute\\SubscribedService` attribute. It
provides a ``ServiceLocator`` for the services of each method's return type.
describes the services needed by the class based on each method's return type.
The service id is ``__METHOD__``. This allows you to add dependencies to your
services based on type-hinted helper methods::

Expand Down Expand Up @@ -916,34 +917,25 @@ Here's an example::
Testing a Service Subscriber
----------------------------

To unit test a service subscriber, you can create a fake ``ServiceLocator``::
To unit test a service subscriber, you can create a fake container::

use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Contracts\Service\ServiceLocatorTrait;
use Symfony\Contracts\Service\ServiceProviderInterface;

$container = new class() extends ServiceLocator {
private $services = [];
// Create the fake services
$foo = new stdClass();
$bar = new stdClass();
$bar->foo = $foo;

public function __construct()
{
parent::__construct([
'foo' => function () {
return $this->services['foo'] = $this->services['foo'] ?? new stdClass();
},
'bar' => function () {
return $this->services['bar'] = $this->services['bar'] ?? $this->createBar();
},
]);
}

private function createBar()
{
$bar = new stdClass();
$bar->foo = $this->get('foo');

return $bar;
}
// Create the fake container
$container = new class([
'foo' => fn () => $foo,
'bar' => fn () => $bar,
]) implements ServiceProviderInterface {
use ServiceLocatorTrait;
};

// Create the service subscriber
$serviceSubscriber = new MyService($container);
// ...

Expand Down

0 comments on commit e3cfa6d

Please sign in to comment.