Skip to content

Commit caba97a

Browse files
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 | #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
2 parents 55a34b7 + f23e460 commit caba97a

File tree

7 files changed

+125
-16
lines changed

7 files changed

+125
-16
lines changed

src/Symfony/Component/DependencyInjection/Argument/RewindableGenerator.php

+17-2
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,19 @@
1414
/**
1515
* @internal
1616
*/
17-
class RewindableGenerator implements \IteratorAggregate
17+
class RewindableGenerator implements \IteratorAggregate, \Countable
1818
{
1919
private $generator;
20+
private $count;
2021

21-
public function __construct(callable $generator)
22+
/**
23+
* @param callable $generator
24+
* @param int|callable $count
25+
*/
26+
public function __construct(callable $generator, $count)
2227
{
2328
$this->generator = $generator;
29+
$this->count = $count;
2430
}
2531

2632
public function getIterator()
@@ -29,4 +35,13 @@ public function getIterator()
2935

3036
return $g();
3137
}
38+
39+
public function count()
40+
{
41+
if (is_callable($count = $this->count)) {
42+
$this->count = $count();
43+
}
44+
45+
return $this->count;
46+
}
3247
}

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

+13
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,19 @@ public function resolveServices($value)
10021002

10031003
yield $k => $this->resolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($v)));
10041004
}
1005+
}, function () use ($value) {
1006+
$count = 0;
1007+
foreach ($value->getValues() as $v) {
1008+
foreach (self::getServiceConditionals($v) as $s) {
1009+
if (!$this->has($s)) {
1010+
continue 2;
1011+
}
1012+
}
1013+
1014+
++$count;
1015+
}
1016+
1017+
return $count;
10051018
});
10061019
} elseif ($value instanceof ClosureProxyArgument) {
10071020
$parameterBag = $this->getParameterBag();

src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php

+33-7
Original file line numberDiff line numberDiff line change
@@ -1362,19 +1362,36 @@ private function instantiateProxy($class, $args, $useConstructor)
13621362
*/
13631363
private function wrapServiceConditionals($value, $code, &$isUnconditional = null, $containerRef = '$this')
13641364
{
1365-
if ($isUnconditional = !$services = ContainerBuilder::getServiceConditionals($value)) {
1365+
if ($isUnconditional = !$condition = $this->getServiceConditionals($value, $containerRef)) {
13661366
return $code;
13671367
}
13681368

1369+
// re-indent the wrapped code
1370+
$code = implode("\n", array_map(function ($line) { return $line ? ' '.$line : $line; }, explode("\n", $code)));
1371+
1372+
return sprintf(" if (%s) {\n%s }\n", $condition, $code);
1373+
}
1374+
1375+
/**
1376+
* Get the conditions to execute for conditional services.
1377+
*
1378+
* @param string $value
1379+
* @param string $containerRef
1380+
*
1381+
* @return null|string
1382+
*/
1383+
private function getServiceConditionals($value, $containerRef = '$this')
1384+
{
1385+
if (!$services = ContainerBuilder::getServiceConditionals($value)) {
1386+
return null;
1387+
}
1388+
13691389
$conditions = array();
13701390
foreach ($services as $service) {
13711391
$conditions[] = sprintf("%s->has('%s')", $containerRef, $service);
13721392
}
13731393

1374-
// re-indent the wrapped code
1375-
$code = implode("\n", array_map(function ($line) { return $line ? ' '.$line : $line; }, explode("\n", $code)));
1376-
1377-
return sprintf(" if (%s) {\n%s }\n", implode(' && ', $conditions), $code);
1394+
return implode(' && ', $conditions);
13781395
}
13791396

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

15251542
return sprintf('array(%s)', implode(', ', $code));
15261543
} elseif ($value instanceof IteratorArgument) {
1544+
$countCode = array();
1545+
$countCode[] = 'function () {';
1546+
$operands = array(0);
1547+
15271548
$code = array();
1528-
$code[] = 'new RewindableGenerator(function() {';
1549+
$code[] = 'new RewindableGenerator(function () {';
15291550
foreach ($value->getValues() as $k => $v) {
1551+
($c = $this->getServiceConditionals($v)) ? $operands[] = "(int) ($c)" : ++$operands[0];
15301552
$v = $this->wrapServiceConditionals($v, sprintf(" yield %s => %s;\n", $this->dumpValue($k, $interpolate), $this->dumpValue($v, $interpolate)));
15311553
foreach (explode("\n", $v) as $v) {
15321554
if ($v) {
15331555
$code[] = ' '.$v;
15341556
}
15351557
}
15361558
}
1537-
$code[] = ' })';
1559+
1560+
$countCode[] = sprintf(' return %s;', implode(' + ', $operands));
1561+
$countCode[] = ' }';
1562+
1563+
$code[] = sprintf(' }, %s)', count($operands) > 1 ? implode("\n", $countCode) : $operands[0]);
15381564

15391565
return implode("\n", $code);
15401566
} elseif ($value instanceof Definition) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Tests\Argument;
13+
14+
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
15+
16+
class RewindableGeneratorTest extends \PHPUnit_Framework_TestCase
17+
{
18+
public function testImplementsCountable()
19+
{
20+
$this->assertInstanceOf(\Countable::class, new RewindableGenerator(function () {
21+
yield 1;
22+
}, 1));
23+
}
24+
25+
public function testCountUsesProvidedValue()
26+
{
27+
$generator = new RewindableGenerator(function () {
28+
yield 1;
29+
}, 3);
30+
31+
$this->assertCount(3, $generator);
32+
}
33+
34+
public function testCountUsesProvidedValueAsCallback()
35+
{
36+
$called = 0;
37+
$generator = new RewindableGenerator(function () {
38+
yield 1;
39+
}, function () use (&$called) {
40+
++$called;
41+
42+
return 3;
43+
});
44+
45+
$this->assertSame(0, $called, 'Count callback is called lazily');
46+
$this->assertCount(3, $generator);
47+
48+
count($generator);
49+
50+
$this->assertSame(1, $called, 'Count callback is called only once');
51+
}
52+
}

src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ public function testCreateServiceWithIteratorArgument()
423423

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

427428
$i = 0;
428429
foreach ($lazyContext->lazyValues as $k => $v) {

src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php

+5-3
Original file line numberDiff line numberDiff line change
@@ -314,13 +314,13 @@ protected function getFooWithInlineService()
314314
*/
315315
protected function getLazyContextService()
316316
{
317-
return $this->services['lazy_context'] = new \LazyContext(new RewindableGenerator(function() {
317+
return $this->services['lazy_context'] = new \LazyContext(new RewindableGenerator(function () {
318318
yield 0 => 'foo';
319319
yield 1 => ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->get('foo.baz')) && false ?: '_'};
320320
yield 2 => array($this->getParameter('foo') => 'foo is '.$this->getParameter('foo').'', 'foobar' => $this->getParameter('foo'));
321321
yield 3 => true;
322322
yield 4 => $this;
323-
}));
323+
}, 5));
324324
}
325325

326326
/**
@@ -333,11 +333,13 @@ protected function getLazyContextService()
333333
*/
334334
protected function getLazyContextIgnoreInvalidRefService()
335335
{
336-
return $this->services['lazy_context_ignore_invalid_ref'] = new \LazyContext(new RewindableGenerator(function() {
336+
return $this->services['lazy_context_ignore_invalid_ref'] = new \LazyContext(new RewindableGenerator(function () {
337337
yield 0 => ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->get('foo.baz')) && false ?: '_'};
338338
if ($this->has('invalid')) {
339339
yield 1 => $this->get('invalid', ContainerInterface::NULL_ON_INVALID_REFERENCE);
340340
}
341+
}, function () {
342+
return 1 + (int) ($this->has('invalid'));
341343
}));
342344
}
343345

src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -313,13 +313,13 @@ protected function getFooWithInlineService()
313313
*/
314314
protected function getLazyContextService()
315315
{
316-
return $this->services['lazy_context'] = new \LazyContext(new RewindableGenerator(function() {
316+
return $this->services['lazy_context'] = new \LazyContext(new RewindableGenerator(function () {
317317
yield 0 => 'foo';
318318
yield 1 => ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->get('foo.baz')) && false ?: '_'};
319319
yield 2 => array('bar' => 'foo is bar', 'foobar' => 'bar');
320320
yield 3 => true;
321321
yield 4 => $this;
322-
}));
322+
}, 5));
323323
}
324324

325325
/**
@@ -332,9 +332,9 @@ protected function getLazyContextService()
332332
*/
333333
protected function getLazyContextIgnoreInvalidRefService()
334334
{
335-
return $this->services['lazy_context_ignore_invalid_ref'] = new \LazyContext(new RewindableGenerator(function() {
335+
return $this->services['lazy_context_ignore_invalid_ref'] = new \LazyContext(new RewindableGenerator(function () {
336336
yield 0 => ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->get('foo.baz')) && false ?: '_'};
337-
}));
337+
}, 1));
338338
}
339339

340340
/**

0 commit comments

Comments
 (0)