diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php index 1d8a0410eb..049f899800 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php @@ -79,7 +79,7 @@ final class ArgumentResolver implements ArgumentResolverInterface $representative = get_class($representative); } - throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument (because there is no default value or because there is a non optional argument after this one).', $representative, $metadata->getName())); + throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one.', $representative, $metadata->getName())); } return $arguments; diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DefaultValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DefaultValueResolver.php index 5dd7c772b2..0962dab885 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DefaultValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DefaultValueResolver.php @@ -27,7 +27,7 @@ final class DefaultValueResolver implements ArgumentValueResolverInterface */ public function supports(Request $request, ArgumentMetadata $argument) { - return $argument->hasDefaultValue(); + return $argument->hasDefaultValue() || $argument->isNullable(); } /** @@ -35,6 +35,6 @@ final class DefaultValueResolver implements ArgumentValueResolverInterface */ public function resolve(Request $request, ArgumentMetadata $argument) { - yield $argument->getDefaultValue(); + yield $argument->hasDefaultValue() ? $argument->getDefaultValue() : null; } } diff --git a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php index ca0e881fef..72c294f3a3 100644 --- a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php +++ b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php @@ -23,6 +23,7 @@ class ArgumentMetadata private $isVariadic; private $hasDefaultValue; private $defaultValue; + private $isNullable; /** * @param string $name @@ -30,14 +31,16 @@ class ArgumentMetadata * @param bool $isVariadic * @param bool $hasDefaultValue * @param mixed $defaultValue + * @param bool $isNullable */ - public function __construct($name, $type, $isVariadic, $hasDefaultValue, $defaultValue) + public function __construct($name, $type, $isVariadic, $hasDefaultValue, $defaultValue, $isNullable = false) { $this->name = $name; $this->type = $type; $this->isVariadic = $isVariadic; $this->hasDefaultValue = $hasDefaultValue; $this->defaultValue = $defaultValue; + $this->isNullable = (bool) $isNullable; } /** @@ -84,6 +87,16 @@ class ArgumentMetadata return $this->hasDefaultValue; } + /** + * Returns whether the argument is nullable in PHP 7.1 or higher. + * + * @return bool + */ + public function isNullable() + { + return $this->isNullable; + } + /** * Returns the default value of the argument. * diff --git a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php index 6f49f38932..efe5966894 100644 --- a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php +++ b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php @@ -18,6 +18,30 @@ namespace Symfony\Component\HttpKernel\ControllerMetadata; */ final class ArgumentMetadataFactory implements ArgumentMetadataFactoryInterface { + /** + * If the ...$arg functionality is available. + * + * Requires at least PHP 5.6.0 or HHVM 3.9.1 + * + * @var bool + */ + private $supportsVariadic; + + /** + * If the reflection supports the getType() method to resolve types. + * + * Requires at least PHP 7.0.0 or HHVM 3.11.0 + * + * @var bool + */ + private $supportsParameterType; + + public function __construct() + { + $this->supportsVariadic = method_exists('ReflectionParameter', 'isVariadic'); + $this->supportsParameterType = method_exists('ReflectionParameter', 'getType'); + } + /** * {@inheritdoc} */ @@ -34,7 +58,7 @@ final class ArgumentMetadataFactory implements ArgumentMetadataFactoryInterface } foreach ($reflection->getParameters() as $param) { - $arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param), $this->isVariadic($param), $this->hasDefaultValue($param), $this->getDefaultValue($param)); + $arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param), $this->isVariadic($param), $this->hasDefaultValue($param), $this->getDefaultValue($param), $this->isNullable($param)); } return $arguments; @@ -49,7 +73,7 @@ final class ArgumentMetadataFactory implements ArgumentMetadataFactoryInterface */ private function isVariadic(\ReflectionParameter $parameter) { - return PHP_VERSION_ID >= 50600 && $parameter->isVariadic(); + return $this->supportsVariadic && $parameter->isVariadic(); } /** @@ -64,6 +88,23 @@ final class ArgumentMetadataFactory implements ArgumentMetadataFactoryInterface return $parameter->isDefaultValueAvailable(); } + /** + * Returns if the argument is allowed to be null but is still mandatory. + * + * @param \ReflectionParameter $parameter + * + * @return bool + */ + private function isNullable(\ReflectionParameter $parameter) + { + if ($this->supportsParameterType) { + return null !== ($type = $parameter->getType()) && $type->allowsNull(); + } + + // fallback for supported php 5.x versions + return $this->hasDefaultValue($parameter) && null === $this->getDefaultValue($parameter); + } + /** * Returns a default value if available. * @@ -85,7 +126,7 @@ final class ArgumentMetadataFactory implements ArgumentMetadataFactoryInterface */ private function getType(\ReflectionParameter $parameter) { - if (PHP_VERSION_ID >= 70000) { + if ($this->supportsParameterType) { return $parameter->hasType() ? (string) $parameter->getType() : null; } diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php index 3f647e0bba..964d832362 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php @@ -19,6 +19,7 @@ use Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolv use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\ExtendingRequest; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\NullableController; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController; use Symfony\Component\HttpFoundation\Request; @@ -202,6 +203,32 @@ class ArgumentResolverTest extends \PHPUnit_Framework_TestCase $resolver->getArguments($request, $controller); } + /** + * @requires PHP 7.1 + */ + public function testGetNullableArguments() + { + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $request->attributes->set('bar', new \stdClass()); + $request->attributes->set('mandatory', 'mandatory'); + $controller = array(new NullableController(), 'action'); + + $this->assertEquals(array('foo', new \stdClass(), 'value', 'mandatory'), self::$resolver->getArguments($request, $controller)); + } + + /** + * @requires PHP 7.1 + */ + public function testGetNullableArgumentsWithDefaults() + { + $request = Request::create('/'); + $request->attributes->set('mandatory', 'mandatory'); + $controller = array(new NullableController(), 'action'); + + $this->assertEquals(array(null, null, 'value', 'mandatory'), self::$resolver->getArguments($request, $controller)); + } + public function __invoke($foo, $bar = null) { } diff --git a/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php index c6c2b597b1..49931f052c 100644 --- a/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php @@ -15,10 +15,14 @@ use Fake\ImportedAndFake; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\BasicTypesController; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\NullableController; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController; class ArgumentMetadataFactoryTest extends \PHPUnit_Framework_TestCase { + /** + * @var ArgumentMetadataFactory + */ private $factory; protected function setUp() @@ -42,9 +46,9 @@ class ArgumentMetadataFactoryTest extends \PHPUnit_Framework_TestCase $arguments = $this->factory->createArgumentMetadata(array($this, 'signature2')); $this->assertEquals(array( - new ArgumentMetadata('foo', self::class, false, true, null), - new ArgumentMetadata('bar', __NAMESPACE__.'\FakeClassThatDoesNotExist', false, true, null), - new ArgumentMetadata('baz', 'Fake\ImportedAndFake', false, true, null), + new ArgumentMetadata('foo', self::class, false, true, null, true), + new ArgumentMetadata('bar', __NAMESPACE__.'\FakeClassThatDoesNotExist', false, true, null, true), + new ArgumentMetadata('baz', 'Fake\ImportedAndFake', false, true, null, true), ), $arguments); } @@ -74,7 +78,7 @@ class ArgumentMetadataFactoryTest extends \PHPUnit_Framework_TestCase $arguments = $this->factory->createArgumentMetadata(array($this, 'signature5')); $this->assertEquals(array( - new ArgumentMetadata('foo', 'array', false, true, null), + new ArgumentMetadata('foo', 'array', false, true, null, true), new ArgumentMetadata('bar', null, false, false, null), ), $arguments); } @@ -106,6 +110,21 @@ class ArgumentMetadataFactoryTest extends \PHPUnit_Framework_TestCase ), $arguments); } + /** + * @requires PHP 7.1 + */ + public function testNullableTypesSignature() + { + $arguments = $this->factory->createArgumentMetadata(array(new NullableController(), 'action')); + + $this->assertEquals(array( + new ArgumentMetadata('foo', 'string', false, false, null, true), + new ArgumentMetadata('bar', \stdClass::class, false, false, null, true), + new ArgumentMetadata('baz', 'string', false, true, 'value', true), + new ArgumentMetadata('mandatory', null, false, false, null), + ), $arguments); + } + private function signature1(ArgumentMetadataFactoryTest $foo, array $bar, callable $baz) { } diff --git a/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataTest.php b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataTest.php index 9713d70f8e..cbb0d1bece 100644 --- a/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataTest.php @@ -15,10 +15,18 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; class ArgumentMetadataTest extends \PHPUnit_Framework_TestCase { - public function testDefaultValueAvailable() + public function testWithBcLayerWithDefault() { $argument = new ArgumentMetadata('foo', 'string', false, true, 'default value'); + $this->assertFalse($argument->isNullable()); + } + + public function testDefaultValueAvailable() + { + $argument = new ArgumentMetadata('foo', 'string', false, true, 'default value', true); + + $this->assertTrue($argument->isNullable()); $this->assertTrue($argument->hasDefaultValue()); $this->assertSame('default value', $argument->getDefaultValue()); } @@ -28,8 +36,9 @@ class ArgumentMetadataTest extends \PHPUnit_Framework_TestCase */ public function testDefaultValueUnavailable() { - $argument = new ArgumentMetadata('foo', 'string', false, false, null); + $argument = new ArgumentMetadata('foo', 'string', false, false, null, false); + $this->assertFalse($argument->isNullable()); $this->assertFalse($argument->hasDefaultValue()); $argument->getDefaultValue(); }