[DI] Add "factory" support to named args and autowiring
This commit is contained in:
parent
4a5f22bc1e
commit
27470de62a
@ -12,8 +12,10 @@
|
||||
namespace Symfony\Component\DependencyInjection\Compiler;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
@ -73,4 +75,90 @@ abstract class AbstractRecursivePass implements CompilerPassInterface
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Definition $definition
|
||||
* @param bool $required
|
||||
*
|
||||
* @return \ReflectionFunctionAbstract|null
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function getConstructor(Definition $definition, $required)
|
||||
{
|
||||
if (is_string($factory = $definition->getFactory())) {
|
||||
if (!function_exists($factory)) {
|
||||
throw new RuntimeException(sprintf('Unable to resolve service "%s": function "%s" does not exist.', $this->currentId, $factory));
|
||||
}
|
||||
$r = new \ReflectionFunction($factory);
|
||||
if (false !== $r->getFileName() && file_exists($r->getFileName())) {
|
||||
$this->container->fileExists($r->getFileName());
|
||||
}
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
if ($factory) {
|
||||
list($class, $method) = $factory;
|
||||
if ($class instanceof Reference) {
|
||||
$class = $this->container->findDefinition((string) $class)->getClass();
|
||||
} elseif (null === $class) {
|
||||
$class = $definition->getClass();
|
||||
}
|
||||
if ('__construct' === $method) {
|
||||
throw new RuntimeException(sprintf('Unable to resolve service "%s": "__construct()" cannot be used as a factory method.', $this->currentId));
|
||||
}
|
||||
|
||||
return $this->getReflectionMethod(new Definition($class), $method);
|
||||
}
|
||||
|
||||
$class = $definition->getClass();
|
||||
|
||||
if (!$r = $this->container->getReflectionClass($class)) {
|
||||
throw new RuntimeException(sprintf('Unable to resolve service "%s": class "%s" does not exist.', $this->currentId, $class));
|
||||
}
|
||||
if (!$r = $r->getConstructor()) {
|
||||
if ($required) {
|
||||
throw new RuntimeException(sprintf('Unable to resolve service "%s": class%s has no constructor.', $this->currentId, sprintf($class !== $this->currentId ? ' "%s"' : '', $class)));
|
||||
}
|
||||
} elseif (!$r->isPublic()) {
|
||||
throw new RuntimeException(sprintf('Unable to resolve service "%s": %s must be public.', $this->currentId, sprintf($class !== $this->currentId ? 'constructor of class "%s"' : 'its constructor', $class)));
|
||||
}
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Definition $definition
|
||||
* @param string $method
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*
|
||||
* @return \ReflectionFunctionAbstract
|
||||
*/
|
||||
protected function getReflectionMethod(Definition $definition, $method)
|
||||
{
|
||||
if ('__construct' === $method) {
|
||||
return $this->getConstructor($definition, true);
|
||||
}
|
||||
|
||||
if (!$class = $definition->getClass()) {
|
||||
throw new RuntimeException(sprintf('Unable to resolve service "%s": the class is not set.', $this->currentId));
|
||||
}
|
||||
|
||||
if (!$r = $this->container->getReflectionClass($class)) {
|
||||
throw new RuntimeException(sprintf('Unable to resolve service "%s": class "%s" does not exist.', $this->currentId, $class));
|
||||
}
|
||||
|
||||
if (!$r->hasMethod($method)) {
|
||||
throw new RuntimeException(sprintf('Unable to resolve service "%s": method "%s()" does not exist.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method));
|
||||
}
|
||||
|
||||
$r = $r->getMethod($method);
|
||||
if (!$r->isPublic()) {
|
||||
throw new RuntimeException(sprintf('Unable to resolve service "%s": method "%s()" must be public.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method));
|
||||
}
|
||||
|
||||
return $r;
|
||||
}
|
||||
}
|
||||
|
@ -95,9 +95,6 @@ class AutowirePass extends AbstractRecursivePass
|
||||
if (!$value->isAutowired() || $value->isAbstract() || !$value->getClass()) {
|
||||
return parent::processValue($value, $isRoot);
|
||||
}
|
||||
if ($value->getFactory()) {
|
||||
throw new RuntimeException(sprintf('Service "%s" can use either autowiring or a factory, not both.', $this->currentId));
|
||||
}
|
||||
if (!$reflectionClass = $this->container->getReflectionClass($value->getClass())) {
|
||||
$this->container->log($this, sprintf('Skipping service "%s": Class or interface "%s" does not exist.', $this->currentId, $value->getClass()));
|
||||
|
||||
@ -107,8 +104,8 @@ class AutowirePass extends AbstractRecursivePass
|
||||
$autowiredMethods = $this->getMethodsToAutowire($reflectionClass);
|
||||
$methodCalls = $value->getMethodCalls();
|
||||
|
||||
if ($constructor = $reflectionClass->getConstructor()) {
|
||||
array_unshift($methodCalls, array($constructor->name, $value->getArguments()));
|
||||
if ($constructor = $this->getConstructor($value, false)) {
|
||||
array_unshift($methodCalls, array($constructor, $value->getArguments()));
|
||||
}
|
||||
|
||||
$methodCalls = $this->autowireCalls($reflectionClass, $methodCalls, $autowiredMethods);
|
||||
@ -143,13 +140,13 @@ class AutowirePass extends AbstractRecursivePass
|
||||
$found = array();
|
||||
$methodsToAutowire = array();
|
||||
|
||||
if ($reflectionMethod = $reflectionClass->getConstructor()) {
|
||||
$methodsToAutowire[strtolower($reflectionMethod->name)] = $reflectionMethod;
|
||||
}
|
||||
|
||||
foreach ($reflectionClass->getMethods() as $reflectionMethod) {
|
||||
$r = $reflectionMethod;
|
||||
|
||||
if ($r->isConstructor()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (false !== $doc = $r->getDocComment()) {
|
||||
if (false !== stripos($doc, '@required') && preg_match('#(?:^/\*\*|\n\s*+\*)\s*+@required(?:\s|\*/$)#i', $doc)) {
|
||||
@ -183,19 +180,13 @@ class AutowirePass extends AbstractRecursivePass
|
||||
foreach ($methodCalls as $i => $call) {
|
||||
list($method, $arguments) = $call;
|
||||
|
||||
if (isset($autowiredMethods[$lcMethod = strtolower($method)]) && $autowiredMethods[$lcMethod]->isPublic()) {
|
||||
if ($method instanceof \ReflectionFunctionAbstract) {
|
||||
$reflectionMethod = $method;
|
||||
} elseif (isset($autowiredMethods[$lcMethod = strtolower($method)]) && $autowiredMethods[$lcMethod]->isPublic()) {
|
||||
$reflectionMethod = $autowiredMethods[$lcMethod];
|
||||
unset($autowiredMethods[$lcMethod]);
|
||||
} else {
|
||||
if (!$reflectionClass->hasMethod($method)) {
|
||||
$class = $reflectionClass->name;
|
||||
throw new RuntimeException(sprintf('Cannot autowire service "%s": method %s() does not exist.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method));
|
||||
}
|
||||
$reflectionMethod = $reflectionClass->getMethod($method);
|
||||
if (!$reflectionMethod->isPublic()) {
|
||||
$class = $reflectionClass->name;
|
||||
throw new RuntimeException(sprintf('Cannot autowire service "%s": method %s() must be public.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method));
|
||||
}
|
||||
$reflectionMethod = $this->getReflectionMethod($this->currentDefinition, $method);
|
||||
}
|
||||
|
||||
$arguments = $this->autowireMethod($reflectionMethod, $arguments);
|
||||
@ -210,7 +201,7 @@ class AutowirePass extends AbstractRecursivePass
|
||||
|
||||
if (!$reflectionMethod->isPublic()) {
|
||||
$class = $reflectionClass->name;
|
||||
throw new RuntimeException(sprintf('Cannot autowire service "%s": method %s() must be public.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method));
|
||||
throw new RuntimeException(sprintf('Cannot autowire service "%s": method "%s()" must be public.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method));
|
||||
}
|
||||
$methodCalls[] = array($method, $this->autowireMethod($reflectionMethod, array()));
|
||||
}
|
||||
@ -221,16 +212,16 @@ class AutowirePass extends AbstractRecursivePass
|
||||
/**
|
||||
* Autowires the constructor or a method.
|
||||
*
|
||||
* @param \ReflectionMethod $reflectionMethod
|
||||
* @param array $arguments
|
||||
* @param \ReflectionFunctionAbstract $reflectionMethod
|
||||
* @param array $arguments
|
||||
*
|
||||
* @return array The autowired arguments
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
private function autowireMethod(\ReflectionMethod $reflectionMethod, array $arguments)
|
||||
private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, array $arguments)
|
||||
{
|
||||
$class = $reflectionMethod->class;
|
||||
$class = $reflectionMethod instanceof \ReflectionMethod ? $reflectionMethod->class : $this->currentId;
|
||||
$method = $reflectionMethod->name;
|
||||
$parameters = $reflectionMethod->getParameters();
|
||||
if (method_exists('ReflectionMethod', 'isVariadic') && $reflectionMethod->isVariadic()) {
|
||||
|
@ -30,18 +30,11 @@ class ResolveNamedArgumentsPass extends AbstractRecursivePass
|
||||
return parent::processValue($value, $isRoot);
|
||||
}
|
||||
|
||||
$parameterBag = $this->container->getParameterBag();
|
||||
|
||||
if ($class = $value->getClass()) {
|
||||
$class = $parameterBag->resolveValue($class);
|
||||
}
|
||||
|
||||
$calls = $value->getMethodCalls();
|
||||
$calls[] = array('__construct', $value->getArguments());
|
||||
|
||||
foreach ($calls as $i => $call) {
|
||||
list($method, $arguments) = $call;
|
||||
$method = $parameterBag->resolveValue($method);
|
||||
$parameters = null;
|
||||
$resolvedArguments = array();
|
||||
|
||||
@ -51,10 +44,14 @@ class ResolveNamedArgumentsPass extends AbstractRecursivePass
|
||||
continue;
|
||||
}
|
||||
if ('' === $key || '$' !== $key[0]) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid key "%s" found in arguments of method "%s" for service "%s": only integer or $named arguments are allowed.', $key, $method, $this->currentId));
|
||||
throw new InvalidArgumentException(sprintf('Invalid key "%s" found in arguments of method "%s()" for service "%s": only integer or $named arguments are allowed.', $key, $method, $this->currentId));
|
||||
}
|
||||
|
||||
$parameters = null !== $parameters ? $parameters : $this->getParameters($class, $method);
|
||||
if (null === $parameters) {
|
||||
$r = $this->getReflectionMethod($value, $method);
|
||||
$class = $r instanceof \ReflectionMethod ? $r->class : $this->currentId;
|
||||
$parameters = $r->getParameters();
|
||||
}
|
||||
|
||||
foreach ($parameters as $j => $p) {
|
||||
if ($key === '$'.$p->name) {
|
||||
@ -64,7 +61,7 @@ class ResolveNamedArgumentsPass extends AbstractRecursivePass
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s::%s" has no argument named "%s". Check your service definition.', $this->currentId, $class, $method, $key));
|
||||
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s()" has no argument named "%s". Check your service definition.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method, $key));
|
||||
}
|
||||
|
||||
if ($resolvedArguments !== $call[1]) {
|
||||
@ -84,34 +81,4 @@ class ResolveNamedArgumentsPass extends AbstractRecursivePass
|
||||
|
||||
return parent::processValue($value, $isRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $class
|
||||
* @param string $method
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getParameters($class, $method)
|
||||
{
|
||||
if (!$class) {
|
||||
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": the class is not set.', $this->currentId));
|
||||
}
|
||||
|
||||
if (!$r = $this->container->getReflectionClass($class)) {
|
||||
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": class "%s" does not exist.', $this->currentId, $class));
|
||||
}
|
||||
|
||||
if (!$r->hasMethod($method)) {
|
||||
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s::%s" does not exist.', $this->currentId, $class, $method));
|
||||
}
|
||||
|
||||
$method = $r->getMethod($method);
|
||||
if (!$method->isPublic()) {
|
||||
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s::%s" must be public.', $this->currentId, $class, $method->name));
|
||||
}
|
||||
|
||||
return $method->getParameters();
|
||||
}
|
||||
}
|
||||
|
@ -611,20 +611,19 @@ class AutowirePassTest extends TestCase
|
||||
$this->assertEquals(array(new Reference('a'), '', new Reference('lille')), $container->getDefinition('foo')->getArguments());
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
|
||||
* @expectedExceptionMessage Service "a" can use either autowiring or a factory, not both.
|
||||
*/
|
||||
public function testWithFactory()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$container->register('a', __NAMESPACE__.'\A')
|
||||
->setFactory('foo')
|
||||
$container->register('foo', Foo::class);
|
||||
$definition = $container->register('a', A::class)
|
||||
->setFactory(array(A::class, 'create'))
|
||||
->setAutowired(true);
|
||||
|
||||
$pass = new AutowirePass();
|
||||
$pass->process($container);
|
||||
|
||||
$this->assertEquals(array(new Reference('foo')), $definition->getArguments());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -662,7 +661,7 @@ class AutowirePassTest extends TestCase
|
||||
{
|
||||
return array(
|
||||
array('setNotAutowireable', 'Cannot autowire service "foo": argument $n of method Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setNotAutowireable() has type "Symfony\Component\DependencyInjection\Tests\Compiler\NotARealClass" but this class does not exist.'),
|
||||
array(null, 'Cannot autowire service "foo": method Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setProtectedMethod() must be public.'),
|
||||
array(null, 'Cannot autowire service "foo": method "Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setProtectedMethod()" must be public.'),
|
||||
);
|
||||
}
|
||||
|
||||
@ -745,6 +744,9 @@ class Bar
|
||||
|
||||
class A
|
||||
{
|
||||
public static function create(Foo $foo)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class B extends A
|
||||
|
@ -37,8 +37,23 @@ class ResolveNamedArgumentsPassTest extends TestCase
|
||||
$this->assertEquals(array(array('setApiKey', array('123'))), $definition->getMethodCalls());
|
||||
}
|
||||
|
||||
public function testWithFactory()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$container->register('factory', NoConstructor::class);
|
||||
$definition = $container->register('foo', NoConstructor::class)
|
||||
->setFactory(array(new Reference('factory'), 'create'))
|
||||
->setArguments(array('$apiKey' => '123'));
|
||||
|
||||
$pass = new ResolveNamedArgumentsPass();
|
||||
$pass->process($container);
|
||||
|
||||
$this->assertSame(array(0 => '123'), $definition->getArguments());
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
|
||||
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
|
||||
*/
|
||||
public function testClassNull()
|
||||
{
|
||||
@ -52,7 +67,7 @@ class ResolveNamedArgumentsPassTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
|
||||
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
|
||||
*/
|
||||
public function testClassNotExist()
|
||||
{
|
||||
@ -66,7 +81,7 @@ class ResolveNamedArgumentsPassTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
|
||||
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
|
||||
*/
|
||||
public function testClassNoConstructor()
|
||||
{
|
||||
@ -96,4 +111,7 @@ class ResolveNamedArgumentsPassTest extends TestCase
|
||||
|
||||
class NoConstructor
|
||||
{
|
||||
public static function create($apiKey)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user