bug #37340 [3.4] Fix support for PHP8 union types (nicolas-grekas)

This PR was merged into the 3.4 branch.

Discussion
----------

[3.4] Fix support for PHP8 union types

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | -

This fixes fatal errors on PHP 8 once union types are used. Note that this doesn't provide support for union types, eg autowiring, serializer, etc will just skip them. If another behavior is desired, that'd be for the `5.x` branch.

With PHP 8 coming, calling the `getName()` on the object returned by `ReflectionParameter::getType()` or `ReflectionFunctionAbstract::getReturnType()` might fail. The reason is that these might return a [`ReflectionUnionType`](https://wiki.php.net/rfc/union_types_v2#reflection) now, which has no `getName()` method (same for `isBuiltin() btw.)

Commits
-------

e09372bcbf [3.4] Fix support for PHP8 union types
This commit is contained in:
Nicolas Grekas 2020-06-18 19:34:15 +02:00
commit cf66f03337
7 changed files with 61 additions and 44 deletions

View File

@ -177,6 +177,7 @@ class ReflectionClassResource implements SelfCheckingResourceInterface, \Seriali
if (!$parametersWithUndefinedConstants) {
yield preg_replace('/^ @@.*/m', '', $m);
} else {
$t = \PHP_VERSION_ID >= 70000 ? $m->getReturnType() : '';
$stack = [
$m->getDocComment(),
$m->getName(),
@ -187,15 +188,16 @@ class ReflectionClassResource implements SelfCheckingResourceInterface, \Seriali
$m->isPrivate(),
$m->isProtected(),
$m->returnsReference(),
\PHP_VERSION_ID >= 70000 && $m->hasReturnType() ? (\PHP_VERSION_ID >= 70100 ? $m->getReturnType()->getName() : (string) $m->getReturnType()) : '',
$t instanceof \ReflectionNamedType ? ((string) $t->allowsNull()).$t->getName() : (string) $t,
];
foreach ($m->getParameters() as $p) {
if (!isset($parametersWithUndefinedConstants[$p->name])) {
$stack[] = (string) $p;
} else {
$t = \PHP_VERSION_ID >= 70000 ? $p->getType() : '';
$stack[] = $p->isOptional();
$stack[] = \PHP_VERSION_ID >= 70000 && $p->hasType() ? (\PHP_VERSION_ID >= 70100 ? $p->getType()->getName() : (string) $p->getType()) : '';
$stack[] = $t instanceof \ReflectionNamedType ? $t->getName() : (string) $t;
$stack[] = $p->isPassedByReference();
$stack[] = \PHP_VERSION_ID >= 50600 ? $p->isVariadic() : '';
$stack[] = $p->getName();

View File

@ -39,26 +39,36 @@ class ProxyHelper
if (!$type) {
return null;
}
if (!\is_string($type)) {
$name = $type instanceof \ReflectionNamedType ? $type->getName() : $type->__toString();
if ($type->isBuiltin()) {
return $noBuiltin ? null : $name;
$types = [];
foreach ($type instanceof \ReflectionUnionType ? $type->getTypes() : [$type] as $type) {
$name = $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type;
if (!\is_string($type) && $type->isBuiltin()) {
if (!$noBuiltin) {
$types[] = $name;
}
continue;
}
$lcName = strtolower($name);
$prefix = $noBuiltin ? '' : '\\';
if ('self' !== $lcName && 'parent' !== $lcName) {
$types[] = '' !== $prefix ? $prefix.$name : $name;
continue;
}
if (!$r instanceof \ReflectionMethod) {
continue;
}
if ('self' === $lcName) {
$types[] = $prefix.$r->getDeclaringClass()->name;
} else {
$types[] = ($parent = $r->getDeclaringClass()->getParentClass()) ? $prefix.$parent->name : null;
}
}
$lcName = strtolower($name);
$prefix = $noBuiltin ? '' : '\\';
if ('self' !== $lcName && 'parent' !== $lcName) {
return $prefix.$name;
}
if (!$r instanceof \ReflectionMethod) {
return null;
}
if ('self' === $lcName) {
return $prefix.$r->getDeclaringClass()->name;
}
return ($parent = $r->getDeclaringClass()->getParentClass()) ? $prefix.$parent->name : null;
return $types ? implode('|', $types) : null;
}
}

View File

@ -1076,7 +1076,7 @@ class OptionsResolver implements Options
return ($class = $parameter->getClass()) ? $class->name : null;
}
if (!($type = $parameter->getType()) || $type->isBuiltin()) {
if (!($type = $parameter->getType()) instanceof \ReflectionNamedType || $type->isBuiltin()) {
return null;
}

View File

@ -519,8 +519,9 @@ class PropertyAccessor implements PropertyAccessorInterface
// handle uninitialized properties in PHP >= 7.4
if (\PHP_VERSION_ID >= 70400 && preg_match('/^Typed property ([\w\\\]+)::\$(\w+) must not be accessed before initialization$/', $e->getMessage(), $matches)) {
$r = new \ReflectionProperty($matches[1], $matches[2]);
$type = ($type = $r->getType()) instanceof \ReflectionNamedType ? $type->getName() : (string) $type;
throw new AccessException(sprintf('The property "%s::$%s" is not readable because it is typed "%s". You should initialize it or declare a default value instead.', $r->getDeclaringClass()->getName(), $r->getName(), $r->getType()->getName()), 0, $e);
throw new AccessException(sprintf('The property "%s::$%s" is not readable because it is typed "%s". You should initialize it or declare a default value instead.', $r->getDeclaringClass()->getName(), $r->getName(), $type), 0, $e);
}
throw $e;

View File

@ -187,26 +187,26 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
$type = $this->extractFromReflectionType($reflectionType, $reflectionMethod);
// HHVM reports variadics with "array" but not builtin type hints
if (!$reflectionType->isBuiltin() && Type::BUILTIN_TYPE_ARRAY === $type->getBuiltinType()) {
if (1 === \count($type) && !$reflectionType->isBuiltin() && Type::BUILTIN_TYPE_ARRAY === $type[0]->getBuiltinType()) {
return null;
}
} elseif (preg_match('/^(?:[^ ]++ ){4}([a-zA-Z_\x7F-\xFF][^ ]++)/', $reflectionParameter, $info)) {
if (Type::BUILTIN_TYPE_ARRAY === $info[1]) {
$type = new Type(Type::BUILTIN_TYPE_ARRAY, $reflectionParameter->allowsNull(), null, true);
$type = [new Type(Type::BUILTIN_TYPE_ARRAY, $reflectionParameter->allowsNull(), null, true)];
} elseif (Type::BUILTIN_TYPE_CALLABLE === $info[1]) {
$type = new Type(Type::BUILTIN_TYPE_CALLABLE, $reflectionParameter->allowsNull());
$type = [new Type(Type::BUILTIN_TYPE_CALLABLE, $reflectionParameter->allowsNull())];
} else {
$type = new Type(Type::BUILTIN_TYPE_OBJECT, $reflectionParameter->allowsNull(), $this->resolveTypeName($info[1], $reflectionMethod));
$type = [new Type(Type::BUILTIN_TYPE_OBJECT, $reflectionParameter->allowsNull(), $this->resolveTypeName($info[1], $reflectionMethod))];
}
} else {
return null;
}
if (\in_array($prefix, $this->arrayMutatorPrefixes)) {
$type = new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $type);
if (1 === \count($type) && \in_array($prefix, $this->arrayMutatorPrefixes)) {
$type = [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), $type[0])];
}
return [$type];
return $type;
}
/**
@ -225,7 +225,7 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
}
if ($this->supportsParameterType && $reflectionType = $reflectionMethod->getReturnType()) {
return [$this->extractFromReflectionType($reflectionType, $reflectionMethod)];
return $this->extractFromReflectionType($reflectionType, $reflectionMethod);
}
return \in_array($prefix, ['is', 'can']) ? [new Type(Type::BUILTIN_TYPE_BOOL)] : null;
@ -234,24 +234,28 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
/**
* Extracts data from the PHP 7 reflection type.
*
* @return Type
* @return Type[]
*/
private function extractFromReflectionType(\ReflectionType $reflectionType, \ReflectionMethod $reflectionMethod)
{
$phpTypeOrClass = $reflectionType instanceof \ReflectionNamedType ? $reflectionType->getName() : $reflectionType->__toString();
$types = [];
$nullable = $reflectionType->allowsNull();
if (Type::BUILTIN_TYPE_ARRAY === $phpTypeOrClass) {
$type = new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true);
} elseif ('void' === $phpTypeOrClass) {
$type = new Type(Type::BUILTIN_TYPE_NULL, $nullable);
} elseif ($reflectionType->isBuiltin()) {
$type = new Type($phpTypeOrClass, $nullable);
} else {
$type = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $this->resolveTypeName($phpTypeOrClass, $reflectionMethod));
foreach ($reflectionType instanceof \ReflectionUnionType ? $reflectionType->getTypes() : [$reflectionType] as $type) {
$phpTypeOrClass = $reflectionType instanceof \ReflectionNamedType ? $reflectionType->getName() : (string) $type;
if (Type::BUILTIN_TYPE_ARRAY === $phpTypeOrClass) {
$types[] = new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true);
} elseif ('void' === $phpTypeOrClass || 'null' === $phpTypeOrClass) {
$types[] = new Type(Type::BUILTIN_TYPE_NULL, $nullable);
} elseif ($reflectionType->isBuiltin()) {
$types[] = new Type($phpTypeOrClass, $nullable);
} else {
$types[] = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $this->resolveTypeName($phpTypeOrClass, $reflectionMethod));
}
}
return $type;
return $types;
}
private function resolveTypeName($name, \ReflectionMethod $reflectionMethod)

View File

@ -388,7 +388,7 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
try {
if (\PHP_VERSION_ID < 70100 && null !== $parameterClass = $parameter->getClass()) {
$parameterClass = $parameterClass->name;
} elseif (\PHP_VERSION_ID >= 70100 && ($parameterType = $parameter->getType()) && !$parameterType->isBuiltin()) {
} elseif (\PHP_VERSION_ID >= 70100 && ($parameterType = $parameter->getType()) instanceof \ReflectionNamedType && !$parameterType->isBuiltin()) {
$parameterClass = $parameterType->getName();
new \ReflectionClass($parameterClass); // throws a \ReflectionException if the class doesn't exist
} else {

View File

@ -91,7 +91,7 @@ class ReflectionCaster
$prefix = Caster::PREFIX_VIRTUAL;
$a += [
$prefix.'name' => $c instanceof \ReflectionNamedType ? $c->getName() : $c->__toString(),
$prefix.'name' => $c instanceof \ReflectionNamedType ? $c->getName() : (string) $c,
$prefix.'allowsNull' => $c->allowsNull(),
$prefix.'isBuiltin' => $c->isBuiltin(),
];
@ -178,7 +178,7 @@ class ReflectionCaster
if (isset($a[$prefix.'returnType'])) {
$v = $a[$prefix.'returnType'];
$v = $v instanceof \ReflectionNamedType ? $v->getName() : $v->__toString();
$v = $v instanceof \ReflectionNamedType ? $v->getName() : (string) $v;
$a[$prefix.'returnType'] = new ClassStub($a[$prefix.'returnType']->allowsNull() ? '?'.$v : $v, [class_exists($v, false) || interface_exists($v, false) || trait_exists($v, false) ? $v : '', '']);
}
if (isset($a[$prefix.'class'])) {
@ -247,7 +247,7 @@ class ReflectionCaster
if (method_exists($c, 'getType')) {
if ($v = $c->getType()) {
$a[$prefix.'typeHint'] = $v instanceof \ReflectionNamedType ? $v->getName() : $v->__toString();
$a[$prefix.'typeHint'] = $v instanceof \ReflectionNamedType ? $v->getName() : (string) $v;
}
} elseif (preg_match('/^(?:[^ ]++ ){4}([a-zA-Z_\x7F-\xFF][^ ]++)/', $c, $v)) {
$a[$prefix.'typeHint'] = $v[1];