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

This commit is contained in:
Nicolas Grekas 2018-06-30 08:39:51 +02:00
parent 04b2c2db4f
commit 6c8e9576a3
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));
}
}