feature #21455 [DI] Allow to count on lazy collection arguments (ogizanagi)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DI] Allow to count on lazy collection arguments

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | https://github.com/symfony/symfony/pull/21450#issuecomment-275931764
| License       | MIT
| Doc PR        | todo (with https://github.com/symfony/symfony-docs/issues/7336)

When using the new iterator feature of the DI component to lazy load collection, we always know the number of arguments in the collection (only the invalidBehavior set to `IGNORE_ON_INVALID_REFERENCE` may change this number). So we are able to generate and use a `RewindableGenerator` implementing `\Countable` by computing this value ahead.

So, in a service accepting `array|iterable`, like in the `GuardAuthenticationListener` (see #21450):

```php
class GuardAuthenticationListener implements ListenerInterface
{
    private $guardAuthenticators;

    /**
       * @param iterable|GuardAuthenticatorInterface[]  $guardAuthenticators   The authenticators, with keys that match what's passed to GuardAuthenticationProvider
       * @param LoggerInterface                         $logger                A LoggerInterface instance
    */
    public function __construct($guardAuthenticators, LoggerInterface $logger = null)
    {
          // ...
    }

    public function handle(GetResponseEvent $event)
    {
        if (null !== $this->logger) {
            $context = array()
            if (is_array($this->guardAuthenticators) || $this->guardAuthenticators instanceof \Countable) {
                $context['authenticators'] = count($this->guardAuthenticators);
            }
            $this->logger->debug('Checking for guard authentication credentials.', $context);
        }
        // ...
    }
}
```

we still keep the ability to call count without loosing the lazy load benefits.

Commits
-------

f23e460 [DI] Allow to count on lazy collection arguments
This commit is contained in:
Nicolas Grekas 2017-02-02 15:11:42 +01:00
commit caba97a62b
7 changed files with 125 additions and 16 deletions

View File

@ -14,13 +14,19 @@ namespace Symfony\Component\DependencyInjection\Argument;
/**
* @internal
*/
class RewindableGenerator implements \IteratorAggregate
class RewindableGenerator implements \IteratorAggregate, \Countable
{
private $generator;
private $count;
public function __construct(callable $generator)
/**
* @param callable $generator
* @param int|callable $count
*/
public function __construct(callable $generator, $count)
{
$this->generator = $generator;
$this->count = $count;
}
public function getIterator()
@ -29,4 +35,13 @@ class RewindableGenerator implements \IteratorAggregate
return $g();
}
public function count()
{
if (is_callable($count = $this->count)) {
$this->count = $count();
}
return $this->count;
}
}

View File

@ -1002,6 +1002,19 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
yield $k => $this->resolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($v)));
}
}, function () use ($value) {
$count = 0;
foreach ($value->getValues() as $v) {
foreach (self::getServiceConditionals($v) as $s) {
if (!$this->has($s)) {
continue 2;
}
}
++$count;
}
return $count;
});
} elseif ($value instanceof ClosureProxyArgument) {
$parameterBag = $this->getParameterBag();

View File

@ -1362,19 +1362,36 @@ EOF;
*/
private function wrapServiceConditionals($value, $code, &$isUnconditional = null, $containerRef = '$this')
{
if ($isUnconditional = !$services = ContainerBuilder::getServiceConditionals($value)) {
if ($isUnconditional = !$condition = $this->getServiceConditionals($value, $containerRef)) {
return $code;
}
// re-indent the wrapped code
$code = implode("\n", array_map(function ($line) { return $line ? ' '.$line : $line; }, explode("\n", $code)));
return sprintf(" if (%s) {\n%s }\n", $condition, $code);
}
/**
* Get the conditions to execute for conditional services.
*
* @param string $value
* @param string $containerRef
*
* @return null|string
*/
private function getServiceConditionals($value, $containerRef = '$this')
{
if (!$services = ContainerBuilder::getServiceConditionals($value)) {
return null;
}
$conditions = array();
foreach ($services as $service) {
$conditions[] = sprintf("%s->has('%s')", $containerRef, $service);
}
// re-indent the wrapped code
$code = implode("\n", array_map(function ($line) { return $line ? ' '.$line : $line; }, explode("\n", $code)));
return sprintf(" if (%s) {\n%s }\n", implode(' && ', $conditions), $code);
return implode(' && ', $conditions);
}
/**
@ -1524,9 +1541,14 @@ EOF;
return sprintf('array(%s)', implode(', ', $code));
} elseif ($value instanceof IteratorArgument) {
$countCode = array();
$countCode[] = 'function () {';
$operands = array(0);
$code = array();
$code[] = 'new RewindableGenerator(function() {';
$code[] = 'new RewindableGenerator(function () {';
foreach ($value->getValues() as $k => $v) {
($c = $this->getServiceConditionals($v)) ? $operands[] = "(int) ($c)" : ++$operands[0];
$v = $this->wrapServiceConditionals($v, sprintf(" yield %s => %s;\n", $this->dumpValue($k, $interpolate), $this->dumpValue($v, $interpolate)));
foreach (explode("\n", $v) as $v) {
if ($v) {
@ -1534,7 +1556,11 @@ EOF;
}
}
}
$code[] = ' })';
$countCode[] = sprintf(' return %s;', implode(' + ', $operands));
$countCode[] = ' }';
$code[] = sprintf(' }, %s)', count($operands) > 1 ? implode("\n", $countCode) : $operands[0]);
return implode("\n", $code);
} elseif ($value instanceof Definition) {

View File

@ -0,0 +1,52 @@
<?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\Tests\Argument;
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
class RewindableGeneratorTest extends \PHPUnit_Framework_TestCase
{
public function testImplementsCountable()
{
$this->assertInstanceOf(\Countable::class, new RewindableGenerator(function () {
yield 1;
}, 1));
}
public function testCountUsesProvidedValue()
{
$generator = new RewindableGenerator(function () {
yield 1;
}, 3);
$this->assertCount(3, $generator);
}
public function testCountUsesProvidedValueAsCallback()
{
$called = 0;
$generator = new RewindableGenerator(function () {
yield 1;
}, function () use (&$called) {
++$called;
return 3;
});
$this->assertSame(0, $called, 'Count callback is called lazily');
$this->assertCount(3, $generator);
count($generator);
$this->assertSame(1, $called, 'Count callback is called only once');
}
}

View File

@ -423,6 +423,7 @@ class ContainerBuilderTest extends \PHPUnit_Framework_TestCase
$lazyContext = $builder->get('lazy_context');
$this->assertInstanceOf(RewindableGenerator::class, $lazyContext->lazyValues);
$this->assertCount(1, $lazyContext->lazyValues);
$i = 0;
foreach ($lazyContext->lazyValues as $k => $v) {

View File

@ -314,13 +314,13 @@ class ProjectServiceContainer extends Container
*/
protected function getLazyContextService()
{
return $this->services['lazy_context'] = new \LazyContext(new RewindableGenerator(function() {
return $this->services['lazy_context'] = new \LazyContext(new RewindableGenerator(function () {
yield 0 => 'foo';
yield 1 => ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->get('foo.baz')) && false ?: '_'};
yield 2 => array($this->getParameter('foo') => 'foo is '.$this->getParameter('foo').'', 'foobar' => $this->getParameter('foo'));
yield 3 => true;
yield 4 => $this;
}));
}, 5));
}
/**
@ -333,11 +333,13 @@ class ProjectServiceContainer extends Container
*/
protected function getLazyContextIgnoreInvalidRefService()
{
return $this->services['lazy_context_ignore_invalid_ref'] = new \LazyContext(new RewindableGenerator(function() {
return $this->services['lazy_context_ignore_invalid_ref'] = new \LazyContext(new RewindableGenerator(function () {
yield 0 => ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->get('foo.baz')) && false ?: '_'};
if ($this->has('invalid')) {
yield 1 => $this->get('invalid', ContainerInterface::NULL_ON_INVALID_REFERENCE);
}
}, function () {
return 1 + (int) ($this->has('invalid'));
}));
}

View File

@ -313,13 +313,13 @@ class ProjectServiceContainer extends Container
*/
protected function getLazyContextService()
{
return $this->services['lazy_context'] = new \LazyContext(new RewindableGenerator(function() {
return $this->services['lazy_context'] = new \LazyContext(new RewindableGenerator(function () {
yield 0 => 'foo';
yield 1 => ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->get('foo.baz')) && false ?: '_'};
yield 2 => array('bar' => 'foo is bar', 'foobar' => 'bar');
yield 3 => true;
yield 4 => $this;
}));
}, 5));
}
/**
@ -332,9 +332,9 @@ class ProjectServiceContainer extends Container
*/
protected function getLazyContextIgnoreInvalidRefService()
{
return $this->services['lazy_context_ignore_invalid_ref'] = new \LazyContext(new RewindableGenerator(function() {
return $this->services['lazy_context_ignore_invalid_ref'] = new \LazyContext(new RewindableGenerator(function () {
yield 0 => ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->get('foo.baz')) && false ?: '_'};
}));
}, 1));
}
/**