Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitf99dfb0

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:```phpnamespace 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:```yamlservices: 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 applicable9b7df39 [DI] Add and wire ServiceSubscriberInterface
2 parentsfa36ce8 +c5e80a2 commitf99dfb0

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
<serviceid="session_listener"class="Symfony\Component\HttpKernel\EventListener\SessionListener">
5555
<tagname="kernel.event_subscriber" />
56-
<argumenttype="service">
57-
<serviceclass="Symfony\Component\DependencyInjection\ServiceLocator">
58-
<tagname="container.service_locator" />
59-
<argumenttype="collection">
60-
<argumentkey="session"type="service"id="session"on-invalid="ignore" />
61-
</argument>
62-
</service>
63-
</argument>
56+
<tagname="container.service_subscriber"id="session" />
57+
<argumenttype="service"id="container" />
6458
</service>
6559

6660
<serviceid="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
<serviceid="test.session.listener"class="Symfony\Component\HttpKernel\EventListener\TestSessionListener">
2424
<tagname="kernel.event_subscriber" />
25-
<argumenttype="service">
26-
<serviceclass="Symfony\Component\DependencyInjection\ServiceLocator">
27-
<tagname="container.service_locator" />
28-
<argumenttype="collection">
29-
<argumentkey="session"type="service"id="session"on-invalid="ignore" />
30-
</argument>
31-
</service>
32-
</argument>
25+
<tagname="container.service_subscriber"id="session" />
26+
<argumenttype="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
namespaceSymfony\Bundle\FrameworkBundle\Routing;
1313

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

174176
returnstr_replace('%%','%',$escapedValue);
175177
}
178+
179+
/**
180+
* {@inheritdoc}
181+
*/
182+
publicstaticfunctiongetSubscribedServices()
183+
{
184+
returnarray(
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
namespaceSymfony\Bundle\TwigBundle\CacheWarmer;
1313

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

96+
/**
97+
* {@inheritdoc}
98+
*/
99+
publicstaticfunctiongetSubscribedServices()
100+
{
101+
returnarray(
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
<serviceid="twig.cache_warmer"class="Symfony\Bundle\TwigBundle\CacheWarmer\TemplateCacheCacheWarmer"public="false">
3030
<tagname="kernel.cache_warmer" />
31-
<argumenttype="service"id="service_container" />
31+
<tagname="container.service_subscriber"id="twig" />
32+
<argumenttype="service"id="container" />
3233
<argumenttype="service"id="templating.finder"on-invalid="ignore" />
3334
<argumenttype="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
newResolveFactoryClassPass(),
5656
newFactoryReturnTypePass($resolveClassPass),
5757
newCheckDefinitionValidityPass(),
58+
newRegisterServiceSubscribersPass(),
5859
newResolveNamedArgumentsPass(),
5960
newAutowirePass(),
6061
newResolveReferencesToAliasesPass(),
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+
namespaceSymfony\Component\DependencyInjection\Compiler;
13+
14+
useSymfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
15+
useSymfony\Component\DependencyInjection\ContainerInterface;
16+
useSymfony\Component\DependencyInjection\Definition;
17+
useSymfony\Component\DependencyInjection\Exception\InvalidArgumentException;
18+
useSymfony\Component\DependencyInjection\Reference;
19+
useSymfony\Component\DependencyInjection\ServiceSubscriberInterface;
20+
useSymfony\Component\DependencyInjection\ServiceLocator;
21+
useSymfony\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 RegisterServiceSubscribersPassextends AbstractRecursivePass
29+
{
30+
private$serviceLocator;
31+
32+
protectedfunctionprocessValue($value,$isRoot =false)
33+
{
34+
if ($valueinstanceof Reference &&$this->serviceLocator &&'container' === (string)$value) {
35+
returnnewReference($this->serviceLocator);
36+
}
37+
38+
if (!$valueinstanceof Definition ||$value->isAbstract() ||$value->isSynthetic() || !$value->hasTag('container.service_subscriber')) {
39+
returnparent::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+
thrownewInvalidArgumentException(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+
thrownewInvalidArgumentException(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']] =newReference($attributes['id']);
62+
}
63+
$class =$value->getClass();
64+
65+
if (!is_subclass_of($class, ServiceSubscriberInterface::class)) {
66+
if (!class_exists($class,false)) {
67+
thrownewInvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.',$class,$this->currentId));
68+
}
69+
70+
thrownewInvalidArgumentException(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+
thrownewInvalidArgumentException(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] =newReference($type);
88+
}
89+
90+
$subscriberMap[$key] =newServiceClosureArgument(newTypedReference((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+
returnparent::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+
namespaceSymfony\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+
publicstaticfunctiongetSubscribedServices();
50+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp