Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6ba9684

Browse files
committed
feature #20953 [DI][EventDispatcher] Add & wire closure-proxy argument type (nicolas-grekas)
This PR was merged into the 3.3-dev branch. Discussion ---------- [DI][EventDispatcher] Add & wire closure-proxy argument type | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #12521, #12007, #20039 | License | MIT | Doc PR | - By resolving event subscribers at compile time, then wrapping listeners in a closure, we get laziness almost for free. This should solve/replaced all the above linked issues. (WIP because tests are missing) Commits ------- ecdf857 [DI][EventDispatcher] Add & wire closure-proxy argument type
2 parents e4c95ed + ecdf857 commit 6ba9684

25 files changed

+672
-60
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Argument;
13+
14+
use Symfony\Component\DependencyInjection\ContainerInterface;
15+
use Symfony\Component\DependencyInjection\Reference;
16+
17+
/**
18+
* @author Nicolas Grekas <[email protected]>
19+
*/
20+
class ClosureProxyArgument implements ArgumentInterface
21+
{
22+
private $reference;
23+
private $method;
24+
25+
public function __construct($id, $method, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE)
26+
{
27+
$this->reference = new Reference($id, $invalidBehavior);
28+
$this->method = $method;
29+
}
30+
31+
/**
32+
* {@inheritdoc}
33+
*/
34+
public function getValues()
35+
{
36+
return array($this->reference, $this->method);
37+
}
38+
39+
/**
40+
* {@inheritdoc}
41+
*/
42+
public function setValues(array $values)
43+
{
44+
list($this->reference, $this->method) = $values;
45+
}
46+
}

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\DependencyInjection;
1313

14+
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1415
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
1516
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
1617
use Symfony\Component\DependencyInjection\Compiler\Compiler;
@@ -976,6 +977,31 @@ public function resolveServices($value)
976977
yield $k => $this->resolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($v)));
977978
}
978979
});
980+
} elseif ($value instanceof ClosureProxyArgument) {
981+
$parameterBag = $this->getParameterBag();
982+
list($reference, $method) = $value->getValues();
983+
if ('service_container' === $id = (string) $reference) {
984+
$class = parent::class;
985+
} elseif (!$this->hasDefinition($id) && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $reference->getInvalidBehavior()) {
986+
return null;
987+
} else {
988+
$class = $parameterBag->resolveValue($this->findDefinition($id)->getClass());
989+
}
990+
if (!method_exists($class, $method = $parameterBag->resolveValue($method))) {
991+
throw new InvalidArgumentException(sprintf('Cannot create closure-proxy for service "%s": method "%s::%s" does not exist.', $id, $class, $method));
992+
}
993+
$r = new \ReflectionMethod($class, $method);
994+
if (!$r->isPublic()) {
995+
throw new RuntimeException(sprintf('Cannot create closure-proxy for service "%s": method "%s::%s" must be public.', $id, $class, $method));
996+
}
997+
foreach ($r->getParameters() as $p) {
998+
if ($p->isPassedByReference()) {
999+
throw new RuntimeException(sprintf('Cannot create closure-proxy for service "%s": parameter "$%s" of method "%s::%s" must not be passed by reference.', $id, $p->name, $class, $method));
1000+
}
1001+
}
1002+
$value = function () use ($id, $method) {
1003+
return call_user_func_array(array($this->get($id), $method), func_get_args());
1004+
};
9791005
} elseif ($value instanceof Reference) {
9801006
$value = $this->get((string) $value, $value->getInvalidBehavior());
9811007
} elseif ($value instanceof Definition) {

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

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Dumper;
1313

14+
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1415
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
1516
use Symfony\Component\DependencyInjection\Variable;
1617
use Symfony\Component\DependencyInjection\Definition;
@@ -62,6 +63,8 @@ class PhpDumper extends Dumper
6263
private $docStar;
6364
private $serviceIdToMethodNameMap;
6465
private $usedMethodNames;
66+
private $classResources = array();
67+
private $baseClass;
6568

6669
/**
6770
* @var \Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface
@@ -117,7 +120,9 @@ public function dump(array $options = array())
117120
'debug' => true,
118121
), $options);
119122

123+
$this->classResources = array();
120124
$this->initializeMethodNamesMap($options['base_class']);
125+
$this->baseClass = $options['base_class'];
121126

122127
$this->docStar = $options['debug'] ? '*' : '';
123128

@@ -164,6 +169,11 @@ public function dump(array $options = array())
164169
;
165170
$this->targetDirRegex = null;
166171

172+
foreach ($this->classResources as $r) {
173+
$this->container->addClassResource($r);
174+
}
175+
$this->classResources = array();
176+
167177
$unusedEnvs = array();
168178
foreach ($this->container->getEnvCounters() as $env => $use) {
169179
if (!$use) {
@@ -1418,6 +1428,32 @@ private function dumpValue($value, $interpolate = true)
14181428
}
14191429

14201430
return sprintf('new %s(%s)', $this->dumpLiteralClass($this->dumpValue($class)), implode(', ', $arguments));
1431+
} elseif ($value instanceof ClosureProxyArgument) {
1432+
list($reference, $method) = $value->getValues();
1433+
$method = substr($this->dumpLiteralClass($this->dumpValue($method)), 1);
1434+
1435+
if ('service_container' === (string) $reference) {
1436+
$class = $this->baseClass;
1437+
} elseif (!$this->container->hasDefinition((string) $reference) && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $reference->getInvalidBehavior()) {
1438+
return 'null';
1439+
} else {
1440+
$class = substr($this->dumpLiteralClass($this->dumpValue($this->container->findDefinition((string) $reference)->getClass())), 1);
1441+
}
1442+
if (false !== strpos($class, '$') || false !== strpos($method, '$')) {
1443+
throw new RuntimeException(sprintf('Cannot dump definition for service "%s": dynamic class names or methods, and closure-proxies are incompatible with each other.', $reference));
1444+
}
1445+
if (!method_exists($class, $method)) {
1446+
throw new InvalidArgumentException(sprintf('Cannot create closure-proxy for service "%s": method "%s::%s" does not exist.', $reference, $class, $method));
1447+
}
1448+
if (!isset($this->classResources[$class])) {
1449+
$this->classResources[$class] = new \ReflectionClass($class);
1450+
}
1451+
$r = $this->classResources[$class]->getMethod($method);
1452+
if (!$r->isPublic()) {
1453+
throw new InvalidArgumentException(sprintf('Cannot create closure-proxy for service "%s": method "%s::%s" must be public.', $reference, $class, $method));
1454+
}
1455+
1456+
return sprintf("/** @closure-proxy %s::%s */ function %s {\n return %s->%s;\n }", $class, $method, $this->generateSignature($r), $this->dumpValue($reference), $this->generateCall($r));
14211457
} elseif ($value instanceof Variable) {
14221458
return '$'.$value;
14231459
} elseif ($value instanceof Reference) {
@@ -1674,4 +1710,93 @@ private function doExport($value)
16741710

16751711
return $export;
16761712
}
1713+
1714+
private function generateSignature(\ReflectionFunctionAbstract $r)
1715+
{
1716+
$signature = array();
1717+
1718+
foreach ($r->getParameters() as $p) {
1719+
$k = '$'.$p->name;
1720+
if (method_exists($p, 'isVariadic') && $p->isVariadic()) {
1721+
$k = '...'.$k;
1722+
}
1723+
if ($p->isPassedByReference()) {
1724+
$k = '&'.$k;
1725+
}
1726+
if (method_exists($p, 'getType')) {
1727+
$type = $p->getType();
1728+
} elseif (preg_match('/^(?:[^ ]++ ){4}([a-zA-Z_\x7F-\xFF][^ ]++)/', $p, $type)) {
1729+
$type = $type[1];
1730+
}
1731+
if ($type && $type = $this->generateTypeHint($type, $r)) {
1732+
$k = $type.' '.$k;
1733+
}
1734+
if ($type && $p->allowsNull()) {
1735+
$k = '?'.$k;
1736+
}
1737+
1738+
try {
1739+
$k .= ' = '.$this->dumpValue($p->getDefaultValue(), false);
1740+
if ($type && $p->allowsNull() && null === $p->getDefaultValue()) {
1741+
$k = substr($k, 1);
1742+
}
1743+
} catch (\ReflectionException $e) {
1744+
if ($type && $p->allowsNull() && !class_exists('ReflectionNamedType', false)) {
1745+
$k .= ' = null';
1746+
$k = substr($k, 1);
1747+
}
1748+
}
1749+
1750+
$signature[] = $k;
1751+
}
1752+
1753+
return ($r->returnsReference() ? '&(' : '(').implode(', ', $signature).')';
1754+
}
1755+
1756+
private function generateCall(\ReflectionFunctionAbstract $r)
1757+
{
1758+
$call = array();
1759+
1760+
foreach ($r->getParameters() as $p) {
1761+
$k = '$'.$p->name;
1762+
if (method_exists($p, 'isVariadic') && $p->isVariadic()) {
1763+
$k = '...'.$k;
1764+
}
1765+
1766+
$call[] = $k;
1767+
}
1768+
1769+
return ($r->isClosure() ? '' : $r->name).'('.implode(', ', $call).')';
1770+
}
1771+
1772+
private function generateTypeHint($type, \ReflectionFunctionAbstract $r)
1773+
{
1774+
if (is_string($type)) {
1775+
$name = $type;
1776+
1777+
if ('callable' === $name || 'array' === $name) {
1778+
return $name;
1779+
}
1780+
} else {
1781+
$name = $type instanceof \ReflectionNamedType ? $type->getName() : $type->__toString();
1782+
1783+
if ($type->isBuiltin()) {
1784+
return $name;
1785+
}
1786+
}
1787+
$lcName = strtolower($name);
1788+
1789+
if ('self' !== $lcName && 'parent' !== $lcName) {
1790+
return '\\'.$name;
1791+
}
1792+
if (!$r instanceof \ReflectionMethod) {
1793+
return;
1794+
}
1795+
if ('self' === $lcName) {
1796+
return '\\'.$r->getDeclaringClass()->name;
1797+
}
1798+
if ($parent = $r->getDeclaringClass()->getParentClass()) {
1799+
return '\\'.$parent->name;
1800+
}
1801+
}
16771802
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Dumper;
1313

14+
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1415
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
1516
use Symfony\Component\DependencyInjection\ContainerInterface;
1617
use Symfony\Component\DependencyInjection\Parameter;
@@ -287,6 +288,11 @@ private function convertParameters(array $parameters, $type, \DOMElement $parent
287288
} elseif ($value instanceof IteratorArgument) {
288289
$element->setAttribute('type', 'iterator');
289290
$this->convertParameters($value->getValues(), $type, $element, 'key');
291+
} elseif ($value instanceof ClosureProxyArgument) {
292+
list($reference, $method) = $value->getValues();
293+
$element->setAttribute('type', 'closure-proxy');
294+
$element->setAttribute('id', (string) $reference);
295+
$element->setAttribute('method', $method);
290296
} elseif ($value instanceof Reference) {
291297
$element->setAttribute('type', 'service');
292298
$element->setAttribute('id', (string) $value);

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Yaml\Dumper as YmlDumper;
1515
use Symfony\Component\DependencyInjection\Alias;
16+
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1617
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
1718
use Symfony\Component\DependencyInjection\ContainerInterface;
1819
use Symfony\Component\DependencyInjection\Definition;
@@ -248,6 +249,8 @@ private function dumpValue($value)
248249
{
249250
if ($value instanceof IteratorArgument) {
250251
$value = array('=iterator' => $value->getValues());
252+
} elseif ($value instanceof ClosureProxyArgument) {
253+
$value = array('=closure_proxy' => $value->getValues());
251254
}
252255

253256
if (is_array($value)) {

src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\Config\Util\XmlUtils;
1616
use Symfony\Component\DependencyInjection\ContainerInterface;
1717
use Symfony\Component\DependencyInjection\Alias;
18+
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1819
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
1920
use Symfony\Component\DependencyInjection\Definition;
2021
use Symfony\Component\DependencyInjection\ChildDefinition;
@@ -378,21 +379,24 @@ private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true)
378379
}
379380
}
380381

382+
$onInvalid = $arg->getAttribute('on-invalid');
383+
$invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
384+
if ('ignore' == $onInvalid) {
385+
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
386+
} elseif ('null' == $onInvalid) {
387+
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
388+
}
389+
381390
switch ($arg->getAttribute('type')) {
382391
case 'service':
383-
$onInvalid = $arg->getAttribute('on-invalid');
384-
$invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
385-
if ('ignore' == $onInvalid) {
386-
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
387-
} elseif ('null' == $onInvalid) {
388-
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
389-
}
390-
391392
$arguments[$key] = new Reference($arg->getAttribute('id'), $invalidBehavior);
392393
break;
393394
case 'expression':
394395
$arguments[$key] = new Expression($arg->nodeValue);
395396
break;
397+
case 'closure-proxy':
398+
$arguments[$key] = new ClosureProxyArgument($arg->getAttribute('id'), $arg->getAttribute('method'), $invalidBehavior);
399+
break;
396400
case 'collection':
397401
$arguments[$key] = $this->getArgumentsAsPhp($arg, $name, false);
398402
break;

src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\DependencyInjection\Loader;
1313

1414
use Symfony\Component\DependencyInjection\Alias;
15+
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1516
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
1617
use Symfony\Component\DependencyInjection\ChildDefinition;
1718
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -460,10 +461,28 @@ private function resolveServices($value)
460461
if (1 !== count($value)) {
461462
throw new InvalidArgumentException('Arguments typed "=iterator" must have no sibling keys.');
462463
}
463-
if (!is_array($value['=iterator'])) {
464+
if (!is_array($value = $value['=iterator'])) {
464465
throw new InvalidArgumentException('Arguments typed "=iterator" must be arrays.');
465466
}
466-
$value = new IteratorArgument(array_map(array($this, 'resolveServices'), $value['=iterator']));
467+
$value = new IteratorArgument(array_map(array($this, 'resolveServices'), $value));
468+
} elseif (array_key_exists('=closure_proxy', $value)) {
469+
if (1 !== count($value)) {
470+
throw new InvalidArgumentException('Arguments typed "=closure_proxy" must have no sibling keys.');
471+
}
472+
if (!is_array($value = $value['=closure_proxy']) || array(0, 1) !== array_keys($value)) {
473+
throw new InvalidArgumentException('Arguments typed "=closure_proxy" must be arrays of [@service, method].');
474+
}
475+
if (!is_string($value[0]) || !is_string($value[1]) || 0 !== strpos($value[0], '@') || 0 === strpos($value[0], '@@')) {
476+
throw new InvalidArgumentException('Arguments typed "=closure_proxy" must be arrays of [@service, method].');
477+
}
478+
if (0 === strpos($value[0], '@?')) {
479+
$value[0] = substr($value[0], 2);
480+
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
481+
} else {
482+
$value[0] = substr($value[0], 1);
483+
$invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
484+
}
485+
$value = new ClosureProxyArgument($value[0], $value[1], $invalidBehavior);
467486
} else {
468487
$value = array_map(array($this, 'resolveServices'), $value);
469488
}

src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@
164164
<xsd:attribute name="index" type="xsd:integer" />
165165
<xsd:attribute name="on-invalid" type="invalid_sequence" />
166166
<xsd:attribute name="strict" type="boolean" />
167+
<xsd:attribute name="method" type="xsd:string" />
167168
</xsd:complexType>
168169

169170
<xsd:complexType name="call" mixed="true">
@@ -190,6 +191,7 @@
190191
<xsd:enumeration value="string" />
191192
<xsd:enumeration value="constant" />
192193
<xsd:enumeration value="iterator" />
194+
<xsd:enumeration value="closure-proxy" />
193195
</xsd:restriction>
194196
</xsd:simpleType>
195197

0 commit comments

Comments
 (0)