8000 feature #30334 [DI] add ReverseContainer: a locator that turns servic… · symfony/symfony@05fe6a9 · GitHub
[go: up one dir, main page]

Skip to content

Commit 05fe6a9

Browse files
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 ------- ac1e429 [DI] add ReverseContainer: a locator that turns services back to their ids
2 parents a116347 + ac1e429 commit 05fe6a9

File tree

7 files changed

+239
-0
lines changed

7 files changed

+239
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class UnusedTagsPass implements CompilerPassInterface
2626
'cache.pool.clearer',
2727
'console.command',
2828
'container.hot_path',
29+
'container.reversible',
2930
'container.service_locator',
3031
'container.service_subscriber',
3132
'controller.service_arguments',

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
3232
use Symfony\Component\Debug\ErrorHandler;
3333
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
34+
use Symfony\Component\DependencyInjection\Compiler\RegisterReverseContainerPass;
3435
use Symfony\Component\DependencyInjection\ContainerBuilder;
3536
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
3637
use Symfony\Component\Form\De 8000 pendencyInjection\FormPass;
@@ -123,6 +124,8 @@ public function build(ContainerBuilder $container)
123124
$container->addCompilerPass(new TestServiceContainerRealRefPass(), PassConfig::TYPE_AFTER_REMOVING);
124125
$this->addCompilerPassIfExists($container, AddMimeTypeGuesserPass::class);
125126
$this->addCompilerPassIfExists($container, MessengerPass::class);
127+
$container->addCompilerPass(new RegisterReverseContainerPass(true));
128+
$container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING);
126129

127130
if ($container->getParameter('kernel.debug')) {
128131
$container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);

src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,11 @@
7373
</service>
7474

7575
<service id="services_resetter" class="Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter" public="true" />
76+
77+
<service id="reverse_container" class="Symfony\Component\DependencyInjection\ReverseContainer">
78+
<argument type="service" id="service_container" />
79+
<argument type="service_locator" />
80+
</service>
81+
<service id="Symfony\Component\DependencyInjection\ReverseContainer" alias="reverse_container" />
7682
</services>
7783
</container>

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
* added `%env(default:param_name:...)%` processor to fallback to a parameter or to null when using `%env(default::...)%`
99
* added support for deprecating aliases
1010
* made `ContainerParametersResource` final and not implement `Serializable` anymore
11+
* added `ReverseContainer`: a container that turns services back to their ids
1112
* added ability to define an index for a tagged collection
1213

1314
4.2.0
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\ContainerInterface;
17+
use Symfony\Component\DependencyInjection\Definition;
18+
use Symfony\Component\DependencyInjection\Reference;
19+
20+
/**
21+
* @author Nicolas Grekas <p@tchwork.com>
22+
*/
23+
class RegisterReverseContainerPass implements CompilerPassInterface
24+
{
25+
private $beforeRemoving;
26+
private $serviceId;
27+
private $tagName;
28+
29+
public function __construct(bool $beforeRemoving, string $serviceId = 'reverse_container', string $tagName = 'container.reversible')
30+
{
31+
$this->beforeRemoving = $beforeRemoving;
32+
$this->serviceId = $serviceId;
33+
$this->tagName = $tagName;
34+
}
35+
36+
public function process(ContainerBuilder $container)
37+
{
38+
if (!$container->hasDefinition($this->serviceId)) {
39+
return;
40+
}
41+
42+
$refType = $this->beforeRemoving ? ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE : ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
43+
$services = [];
44+
foreach ($container->findTaggedServiceIds($this->tagName) as $id => $tags) {
45+
$services[$id] = new Reference($id, $refType);
46+
}
47+
48+
if ($this->beforeRemoving) {
49+
// prevent inlining of the reverse container
50+
$services[$this->serviceId] = new Reference($this->serviceId, $refType);
51+
}
52+
$locator = $container->getDefinition($this->serviceId)->getArgument(1);
53+
54+
if ($locator instanceof Reference) {
55+
$locator = $container->getDefinition((string) $locator);
56+
}
57+
if ($locator instanceof Definition) {
58+
foreach ($services as $id => $ref) {
59+
$services[$id] = new ServiceClosureArgument($ref);
60+
}
61+
$locator->replaceArgument(0, $services);
62+
} else {
63+
$locator->setValues($services);
64+
}
65+
}
66+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection;
13+
14+
use Psr\Container\ContainerInterface;
15+
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
16+
17+
/**
18+
* Turns public and "container.reversible" services back to their ids.
19+
*
20+
* @author Nicolas Grekas <p@tchwork.com>
21+
*/
22+
final class ReverseContainer
23+
{
24+
private $serviceContainer;
25+
private $reversibleLocator;
26+
private $tagName;
27+
private $getServiceId;
28+
29+
public function __construct(Container $serviceContainer, ContainerInterface $reversibleLocator, string $tagName = 'container.reversible')
30+
{
31+
$this->serviceContainer = $serviceContainer;
32+
$this->reversibleLocator = $reversibleLocator;
33+
$this->tagName = $tagName;
34+
$this->getServiceId = \Closure::bind(function ($service): ?string {
35+
return array_search($service, $this->services, true) ?: array_search($service, $this->privates, true) ?: null;
36+
}, $serviceContainer, Container::class);
37+
}
38+
39+
/**
40+
* Returns the id of the passed object when it exists as a service.
41+
*
42+
* To be reversible, services need to be either public or be tagged with "container.reversible".
43+
*
44+
* @param object $service
45+
*/
46+
public function getId($service): ?string
47+
{
48+
if ($this->serviceContainer === $service) {
49+
return 'service_container';
50+
}
51+
52+
if (null === $id = ($this->getServiceId)($service)) {
53+
return null;
54+
}
55+
56+
if ($this->serviceContainer->has($id) || $this->reversibleLocator->has($id)) {
57+
return $id;
58+
}
59+
60+
return null;
61+
}
62+
63+
/**
64+
* @return object
65+
*
66+
* @throws ServiceNotFoundException When the service is not reversible
67+
*/
68+
public function getService(string $id)
69+
{
70+
if ($this->serviceContainer->has($id)) {
71+
return $this->serviceContainer->get($id);
72+
}
73+
74+
if ($this->reversibleLocator->has($id)) {
75+
return $this->reversibleLocator->get($id);
76+
}
77+
78+
if (isset($this->serviceContainer->getRemovedIds()[$id])) {
79+
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));
80+
}
81+
82+
// will throw a ServiceNotFoundException
83+
$this->serviceContainer->get($id);
84+
}
85+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
16+
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
17+
use Symfony\Component\DependencyInjection\Compiler\RegisterReverseContainerPass;
18+
use Symfony\Component\DependencyInjection\ContainerBuilder;
19+
use Symfony\Component\DependencyInjection\Reference;
20+
use Symfony\Component\DependencyInjection\ReverseContainer;
21+
22+
class RegisterReverseContainerPassTest extends TestCase
23+
{
24+
public function testCompileRemovesUnusedServices()
25+
{
26+
$container = new ContainerBuilder();
27+
$container->register('foo', 'stdClass');
28+
$container->register('reverse_container', ReverseContainer::class)
29+
->addArgument(new Reference('service_container'))
30+
->addArgument(new ServiceLocatorArgument([]))
31+
->setPublic(true);
32+
33+
$container->addCompilerPass(new RegisterReverseContainerPass(true));
34+
$container->compile();
35+
36+
$this->assertFalse($container->has('foo'));
37+
}
38+
39+
public function testPublicServices()
40+
{
41+
$container = new ContainerBuilder();
42+
$container->register('foo', 'stdClass')->setPublic(true);
43+
$container->register('reverse_container', ReverseContainer::class)
44+
->addArgument(new Reference('service_container'))
45+
->addArgument(new ServiceLocatorArgument([]))
46+
->setPublic(true);
47+
48+
$container->addCompilerPass(new RegisterReverseContainerPass(true));
49+
$container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING);
50+
$container->compile();
51+
52+
$foo = $container->get('foo');
53+
54+
$this->assertSame('foo', $container->get('reverse_container')->getId($foo));
55+
$this->assertSame($foo, $container->get('reverse_container')->getService('foo'));
56+
}
57+
58+
public function testReversibleServices()
59+
{
60+
$container = new ContainerBuilder();
61+
$container->register('bar', 'stdClass')->setProperty('foo', new Reference('foo'))->setPublic(true);
62+
$container->register('foo', 'stdClass')->addTag('container.reversible');
63+
$container->register('reverse_container', ReverseContainer::class)
64+
->addArgument(new Reference('service_container'))
65+
->addArgument(new ServiceLocatorArgument([]))
66+
->setPublic(true);
67+
68+
$container->addCompilerPass(new RegisterReverseContainerPass(true));
69+
$container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING);
70+
$container->compile();
71+
72+
$foo = $container->get('bar')->foo;
73+
74+
$this->assertSame('foo', $container->get('reverse_container')->getId($foo));
75+
$this->assertSame($foo, $container->get('reverse_container')->getService('foo'));
76+
}
77+
}

0 commit comments

Comments
 (0)
0