Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f5cfdf0

Browse files
[DI] Add and wire ServiceSubscriberInterface
1 parent e7c12d3 commit f5cfdf0

File tree

9 files changed

+422
-1
lines changed

9 files changed

+422
-1
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class UnusedTagsPass implements CompilerPassInterface
3333
'kernel.event_listener',
3434
'kernel.event_subscriber',
3535
'kernel.fragment_renderer',
36+
'kernel.service_subscriber',
3637
'monolog.logger',
3738
'routing.expression_language_provider',
3839
'routing.loader',

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public function testProcess()
2323
$container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerBuilder')->setMethods(array('findTaggedServiceIds', 'findUnusedTags', 'findTags', 'log'))->getMock();
2424
$container->expects($this->once())
2525
->method('log')
26-
->with($pass, 'Tag "kenrel.event_subscriber" was defined on service(s) "foo", "bar", but was never used. Did you mean "kernel.event_subscriber"?');
26+
->with($pass, 'Tag "kenrel.event_subscriber" was defined on service(s) "foo", "bar", but was never used. Did you mean "kernel.event_subscriber", "kernel.service_subscriber"?');
2727
$container->expects($this->once())
2828
->method('findTags')
2929
->will($this->returnValue(array('kenrel.event_subscriber')));

src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public function __construct()
5454
new ResolveFactoryClassPass(),
5555
new FactoryReturnTypePass($resolveClassPass),
5656
new CheckDefinitionValidityPass(),
57+
new RegisterServiceSubscribersPass(),
5758
new ResolveNamedArgumentsPass(),
5859
new AutowirePass(),
5960
new ResolveReferencesToAliasesPass(),
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\ContainerInterface;
16+
use Symfony\Component\DependencyInjection\Definition;
17+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
18+
use Symfony\Component\DependencyInjection\Reference;
19+
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
20+
use Symfony\Component\DependencyInjection\ServiceLocator;
21+
use Symfony\Component\DependencyInjection\TypedReference;
22+
23+
/**
24+
* Compiler pass to register tagged services that require a service locator.
25+
*
26+
* @author Nicolas Grekas <[email protected]>
27+
*
28+
* @experimental in version 3.3
29+
*/
30+
class RegisterServiceSubscribersPass extends AbstractRecursivePass
31+
{
32+
private $serviceLocator;
33+
34+
protected function processValue($value, $isRoot = false)
35+
{
36+
if ($value instanceof Reference && $this->serviceLocator && 'service_container' === (string) $value) {
37+
return new Reference($this->serviceLocator);
38+
}
39+
40+
if (!$value instanceof Definition || $value->isAbstract() || $value->isSynthetic() || !$value->hasTag('kernel.service_subscriber')) {
41+
return parent::processValue($value, $isRoot);
42+
}
43+
44+
$serviceMap = array();
45+
46+
foreach ($value->getTag('kernel.service_subscriber') as $attributes) {
47+
if (!$attributes) {
48+
continue;
49+
}
50+
ksort($attributes);
51+
if (array('key', 'service') !== $key = array_keys(array_filter($attributes))) {
52+
throw new InvalidArgumentException(sprintf('A "kernel.service_subscriber" tag must have either zero or exactly two non-empty "key" and "service" attributes, "%s" given for service "%s".', implode('", "', $key), $this->currentId));
53+
}
54+
if (isset($serviceMap[$attributes['key']])) {
55+
continue;
56+
}
57+
$serviceMap[$attributes['key']] = new Reference($attributes['service']);
58+
}
59+
$class = $value->getClass();
60+
61+
if (!is_subclass_of($class, ServiceSubscriberInterface::class)) {
62+
if (!class_exists($class, false)) {
63+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $this->currentId));
64+
}
65+
66+
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $this->currentId, ServiceSubscriberInterface::class));
67+
}
68+
$this->container->addObjectResource($class);
69+
$subscriberMap = array();
70+
71+
foreach ($class::getSubscribedServices() as $key => $type) {
72+
if (!is_string($type) || !preg_match('/^\??[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+$/', $type)) {
73+
throw new InvalidArgumentException(sprintf('%s::getSubscribedServices() must return valid PHP types for service "%s" key "%s", "%s" returned.', $class, $this->currentId, $key, is_string($type) ? $type : gettype($type)));
74+
}
75+
if ($optionalBehavior = '?' === $type[0]) {
76+
$type = substr($type, 1);
77+
$optionalBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
78+
}
79+
if (is_int($key)) {
80+
$key = $type;
81+
}
82+
if (!isset($serviceMap[$key])) {
83+
$serviceMap[$key] = new Reference($type);
84+
}
85+
86+
$subscriberMap[$key] = new ServiceClosureArgument(new TypedReference((string) $serviceMap[$key], $type, $optionalBehavior ?: ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE));
87+
unset($serviceMap[$key]);
88+
}
89+
90+
if ($serviceMap = array_keys($serviceMap)) {
91+
$this->container->log($this, sprintf('Service keys "%s" do not exist in the map returned by %s::getSubscribedServices() for service "%s".', implode('", "', $serviceMap), $class, $this->currentId));
92+
}
93+
94+
$serviceLocator = $this->serviceLocator;
95+
$this->serviceLocator = 'service_locator.'.$this->currentId.'.'.md5(serialize($value));
96+
$this->container->register($this->serviceLocator, ServiceLocator::class)->addArgument($subscriberMap)->setPublic(false)->setAutowired($value->isAutowired());
97+
98+
try {
99+
return parent::processValue($value);
100+
} finally {
101+
$this->serviceLocator = $serviceLocator;
102+
}
103+
}
104+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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+
/**
15+
* A ServiceSubscriber exposes its dependencies via the static {@link getSubscribedServices} method.
16+
*
17+
* The getSubscribedServices method returns an array of service types required by such instances,
18+
* optionally keyed by the service names used internally. Service types that start with an interrogation
19+
* mark "?" are optional, while the other ones are mandatory service dependencies.
20+
*
21+
* The injected service locators SHOULD NOT allow access to any other services not specified by the method.
22+
*
23+
* It is expected that ServiceSubscriber instances consume PSR-11-based service locators internally.
24+
* This interface does not dictate any injection method for these service locators, although constructor
25+
* injection is recommended.
26+
*
27+
* @author Nicolas Grekas <[email protected]>
28+
*
29+
* @experimental in version 3.3
30+
*/
31+
interface ServiceSubscriberInterface
32+
{
33+
/**
34+
* Returns an array of service types required by such instances, optionally keyed by the service names used internally.
35+
*
36+
* For mandatory dependencies:
37+
*
38+
* * array('logger' => 'Psr\Log\LoggerInterface') means the objects use the "logger" name
39+
* internally to fetch a service which must implement Psr\Log\LoggerInterface.
40+
* * array('Psr\Log\LoggerInterface') is a shortcut for
41+
* * array('Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface')
42+
*
43+
* otherwise:
44+
*
45+
* * array('logger' => '?Psr\Log\LoggerInterface') denotes an optional dependency
46+
* * array('?Psr\Log\LoggerInterface') is a shortcut for
47+
* * array('Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface')
48+
*
49+
* @return array The required service types, optionally keyed by service names
50+
*/
51+
public static function getSubscribedServices();
52+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\ServiceClosureArgument;
16+
use Symfony\Component\DependencyInjection\Compiler\RegisterServiceSubscribersPass;
17+
use Symfony\Component\DependencyInjection\ContainerBuilder;
18+
use Symfony\Component\DependencyInjection\ContainerInterface;
19+
use Symfony\Component\DependencyInjection\Reference;
20+
use Symfony\Component\DependencyInjection\ServiceLocator;
21+
use Symfony\Component\DependencyInjection\TypedReference;
22+
23+
require_once __DIR__.'/../Fixtures/includes/classes.php';
24+
25+
class RegisterServiceSubscribersPassTest extends TestCase
26+
{
27+
/**
28+
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
29+
* @expectedExceptionMessage Service "foo" must implement interface "Symfony\Component\DependencyInjection\ServiceSubscriberInterface".
30+
*/
31+
public function testInvalidClass()
32+
{
33+
$container = new ContainerBuilder();
34+
35+
$container->register('foo', 'stdClass')
36+
->addTag('kernel.service_subscriber')
37+
;
38+
39+
$pass = new RegisterServiceSubscribersPass();
40+
$pass->process($container);
41+
}
42+
43+
/**
44+
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
45+
* @expectedExceptionMessage A "kernel.service_subscriber" tag must have either zero or exactly two non-empty "key" and "service" attributes, "bar" given for service "foo".
46+
*/
47+
public function testInvalidAttributes()
48+
{
49+
$container = new ContainerBuilder();
50+
51+
$container->register('foo', 'TestServiceSubscriber')
52+
->addTag('kernel.service_subscriber', array('bar' => '123'))
53+
;
54+
55+
$pass = new RegisterServiceSubscribersPass();
56+
$pass->process($container);
57+
}
58+
59+
public function testNoAttributes()
60+
{
61+
$container = new ContainerBuilder();
62+
63+
$container->register('foo', 'TestServiceSubscriber')
64+
->addArgument(new Reference('service_container'))
65+
->addTag('kernel.service_subscriber')
66+
;
67+
68+
$pass = new RegisterServiceSubscribersPass();
69+
$pass->process($container);
70+
71+
$foo = $container->getDefinition('foo');
72+
$locator = $container->getDefinition((string) $foo->getArgument(0));
73+
74+
$this->assertFalse($locator->isPublic());
75+
$this->assertSame(ServiceLocator::class, $locator->getClass());
76+
77+
$expected = array(
78+
'TestServiceSubscriber' => new ServiceClosureArgument(new TypedReference('TestServiceSubscriber', 'TestServiceSubscriber')),
79+
'stdClass' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
80+
'bar' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass')),
81+
'baz' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
82+
);
83+
84+
$this->assertEquals($expected, $locator->getArgument(0));
85+
}
86+
87+
public function testWithAttributes()
88+
{
89+
$container = new ContainerBuilder();
90+
91+
$container->register('foo', 'TestServiceSubscriber')
92+
->addArgument(new Reference('service_container'))
93+
->addTag('kernel.service_subscriber', array('key' => 'bar', 'service' => 'bar'))
94+
->addTag('kernel.service_subscriber', array('key' => 'bar', 'service' => 'baz')) // should be ignored: the first wins
95+
;
96+
97+
$pass = new RegisterServiceSubscribersPass();
98+
$pass->process($container);
99+
100+
$foo = $container->getDefinition('foo');
101+
$locator = $container->getDefinition((string) $foo->getArgument(0));
102+
103+
$this->assertFalse($locator->isPublic());
104+
$this->assertSame(ServiceLocator::class, $locator->getClass());
105+
106+
$expected = array(
107+
'TestServiceSubscriber' => new ServiceClosureArgument(new TypedReference('TestServiceSubscriber', 'TestServiceSubscriber')),
108+
'stdClass' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
109+
'bar' => new ServiceClosureArgument(new TypedReference('bar', 'stdClass')),
110+
'baz' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
111+
);
112+
113+
$this->assertEquals($expected, $locator->getArgument(0));
114+
}
115+
}

src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,4 +650,23 @@ public function testServiceLocator()
650650

651651
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_locator.php', $dumper->dump());
652652
}
653+
654+
public function testServiceSubscriber()
655+
{
656+
$container = new ContainerBuilder();
657+
$container->register('foo_service', 'TestServiceSubscriber')
658+
->setAutowired(true)
659+
->addArgument(new Reference('service_container'))
660+
->addTag('kernel.service_subscriber', array(
661+
'key' => 'test',
662+
'service' => 'TestServiceSubscriber',
663+
))
664+
;
665+
$container->register('TestServiceSubscriber', 'TestServiceSubscriber');
666+
$container->compile();
667+
668+
$dumper = new PhpDumper($container);
669+
670+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_subscriber.php', $dumper->dump());
671+
}
653672
}

src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use Symfony\Component\DependencyInjection\Definition;
44
use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface as ProxyDumper;
5+
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
56

67
function sc_configure($instance)
78
{
@@ -107,3 +108,20 @@ public function __construct($lazyValues)
107108
$this->lazyValues = $lazyValues;
108109
}
109110
}
111+
112+
class TestServiceSubscriber implements ServiceSubscriberInterface
113+
{
114+
public function __construct($container)
115+
{
116+
}
117+
118+
public static function getSubscribedServices()
119+
{
120+
return array(
121+
__CLASS__,
122+
'?stdClass',
123+
'bar' => 'stdClass',
124+
'baz' => '?stdClass',
125+
);
126+
}
127+
}

0 commit comments

Comments
 (0)