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)) {