Skip to content

ReflectionType improvements #2068

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 5 commits into from

Conversation

trowski
Copy link
Member

@trowski trowski commented Aug 9, 2016

Implements a portion of @morrisonlevi's ReflectionType Improvements RFC that is necessary due to the Nullable Types RFC.

ReflectionParameter::getType() and ReflectionFunctionAbstract::getReturnType() now return an instance of ReflectionNamedType. This class extends ReflectionType, adding a getName() method that returns the type name as a string. This method provides a means of retrieving the type name without casting the ReflectionType object to a string.

ReflectionType::__toString() was updated to prepend a ? if the type is nullable, maintaining the string representation of ReflectionType as a syntax-valid representation of the type.

The BC impact of this PR should be minimal. In fact, not applying these changes may have a bigger impact to BC. Without the changes in this PR projects such as PHPUnit, PHPSpec, and Mockery fail to produce code compatible with function signatures including nullable types. These projects depend on the string representation of ReflectionType to generate code compatible with the parent class or interface. After the changes in this PR, these projects work as expected with nullable types.

@krakjoe @dshafik I'd like to merge this for 7.1. I think these changes to reflection are necessary and should not wait for 7.2.

@dshafik
Copy link
Contributor

dshafik commented Aug 10, 2016

@trowski I would like to see this in 7.1 please. It looks like the CI failed due to the rand() changes and I agree 100% that not making these changes is a bigger problem than breaking BC.

Please ensure these get merged before beta3.

@Majkl578
Copy link
Contributor

There should also be a method to detect whether the type is nullable or not. There is allowsNull now, but it serves different purpose. The only way would be some smelly code like this: ((string) $type)[0] === '?').
(Well, whole parameter/return reflection api is kinda messy, but at least this. :))

@morrisonlevi
Copy link
Contributor

You can't actually tell at runtime if it's implicit or explicitly null.
This would require changing allow_null from a boolean to an enum.

On Tue, Aug 9, 2016, 6:57 PM Michael Moravec notifications@github.com
wrote:

There should also be a method to detect whether the type is nullable or
not. There is allowsNull now, but it serves different purpose. The only way
would be some smelly code like this: ((string) $type)[0] === '?').
(Well, whole parameter/return reflection api is kinda messy, but at least
this. :))


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#2068 (comment), or mute
the thread
https://github.com/notifications/unsubscribe-auth/AAPdhOAhPxmt0AgYYQrq9gijE7SSP7Cmks5qeSHqgaJpZM4JgjqB
.

@Majkl578
Copy link
Contributor

That is strange.
NULL as a default value does not imply nullable type, i.e. signatures foo(?int $i) and foo(int $i = NULL) are not semantically equivalent and are not compatible with each other (and warning is produced).

In such case, this patch is broken:

class Test {
    function foo(int $z = null) {}
}

$rp = new ReflectionParameter(['Test', 'foo'], 'z');
var_dump((string) $rp->getType()); // oops, "?int", but should be "int"

@trowski
Copy link
Member Author

trowski commented Aug 10, 2016

@Majkl578 In your example, (string) $rp->getType() should return ?int as foo(?int $i = null) and foo(int $i = null) are semantically equivalent. The latter syntax is only retained for compatibility.

To detect foo(?int $i) vs. foo(?int $i = null), use ReflectionParameter::isOptional().

function foo(?int $i) {}
function bar(?int $i = null) {}

$parameter = new ReflectionParameter('foo', 'i');
var_dump($parameter->getType()->allowsNull()); // bool(true)
var_dump($parameter->isOptional()); // bool(false)

$parameter = new ReflectionParameter('bar', 'i');
var_dump($parameter->getType()->allowsNull()); // bool(true)
var_dump($parameter->isOptional()); // bool(true)
var_dump($parameter->getDefaultValue()); // NULL

@nikic
Copy link
Member

nikic commented Aug 11, 2016

Merged via 622d2f4. Thanks!

@nikic nikic closed this Aug 11, 2016
@Majkl578
Copy link
Contributor

Majkl578 commented Aug 16, 2016

@trowski Well, I still disagree, having default value and requiring value are two different things (for caller, not callee).
There still seems to be another fundamental problem with this patch though -- compatibility with FQCN and nullable. I'm testing doctrine/common and its proxy generator component against PHP-7.1 branch right now. Since proxied classes are always generated with FQCN return types / type hints (thus prefixed with backslash), the produced code with this patch results in parse error anyway.
I.e. reconstructing the following:

public function returnsNullableObject() : ?\stdClass

results in:

public function returnsNullableObject(): \?stdClass

This unfortunately makes your original claim void:

After the changes in this PR, these projects work as expected with nullable types.

So libraries still will have to manually resolve this issue manually. And in order to not change method signature in proxied class (because there is no way to differentiate between nullable type and default null value, or both), it'd still require a hack such as ((string) $returnType)[0] === '?'. 😢

Also there is a bug (not sure whether caused by this patch or not, will need to bisect later):

namespace Foo;
function x(?\stdClass $y) {}

var_dump((new \ReflectionParameter('Foo\x', 'y'))->getClass()); // Fatal error: Uncaught ReflectionException: Class Foo\stdClass does not exist

EDIT:

Forgot to reply to NULL detection. How would you propose to solve this issue:

function x(?int $a = null) {}
function y(int $a = null) {}

foreach (['x', 'y'] as $fn) {
        echo $fn, ":\n";
        $rp = new \ReflectionParameter($fn, 'a');
        var_dump($rp->getType()->allowsNull());
        var_dump($rp->isOptional());
}
x:
bool(true)
bool(true)
y:
bool(true)
bool(true)

@trowski
Copy link
Member Author

trowski commented Aug 16, 2016

Looks like I focused on scalar types when I was experimenting with PHPUnit, etc. There does seem to be an issue with ReflectionType::__toString() when dynamically generating code in that it does not append a \ to the beginning of the fully-qualified name. Perhaps it should (and of course nullables would append ?\). @dshafik @nikic Thoughts on further changing __toString() here? Would there be BC concerns?

@Majkl578 You still seem confused about function (?int $a = null) { ... } vs function (int $a = null) { ... }. These two are semantically equivalent. There is no way to determine which sytanx was used to define the function because it does not matter. Both result in a function that accepts a nullable int with a default value of null. Also, there is no need to use hacks such as ((string) $returnType)[0] === '?' as $returnType->allowsNull() will indicate if the type is nullable.

@nikic
Copy link
Member

nikic commented Aug 16, 2016

@Majkl578 Handling of ?\stdClass parameter types fixed in 1397f75, thanks.

@nikic
Copy link
Member

nikic commented Aug 16, 2016

@trowski Interesting question. Usually I'd say that leading backslashes have nothing lost in strings, but this situation is special in that the output of ReflectionType::__toString() is likely to be used for code-generation, where this does matter. Not making the class names fully qualified means that anyone doing codegen based on ReflectionType is required to reimplement the name formatting logic. Currently, this is hardly worth mentioning, but thinking forward to types like (callable(A|(B[])): ?callable(C))|string, this will become unpleasant. So I'd say that yes, we should fully qualify names in __toString(). Of course this does break BC for people already manually prepending the backslash, but I don't think we can properly integrate nullable in ReflectionType without requiring some form of code-adjustment on behalf of ReflectionType consumers.

@morrisonlevi Your thoughts on this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants