feature #27783 [DI] Add ServiceLocatorArgument to generate array-based locators optimized for OPcache shared memory (nicolas-grekas)

This PR was merged into the 4.2-dev branch.

Discussion
----------

[DI] Add ServiceLocatorArgument to generate array-based locators optimized for OPcache shared memory

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | -
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

Right now, to generate service locators, we use collections of closures described using `ServiceClosureArgument`. This works well, but it doesn't scale well when the number of services grows, because we have to load as many closures as there are services, even if we never call them.

This PR introduces `ServiceLocatorArgument`, which describes the same thing, but allows dumping optimized locators: instead of a collection of closures, this generates a static array that OPcache can put in shared memory (see fixtures for example.)

Once this PR is merged, we'll be able to update `ServiceLocatorPass::register()` to leverage it and generate these optimized locators everywhere. One particular I have in mind in the locator used by `ServiceArgumentResolver`, which can grow fast (it has as many entries as there are actions.)

Commits
-------

6c8e9576a3 [DI] Add ServiceLocatorArgument to generate array-based locators optimized for OPcache shared memory
This commit is contained in:
Nicolas Grekas 2018-07-07 17:01:23 +02:00
commit 6d3f63d6f3
31 changed files with 525 additions and 147 deletions

View File

@ -17,6 +17,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
@ -334,6 +335,8 @@ class TextDescriptor extends Descriptor
$argumentsInformation[] = sprintf('Service(%s)', (string) $argument);
} elseif ($argument instanceof IteratorArgument) {
$argumentsInformation[] = sprintf('Iterator (%d element(s))', count($argument->getValues()));
} elseif ($argument instanceof ServiceLocatorArgument) {
$argumentsInformation[] = sprintf('Service locator (%d element(s))', count($argument->getValues()));
} elseif ($argument instanceof Definition) {
$argumentsInformation[] = 'Inlined Service';
} else {

View File

@ -14,6 +14,7 @@ namespace Symfony\Bundle\FrameworkBundle\Console\Descriptor;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
@ -387,8 +388,8 @@ class XmlDescriptor extends Descriptor
if ($argument instanceof Reference) {
$argumentXML->setAttribute('type', 'service');
$argumentXML->setAttribute('id', (string) $argument);
} elseif ($argument instanceof IteratorArgument) {
$argumentXML->setAttribute('type', 'iterator');
} elseif ($argument instanceof IteratorArgument || $argument instanceof ServiceLocatorArgument) {
$argumentXML->setAttribute('type', $argument instanceof IteratorArgument ? 'iterator' : 'service_locator');
foreach ($this->getArgumentNodes($argument->getValues(), $dom) as $childArgumentXML) {
$argumentXML->appendChild($childArgumentXML);

View File

@ -11,8 +11,8 @@
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
@ -32,7 +32,7 @@ class TestServiceContainerWeakRefPass implements CompilerPassInterface
foreach ($definitions as $id => $definition) {
if ($id && '.' !== $id[0] && (!$definition->isPublic() || $definition->isPrivate()) && !$definition->getErrors() && !$definition->isAbstract()) {
$privateServices[$id] = new ServiceClosureArgument(new Reference($id, ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE));
$privateServices[$id] = new Reference($id, ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE);
}
}
@ -44,13 +44,15 @@ class TestServiceContainerWeakRefPass implements CompilerPassInterface
$alias = $aliases[$target];
}
if (isset($definitions[$target]) && !$definitions[$target]->getErrors() && !$definitions[$target]->isAbstract()) {
$privateServices[$id] = new ServiceClosureArgument(new Reference($target, ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE));
$privateServices[$id] = new Reference($target, ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE);
}
}
}
if ($privateServices) {
$definitions['test.private_services_locator']->replaceArgument(0, $privateServices);
$id = (string) ServiceLocatorTagPass::register($container, $privateServices);
$container->setDefinition('test.private_services_locator', $container->getDefinition($id))->setPublic(true);
$container->removeDefinition($id);
}
}
}

View File

@ -58,14 +58,9 @@
<service id="session_listener" class="Symfony\Component\HttpKernel\EventListener\SessionListener">
<tag name="kernel.event_subscriber" />
<argument type="service">
<service class="Symfony\Component\DependencyInjection\ServiceLocator">
<tag name="container.service_locator" />
<argument type="collection">
<argument key="session" type="service" id="session" on-invalid="ignore" />
<argument key="initialized_session" type="service" id="session" on-invalid="ignore_uninitialized" />
</argument>
</service>
<argument type="service_locator">
<argument key="session" type="service" id="session" on-invalid="ignore" />
<argument key="initialized_session" type="service" id="session" on-invalid="ignore_uninitialized" />
</argument>
</service>

View File

@ -24,13 +24,8 @@
<service id="test.session.listener" class="Symfony\Component\HttpKernel\EventListener\TestSessionListener">
<tag name="kernel.event_subscriber" />
<argument type="service">
<service class="Symfony\Component\DependencyInjection\ServiceLocator">
<tag name="container.service_locator" />
<argument type="collection">
<argument key="session" type="service" id="session" on-invalid="ignore" />
</argument>
</service>
<argument type="service_locator">
<argument key="session" type="service" id="session" on-invalid="ignore" />
</argument>
</service>

View File

@ -19,7 +19,7 @@
"php": "^7.1.3",
"ext-xml": "*",
"symfony/cache": "~3.4|~4.0",
"symfony/dependency-injection": "^4.1.1",
"symfony/dependency-injection": "^4.2",
"symfony/config": "~4.2",
"symfony/event-dispatcher": "^4.1",
"symfony/http-foundation": "^4.1",

View File

@ -27,14 +27,9 @@
<service id="Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface" alias="security.token_storage" />
<service id="security.helper" class="Symfony\Component\Security\Core\Security">
<argument type="service">
<service class="Symfony\Component\DependencyInjection\ServiceLocator">
<tag name="container.service_locator" />
<argument type="collection">
<argument key="security.token_storage" type="service" id="security.token_storage" />
<argument key="security.authorization_checker" type="service" id="security.authorization_checker" />
</argument>
</service>
<argument type="service_locator">
<argument key="security.token_storage" type="service" id="security.token_storage" />
<argument key="security.authorization_checker" type="service" id="security.authorization_checker" />
</argument>
</service>
<service id="Symfony\Component\Security\Core\Security" alias="security.helper" />

View File

@ -20,7 +20,7 @@
"ext-xml": "*",
"symfony/config": "^4.2",
"symfony/security": "~4.2",
"symfony/dependency-injection": "^3.4.3|^4.0.3",
"symfony/dependency-injection": "^4.2",
"symfony/http-kernel": "^4.1"
},
"require-dev": {

View File

@ -11,9 +11,6 @@
namespace Symfony\Component\DependencyInjection\Argument;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
/**
* Represents a collection of values to lazily iterate over.
*
@ -21,35 +18,5 @@ use Symfony\Component\DependencyInjection\Reference;
*/
class IteratorArgument implements ArgumentInterface
{
private $values;
/**
* @param Reference[] $values
*/
public function __construct(array $values)
{
$this->setValues($values);
}
/**
* @return array The values to lazily iterate over
*/
public function getValues()
{
return $this->values;
}
/**
* @param Reference[] $values The service references to lazily iterate over
*/
public function setValues(array $values)
{
foreach ($values as $k => $v) {
if (null !== $v && !$v instanceof Reference) {
throw new InvalidArgumentException(sprintf('An IteratorArgument must hold only Reference instances, "%s" given.', is_object($v) ? get_class($v) : gettype($v)));
}
}
$this->values = $values;
}
use ReferenceSetArgumentTrait;
}

View File

@ -0,0 +1,54 @@
<?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\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
trait ReferenceSetArgumentTrait
{
private $values;
/**
* @param Reference[] $values
*/
public function __construct(array $values)
{
$this->setValues($values);
}
/**
* @return Reference[] The values in the set
*/
public function getValues()
{
return $this->values;
}
/**
* @param Reference[] $values The service references to put in the set
*/
public function setValues(array $values)
{
foreach ($values as $k => $v) {
if (null !== $v && !$v instanceof Reference) {
throw new InvalidArgumentException(sprintf('A %s must hold only Reference instances, "%s" given.', __CLASS__, is_object($v) ? get_class($v) : gettype($v)));
}
}
$this->values = $values;
}
}

View File

@ -0,0 +1,40 @@
<?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\ServiceLocator as BaseServiceLocator;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class ServiceLocator extends BaseServiceLocator
{
private $factory;
private $serviceMap;
public function __construct(\Closure $factory, array $serviceMap)
{
$this->factory = $factory;
$this->serviceMap = $serviceMap;
parent::__construct($serviceMap);
}
/**
* {@inheritdoc}
*/
public function get($id)
{
return isset($this->serviceMap[$id]) ? ($this->factory)(...$this->serviceMap[$id]) : parent::get($id);
}
}

View File

@ -0,0 +1,22 @@
<?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;
/**
* Represents a closure acting as a service locator.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class ServiceLocatorArgument implements ArgumentInterface
{
use ReferenceSetArgumentTrait;
}

View File

@ -5,6 +5,7 @@ CHANGELOG
-----
* added `ServiceSubscriberTrait`
* added `ServiceLocatorArgument` for creating optimized service-locators
4.1.0
-----

View File

@ -11,13 +11,11 @@
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\EnvVarProcessor;
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\Reference;
/**
@ -41,7 +39,7 @@ class RegisterEnvVarProcessorsPass implements CompilerPassInterface
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, EnvVarProcessorInterface::class));
}
foreach ($class::getProvidedTypes() as $prefix => $type) {
$processors[$prefix] = new ServiceClosureArgument(new Reference($id));
$processors[$prefix] = new Reference($id);
$types[$prefix] = self::validateProvidedTypes($type, $class);
}
}
@ -56,9 +54,8 @@ class RegisterEnvVarProcessorsPass implements CompilerPassInterface
}
if ($processors) {
$container->register('container.env_var_processors_locator', ServiceLocator::class)
$container->setAlias('container.env_var_processors_locator', (string) ServiceLocatorTagPass::register($container, $processors))
->setPublic(true)
->setArguments(array($processors))
;
}
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
@ -28,6 +29,9 @@ final class ServiceLocatorTagPass extends AbstractRecursivePass
{
protected function processValue($value, $isRoot = false)
{
if ($value instanceof ServiceLocatorArgument) {
return self::register($this->container, $value->getValues());
}
if (!$value instanceof Definition || !$value->hasTag('container.service_locator')) {
return parent::processValue($value, $isRoot);
}

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
@ -401,6 +402,30 @@ class Container implements ResettableContainerInterface
}
}
/**
* @internal
*/
final protected function getService($registry, $id, $method, $load)
{
if ('service_container' === $id) {
return $this;
}
if (\is_string($load)) {
throw new RuntimeException($load);
}
if (null === $method) {
return false !== $registry ? $this->{$registry}[$id] ?? null : null;
}
if (false !== $registry) {
return $this->{$registry}[$id] ?? $this->{$registry}[$id] = $load ? $this->load($method) : $this->{$method}();
}
if (!$load) {
return $this->{$method}();
}
return ($factory = $this->factories[$id] ?? $this->factories['service_container'][$id] ?? null) ? $factory() : $this->load($method);
}
private function __clone()
{
}

View File

@ -15,6 +15,8 @@ use Psr\Container\ContainerInterface as PsrContainerInterface;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocator;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Compiler\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
@ -1215,6 +1217,14 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
return $count;
});
} elseif ($value instanceof ServiceLocatorArgument) {
$refs = array();
foreach ($value->getValues() as $k => $v) {
if ($v) {
$refs[$k] = array($v);
}
}
$value = new ServiceLocator(\Closure::fromCallable(array($this, 'resolveServices')), $refs);
} elseif ($value instanceof Reference) {
$value = $this->doGet((string) $value, $value->getInvalidBehavior(), $inlineServices);
} elseif ($value instanceof Definition) {

View File

@ -14,6 +14,8 @@ namespace Symfony\Component\DependencyInjection\Dumper;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocator;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Variable;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Compiler\AnalyzeServiceReferencesPass;
@ -22,6 +24,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator as BaseServiceLocator;
use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\Exception\EnvParameterException;
@ -71,6 +74,8 @@ class PhpDumper extends Dumper
private $circularReferences = array();
private $singleUsePrivateIds = array();
private $addThrow = false;
private $locatedIds = array();
private $serviceLocatorTag;
/**
* @var ProxyDumper
@ -113,6 +118,7 @@ class PhpDumper extends Dumper
*/
public function dump(array $options = array())
{
$this->locatedIds = array();
$this->targetDirRegex = null;
$this->inlinedRequires = array();
$options = array_merge(array(
@ -123,6 +129,7 @@ class PhpDumper extends Dumper
'debug' => true,
'hot_path_tag' => 'container.hot_path',
'inline_class_loader_parameter' => 'container.dumper.inline_class_loader',
'service_locator_tag' => 'container.service_locator',
'build_time' => time(),
), $options);
@ -131,6 +138,7 @@ class PhpDumper extends Dumper
$this->asFiles = $options['as_files'];
$this->hotPathTag = $options['hot_path_tag'];
$this->inlineRequires = $options['inline_class_loader_parameter'] && $this->container->hasParameter($options['inline_class_loader_parameter']) && $this->container->getParameter($options['inline_class_loader_parameter']);
$this->serviceLocatorTag = $options['service_locator_tag'];
if (0 !== strpos($baseClass = $options['base_class'], '\\') && 'Container' !== $baseClass) {
$baseClass = sprintf('%s\%s', $options['namespace'] ? '\\'.$options['namespace'] : '', $baseClass);
@ -269,6 +277,7 @@ EOF;
$this->targetDirRegex = null;
$this->inlinedRequires = array();
$this->circularReferences = array();
$this->locatedIds = array();
$unusedEnvs = array();
foreach ($this->container->getEnvCounters() as $env => $use) {
@ -508,7 +517,7 @@ EOTXT
throw new ServiceCircularReferenceException($id, array($id));
}
$code .= $this->addNewInstance($def, '$'.$name, ' = ', $id);
$code .= $this->addNewInstance($def, ' $'.$name.' = ', $id);
if (!$this->hasReference($id, array($def->getProperties(), $def->getMethodCalls(), $def->getConfigurator()), true)) {
$code .= $this->addServiceProperties($def, $name);
@ -552,7 +561,7 @@ EOTXT
$instantiation .= ' = ';
}
$code = $this->addNewInstance($definition, $return, $instantiation, $id);
$code = $this->addNewInstance($definition, ' '.$return.$instantiation, $id);
if (!$isSimpleInstance) {
$code .= "\n";
@ -816,7 +825,7 @@ EOF;
}
if ($definition->isPublic()) {
$publicServices .= $this->addService($id, $definition);
} elseif (!$this->isTrivialInstance($definition)) {
} elseif (!$this->isTrivialInstance($definition) || isset($this->locatedIds[$id])) {
$privateServices .= $this->addService($id, $definition);
}
}
@ -829,7 +838,7 @@ EOF;
$definitions = $this->container->getDefinitions();
ksort($definitions);
foreach ($definitions as $id => $definition) {
if (!$definition->isSynthetic() && !$this->isHotPath($definition) && ($definition->isPublic() || !$this->isTrivialInstance($definition))) {
if (!$definition->isSynthetic() && !$this->isHotPath($definition) && ($definition->isPublic() || !$this->isTrivialInstance($definition) || isset($this->locatedIds[$id]))) {
$code = $this->addService($id, $definition, $file);
if (!$definition->isShared()) {
@ -850,10 +859,18 @@ EOF;
}
}
private function addNewInstance(Definition $definition, $return, $instantiation, $id)
private function addNewInstance(Definition $definition, string $return = '', string $id = null)
{
$class = $this->dumpValue($definition->getClass());
$return = ' '.$return.$instantiation;
$tail = $return ? ";\n" : '';
if (BaseServiceLocator::class === $definition->getClass() && $definition->hasTag($this->serviceLocatorTag)) {
$arguments = array();
foreach ($definition->getArgument(0) as $k => $argument) {
$arguments[$k] = $argument->getValues()[0];
}
return $return.$this->dumpValue(new ServiceLocatorArgument($arguments)).$tail;
}
$arguments = array();
foreach ($definition->getArguments() as $value) {
@ -862,6 +879,7 @@ EOF;
if (null !== $definition->getFactory()) {
$callable = $definition->getFactory();
if (is_array($callable)) {
if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $callable[1])) {
throw new RuntimeException(sprintf('Cannot dump definition because of invalid factory method (%s)', $callable[1] ?: 'n/a'));
@ -869,34 +887,34 @@ EOF;
if ($callable[0] instanceof Reference
|| ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0]))) {
return $return.sprintf("%s->%s(%s);\n", $this->dumpValue($callable[0]), $callable[1], $arguments ? implode(', ', $arguments) : '');
return $return.sprintf('%s->%s(%s)', $this->dumpValue($callable[0]), $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
}
$class = $this->dumpValue($callable[0]);
// If the class is a string we can optimize away
if (0 === strpos($class, "'") && false === strpos($class, '$')) {
if ("''" === $class) {
throw new RuntimeException(sprintf('Cannot dump definition: The "%s" service is defined to be created by a factory but is missing the service reference, did you forget to define the factory service id or class?', $id));
throw new RuntimeException(sprintf('Cannot dump definition: %s service is defined to be created by a factory but is missing the service reference, did you forget to define the factory service id or class?', $id ? 'The "'.$id.'"' : 'inline'));
}
return $return.sprintf("%s::%s(%s);\n", $this->dumpLiteralClass($class), $callable[1], $arguments ? implode(', ', $arguments) : '');
return $return.sprintf('%s::%s(%s)', $this->dumpLiteralClass($class), $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
}
if (0 === strpos($class, 'new ')) {
return $return.sprintf("(%s)->%s(%s);\n", $class, $callable[1], $arguments ? implode(', ', $arguments) : '');
return $return.sprintf('(%s)->%s(%s)', $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
}
return $return.sprintf("[%s, '%s'](%s);\n", $class, $callable[1], $arguments ? implode(', ', $arguments) : '');
return $return.sprintf("[%s, '%s'](%s)", $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
}
return $return.sprintf("%s(%s);\n", $this->dumpLiteralClass($this->dumpValue($callable)), $arguments ? implode(', ', $arguments) : '');
return $return.sprintf('%s(%s)', $this->dumpLiteralClass($this->dumpValue($callable)), $arguments ? implode(', ', $arguments) : '').$tail;
}
if (false !== strpos($class, '$')) {
return sprintf(" \$class = %s;\n\n%snew \$class(%s);\n", $class, $return, implode(', ', $arguments));
if (null === $class = $definition->getClass()) {
throw new RuntimeException('Cannot dump definitions which have no class nor factory.');
}
return $return.sprintf("new %s(%s);\n", $this->dumpLiteralClass($class), implode(', ', $arguments));
return $return.sprintf('new %s(%s)', $this->dumpLiteralClass($this->dumpValue($class)), implode(', ', $arguments)).$tail;
}
private function startClass(string $class, string $baseClass, string $baseClassWithNamespace): string
@ -1537,6 +1555,27 @@ EOF;
return implode("\n", $code);
}
if ($value instanceof ServiceLocatorArgument) {
$serviceMap = '';
foreach ($value->getValues() as $k => $v) {
if (!$v) {
continue;
}
$definition = $this->container->findDefinition($id = (string) $v);
$load = !($e = $definition->getErrors()) ? $this->asFiles && !$this->isHotPath($definition) : reset($e);
$serviceMap .= sprintf("\n %s => array(%s, %s, %s, %s),",
$this->export($k),
$this->export($definition->isShared() ? ($definition->isPublic() ? 'services' : 'privates') : false),
$this->export($id),
$this->export(ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE !== $v->getInvalidBehavior() && !\is_string($load) ? $this->generateMethodName($id).($load ? '.php' : '') : null),
$this->export($load)
);
$this->locatedIds[$id] = true;
}
return sprintf('new \%s(\Closure::fromCallable(array($this, \'getService\')), array(%s%s))', ServiceLocator::class, $serviceMap, $serviceMap ? "\n " : '');
}
} finally {
list($this->definitionVariables, $this->referenceVariables, $this->variableCount) = $scope;
}
@ -1559,50 +1598,7 @@ EOF;
throw new RuntimeException('Cannot dump definitions which have a configurator.');
}
$arguments = array();
foreach ($value->getArguments() as $argument) {
$arguments[] = $this->dumpValue($argument);
}
if (null !== $value->getFactory()) {
$factory = $value->getFactory();
if (is_string($factory)) {
return sprintf('%s(%s)', $this->dumpLiteralClass($this->dumpValue($factory)), implode(', ', $arguments));
}
if (is_array($factory)) {
if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $factory[1])) {
throw new RuntimeException(sprintf('Cannot dump definition because of invalid factory method (%s)', $factory[1] ?: 'n/a'));
}
$class = $this->dumpValue($factory[0]);
if (is_string($factory[0])) {
return sprintf('%s::%s(%s)', $this->dumpLiteralClass($class), $factory[1], implode(', ', $arguments));
}
if ($factory[0] instanceof Definition) {
if (0 === strpos($class, 'new ')) {
return sprintf('(%s)->%s(%s)', $class, $factory[1], implode(', ', $arguments));
}
return sprintf("[%s, '%s'](%s)", $class, $factory[1], implode(', ', $arguments));
}
if ($factory[0] instanceof Reference) {
return sprintf('%s->%s(%s)', $class, $factory[1], implode(', ', $arguments));
}
}
throw new RuntimeException('Cannot dump definition because of invalid factory');
}
$class = $value->getClass();
if (null === $class) {
throw new RuntimeException('Cannot dump definitions which have no class nor factory.');
}
return sprintf('new %s(%s)', $this->dumpLiteralClass($this->dumpValue($class)), implode(', ', $arguments));
return $this->addNewInstance($value);
} elseif ($value instanceof Variable) {
return '$'.$value;
} elseif ($value instanceof Reference) {
@ -1696,7 +1692,7 @@ EOF;
return sprintf('$this->throw(%s)', $this->export(reset($e)));
}
$code = substr($this->addNewInstance($definition, '', '', $id), 8, -2);
$code = $this->addNewInstance($definition, '', $id);
if ($definition->isShared() && !isset($this->singleUsePrivateIds[$id])) {
$code = sprintf('$this->%s[\'%s\'] = %s', $definition->isPublic() ? 'services' : 'privates', $id, $code);
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Dumper;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Parameter;
@ -280,6 +281,9 @@ class XmlDumper extends Dumper
} elseif ($value instanceof IteratorArgument) {
$element->setAttribute('type', 'iterator');
$this->convertParameters($value->getValues(), $type, $element, 'key');
} elseif ($value instanceof ServiceLocatorArgument) {
$element->setAttribute('type', 'service_locator');
$this->convertParameters($value->getValues(), $type, $element, 'key');
} elseif ($value instanceof Reference) {
$element->setAttribute('type', 'service');
$element->setAttribute('id', (string) $value);

View File

@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
@ -233,6 +234,8 @@ class YamlDumper extends Dumper
}
if ($value instanceof IteratorArgument) {
$tag = 'iterator';
} elseif ($value instanceof ServiceLocatorArgument) {
$tag = 'service_locator';
} else {
throw new RuntimeException(sprintf('Unspecified Yaml tag for type "%s".', get_class($value)));
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
@ -92,6 +93,16 @@ function inline(string $class = null): InlineServiceConfigurator
return new InlineServiceConfigurator(new Definition($class));
}
/**
* Creates a service locator.
*
* @param ReferenceConfigurator[] $values
*/
function service_locator(array $values): ServiceLocatorArgument
{
return new ServiceLocatorArgument(AbstractConfigurator::processValue($values, true));
}
/**
* Creates a lazy iterator.
*

View File

@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
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\ContainerBuilder;
@ -510,7 +511,15 @@ class XmlFileLoader extends FileLoader
try {
$arguments[$key] = new IteratorArgument($arg);
} catch (InvalidArgumentException $e) {
throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="iterator" only accepts collections of type="service" references in "%s".', $name, $file));
throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="%s" only accepts collections of type="service" references in "%s".', $name, $file));
}
break;
case 'service_locator':
$arg = $this->getArgumentsAsPhp($arg, $name, $file, false);
try {
$arguments[$key] = new ServiceLocatorArgument($arg);
} catch (InvalidArgumentException $e) {
throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="%s" only accepts maps of type="service" references in "%s".', $name, $file));
}
break;
case 'tagged':

View File

@ -15,6 +15,7 @@ use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@ -691,6 +692,17 @@ class YamlFileLoader extends FileLoader
throw new InvalidArgumentException(sprintf('"!iterator" tag only accepts arrays of "@service" references in "%s".', $file));
}
}
if ('service_locator' === $value->getTag()) {
if (!is_array($argument)) {
throw new InvalidArgumentException(sprintf('"!service_locator" tag only accepts maps in "%s".', $file));
}
$argument = $this->resolveServices($argument, $file, $isParameter);
try {
return new ServiceLocatorArgument($argument);
} catch (InvalidArgumentException $e) {
throw new InvalidArgumentException(sprintf('"!service_locator" tag only accepts maps of "@service" references in "%s".', $file));
}
}
if ('tagged' === $value->getTag()) {
if (!is_string($argument) || !$argument) {
throw new InvalidArgumentException(sprintf('"!tagged" tag only accepts non empty string in "%s".', $file));

View File

@ -259,6 +259,7 @@
<xsd:enumeration value="string" />
<xsd:enumeration value="constant" />
<xsd:enumeration value="iterator" />
<xsd:enumeration value="service_locator" />
<xsd:enumeration value="tagged" />
</xsd:restriction>
</xsd:simpleType>

View File

@ -1431,6 +1431,33 @@ class ContainerBuilderTest extends TestCase
$container->get('errored_definition');
}
public function testServiceLocatorArgument()
{
$container = include __DIR__.'/Fixtures/containers/container_service_locator_argument.php';
$container->compile();
$locator = $container->get('bar')->locator;
$this->assertInstanceOf(ServiceLocator::class, $locator);
$this->assertSame($container->get('foo1'), $locator->get('foo1'));
$this->assertEquals(new \stdClass(), $locator->get('foo2'));
$this->assertSame($locator->get('foo2'), $locator->get('foo2'));
$this->assertEquals(new \stdClass(), $locator->get('foo3'));
$this->assertNotSame($locator->get('foo3'), $locator->get('foo3'));
try {
$locator->get('foo4');
$this->fail('RuntimeException expected.');
} catch (RuntimeException $e) {
$this->assertSame('BOOM', $e->getMessage());
}
$this->assertNull($locator->get('foo5'));
$container->set('foo5', $foo5 = new \stdClass());
$this->assertSame($foo5, $locator->get('foo5'));
}
}
class FooClass

View File

@ -17,10 +17,12 @@ use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocator as ArgumentServiceLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\DependencyInjection\Reference;
@ -1015,6 +1017,38 @@ class PhpDumperTest extends TestCase
$container = new \Symfony_DI_PhpDumper_Errored_Definition();
$container->get('runtime_error');
}
public function testServiceLocatorArgument()
{
$container = include self::$fixturesPath.'/containers/container_service_locator_argument.php';
$container->compile();
$dumper = new PhpDumper($container);
$dump = $dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Service_Locator_Argument'));
$this->assertStringEqualsFile(self::$fixturesPath.'/php/services_service_locator_argument.php', str_replace(str_replace('\\', '\\\\', self::$fixturesPath.DIRECTORY_SEPARATOR.'includes'.DIRECTORY_SEPARATOR), '%path%', $dump));
eval('?>'.$dump);
$container = new \Symfony_DI_PhpDumper_Service_Locator_Argument();
$locator = $container->get('bar')->locator;
$this->assertInstanceOf(ArgumentServiceLocator::class, $locator);
$this->assertSame($container->get('foo1'), $locator->get('foo1'));
$this->assertEquals(new \stdClass(), $locator->get('foo2'));
$this->assertSame($locator->get('foo2'), $locator->get('foo2'));
$this->assertEquals(new \stdClass(), $locator->get('foo3'));
$this->assertNotSame($locator->get('foo3'), $locator->get('foo3'));
try {
$locator->get('foo4');
$this->fail('RuntimeException expected.');
} catch (RuntimeException $e) {
$this->assertSame('BOOM', $e->getMessage());
}
$this->assertNull($locator->get('foo5'));
$container->set('foo5', $foo5 = new \stdClass());
$this->assertSame($foo5, $locator->get('foo5'));
}
}
class Rot13EnvVarProcessor implements EnvVarProcessorInterface

View File

@ -0,0 +1,46 @@
<?php
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
$container = new ContainerBuilder();
$container
->register('foo1', 'stdClass')
->setPublic(true)
;
$container
->register('foo2', 'stdClass')
;
$container
->register('foo3', 'stdClass')
->setShared(false)
;
$container
->register('foo4', 'stdClass')
->addError('BOOM')
;
$container
->register('foo5', 'stdClass')
->setPublic(true)
->setSynthetic(true)
;
$container
->register('bar', 'stdClass')
->setProperty('locator', new ServiceLocatorArgument(array(
'foo1' => new Reference('foo1'),
'foo2' => new Reference('foo2'),
'foo3' => new Reference('foo3'),
'foo4' => new Reference('foo4', $container::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE),
'foo5' => new Reference('foo5', $container::IGNORE_ON_UNINITIALIZED_REFERENCE),
)))
->setPublic(true)
;
return $container;

View File

@ -82,9 +82,7 @@ class Symfony_DI_PhpDumper_Test_EnvParameters extends Container
*/
protected function getTestService()
{
$class = $this->getEnv('FOO');
return $this->services['test'] = new $class($this->getEnv('Bar'), 'foo'.$this->getEnv('string:FOO').'baz', $this->getEnv('int:Baz'));
return $this->services['test'] = new ${($_ = $this->getEnv('FOO')) && false ?: "_"}($this->getEnv('Bar'), 'foo'.$this->getEnv('string:FOO').'baz', $this->getEnv('int:Baz'));
}
public function getParameter($name)

View File

@ -56,6 +56,7 @@ class Symfony_DI_PhpDumper_Test_Rot13Parameters extends Container
public function getRemovedIds()
{
return array(
'.service_locator.GU08LT9' => true,
'Psr\\Container\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
);
@ -78,9 +79,9 @@ class Symfony_DI_PhpDumper_Test_Rot13Parameters extends Container
*/
protected function getContainer_EnvVarProcessorsLocatorService()
{
return $this->services['container.env_var_processors_locator'] = new \Symfony\Component\DependencyInjection\ServiceLocator(array('rot13' => function () {
return ($this->services['Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor'] ?? $this->services['Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\Rot13EnvVarProcessor());
}));
return $this->services['container.env_var_processors_locator'] = new \Symfony\Component\DependencyInjection\Argument\ServiceLocator(\Closure::fromCallable(array($this, 'getService')), array(
'rot13' => array('services', 'Symfony\\Component\\DependencyInjection\\Tests\\Dumper\\Rot13EnvVarProcessor', 'getRot13EnvVarProcessorService', false),
));
}
public function getParameter($name)

View File

@ -0,0 +1,128 @@
<?php
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
/**
* This class has been auto-generated
* by the Symfony Dependency Injection Component.
*
* @final since Symfony 3.3
*/
class Symfony_DI_PhpDumper_Service_Locator_Argument extends Container
{
private $parameters;
private $targetDirs = array();
/**
* @internal but protected for BC on cache:clear
*/
protected $privates = array();
public function __construct()
{
$this->services = $this->privates = array();
$this->syntheticIds = array(
'foo5' => true,
);
$this->methodMap = array(
'bar' => 'getBarService',
'foo1' => 'getFoo1Service',
);
$this->aliases = array();
}
public function reset()
{
$this->privates = array();
parent::reset();
}
public function compile()
{
throw new LogicException('You cannot compile a dumped container that was already compiled.');
}
public function isCompiled()
{
return true;
}
public function getRemovedIds()
{
return array(
'.service_locator.38dy3OH' => true,
'Psr\\Container\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
'foo2' => true,
'foo3' => true,
'foo4' => true,
);
}
/**
* Gets the public 'bar' shared service.
*
* @return \stdClass
*/
protected function getBarService()
{
$this->services['bar'] = $instance = new \stdClass();
$instance->locator = new \Symfony\Component\DependencyInjection\Argument\ServiceLocator(\Closure::fromCallable(array($this, 'getService')), array(
'foo1' => array('services', 'foo1', 'getFoo1Service', false),
'foo2' => array('privates', 'foo2', 'getFoo2Service', false),
'foo3' => array(false, 'foo3', 'getFoo3Service', false),
'foo4' => array('privates', 'foo4', NULL, 'BOOM'),
'foo5' => array('services', 'foo5', NULL, false),
));
return $instance;
}
/**
* Gets the public 'foo1' shared service.
*
* @return \stdClass
*/
protected function getFoo1Service()
{
return $this->services['foo1'] = new \stdClass();
}
/**
* Gets the private 'foo2' shared service.
*
* @return \stdClass
*/
protected function getFoo2Service()
{
return $this->privates['foo2'] = new \stdClass();
}
/**
* Gets the private 'foo3' service.
*
* @return \stdClass
*/
protected function getFoo3Service()
{
return new \stdClass();
}
/**
* Gets the private 'foo4' shared service.
*
* @return \stdClass
*/
protected function getFoo4Service()
{
return $this->privates['foo4'] = new \stdClass();
}
}

View File

@ -79,14 +79,11 @@ class ProjectServiceContainer extends Container
*/
protected function getFooServiceService()
{
return $this->services['foo_service'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber((new \Symfony\Component\DependencyInjection\ServiceLocator(array('Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => function (): ?\Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition {
return ($this->privates['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] ?? $this->privates['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition());
}, 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber' => function (): \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber {
return ($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] ?? $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber());
}, 'bar' => function (): \Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition {
return ($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] ?? $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber());
}, 'baz' => function (): ?\Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition {
return ($this->privates['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] ?? $this->privates['Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition());
})))->withContext('foo_service', $this));
return $this->services['foo_service'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber((new \Symfony\Component\DependencyInjection\Argument\ServiceLocator(\Closure::fromCallable(array($this, 'getService')), array(
'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => array('privates', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'getCustomDefinitionService', false),
'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber' => array('services', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber', 'getTestServiceSubscriberService', false),
'bar' => array('services', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\TestServiceSubscriber', 'getTestServiceSubscriberService', false),
'baz' => array('privates', 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition', 'getCustomDefinitionService', false),
)))->withContext('foo_service', $this));
}
}