8000 [HttpKernel] Added nullable support for php 7.1 and below by linaori · Pull Request #19785 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[HttpKernel] Added nullable support for php 7.1 and below #19785

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions UPGRADE-3.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,8 @@ Yaml
* Mappings with a colon (`:`) that is not followed by a whitespace are deprecated
and will lead to a `ParseException` in Symfony 4.0 (e.g. `foo:bar` must be
`foo: bar`).

HttpKernel
----------

* Added the `$isNullable` argument to the `ArgumentMetadata` as deprecated optional value, but will no longer be optional as of 4.0.
2 changes: 2 additions & 0 deletions UPGRADE-4.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ HttpKernel
have your own `ControllerResolverInterface` implementation, you should
inject an `ArgumentResolverInterface` instance.

* The `$isNullable` argument to the `ArgumentMetadata` is no longer optional.

Serializer
----------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function testResolveNoToken()
{
$tokenStorage = new TokenStorage();
$resolver = new SecurityUserValueResolver($tokenStorage);
$metadata = new ArgumentMetadata('foo', UserInterface::class, false, false, null);
$metadata = new ArgumentMetadata('foo', UserInterface::class, false, false, null, false);

$this->assertFalse($resolver->supports(Request::create('/'), $metadata));
}
Expand All @@ -39,7 +39,7 @@ public function testResolveNoUser()
$tokenStorage->setToken($token);

$resolver = new SecurityUserValueResolver($tokenStorage);
$metadata = new ArgumentMetadata('foo', get_class($mock), false, false, null);
$metadata = new ArgumentMetadata('foo', get_class($mock), false, false, null, false);

$this->assertFalse($resolver->supports(Request::create('/'), $metadata));
}
Expand All @@ -48,7 +48,7 @@ public function testResolveWrongType()
{
$tokenStorage = new TokenStorage();
$resolver = new SecurityUserValueResolver($tokenStorage);
$metadata = new ArgumentMetadata('foo', null, false, false, null);
$metadata = new ArgumentMetadata('foo', null, false, false, null, false);

$this->assertFalse($resolver->supports(Request::create('/'), $metadata));
}
Expand All @@ -62,7 +62,7 @@ public function testResolve()
$tokenStorage->setToken($token);

$resolver = new SecurityUserValueResolver($tokenStorage);
$metadata = new ArgumentMetadata('foo', UserInterface::class, false, false, null);
$metadata = new ArgumentMetadata('foo', UserInterface::class, false, false, null, false);

$this->assertTrue($resolver->supports(Request::create('/'), $metadata));
$this->assertSame(array($user), iterator_to_array($resolver->resolve(Request::create('/'), $metadata)));
Expand Down
4 changes: 4 additions & 0 deletions src/Symfony/Component/HttpKernel/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
CHANGELOG
=========

3.2.0
-----
* Added the `$isNullable` argument to the `ArgumentMetadata` as deprecated optional value, but will no longer be optional as of 4.0.

3.1.0
-----
* deprecated passing objects as URI attributes to the ESI and SSI renderers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public function getArguments(Request $request, $controller)
$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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ final class DefaultValueResolver implements ArgumentValueResolverInterface
*/
public function supports(Request $request, ArgumentMetadata $argument)
{
return $argument->hasDefaultValue();
return $argument->hasDefaultValue() || $argument->isNullable();
}

/**
* {@inheritdoc}
*/
public function resolve(Request $request, ArgumentMetadata $argument)
{
yield $argument->getDefaultValue();
yield $argument->hasDefaultValue() ? $argument->getDefaultValue() : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,28 @@ class ArgumentMetadata
private $isVariadic;
private $hasDefaultValue;
private $defaultValue;
private $isNullable;

/**
* @param string $name
* @param string $type
* @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 = null)
{
$this->name = $name;
$this->type = $type;
$this->isVariadic = $isVariadic;
$this->hasDefaultValue = $hasDefaultValue;
$this->defaultValue = $defaultValue;
$this->isNullable = (bool) $isNullable;

if (null === $isNullable) {
@trigger_error(sprintf('As of 3.2 the $isNullable argument has been added to %s with a default value. As of 4.0 the argument will become mandatory.', __CLASS__), E_USER_DEPRECATED);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor but: I think both $isNullable and %s should be enclosed with double quotes (like in the ArgumentResolver exception you've tweaked for instance). :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Challenging the deprecation again: wouldn't it make sense to have $isNullable default to false and remove the deprecation, thus merge on 3.1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, one option could be to add it like this on 3.1 and deprecate it in 3.2 by changing the default value. The isNullable() method will still be a new feature though.

Copy link
Member
F438

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why deprecate anything now or later? Let's keep the param optional, always. Or would that hurt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, btw, forward compat with newer php versions should be considered as a bug fix (as done previously)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it wouldn't hurt actually, I'll switch active branches in that case and re-open the other PR ^^

}
}

/**
Expand Down Expand Up @@ -84,6 +91,16 @@ public function hasDefaultValue()
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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function createArgumentMetadata($controller)
}

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;
Expand Down Expand Up @@ -64,6 +64,23 @@ private function hasDefaultValue(\ReflectionParameter $parameter)
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 (PHP_VERSION_ID >= 70000) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you use method_exists here ? Because this constant may differ on hhvm.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't hhvm supposed to correspond with a certain php version?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarilly, for example hhvm can support some php7 features and set this constant to php5 (see this comment) and as we don't know how they are gonna support php7.1 features, i think it is safer to use method_exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor
@GuilhemN GuilhemN Aug 30, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's a hack as it actually checks that the feature you need is available (it's imo less hacky than depending on a version). For the same reason, I think we should update the examples you're pointing as it could create unexpected behaviors.

But I'm not using hvvm so it won't impact me anyway, I just think it would be weird if it doesn't work sometimes when it should.
Moreover the solution to this is not complicated: class_exists($parameter, 'getType').

Maybe we could ask someone of the hvvm team if it's safe to depend on this constant for this feature?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

method_exists is fast, let's use it imho

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I update this for the other cases in this class as well while I'm at it? We do a lose a bit of verbosity of why this check is being done. The version fallbacks should definitely be removed in the future. How would you suggest it should be traced back?

I'm not familiar with HHVM or how they version it. Maybe this is a generic problem which should be solved framework wide?

Copy link
Member
@nicolas-grekas nicolas-grekas Aug 31, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's on a case by case basis I'd say. Logically, HHVM should expose a consistent value for PHP_VERSION_ID when it is compatible enough with it. But this is a moving target and sometimes (like here) we can use feature tests for better portability. So yes, let's check the existing places and see how this applies.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been poking around on #hhvm and basically what I was told:

HHVM recommends doing feature detection, not version sniffing

I would really love to see a generic feature detection system which is determined either during container compilation or kernel boot-up

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean? This checks are only needed for php7 features and higher but we can't have something generic as hhvm allows to enable some features but not necessary all of them.

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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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;

Expand Down Expand Up @@ -202,6 +203,32 @@ public function testGetArgumentWithoutArray()
$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)
{
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@
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()
Expand All @@ -31,9 +35,9 @@ public function testSignature1()
$arguments = $this->factory->createArgumentMetadata(array($this, 'signature1'));

$this->assertEquals(array(
new ArgumentMetadata('foo', self::class, false, false, null),
new ArgumentMetadata('bar', 'array', false, false, null),
new ArgumentMetadata('baz', 'callable', false, false, null),
new ArgumentMetadata('foo', self::class, false, false, null, false),
new ArgumentMetadata('bar', 'array', false, false, null, false),
new ArgumentMetadata('baz', 'callable', false, false, null, false),
), $arguments);
}

Expand All @@ -42,9 +46,9 @@ public function testSignature2()
$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);
}

Expand All @@ -53,8 +57,8 @@ public function testSignature3()
$arguments = $this->factory->createArgumentMetadata(array($this, 'signature3'));

$this->assertEquals(array(
new ArgumentMetadata('bar', __NAMESPACE__.'\FakeClassThatDoesNotExist', false, false, null),
new ArgumentMetadata('baz', 'Fake\ImportedAndFake', false, false, null),
new ArgumentMetadata('bar', __NAMESPACE__.'\FakeClassThatDoesNotExist', false, false, null, false),
new ArgumentMetadata('baz', 'Fake\ImportedAndFake', false, false, null, false),
), $arguments);
}

Expand All @@ -63,9 +67,9 @@ public function testSignature4()
$arguments = $this->factory->createArgumentMetadata(array($this, 'signature4'));

$this->assertEquals(array(
new ArgumentMetadata('foo', null, false, true, 'default'),
new ArgumentMetadata('bar', null, false, true, 500),
new ArgumentMetadata('baz', null, false, true, array()),
new ArgumentMetadata('foo', null, false, true, 'default', false),
new ArgumentMetadata('bar', null, false, true, 500, false),
new ArgumentMetadata('baz', null, false, true, array(), false),
), $arguments);
}

Expand All @@ -74,8 +78,8 @@ public function testSignature5()
$arguments = $this->factory->createArgumentMetadata(array($this, 'signature5'));

$this->assertEquals(array(
new ArgumentMetadata('foo', 'array', false, true, null),
new ArgumentMetadata('bar', null, false, false, null),
new ArgumentMetadata('foo', 'array', false, true, null, true),
new ArgumentMetadata('bar', null, false, false, null, false),
), $arguments);
}

Expand All @@ -87,8 +91,8 @@ public function testVariadicSignature()
$arguments = $this->factory->createArgumentMetadata(array(new VariadicController(), 'action'));

$this->assertEquals(array(
new ArgumentMetadata('foo', null, false, false, null),
new ArgumentMetadata('bar', null, true, false, null),
new ArgumentMetadata('foo', null, false, false, null, false),
new ArgumentMetadata('bar', null, true, false, null, false),
), $arguments);
}

Expand All @@ -100,9 +104,24 @@ public function testBasicTypesSignature()
$arguments = $this->factory->createArgumentMetadata(array(new BasicTypesController(), 'action'));

$this->assertEquals(array(
new ArgumentMetadata('foo', 'string', false, false, null),
new ArgumentMetadata('bar', 'int', false, false, null),
new ArgumentMetadata('baz', 'float', false, false, null),
new ArgumentMetadata('foo', 'string', false, false, null, false),
new ArgumentMetadata('bar', 'int', false, false, null, false),
new ArgumentMetadata('baz', 'float', false, false, null, false),
D411 ), $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, false),
), $arguments);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,21 @@

class ArgumentMetadataTest extends \PHPUnit_Framework_TestCase
{
public function testDefaultValueAvailable()
/**
* @group legacy
*/
public function testWithBcLayerForNullable()
{
$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());
}
Expand All @@ -28,8 +39,9 @@ public function testDefaultValueAvailable()
*/
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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller;

class NullableController
{
public function action(? string $foo, ? \stdClass $bar, ? string $baz = 'value', $mandatory)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for my minor comment but I don't know if a syntax has been defined yet for this, because I don't really like having a space after the ?. The RFC for now don't use any space.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intuitively, I tend to agree with not having a space.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's exactly how I had it, but this was fixed by fabbot.io :(

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then fabbot needs a fix and we should remove the space here

{
}
}
0