Passed
Pull Request — master (#1549)
by
unknown
02:36
created

TypedPropertiesDriver::allowsNull()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 8
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 14
rs 9.6111
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
     * In order to deserialize non-discriminated unions, each possible type is attempted in turn.
52
     * Therefore, the types must be ordered from most specific to least specific, so that the most specific type is attempted first.
53
     *
54
     * ReflectionUnionType::getTypes() does not return types in that order, so we need to reorder them.
55
     *
56
     * This method reorders the types in the following order:
57
     *  - primitives in speficity order: null, true, false, int, float, bool, string
58
     *  - classes and interaces in order of most number of required properties
59
     */
60
    private function reorderTypes(array $type): array
61
    {
62
        $self = $this;
63
        if ($type['params']) {
64
            uasort($type['params'], static function ($a, $b) use ($self) {
65
                if (\class_exists($a['name']) && \class_exists($b['name'])) {
66
                    $aMetadata = $self->loadMetadataForClass(new \ReflectionClass($a['name']));
67
                    $bMetadata = $self->loadMetadataForClass(new \ReflectionClass($b['name']));
68
                    $aRequiredPropertyCount = 0;
69
                    $bRequiredPropertyCount = 0;
70
                    foreach ($aMetadata->propertyMetadata as $propertyMetadata) {
71
                        if ($propertyMetadata->type && !$self->allowsNull($propertyMetadata->type)) {
72
                            $aRequiredPropertyCount++;
73
                        }
74
                    }
75
76
                    foreach ($bMetadata->propertyMetadata as $propertyMetadata) {
77
                        if ($propertyMetadata->type && !$self->allowsNull($propertyMetadata->type)) {
78
                            $bRequiredPropertyCount++;
79
                        }
80
                    }
81
82
                    return $bRequiredPropertyCount <=> $aRequiredPropertyCount;
83
                }
84
85
                if (\class_exists($a['name'])) {
86
                    return 1;
87
                }
88
89
                if (\class_exists($b['name'])) {
90
                    return -1;
91
                }
92
93
                $order = ['null' => 0, 'true' => 1, 'false' => 2, 'bool' => 3, 'int' => 4, 'float' => 5, 'string' => 6];
94
95
                return ($order[$a['name']] ?? 7) <=> ($order[$b['name']] ?? 7);
96
            });
97
        }
98
99
        return $type;
100
    }
101
102
    private function allowsNull(array $type)
103
    {
104
        $allowsNull = false;
105
        if ('union' === $type['name']) {
106
            foreach ($type['params'] as $param) {
107
                if ('NULL' === $param['name']) {
108
                    $allowsNull = true;
109
                }
110
            }
111
        } elseif ('NULL' === $type['name']) {
112
            $allowsNull = true;
113
        }
114
115
        return $allowsNull;
116
    }
117
118
    private function getDefaultWhiteList(): array
119
    {
120
        return [
121
            'int',
122
            'float',
123
            'bool',
124
            'boolean',
125
            'string',
126
            'double',
127
            'iterable',
128
            'resource',
129
        ];
130
    }
131
132
    /**
133
     * @return SerializerClassMetadata|null
134
     */
135
    public function loadMetadataForClass(ReflectionClass $class): ?ClassMetadata
136
    {
137
        $classMetadata = $this->delegate->loadMetadataForClass($class);
138
139
        if (null === $classMetadata) {
140
            return null;
141
        }
142
143
        \assert($classMetadata instanceof SerializerClassMetadata);
144
145
        // We base our scan on the internal driver's property list so that we
146
        // respect any internal allow/blocklist like in the AnnotationDriver
147
        foreach ($classMetadata->propertyMetadata as $propertyMetadata) {
148
            // If the inner driver provides a type, don't guess anymore.
149
            if ($propertyMetadata->type) {
150
                continue;
151
            }
152
153
            try {
154
                $reflectionType = $this->getReflectionType($propertyMetadata);
155
156
                if ($this->shouldTypeHint($reflectionType)) {
157
                    $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

157
                    /** @scrutinizer ignore-call */ 
158
                    $type = $reflectionType->getName();
Loading history...
158
159
                    $propertyMetadata->setType($this->typeParser->parse($type));
160
                } elseif ($this->shouldTypeHintUnion($reflectionType)) {
161
                    $propertyMetadata->setType($this->reorderTypes([
162
                        'name' => 'union',
163
                        '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

163
                        'params' => array_map(fn (string $type) => $this->typeParser->parse($type), $reflectionType->/** @scrutinizer ignore-call */ getTypes()),
Loading history...
164
                    ]));
165
                }
166
            } catch (ReflectionException $e) {
167
                continue;
168
            }
169
        }
170
171
        return $classMetadata;
172
    }
173
174
    private function getReflectionType(PropertyMetadata $propertyMetadata): ?ReflectionType
175
    {
176
        if ($this->isNotSupportedVirtualProperty($propertyMetadata)) {
177
            return null;
178
        }
179
180
        if ($propertyMetadata instanceof VirtualPropertyMetadata) {
181
            return (new ReflectionMethod($propertyMetadata->class, $propertyMetadata->getter))
182
                ->getReturnType();
183
        }
184
185
        return (new ReflectionProperty($propertyMetadata->class, $propertyMetadata->name))
186
            ->getType();
187
    }
188
189
    private function isNotSupportedVirtualProperty(PropertyMetadata $propertyMetadata): bool
190
    {
191
        return $propertyMetadata instanceof StaticPropertyMetadata
192
            || $propertyMetadata instanceof ExpressionPropertyMetadata;
193
    }
194
195
    /**
196
     * @phpstan-assert-if-true \ReflectionNamedType $reflectionType
197
     */
198
    private function shouldTypeHint(?ReflectionType $reflectionType): bool
199
    {
200
        if (!$reflectionType instanceof ReflectionNamedType) {
201
            return false;
202
        }
203
204
        if (in_array($reflectionType->getName(), $this->allowList, true)) {
205
            return true;
206
        }
207
208
        return class_exists($reflectionType->getName())
209
            || interface_exists($reflectionType->getName());
210
    }
211
212
    /**
213
     * @phpstan-assert-if-true \ReflectionUnionType $reflectionType
214
     */
215
    private function shouldTypeHintUnion(?ReflectionType $reflectionType)
216
    {
217
        if (!$reflectionType instanceof \ReflectionUnionType) {
218
            return false;
219
        }
220
221
        foreach ($reflectionType->getTypes() as $type) {
222
            if ($this->shouldTypeHint($type)) {
223
                return true;
224
            }
225
        }
226
227
        return false;
228
    }
229
}
230