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 ReflectionMethod; |
12
|
|
|
use ReflectionProperty; |
13
|
|
|
use Tebru\AnnotationReader\AnnotationCollection; |
14
|
|
|
use Tebru\Gson\Annotation\Type; |
15
|
|
|
use Tebru\PhpType\TypeToken; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Class TypeToken |
19
|
|
|
* |
20
|
|
|
* Creates a [@see TypeToken] for a property |
21
|
|
|
* |
22
|
|
|
* @author Nate Brunette <[email protected]> |
23
|
|
|
*/ |
24
|
|
|
final class TypeTokenFactory |
25
|
|
|
{ |
26
|
|
|
/** |
27
|
|
|
* Regex to get full class names from imported use statements |
28
|
|
|
*/ |
29
|
|
|
private const USE_PATTERN = '/use\s+(?:(?<namespace>[^;]+\\\\)[^;]*[\s,{](?<classname>\w+)\s+as\s+:REPLACE:[^;]*};|(?<group>[^;]+\\\\){[^;]*:REPLACE:[^;]*};|(?<alias>[^;]+)\s+as\s+:REPLACE:;|(?<default>[^;]+:REPLACE:);)/'; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Attempts to guess a property type based method type hints, defaults to wildcard type |
33
|
|
|
* |
34
|
|
|
* - @Type annotation if it exists |
35
|
|
|
* - Getter return type if it exists |
36
|
|
|
* - Setter typehint if it exists |
37
|
|
|
* - Getter docblock |
38
|
|
|
* - Setter docblock |
39
|
|
|
* - Property docblock |
40
|
|
|
* - Property default value if it exists |
41
|
|
|
* - Setter default value if it exists |
42
|
|
|
* - Defaults to wildcard type |
43
|
|
|
* |
44
|
|
|
* @param AnnotationCollection $annotations |
45
|
|
|
* @param ReflectionProperty|null $property |
46
|
|
|
* @param ReflectionMethod|null $getterMethod |
47
|
|
|
* @param ReflectionMethod|null $setterMethod |
48
|
|
|
* @return TypeToken |
49
|
|
|
*/ |
50
|
34 |
|
public function create( |
51
|
|
|
AnnotationCollection $annotations, |
52
|
|
|
?ReflectionMethod $getterMethod = null, |
53
|
|
|
?ReflectionMethod $setterMethod = null, |
54
|
|
|
?ReflectionProperty $property = null |
55
|
|
|
): TypeToken { |
56
|
|
|
/** @var Type $typeAnnotation */ |
57
|
34 |
|
$typeAnnotation = $annotations->get(Type::class); |
58
|
|
|
|
59
|
34 |
|
if (null !== $typeAnnotation) { |
60
|
1 |
|
return $typeAnnotation->getType(); |
61
|
|
|
} |
62
|
|
|
|
63
|
33 |
|
if (null !== $getterMethod && null !== $getterMethod->getReturnType()) { |
64
|
4 |
|
$getterType = TypeToken::create((string)$getterMethod->getReturnType()); |
65
|
4 |
|
return $this->checkGenericArray($getterType, $property, $getterMethod, $setterMethod); |
66
|
|
|
} |
67
|
|
|
|
68
|
29 |
|
if (null !== $setterMethod && [] !== $setterMethod->getParameters()) { |
69
|
5 |
|
$parameter = $setterMethod->getParameters()[0]; |
70
|
5 |
|
if (null !== $parameter->getType()) { |
71
|
2 |
|
$setterType = TypeToken::create((string)$parameter->getType()); |
72
|
2 |
|
return $this->checkGenericArray($setterType, $property, $getterMethod, $setterMethod); |
73
|
|
|
} |
74
|
|
|
} |
75
|
|
|
|
76
|
27 |
|
$type = $this->checkDocBlocks($property, $getterMethod, $setterMethod); |
77
|
27 |
|
if ($type !== null) { |
78
|
21 |
|
return $this->checkGenericArray( |
79
|
21 |
|
$type, |
80
|
21 |
|
$property, |
81
|
21 |
|
$getterMethod, |
82
|
21 |
|
$setterMethod |
83
|
|
|
); |
84
|
|
|
} |
85
|
|
|
|
86
|
6 |
|
if ($property !== null && $property->isDefault()) { |
87
|
4 |
|
$defaultProperty = $property->getDeclaringClass()->getDefaultProperties()[$property->getName()]; |
88
|
4 |
|
if ($defaultProperty !== null) { |
89
|
1 |
|
return $this->checkGenericArray( |
90
|
1 |
|
TypeToken::createFromVariable($defaultProperty), |
91
|
1 |
|
$property, |
92
|
1 |
|
$getterMethod, |
93
|
1 |
|
$setterMethod |
94
|
|
|
); |
95
|
|
|
} |
96
|
|
|
} |
97
|
|
|
|
98
|
5 |
|
if (null !== $setterMethod && [] !== $setterMethod->getParameters()) { |
99
|
2 |
|
$parameter = $setterMethod->getParameters()[0]; |
100
|
2 |
|
if ($parameter->isDefaultValueAvailable() && null !== $parameter->getDefaultValue()) { |
101
|
1 |
|
$setterType = TypeToken::create(\gettype($parameter->getDefaultValue())); |
102
|
1 |
|
return $this->checkGenericArray($setterType, $property, $getterMethod, $setterMethod); |
103
|
|
|
} |
104
|
|
|
} |
105
|
|
|
|
106
|
4 |
|
return TypeToken::create(TypeToken::WILDCARD); |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* Attempt to get type from docblocks |
111
|
|
|
* |
112
|
|
|
* Checking in the order of property, getter, setter: |
113
|
|
|
* Attempt to pull the type from the relevant portion of the docblock, then |
114
|
|
|
* convert that type to a [@see TypeToken] converting to the full class name or |
115
|
|
|
* generic array syntax if relevant. |
116
|
|
|
* |
117
|
|
|
* @param null|ReflectionProperty $property |
118
|
|
|
* @param null|ReflectionMethod $getter |
119
|
|
|
* @param null|ReflectionMethod $setter |
120
|
|
|
* @return null|TypeToken |
121
|
|
|
*/ |
122
|
29 |
|
private function checkDocBlocks( |
123
|
|
|
?ReflectionProperty $property, |
124
|
|
|
?ReflectionMethod $getter, |
125
|
|
|
?ReflectionMethod $setter |
126
|
|
|
): ?TypeToken { |
127
|
29 |
|
if ($getter !== null) { |
128
|
4 |
|
$type = $this->getType($getter->getDocComment() ?: null, 'return'); |
129
|
4 |
|
if ($type !== null) { |
130
|
2 |
|
$class = $getter->getDeclaringClass(); |
131
|
2 |
|
return $this->getTypeToken($type, $class->getNamespaceName(), $class->getFileName()); |
132
|
|
|
} |
133
|
|
|
} |
134
|
|
|
|
135
|
27 |
|
if ($setter !== null) { |
136
|
4 |
|
$parameters = $setter->getParameters(); |
137
|
4 |
|
if (\count($parameters) === 1) { |
138
|
4 |
|
$type = $this->getType($setter->getDocComment() ?: null, 'param', $parameters[0]->getName()); |
139
|
4 |
|
if ($type !== null) { |
140
|
2 |
|
$class = $setter->getDeclaringClass(); |
141
|
2 |
|
return $this->getTypeToken($type, $class->getNamespaceName(), $class->getFileName()); |
142
|
|
|
} |
143
|
|
|
} |
144
|
|
|
} |
145
|
|
|
|
146
|
25 |
|
if ($property !== null) { |
147
|
23 |
|
$type = $this->getType($property->getDocComment() ?: null, 'var'); |
148
|
23 |
|
if ($type !== null) { |
149
|
19 |
|
$class = $property->getDeclaringClass(); |
150
|
19 |
|
return $this->getTypeToken($type, $class->getNamespaceName(), $class->getFileName()); |
151
|
|
|
} |
152
|
|
|
} |
153
|
|
|
|
154
|
6 |
|
return null; |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* Parse docblock and return type for parameter |
159
|
|
|
* |
160
|
|
|
* @param string $comment |
161
|
|
|
* @param string $annotation |
162
|
|
|
* @param null|string $parameter |
163
|
|
|
* @return null|string |
164
|
|
|
*/ |
165
|
29 |
|
private function getType(?string $comment, string $annotation, ?string $parameter = null): ?string |
166
|
|
|
{ |
167
|
29 |
|
if ($comment === null) { |
168
|
3 |
|
return null; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
// for setters, we look for the param name as well |
172
|
26 |
|
$pattern = '/@'.$annotation.'\s+([a-zA-Z0-9|\[\]\\\\]+)'; |
173
|
26 |
|
if ($parameter !== null) { |
174
|
2 |
|
$pattern .= '\s+\$'.$parameter; |
175
|
|
|
} |
176
|
26 |
|
$pattern .= '/'; |
177
|
|
|
|
178
|
26 |
|
\preg_match($pattern, $comment, $matches); |
179
|
|
|
|
180
|
|
|
/** @var string|null $type */ |
181
|
26 |
|
$type = $matches[1] ?? null; |
182
|
26 |
|
if ($type === null) { |
183
|
1 |
|
return null; |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
// if not nullable |
187
|
25 |
|
if (\strpos($type, '|') === false) { |
188
|
20 |
|
return $type; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
// if > 2 types |
192
|
5 |
|
if (\substr_count($type, '|') !== 1) { |
193
|
1 |
|
return null; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
// if one of the types is not null |
197
|
4 |
|
if (\stripos(\strtolower($type), 'null') === false) { |
198
|
1 |
|
return null; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
// return the non-null type |
202
|
3 |
|
foreach (\explode('|', $type) as $potentialType) { |
203
|
3 |
|
$potentialType = \trim($potentialType); |
204
|
3 |
|
if (\strtolower($potentialType) !== 'null') { |
205
|
3 |
|
return $potentialType; |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
// The should never be hit |
210
|
|
|
// @codeCoverageIgnoreStart |
211
|
|
|
return null; |
212
|
|
|
// @codeCoverageIgnoreEnd |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* Converts types as int[] to array<int> |
217
|
|
|
* |
218
|
|
|
* @param string $type |
219
|
|
|
* @param string $namespace |
220
|
|
|
* @param string $filename |
221
|
|
|
* @return string |
222
|
|
|
*/ |
223
|
23 |
|
private function unwrapArray(string $type, string $namespace, string $filename): string |
224
|
|
|
{ |
225
|
|
|
// if not in array syntax |
226
|
23 |
|
if (\strpos($type, '[]') === false) { |
227
|
|
|
// convert mixed to wildcard |
228
|
23 |
|
return $type === 'mixed' ? TypeToken::WILDCARD : $type; |
229
|
|
|
} |
230
|
|
|
|
231
|
5 |
|
$parts = \explode('[]', $type); |
232
|
5 |
|
$primaryType = \array_shift($parts); |
233
|
5 |
|
$numParts = \count($parts); |
234
|
|
|
|
235
|
5 |
|
$primaryTypeToken = $this->getTypeToken($primaryType, $namespace, $filename); |
236
|
|
|
|
237
|
5 |
|
return \str_repeat('array<', $numParts) . $primaryTypeToken->getRawType() . \str_repeat('>', $numParts); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* Using the type found in docblock, attempt to resolve imported classes |
242
|
|
|
* |
243
|
|
|
* @param string $type |
244
|
|
|
* @param string $namespace |
245
|
|
|
* @param string $filename |
246
|
|
|
* @return TypeToken |
247
|
|
|
*/ |
248
|
23 |
|
private function getTypeToken(string $type, string $namespace, string $filename): TypeToken |
249
|
|
|
{ |
250
|
|
|
// convert syntax if generic array |
251
|
23 |
|
$type = $this->unwrapArray($type, $namespace, $filename); |
252
|
23 |
|
$typeToken = TypeToken::create($type); |
253
|
|
|
|
254
|
23 |
|
if (!$typeToken->isObject()) { |
255
|
13 |
|
return $typeToken; |
256
|
|
|
} |
257
|
|
|
|
258
|
13 |
|
$firstSlash = \strpos($type, '\\'); |
259
|
13 |
|
if ($firstSlash === 0) { |
260
|
1 |
|
return TypeToken::create(substr($type, 1)); |
261
|
|
|
} |
262
|
|
|
|
263
|
12 |
|
if ($firstSlash === false && (\class_exists($type) || \interface_exists($type))) { |
264
|
1 |
|
return $typeToken; |
265
|
|
|
} |
266
|
|
|
|
267
|
11 |
|
$pattern = \str_replace(':REPLACE:', $type, self::USE_PATTERN); |
268
|
11 |
|
\preg_match($pattern, \file_get_contents($filename), $matches); |
269
|
|
|
|
270
|
|
|
// normal use statement syntax |
271
|
11 |
|
if (!empty($matches['default'])) { |
272
|
1 |
|
return TypeToken::create($matches['default']); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
// aliased use statement |
276
|
10 |
|
if (!empty($matches['alias'])) { |
277
|
2 |
|
return TypeToken::create($matches['alias']); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
// group use statement |
281
|
8 |
|
if (!empty($matches['group'])) { |
282
|
3 |
|
return TypeToken::create($matches['group'].$type); |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
// grouped aliased use statement |
286
|
5 |
|
if (!empty($matches['namespace']) && !empty($matches['classname'])) { |
287
|
1 |
|
return TypeToken::create($matches['namespace'].$matches['classname']); |
288
|
|
|
} |
289
|
|
|
|
290
|
4 |
|
return TypeToken::create($namespace.'\\'.$type); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* If the type is just 'array', check the docblock to see if there's a more specific type |
295
|
|
|
* |
296
|
|
|
* @param TypeToken $type |
297
|
|
|
* @param null|ReflectionProperty $property |
298
|
|
|
* @param null|ReflectionMethod $getter |
299
|
|
|
* @param null|ReflectionMethod $setter |
300
|
|
|
* @return TypeToken |
301
|
|
|
*/ |
302
|
29 |
|
private function checkGenericArray( |
303
|
|
|
TypeToken $type, |
304
|
|
|
?ReflectionProperty $property, |
305
|
|
|
?ReflectionMethod $getter, |
306
|
|
|
?ReflectionMethod $setter |
307
|
|
|
): TypeToken { |
308
|
29 |
|
return $type->isArray() |
309
|
6 |
|
? $this->checkDocBlocks($property, $getter, $setter) ?? $type |
310
|
29 |
|
: $type; |
311
|
|
|
} |
312
|
|
|
} |
313
|
|
|
|