Skip to content

Commit 463e5f2

Browse files
[DI][EventDispatcher] Add & wire closure-proxy argument type
1 parent e98c068 commit 463e5f2

File tree

12 files changed

+258
-51
lines changed

12 files changed

+258
-51
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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\Argument;
13+
14+
use Symfony\Component\DependencyInjection\Reference;
15+
16+
/**
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
class ClosureProxyArgument
20+
{
21+
private $reference;
22+
private $method;
23+
24+
public function __construct(Reference $reference, $method)
25+
{
26+
$this->reference = $reference;
27+
$this->method = $method;
28+
}
29+
30+
/**
31+
* {@inheritdoc}
32+
*/
33+
public function getValues()
34+
{
35+
return array($this->reference, $this->method);
36+
}
37+
38+
/**
39+
* {@inheritdoc}
40+
*/
41+
public function setValues(array $values)
42+
{
43+
list($this->reference, $this->method) = $values;
44+
}
45+
}

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

+23-1
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\Compiler\Compiler;
1516
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1617
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
@@ -96,7 +97,7 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
9697
private $envPlaceholders = array();
9798

9899
/**
99-
* @var int[] A map of env vars to their resolution counter.
100+
* @var int[] A map of env vars to their resolution counter
100101
*/
101102
private $envCounters = array();
102103

@@ -961,6 +962,27 @@ public function resolveServices($value)
961962
foreach ($value as $k => $v) {
962963
$value[$k] = $this->resolveServices($v);
963964
}
965+
} elseif ($value instanceof ClosureProxyArgument) {
966+
list($reference, $method) = $value->getValues();
967+
if ('service_container' === $id = (string) $reference) {
968+
$class = parent::class;
969+
} elseif (!$this->hasDefinition($id) && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $reference->getInvalidBehavior()) {
970+
return null;
971+
} else {
972+
$class = $this->findDefinition($id)->getClass();
973+
}
974+
$r = new \ReflectionMethod($class, $method);
975+
if (!$r->isPublic()) {
976+
throw new InvalidArgumentException(sprintf('Cannot create closure-proxy for non-public method "%s" of the "%s".', $method, $id));
977+
}
978+
foreach ($r->getParameters() as $p) {
979+
if ($p->isPassedByReference()) {
980+
throw new RuntimeException(sprintf('ContainerBuilder cannot create closure-proxy at runtime for method "%s::%s" because parameter "$%s" is passed by reference.', $class, $method, $p->name));
981+
}
982+
}
983+
$value = function () use ($id, $method) {
984+
return call_user_func_array(array($this->get($id), $method), func_get_args());
985+
};
964986
} elseif ($value instanceof Reference) {
965987
$value = $this->get((string) $value, $value->getInvalidBehavior());
966988
} elseif ($value instanceof Definition) {

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

+122
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\Variable;
1516
use Symfony\Component\DependencyInjection\Definition;
1617
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -61,6 +62,8 @@ class PhpDumper extends Dumper
6162
private $docStar;
6263
private $serviceIdToMethodNameMap;
6364
private $usedMethodNames;
65+
private $classResources = array();
66+
private $baseClass;
6467

6568
/**
6669
* @var \Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface
@@ -112,7 +115,9 @@ public function dump(array $options = array())
112115
'debug' => true,
113116
), $options);
114117

118+
$this->classResources = array();
115119
$this->initializeMethodNamesMap($options['base_class']);
120+
$this->baseClass = $options['base_class'];
116121

117122
$this->docStar = $options['debug'] ? '*' : '';
118123

@@ -159,6 +164,11 @@ public function dump(array $options = array())
159164
;
160165
$this->targetDirRegex = null;
161166

167+
foreach ($this->classResources as $r) {
168+
$this->container->addClassResource($r);
169+
}
170+
$this->classResources = array();
171+
162172
$unusedEnvs = array();
163173
foreach ($this->container->getEnvCounters() as $env => $use) {
164174
if (!$use) {
@@ -1398,6 +1408,25 @@ private function dumpValue($value, $interpolate = true)
13981408
}
13991409

14001410
return sprintf('new %s(%s)', $this->dumpLiteralClass($this->dumpValue($class)), implode(', ', $arguments));
1411+
} elseif ($value instanceof ClosureProxyArgument) {
1412+
list($reference, $method) = $value->getValues();
1413+
1414+
if ('service_container' === (string) $reference) {
1415+
$class = $this->baseClass;
1416+
} elseif (!$this->hasDefinition((string) $reference) && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $reference->getInvalidBehavior()) {
1417+
return 'null';
1418+
} else {
1419+
$class = $this->container->findDefinition((string) $reference)->getClass();
1420+
}
1421+
if (!isset($this->classResources[$class])) {
1422+
$this->classResources[$class] = new \ReflectionClass($class);
1423+
}
1424+
$r = $this->classResources[$class]->getMethod($method);
1425+
if (!$r->isPublic()) {
1426+
throw new InvalidArgumentException(sprintf('Cannot create closure-proxy for non-public method "%s" of the "%s".', $method, $reference));
1427+
}
1428+
1429+
return sprintf('/** @closure-proxy %s::%s */ function %s { return %s->%s; }', $class, $method, $this->generateSignature($r), $this->dumpValue($reference), $this->generateCall($r));
14011430
} elseif ($value instanceof Variable) {
14021431
return '$'.$value;
14031432
} elseif ($value instanceof Reference) {
@@ -1654,4 +1683,97 @@ private function doExport($value)
16541683

16551684
return $export;
16561685
}
1686+
1687+
private function generateSignature(\ReflectionFunctionAbstract $r)
1688+
{
1689+
$signature = array();
1690+
1691+
foreach ($r->getParameters() as $p) {
1692+
$k = '$'.$p->name;
1693+
if (method_exists($p, 'isVariadic') && $p->isVariadic()) {
1694+
$k = '...'.$k;
1695+
}
1696+
if ($p->isPassedByReference()) {
1697+
$k = '&'.$k;
1698+
}
1699+
if (method_exists($p, 'getType')) {
1700+
if ($type = $p->getType()) {
1701+
$k = $this->generateTypeHint($type, $r).' '.$k;
1702+
}
1703+
} elseif (preg_match('/^(?:[^ ]++ ){4}([a-zA-Z_\x7F-\xFF][^ ]++)/', $p, $type)) {
1704+
$k = $type[1].' '.$k;
1705+
}
1706+
if ($type && $p->allowsNull()) {
1707+
$k = '?'.$k;
1708+
}
1709+
1710+
try {
1711+
$k .= ' = '.$this->dumpValue($p->getDefaultValue(), false);
1712+
if ($type && $p->allowsNull() && null === $p->getDefaultValue()) {
1713+
$k = substr($k, 1);
1714+
}
1715+
} catch (\ReflectionException $e) {
1716+
if ($type && $p->allowsNull() && !class_exists('ReflectionNamedType', false)) {
1717+
$k .= ' = null';
1718+
$k = substr($k, 1);
1719+
}
1720+
}
1721+
1722+
$signature[] = $k;
1723+
}
1724+
1725+
$signature = ($r->returnsReference() ? '&(' : '(').implode(', ', $signature).')';
1726+
1727+
if (method_exists($r, 'getReturnType') && $type = $r->getReturnType()) {
1728+
$signature .= ': '.($type->allowsNull() ? '?' : '').$this->generateTypeHint($type, $r);
1729+
}
1730+
1731+
return $signature;
1732+
}
1733+
1734+
private function generateCall(\ReflectionFunctionAbstract $r)
1735+
{
1736+
$call = array();
1737+
1738+
foreach ($r->getParameters() as $p) {
1739+
$k = '$'.$p->name;
1740+
if (method_exists($p, 'isVariadic') && $p->isVariadic()) {
1741+
$k = '...'.$k;
1742+
}
1743+
1744+
$call[] = $k;
1745+
}
1746+
1747+
return ($r->isClosure() ? '' : $r->name).'('.implode(', ', $call).')';
1748+
}
1749+
1750+
private function generateTypeHint($type, \ReflectionFunctionAbstract $r)
1751+
{
1752+
$type = $type instanceof \ReflectionNamedType ? $type->getName() : $type->__toString();
1753+
1754+
switch (strtolower($type)) {
1755+
case 'parent':
1756+
if ($r instanceof \ReflectionMethod) {
1757+
$type = $r->getDeclaringClass()->getParentClass()->name;
1758+
}
1759+
break;
1760+
1761+
case 'self':
1762+
if ($r instanceof \ReflectionMethod) {
1763+
$type = $r->getDeclaringClass()->name;
1764+
}
1765+
break;
1766+
1767+
case 'array':
1768+
case 'bool':
1769+
case 'float':
1770+
case 'int':
1771+
case 'iterable':
1772+
case 'string':
1773+
case 'void':
1774+
return $type;
1775+
}
1776+
1777+
return '\\'.$type;
1778+
}
16571779
}

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

+6
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\ContainerInterface;
1516
use Symfony\Component\DependencyInjection\Parameter;
1617
use Symfony\Component\DependencyInjection\Reference;
@@ -283,6 +284,11 @@ private function convertParameters(array $parameters, $type, \DOMElement $parent
283284
if (is_array($value)) {
284285
$element->setAttribute('type', 'collection');
285286
$this->convertParameters($value, $type, $element, 'key');
287+
} elseif ($value instanceof ClosureProxyArgument) {
288+
list($reference, $method) = $value->getValues();
289+
$element->setAttribute('type', 'closure-proxy');
290+
$element->setAttribute('id', (string) $reference);
291+
$element->setAttribute('method', $method);
286292
} elseif ($value instanceof Reference) {
287293
$element->setAttribute('type', 'service');
288294
$element->setAttribute('id', (string) $value);

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

+5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\DependencyInjection\Dumper;
1313

1414
use Symfony\Component\Yaml\Dumper as YmlDumper;
15+
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1516
use Symfony\Component\DependencyInjection\Alias;
1617
use Symfony\Component\DependencyInjection\ContainerInterface;
1718
use Symfony\Component\DependencyInjection\Definition;
@@ -252,6 +253,10 @@ private function dumpValue($value)
252253
}
253254

254255
return $code;
256+
} elseif ($value instanceof ClosureProxyArgument) {
257+
list($reference, $method) = $value->getValues();
258+
259+
return array($this->getServiceCall((string) $reference, $reference), $method);
255260
} elseif ($value instanceof Reference) {
256261
return $this->getServiceCall((string) $value, $value);
257262
} elseif ($value instanceof Parameter) {

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

+4
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\Definition;
1920
use Symfony\Component\DependencyInjection\ChildDefinition;
2021
use Symfony\Component\DependencyInjection\Reference;
@@ -398,6 +399,9 @@ private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true)
398399
case 'expression':
399400
$arguments[$key] = new Expression($arg->nodeValue);
400401
break;
402+
case 'closure-proxy':
403+
$arguments[$key] = new ClosureProxyArgument(new Reference($arg->getAttribute('id')), $arg->getAttribute('method'));
404+
break;
401405
case 'collection':
402406
$arguments[$key] = $this->getArgumentsAsPhp($arg, $name, false);
403407
break;

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

+2
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="xsd:string" />
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">
@@ -189,6 +190,7 @@
189190
<xsd:enumeration value="expression" />
190191
<xsd:enumeration value="string" />
191192
<xsd:enumeration value="constant" />
193+
<xsd:enumeration value="closure-proxy" />
192194
</xsd:restriction>
193195
</xsd:simpleType>
194196

src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,13 @@ public function __construct($listener, $name, Stopwatch $stopwatch, EventDispatc
4646
$this->name = is_object($listener[0]) ? get_class($listener[0]) : $listener[0];
4747
$this->pretty = $this->name.'::'.$listener[1];
4848
} elseif ($listener instanceof \Closure) {
49-
$this->pretty = $this->name = 'closure';
49+
$r = new \ReflectionFunction($listener);
50+
if (preg_match('#^/\*\* @closure-proxy ([^: ]++)::([^: ]++) \*/$#', $r->getDocComment(), $m)) {
51+
$this->name = $m[1];
52+
$this->pretty = $m[1].'::'.$m[2];
53+
} else {
54+
$this->pretty = $this->name = 'closure';
55+
}
5056
} elseif (is_string($listener)) {
5157
$this->pretty = $this->name = $listener;
5258
} else {

0 commit comments

Comments
 (0)