Skip to content

Commit 320529e

Browse files
committed
feature #21553 [DI] Replace container injection by explicit service locators (chalasr)
This PR was merged into the 3.3-dev branch. Discussion ---------- [DI] Replace container injection by explicit service locators | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #20658 | License | MIT | Doc PR | symfony/symfony-docs#7458 This adds a new `ServiceLocatorArgument` (`!service_locator`) argument type which takes a list of services, meant to be used as a concrete service locator in order to avoid the remaining needs for injecting the container when it's only a matter of dependency lazy-loading. Config: ```yaml App\FooBar: [!service_locator { key1: '@Service1', key2: '@service2' }] ``` ```xml <service class="App\FooBar" public="false"> <argument type="service-locator"> <argument type="service" key="key1" id="service1"/> <argument type="service" key="key2" id="service2"/> </argument> </service> ``` ```php new ServiceLocatorArgument(array('key1' => new Reference('service1'), 'key2' => new Reference('service2')); ``` Usage: ```php $locator->has('key1') // true $locator->has('service1') // false, the defined key must be used $locator->get('key1'); // service1 instance $locator->get('service1'); // exception $locator->has('not-specified') // false $locator->get('not-specified'); // exception ``` We have some concrete use cases in the core where this would be useful (see e.g. SecurityBundle's FirewallMap), same in userland/3rd party code (see related RFC). Commits ------- e7935c0 [DI] Replace container injection by explicit service locators
2 parents e43bd57 + e7935c0 commit 320529e

37 files changed

+474
-9
lines changed

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
1717
use Symfony\Component\DependencyInjection\Alias;
1818
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
19+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
1920
use Symfony\Component\DependencyInjection\ChildDefinition;
2021
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
2122
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
@@ -239,7 +240,7 @@ private function createFirewalls($config, ContainerBuilder $container)
239240

240241
// load firewall map
241242
$mapDef = $container->getDefinition('security.firewall.map');
242-
$map = $authenticationProviders = array();
243+
$map = $authenticationProviders = $contextRefs = array();
243244
foreach ($firewalls as $name => $firewall) {
244245
$configId = 'security.firewall.map.config.'.$name;
245246

@@ -253,8 +254,10 @@ private function createFirewalls($config, ContainerBuilder $container)
253254
->replaceArgument(2, new Reference($configId))
254255
;
255256

257+
$contextRefs[$contextId] = new Reference($contextId);
256258
$map[$contextId] = $matcher;
257259
}
260+
$mapDef->replaceArgument(0, new ServiceLocatorArgument($contextRefs));
258261
$mapDef->replaceArgument(1, new IteratorArgument($map));
259262

260263
// add authentication providers to authentication manager

src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@
105105
</service>
106106

107107
<service id="security.firewall.map" class="Symfony\Bundle\SecurityBundle\Security\FirewallMap" public="false">
108-
<argument type="service" id="service_container" />
109-
<argument />
108+
<argument /> <!-- Firewall context locator -->
109+
<argument /> <!-- Request matchers -->
110110
</service>
111111

112112
<service id="security.firewall.context" class="Symfony\Bundle\SecurityBundle\Security\FirewallContext" abstract="true">

src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php

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

1212
namespace Symfony\Bundle\SecurityBundle\Security;
1313

14+
use Psr\Container\ContainerInterface;
1415
use Symfony\Component\Security\Http\FirewallMapInterface;
1516
use Symfony\Component\HttpFoundation\Request;
16-
use Symfony\Component\DependencyInjection\ContainerInterface;
1717

1818
/**
1919
* This is a lazy-loading firewall map implementation.
@@ -116,9 +116,6 @@ public function __construct(ContainerInterface $container, $map)
116116
$this->contexts = new \SplObjectStorage();
117117
}
118118

119-
/**
120-
* {@inheritdoc}
121-
*/
122119
public function getListeners(Request $request)
123120
{
124121
$context = $this->getFirewallContext($request);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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\Argument;
13+
14+
use Symfony\Component\DependencyInjection\Reference;
15+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
16+
17+
/**
18+
* Represents a service locator able to lazy load a given range of services.
19+
*
20+
* @author Robin Chalas <robin.chalas@gmail.com>
21+
*
22+
* @experimental in version 3.3
23+
*/
24+
class ServiceLocatorArgument implements ArgumentInterface
25+
{
26+
private $values;
27+
28+
/**
29+
* @param Reference[] $values An array of references indexed by identifier
30+
*/
31+
public function __construct(array $values)
32+
{
33+
$this->setValues($values);
34+
}
35+
36+
public function getValues()
37+
{
38+
return $this->values;
39+
}
40+
41+
public function setValues(array $values)
42+
{
43+
foreach ($values as $v) {
44+
if (!$v instanceof Reference) {
45+
throw new InvalidArgumentException('Values of a ServiceLocatorArgument must be Reference objects.');
46+
}
47+
}
48+
49+
$this->values = $values;
50+
}
51+
}

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+
* [EXPERIMENTAL] added "service-locator" argument for lazy loading a set of identified values and services
78
* [EXPERIMENTAL] added prototype services for PSR4-based discovery and registration
89
* added `ContainerBuilder::getReflectionClass()` for retrieving and tracking reflection class info
910
* deprecated `ContainerBuilder::getClassResource()`, use `ContainerBuilder::getReflectionClass()` or `ContainerBuilder::addObjectResource()` instead

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1515
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
1616
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
17+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
1718
use Symfony\Component\DependencyInjection\Compiler\Compiler;
1819
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1920
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
@@ -1122,6 +1123,15 @@ public function resolveServices($value)
11221123
foreach ($value as $k => $v) {
11231124
$value[$k] = $this->resolveServices($v);
11241125
}
1126+
} elseif ($value instanceof ServiceLocatorArgument) {
1127+
$parameterBag = $this->getParameterBag();
1128+
$services = array();
1129+
foreach ($value->getValues() as $k => $v) {
1130+
$services[$k] = function () use ($v, $parameterBag) {
1131+
return $this->resolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($v)));
1132+
};
1133+
}
1134+
$value = new ServiceLocator($services);
11251135
} elseif ($value instanceof IteratorArgument) {
11261136
$parameterBag = $this->getParameterBag();
11271137
$value = new RewindableGenerator(function () use ($value, $parameterBag) {

src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php

+10
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1515
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
16+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
1617
use Symfony\Component\DependencyInjection\Variable;
1718
use Symfony\Component\DependencyInjection\Definition;
1819
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -897,6 +898,7 @@ private function startClass($class, $baseClass, $namespace)
897898
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
898899
use Symfony\Component\DependencyInjection\Exception\LogicException;
899900
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
901+
use Symfony\Component\DependencyInjection\ServiceLocator;
900902
$bagClass
901903
902904
/*{$this->docStar}
@@ -1536,6 +1538,14 @@ private function dumpValue($value, $interpolate = true)
15361538
}
15371539

15381540
return sprintf('array(%s)', implode(', ', $code));
1541+
} elseif ($value instanceof ServiceLocatorArgument) {
1542+
$code = "\n";
1543+
foreach ($value->getValues() as $k => $v) {
1544+
$code .= sprintf(" %s => function () { return %s; },\n", $this->dumpValue($k, $interpolate), $this->dumpValue($v, $interpolate));
1545+
}
1546+
$code .= ' ';
1547+
1548+
return sprintf('new ServiceLocator(array(%s))', $code);
15391549
} elseif ($value instanceof IteratorArgument) {
15401550
$countCode = array();
15411551
$countCode[] = 'function () {';

src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1515
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
16+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
1617
use Symfony\Component\DependencyInjection\ContainerInterface;
1718
use Symfony\Component\DependencyInjection\Parameter;
1819
use Symfony\Component\DependencyInjection\Reference;
@@ -291,6 +292,9 @@ private function convertParameters(array $parameters, $type, \DOMElement $parent
291292
if (is_array($value)) {
292293
$element->setAttribute('type', 'collection');
293294
$this->convertParameters($value, $type, $element, 'key');
295+
} elseif ($value instanceof ServiceLocatorArgument) {
296+
$element->setAttribute('type', 'service-locator');
297+
$this->convertParameters($value->getValues(), $type, $element);
294298
} elseif ($value instanceof IteratorArgument) {
295299
$element->setAttribute('type', 'iterator');
296300
$this->convertParameters($value->getValues(), $type, $element, 'key');

src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
1818
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1919
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
20+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
2021
use Symfony\Component\DependencyInjection\ContainerInterface;
2122
use Symfony\Component\DependencyInjection\Definition;
2223
use Symfony\Component\DependencyInjection\Parameter;
@@ -258,6 +259,8 @@ private function dumpValue($value)
258259
$tag = 'iterator';
259260
} elseif ($value instanceof ClosureProxyArgument) {
260261
$tag = 'closure_proxy';
262+
} elseif ($value instanceof ServiceLocatorArgument) {
263+
$tag = 'service_locator';
261264
} else {
262265
throw new RuntimeException(sprintf('Unspecified Yaml tag for type "%s".', get_class($value)));
263266
}

src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Loader;
1313

14-
use Symfony\Component\Config\Resource\FileResource;
1514
use Symfony\Component\Config\Util\XmlUtils;
1615
use Symfony\Component\DependencyInjection\ContainerInterface;
1716
use Symfony\Component\DependencyInjection\Alias;
1817
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1918
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
19+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
2020
use Symfony\Component\DependencyInjection\Definition;
2121
use Symfony\Component\DependencyInjection\ChildDefinition;
2222
use Symfony\Component\DependencyInjection\Reference;
@@ -498,6 +498,15 @@ private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true,
498498
case 'iterator':
499499
$arguments[$key] = new IteratorArgument($this->getArgumentsAsPhp($arg, $name, false));
500500
break;
501+
case 'service-locator':
502+
$values = $this->getArgumentsAsPhp($arg, $name, false);
503+
foreach ($values as $v) {
504+
if (!$v instanceof Reference) {
505+
throw new InvalidArgumentException('"service-locator" argument values must be services.');
506+
}
507+
}
508+
$arguments[$key] = new ServiceLocatorArgument($values);
509+
break;
501510
case 'string':
502511
$arguments[$key] = $arg->nodeValue;
503512
break;

src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php

+14-1
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
use Symfony\Component\DependencyInjection\Alias;
1515
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1616
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
17+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
1718
use Symfony\Component\DependencyInjection\ChildDefinition;
1819
use Symfony\Component\DependencyInjection\ContainerInterface;
1920
use Symfony\Component\DependencyInjection\Definition;
2021
use Symfony\Component\DependencyInjection\Reference;
2122
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
2223
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
23-
use Symfony\Component\Config\Resource\FileResource;
2424
use Symfony\Component\Yaml\Exception\ParseException;
2525
use Symfony\Component\Yaml\Parser as YamlParser;
2626
use Symfony\Component\Yaml\Tag\TaggedValue;
@@ -616,6 +616,19 @@ private function resolveServices($value)
616616

617617
return new IteratorArgument(array_map(array($this, 'resolveServices'), $argument));
618618
}
619+
if ('service_locator' === $value->getTag()) {
620+
if (!is_array($argument)) {
621+
throw new InvalidArgumentException('"!service_locator" tag only accepts mappings.');
622+
}
623+
624+
foreach ($argument as $v) {
625+
if (!is_string($v) || 0 !== strpos($v[0], '@') || 0 === strpos($v[0], '@@')) {
626+
throw new InvalidArgumentException('"!service_locator" tagged values must be {key: @service} mappings.');
627+
}
628+
}
629+
630+
return new ServiceLocatorArgument(array_map(array($this, 'resolveServices'), $argument));
631+
}
619632
if ('closure_proxy' === $value->getTag()) {
620633
if (!is_array($argument) || array(0, 1) !== array_keys($argument) || !is_string($argument[0]) || !is_string($argument[1]) || 0 !== strpos($argument[0], '@') || 0 === strpos($argument[0], '@@')) {
621634
throw new InvalidArgumentException('"!closure_proxy" tagged values must be arrays of [@service, method].');

src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd

+1
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@
246246
<xsd:enumeration value="string" />
247247
<xsd:enumeration value="constant" />
248248
<xsd:enumeration value="iterator" />
249+
<xsd:enumeration value="service-locator" />
249250
<xsd:enumeration value="closure-proxy" />
250251
</xsd:restriction>
251252
</xsd:simpleType>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
use Psr\Container\ContainerInterface as PsrContainerInterface;
15+
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
16+
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
17+
18+
/**
19+
* @author Robin Chalas <robin.chalas@gmail.com>
20+
* @author Nicolas Grekas <p@tchwork.com>
21+
*
22+
* @experimental in version 3.3
23+
*/
24+
class ServiceLocator implements PsrContainerInterface
25+
{
26+
private $factories;
27+
private $values = array();
28+
29+
/**
30+
* @param callable[] $factories
31+
*/
32+
public function __construct(array $factories)
33+
{
34+
$this->factories = $factories;
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function has($id)
41+
{
42+
return isset($this->factories[$id]);
43+
}
44+
45+
/**
46+
* {@inheritdoc}
47+
*/
48+
public function get($id)
49+
{
50+
if (!isset($this->factories[$id])) {
51+
throw new ServiceNotFoundException($id, null, null, array_keys($this->factories));
52+
}
53+
54+
if (true === $factory = $this->factories[$id]) {
55+
throw new ServiceCircularReferenceException($id, array($id, $id));
56+
}
57+
58+
if (false !== $factory) {
59+
$this->factories[$id] = true;
60+
$this->values[$id] = $factory();
61+
$this->factories[$id] = false;
62+
}
63+
64+
return $this->values[$id];
65+
}
66+
67+
public function __invoke($id)
68+
{
69+
return isset($this->factories[$id]) ? $this->get($id) : null;
70+
}
71+
}

src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php

+20
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
2222
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
2323
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
24+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
2425
use Symfony\Component\DependencyInjection\ChildDefinition;
2526
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
2627
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -33,6 +34,7 @@
3334
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
3435
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
3536
use Symfony\Component\Config\Resource\FileResource;
37+
use Symfony\Component\DependencyInjection\ServiceLocator;
3638
use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition;
3739
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
3840
use Symfony\Component\ExpressionLanguage\Expression;
@@ -437,6 +439,24 @@ public function testCreateServiceWithIteratorArgument()
437439
$this->assertEquals(1, $i);
438440
}
439441

442+
public function testCreateServiceWithServiceLocatorArgument()
443+
{
444+
$builder = new ContainerBuilder();
445+
$builder->register('bar', 'stdClass');
446+
$builder
447+
->register('lazy_context', 'LazyContext')
448+
->setArguments(array(new ServiceLocatorArgument(array('bar' => new Reference('bar'), 'invalid' => new Reference('invalid', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)))))
449+
;
450+
451+
$lazyContext = $builder->get('lazy_context');
452+
$locator = $lazyContext->lazyValues;
453+
454+
$this->assertInstanceOf(ServiceLocator::class, $locator);
455+
$this->assertInstanceOf('stdClass', $locator->get('bar'));
456+
$this->assertNull($locator->get('invalid'));
457+
$this->assertSame($locator->get('bar'), $locator('bar'), '->get() should be used when invoking ServiceLocator');
458+
}
459+
440460
/**
441461
* @expectedException \RuntimeException
442462
*/

0 commit comments

Comments
 (0)