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

Skip to content

Commit ffc93ea

Browse files
[DI] Add and wire ServiceSubscriberInterface
1 parent c12727d commit ffc93ea

File tree

9 files changed

+430
-0
lines changed

9 files changed

+430
-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
@@ -23,6 +23,7 @@ class UnusedTagsPass implements CompilerPassInterface
2323
{
2424
private $whitelist = array(
2525
'console.command',
26+
'container.service_subscriber',
2627
'config_cache.resource_checker',
2728
'data_collector',
2829
'form.type',

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
3.3.0
55
-----
66

7+
* added "ServiceSubscriberInterface" - to allow for per-class explicit service-locator definitions
78
* added "container.service_locator" tag for defining service-locator services
89
* added anonymous services support in YAML configuration files using the `!service` tag.
910
* added "TypedReference" and "ServiceClosureArgument" for creating service-locator services

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public function __construct()
5555
new ResolveFactoryClassPass(),
5656
new FactoryReturnTypePass($resolveClassPass),
5757
new CheckDefinitionValidityPass(),
58+
new RegisterServiceSubscribersPass(),
5859
new ResolveNamedArgumentsPass(),
5960
new AutowirePass(),
6061
new ResolveReferencesToAliasesPass(),
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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+
class RegisterServiceSubscribersPass extends AbstractRecursivePass
29+
{
30+
private $serviceLocator;
31+
32+
protected function processValue($value, $isRoot = false)
33+
{
34+
if ($value instanceof Reference && $this->serviceLocator && 'container' === (string) $value) {
35+
return new Reference($this->serviceLocator);
36+
}
37+
38+
if (!$value instanceof Definition || $value->isAbstract() || $value->isSynthetic() || !$value->hasTag('container.service_subscriber')) {
39+
return parent::processValue($value, $isRoot);
40+
}
41+
42+
$serviceMap = array();
43+
44+
foreach ($value->getTag('container.service_subscriber') as $attributes) {
45+
if (!$attributes) {
46+
continue;
47+
}
48+
ksort($attributes);
49+
if (array() !== array_diff(array_keys($attributes), array('id', 'key'))) {
50+
throw new InvalidArgumentException(sprintf('The "container.service_subscriber" tag accepts only the "key" and "id" attributes, "%s" given for service "%s".', implode('", "', array_keys($attributes)), $this->currentId));
51+
}
52+
if (!array_key_exists('id', $attributes)) {
53+
throw new InvalidArgumentException(sprintf('Missing "id" attribute on "container.service_subscriber" tag with key="%s" for service "%s".', $attributes['key'], $this->currentId));
54+
}
55+
if (!array_key_exists('key', $attributes)) {
56+
$attributes['key'] = $attributes['id'];
57+
}
58+
if (isset($serviceMap[$attributes['key']])) {
59+
continue;
60+
}
61+
$serviceMap[$attributes['key']] = new Reference($attributes['id']);
62+
}
63+
$class = $value->getClass();
64+
65+
if (!is_subclass_of($class, ServiceSubscriberInterface::class)) {
66+
if (!class_exists($class, false)) {
67+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $this->currentId));
68+
}
69+
70+
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $this->currentId, ServiceSubscriberInterface::class));
71+
}
72+
$this->container->addObjectResource($class);
73+
$subscriberMap = array();
74+
75+
foreach ($class::getSubscribedServices() as $key => $type) {
76+
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)) {
77+
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)));
78+
}
79+
if ($optionalBehavior = '?' === $type[0]) {
80+
$type = substr($type, 1);
81+
$optionalBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
82+
}
83+
if (is_int($key)) {
84+
$key = $type;
85+
}
86+
if (!isset($serviceMap[$key])) {
87+
$serviceMap[$key] = new Reference($type);
88+
}
89+
90+
$subscriberMap[$key] = new ServiceClosureArgument(new TypedReference((string) $serviceMap[$key], $type, $optionalBehavior ?: ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE));
91+
unset($serviceMap[$key]);
92+
}
93+
94+
if ($serviceMap = array_keys($serviceMap)) {
95+
$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));
96+
}
97+
98+
$serviceLocator = $this->serviceLocator;
99+
$this->serviceLocator = 'container.'.$this->currentId.'.'.md5(serialize($value));
100+
$this->container->register($this->serviceLocator, ServiceLocator::class)
101+
->addArgument($subscriberMap)
102+
->setPublic(false)
103+
->setAutowired($value->isAutowired())
104+
->addTag('container.service_locator');
105+
106+
try {
107+
return parent::processValue($value);
108+
} finally {
109+
$this->serviceLocator = $serviceLocator;
110+
}
111+
}
112+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
interface ServiceSubscriberInterface
30+
{
31+
/**
32+
* Returns an array of service types required by such instances, optionally keyed by the service names used internally.
33+
*
34+
* For mandatory dependencies:
35+
*
36+
* * array('logger' => 'Psr\Log\LoggerInterface') means the objects use the "logger" name
37+
* internally to fetch a service which must implement Psr\Log\LoggerInterface.
38+
* * array('Psr\Log\LoggerInterface') is a shortcut for
39+
* * array('Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface')
40+
*
41+
* otherwise:
42+
*
43+
* * array('logger' => '?Psr\Log\LoggerInterface') denotes an optional dependency
44+
* * array('?Psr\Log\LoggerInterface') is a shortcut for
45+
* * array('Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface')
46+
*
47+
* @return array The required service types, optionally keyed by service names
48+
*/
49+
public static function getSubscribedServices();
50+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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('container.service_subscriber')
37+
;
38+
39+
$pass = new RegisterServiceSubscribersPass();
40+
$pass->process($container);
41+
}
42+
43+
/**
44+
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
45+
* @expectedExceptionMessage The "container.service_subscriber" tag accepts only the "key" and "id" attributes, "bar" given for service "foo".
46+
*/
47+
public function testInvalidAttributes()
48+
{
49+
$container = new ContainerBuilder();
50+
51+
$container->register('foo', 'TestServiceSubscriber')
52+
->addTag('container.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('container'))
65+
->addTag('container.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->isAutowired());
75+
$this->assertFalse($locator->isPublic());
76+
$this->assertSame(ServiceLocator::class, $locator->getClass());
77+
78+
$expected = array(
79+
'TestServiceSubscriber' => new ServiceClosureArgument(new TypedReference('TestServiceSubscriber', 'TestServiceSubscriber')),
80+
'stdClass' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
81+
'bar' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass')),
82+
'baz' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
83+
);
84+
85+
$this->assertEquals($expected, $locator->getArgument(0));
86+
}
87+
88+
public function testWithAttributes()
89+
{
90+
$container = new ContainerBuilder();
91+
92+
$container->register('foo', 'TestServiceSubscriber')
93+
->setAutowired(true)
94+
->addArgument(new Reference('container'))
95+
->addTag('container.service_subscriber', array('key' => 'bar', 'id' => 'bar'))
96+
->addTag('container.service_subscriber', array('key' => 'bar', 'id' => 'baz')) // should be ignored: the first wins
97+
;
98+
99+
$pass = new RegisterServiceSubscribersPass();
100+
$pass->process($container);
101+
102+
$foo = $container->getDefinition('foo');
103+
$locator = $container->getDefinition((string) $foo->getArgument(0));
104+
105+
$this->assertTrue($locator->isAutowired());
106+
$this->assertFalse($locator->isPublic());
107+
$this->assertSame(ServiceLocator::class, $locator->getClass());
108+
109+
$expected = array(
110+
'TestServiceSubscriber' => new ServiceClosureArgument(new TypedReference('TestServiceSubscriber', 'TestServiceSubscriber')),
111+
'stdClass' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
112+
'bar' => new ServiceClosureArgument(new TypedReference('bar', 'stdClass')),
113+
'baz' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
114+
);
115+
116+
$this->assertEquals($expected, $locator->getArgument(0));
117+
}
118+
}

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

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

655655
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_locator.php', $dumper->dump());
656656
}
657+
658+
public function testServiceSubscriber()
659+
{
660+
$container = new ContainerBuilder();
661+
$container->register('foo_service', 'TestServiceSubscriber')
662+
->setAutowired(true)
663+
->addArgument(new Reference('container'))
664+
->addTag('container.service_subscriber', array(
665+
'key' => 'test',
666+
'id' => 'TestServiceSubscriber',
667+
))
668+
;
669+
$container->register('TestServiceSubscriber', 'TestServiceSubscriber');
670+
$container->compile();
671+
672+
$dumper = new PhpDumper($container);
673+
674+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_subscriber.php', $dumper->dump());
675+
}
657676
}

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)