1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file is part of phpDocumentor. |
4
|
|
|
* |
5
|
|
|
* For the full copyright and license information, please view the LICENSE |
6
|
|
|
* file that was distributed with this source code. |
7
|
|
|
* |
8
|
|
|
* @copyright 2010-2015 Mike van Riel<[email protected]> |
9
|
|
|
* @license http://www.opensource.org/licenses/mit-license.php MIT |
10
|
|
|
* @link http://phpdoc.org |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
namespace phpDocumentor\Reflection; |
14
|
|
|
|
15
|
|
|
use phpDocumentor\Reflection\Types\Array_; |
16
|
|
|
use phpDocumentor\Reflection\Types\Compound; |
17
|
|
|
use phpDocumentor\Reflection\Types\Context; |
18
|
|
|
use phpDocumentor\Reflection\Types\Iterable_; |
19
|
|
|
use phpDocumentor\Reflection\Types\Nullable; |
20
|
|
|
use phpDocumentor\Reflection\Types\Object_; |
21
|
|
|
|
22
|
|
|
final class TypeResolver |
23
|
|
|
{ |
24
|
|
|
/** @var string Definition of the ARRAY operator for types */ |
25
|
|
|
const OPERATOR_ARRAY = '[]'; |
26
|
|
|
|
27
|
|
|
/** @var string Definition of the NAMESPACE operator in PHP */ |
28
|
|
|
const OPERATOR_NAMESPACE = '\\'; |
29
|
|
|
|
30
|
|
|
/** @var integer the iterator parser is inside a compound context */ |
31
|
|
|
const PARSER_IN_COMPOUND = 0; |
32
|
|
|
|
33
|
|
|
/** @var integer the iterator parser is inside a nullable expression context */ |
34
|
|
|
const PARSER_IN_NULLABLE = 1; |
35
|
|
|
|
36
|
|
|
/** @var integer the iterator parser is inside an array expression context */ |
37
|
|
|
const PARSER_IN_ARRAY_EXPRESSION = 2; |
38
|
|
|
|
39
|
|
|
/** @var string[] List of recognized keywords and unto which Value Object they map */ |
40
|
|
|
private $keywords = array( |
41
|
|
|
'string' => Types\String_::class, |
42
|
|
|
'int' => Types\Integer::class, |
43
|
|
|
'integer' => Types\Integer::class, |
44
|
|
|
'bool' => Types\Boolean::class, |
45
|
|
|
'boolean' => Types\Boolean::class, |
46
|
|
|
'float' => Types\Float_::class, |
47
|
|
|
'double' => Types\Float_::class, |
48
|
|
|
'object' => Object_::class, |
49
|
|
|
'mixed' => Types\Mixed_::class, |
50
|
|
|
'array' => Array_::class, |
51
|
|
|
'resource' => Types\Resource_::class, |
52
|
|
|
'void' => Types\Void_::class, |
53
|
|
|
'null' => Types\Null_::class, |
54
|
|
|
'scalar' => Types\Scalar::class, |
55
|
|
|
'callback' => Types\Callable_::class, |
56
|
|
|
'callable' => Types\Callable_::class, |
57
|
|
|
'false' => Types\Boolean::class, |
58
|
|
|
'true' => Types\Boolean::class, |
59
|
|
|
'self' => Types\Self_::class, |
60
|
|
|
'$this' => Types\This::class, |
61
|
|
|
'static' => Types\Static_::class, |
62
|
|
|
'parent' => Types\Parent_::class, |
63
|
|
|
'iterable' => Iterable_::class, |
64
|
|
|
); |
65
|
|
|
|
66
|
|
|
/** @var FqsenResolver */ |
67
|
|
|
private $fqsenResolver; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Initializes this TypeResolver with the means to create and resolve Fqsen objects. |
71
|
|
|
* |
72
|
|
|
* @param FqsenResolver $fqsenResolver |
73
|
|
|
*/ |
74
|
34 |
|
public function __construct(FqsenResolver $fqsenResolver = null) |
75
|
|
|
{ |
76
|
34 |
|
$this->fqsenResolver = $fqsenResolver ?: new FqsenResolver(); |
77
|
34 |
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Analyzes the given type and returns the FQCN variant. |
81
|
|
|
* |
82
|
|
|
* When a type is provided this method checks whether it is not a keyword or |
83
|
|
|
* Fully Qualified Class Name. If so it will use the given namespace and |
84
|
|
|
* aliases to expand the type to a FQCN representation. |
85
|
|
|
* |
86
|
|
|
* This method only works as expected if the namespace and aliases are set; |
87
|
|
|
* no dynamic reflection is being performed here. |
88
|
|
|
* |
89
|
|
|
* @param string $type The relative or absolute type. |
90
|
|
|
* @param Context $context |
91
|
|
|
* |
92
|
|
|
* @uses Context::getNamespace() to determine with what to prefix the type name. |
93
|
|
|
* @uses Context::getNamespaceAliases() to check whether the first part of the relative type name should not be |
94
|
|
|
* replaced with another namespace. |
95
|
|
|
* |
96
|
|
|
* @return Type |
97
|
|
|
*/ |
98
|
31 |
|
public function resolve($type, Context $context = null) |
99
|
|
|
{ |
100
|
31 |
|
if (!is_string($type)) { |
101
|
1 |
|
throw new \InvalidArgumentException( |
102
|
1 |
|
'Attempted to resolve type but it appeared not to be a string, received: ' . var_export($type, true) |
103
|
1 |
|
); |
104
|
|
|
} |
105
|
|
|
|
106
|
30 |
|
$type = trim($type); |
107
|
30 |
|
if (!$type) { |
108
|
1 |
|
throw new \InvalidArgumentException('Attempted to resolve "' . $type . '" but it appears to be empty'); |
109
|
|
|
} |
110
|
|
|
|
111
|
29 |
|
if ($context === null) { |
112
|
|
|
$context = new Context(''); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
// split the type string into tokens `|`, `?`, `(`, `)[]` and type names |
116
|
29 |
|
$tokens = preg_split('/(\||\?|\(|\)(?:\[\])+)/', $type, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); |
117
|
29 |
|
$tokenIterator = new \ArrayIterator($tokens); |
118
|
|
|
|
119
|
29 |
|
return $this->parseTypes($tokenIterator, $context, self::PARSER_IN_COMPOUND); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Analyse each tokens and creates types |
124
|
|
|
* |
125
|
|
|
* @param \ArrayIterator $tokens the iterator on tokens |
126
|
|
|
* @param Context $context |
127
|
|
|
* @param integer $parserContext on of self::PARSER_* constants, indicating |
128
|
|
|
* the context where we are in the parsing |
129
|
|
|
* |
130
|
|
|
* @return Type |
131
|
|
|
*/ |
132
|
29 |
|
private function parseTypes(\ArrayIterator $tokens, Context $context, $parserContext) |
133
|
|
|
{ |
134
|
29 |
|
$types = array(); |
135
|
29 |
|
$token = ''; |
136
|
29 |
|
while ($tokens->valid()) { |
137
|
29 |
|
$token = $tokens->current(); |
138
|
|
|
|
139
|
29 |
|
if ($token == '|') { |
140
|
4 |
|
if (count($types) == 0) { |
141
|
|
|
throw new \RuntimeException( |
142
|
|
|
'A type is missing before a type separator' |
143
|
|
|
); |
144
|
|
|
} |
145
|
|
|
if ($parserContext !== self::PARSER_IN_COMPOUND |
146
|
4 |
|
&& $parserContext !== self::PARSER_IN_ARRAY_EXPRESSION) { |
147
|
|
|
throw new \RuntimeException( |
148
|
|
|
'Unexpected type separator' |
149
|
|
|
); |
150
|
|
|
} |
151
|
4 |
|
$tokens->next(); |
152
|
|
|
|
|
|
|
|
153
|
29 |
|
} else if ($token == '?') { |
154
|
|
|
if ($parserContext !== self::PARSER_IN_COMPOUND |
155
|
1 |
|
&& $parserContext !== self::PARSER_IN_ARRAY_EXPRESSION) { |
156
|
|
|
throw new \RuntimeException( |
157
|
|
|
'Unexpected nullable character' |
158
|
|
|
); |
159
|
|
|
} |
160
|
|
|
|
161
|
1 |
|
$tokens->next(); |
162
|
1 |
|
$type = $this->parseTypes($tokens, $context, self::PARSER_IN_NULLABLE); |
163
|
1 |
|
$types[] = new Nullable($type); |
164
|
|
|
|
|
|
|
|
165
|
29 |
|
} else if ($token === '(') { |
166
|
1 |
|
$tokens->next(); |
167
|
1 |
|
$type = $this->parseTypes($tokens, $context, self::PARSER_IN_ARRAY_EXPRESSION); |
168
|
|
|
|
169
|
1 |
|
$resolvedType = new Array_($type); |
170
|
|
|
|
171
|
|
|
// we generates arrays corresponding to the number of '[]' |
172
|
|
|
// after the ')' |
173
|
1 |
|
$numberOfArrays = (strlen($tokens->current()) -1) / 2; |
174
|
1 |
|
for ($i = 0; $i < $numberOfArrays - 1; $i++) { |
175
|
|
|
$resolvedType = new Array_($resolvedType); |
176
|
|
|
} |
177
|
1 |
|
$types[] = $resolvedType; |
178
|
1 |
|
$tokens->next(); |
179
|
|
|
|
|
|
|
|
180
|
1 |
|
} else if ($parserContext === self::PARSER_IN_ARRAY_EXPRESSION |
181
|
29 |
|
&& $token[0] === ')' |
182
|
29 |
|
) { |
183
|
1 |
|
break; |
184
|
|
|
|
|
|
|
|
185
|
|
|
} else { |
186
|
29 |
|
$type = $this->resolveSingleType($token, $context); |
187
|
29 |
|
$tokens->next(); |
188
|
29 |
|
if ($parserContext === self::PARSER_IN_NULLABLE) { |
189
|
1 |
|
return $type; |
190
|
|
|
} |
191
|
28 |
|
$types[] = $type; |
192
|
|
|
} |
193
|
29 |
|
} |
194
|
|
|
|
195
|
29 |
|
if ($token == '|') { |
196
|
|
|
throw new \RuntimeException( |
197
|
|
|
'A type is missing after a type separator' |
198
|
|
|
); |
199
|
|
|
} |
200
|
29 |
|
if (count($types) == 0) { |
201
|
|
|
if ($parserContext == self::PARSER_IN_NULLABLE) { |
202
|
|
|
throw new \RuntimeException( |
203
|
|
|
'A type is missing after a nullable character' |
204
|
|
|
); |
205
|
|
|
} |
206
|
|
|
if ($parserContext == self::PARSER_IN_ARRAY_EXPRESSION) { |
207
|
|
|
throw new \RuntimeException( |
208
|
|
|
'A type is missing in an array expression' |
209
|
|
|
); |
210
|
|
|
} |
211
|
|
|
throw new \RuntimeException( |
212
|
|
|
'No types in a compound list' |
213
|
|
|
); |
214
|
29 |
|
} else if (count($types) == 1) { |
215
|
26 |
|
return $types[0]; |
216
|
|
|
} |
217
|
4 |
|
return new Compound($types); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* resolve the given type into a type object |
222
|
|
|
* |
223
|
|
|
* @param string $type the type string, representing a single type |
224
|
|
|
* @param Context $context |
225
|
|
|
* @return Type|Array_|Object_ |
226
|
|
|
*/ |
227
|
29 |
|
private function resolveSingleType($type, Context $context) |
228
|
|
|
{ |
229
|
29 |
|
switch (true) { |
230
|
29 |
|
case $this->isKeyword($type): |
231
|
23 |
|
return $this->resolveKeyword($type); |
232
|
8 |
|
case $this->isTypedArray($type): |
233
|
2 |
|
return $this->resolveTypedArray($type, $context); |
234
|
7 |
|
case $this->isFqsen($type): |
235
|
4 |
|
return $this->resolveTypedObject($type); |
236
|
5 |
|
case $this->isPartialStructuralElementName($type): |
237
|
5 |
|
return $this->resolveTypedObject($type, $context); |
238
|
|
|
// @codeCoverageIgnoreStart |
239
|
|
|
default: |
240
|
|
|
// I haven't got the foggiest how the logic would come here but added this as a defense. |
241
|
|
|
throw new \RuntimeException( |
242
|
|
|
'Unable to resolve type "' . $type . '", there is no known method to resolve it' |
243
|
|
|
); |
244
|
|
|
} |
245
|
|
|
// @codeCoverageIgnoreEnd |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Adds a keyword to the list of Keywords and associates it with a specific Value Object. |
250
|
|
|
* |
251
|
|
|
* @param string $keyword |
252
|
|
|
* @param string $typeClassName |
253
|
|
|
* |
254
|
|
|
* @return void |
255
|
|
|
*/ |
256
|
3 |
|
public function addKeyword($keyword, $typeClassName) |
257
|
|
|
{ |
258
|
3 |
|
if (!class_exists($typeClassName)) { |
259
|
1 |
|
throw new \InvalidArgumentException( |
260
|
1 |
|
'The Value Object that needs to be created with a keyword "' . $keyword . '" must be an existing class' |
261
|
1 |
|
. ' but we could not find the class ' . $typeClassName |
262
|
1 |
|
); |
263
|
|
|
} |
264
|
|
|
|
265
|
2 |
|
if (!in_array(Type::class, class_implements($typeClassName))) { |
266
|
1 |
|
throw new \InvalidArgumentException( |
267
|
1 |
|
'The class "' . $typeClassName . '" must implement the interface "phpDocumentor\Reflection\Type"' |
268
|
1 |
|
); |
269
|
|
|
} |
270
|
|
|
|
271
|
1 |
|
$this->keywords[$keyword] = $typeClassName; |
272
|
1 |
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* Detects whether the given type represents an array. |
276
|
|
|
* |
277
|
|
|
* @param string $type A relative or absolute type as defined in the phpDocumentor documentation. |
278
|
|
|
* |
279
|
|
|
* @return bool |
280
|
|
|
*/ |
281
|
8 |
|
private function isTypedArray($type) |
282
|
|
|
{ |
283
|
8 |
|
return substr($type, -2) === self::OPERATOR_ARRAY; |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
/** |
287
|
|
|
* Detects whether the given type represents a PHPDoc keyword. |
288
|
|
|
* |
289
|
|
|
* @param string $type A relative or absolute type as defined in the phpDocumentor documentation. |
290
|
|
|
* |
291
|
|
|
* @return bool |
292
|
|
|
*/ |
293
|
29 |
|
private function isKeyword($type) |
294
|
|
|
{ |
295
|
29 |
|
return in_array(strtolower($type), array_keys($this->keywords), true); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* Detects whether the given type represents a relative structural element name. |
300
|
|
|
* |
301
|
|
|
* @param string $type A relative or absolute type as defined in the phpDocumentor documentation. |
302
|
|
|
* |
303
|
|
|
* @return bool |
304
|
|
|
*/ |
305
|
5 |
|
private function isPartialStructuralElementName($type) |
306
|
|
|
{ |
307
|
5 |
|
return ($type[0] !== self::OPERATOR_NAMESPACE) && !$this->isKeyword($type); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* Tests whether the given type is a Fully Qualified Structural Element Name. |
312
|
|
|
* |
313
|
|
|
* @param string $type |
314
|
|
|
* |
315
|
|
|
* @return bool |
316
|
|
|
*/ |
317
|
7 |
|
private function isFqsen($type) |
318
|
|
|
{ |
319
|
7 |
|
return strpos($type, self::OPERATOR_NAMESPACE) === 0; |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
/** |
323
|
|
|
* Resolves the given typed array string (i.e. `string[]`) into an Array object with the right types set. |
324
|
|
|
* |
325
|
|
|
* @param string $type |
326
|
|
|
* @param Context $context |
327
|
|
|
* |
328
|
|
|
* @return Array_ |
329
|
|
|
*/ |
330
|
2 |
|
private function resolveTypedArray($type, Context $context) |
331
|
|
|
{ |
332
|
2 |
|
return new Array_($this->resolveSingleType(substr($type, 0, -2), $context)); |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* Resolves the given keyword (such as `string`) into a Type object representing that keyword. |
337
|
|
|
* |
338
|
|
|
* @param string $type |
339
|
|
|
* |
340
|
|
|
* @return Type |
341
|
|
|
*/ |
342
|
23 |
|
private function resolveKeyword($type) |
343
|
|
|
{ |
344
|
23 |
|
$className = $this->keywords[strtolower($type)]; |
345
|
|
|
|
346
|
23 |
|
return new $className(); |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
/** |
350
|
|
|
* Resolves the given FQSEN string into an FQSEN object. |
351
|
|
|
* |
352
|
|
|
* @param string $type |
353
|
|
|
* @param Context|null $context |
354
|
|
|
* |
355
|
|
|
* @return Object_ |
356
|
|
|
*/ |
357
|
7 |
|
private function resolveTypedObject($type, Context $context = null) |
358
|
|
|
{ |
359
|
7 |
|
return new Object_($this->fqsenResolver->resolve($type, $context)); |
360
|
|
|
} |
361
|
|
|
} |
362
|
|
|
|