Skip to content

Commit 14d3d5a

Browse files
committed
Rewrote #901 so that ReflectionUnionType is kept internally for union types
When an union type of `T|null` or `?T` is met, BetterReflection will (from now on) keep a `ReflectionUnionType` internally, while exposing a `ReflectionNamedType` with `ReflectionNamedType#allowsNull() === true` only at adapter level. While this is a BC break, it leads to a much cleaner API around handling `null` types, and inspecting types for type analysis. Ref: #902 (comment) Ref: Roave/BackwardCompatibilityCheck#324
1 parent 0d78168 commit 14d3d5a

25 files changed

+292
-115
lines changed

src/Reflection/Adapter/ReflectionEnum.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ public function isBacked(): bool
509509
public function getBackingType(): ?ReflectionNamedType
510510
{
511511
if ($this->betterReflectionEnum->isBacked()) {
512-
return new ReflectionNamedType($this->betterReflectionEnum->getBackingType());
512+
return new ReflectionNamedType($this->betterReflectionEnum->getBackingType(), false);
513513
}
514514

515515
return null;

src/Reflection/Adapter/ReflectionNamedType.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
*/
1313
final class ReflectionNamedType extends CoreReflectionNamedType
1414
{
15-
public function __construct(private BetterReflectionNamedType $betterReflectionType)
15+
public function __construct(private BetterReflectionNamedType $betterReflectionType, private bool $allowsNull)
1616
{
1717
}
1818

@@ -23,12 +23,13 @@ public function getName(): string
2323

2424
public function __toString(): string
2525
{
26-
return $this->betterReflectionType->__toString();
26+
return ($this->allowsNull ? '?' : '')
27+
. $this->betterReflectionType->__toString();
2728
}
2829

2930
public function allowsNull(): bool
3031
{
31-
return $this->betterReflectionType->allowsNull();
32+
return $this->allowsNull;
3233
}
3334

3435
public function isBuiltin(): bool

src/Reflection/Adapter/ReflectionType.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
use Roave\BetterReflection\Reflection\ReflectionNamedType as BetterReflectionNamedType;
1010
use Roave\BetterReflection\Reflection\ReflectionUnionType as BetterReflectionUnionType;
1111

12+
use function array_filter;
13+
use function array_values;
14+
use function count;
15+
1216
abstract class ReflectionType extends CoreReflectionType
1317
{
1418
public static function fromTypeOrNull(BetterReflectionNamedType|BetterReflectionUnionType|BetterReflectionIntersectionType|null $betterReflectionType): ReflectionUnionType|ReflectionNamedType|ReflectionIntersectionType|null
@@ -18,13 +22,27 @@ public static function fromTypeOrNull(BetterReflectionNamedType|BetterReflection
1822
}
1923

2024
if ($betterReflectionType instanceof BetterReflectionUnionType) {
25+
// php-src has this weird behavior where a union type composed of a single type `T`
26+
// together with `null` means that a `ReflectionNamedType` for `?T` is produced,
27+
// rather than `T|null`. This is done to keep BC compatibility with PHP 7.1 (which
28+
// introduced nullable types), but at reflection level, this is mostly a nuisance.
29+
// In order to keep parity with core, we stashed this weird behavior in here.
30+
$nonNullTypes = array_values(array_filter(
31+
$betterReflectionType->getTypes(),
32+
static fn (BetterReflectionNamedType $type): bool => $type->getName() !== 'null',
33+
));
34+
35+
if ($betterReflectionType->allowsNull() && count($nonNullTypes) === 1) {
36+
return new ReflectionNamedType($nonNullTypes[0], true);
37+
}
38+
2139
return new ReflectionUnionType($betterReflectionType);
2240
}
2341

2442
if ($betterReflectionType instanceof BetterReflectionIntersectionType) {
2543
return new ReflectionIntersectionType($betterReflectionType);
2644
}
2745

28-
return new ReflectionNamedType($betterReflectionType);
46+
return new ReflectionNamedType($betterReflectionType, false);
2947
}
3048
}

src/Reflection/ReflectionIntersectionType.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function __construct(
2222
ReflectionParameter|ReflectionMethod|ReflectionFunction|ReflectionEnum|ReflectionProperty $owner,
2323
IntersectionType $type,
2424
) {
25-
parent::__construct($reflector, $owner, false);
25+
parent::__construct($reflector, $owner);
2626

2727
$this->types = array_filter(
2828
array_map(static fn (Node\Identifier|Node\Name $type): ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType => ReflectionType::createFromNode($reflector, $owner, $type), $type->types),
@@ -38,6 +38,11 @@ public function getTypes(): array
3838
return $this->types;
3939
}
4040

41+
public function allowsNull(): bool
42+
{
43+
return false;
44+
}
45+
4146
public function __toString(): string
4247
{
4348
return implode('&', array_map(static fn (ReflectionNamedType $type): string => $type->__toString(), $this->types));

src/Reflection/ReflectionNamedType.php

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,8 @@ public function __construct(
4040
Reflector $reflector,
4141
ReflectionParameter|ReflectionMethod|ReflectionFunction|ReflectionEnum|ReflectionProperty $owner,
4242
Identifier|Name $type,
43-
bool $allowsNull,
4443
) {
45-
parent::__construct($reflector, $owner, $allowsNull);
44+
parent::__construct($reflector, $owner);
4645

4746
$this->name = $type->toString();
4847
}
@@ -102,13 +101,13 @@ public function getClass(): ReflectionClass
102101
throw new LogicException(sprintf('The type %s cannot be resolved to class', $this->name));
103102
}
104103

105-
public function __toString(): string
104+
public function allowsNull(): bool
106105
{
107-
$name = '';
108-
if ($this->allowsNull()) {
109-
$name .= '?';
110-
}
106+
return false;
107+
}
111108

112-
return $name . $this->getName();
109+
public function __toString(): string
110+
{
111+
return $this->getName();
113112
}
114113
}

src/Reflection/ReflectionType.php

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,11 @@
1111
use PhpParser\Node\UnionType;
1212
use Roave\BetterReflection\Reflector\Reflector;
1313

14-
use function array_filter;
15-
use function array_values;
16-
use function count;
17-
1814
abstract class ReflectionType
1915
{
2016
protected function __construct(
2117
protected Reflector $reflector,
2218
protected ReflectionParameter|ReflectionMethod|ReflectionFunction|ReflectionEnum|ReflectionProperty $owner,
23-
private bool $allowsNull,
2419
) {
2520
}
2621

@@ -31,41 +26,36 @@ public static function createFromNode(
3126
Reflector $reflector,
3227
ReflectionParameter|ReflectionMethod|ReflectionFunction|ReflectionEnum|ReflectionProperty $owner,
3328
Identifier|Name|NullableType|UnionType|IntersectionType $type,
34-
bool $forceAllowsNull = false,
29+
bool $allowsNull = false,
3530
): ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType {
36-
$allowsNull = $forceAllowsNull;
3731
if ($type instanceof NullableType) {
3832
$type = $type->type;
3933
$allowsNull = true;
4034
}
4135

4236
if ($type instanceof Identifier || $type instanceof Name) {
43-
return new ReflectionNamedType($reflector, $owner, $type, $allowsNull);
37+
if ($allowsNull) {
38+
return new ReflectionUnionType(
39+
$reflector,
40+
$owner,
41+
new UnionType([$type, new Identifier('null')]),
42+
);
43+
}
44+
45+
return new ReflectionNamedType($reflector, $owner, $type);
4446
}
4547

4648
if ($type instanceof IntersectionType) {
4749
return new ReflectionIntersectionType($reflector, $owner, $type);
4850
}
4951

50-
$nonNullTypes = array_values(array_filter(
51-
$type->types,
52-
static fn (Identifier|Name $type): bool => $type->toString() !== 'null',
53-
));
54-
55-
if (count($nonNullTypes) === 1) {
56-
return self::createFromNode($reflector, $owner, $nonNullTypes[0], true);
57-
}
58-
59-
return new ReflectionUnionType($reflector, $owner, $type, $allowsNull);
52+
return new ReflectionUnionType($reflector, $owner, $type);
6053
}
6154

6255
/**
63-
* Does the parameter allow null?
56+
* Does the type allow null?
6457
*/
65-
public function allowsNull(): bool
66-
{
67-
return $this->allowsNull;
68-
}
58+
abstract public function allowsNull(): bool;
6959

7060
/**
7161
* Convert this string type to a string

src/Reflection/ReflectionUnionType.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@ public function __construct(
2121
Reflector $reflector,
2222
ReflectionParameter|ReflectionMethod|ReflectionFunction|ReflectionEnum|ReflectionProperty $owner,
2323
UnionType $type,
24-
bool $allowsNull,
2524
) {
26-
parent::__construct($reflector, $owner, $allowsNull);
25+
parent::__construct($reflector, $owner);
2726

2827
$this->types = array_filter(
2928
array_map(static fn (Node\Identifier|Node\Name $type): ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType => ReflectionType::createFromNode($reflector, $owner, $type), $type->types),
@@ -39,6 +38,17 @@ public function getTypes(): array
3938
return $this->types;
4039
}
4140

41+
public function allowsNull(): bool
42+
{
43+
foreach ($this->types as $type) {
44+
if ($type->getName() === 'null') {
45+
return true;
46+
}
47+
}
48+
49+
return false;
50+
}
51+
4252
public function __toString(): string
4353
{
4454
return implode('|', array_map(static fn (ReflectionType $type): string => $type->__toString(), $this->types));

src/Reflection/StringCast/ReflectionFunctionStringCast.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ private static function parametersToString(ReflectionFunction $functionReflectio
7272

7373
private static function returnTypeToString(ReflectionFunction $methodReflection): string
7474
{
75-
return $methodReflection->getReturnType()?->__toString() ?? '';
75+
$type = $methodReflection->getReturnType();
76+
77+
if ($type === null) {
78+
return '';
79+
}
80+
81+
return ReflectionTypeStringCast::toString($type);
7682
}
7783
}

src/Reflection/StringCast/ReflectionMethodStringCast.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ private static function parametersToString(ReflectionMethod $methodReflection):
126126

127127
private static function returnTypeToString(ReflectionMethod $methodReflection): string
128128
{
129-
return $methodReflection->getReturnType()?->__toString() ?? '';
129+
$type = $methodReflection->getReturnType();
130+
131+
if ($type === null) {
132+
return '';
133+
}
134+
135+
return ReflectionTypeStringCast::toString($type);
130136
}
131137
}

src/Reflection/StringCast/ReflectionParameterStringCast.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@ public static function toString(ReflectionParameter $parameterReflection): strin
3434

3535
private static function typeToString(ReflectionParameter $parameterReflection): string
3636
{
37-
if (! $parameterReflection->hasType()) {
37+
$type = $parameterReflection->getType();
38+
39+
if ($type === null) {
3840
return '';
3941
}
4042

41-
return (string) $parameterReflection->getType() . ' ';
43+
return ReflectionTypeStringCast::toString($type) . ' ';
4244
}
4345

4446
private static function valueToString(ReflectionParameter $parameterReflection): string

0 commit comments

Comments
 (0)