Skip to content

Commit

Permalink
feature #27783 [DI] Add ServiceLocatorArgument to generate array-base…
Browse files Browse the repository at this point in the history
…d 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
-------

6c8e957 [DI] Add ServiceLocatorArgument to generate array-based locators optimized for OPcache shared memory
  • Loading branch information
nicolas-grekas committed Jul 7, 2018
2 parents 1cf8146 + 6c8e957 commit 6d3f63d
Show file tree
Hide file tree
Showing 31 changed files with 525 additions and 147 deletions.
Expand Up @@ -17,6 +17,7 @@
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;
Expand Down Expand Up @@ -334,6 +335,8 @@ protected function describeContainerDefinition(Definition $definition, array $op
$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 {
Expand Down
Expand Up @@ -14,6 +14,7 @@
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;
Expand Down Expand Up @@ -387,8 +388,8 @@ private function getArgumentNodes(array $arguments, \DOMDocument $dom)
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);
Expand Down
Expand Up @@ -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;

Expand All @@ -32,7 +32,7 @@ public function process(ContainerBuilder $container)

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);
}
}

Expand All @@ -44,13 +44,15 @@ public function process(ContainerBuilder $container)
$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);
}
}
}
11 changes: 3 additions & 8 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml
Expand Up @@ -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>

Expand Down
9 changes: 2 additions & 7 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/test.xml
Expand Up @@ -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>

Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/FrameworkBundle/composer.json
Expand Up @@ -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",
Expand Down
11 changes: 3 additions & 8 deletions src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
Expand Up @@ -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" />
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/SecurityBundle/composer.json
Expand Up @@ -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": {
Expand Down
Expand Up @@ -11,45 +11,12 @@

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.
*
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
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;
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
-----

* added `ServiceSubscriberTrait`
* added `ServiceLocatorArgument` for creating optimized service-locators

4.1.0
-----
Expand Down
Expand Up @@ -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;

/**
Expand All @@ -41,7 +39,7 @@ public function process(ContainerBuilder $container)
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);
}
}
Expand All @@ -56,9 +54,8 @@ public function process(ContainerBuilder $container)
}

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))
;
}
}
Expand Down
Expand Up @@ -13,6 +13,7 @@

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;
Expand All @@ -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);
}
Expand Down

0 comments on commit 6d3f63d

Please sign in to comment.