diff --git a/src/TypeResolver.php b/src/TypeResolver.php index 4d1e9c7..4f2cfca 100644 --- a/src/TypeResolver.php +++ b/src/TypeResolver.php @@ -16,6 +16,7 @@ use ArrayIterator; use InvalidArgumentException; use phpDocumentor\Reflection\Types\Array_; +use phpDocumentor\Reflection\Types\ClassString; use phpDocumentor\Reflection\Types\Collection; use phpDocumentor\Reflection\Types\Compound; use phpDocumentor\Reflection\Types\Context; @@ -66,6 +67,7 @@ final class TypeResolver */ private $keywords = [ 'string' => Types\String_::class, + 'class-string' => Types\ClassString::class, 'int' => Types\Integer::class, 'integer' => Types\Integer::class, 'bool' => Types\Boolean::class, @@ -221,7 +223,11 @@ private function parseTypes(ArrayIterator $tokens, Context $context, int $parser $classType = array_pop($types); if ($classType !== null) { - $types[] = $this->resolveCollection($tokens, $classType, $context); + if ((string)$classType === 'class-string') { + $types[] = $this->resolveClassString($tokens, $context); + } else { + $types[] = $this->resolveCollection($tokens, $classType, $context); + } } $tokens->next(); @@ -386,6 +392,36 @@ private function resolveTypedObject(string $type, ?Context $context = null) : Ob return new Object_($this->fqsenResolver->resolve($type, $context)); } + /** + * Resolves class string + */ + private function resolveClassString(ArrayIterator $tokens, Context $context) : Type + { + $tokens->next(); + + $classType = $this->parseTypes($tokens, $context, self::PARSER_IN_COLLECTION_EXPRESSION); + + if (!$classType instanceof Object_ || $classType->getFqsen() === null) { + throw new RuntimeException( + $classType . ' is not a class string' + ); + } + + if ($tokens->current() !== '>') { + if (empty($tokens->current())) { + throw new RuntimeException( + 'class-string: ">" is missing' + ); + } + + throw new RuntimeException( + 'Unexpected character "' . $tokens->current() . '", ">" is missing' + ); + } + + return new ClassString($classType->getFqsen()); + } + /** * Resolves the collection values and keys * diff --git a/src/Types/ClassString.php b/src/Types/ClassString.php new file mode 100644 index 0000000..8a51d1a --- /dev/null +++ b/src/Types/ClassString.php @@ -0,0 +1,54 @@ +fqsen = $fqsen; + } + + /** + * Returns the FQSEN associated with this object. + */ + public function getFqsen() : ?Fqsen + { + return $this->fqsen; + } + + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + */ + public function __toString() : string + { + if ($this->fqsen === null) { + return 'class-string'; + } else { + return 'class-string<' . (string)$this->fqsen . '>'; + } + } +} diff --git a/tests/unit/TypeResolverTest.php b/tests/unit/TypeResolverTest.php index 9a011bf..f54dc0b 100644 --- a/tests/unit/TypeResolverTest.php +++ b/tests/unit/TypeResolverTest.php @@ -16,6 +16,7 @@ use Mockery as m; use phpDocumentor\Reflection\Types\Array_; use phpDocumentor\Reflection\Types\Boolean; +use phpDocumentor\Reflection\Types\ClassString; use phpDocumentor\Reflection\Types\Compound; use phpDocumentor\Reflection\Types\Context; use phpDocumentor\Reflection\Types\Iterable_; @@ -59,6 +60,30 @@ public function testResolvingKeywords(string $keyword, string $expectedClass) : $this->assertInstanceOf($expectedClass, $resolvedType); } + /** + * @uses \phpDocumentor\Reflection\Types\Context + * @uses \phpDocumentor\Reflection\Types\Object_ + * @uses \phpDocumentor\Reflection\Types\String_ + * + * @covers ::__construct + * @covers ::resolve + * @covers :: + * + * @dataProvider provideClassStrings + */ + public function testResolvingClassStrings(string $classString, bool $throwsException) : void + { + $fixture = new TypeResolver(); + + if ($throwsException) { + $this->expectException('RuntimeException'); + } + + $resolvedType = $fixture->resolve($classString, new Context('')); + + $this->assertInstanceOf(ClassString::class, $resolvedType); + } + /** * @uses \phpDocumentor\Reflection\Types\Context * @uses \phpDocumentor\Reflection\Types\Object_ @@ -634,6 +659,7 @@ public function provideKeywords() : array { return [ ['string', Types\String_::class], + ['class-string', Types\ClassString::class], ['int', Types\Integer::class], ['integer', Types\Integer::class], ['float', Types\Float_::class], @@ -657,6 +683,20 @@ public function provideKeywords() : array ]; } + /** + * Returns a list of class string types and whether they throw an exception. + * + * @return (string|bool)[][] + */ + public function provideClassStrings() : array + { + return [ + ['class-string<\phpDocumentor\Reflection>', false], + ['class-string<\phpDocumentor\Reflection\DocBlock>', false], + ['class-string', true], + ]; + } + /** * Provides a list of FQSENs to test the resolution patterns with. * diff --git a/tests/unit/Types/ClassStringTest.php b/tests/unit/Types/ClassStringTest.php new file mode 100644 index 0000000..4f779d4 --- /dev/null +++ b/tests/unit/Types/ClassStringTest.php @@ -0,0 +1,43 @@ +assertSame($expectedString, (string) $array); + } + + /** + * @return mixed[] + */ + public function provideClassStrings() : array + { + return [ + 'generic clss string' => [new ClassString(), 'class-string'], + 'typed class string' => [new ClassString(new Fqsen('\Foo\Bar')), 'class-string<\Foo\Bar>'], + ]; + } +}