Skip to content

Commit

Permalink
[DI][EventDispatcher] Add & wire closure-proxy argument type
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-grekas committed Jan 6, 2017
1 parent 924469c commit ecdf857
Show file tree
Hide file tree
Showing 25 changed files with 672 additions and 60 deletions.
@@ -0,0 +1,46 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <p@tchwork.com>
*/
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;
}
}
26 changes: 26 additions & 0 deletions src/Symfony/Component/DependencyInjection/ContainerBuilder.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
125 changes: 125 additions & 0 deletions src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'] ? '*' : '';

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
}
}
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down
20 changes: 12 additions & 8 deletions src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Expand Up @@ -164,6 +164,7 @@
<xsd:attribute name="index" type="xsd:integer" />
<xsd:attribute name="on-invalid" type="invalid_sequence" />
<xsd:attribute name="strict" type="boolean" />
<xsd:attribute name="method" type="xsd:string" />
</xsd:complexType>

<xsd:complexType name="call" mixed="true">
Expand All @@ -190,6 +191,7 @@
<xsd:enumeration value="string" />
<xsd:enumeration value="constant" />
<xsd:enumeration value="iterator" />
<xsd:enumeration value="closure-proxy" />
</xsd:restriction>
</xsd:simpleType>

Expand Down

0 comments on commit ecdf857

Please sign in to comment.