Passed
Pull Request — master (#1546)
by
unknown
11:49
created

TypedPropertiesDriver::shouldTypeHintUnion()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 4
nop 1
dl 0
loc 13
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace JMS\Serializer\Metadata\Driver;
6
7
use JMS\Serializer\Metadata\ClassMetadata as SerializerClassMetadata;
8
use JMS\Serializer\Metadata\ExpressionPropertyMetadata;
9
use JMS\Serializer\Metadata\PropertyMetadata;
10
use JMS\Serializer\Metadata\StaticPropertyMetadata;
11
use JMS\Serializer\Metadata\VirtualPropertyMetadata;
12
use JMS\Serializer\Type\Parser;
13
use JMS\Serializer\Type\ParserInterface;
14
use Metadata\ClassMetadata;
15
use Metadata\Driver\DriverInterface;
16
use ReflectionClass;
17
use ReflectionException;
18
use ReflectionMethod;
19
use ReflectionNamedType;
20
use ReflectionProperty;
21
use ReflectionType;
22
23
class TypedPropertiesDriver implements DriverInterface
24
{
25
    /**
26
     * @var DriverInterface
27
     */
28
    protected $delegate;
29
30
    /**
31
     * @var ParserInterface
32
     */
33
    protected $typeParser;
34
35
    /**
36
     * @var string[]
37
     */
38
    private $allowList;
39
40
    /**
41
     * @param string[] $allowList
42
     */
43
    public function __construct(DriverInterface $delegate, ?ParserInterface $typeParser = null, array $allowList = [])
44
    {
45
        $this->delegate = $delegate;
46
        $this->typeParser = $typeParser ?: new Parser();
47
        $this->allowList = array_merge($allowList, $this->getDefaultWhiteList());
48
    }
49
50
    /**
51
     *  ReflectionUnionType::getTypes() returns the types sorted according to these rules:
52
     * - Classes, interfaces, traits, iterable (replaced by Traversable), ReflectionIntersectionType objects, parent and self:
53
     *     these types will be returned first, in the order in which they were declared.
54
     * - static and all built-in types (iterable replaced by array) will come next. They will always be returned in this order:
55
     *     static, callable, array, string, int, float, bool (or false or true), null.
56
     *
57
     * For determining types of primitives, it is necessary to reorder primitives so that they are tested from lowest specificity to highest:
58
     * i.e. null, true, false, int, float, bool, string
59
     */
60
    private function reorderTypes(array $type): array
61
    {
62
        if ($type['params']) {
63
            uasort($type['params'], static function ($a, $b) {
64
                $order = ['null' => 0, 'true' => 1, 'false' => 2, 'bool' => 3, 'int' => 4, 'float' => 5, 'string' => 6];
65
66
                return ($order[$a['name']] ?? 7) <=> ($order[$b['name']] ?? 7);
67
            });
68
        }
69
70
        return $type;
71
    }
72
73
    private function getDefaultWhiteList(): array
74
    {
75
        return [
76
            'int',
77
            'float',
78
            'bool',
79
            'boolean',
80
            'string',
81
            'double',
82
            'iterable',
83
            'resource',
84
        ];
85
    }
86
87
    /**
88
     * @return SerializerClassMetadata|null
89
     */
90
    public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata
91
    {
92
        $classMetadata = $this->delegate->loadMetadataForClass($class);
93
94
        if (null === $classMetadata) {
95
            return null;
96
        }
97
98
        \assert($classMetadata instanceof SerializerClassMetadata);
99
100
        // We base our scan on the internal driver's property list so that we
101
        // respect any internal allow/blocklist like in the AnnotationDriver
102
        foreach ($classMetadata->propertyMetadata as $propertyMetadata) {
103
            // If the inner driver provides a type, don't guess anymore.
104
            if ($propertyMetadata->type) {
105
                continue;
106
            }
107
108
            try {
109
                $reflectionType = $this->getReflectionType($propertyMetadata);
110
111
                if ($this->shouldTypeHint($reflectionType)) {
112
                    $type = $reflectionType->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

112
                    /** @scrutinizer ignore-call */ 
113
                    $type = $reflectionType->getName();
Loading history...
113
114
                    $propertyMetadata->setType($this->typeParser->parse($type));
115
                } elseif ($this->shouldTypeHintUnion($reflectionType)) {
116
                    $propertyMetadata->setType($this->reorderTypes([
117
                        'name' => 'union',
118
                        'params' => array_map(fn (string $type) => $this->typeParser->parse($type), $reflectionType->getTypes()),
0 ignored issues
show
Bug introduced by
The method getTypes() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionUnionType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

118
                        'params' => array_map(fn (string $type) => $this->typeParser->parse($type), $reflectionType->/** @scrutinizer ignore-call */ getTypes()),
Loading history...
119
                    ]));
120
                }
121
            } catch (ReflectionException $e) {
122
                continue;
123
            }
124
        }
125
126
        return $classMetadata;
127
    }
128
129
    private function getReflectionType(PropertyMetadata $propertyMetadata): ?ReflectionType
130
    {
131
        if ($this->isNotSupportedVirtualProperty($propertyMetadata)) {
132
            return null;
133
        }
134
135
        if ($propertyMetadata instanceof VirtualPropertyMetadata) {
136
            return (new ReflectionMethod($propertyMetadata->class, $propertyMetadata->getter))
137
                ->getReturnType();
138
        }
139
140
        return (new ReflectionProperty($propertyMetadata->class, $propertyMetadata->name))
141
            ->getType();
142
    }
143
144
    private function isNotSupportedVirtualProperty(PropertyMetadata $propertyMetadata): bool
145
    {
146
        return $propertyMetadata instanceof StaticPropertyMetadata
147
            || $propertyMetadata instanceof ExpressionPropertyMetadata;
148
    }
149
150
    /**
151
     * @phpstan-assert-if-true \ReflectionNamedType $reflectionType
152
     */
153
    private function shouldTypeHint(?ReflectionType $reflectionType): bool
154
    {
155
        if (!$reflectionType instanceof ReflectionNamedType) {
156
            return false;
157
        }
158
159
        if (in_array($reflectionType->getName(), $this->allowList, true)) {
160
            return true;
161
        }
162
163
        return class_exists($reflectionType->getName())
164
            || interface_exists($reflectionType->getName());
165
    }
166
167
    /**
168
     * @phpstan-assert-if-true \ReflectionUnionType $reflectionType
169
     */
170
    private function shouldTypeHintUnion(?ReflectionType $reflectionType)
171
    {
172
        if (!$reflectionType instanceof \ReflectionUnionType) {
173
            return false;
174
        }
175
176
        foreach ($reflectionType->getTypes() as $type) {
177
            if ($this->shouldTypeHint($type)) {
178
                return true;
179
            }
180
        }
181
182
        return false;
183
    }
184
}
185