18
18
use phpDocumentor \Reflection \Types \Iterable_ ;
19
19
use phpDocumentor \Reflection \Types \Nullable ;
20
20
use phpDocumentor \Reflection \Types \Object_ ;
21
+ use phpDocumentor \Reflection \Types \Collection ;
22
+ use phpDocumentor \Reflection \Types \String_ ;
23
+ use phpDocumentor \Reflection \Types \Integer ;
21
24
22
25
final class TypeResolver
23
26
{
@@ -36,6 +39,10 @@ final class TypeResolver
36
39
/** @var integer the iterator parser is inside an array expression context */
37
40
const PARSER_IN_ARRAY_EXPRESSION = 2 ;
38
41
42
+ /** @var integer the iterator parser is inside a collection expression context */
43
+ const PARSER_IN_COLLECTION_EXPRESSION = 3 ;
44
+
45
+
39
46
/** @var string[] List of recognized keywords and unto which Value Object they map */
40
47
private $ keywords = array (
41
48
'string ' => Types \String_::class,
@@ -112,8 +119,8 @@ public function resolve($type, Context $context = null)
112
119
$ context = new Context ('' );
113
120
}
114
121
115
- // split the type string into tokens `|`, `?`, `(`, `)[]` and type names
116
- $ tokens = preg_split ('/(\||\?|\ (|\)(?:\[ \])+)/ ' , $ type , -1 , PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );
122
+ // split the type string into tokens `|`, `?`, `(`, `)[]`, '<', '>' and type names
123
+ $ tokens = preg_split ('/( \\ || \\ ?|<|>|,| \\ (| \\ )(?: \\ [ \ \])+)/ ' , $ type , -1 , PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE );
117
124
$ tokenIterator = new \ArrayIterator ($ tokens );
118
125
119
126
return $ this ->parseTypes ($ tokenIterator , $ context , self ::PARSER_IN_COMPOUND );
@@ -143,7 +150,9 @@ private function parseTypes(\ArrayIterator $tokens, Context $context, $parserCon
143
150
);
144
151
}
145
152
if ($ parserContext !== self ::PARSER_IN_COMPOUND
146
- && $ parserContext !== self ::PARSER_IN_ARRAY_EXPRESSION ) {
153
+ && $ parserContext !== self ::PARSER_IN_ARRAY_EXPRESSION
154
+ && $ parserContext !== self ::PARSER_IN_COLLECTION_EXPRESSION
155
+ ) {
147
156
throw new \RuntimeException (
148
157
'Unexpected type separator '
149
158
);
@@ -152,7 +161,9 @@ private function parseTypes(\ArrayIterator $tokens, Context $context, $parserCon
152
161
153
162
} else if ($ token == '? ' ) {
154
163
if ($ parserContext !== self ::PARSER_IN_COMPOUND
155
- && $ parserContext !== self ::PARSER_IN_ARRAY_EXPRESSION ) {
164
+ && $ parserContext !== self ::PARSER_IN_ARRAY_EXPRESSION
165
+ && $ parserContext !== self ::PARSER_IN_COLLECTION_EXPRESSION
166
+ ) {
156
167
throw new \RuntimeException (
157
168
'Unexpected nullable character '
158
169
);
@@ -182,6 +193,22 @@ private function parseTypes(\ArrayIterator $tokens, Context $context, $parserCon
182
193
) {
183
194
break ;
184
195
196
+ } else if ($ token === '< ' ) {
197
+ if (count ($ types ) === 0 ) {
198
+ throw new \RuntimeException (
199
+ 'Unexpected collection operator "<", class name is missing '
200
+ );
201
+ }
202
+ $ classType = array_pop ($ types );
203
+
204
+ $ types [] = $ this ->resolveCollection ($ tokens , $ classType , $ context );
205
+
206
+ $ tokens ->next ();
207
+
208
+ } else if ($ parserContext === self ::PARSER_IN_COLLECTION_EXPRESSION
209
+ && ($ token === '> ' || $ token === ', ' )
210
+ ) {
211
+ break ;
185
212
} else {
186
213
$ type = $ this ->resolveSingleType ($ token , $ context );
187
214
$ tokens ->next ();
@@ -197,6 +224,7 @@ private function parseTypes(\ArrayIterator $tokens, Context $context, $parserCon
197
224
'A type is missing after a type separator '
198
225
);
199
226
}
227
+
200
228
if (count ($ types ) == 0 ) {
201
229
if ($ parserContext == self ::PARSER_IN_NULLABLE ) {
202
230
throw new \RuntimeException (
@@ -208,6 +236,11 @@ private function parseTypes(\ArrayIterator $tokens, Context $context, $parserCon
208
236
'A type is missing in an array expression '
209
237
);
210
238
}
239
+ if ($ parserContext == self ::PARSER_IN_COLLECTION_EXPRESSION ) {
240
+ throw new \RuntimeException (
241
+ 'A type is missing in a collection expression '
242
+ );
243
+ }
211
244
throw new \RuntimeException (
212
245
'No types in a compound list '
213
246
);
@@ -358,4 +391,79 @@ private function resolveTypedObject($type, Context $context = null)
358
391
{
359
392
return new Object_ ($ this ->fqsenResolver ->resolve ($ type , $ context ));
360
393
}
394
+
395
+ /**
396
+ * Resolves the collection values and keys
397
+ *
398
+ * @param \ArrayIterator $tokens
399
+ * @param Type $classType
400
+ * @param Context|null $context
401
+ * @return Array_|Collection
402
+ */
403
+ private function resolveCollection (\ArrayIterator $ tokens , Type $ classType , Context $ context = null ) {
404
+
405
+ $ isArray = ('array ' == (string ) $ classType );
406
+
407
+ // allow only "array" or class name before "<"
408
+ if (!$ isArray
409
+ && (! $ classType instanceof Object_ || $ classType ->getFqsen () === null )) {
410
+ throw new \RuntimeException (
411
+ $ classType .' is not a collection '
412
+ );
413
+ }
414
+
415
+ $ tokens ->next ();
416
+
417
+ $ valueType = $ this ->parseTypes ($ tokens , $ context , self ::PARSER_IN_COLLECTION_EXPRESSION );
418
+ $ keyType = null ;
419
+
420
+ if ($ tokens ->current () == ', ' ) {
421
+ // if we have a coma, then we just parsed the key type, not the value type
422
+ $ keyType = $ valueType ;
423
+ if ($ isArray ) {
424
+ // check the key type for an "array" collection. We allow only
425
+ // strings or integers.
426
+ if (! $ keyType instanceof String_ &&
427
+ ! $ keyType instanceof Integer &&
428
+ ! $ keyType instanceof Compound
429
+ ) {
430
+ throw new \RuntimeException (
431
+ 'An array can have only integers or strings as keys '
432
+ );
433
+ }
434
+ if ($ keyType instanceof Compound) {
435
+ foreach ($ keyType ->getIterator () as $ item ) {
436
+ if (! $ item instanceof String_ &&
437
+ ! $ item instanceof Integer
438
+ ) {
439
+ throw new \RuntimeException (
440
+ 'An array can have only integers or strings as keys '
441
+ );
442
+ }
443
+ }
444
+ }
445
+ }
446
+ $ tokens ->next ();
447
+ // now let's parse the value type
448
+ $ valueType = $ this ->parseTypes ($ tokens , $ context , self ::PARSER_IN_COLLECTION_EXPRESSION );
449
+ }
450
+
451
+ if ($ tokens ->current () !== '> ' ) {
452
+ if ($ tokens ->current () == '' ) {
453
+ throw new \RuntimeException (
454
+ 'Collection: ">" is missing '
455
+ );
456
+ }
457
+
458
+ throw new \RuntimeException (
459
+ 'Unexpected character " ' .$ tokens ->current ().'", ">" is missing '
460
+ );
461
+ }
462
+ if ($ isArray ) {
463
+ return new Array_ ($ valueType , $ keyType );
464
+ }
465
+ else {
466
+ return new Collection ($ classType ->getFqsen (), $ valueType , $ keyType );
467
+ }
468
+ }
361
469
}
0 commit comments