diff --git a/src/Symfony/Component/DependencyInjection/Argument/ClosureProxyArgument.php b/src/Symfony/Component/DependencyInjection/Argument/ClosureProxyArgument.php
new file mode 100644
index 0000000000000..78d5bd5cb4afd
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Argument/ClosureProxyArgument.php
@@ -0,0 +1,46 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\DependencyInjection\Argument;
+
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\Reference;
+
+/**
+ * @author Nicolas Grekas
+ */
+class ClosureProxyArgument implements ArgumentInterface
+{
+ private $reference;
+ private $method;
+
+ public function __construct($id, $method, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE)
+ {
+ $this->reference = new Reference($id, $invalidBehavior);
+ $this->method = $method;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues()
+ {
+ return array($this->reference, $this->method);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setValues(array $values)
+ {
+ list($this->reference, $this->method) = $values;
+ }
+}
diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php
index 31b38ae33aaf8..981adf6f46231 100644
--- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php
+++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\DependencyInjection;
+use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Compiler\Compiler;
@@ -976,6 +977,31 @@ public function resolveServices($value)
yield $k => $this->resolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($v)));
}
});
+ } elseif ($value instanceof ClosureProxyArgument) {
+ $parameterBag = $this->getParameterBag();
+ list($reference, $method) = $value->getValues();
+ if ('service_container' === $id = (string) $reference) {
+ $class = parent::class;
+ } elseif (!$this->hasDefinition($id) && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $reference->getInvalidBehavior()) {
+ return null;
+ } else {
+ $class = $parameterBag->resolveValue($this->findDefinition($id)->getClass());
+ }
+ if (!method_exists($class, $method = $parameterBag->resolveValue($method))) {
+ throw new InvalidArgumentException(sprintf('Cannot create closure-proxy for service "%s": method "%s::%s" does not exist.', $id, $class, $method));
+ }
+ $r = new \ReflectionMethod($class, $method);
+ if (!$r->isPublic()) {
+ throw new RuntimeException(sprintf('Cannot create closure-proxy for service "%s": method "%s::%s" must be public.', $id, $class, $method));
+ }
+ foreach ($r->getParameters() as $p) {
+ if ($p->isPassedByReference()) {
+ 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));
+ }
+ }
+ $value = function () use ($id, $method) {
+ return call_user_func_array(array($this->get($id), $method), func_get_args());
+ };
} elseif ($value instanceof Reference) {
$value = $this->get((string) $value, $value->getInvalidBehavior());
} elseif ($value instanceof Definition) {
diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
index 7b579e161901e..ad6e8fdd24b2d 100644
--- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
+++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\DependencyInjection\Dumper;
+use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Variable;
use Symfony\Component\DependencyInjection\Definition;
@@ -62,6 +63,8 @@ class PhpDumper extends Dumper
private $docStar;
private $serviceIdToMethodNameMap;
private $usedMethodNames;
+ private $classResources = array();
+ private $baseClass;
/**
* @var \Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface
@@ -117,7 +120,9 @@ public function dump(array $options = array())
'debug' => true,
), $options);
+ $this->classResources = array();
$this->initializeMethodNamesMap($options['base_class']);
+ $this->baseClass = $options['base_class'];
$this->docStar = $options['debug'] ? '*' : '';
@@ -164,6 +169,11 @@ public function dump(array $options = array())
;
$this->targetDirRegex = null;
+ foreach ($this->classResources as $r) {
+ $this->container->addClassResource($r);
+ }
+ $this->classResources = array();
+
$unusedEnvs = array();
foreach ($this->container->getEnvCounters() as $env => $use) {
if (!$use) {
@@ -1418,6 +1428,32 @@ private function dumpValue($value, $interpolate = true)
}
return sprintf('new %s(%s)', $this->dumpLiteralClass($this->dumpValue($class)), implode(', ', $arguments));
+ } elseif ($value instanceof ClosureProxyArgument) {
+ list($reference, $method) = $value->getValues();
+ $method = substr($this->dumpLiteralClass($this->dumpValue($method)), 1);
+
+ if ('service_container' === (string) $reference) {
+ $class = $this->baseClass;
+ } elseif (!$this->container->hasDefinition((string) $reference) && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE !== $reference->getInvalidBehavior()) {
+ return 'null';
+ } else {
+ $class = substr($this->dumpLiteralClass($this->dumpValue($this->container->findDefinition((string) $reference)->getClass())), 1);
+ }
+ if (false !== strpos($class, '$') || false !== strpos($method, '$')) {
+ throw new RuntimeException(sprintf('Cannot dump definition for service "%s": dynamic class names or methods, and closure-proxies are incompatible with each other.', $reference));
+ }
+ if (!method_exists($class, $method)) {
+ throw new InvalidArgumentException(sprintf('Cannot create closure-proxy for service "%s": method "%s::%s" does not exist.', $reference, $class, $method));
+ }
+ if (!isset($this->classResources[$class])) {
+ $this->classResources[$class] = new \ReflectionClass($class);
+ }
+ $r = $this->classResources[$class]->getMethod($method);
+ if (!$r->isPublic()) {
+ throw new InvalidArgumentException(sprintf('Cannot create closure-proxy for service "%s": method "%s::%s" must be public.', $reference, $class, $method));
+ }
+
+ return sprintf("/** @closure-proxy %s::%s */ function %s {\n return %s->%s;\n }", $class, $method, $this->generateSignature($r), $this->dumpValue($reference), $this->generateCall($r));
} elseif ($value instanceof Variable) {
return '$'.$value;
} elseif ($value instanceof Reference) {
@@ -1674,4 +1710,93 @@ private function doExport($value)
return $export;
}
+
+ private function generateSignature(\ReflectionFunctionAbstract $r)
+ {
+ $signature = array();
+
+ foreach ($r->getParameters() as $p) {
+ $k = '$'.$p->name;
+ if (method_exists($p, 'isVariadic') && $p->isVariadic()) {
+ $k = '...'.$k;
+ }
+ if ($p->isPassedByReference()) {
+ $k = '&'.$k;
+ }
+ if (method_exists($p, 'getType')) {
+ $type = $p->getType();
+ } elseif (preg_match('/^(?:[^ ]++ ){4}([a-zA-Z_\x7F-\xFF][^ ]++)/', $p, $type)) {
+ $type = $type[1];
+ }
+ if ($type && $type = $this->generateTypeHint($type, $r)) {
+ $k = $type.' '.$k;
+ }
+ if ($type && $p->allowsNull()) {
+ $k = '?'.$k;
+ }
+
+ try {
+ $k .= ' = '.$this->dumpValue($p->getDefaultValue(), false);
+ if ($type && $p->allowsNull() && null === $p->getDefaultValue()) {
+ $k = substr($k, 1);
+ }
+ } catch (\ReflectionException $e) {
+ if ($type && $p->allowsNull() && !class_exists('ReflectionNamedType', false)) {
+ $k .= ' = null';
+ $k = substr($k, 1);
+ }
+ }
+
+ $signature[] = $k;
+ }
+
+ return ($r->returnsReference() ? '&(' : '(').implode(', ', $signature).')';
+ }
+
+ private function generateCall(\ReflectionFunctionAbstract $r)
+ {
+ $call = array();
+
+ foreach ($r->getParameters() as $p) {
+ $k = '$'.$p->name;
+ if (method_exists($p, 'isVariadic') && $p->isVariadic()) {
+ $k = '...'.$k;
+ }
+
+ $call[] = $k;
+ }
+
+ return ($r->isClosure() ? '' : $r->name).'('.implode(', ', $call).')';
+ }
+
+ private function generateTypeHint($type, \ReflectionFunctionAbstract $r)
+ {
+ if (is_string($type)) {
+ $name = $type;
+
+ if ('callable' === $name || 'array' === $name) {
+ return $name;
+ }
+ } else {
+ $name = $type instanceof \ReflectionNamedType ? $type->getName() : $type->__toString();
+
+ if ($type->isBuiltin()) {
+ return $name;
+ }
+ }
+ $lcName = strtolower($name);
+
+ if ('self' !== $lcName && 'parent' !== $lcName) {
+ return '\\'.$name;
+ }
+ if (!$r instanceof \ReflectionMethod) {
+ return;
+ }
+ if ('self' === $lcName) {
+ return '\\'.$r->getDeclaringClass()->name;
+ }
+ if ($parent = $r->getDeclaringClass()->getParentClass()) {
+ return '\\'.$parent->name;
+ }
+ }
}
diff --git a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php
index 52351e5e6b258..e9060930b1f1a 100644
--- a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php
+++ b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\DependencyInjection\Dumper;
+use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Parameter;
@@ -287,6 +288,11 @@ private function convertParameters(array $parameters, $type, \DOMElement $parent
} elseif ($value instanceof IteratorArgument) {
$element->setAttribute('type', 'iterator');
$this->convertParameters($value->getValues(), $type, $element, 'key');
+ } elseif ($value instanceof ClosureProxyArgument) {
+ list($reference, $method) = $value->getValues();
+ $element->setAttribute('type', 'closure-proxy');
+ $element->setAttribute('id', (string) $reference);
+ $element->setAttribute('method', $method);
} elseif ($value instanceof Reference) {
$element->setAttribute('type', 'service');
$element->setAttribute('id', (string) $value);
diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php
index 401c179f3aa4f..b8d936658f118 100644
--- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php
+++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php
@@ -13,6 +13,7 @@
use Symfony\Component\Yaml\Dumper as YmlDumper;
use Symfony\Component\DependencyInjection\Alias;
+use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
@@ -248,6 +249,8 @@ private function dumpValue($value)
{
if ($value instanceof IteratorArgument) {
$value = array('=iterator' => $value->getValues());
+ } elseif ($value instanceof ClosureProxyArgument) {
+ $value = array('=closure_proxy' => $value->getValues());
}
if (is_array($value)) {
diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
index 6a9ffafcfb513..d90777a9cfb28 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
@@ -15,6 +15,7 @@
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Alias;
+use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ChildDefinition;
@@ -378,21 +379,24 @@ private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true)
}
}
+ $onInvalid = $arg->getAttribute('on-invalid');
+ $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
+ if ('ignore' == $onInvalid) {
+ $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
+ } elseif ('null' == $onInvalid) {
+ $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
+ }
+
switch ($arg->getAttribute('type')) {
case 'service':
- $onInvalid = $arg->getAttribute('on-invalid');
- $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
- if ('ignore' == $onInvalid) {
- $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
- } elseif ('null' == $onInvalid) {
- $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
- }
-
$arguments[$key] = new Reference($arg->getAttribute('id'), $invalidBehavior);
break;
case 'expression':
$arguments[$key] = new Expression($arg->nodeValue);
break;
+ case 'closure-proxy':
+ $arguments[$key] = new ClosureProxyArgument($arg->getAttribute('id'), $arg->getAttribute('method'), $invalidBehavior);
+ break;
case 'collection':
$arguments[$key] = $this->getArgumentsAsPhp($arg, $name, false);
break;
diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
index 7eec03a20f7f1..0cbe6f51299d3 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Alias;
+use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -460,10 +461,28 @@ private function resolveServices($value)
if (1 !== count($value)) {
throw new InvalidArgumentException('Arguments typed "=iterator" must have no sibling keys.');
}
- if (!is_array($value['=iterator'])) {
+ if (!is_array($value = $value['=iterator'])) {
throw new InvalidArgumentException('Arguments typed "=iterator" must be arrays.');
}
- $value = new IteratorArgument(array_map(array($this, 'resolveServices'), $value['=iterator']));
+ $value = new IteratorArgument(array_map(array($this, 'resolveServices'), $value));
+ } elseif (array_key_exists('=closure_proxy', $value)) {
+ if (1 !== count($value)) {
+ throw new InvalidArgumentException('Arguments typed "=closure_proxy" must have no sibling keys.');
+ }
+ if (!is_array($value = $value['=closure_proxy']) || array(0, 1) !== array_keys($value)) {
+ throw new InvalidArgumentException('Arguments typed "=closure_proxy" must be arrays of [@service, method].');
+ }
+ if (!is_string($value[0]) || !is_string($value[1]) || 0 !== strpos($value[0], '@') || 0 === strpos($value[0], '@@')) {
+ throw new InvalidArgumentException('Arguments typed "=closure_proxy" must be arrays of [@service, method].');
+ }
+ if (0 === strpos($value[0], '@?')) {
+ $value[0] = substr($value[0], 2);
+ $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
+ } else {
+ $value[0] = substr($value[0], 1);
+ $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
+ }
+ $value = new ClosureProxyArgument($value[0], $value[1], $invalidBehavior);
} else {
$value = array_map(array($this, 'resolveServices'), $value);
}
diff --git a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd
index 13d6532322ee2..1e99d80ffe219 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd
+++ b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd
@@ -164,6 +164,7 @@
+
@@ -190,6 +191,7 @@
+
diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
index c91a53473cf6b..3fa87ff9c9f8d 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php
@@ -16,6 +16,7 @@
use Symfony\Component\Config\Resource\ResourceInterface;
use Symfony\Component\DependencyInjection\Alias;
+use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\ChildDefinition;
@@ -868,6 +869,63 @@ public function testAutowiring()
$this->assertEquals('a', (string) $container->getDefinition('b')->getArgument(0));
}
+
+ public function testClosureProxy()
+ {
+ $container = new ContainerBuilder();
+
+ $container->register('foo', 'stdClass')
+ ->setProperty('foo', new ClosureProxyArgument('bar', 'c'))
+ ;
+ $container->register('bar', A::class);
+
+ $foo = $container->get('foo');
+
+ $this->assertInstanceOf('Closure', $foo->foo);
+ $this->assertSame(123, call_user_func($foo->foo));
+ }
+
+ public function testClosureProxyContainer()
+ {
+ $container = new ContainerBuilder();
+
+ $container->register('foo', 'stdClass')
+ ->setProperty('foo', new ClosureProxyArgument('service_container', 'get'))
+ ;
+
+ $foo = $container->get('foo');
+
+ $this->assertInstanceOf('Closure', $foo->foo);
+ $this->assertSame($foo, call_user_func($foo->foo, 'foo'));
+ }
+
+ public function testClosureProxyOnInvalidNull()
+ {
+ $container = new ContainerBuilder();
+
+ $container->register('foo', 'stdClass')
+ ->setProperty('foo', new ClosureProxyArgument('bar', 'c', ContainerInterface::NULL_ON_INVALID_REFERENCE))
+ ;
+
+ $foo = $container->get('foo');
+
+ $this->assertNull($foo->foo);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException
+ * @expectedExceptionMessage You have requested a non-existent service "bar".
+ */
+ public function testClosureProxyOnInvalidException()
+ {
+ $container = new ContainerBuilder();
+
+ $container->register('foo', 'stdClass')
+ ->setProperty('foo', new ClosureProxyArgument('bar', 'c'))
+ ;
+
+ $container->get('foo');
+ }
}
class FooClass
@@ -876,6 +934,10 @@ class FooClass
class A
{
+ public function c()
+ {
+ return 123;
+ }
}
class B
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php
index 231be9df7189d..9375129a4f390 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php
@@ -459,4 +459,31 @@ public function testLazyArgumentProvideGenerator()
}
}
}
+
+ public function testClosureProxy()
+ {
+ $container = include self::$fixturesPath.'/containers/container31.php';
+ $container->compile();
+ $dumper = new PhpDumper($container);
+
+ $dump = $dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_Closure_Proxy'));
+ $this->assertEquals(file_get_contents(self::$fixturesPath.'/php/services31.php'), $dumper->dump());
+ $res = $container->getResources();
+ $this->assertSame(realpath(self::$fixturesPath.'/containers/container31.php'), array_pop($res)->getResource());
+ }
+
+ /**
+ * @requires PHP 7.1
+ */
+ public function testClosureProxyPhp71()
+ {
+ $container = include self::$fixturesPath.'/containers/container32.php';
+ $container->compile();
+ $dumper = new PhpDumper($container);
+
+ $dump = $dumper->dump(array('class' => 'Symfony_DI_PhpDumper_Test_Closure_Proxy_Php71'));
+ $this->assertEquals(file_get_contents(self::$fixturesPath.'/php/services32.php'), $dumper->dump());
+ $res = $container->getResources();
+ $this->assertSame(realpath(self::$fixturesPath.'/containers/container32.php'), array_pop($res)->getResource());
+ }
}
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container31.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container31.php
new file mode 100644
index 0000000000000..e8493ad02cdf6
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container31.php
@@ -0,0 +1,37 @@
+register('foo', Foo::class);
+
+$container->register('bar', 'stdClass')
+ ->setProperty('foo', array(
+ new ClosureProxyArgument('foo', 'withNoArgs'),
+ new ClosureProxyArgument('foo', 'withArgs'),
+ new ClosureProxyArgument('foo', 'withRefs'),
+ ))
+;
+
+return $container;
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container32.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container32.php
new file mode 100644
index 0000000000000..00d5654a5b464
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container32.php
@@ -0,0 +1,37 @@
+register('foo', Foo::class);
+
+$container->register('bar', 'stdClass')
+ ->setProperty('foo', array(
+ new ClosureProxyArgument('foo', 'withVariadic'),
+ new ClosureProxyArgument('foo', 'withNullable'),
+ new ClosureProxyArgument('foo', 'withReturnType'),
+ ))
+;
+
+return $container;
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php
index 0d8f957765409..91e32b52fe815 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container9.php
@@ -2,6 +2,7 @@
require_once __DIR__.'/../includes/classes.php';
+use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -138,5 +139,9 @@
->register('lazy_context_ignore_invalid_ref', 'LazyContext')
->setArguments(array(new IteratorArgument(array(new Reference('foo.baz'), new Reference('invalid', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)))))
;
+$container
+ ->register('closure_proxy', 'BarClass')
+ ->setArguments(array(new ClosureProxyArgument('closure_proxy', 'getBaz')))
+;
return $container;
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot
index c83909d41e7a7..2c19aaf8bcff7 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/graphviz/services9.dot
@@ -28,6 +28,7 @@ digraph sc {
node_factory_service_simple [label="factory_service_simple\nBar\n", shape=record, fillcolor="#eeeeee", style="filled"];
node_lazy_context [label="lazy_context\nLazyContext\n", shape=record, fillcolor="#eeeeee", style="filled"];
node_lazy_context_ignore_invalid_ref [label="lazy_context_ignore_invalid_ref\nLazyContext\n", shape=record, fillcolor="#eeeeee", style="filled"];
+ node_closure_proxy [label="closure_proxy\nBarClass\n", shape=record, fillcolor="#eeeeee", style="filled"];
node_service_container [label="service_container\nSymfony\\Component\\DependencyInjection\\ContainerBuilder\n", shape=record, fillcolor="#9999ff", style="filled"];
node_foo2 [label="foo2\n\n", shape=record, fillcolor="#ff9999", style="filled"];
node_foo3 [label="foo3\n\n", shape=record, fillcolor="#ff9999", style="filled"];
@@ -50,4 +51,5 @@ digraph sc {
node_lazy_context -> node_service_container [label="" style="filled" color="#9999ff"];
node_lazy_context_ignore_invalid_ref -> node_foo_baz [label="" style="filled" color="#9999ff"];
node_lazy_context_ignore_invalid_ref -> node_invalid [label="" style="filled" color="#9999ff"];
+ node_closure_proxy -> node_closure_proxy [label="" style="filled" color="#9999ff"];
}
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services31.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services31.php
new file mode 100644
index 0000000000000..33e947bc2d308
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services31.php
@@ -0,0 +1,87 @@
+services = array();
+ $this->methodMap = array(
+ 'bar' => 'getBarService',
+ 'foo' => 'getFooService',
+ );
+
+ $this->aliases = array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function compile()
+ {
+ throw new LogicException('You cannot compile a dumped frozen container.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isFrozen()
+ {
+ return true;
+ }
+
+ /**
+ * Gets the 'bar' service.
+ *
+ * This service is shared.
+ * This method always returns the same instance of the service.
+ *
+ * @return \stdClass A stdClass instance
+ */
+ protected function getBarService()
+ {
+ $this->services['bar'] = $instance = new \stdClass();
+
+ $instance->foo = array(0 => /** @closure-proxy Symfony\Component\DependencyInjection\Tests\Fixtures\Container31\Foo::withNoArgs */ function () {
+ return $this->get('foo')->withNoArgs();
+ }, 1 => /** @closure-proxy Symfony\Component\DependencyInjection\Tests\Fixtures\Container31\Foo::withArgs */ function ($a, \Symfony\Component\DependencyInjection\Tests\Fixtures\Container31\Foo $b = NULL, $c = array(0 => 123)) {
+ return $this->get('foo')->withArgs($a, $b, $c);
+ }, 2 => /** @closure-proxy Symfony\Component\DependencyInjection\Tests\Fixtures\Container31\Foo::withRefs */ function &(&$a = NULL, &$b) {
+ return $this->get('foo')->withRefs($a, $b);
+ });
+
+ return $instance;
+ }
+
+ /**
+ * Gets the 'foo' service.
+ *
+ * This service is shared.
+ * This method always returns the same instance of the service.
+ *
+ * @return \Symfony\Component\DependencyInjection\Tests\Fixtures\Container31\Foo A Symfony\Component\DependencyInjection\Tests\Fixtures\Container31\Foo instance
+ */
+ protected function getFooService()
+ {
+ return $this->services['foo'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\Container31\Foo();
+ }
+}
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services32.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services32.php
new file mode 100644
index 0000000000000..1407ac60b6573
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services32.php
@@ -0,0 +1,87 @@
+services = array();
+ $this->methodMap = array(
+ 'bar' => 'getBarService',
+ 'foo' => 'getFooService',
+ );
+
+ $this->aliases = array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function compile()
+ {
+ throw new LogicException('You cannot compile a dumped frozen container.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isFrozen()
+ {
+ return true;
+ }
+
+ /**
+ * Gets the 'bar' service.
+ *
+ * This service is shared.
+ * This method always returns the same instance of the service.
+ *
+ * @return \stdClass A stdClass instance
+ */
+ protected function getBarService()
+ {
+ $this->services['bar'] = $instance = new \stdClass();
+
+ $instance->foo = array(0 => /** @closure-proxy Symfony\Component\DependencyInjection\Tests\Fixtures\Container32\Foo::withVariadic */ function ($a, &...$c) {
+ return $this->get('foo')->withVariadic($a, ...$c);
+ }, 1 => /** @closure-proxy Symfony\Component\DependencyInjection\Tests\Fixtures\Container32\Foo::withNullable */ function (?int $a) {
+ return $this->get('foo')->withNullable($a);
+ }, 2 => /** @closure-proxy Symfony\Component\DependencyInjection\Tests\Fixtures\Container32\Foo::withReturnType */ function () {
+ return $this->get('foo')->withReturnType();
+ });
+
+ return $instance;
+ }
+
+ /**
+ * Gets the 'foo' service.
+ *
+ * This service is shared.
+ * This method always returns the same instance of the service.
+ *
+ * @return \Symfony\Component\DependencyInjection\Tests\Fixtures\Container32\Foo A Symfony\Component\DependencyInjection\Tests\Fixtures\Container32\Foo instance
+ */
+ protected function getFooService()
+ {
+ return $this->services['foo'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\Container32\Foo();
+ }
+}
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php
index 263968641ae8a..a03826de9368c 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9.php
@@ -28,6 +28,7 @@ public function __construct()
$this->methodMap = array(
'bar' => 'getBarService',
'baz' => 'getBazService',
+ 'closure_proxy' => 'getClosureProxyService',
'configurator_service' => 'getConfiguratorServiceService',
'configurator_service_simple' => 'getConfiguratorServiceSimpleService',
'configured_service' => 'getConfiguredServiceService',
@@ -101,6 +102,21 @@ protected function getBazService()
return $instance;
}
+ /**
+ * Gets the 'closure_proxy' service.
+ *
+ * This service is shared.
+ * This method always returns the same instance of the service.
+ *
+ * @return \BarClass A BarClass instance
+ */
+ protected function getClosureProxyService()
+ {
+ return $this->services['closure_proxy'] = new \BarClass(/** @closure-proxy BarClass::getBaz */ function () {
+ return $this->get('closure_proxy')->getBaz();
+ });
+ }
+
/**
* Gets the 'configured_service' service.
*
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php
index 3c85f6a3aa1ec..d018c44d27ce0 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_compiled.php
@@ -30,6 +30,7 @@ public function __construct()
$this->methodMap = array(
'bar' => 'getBarService',
'baz' => 'getBazService',
+ 'closure_proxy' => 'getClosureProxyService',
'configured_service' => 'getConfiguredServiceService',
'configured_service_simple' => 'getConfiguredServiceSimpleService',
'decorator_service' => 'getDecoratorServiceService',
@@ -107,6 +108,21 @@ protected function getBazService()
return $instance;
}
+ /**
+ * Gets the 'closure_proxy' service.
+ *
+ * This service is shared.
+ * This method always returns the same instance of the service.
+ *
+ * @return \BarClass A BarClass instance
+ */
+ protected function getClosureProxyService()
+ {
+ return $this->services['closure_proxy'] = new \BarClass(/** @closure-proxy BarClass::getBaz */ function () {
+ return $this->get('closure_proxy')->getBaz();
+ });
+ }
+
/**
* Gets the 'configured_service' service.
*
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml
index fe17df4f68025..98861bc7c596e 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services9.xml
@@ -133,6 +133,9 @@
+
+
+
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml
index 3c517354eea0e..07076cbbab441 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services9.yml
@@ -113,5 +113,8 @@ services:
lazy_context_ignore_invalid_ref:
class: LazyContext
arguments: [{ '=iterator': ['@foo.baz', '@?invalid'] }]
+ closure_proxy:
+ class: BarClass
+ arguments: [{ '=closure_proxy': ['@closure_proxy', getBaz] }]
alias_for_foo: '@foo'
alias_for_alias: '@foo'
diff --git a/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php b/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php
index 45208a19b2440..5e580806e0da4 100644
--- a/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php
+++ b/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php
@@ -46,7 +46,13 @@ public function __construct($listener, $name, Stopwatch $stopwatch, EventDispatc
$this->name = is_object($listener[0]) ? get_class($listener[0]) : $listener[0];
$this->pretty = $this->name.'::'.$listener[1];
} elseif ($listener instanceof \Closure) {
- $this->pretty = $this->name = 'closure';
+ $r = new \ReflectionFunction($listener);
+ if (preg_match('#^/\*\* @closure-proxy ([^: ]++)::([^: ]++) \*/$#', $r->getDocComment(), $m)) {
+ $this->name = $m[1];
+ $this->pretty = $m[1].'::'.$m[2];
+ } else {
+ $this->pretty = $this->name = 'closure';
+ }
} elseif (is_string($listener)) {
$this->pretty = $this->name = $listener;
} else {
diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php
index 4636ba3ad8d7d..8f69de1b969cf 100644
--- a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php
+++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php
@@ -11,9 +11,11 @@
namespace Symfony\Component\EventDispatcher\DependencyInjection;
+use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
+use Symfony\Component\EventDispatcher\EventDispatcher;
/**
* Compiler pass to register tagged services for an event dispatcher.
@@ -59,10 +61,6 @@ public function process(ContainerBuilder $container)
foreach ($container->findTaggedServiceIds($this->listenerTag) as $id => $events) {
$def = $container->getDefinition($id);
- if (!$def->isPublic()) {
- throw new InvalidArgumentException(sprintf('The service "%s" must be public as event listeners are lazy-loaded.', $id));
- }
-
if ($def->isAbstract()) {
throw new InvalidArgumentException(sprintf('The service "%s" must not be abstract as event listeners are lazy-loaded.', $id));
}
@@ -82,16 +80,14 @@ public function process(ContainerBuilder $container)
$event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']);
}
- $definition->addMethodCall('addListenerService', array($event['event'], array($id, $event['method']), $priority));
+ $definition->addMethodCall('addListener', array($event['event'], new ClosureProxyArgument($id, $event['method']), $priority));
}
}
+ $extractingDispatcher = new ExtractingEventDispatcher();
+
foreach ($container->findTaggedServiceIds($this->subscriberTag) as $id => $attributes) {
$def = $container->getDefinition($id);
- if (!$def->isPublic()) {
- throw new InvalidArgumentException(sprintf('The service "%s" must be public as event subscribers are lazy-loaded.', $id));
- }
-
if ($def->isAbstract()) {
throw new InvalidArgumentException(sprintf('The service "%s" must not be abstract as event subscribers are lazy-loaded.', $id));
}
@@ -108,7 +104,26 @@ public function process(ContainerBuilder $container)
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface));
}
- $definition->addMethodCall('addSubscriberService', array($id, $class));
+ $r = new \ReflectionClass($class);
+ $extractingDispatcher->addSubscriber($r->newInstanceWithoutConstructor());
+ foreach ($extractingDispatcher->listeners as $args) {
+ $args[1] = new ClosureProxyArgument($id, $args[1]);
+ $definition->addMethodCall('addListener', $args);
+ }
+ $extractingDispatcher->listeners = array();
}
}
}
+
+/**
+ * @internal
+ */
+class ExtractingEventDispatcher extends EventDispatcher
+{
+ public $listeners = array();
+
+ public function addListener($eventName, $listener, $priority = 0)
+ {
+ $this->listeners[] = array($eventName, $listener[1], $priority);
+ }
+}
diff --git a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php
index cb04f74beb6d4..27c1a5a1d1255 100644
--- a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php
+++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\EventDispatcher\Tests\DependencyInjection;
+use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
@@ -30,9 +31,6 @@ public function testEventSubscriberWithoutInterface()
);
$definition = $this->getMockBuilder('Symfony\Component\DependencyInjection\Definition')->getMock();
- $definition->expects($this->atLeastOnce())
- ->method('isPublic')
- ->will($this->returnValue(true));
$definition->expects($this->atLeastOnce())
->method('getClass')
->will($this->returnValue('stdClass'));
@@ -62,9 +60,6 @@ public function testValidEventSubscriber()
);
$definition = $this->getMockBuilder('Symfony\Component\DependencyInjection\Definition')->getMock();
- $definition->expects($this->atLeastOnce())
- ->method('isPublic')
- ->will($this->returnValue(true));
$definition->expects($this->atLeastOnce())
->method('getClass')
->will($this->returnValue('Symfony\Component\EventDispatcher\Tests\DependencyInjection\SubscriberService'));
@@ -91,34 +86,6 @@ public function testValidEventSubscriber()
$registerListenersPass->process($builder);
}
- /**
- * @expectedException \InvalidArgumentException
- * @expectedExceptionMessage The service "foo" must be public as event listeners are lazy-loaded.
- */
- public function testPrivateEventListener()
- {
- $container = new ContainerBuilder();
- $container->register('foo', 'stdClass')->setPublic(false)->addTag('kernel.event_listener', array());
- $container->register('event_dispatcher', 'stdClass');
-
- $registerListenersPass = new RegisterListenersPass();
- $registerListenersPass->process($container);
- }
-
- /**
- * @expectedException \InvalidArgumentException
- * @expectedExceptionMessage The service "foo" must be public as event subscribers are lazy-loaded.
- */
- public function testPrivateEventSubscriber()
- {
- $container = new ContainerBuilder();
- $container->register('foo', 'stdClass')->setPublic(false)->addTag('kernel.event_subscriber', array());
- $container->register('event_dispatcher', 'stdClass');
-
- $registerListenersPass = new RegisterListenersPass();
- $registerListenersPass->process($container);
- }
-
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage The service "foo" must not be abstract as event listeners are lazy-loaded.
@@ -161,14 +128,15 @@ public function testEventSubscriberResolvableClassName()
$definition = $container->getDefinition('event_dispatcher');
$expected_calls = array(
array(
- 'addSubscriberService',
+ 'addListener',
array(
- 'foo',
- 'Symfony\Component\EventDispatcher\Tests\DependencyInjection\SubscriberService',
+ 'event',
+ new ClosureProxyArgument('foo', 'onEvent'),
+ 0,
),
),
);
- $this->assertSame($expected_calls, $definition->getMethodCalls());
+ $this->assertEquals($expected_calls, $definition->getMethodCalls());
}
/**
@@ -190,5 +158,8 @@ class SubscriberService implements \Symfony\Component\EventDispatcher\EventSubsc
{
public static function getSubscribedEvents()
{
+ return array(
+ 'event' => 'onEvent',
+ );
}
}
diff --git a/src/Symfony/Component/EventDispatcher/composer.json b/src/Symfony/Component/EventDispatcher/composer.json
index a62be81ea16d9..faa0429e2d1a0 100644
--- a/src/Symfony/Component/EventDispatcher/composer.json
+++ b/src/Symfony/Component/EventDispatcher/composer.json
@@ -19,12 +19,15 @@
"php": ">=5.5.9"
},
"require-dev": {
- "symfony/dependency-injection": "~2.8|~3.0",
+ "symfony/dependency-injection": "~3.3",
"symfony/expression-language": "~2.8|~3.0",
"symfony/config": "~2.8|~3.0",
"symfony/stopwatch": "~2.8|~3.0",
"psr/log": "~1.0"
},
+ "conflict": {
+ "symfony/dependency-injection": "<3.3"
+ },
"suggest": {
"symfony/dependency-injection": "",
"symfony/http-kernel": ""
diff --git a/src/Symfony/Component/VarDumper/Caster/ClassStub.php b/src/Symfony/Component/VarDumper/Caster/ClassStub.php
index 59efecda9ebe6..2b3e9dbd2dcaf 100644
--- a/src/Symfony/Component/VarDumper/Caster/ClassStub.php
+++ b/src/Symfony/Component/VarDumper/Caster/ClassStub.php
@@ -36,6 +36,10 @@ public function __construct($identifier, $callable = null)
if (null !== $callable) {
if ($callable instanceof \Closure) {
$r = new \ReflectionFunction($callable);
+
+ if (preg_match('#^/\*\* @closure-proxy ([^: ]++)::([^: ]++) \*/$#', $r->getDocComment(), $m)) {
+ $r = array($m[1], $m[2]);
+ }
} elseif (is_object($callable)) {
$r = array($callable, '__invoke');
} elseif (is_array($callable)) {