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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 2 additions & 8 deletions
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

Lines changed: 2 additions & 8 deletions
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

Lines changed: 13 additions & 1 deletion
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

Lines changed: 12 additions & 1 deletion
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

Lines changed: 2 additions & 1 deletion
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

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 <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+
}
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 <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)
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