Skip to content

Commit 13ebf89

Browse files
laurentjjaapio
authored andcommitted
Support of collections
1 parent 0675d2c commit 13ebf89

File tree

5 files changed

+454
-14
lines changed

5 files changed

+454
-14
lines changed

src/TypeResolver.php

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
use phpDocumentor\Reflection\Types\Iterable_;
1919
use phpDocumentor\Reflection\Types\Nullable;
2020
use phpDocumentor\Reflection\Types\Object_;
21+
use phpDocumentor\Reflection\Types\Collection;
22+
use phpDocumentor\Reflection\Types\String_;
23+
use phpDocumentor\Reflection\Types\Integer;
2124

2225
final class TypeResolver
2326
{
@@ -36,6 +39,10 @@ final class TypeResolver
3639
/** @var integer the iterator parser is inside an array expression context */
3740
const PARSER_IN_ARRAY_EXPRESSION = 2;
3841

42+
/** @var integer the iterator parser is inside a collection expression context */
43+
const PARSER_IN_COLLECTION_EXPRESSION = 3;
44+
45+
3946
/** @var string[] List of recognized keywords and unto which Value Object they map */
4047
private $keywords = array(
4148
'string' => Types\String_::class,
@@ -112,8 +119,8 @@ public function resolve($type, Context $context = null)
112119
$context = new Context('');
113120
}
114121

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);
117124
$tokenIterator = new \ArrayIterator($tokens);
118125

119126
return $this->parseTypes($tokenIterator, $context, self::PARSER_IN_COMPOUND);
@@ -143,7 +150,9 @@ private function parseTypes(\ArrayIterator $tokens, Context $context, $parserCon
143150
);
144151
}
145152
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+
) {
147156
throw new \RuntimeException(
148157
'Unexpected type separator'
149158
);
@@ -152,7 +161,9 @@ private function parseTypes(\ArrayIterator $tokens, Context $context, $parserCon
152161

153162
} else if ($token == '?') {
154163
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+
) {
156167
throw new \RuntimeException(
157168
'Unexpected nullable character'
158169
);
@@ -182,6 +193,22 @@ private function parseTypes(\ArrayIterator $tokens, Context $context, $parserCon
182193
) {
183194
break;
184195

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;
185212
} else {
186213
$type = $this->resolveSingleType($token, $context);
187214
$tokens->next();
@@ -197,6 +224,7 @@ private function parseTypes(\ArrayIterator $tokens, Context $context, $parserCon
197224
'A type is missing after a type separator'
198225
);
199226
}
227+
200228
if (count($types) == 0) {
201229
if ($parserContext == self::PARSER_IN_NULLABLE) {
202230
throw new \RuntimeException(
@@ -208,6 +236,11 @@ private function parseTypes(\ArrayIterator $tokens, Context $context, $parserCon
208236
'A type is missing in an array expression'
209237
);
210238
}
239+
if ($parserContext == self::PARSER_IN_COLLECTION_EXPRESSION) {
240+
throw new \RuntimeException(
241+
'A type is missing in a collection expression'
242+
);
243+
}
211244
throw new \RuntimeException(
212245
'No types in a compound list'
213246
);
@@ -358,4 +391,79 @@ private function resolveTypedObject($type, Context $context = null)
358391
{
359392
return new Object_($this->fqsenResolver->resolve($type, $context));
360393
}
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+
}
361469
}

src/Types/Array_.php

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,33 @@
2323
* 2. Types (`string[]`), where the value type is provided by preceding an opening and closing square bracket with a
2424
* type name.
2525
*/
26-
final class Array_ implements Type
26+
class Array_ implements Type
2727
{
2828
/** @var Type */
29-
private $valueType;
29+
protected $valueType;
30+
31+
/** @var Type|null */
32+
protected $keyType;
3033

3134
/** @var Type */
32-
private $keyType;
35+
protected $defaultKeyType;
3336

3437
/**
35-
* Initializes this representation of an array with the given Type or Fqsen.
38+
* Initializes this representation of an array with the given Type.
3639
*
3740
* @param Type $valueType
3841
* @param Type $keyType
3942
*/
4043
public function __construct(Type $valueType = null, Type $keyType = null)
4144
{
42-
if ($keyType === null) {
43-
$keyType = new Compound([ new String_(), new Integer() ]);
44-
}
4545
if ($valueType === null) {
4646
$valueType = new Mixed_();
4747
}
4848

4949
$this->valueType = $valueType;
50+
$this->defaultKeyType = new Compound([ new String_(), new Integer() ]);
5051
$this->keyType = $keyType;
52+
5153
}
5254

5355
/**
@@ -57,6 +59,9 @@ public function __construct(Type $valueType = null, Type $keyType = null)
5759
*/
5860
public function getKeyType()
5961
{
62+
if ($this->keyType === null) {
63+
return $this->defaultKeyType;
64+
}
6065
return $this->keyType;
6166
}
6267

@@ -77,6 +82,10 @@ public function getValueType()
7782
*/
7883
public function __toString()
7984
{
85+
if ($this->keyType) {
86+
return 'array<'.$this->keyType.','.$this->valueType.'>';
87+
}
88+
8089
if ($this->valueType instanceof Mixed_) {
8190
return 'array';
8291
}

src/Types/Collection.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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\Types;
14+
15+
use phpDocumentor\Reflection\Type;
16+
use phpDocumentor\Reflection\Fqsen;
17+
18+
/**
19+
* Represents a collection type as described in the PSR-5, the PHPDoc Standard.
20+
*
21+
* A collection can be represented in two forms:
22+
*
23+
* 1. `ACollectionObject<aValueType>`
24+
* 2. `ACollectionObject<aValueType,aKeyType>`
25+
*
26+
* - ACollectionObject can be 'array' or an object that can act as an array
27+
* - aValueType and aKeyType can be any type expression
28+
*/
29+
class Collection extends Array_
30+
{
31+
32+
/** @var Fqsen */
33+
private $fqsen;
34+
35+
/**
36+
* Initializes this representation of an array with the given Type or Fqsen.
37+
*
38+
* @param Type $valueType
39+
* @param Type $keyType
40+
*/
41+
public function __construct(Fqsen $fqsen, Type $valueType, Type $keyType = null)
42+
{
43+
parent::__construct($valueType, $keyType);
44+
45+
$this->fqsen = $fqsen;
46+
47+
}
48+
49+
/**
50+
* Returns the FQSEN associated with this object.
51+
*
52+
* @return Fqsen
53+
*/
54+
public function getFqsen()
55+
{
56+
return $this->fqsen;
57+
}
58+
59+
/**
60+
* Returns a rendered output of the Type as it would be used in a DocBlock.
61+
*
62+
* @return string
63+
*/
64+
public function __toString()
65+
{
66+
if ($this->keyType === null) {
67+
return $this->fqsen.'<'.$this->valueType . '>';
68+
}
69+
return $this->fqsen.'<'.$this->keyType . ',' . $this->valueType . '>';
70+
}
71+
}

0 commit comments

Comments
 (0)