Navigation Menu

Skip to content

Commit

Permalink
feature #21553 [DI] Replace container injection by explicit service l…
Browse files Browse the repository at this point in the history
…ocators (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
  • Loading branch information
fabpot committed Feb 13, 2017
2 parents e43bd57 + e7935c0 commit 320529e
Show file tree
Hide file tree
Showing 37 changed files with 474 additions and 9 deletions.
Expand Up @@ -16,6 +16,7 @@
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
Expand Down Expand Up @@ -239,7 +240,7 @@ private function createFirewalls($config, ContainerBuilder $container)

// load firewall map
$mapDef = $container->getDefinition('security.firewall.map');
$map = $authenticationProviders = array();
$map = $authenticationProviders = $contextRefs = array();
foreach ($firewalls as $name => $firewall) {
$configId = 'security.firewall.map.config.'.$name;

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

$contextRefs[$contextId] = new Reference($contextId);
$map[$contextId] = $matcher;
}
$mapDef->replaceArgument(0, new ServiceLocatorArgument($contextRefs));
$mapDef->replaceArgument(1, new IteratorArgument($map));

// add authentication providers to authentication manager
Expand Down
Expand Up @@ -105,8 +105,8 @@
</service>

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

<service id="security.firewall.context" class="Symfony\Bundle\SecurityBundle\Security\FirewallContext" abstract="true">
Expand Down
5 changes: 1 addition & 4 deletions src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php
Expand Up @@ -11,9 +11,9 @@

namespace Symfony\Bundle\SecurityBundle\Security;

use Psr\Container\ContainerInterface;
use Symfony\Component\Security\Http\FirewallMapInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DependencyInjection\ContainerInterface;

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

/**
* {@inheritdoc}
*/
public function getListeners(Request $request)
{
$context = $this->getFirewallContext($request);
Expand Down
@@ -0,0 +1,51 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\DependencyInjection\Argument;

use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

/**
* Represents a service locator able to lazy load a given range of services.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*
* @experimental in version 3.3
*/
class ServiceLocatorArgument implements ArgumentInterface
{
private $values;

/**
* @param Reference[] $values An array of references indexed by identifier
*/
public function __construct(array $values)
{
$this->setValues($values);
}

public function getValues()
{
return $this->values;
}

public function setValues(array $values)
{
foreach ($values as $v) {
if (!$v instanceof Reference) {
throw new InvalidArgumentException('Values of a ServiceLocatorArgument must be Reference objects.');
}
}

$this->values = $values;
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
3.3.0
-----

* [EXPERIMENTAL] added "service-locator" argument for lazy loading a set of identified values and services
* [EXPERIMENTAL] added prototype services for PSR4-based discovery and registration
* added `ContainerBuilder::getReflectionClass()` for retrieving and tracking reflection class info
* deprecated `ContainerBuilder::getClassResource()`, use `ContainerBuilder::getReflectionClass()` or `ContainerBuilder::addObjectResource()` instead
Expand Down
10 changes: 10 additions & 0 deletions src/Symfony/Component/DependencyInjection/ContainerBuilder.php
Expand Up @@ -14,6 +14,7 @@
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Compiler\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
Expand Down Expand Up @@ -1122,6 +1123,15 @@ public function resolveServices($value)
foreach ($value as $k => $v) {
$value[$k] = $this->resolveServices($v);
}
} elseif ($value instanceof ServiceLocatorArgument) {
$parameterBag = $this->getParameterBag();
$services = array();
foreach ($value->getValues() as $k => $v) {
$services[$k] = function () use ($v, $parameterBag) {
return $this->resolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($v)));
};
}
$value = new ServiceLocator($services);
} elseif ($value instanceof IteratorArgument) {
$parameterBag = $this->getParameterBag();
$value = new RewindableGenerator(function () use ($value, $parameterBag) {
Expand Down
10 changes: 10 additions & 0 deletions src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Variable;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand Down Expand Up @@ -897,6 +898,7 @@ private function startClass($class, $baseClass, $namespace)
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ServiceLocator;
$bagClass
/*{$this->docStar}
Expand Down Expand Up @@ -1536,6 +1538,14 @@ private function dumpValue($value, $interpolate = true)
}

return sprintf('array(%s)', implode(', ', $code));
} elseif ($value instanceof ServiceLocatorArgument) {
$code = "\n";
foreach ($value->getValues() as $k => $v) {
$code .= sprintf(" %s => function () { return %s; },\n", $this->dumpValue($k, $interpolate), $this->dumpValue($v, $interpolate));
}
$code .= ' ';

return sprintf('new ServiceLocator(array(%s))', $code);
} elseif ($value instanceof IteratorArgument) {
$countCode = array();
$countCode[] = 'function () {';
Expand Down
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\Reference;
Expand Down Expand Up @@ -291,6 +292,9 @@ private function convertParameters(array $parameters, $type, \DOMElement $parent
if (is_array($value)) {
$element->setAttribute('type', 'collection');
$this->convertParameters($value, $type, $element, 'key');
} elseif ($value instanceof ServiceLocatorArgument) {
$element->setAttribute('type', 'service-locator');
$this->convertParameters($value->getValues(), $type, $element);
} elseif ($value instanceof IteratorArgument) {
$element->setAttribute('type', 'iterator');
$this->convertParameters($value->getValues(), $type, $element, 'key');
Expand Down
Expand Up @@ -17,6 +17,7 @@
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Parameter;
Expand Down Expand Up @@ -258,6 +259,8 @@ private function dumpValue($value)
$tag = 'iterator';
} elseif ($value instanceof ClosureProxyArgument) {
$tag = 'closure_proxy';
} elseif ($value instanceof ServiceLocatorArgument) {
$tag = 'service_locator';
} else {
throw new RuntimeException(sprintf('Unspecified Yaml tag for type "%s".', get_class($value)));
}
Expand Down
Expand Up @@ -11,12 +11,12 @@

namespace Symfony\Component\DependencyInjection\Loader;

use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Reference;
Expand Down Expand Up @@ -498,6 +498,15 @@ private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true,
case 'iterator':
$arguments[$key] = new IteratorArgument($this->getArgumentsAsPhp($arg, $name, false));
break;
case 'service-locator':
$values = $this->getArgumentsAsPhp($arg, $name, false);
foreach ($values as $v) {
if (!$v instanceof Reference) {
throw new InvalidArgumentException('"service-locator" argument values must be services.');
}
}
$arguments[$key] = new ServiceLocatorArgument($values);
break;
case 'string':
$arguments[$key] = $arg->nodeValue;
break;
Expand Down
Expand Up @@ -14,13 +14,13 @@
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Parser as YamlParser;
use Symfony\Component\Yaml\Tag\TaggedValue;
Expand Down Expand Up @@ -616,6 +616,19 @@ private function resolveServices($value)

return new IteratorArgument(array_map(array($this, 'resolveServices'), $argument));
}
if ('service_locator' === $value->getTag()) {
if (!is_array($argument)) {
throw new InvalidArgumentException('"!service_locator" tag only accepts mappings.');
}

foreach ($argument as $v) {
if (!is_string($v) || 0 !== strpos($v[0], '@') || 0 === strpos($v[0], '@@')) {
throw new InvalidArgumentException('"!service_locator" tagged values must be {key: @service} mappings.');
}
}

return new ServiceLocatorArgument(array_map(array($this, 'resolveServices'), $argument));
}
if ('closure_proxy' === $value->getTag()) {
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], '@@')) {
throw new InvalidArgumentException('"!closure_proxy" tagged values must be arrays of [@service, method].');
Expand Down
Expand Up @@ -246,6 +246,7 @@
<xsd:enumeration value="string" />
<xsd:enumeration value="constant" />
<xsd:enumeration value="iterator" />
<xsd:enumeration value="service-locator" />
<xsd:enumeration value="closure-proxy" />
</xsd:restriction>
</xsd:simpleType>
Expand Down
71 changes: 71 additions & 0 deletions src/Symfony/Component/DependencyInjection/ServiceLocator.php
@@ -0,0 +1,71 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\DependencyInjection;

use Psr\Container\ContainerInterface as PsrContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;

/**
* @author Robin Chalas <robin.chalas@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*
* @experimental in version 3.3
*/
class ServiceLocator implements PsrContainerInterface
{
private $factories;
private $values = array();

/**
* @param callable[] $factories
*/
public function __construct(array $factories)
{
$this->factories = $factories;
}

/**
* {@inheritdoc}
*/
public function has($id)
{
return isset($this->factories[$id]);
}

/**
* {@inheritdoc}
*/
public function get($id)
{
if (!isset($this->factories[$id])) {
throw new ServiceNotFoundException($id, null, null, array_keys($this->factories));
}

if (true === $factory = $this->factories[$id]) {
throw new ServiceCircularReferenceException($id, array($id, $id));
}

if (false !== $factory) {
$this->factories[$id] = true;
$this->values[$id] = $factory();
$this->factories[$id] = false;
}

return $this->values[$id];
}

public function __invoke($id)
{
return isset($this->factories[$id]) ? $this->get($id) : null;
}
}
Expand Up @@ -21,6 +21,7 @@
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand All @@ -33,6 +34,7 @@
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
use Symfony\Component\ExpressionLanguage\Expression;
Expand Down Expand Up @@ -437,6 +439,24 @@ public function testCreateServiceWithIteratorArgument()
$this->assertEquals(1, $i);
}

public function testCreateServiceWithServiceLocatorArgument()
{
$builder = new ContainerBuilder();
$builder->register('bar', 'stdClass');
$builder
->register('lazy_context', 'LazyContext')
->setArguments(array(new ServiceLocatorArgument(array('bar' => new Reference('bar'), 'invalid' => new Reference('invalid', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)))))
;

$lazyContext = $builder->get('lazy_context');
$locator = $lazyContext->lazyValues;

$this->assertInstanceOf(ServiceLocator::class, $locator);
$this->assertInstanceOf('stdClass', $locator->get('bar'));
$this->assertNull($locator->get('invalid'));
$this->assertSame($locator->get('bar'), $locator('bar'), '->get() should be used when invoking ServiceLocator');
}

/**
* @expectedException \RuntimeException
*/
Expand Down

0 comments on commit 320529e

Please sign in to comment.