Completed
Push — master ( b4285f...f974e5 )
by Marcin
18s queued 15s
created

TypedPropertiesDriver::shouldTypeHintInsideUnion()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 1
dl 0
loc 3
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 $types): array
61
    {
62
        uasort($types, static function ($a, $b) {
63
            $order = ['null' => 0, 'true' => 1, 'false' => 2, 'bool' => 3, 'int' => 4, 'float' => 5, 'array' => 6, 'string' => 7];
64
65
            return ($order[$a['name']] ?? 8) <=> ($order[$b['name']] ?? 8);
66
        });
67
68
        return $types;
69
    }
70
71
    private function getDefaultWhiteList(): array
72
    {
73
        return [
74
            'int',
75
            'float',
76
            'bool',
77
            'boolean',
78
            'true',
79
            'false',
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([
117
                        'name' => 'union',
118
                        'params' => [
119
                            $this->reorderTypes(
120
                                array_map(
121
                                    fn (string $type) => $this->typeParser->parse($type),
122
                                    array_filter($reflectionType->getTypes(), [$this, 'shouldTypeHintInsideUnion']),
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

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