feature #30334 [DI] add ReverseContainer: a locator that turns services back to their ids (nicolas-grekas)

This PR was merged into the 4.3-dev branch.

Discussion
----------

[DI] add ReverseContainer: a locator that turns services back to their ids

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

This PR introduces a `ReverseContainer`, which is a class you can type hint for to get it as a service.

When you have a `ReverseContainer` at hand, you can then use it to know the service id of an object (if the object is not found, `null` is returned):
`$id = $reverseContainer->getId($someObject);`

You can also call `$reverseContainer->getService($id);` and get the service in return.

To be reversible, a service must either be public or be tagged with `container.reversible`.

I'm using this feature to serialize service references in a message, then send them through a Messenger bus, allowing the handler on the other side to use that referenced service to process the message. More specifically, my use case is sending messages for early cache expiration events through a bus and have a worker compute the soon-to-expire value in the background. The reversible services are the computation callbacks and the cache pools I need to compute the value for.

Commits
-------

ac1e4291e8 [DI] add ReverseContainer: a locator that turns services back to their ids
This commit is contained in:
Nicolas Grekas 2019-03-15 13:51:46 +01:00
commit 05fe6a939a
7 changed files with 239 additions and 0 deletions

View File

@ -26,6 +26,7 @@ class UnusedTagsPass implements CompilerPassInterface
'cache.pool.clearer',
'console.command',
'container.hot_path',
'container.reversible',
'container.service_locator',
'container.service_subscriber',
'controller.service_arguments',

View File

@ -31,6 +31,7 @@ use Symfony\Component\Config\Resource\ClassExistenceResource;
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\Compiler\RegisterReverseContainerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
use Symfony\Component\Form\DependencyInjection\FormPass;
@ -123,6 +124,8 @@ class FrameworkBundle extends Bundle
$container->addCompilerPass(new TestServiceContainerRealRefPass(), PassConfig::TYPE_AFTER_REMOVING);
$this->addCompilerPassIfExists($container, AddMimeTypeGuesserPass::class);
$this->addCompilerPassIfExists($container, MessengerPass::class);
$container->addCompilerPass(new RegisterReverseContainerPass(true));
$container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING);
if ($container->getParameter('kernel.debug')) {
$container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);

View File

@ -73,5 +73,11 @@
</service>
<service id="services_resetter" class="Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter" public="true" />
<service id="reverse_container" class="Symfony\Component\DependencyInjection\ReverseContainer">
<argument type="service" id="service_container" />
<argument type="service_locator" />
</service>
<service id="Symfony\Component\DependencyInjection\ReverseContainer" alias="reverse_container" />
</services>
</container>

View File

@ -8,6 +8,7 @@ CHANGELOG
* added `%env(default:param_name:...)%` processor to fallback to a parameter or to null when using `%env(default::...)%`
* added support for deprecating aliases
* made `ContainerParametersResource` final and not implement `Serializable` anymore
* added `ReverseContainer`: a container that turns services back to their ids
* added ability to define an index for a tagged collection
4.2.0

View File

@ -0,0 +1,66 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class RegisterReverseContainerPass implements CompilerPassInterface
{
private $beforeRemoving;
private $serviceId;
private $tagName;
public function __construct(bool $beforeRemoving, string $serviceId = 'reverse_container', string $tagName = 'container.reversible')
{
$this->beforeRemoving = $beforeRemoving;
$this->serviceId = $serviceId;
$this->tagName = $tagName;
}
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition($this->serviceId)) {
return;
}
$refType = $this->beforeRemoving ? ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE : ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
$services = [];
foreach ($container->findTaggedServiceIds($this->tagName) as $id => $tags) {
$services[$id] = new Reference($id, $refType);
}
if ($this->beforeRemoving) {
// prevent inlining of the reverse container
$services[$this->serviceId] = new Reference($this->serviceId, $refType);
}
$locator = $container->getDefinition($this->serviceId)->getArgument(1);
if ($locator instanceof Reference) {
$locator = $container->getDefinition((string) $locator);
}
if ($locator instanceof Definition) {
foreach ($services as $id => $ref) {
$services[$id] = new ServiceClosureArgument($ref);
}
$locator->replaceArgument(0, $services);
} else {
$locator->setValues($services);
}
}
}

View File

@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
/**
* Turns public and "container.reversible" services back to their ids.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class ReverseContainer
{
private $serviceContainer;
private $reversibleLocator;
private $tagName;
private $getServiceId;
public function __construct(Container $serviceContainer, ContainerInterface $reversibleLocator, string $tagName = 'container.reversible')
{
$this->serviceContainer = $serviceContainer;
$this->reversibleLocator = $reversibleLocator;
$this->tagName = $tagName;
$this->getServiceId = \Closure::bind(function ($service): ?string {
return array_search($service, $this->services, true) ?: array_search($service, $this->privates, true) ?: null;
}, $serviceContainer, Container::class);
}
/**
* Returns the id of the passed object when it exists as a service.
*
* To be reversible, services need to be either public or be tagged with "container.reversible".
*
* @param object $service
*/
public function getId($service): ?string
{
if ($this->serviceContainer === $service) {
return 'service_container';
}
if (null === $id = ($this->getServiceId)($service)) {
return null;
}
if ($this->serviceContainer->has($id) || $this->reversibleLocator->has($id)) {
return $id;
}
return null;
}
/**
* @return object
*
* @throws ServiceNotFoundException When the service is not reversible
*/
public function getService(string $id)
{
if ($this->serviceContainer->has($id)) {
return $this->serviceContainer->get($id);
}
if ($this->reversibleLocator->has($id)) {
return $this->reversibleLocator->get($id);
}
if (isset($this->serviceContainer->getRemovedIds()[$id])) {
throw new ServiceNotFoundException($id, null, null, [], sprintf('The "%s" service is private and cannot be accessed by reference. You should either make it public, or tag it as "%s".', $id, $this->tagName));
}
// will throw a ServiceNotFoundException
$this->serviceContainer->get($id);
}
}

View File

@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\Compiler\RegisterReverseContainerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ReverseContainer;
class RegisterReverseContainerPassTest extends TestCase
{
public function testCompileRemovesUnusedServices()
{
$container = new ContainerBuilder();
$container->register('foo', 'stdClass');
$container->register('reverse_container', ReverseContainer::class)
->addArgument(new Reference('service_container'))
->addArgument(new ServiceLocatorArgument([]))
->setPublic(true);
$container->addCompilerPass(new RegisterReverseContainerPass(true));
$container->compile();
$this->assertFalse($container->has('foo'));
}
public function testPublicServices()
{
$container = new ContainerBuilder();
$container->register('foo', 'stdClass')->setPublic(true);
$container->register('reverse_container', ReverseContainer::class)
->addArgument(new Reference('service_container'))
->addArgument(new ServiceLocatorArgument([]))
->setPublic(true);
$container->addCompilerPass(new RegisterReverseContainerPass(true));
$container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING);
$container->compile();
$foo = $container->get('foo');
$this->assertSame('foo', $container->get('reverse_container')->getId($foo));
$this->assertSame($foo, $container->get('reverse_container')->getService('foo'));
}
public function testReversibleServices()
{
$container = new ContainerBuilder();
$container->register('bar', 'stdClass')->setProperty('foo', new Reference('foo'))->setPublic(true);
$container->register('foo', 'stdClass')->addTag('container.reversible');
$container->register('reverse_container', ReverseContainer::class)
->addArgument(new Reference('service_container'))
->addArgument(new ServiceLocatorArgument([]))
->setPublic(true);
$container->addCompilerPass(new RegisterReverseContainerPass(true));
$container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING);
$container->compile();
$foo = $container->get('bar')->foo;
$this->assertSame('foo', $container->get('reverse_container')->getId($foo));
$this->assertSame($foo, $container->get('reverse_container')->getService('foo'));
}
}