Permalink
Browse files

feature #21455 [DI] Allow to count on lazy collection arguments (ogiz…

…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...
2 parents 55a34b7 + f23e460 commit caba97a62b95ce87597d260bbea99c7ad0189da2 @nicolas-grekas nicolas-grekas committed Feb 2, 2017
@@ -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()
@@ -29,4 +35,13 @@ public function getIterator()
return $g();
}
+
+ public function count()
+ {
+ if (is_callable($count = $this->count)) {
+ $this->count = $count();
+ }
+
+ return $this->count;
+ }
}
@@ -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();
@@ -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);
}
/**
@@ -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) {
@@ -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');
+ }
+}
@@ -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) {
@@ -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));
}
/**
@@ -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'));
}));
}
@@ -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));
}
/**
@@ -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));
}
/**

0 comments on commit caba97a

Please sign in to comment.