Skip to content

Commit c8dfc57

Browse files
[DI] add ReverseContainer: a locator that turns services back to their ids
1 parent 2e8bf33 commit c8dfc57

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\DependencyInjection\FormPass;
@@ -125,6 +126,8 @@ public function build(ContainerBuilder $container)
125126
$container->addCompilerPass(new TestServiceContainerRealRefPass(), PassConfig::TYPE_AFTER_REMOVING);
126127
$this->addCompilerPassIfExists($container, AddMimeTypeGuesserPass::class);
127128
$this->addCompilerPassIfExists($container, MessengerPass::class);
129+
$container->addCompilerPass(new RegisterReverseContainerPass(true));
130+
$container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING);
128131

129132
if ($container->getParameter('kernel.debug')) {
130133
$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
@@ -9,6 +9,7 @@ CHANGELOG
99
* added `%env(nullable:...)%` processor to allow empty variables to be processed as null values
1010
* added support for deprecating aliases
1111
* made `ContainerParametersResource` final and not implement `Serializable` anymore
12+
* added `ReverseContainer`: a container that turns services back to their ids
1213
* added ability to define an index for a tagged collection
1314

1415
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy