Skip to content

Commit f99dfb0

Browse files
committed
feature #21708 [DI] Add and wire ServiceSubscriberInterface - aka explicit service locators (nicolas-grekas)
This PR was squashed before being merged into the 3.3-dev branch (closes #21708). Discussion ---------- [DI] Add and wire ServiceSubscriberInterface - aka explicit service locators | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | no test yet | Fixed tickets | #20658 | License | MIT | Doc PR | - This PR implements the second and missing part of #20658: it enables objects to declare their service dependencies in a similar way than we do for EventSubscribers: via a static method. Here is the interface and its current description: ```php namespace Symfony\Component\DependencyInjection; /** * A ServiceSubscriber exposes its dependencies via the static {@link getSubscribedServices} method. * * The getSubscribedServices method returns an array of service types required by such instances, * optionally keyed by the service names used internally. Service types that start with an interrogation * mark "?" are optional, while the other ones are mandatory service dependencies. * * The injected service locators SHOULD NOT allow access to any other services not specified by the method. * * It is expected that ServiceSubscriber instances consume PSR-11-based service locators internally. * This interface does not dictate any injection method for these service locators, although constructor * injection is recommended. * * @author Nicolas Grekas <p@tchwork.com> */ interface ServiceSubscriberInterface { /** * Returns an array of service types required by such instances, optionally keyed by the service names used internally. * * For mandatory dependencies: * * * array('logger' => 'Psr\Log\LoggerInterface') means the objects use the "logger" name * internally to fetch a service which must implement Psr\Log\LoggerInterface. * * array('Psr\Log\LoggerInterface') is a shortcut for * * array('Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface') * * otherwise: * * * array('logger' => '?Psr\Log\LoggerInterface') denotes an optional dependency * * array('?Psr\Log\LoggerInterface') is a shortcut for * * array('Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface') * * @return array The required service types, optionally keyed by service names */ public static function getSubscribedServices(); } ``` We could then have eg a controller-as-a-service implement this interface, and be auto or manually wired according to the return value of this method - using the "kernel.service_subscriber" tag to do so. eg: ```yaml services: App\Controller\FooController: arguments: [service_container] tags: - name: kernel.service_subscriber key: logger service: monolog.logger.foo_channel ``` The benefits are: - it keeps the lazy-behavior gained by service locators / container injection - it allows the referenced services to be made private from the pov of the main Symfony DIC - thus enables some compiler optimizations - it makes dependencies autowirable (while keeping manual wiring possible) - it does not add any strong coupling at the architecture level - and most importantly and contrary to regular container injection, *it makes dependencies explicit* - each classes declaring which services it consumes. Some might argue that: - it requires to be explicit - thus more verbose. Yet many others think it's a good thing - ie it's worth it. - some coupling happens at the dependency level, since you need to get the DI component to get the interface definition. This is something that the PHP-FIG could address at some point. Commits ------- c5e80a2 implement ServiceSubscriberInterface where applicable 9b7df39 [DI] Add and wire ServiceSubscriberInterface
2 parents fa36ce8 + c5e80a2 commit f99dfb0

File tree

17 files changed

+506
-23
lines changed

17 files changed

+506
-23
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class UnusedTagsPass implements CompilerPassInterface
2424
private $whitelist = array(
2525
'console.command',
2626
'container.service_locator',
27+
'container.service_subscriber',
2728
'config_cache.resource_checker',
2829
'data_collector',
2930
'form.type',

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

+2-8
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,8 @@
5353

5454
<service id="session_listener" class="Symfony\Component\HttpKernel\EventListener\SessionListener">
5555
<tag name="kernel.event_subscriber" />
56-
<argument type="service">
57-
<service class="Symfony\Component\DependencyInjection\ServiceLocator">
58-
<tag name="container.service_locator" />
59-
<argument type="collection">
60-
<argument key="session" type="service" id="session" on-invalid="ignore" />
61-
</argument>
62-
</service>
63-
</argument>
56+
<tag name="container.service_subscriber" id="session" />
57+
<argument type="service" id="container" />
6458
</service>
6559

6660
<service id="session.save_listener" class="Symfony\Component\HttpKernel\EventListener\SaveSessionListener">

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

+2-8
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,8 @@
2222

2323
<service id="test.session.listener" class="Symfony\Component\HttpKernel\EventListener\TestSessionListener">
2424
<tag name="kernel.event_subscriber" />
25-
<argument type="service">
26-
<service class="Symfony\Component\DependencyInjection\ServiceLocator">
27-
<tag name="container.service_locator" />
28-
<argument type="collection">
29-
<argument key="session" type="service" id="session" on-invalid="ignore" />
30-
</argument>
31-
</service>
32-
</argument>
25+
<tag name="container.service_subscriber" id="session" />
26+
<argument type="service" id="container" />
3327
</service>
3428
</services>
3529
</container>

src/Symfony/Bundle/FrameworkBundle/Routing/Router.php

+13-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
namespace Symfony\Bundle\FrameworkBundle\Routing;
1313

14+
use Symfony\Component\Config\Loader\LoaderInterface;
1415
use Symfony\Component\DependencyInjection\Config\ContainerParametersResource;
16+
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
1517
use Symfony\Component\Routing\Router as BaseRouter;
1618
use Symfony\Component\Routing\RequestContext;
1719
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -25,7 +27,7 @@
2527
*
2628
* @author Fabien Potencier <fabien@symfony.com>
2729
*/
28-
class Router extends BaseRouter implements WarmableInterface
30+
class Router extends BaseRouter implements WarmableInterface, ServiceSubscriberInterface
2931
{
3032
private $container;
3133
private $collectedParameters = array();
@@ -173,4 +175,14 @@ private function resolve($value)
173175

174176
return str_replace('%%', '%', $escapedValue);
175177
}
178+
179+
/**
180+
* {@inheritdoc}
181+
*/
182+
public static function getSubscribedServices()
183+
{
184+
return array(
185+
'routing.loader' => LoaderInterface::class,
186+
);
187+
}
176188
}

src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheCacheWarmer.php

+12-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Bundle\TwigBundle\CacheWarmer;
1313

1414
use Psr\Container\ContainerInterface;
15+
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
1516
use Symfony\Component\Finder\Finder;
1617
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
1718
use Symfony\Bundle\FrameworkBundle\CacheWarmer\TemplateFinderInterface;
@@ -25,7 +26,7 @@
2526
*
2627
* @author Fabien Potencier <fabien@symfony.com>
2728
*/
28-
class TemplateCacheCacheWarmer implements CacheWarmerInterface
29+
class TemplateCacheCacheWarmer implements CacheWarmerInterface, ServiceSubscriberInterface
2930
{
3031
protected $container;
3132
protected $finder;
@@ -92,6 +93,16 @@ public function isOptional()
9293
return true;
9394
}
9495

96+
/**
97+
* {@inheritdoc}
98+
*/
99+
public static function getSubscribedServices()
100+
{
101+
return array(
102+
'twig' => \Twig_Environment::class,
103+
);
104+
}
105+
95106
/**
96107
* Find templates in the given directory.
97108
*

src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828

2929
<service id="twig.cache_warmer" class="Symfony\Bundle\TwigBundle\CacheWarmer\TemplateCacheCacheWarmer" public="false">
3030
<tag name="kernel.cache_warmer" />
31-
<argument type="service" id="service_container" />
31+
<tag name="container.service_subscriber" id="twig" />
32+
<argument type="service" id="container" />
3233
<argument type="service" id="templating.finder" on-invalid="ignore" />
3334
<argument type="collection" /> <!-- Twig paths -->
3435
</service>

src/Symfony/Component/DependencyInjection/CHANGELOG.md

+1
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

+1
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(),
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 <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\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 <p@tchwork.com>
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+
}
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 <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+
/**
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 <p@tchwork.com>
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+
}

0 commit comments

Comments
 (0)