1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* This file is part of phpDocumentor. |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
* |
11
|
|
|
* @link http://phpdoc.org |
12
|
|
|
*/ |
13
|
|
|
|
14
|
|
|
namespace phpDocumentor\Reflection; |
15
|
|
|
|
16
|
|
|
use ArrayIterator; |
17
|
|
|
use InvalidArgumentException; |
18
|
|
|
use phpDocumentor\Reflection\Types\Array_; |
19
|
|
|
use phpDocumentor\Reflection\Types\Collection; |
20
|
|
|
use phpDocumentor\Reflection\Types\Compound; |
21
|
|
|
use phpDocumentor\Reflection\Types\Context; |
22
|
|
|
use phpDocumentor\Reflection\Types\Integer; |
23
|
|
|
use phpDocumentor\Reflection\Types\Iterable_; |
24
|
|
|
use phpDocumentor\Reflection\Types\Nullable; |
25
|
|
|
use phpDocumentor\Reflection\Types\Object_; |
26
|
|
|
use phpDocumentor\Reflection\Types\String_; |
27
|
|
|
use RuntimeException; |
28
|
|
|
use const PREG_SPLIT_DELIM_CAPTURE; |
29
|
|
|
use const PREG_SPLIT_NO_EMPTY; |
30
|
|
|
use function array_keys; |
31
|
|
|
use function array_pop; |
32
|
|
|
use function class_exists; |
33
|
|
|
use function class_implements; |
34
|
|
|
use function count; |
35
|
|
|
use function in_array; |
36
|
|
|
use function preg_split; |
37
|
|
|
use function strlen; |
38
|
|
|
use function strpos; |
39
|
|
|
use function strtolower; |
40
|
|
|
use function substr; |
41
|
|
|
use function trim; |
42
|
|
|
|
43
|
|
|
final class TypeResolver |
44
|
|
|
{ |
45
|
|
|
/** @var string Definition of the ARRAY operator for types */ |
46
|
|
|
private const OPERATOR_ARRAY = '[]'; |
47
|
|
|
|
48
|
|
|
/** @var string Definition of the NAMESPACE operator in PHP */ |
49
|
|
|
private const OPERATOR_NAMESPACE = '\\'; |
50
|
|
|
|
51
|
|
|
/** @var int the iterator parser is inside a compound context */ |
52
|
|
|
private const PARSER_IN_COMPOUND = 0; |
53
|
|
|
|
54
|
|
|
/** @var int the iterator parser is inside a nullable expression context */ |
55
|
|
|
private const PARSER_IN_NULLABLE = 1; |
56
|
|
|
|
57
|
|
|
/** @var int the iterator parser is inside an array expression context */ |
58
|
|
|
private const PARSER_IN_ARRAY_EXPRESSION = 2; |
59
|
|
|
|
60
|
|
|
/** @var int the iterator parser is inside a collection expression context */ |
61
|
|
|
private const PARSER_IN_COLLECTION_EXPRESSION = 3; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @var array<string, string> List of recognized keywords and unto which Value Object they map |
65
|
|
|
* @psalm-var array<string, class-string<Type>> |
66
|
|
|
*/ |
67
|
|
|
private $keywords = [ |
68
|
|
|
'string' => Types\String_::class, |
69
|
|
|
'int' => Types\Integer::class, |
70
|
|
|
'integer' => Types\Integer::class, |
71
|
|
|
'bool' => Types\Boolean::class, |
72
|
|
|
'boolean' => Types\Boolean::class, |
73
|
|
|
'real' => Types\Float_::class, |
74
|
|
|
'float' => Types\Float_::class, |
75
|
|
|
'double' => Types\Float_::class, |
76
|
|
|
'object' => Object_::class, |
77
|
|
|
'mixed' => Types\Mixed_::class, |
78
|
|
|
'array' => Array_::class, |
79
|
|
|
'resource' => Types\Resource_::class, |
80
|
|
|
'void' => Types\Void_::class, |
81
|
|
|
'null' => Types\Null_::class, |
82
|
|
|
'scalar' => Types\Scalar::class, |
83
|
|
|
'callback' => Types\Callable_::class, |
84
|
|
|
'callable' => Types\Callable_::class, |
85
|
|
|
'false' => Types\Boolean::class, |
86
|
|
|
'true' => Types\Boolean::class, |
87
|
|
|
'self' => Types\Self_::class, |
88
|
|
|
'$this' => Types\This::class, |
89
|
|
|
'static' => Types\Static_::class, |
90
|
|
|
'parent' => Types\Parent_::class, |
91
|
|
|
'iterable' => Iterable_::class, |
92
|
|
|
]; |
93
|
|
|
|
94
|
|
|
/** @var FqsenResolver */ |
95
|
|
|
private $fqsenResolver; |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Initializes this TypeResolver with the means to create and resolve Fqsen objects. |
99
|
|
|
*/ |
100
|
54 |
|
public function __construct(?FqsenResolver $fqsenResolver = null) |
101
|
|
|
{ |
102
|
54 |
|
$this->fqsenResolver = $fqsenResolver ?: new FqsenResolver(); |
103
|
54 |
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Analyzes the given type and returns the FQCN variant. |
107
|
|
|
* |
108
|
|
|
* When a type is provided this method checks whether it is not a keyword or |
109
|
|
|
* Fully Qualified Class Name. If so it will use the given namespace and |
110
|
|
|
* aliases to expand the type to a FQCN representation. |
111
|
|
|
* |
112
|
|
|
* This method only works as expected if the namespace and aliases are set; |
113
|
|
|
* no dynamic reflection is being performed here. |
114
|
|
|
* |
115
|
|
|
* @uses Context::getNamespaceAliases() to check whether the first part of the relative type name should not be |
116
|
|
|
* replaced with another namespace. |
117
|
|
|
* @uses Context::getNamespace() to determine with what to prefix the type name. |
118
|
|
|
* |
119
|
|
|
* @param string $type The relative or absolute type. |
120
|
|
|
*/ |
121
|
51 |
|
public function resolve(string $type, ?Context $context = null) : Type |
122
|
|
|
{ |
123
|
51 |
|
$type = trim($type); |
124
|
51 |
|
if (!$type) { |
125
|
1 |
|
throw new InvalidArgumentException('Attempted to resolve "' . $type . '" but it appears to be empty'); |
126
|
|
|
} |
127
|
|
|
|
128
|
50 |
|
if ($context === null) { |
129
|
1 |
|
$context = new Context(''); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
// split the type string into tokens `|`, `?`, `<`, `>`, `,`, `(`, `)[]`, '<', '>' and type names |
133
|
50 |
|
$tokens = preg_split( |
134
|
50 |
|
'/(\\||\\?|<|>|, ?|\\(|\\)(?:\\[\\])+)/', |
135
|
50 |
|
$type, |
136
|
50 |
|
-1, |
137
|
50 |
|
PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE |
138
|
|
|
); |
139
|
|
|
|
140
|
50 |
|
if ($tokens === false) { |
141
|
|
|
throw new InvalidArgumentException('Unable to split the type string "' . $type . '" into tokens'); |
142
|
|
|
} |
143
|
|
|
|
144
|
50 |
|
$tokenIterator = new ArrayIterator($tokens); |
145
|
|
|
|
146
|
50 |
|
return $this->parseTypes($tokenIterator, $context, self::PARSER_IN_COMPOUND); |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* Analyse each tokens and creates types |
151
|
|
|
* |
152
|
|
|
* @param ArrayIterator $tokens the iterator on tokens |
153
|
|
|
* @param int $parserContext on of self::PARSER_* constants, indicating |
154
|
|
|
* the context where we are in the parsing |
155
|
|
|
*/ |
156
|
50 |
|
private function parseTypes(ArrayIterator $tokens, Context $context, int $parserContext) : Type |
157
|
|
|
{ |
158
|
50 |
|
$types = []; |
159
|
50 |
|
$token = ''; |
160
|
50 |
|
while ($tokens->valid()) { |
161
|
50 |
|
$token = $tokens->current(); |
162
|
|
|
|
163
|
50 |
|
if ($token === '|') { |
164
|
14 |
|
if (count($types) === 0) { |
165
|
|
|
throw new RuntimeException( |
166
|
|
|
'A type is missing before a type separator' |
167
|
|
|
); |
168
|
|
|
} |
169
|
|
|
|
170
|
14 |
|
if ($parserContext !== self::PARSER_IN_COMPOUND |
171
|
14 |
|
&& $parserContext !== self::PARSER_IN_ARRAY_EXPRESSION |
172
|
14 |
|
&& $parserContext !== self::PARSER_IN_COLLECTION_EXPRESSION |
173
|
|
|
) { |
174
|
|
|
throw new RuntimeException( |
175
|
|
|
'Unexpected type separator' |
176
|
|
|
); |
177
|
|
|
} |
178
|
|
|
|
179
|
14 |
|
$tokens->next(); |
180
|
50 |
|
} elseif ($token === '?') { |
181
|
2 |
|
if ($parserContext !== self::PARSER_IN_COMPOUND |
182
|
2 |
|
&& $parserContext !== self::PARSER_IN_ARRAY_EXPRESSION |
183
|
2 |
|
&& $parserContext !== self::PARSER_IN_COLLECTION_EXPRESSION |
184
|
|
|
) { |
185
|
|
|
throw new RuntimeException( |
186
|
|
|
'Unexpected nullable character' |
187
|
|
|
); |
188
|
|
|
} |
189
|
|
|
|
190
|
2 |
|
$tokens->next(); |
191
|
2 |
|
$type = $this->parseTypes($tokens, $context, self::PARSER_IN_NULLABLE); |
192
|
2 |
|
$types[] = new Nullable($type); |
193
|
50 |
|
} elseif ($token === '(') { |
194
|
5 |
|
$tokens->next(); |
195
|
5 |
|
$type = $this->parseTypes($tokens, $context, self::PARSER_IN_ARRAY_EXPRESSION); |
196
|
|
|
|
197
|
5 |
|
$resolvedType = new Array_($type); |
198
|
|
|
|
199
|
5 |
|
$token = $tokens->current(); |
200
|
|
|
// Someone did not properly close their array expression .. |
201
|
5 |
|
if ($token === null) { |
202
|
1 |
|
break; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
// we generate arrays corresponding to the number of '[]' after the ')' |
206
|
4 |
|
$numberOfArrays = (strlen($token) - 1) / 2; |
207
|
4 |
|
for ($i = 0; $i < $numberOfArrays - 1; ++$i) { |
208
|
1 |
|
$resolvedType = new Array_($resolvedType); |
209
|
|
|
} |
210
|
|
|
|
211
|
4 |
|
$types[] = $resolvedType; |
212
|
4 |
|
$tokens->next(); |
213
|
50 |
|
} elseif ($parserContext === self::PARSER_IN_ARRAY_EXPRESSION && $token[0] === ')') { |
214
|
4 |
|
break; |
215
|
50 |
|
} elseif ($token === '<') { |
216
|
13 |
|
if (count($types) === 0) { |
217
|
1 |
|
throw new RuntimeException( |
218
|
1 |
|
'Unexpected collection operator "<", class name is missing' |
219
|
|
|
); |
220
|
|
|
} |
221
|
|
|
|
222
|
12 |
|
$classType = array_pop($types); |
223
|
12 |
|
if ($classType !== null) { |
224
|
12 |
|
$types[] = $this->resolveCollection($tokens, $classType, $context); |
225
|
|
|
} |
226
|
|
|
|
227
|
8 |
|
$tokens->next(); |
228
|
49 |
|
} elseif ($parserContext === self::PARSER_IN_COLLECTION_EXPRESSION |
229
|
49 |
|
&& ($token === '>' || trim($token) === ',') |
230
|
|
|
) { |
231
|
10 |
|
break; |
232
|
|
|
} else { |
233
|
49 |
|
$type = $this->resolveSingleType($token, $context); |
234
|
49 |
|
$tokens->next(); |
235
|
49 |
|
if ($parserContext === self::PARSER_IN_NULLABLE) { |
236
|
2 |
|
return $type; |
237
|
|
|
} |
238
|
|
|
|
239
|
48 |
|
$types[] = $type; |
240
|
|
|
} |
241
|
|
|
} |
242
|
|
|
|
243
|
48 |
|
if ($token === '|') { |
244
|
|
|
throw new RuntimeException( |
245
|
|
|
'A type is missing after a type separator' |
246
|
|
|
); |
247
|
|
|
} |
248
|
|
|
|
249
|
48 |
|
if (count($types) === 0) { |
250
|
1 |
|
if ($parserContext === self::PARSER_IN_NULLABLE) { |
251
|
|
|
throw new RuntimeException( |
252
|
|
|
'A type is missing after a nullable character' |
253
|
|
|
); |
254
|
|
|
} |
255
|
|
|
|
256
|
1 |
|
if ($parserContext === self::PARSER_IN_ARRAY_EXPRESSION) { |
257
|
|
|
throw new RuntimeException( |
258
|
|
|
'A type is missing in an array expression' |
259
|
|
|
); |
260
|
|
|
} |
261
|
|
|
|
262
|
1 |
|
if ($parserContext === self::PARSER_IN_COLLECTION_EXPRESSION) { |
263
|
|
|
throw new RuntimeException( |
264
|
1 |
|
'A type is missing in a collection expression' |
265
|
|
|
); |
266
|
|
|
} |
267
|
48 |
|
} elseif (count($types) === 1) { |
268
|
41 |
|
return $types[0]; |
269
|
|
|
} |
270
|
|
|
|
271
|
14 |
|
return new Compound($types); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* resolve the given type into a type object |
276
|
|
|
* |
277
|
|
|
* @param string $type the type string, representing a single type |
278
|
|
|
* |
279
|
|
|
* @return Type|Array_|Object_ |
280
|
|
|
*/ |
281
|
49 |
|
private function resolveSingleType(string $type, Context $context) |
282
|
|
|
{ |
283
|
|
|
switch (true) { |
284
|
49 |
|
case $this->isKeyword($type): |
285
|
43 |
|
return $this->resolveKeyword($type); |
286
|
20 |
|
case $this->isTypedArray($type): |
287
|
5 |
|
return $this->resolveTypedArray($type, $context); |
288
|
17 |
|
case $this->isFqsen($type): |
289
|
9 |
|
return $this->resolveTypedObject($type); |
290
|
10 |
|
case $this->isPartialStructuralElementName($type): |
291
|
10 |
|
return $this->resolveTypedObject($type, $context); |
292
|
|
|
// @codeCoverageIgnoreStart |
293
|
|
|
default: |
294
|
|
|
// I haven't got the foggiest how the logic would come here but added this as a defense. |
295
|
|
|
throw new RuntimeException( |
296
|
|
|
'Unable to resolve type "' . $type . '", there is no known method to resolve it' |
297
|
|
|
); |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
// @codeCoverageIgnoreEnd |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* Adds a keyword to the list of Keywords and associates it with a specific Value Object. |
305
|
|
|
*/ |
306
|
3 |
|
public function addKeyword(string $keyword, string $typeClassName) : void |
307
|
|
|
{ |
308
|
3 |
|
if (!class_exists($typeClassName)) { |
309
|
1 |
|
throw new InvalidArgumentException( |
310
|
1 |
|
'The Value Object that needs to be created with a keyword "' . $keyword . '" must be an existing class' |
311
|
1 |
|
. ' but we could not find the class ' . $typeClassName |
312
|
|
|
); |
313
|
|
|
} |
314
|
|
|
|
315
|
2 |
|
if (!in_array(Type::class, class_implements($typeClassName), true)) { |
316
|
1 |
|
throw new InvalidArgumentException( |
317
|
1 |
|
'The class "' . $typeClassName . '" must implement the interface "phpDocumentor\Reflection\Type"' |
318
|
|
|
); |
319
|
|
|
} |
320
|
|
|
|
321
|
1 |
|
$this->keywords[$keyword] = $typeClassName; |
322
|
1 |
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* Detects whether the given type represents an array. |
326
|
|
|
* |
327
|
|
|
* @param string $type A relative or absolute type as defined in the phpDocumentor documentation. |
328
|
|
|
*/ |
329
|
20 |
|
private function isTypedArray(string $type) : bool |
330
|
|
|
{ |
331
|
20 |
|
return substr($type, -2) === self::OPERATOR_ARRAY; |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
/** |
335
|
|
|
* Detects whether the given type represents a PHPDoc keyword. |
336
|
|
|
* |
337
|
|
|
* @param string $type A relative or absolute type as defined in the phpDocumentor documentation. |
338
|
|
|
*/ |
339
|
49 |
|
private function isKeyword(string $type) : bool |
340
|
|
|
{ |
341
|
49 |
|
return in_array(strtolower($type), array_keys($this->keywords), true); |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
/** |
345
|
|
|
* Detects whether the given type represents a relative structural element name. |
346
|
|
|
* |
347
|
|
|
* @param string $type A relative or absolute type as defined in the phpDocumentor documentation. |
348
|
|
|
*/ |
349
|
10 |
|
private function isPartialStructuralElementName(string $type) : bool |
350
|
|
|
{ |
351
|
10 |
|
return ($type[0] !== self::OPERATOR_NAMESPACE) && !$this->isKeyword($type); |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* Tests whether the given type is a Fully Qualified Structural Element Name. |
356
|
|
|
*/ |
357
|
17 |
|
private function isFqsen(string $type) : bool |
358
|
|
|
{ |
359
|
17 |
|
return strpos($type, self::OPERATOR_NAMESPACE) === 0; |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
/** |
363
|
|
|
* Resolves the given typed array string (i.e. `string[]`) into an Array object with the right types set. |
364
|
|
|
*/ |
365
|
5 |
|
private function resolveTypedArray(string $type, Context $context) : Array_ |
366
|
|
|
{ |
367
|
5 |
|
return new Array_($this->resolveSingleType(substr($type, 0, -2), $context)); |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
/** |
371
|
|
|
* Resolves the given keyword (such as `string`) into a Type object representing that keyword. |
372
|
|
|
*/ |
373
|
43 |
|
private function resolveKeyword(string $type) : Type |
374
|
|
|
{ |
375
|
43 |
|
$className = $this->keywords[strtolower($type)]; |
376
|
43 |
|
return new $className(); |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
/** |
380
|
|
|
* Resolves the given FQSEN string into an FQSEN object. |
381
|
|
|
*/ |
382
|
17 |
|
private function resolveTypedObject(string $type, ?Context $context = null) : Object_ |
383
|
|
|
{ |
384
|
17 |
|
return new Object_($this->fqsenResolver->resolve($type, $context)); |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
/** |
388
|
|
|
* Resolves the collection values and keys |
389
|
|
|
* |
390
|
|
|
* @return Array_|Iterable_|Collection |
391
|
|
|
*/ |
392
|
12 |
|
private function resolveCollection(ArrayIterator $tokens, Type $classType, Context $context) : Type |
393
|
|
|
{ |
394
|
12 |
|
$isArray = ((string) $classType === 'array'); |
395
|
12 |
|
$isIterable = ((string) $classType === 'iterable'); |
396
|
|
|
|
397
|
|
|
// allow only "array", "iterable" or class name before "<" |
398
|
12 |
|
if (!$isArray && !$isIterable |
399
|
12 |
|
&& (!$classType instanceof Object_ || $classType->getFqsen() === null)) { |
400
|
1 |
|
throw new RuntimeException( |
401
|
1 |
|
$classType . ' is not a collection' |
402
|
|
|
); |
403
|
|
|
} |
404
|
|
|
|
405
|
11 |
|
$tokens->next(); |
406
|
|
|
|
407
|
11 |
|
$valueType = $this->parseTypes($tokens, $context, self::PARSER_IN_COLLECTION_EXPRESSION); |
408
|
11 |
|
$keyType = null; |
409
|
|
|
|
410
|
11 |
|
if ($tokens->current() !== null && trim($tokens->current()) === ',') { |
411
|
|
|
// if we have a comma, then we just parsed the key type, not the value type |
412
|
7 |
|
$keyType = $valueType; |
413
|
7 |
|
if ($isArray) { |
414
|
|
|
// check the key type for an "array" collection. We allow only |
415
|
|
|
// strings or integers. |
416
|
5 |
|
if (!$keyType instanceof String_ && |
417
|
5 |
|
!$keyType instanceof Integer && |
418
|
5 |
|
!$keyType instanceof Compound |
419
|
|
|
) { |
420
|
1 |
|
throw new RuntimeException( |
421
|
1 |
|
'An array can have only integers or strings as keys' |
422
|
|
|
); |
423
|
|
|
} |
424
|
|
|
|
425
|
4 |
|
if ($keyType instanceof Compound) { |
426
|
|
|
foreach ($keyType->getIterator() as $item) { |
427
|
|
|
if (!$item instanceof String_ && |
428
|
|
|
!$item instanceof Integer |
429
|
|
|
) { |
430
|
|
|
throw new RuntimeException( |
431
|
|
|
'An array can have only integers or strings as keys' |
432
|
|
|
); |
433
|
|
|
} |
434
|
|
|
} |
435
|
|
|
} |
436
|
|
|
} |
437
|
|
|
|
438
|
6 |
|
$tokens->next(); |
439
|
|
|
// now let's parse the value type |
440
|
6 |
|
$valueType = $this->parseTypes($tokens, $context, self::PARSER_IN_COLLECTION_EXPRESSION); |
441
|
|
|
} |
442
|
|
|
|
443
|
9 |
|
if ($tokens->current() !== '>') { |
444
|
1 |
|
if (empty($tokens->current())) { |
445
|
1 |
|
throw new RuntimeException( |
446
|
1 |
|
'Collection: ">" is missing' |
447
|
|
|
); |
448
|
|
|
} |
449
|
|
|
|
450
|
|
|
throw new RuntimeException( |
451
|
|
|
'Unexpected character "' . $tokens->current() . '", ">" is missing' |
452
|
|
|
); |
453
|
|
|
} |
454
|
|
|
|
455
|
8 |
|
if ($isArray) { |
456
|
4 |
|
return new Array_($valueType, $keyType); |
457
|
|
|
} |
458
|
|
|
|
459
|
4 |
|
if ($isIterable) { |
460
|
1 |
|
return new Iterable_($valueType, $keyType); |
461
|
|
|
} |
462
|
|
|
|
463
|
|
|
/** @psalm-suppress RedundantCondition */ |
464
|
3 |
|
if ($classType instanceof Object_) { |
465
|
3 |
|
return $this->makeCollectionFromObject($classType, $valueType, $keyType); |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
throw new RuntimeException('Invalid $classType provided'); |
469
|
|
|
} |
470
|
|
|
|
471
|
3 |
|
private function makeCollectionFromObject(Object_ $object, Type $valueType, ?Type $keyType = null) : Collection |
472
|
|
|
{ |
473
|
3 |
|
return new Collection($object->getFqsen(), $valueType, $keyType); |
474
|
|
|
} |
475
|
|
|
} |
476
|
|
|
|