Skip to content

Commit

Permalink
feature #21455 [DI] Allow to count on lazy collection arguments (ogiz…
Browse files Browse the repository at this point in the history
…anagi)

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 | #21450 (comment)
| License       | MIT
| Doc PR        | todo (with symfony/symfony-docs#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
  • Loading branch information
nicolas-grekas committed Feb 2, 2017
2 parents 55a34b7 + f23e460 commit caba97a
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@
/**
* @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()
Expand All @@ -29,4 +35,13 @@ public function getIterator()

return $g();
}

public function count()
{
if (is_callable($count = $this->count)) {
$this->count = $count();
}

return $this->count;
}
}
13 changes: 13 additions & 0 deletions src/Symfony/Component/DependencyInjection/ContainerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,19 @@ public function resolveServices($value)

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();
Expand Down
40 changes: 33 additions & 7 deletions src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -1362,19 +1362,36 @@ private function instantiateProxy($class, $args, $useConstructor)
*/
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);
}

/**
Expand Down Expand Up @@ -1524,17 +1541,26 @@ private function dumpValue($value, $interpolate = true)

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) {
$code[] = ' '.$v;
}
}
}
$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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ public function testCreateServiceWithIteratorArgument()

$lazyContext = $builder->get('lazy_context');
$this->assertInstanceOf(RewindableGenerator::class, $lazyContext->lazyValues);
$this->assertCount(1, $lazyContext->lazyValues);

$i = 0;
foreach ($lazyContext->lazyValues as $k => $v) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,13 +314,13 @@ protected function getFooWithInlineService()
*/
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));
}

/**
Expand All @@ -333,11 +333,13 @@ protected function getLazyContextService()
*/
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'));
}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,13 +313,13 @@ protected function getFooWithInlineService()
*/
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));
}

/**
Expand All @@ -332,9 +332,9 @@ protected function getLazyContextService()
*/
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));
}

/**
Expand Down

0 comments on commit caba97a

Please sign in to comment.