Completed
Push — master ( 771de8...ee4de0 )
by Nate
03:20 queued 01:31
created

TypeTokenFactory::getTypeFromDoc()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 38
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 8

Importance

Changes 0
Metric Value
cc 8
eloc 17
nc 7
nop 4
dl 0
loc 38
ccs 17
cts 17
cp 1
crap 8
rs 8.4444
c 0
b 0
f 0
1
<?php
2
/*
3
 * Copyright (c) Nate Brunette.
4
 * Distributed under the MIT License (http://opensource.org/licenses/MIT)
5
 */
6
7
declare(strict_types=1);
8
9
namespace Tebru\Gson\Internal;
10
11
use InvalidArgumentException;
12
use phpDocumentor\Reflection\DocBlock\Tag;
13
use phpDocumentor\Reflection\DocBlock\Tags\Param;
14
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
15
use phpDocumentor\Reflection\DocBlock\Tags\Var_;
16
use phpDocumentor\Reflection\DocBlockFactory;
17
use phpDocumentor\Reflection\Types\Compound;
18
use phpDocumentor\Reflection\Types\ContextFactory;
19
use phpDocumentor\Reflection\Types\Null_;
20
use ReflectionMethod;
21
use ReflectionProperty;
22
use Reflector;
23
use Tebru\AnnotationReader\AnnotationCollection;
24
use Tebru\Gson\Annotation\Type;
25
use Tebru\PhpType\TypeToken;
26
27
/**
28
 * Class TypeToken
29
 *
30
 * Creates a [@see TypeToken] for a property
31
 *
32
 * @author Nate Brunette <[email protected]>
33
 */
34
final class TypeTokenFactory
35
{
36
    /**
37
     * @var DocBlockFactory
38
     */
39
    private $docBlockFactory;
40
41
    /**
42
     * @var ContextFactory
43
     */
44
    private $contextFactory;
45
46
    /**
47
     * Constructor
48
     *
49
     * @param null|DocBlockFactory $docBlockFactory
50
     * @param null|ContextFactory $contextFactory
51
     */
52 37
    public function __construct(?DocBlockFactory $docBlockFactory = null, ?ContextFactory $contextFactory = null)
53
    {
54 37
        $this->docBlockFactory = $docBlockFactory ?? DocBlockFactory::createInstance();
55 37
        $this->contextFactory = $contextFactory ?? new ContextFactory();
56 37
    }
57
58
    /**
59
     * Attempts to guess a property type based method type hints, defaults to wildcard type
60
     *
61
     * - @Type annotation if it exists
62
     * - Getter return type if it exists
63
     * - Setter typehint if it exists
64
     * - Getter docblock
65
     * - Setter docblock
66
     * - Property docblock
67
     * - Property default value if it exists
68
     * - Setter default value if it exists
69
     * - Defaults to wildcard type
70
     *
71
     * @param AnnotationCollection $annotations
72
     * @param ReflectionProperty|null $property
73
     * @param ReflectionMethod|null $getterMethod
74
     * @param ReflectionMethod|null $setterMethod
75
     * @return TypeToken
76
     */
77 35
    public function create(
78
        AnnotationCollection $annotations,
79
        ?ReflectionMethod $getterMethod = null,
80
        ?ReflectionMethod $setterMethod = null,
81
        ?ReflectionProperty $property = null
82
    ): TypeToken {
83
        /** @var Type $typeAnnotation */
84 35
        $typeAnnotation = $annotations->get(Type::class);
85
86 35
        if (null !== $typeAnnotation) {
87 1
            return $typeAnnotation->getType();
88
        }
89
90 34
        if (null !== $getterMethod && null !== $getterMethod->getReturnType()) {
91 4
            $getterType = TypeToken::create((string)$getterMethod->getReturnType());
92 4
            return $this->checkGenericArray($getterType, $property, $getterMethod, $setterMethod);
93
        }
94
95 30
        if (null !== $setterMethod && [] !== $setterMethod->getParameters()) {
96 6
            $parameter = $setterMethod->getParameters()[0];
97 6
            if (null !== $parameter->getType()) {
98 2
                $setterType = TypeToken::create((string)$parameter->getType());
99 2
                return $this->checkGenericArray($setterType, $property, $getterMethod, $setterMethod);
100
            }
101
        }
102
103 28
        $type = $this->checkDocBlocks($property, $getterMethod, $setterMethod);
104 28
        if ($type !== null) {
105 20
            return $this->checkGenericArray(
106 20
                $type,
107 20
                $property,
108 20
                $getterMethod,
109 20
                $setterMethod
110
            );
111
        }
112
113 8
        if ($property !== null && $property->isDefault()) {
114 5
            $defaultProperty = $property->getDeclaringClass()->getDefaultProperties()[$property->getName()];
115 5
            if ($defaultProperty !== null) {
116 1
                return $this->checkGenericArray(
117 1
                    TypeToken::createFromVariable($defaultProperty),
118 1
                    $property,
119 1
                    $getterMethod,
120 1
                    $setterMethod
121
                );
122
            }
123
        }
124
125 7
        if (null !== $setterMethod && [] !== $setterMethod->getParameters()) {
126 3
            $parameter = $setterMethod->getParameters()[0];
127 3
            if ($parameter->isDefaultValueAvailable() && null !== $parameter->getDefaultValue()) {
128 1
                $setterType = TypeToken::create(\gettype($parameter->getDefaultValue()));
129 1
                return $this->checkGenericArray($setterType, $property, $getterMethod, $setterMethod);
130
            }
131
        }
132
133 6
        return TypeToken::create(TypeToken::WILDCARD);
134
    }
135
136
    /**
137
     * Attempt to get type from docblocks
138
     *
139
     * Checking in the order of property, getter, setter:
140
     *   Attempt to pull the type from the relevant portion of the docblock, then
141
     *   convert that type to a [@see TypeToken] converting to the full class name or
142
     *   generic array syntax if relevant.
143
     *
144
     * @param null|ReflectionProperty $property
145
     * @param null|ReflectionMethod $getter
146
     * @param null|ReflectionMethod $setter
147
     * @return null|TypeToken
148
     */
149 30
    private function checkDocBlocks(
150
        ?ReflectionProperty $property,
151
        ?ReflectionMethod $getter,
152
        ?ReflectionMethod $setter
153
    ): ?TypeToken {
154 30
        if ($getter !== null) {
155 4
            $docComment = $getter->getDocComment() ?: null;
156 4
            $tag = $this->getTypeFromDoc($getter, $docComment, 'return');
157 4
            if ($tag !== null) {
158 2
                return $tag;
159
            }
160
        }
161
162 28
        if ($setter !== null) {
163 5
            $docComment = $setter->getDocComment() ?: null;
164 5
            $parameters = $setter->getParameters();
165 5
            if (\count($parameters) === 1) {
166 5
                $tag = $this->getTypeFromDoc($setter, $docComment, 'param', $parameters[0]->getName());
167 5
                if ($tag !== null) {
168 2
                    return $tag;
169
                }
170
            }
171
        }
172
173 26
        if ($property !== null) {
174 23
            $docComment = $property->getDocComment() ?: null;
175 23
            $tag = $this->getTypeFromDoc($property, $docComment, 'var');
176 23
            if ($tag !== null) {
177 18
                return $tag;
178
            }
179
        }
180
181 8
        return null;
182
    }
183
184
    /**
185
     * Get [@see TypeToken] from docblock
186
     *
187
     * @param Reflector $reflector
188
     * @param null|string $docComment
189
     * @param string $tagName
190
     * @param null|string $variableName
191
     * @return null|TypeToken
192
     */
193 30
    private function getTypeFromDoc(
194
        Reflector $reflector,
195
        ?string $docComment,
196
        string $tagName,
197
        ?string $variableName = null
198
    ): ?TypeToken {
199 30
        if ($docComment === null || $docComment === '') {
200 3
            return null;
201
        }
202
203
        try {
204 27
            $docblock = $this->docBlockFactory->create(
205 27
                $docComment,
206 27
                $this->contextFactory->createFromReflector($reflector)
207
            );
208 1
        } /** @noinspection BadExceptionsProcessingInspection */ catch (InvalidArgumentException $exception) {
209
            // exception likely caused by an empty type
210 1
            return null;
211
        }
212
213 26
        $tags = $docblock->getTagsByName($tagName);
214
215 26
        if (empty($tags)) {
216 1
            return null;
217
        }
218
219 25
        if ($tagName !== 'param') {
220 22
            return $this->getTypeFromTag($tags[0]);
221
        }
222
223
        /** @var Param $tag */
224 3
        foreach ($tags as $tag) {
225 3
            if ($tag->getVariableName() === $variableName) {
226 3
                return $this->getTypeFromTag($tag);
227
            }
228
        }
229
230 1
        return null;
231
    }
232
233
    /**
234
     * Get the type token from tag
235
     *
236
     * @param Var_|Param|Return_|Tag $tag
237
     * @return null|TypeToken
238
     */
239 24
    private function getTypeFromTag(Tag $tag): ?TypeToken
240
    {
241 24
        $type = $tag->getType();
242 24
        if (!$type instanceof Compound) {
243 19
             $type = $this->stripSlashes((string)$type);
244
245 19
            return TypeToken::create($this->unwrapArray($type));
246
        }
247
248 5
        $types = \iterator_to_array($type->getIterator());
249 5
        $types = \array_values(\array_filter($types, function ($innerType) {
250 5
            return !$innerType instanceof Null_;
251 5
        }));
252 5
        $count = \count($types);
253
254 5
        if ($count !== 1) {
255 2
            return null;
256
        }
257
258 3
        $type = $this->stripSlashes((string)$types[0]);
259
260 3
        return TypeToken::create($this->unwrapArray($type));
261
    }
262
263
    /**
264
     * Remove the initial '\' if it exists
265
     *
266
     * @param string $type
267
     * @return string
268
     */
269 22
    private function stripSlashes(string $type): string
270
    {
271 22
        if ($type[0] === '\\') {
272 12
            $type = substr($type, 1);
273
        }
274
275 22
        return $type;
276
    }
277
278
    /**
279
     * Converts types as int[] to array<int>
280
     *
281
     * @param string $type
282
     * @return string
283
     */
284 22
    private function unwrapArray(string $type): string
285
    {
286
        // if not in array syntax
287 22
        if (\strpos($type, '[]') === false) {
288
            // convert mixed to wildcard
289 17
            return $type === 'mixed' ? TypeToken::WILDCARD : $type;
290
        }
291
292 5
        $parts = \explode('[]', $type);
293 5
        $primaryType = \array_shift($parts);
294
295 5
        $numParts = \count($parts);
296
297
        // same as mixed
298 5
        if ($primaryType === 'array') {
299 1
            $primaryType = TypeToken::WILDCARD;
300 1
            $numParts++;
301
        }
302
303 5
        return \str_repeat('array<', $numParts) . $primaryType . \str_repeat('>', $numParts);
304
    }
305
306
    /**
307
     * If the type is just 'array', check the docblock to see if there's a more specific type
308
     *
309
     * @param TypeToken $type
310
     * @param null|ReflectionProperty $property
311
     * @param null|ReflectionMethod $getter
312
     * @param null|ReflectionMethod $setter
313
     * @return TypeToken
314
     */
315 28
    private function checkGenericArray(
316
        TypeToken $type,
317
        ?ReflectionProperty $property,
318
        ?ReflectionMethod $getter,
319
        ?ReflectionMethod $setter
320
    ): TypeToken {
321 28
        return $type->isArray()
322 6
            ? $this->checkDocBlocks($property, $getter, $setter) ?? $type
323 28
            : $type;
324
    }
325
}
326