From fab71692559e3b3f383509ef193a255dde8f0e9d Mon Sep 17 00:00:00 2001 From: Mike van Riel Date: Sun, 7 Jun 2015 11:03:28 +0200 Subject: [PATCH 1/4] Add support for types and improve resolution As part of the re-architecting of phpDocumentor and its Reflection component we needed to move the type classes from the \phpDocumentor\Descriptor namespace to \phpDocumentor\Reflection\Types. In addition to this I have removed the resolution / type creation from the Collection class into a new Resolver class that is capable of resolving any type into a Type or Fqsen object. This is the first commit with the base classes and a test for the Resolver functionality in subsequent commits we need to add tests for the other classes and to refactor the collection object. --- src/Type.php | 18 ++ src/Types/Array_.php | 63 +++++ src/Types/Boolean.php | 23 ++ src/Types/Callable_.php | 23 ++ src/Types/Collection.php | 113 ++++++++ src/Types/Compound.php | 60 +++++ src/Types/Float.php | 23 ++ src/Types/Integer.php | 23 ++ src/Types/Mixed.php | 23 ++ src/Types/Null_.php | 23 ++ src/Types/Object_.php | 23 ++ src/Types/Resolver.php | 251 ++++++++++++++++++ src/Types/Resource.php | 23 ++ src/Types/Scalar.php | 23 ++ src/Types/Self_.php | 23 ++ src/Types/Static_.php | 23 ++ src/Types/String.php | 23 ++ src/Types/This.php | 23 ++ src/Types/Void.php | 23 ++ .../Reflection/Types/ResolverTest.php | 235 ++++++++++++++++ 20 files changed, 1062 insertions(+) create mode 100644 src/Type.php create mode 100644 src/Types/Array_.php create mode 100644 src/Types/Boolean.php create mode 100644 src/Types/Callable_.php create mode 100644 src/Types/Collection.php create mode 100644 src/Types/Compound.php create mode 100644 src/Types/Float.php create mode 100644 src/Types/Integer.php create mode 100644 src/Types/Mixed.php create mode 100644 src/Types/Null_.php create mode 100644 src/Types/Object_.php create mode 100644 src/Types/Resolver.php create mode 100644 src/Types/Resource.php create mode 100644 src/Types/Scalar.php create mode 100644 src/Types/Self_.php create mode 100644 src/Types/Static_.php create mode 100644 src/Types/String.php create mode 100644 src/Types/This.php create mode 100644 src/Types/Void.php create mode 100644 tests/phpDocumentor/Reflection/Types/ResolverTest.php diff --git a/src/Type.php b/src/Type.php new file mode 100644 index 0000000..33ca559 --- /dev/null +++ b/src/Type.php @@ -0,0 +1,18 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection; + +interface Type +{ + public function __toString(); +} diff --git a/src/Types/Array_.php b/src/Types/Array_.php new file mode 100644 index 0000000..ffb364a --- /dev/null +++ b/src/Types/Array_.php @@ -0,0 +1,63 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Type; + +class Array_ implements Type +{ + /** @var Type */ + private $valueType; + + /** @var Type */ + private $keyType; + + public function __construct(Type $valueType = null, Type $keyType = null) + { + if ($keyType === null) { + $keyType = new Mixed(); + } + if ($valueType === null) { + $valueType = new Mixed(); + } + + $this->valueType = $valueType; + $this->keyType = $keyType; + } + + /** + * @return Type + */ + public function getKeyType() + { + return $this->keyType; + } + + /** + * @return Type + */ + public function getValueType() + { + return $this->valueType; + } + + public function __toString() + { + if ($this->valueType instanceof Mixed) { + return 'array'; + } + + return $this->valueType . '[]'; + } +} diff --git a/src/Types/Boolean.php b/src/Types/Boolean.php new file mode 100644 index 0000000..08db91c --- /dev/null +++ b/src/Types/Boolean.php @@ -0,0 +1,23 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Type; + +final class Boolean implements Type +{ + public function __toString() + { + return 'bool'; + } +} diff --git a/src/Types/Callable_.php b/src/Types/Callable_.php new file mode 100644 index 0000000..4479a93 --- /dev/null +++ b/src/Types/Callable_.php @@ -0,0 +1,23 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Type; + +final class Callable_ implements Type +{ + public function __toString() + { + return 'callable'; + } +} diff --git a/src/Types/Collection.php b/src/Types/Collection.php new file mode 100644 index 0000000..796d9e7 --- /dev/null +++ b/src/Types/Collection.php @@ -0,0 +1,113 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Types\Resolver; +use phpDocumentor\Reflection\DocBlock\Context; + +/** + * Collection + */ +class Collection extends \ArrayObject +{ + /** @var string Definition of the OR operator for types */ + const OPERATOR_OR = '|'; + + /** + * Current invoking location. + * + * This is used to prepend to type with a relative location. + * May also be 'default' or 'global', in which case they are ignored. + * + * @var Context + */ + protected $context = null; + /** + * @var Resolver + */ + private $resolver; + + /** + * Registers the namespace and aliases; uses that to add and expand the given types. + * + * @param string[] $types Array containing a list of types to add to this container. + * @param Context $context + * @param Resolver $resolver + */ + public function __construct(array $types = array(), Context $context = null, Resolver $resolver = null) + { + $this->context = $context ?: new Context(''); + $this->resolver = $resolver ?: new Resolver(); + + foreach ($types as $type) { + $this->add($type); + } + } + + /** + * Returns the current invoking location. + * + * @return Context + */ + public function getContext() + { + return $this->context; + } + + /** + * Adds a new type to the collection and expands it if it contains a + * relative namespace. + * + * If a class in the type contains a relative namespace than this collection + * will try to expand that into a FQCN. + * + * @param string $type A 'Type' as defined in the phpDocumentor + * documentation. + * + * @throws \InvalidArgumentException if a non-string argument is passed. + * + * @see http://phpdoc.org/docs/latest/for-users/types.html for the + * definition of a type. + * + * @return void + */ + public function add($type) + { + if (!is_string($type)) { + throw new \InvalidArgumentException( + 'A type should be represented by a string, received: ' + . var_export($type, true) + ); + } + + // separate the type by the OR operator + $type_parts = explode(self::OPERATOR_OR, $type); + foreach ($type_parts as $part) { + $expanded_type = $this->resolver->resolve($part, $this->context); + if ($expanded_type) { + $this[] = $expanded_type; + } + } + } + + /** + * Returns a string representation of the collection. + * + * @return string The resolved types across the collection, separated with + * {@link self::OPERATOR_OR}. + */ + public function __toString() + { + return implode(self::OPERATOR_OR, $this->getArrayCopy()); + } +} diff --git a/src/Types/Compound.php b/src/Types/Compound.php new file mode 100644 index 0000000..5421ede --- /dev/null +++ b/src/Types/Compound.php @@ -0,0 +1,60 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Fqsen; +use phpDocumentor\Reflection\Type; + +final class Compound implements Type +{ + /** @var Type[] */ + private $types = []; + + /** + * @param Type[]|Fqsen[] $types + */ + public function __construct($types) + { + $this->types = $types; + } + + /** + * @param integer $index + * + * @return null|Type|Fqsen + */ + public function get($index) + { + if (!$this->has($index)) { + return null; + } + + return $this->types[$index]; + } + + /** + * @param integer $index + * + * @return bool + */ + public function has($index) + { + return isset($this->types[$index]); + } + + public function __toString() + { + return implode('|', $this->types); + } +} diff --git a/src/Types/Float.php b/src/Types/Float.php new file mode 100644 index 0000000..a51dcaa --- /dev/null +++ b/src/Types/Float.php @@ -0,0 +1,23 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Type; + +final class Float implements Type +{ + public function __toString() + { + return 'float'; + } +} diff --git a/src/Types/Integer.php b/src/Types/Integer.php new file mode 100644 index 0000000..99c8f96 --- /dev/null +++ b/src/Types/Integer.php @@ -0,0 +1,23 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Type; + +final class Integer implements Type +{ + public function __toString() + { + return 'int'; + } +} diff --git a/src/Types/Mixed.php b/src/Types/Mixed.php new file mode 100644 index 0000000..904315b --- /dev/null +++ b/src/Types/Mixed.php @@ -0,0 +1,23 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Type; + +final class Mixed implements Type +{ + public function __toString() + { + return 'mixed'; + } +} diff --git a/src/Types/Null_.php b/src/Types/Null_.php new file mode 100644 index 0000000..6a78cb8 --- /dev/null +++ b/src/Types/Null_.php @@ -0,0 +1,23 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Type; + +final class Null_ implements Type +{ + public function __toString() + { + return 'null'; + } +} diff --git a/src/Types/Object_.php b/src/Types/Object_.php new file mode 100644 index 0000000..77cecb0 --- /dev/null +++ b/src/Types/Object_.php @@ -0,0 +1,23 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Type; + +final class Object_ implements Type +{ + public function __toString() + { + return 'object'; + } +} diff --git a/src/Types/Resolver.php b/src/Types/Resolver.php new file mode 100644 index 0000000..451bbb0 --- /dev/null +++ b/src/Types/Resolver.php @@ -0,0 +1,251 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\DocBlock\Context; +use phpDocumentor\Reflection\Fqsen; +use phpDocumentor\Reflection\Type; + +class Resolver +{ + /** @var string Definition of the ARRAY operator for types */ + const OPERATOR_ARRAY = '[]'; + + /** @var string Definition of the NAMESPACE operator in PHP */ + const OPERATOR_NAMESPACE = '\\'; + + /** @var string[] List of recognized keywords and unto which Value Object they map */ + private $keywords = array( + 'string' => 'phpDocumentor\Reflection\Types\String', + 'int' => 'phpDocumentor\Reflection\Types\Integer', + 'integer' => 'phpDocumentor\Reflection\Types\Integer', + 'bool' => 'phpDocumentor\Reflection\Types\Boolean', + 'boolean' => 'phpDocumentor\Reflection\Types\Boolean', + 'float' => 'phpDocumentor\Reflection\Types\Float', + 'double' => 'phpDocumentor\Reflection\Types\Float', + 'object' => 'phpDocumentor\Reflection\Types\Object_', + 'mixed' => 'phpDocumentor\Reflection\Types\Mixed', + 'array' => 'phpDocumentor\Reflection\Types\Array_', + 'resource' => 'phpDocumentor\Reflection\Types\Resource', + 'void' => 'phpDocumentor\Reflection\Types\Void', + 'null' => 'phpDocumentor\Reflection\Types\Null_', + 'scalar' => 'phpDocumentor\Reflection\Types\Scalar', + 'callback' => 'phpDocumentor\Reflection\Types\Callable_', + 'callable' => 'phpDocumentor\Reflection\Types\Callable_', + 'false' => 'phpDocumentor\Reflection\Types\Boolean', + 'true' => 'phpDocumentor\Reflection\Types\Boolean', + 'self' => 'phpDocumentor\Reflection\Types\Self_', + '$this' => 'phpDocumentor\Reflection\Types\This', + 'static' => 'phpDocumentor\Reflection\Types\Static_' + ); + + /** + * Analyzes the given type and returns the FQCN variant. + * + * When a type is provided this method checks whether it is not a keyword or + * Fully Qualified Class Name. If so it will use the given namespace and + * aliases to expand the type to a FQCN representation. + * + * This method only works as expected if the namespace and aliases are set; + * no dynamic reflection is being performed here. + * + * @param string $type The relative or absolute type. + * + * @uses Context::getNamespace() to determine with what to prefix the type name. + * @uses Context::getNamespaceAliases() to check whether the first part of the relative type name should not be + * replaced with another namespace. + * + * @return Fqsen|Type|null + */ + public function resolve($type, Context $context) + { + if (!is_string($type)) { + throw new \InvalidArgumentException( + 'Attempted to resolve type but it appeared not to be a string, received: ' . var_export($type, true) + ); + } + + $type = trim($type); + if (!$type) { + throw new \InvalidArgumentException('Attempted to resolve "' . $type . '" but it appears to be empty'); + } + + switch (true) { + case $this->isKeyword($type): + return $this->resolveKeyword($type); + case ($this->isCompoundType($type)): + return $this->resolveCompoundType($type, $context); + case $this->isFqsen($type): + return $this->resolveFqsen($type); + case $this->isTypedArray($type): + return $this->resolveTypedArray($type, $context); + case $this->isPartialStructuralElementName($type): + return $this->resolvePartialStructuralElementName($type, $context); + // @codeCoverageIgnoreStart + default: + // I haven't got the foggiest how the logic would come here but added this as a defense. + throw new \RuntimeException( + 'Unable to resolve type "' . $type . '", there is no known method to resolve it' + ); + } + // @codeCoverageIgnoreEnd + } + + /** + * Detects whether the given type represents an array. + * + * @param string $type A relative or absolute type as defined in the phpDocumentor documentation. + * + * @return bool + */ + private function isTypedArray($type) + { + return substr($type, -2) === self::OPERATOR_ARRAY; + } + + /** + * Detects whether the given type represents a PHPDoc keyword. + * + * @param string $type A relative or absolute type as defined in the phpDocumentor documentation. + * + * @return bool + */ + private function isKeyword($type) + { + return in_array(strtolower($type), array_keys($this->keywords), true); + } + + /** + * Detects whether the given type represents a relative structural element name. + * + * @param string $type A relative or absolute type as defined in the phpDocumentor documentation. + * + * @return bool + */ + private function isPartialStructuralElementName($type) + { + return ($type[0] !== self::OPERATOR_NAMESPACE) && !$this->isKeyword($type); + } + + /** + * Tests whether the given type is a Fully Qualified Structural Element Name. + * + * @param string $type + * + * @return bool + */ + private function isFqsen($type) + { + return strpos($type, self::OPERATOR_NAMESPACE) === 0; + } + + /** + * Tests whether the given type is a compound type (i.e. `string|int`). + * + * @param string $type + * + * @return bool + */ + private function isCompoundType($type) + { + return strpos($type, '|') !== false; + } + + /** + * Resolves the given typed array string (i.e. `string[]`) into an Array object with the right types set. + * + * @param string $type + * @param Context $context + * + * @return Array_ + */ + private function resolveTypedArray($type, Context $context) + { + return new Array_($this->resolve(substr($type, 0, -2), $context)); + } + + /** + * Resolves the given keyword (such as `string`) into a Type object representing that keyword. + * + * @param string $type + * + * @return Type + */ + private function resolveKeyword($type) + { + $className = $this->keywords[strtolower($type)]; + + return new $className(); + } + + /** + * Resolves the given FQSEN string into an FQSEN object. + * + * @param string $type + * + * @return Fqsen + */ + private function resolveFqsen($type) + { + return new Fqsen($type); + } + + /** + * Resolves a partial Structural Element Name (i.e. `Reflection\DocBlock`) to its FQSEN representation + * (i.e. `\phpDocumentor\Reflection\DocBlock`) based on the Namespace and aliases mentioned in the Context. + * + * @param string $type + * @param Context $context + * + * @return Fqsen + */ + private function resolvePartialStructuralElementName($type, Context $context) + { + $typeParts = explode(self::OPERATOR_NAMESPACE, $type, 2); + + $namespaceAliases = $context->getNamespaceAliases(); + + // if the first segment is not an alias; prepend namespace name and return + if (!isset($namespaceAliases[$typeParts[0]])) { + $namespace = $context->getNamespace(); + if ('' !== $namespace) { + $namespace .= self::OPERATOR_NAMESPACE; + } + + return new Fqsen(self::OPERATOR_NAMESPACE . $namespace . $type); + } + + $typeParts[0] = $namespaceAliases[$typeParts[0]]; + + return new Fqsen(self::OPERATOR_NAMESPACE . implode(self::OPERATOR_NAMESPACE, $typeParts)); + } + + /** + * Resolves a compound type (i.e. `string|int`) into the appropriate Type objects or FQSEN. + * + * @param string $type + * @param Context $context + * + * @return Compound + */ + private function resolveCompoundType($type, Context $context) + { + $types = []; + + foreach (explode('|', $type) as $part) { + $types[] = $this->resolve($part, $context); + } + + return new Compound($types); + } +} diff --git a/src/Types/Resource.php b/src/Types/Resource.php new file mode 100644 index 0000000..2a8a201 --- /dev/null +++ b/src/Types/Resource.php @@ -0,0 +1,23 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Type; + +final class Resource implements Type +{ + public function __toString() + { + return 'resource'; + } +} diff --git a/src/Types/Scalar.php b/src/Types/Scalar.php new file mode 100644 index 0000000..5dadde8 --- /dev/null +++ b/src/Types/Scalar.php @@ -0,0 +1,23 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Type; + +class Scalar implements Type +{ + public function __toString() + { + return 'scalar'; + } +} diff --git a/src/Types/Self_.php b/src/Types/Self_.php new file mode 100644 index 0000000..933b4b8 --- /dev/null +++ b/src/Types/Self_.php @@ -0,0 +1,23 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Type; + +final class Self_ implements Type +{ + public function __toString() + { + return 'self'; + } +} diff --git a/src/Types/Static_.php b/src/Types/Static_.php new file mode 100644 index 0000000..4922fd3 --- /dev/null +++ b/src/Types/Static_.php @@ -0,0 +1,23 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Type; + +final class Static_ implements Type +{ + public function __toString() + { + return 'static'; + } +} diff --git a/src/Types/String.php b/src/Types/String.php new file mode 100644 index 0000000..8e4e5a2 --- /dev/null +++ b/src/Types/String.php @@ -0,0 +1,23 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Type; + +final class String implements Type +{ + public function __toString() + { + return 'string'; + } +} diff --git a/src/Types/This.php b/src/Types/This.php new file mode 100644 index 0000000..d0aec28 --- /dev/null +++ b/src/Types/This.php @@ -0,0 +1,23 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Type; + +final class This implements Type +{ + public function __toString() + { + return '$this'; + } +} diff --git a/src/Types/Void.php b/src/Types/Void.php new file mode 100644 index 0000000..f323349 --- /dev/null +++ b/src/Types/Void.php @@ -0,0 +1,23 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\Type; + +final class Void implements Type +{ + public function __toString() + { + return 'void'; + } +} diff --git a/tests/phpDocumentor/Reflection/Types/ResolverTest.php b/tests/phpDocumentor/Reflection/Types/ResolverTest.php new file mode 100644 index 0000000..2c07b9e --- /dev/null +++ b/tests/phpDocumentor/Reflection/Types/ResolverTest.php @@ -0,0 +1,235 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://phpdoc.org + */ + +namespace phpDocumentor\Reflection\Types; + +use phpDocumentor\Reflection\DocBlock\Context; + +/** + * @coversDefaultClass phpDocumentor\Reflection\Types\Resolver + */ +class ResolverTest extends \PHPUnit_Framework_TestCase +{ + /** + * @param string $keyword + * @param string $expectedClass + * + * @covers ::resolve + * @covers :: + * + * @uses phpDocumentor\Reflection\DocBlock\Context + * @uses phpDocumentor\Reflection\Types\Array_ + * + * @dataProvider provideKeywords + */ + public function testResolvingKeywords($keyword, $expectedClass) + { + $fixture = new Resolver(); + + $resolvedType = $fixture->resolve($keyword, new Context('')); + + $this->assertInstanceOf($expectedClass, $resolvedType); + } + + /** + * @param string $fqsen + * + * @covers ::resolve + * @covers :: + * + * @uses phpDocumentor\Reflection\DocBlock\Context + * @uses phpDocumentor\Reflection\Fqsen + * + * @dataProvider provideFqsen + */ + public function testResolvingFQSENs($fqsen) + { + $fixture = new Resolver(); + + $resolvedType = $fixture->resolve($fqsen, new Context('')); + + $this->assertInstanceOf('phpDocumentor\Reflection\Fqsen', $resolvedType); + $this->assertSame($fqsen, (string)$resolvedType); + } + + + /** + * @covers ::resolve + * @covers :: + * + * @uses phpDocumentor\Reflection\DocBlock\Context + * @uses phpDocumentor\Reflection\Fqsen + */ + public function testResolvingRelativeQSENsBasedOnNamespace() + { + $fixture = new Resolver(); + + $resolvedType = $fixture->resolve('DocBlock', new Context('phpDocumentor\Reflection')); + + $this->assertInstanceOf('phpDocumentor\Reflection\Fqsen', $resolvedType); + $this->assertSame('\phpDocumentor\Reflection\DocBlock', (string)$resolvedType); + } + + /** + * @covers ::resolve + * @covers :: + * + * @uses phpDocumentor\Reflection\DocBlock\Context + * @uses phpDocumentor\Reflection\Fqsen + */ + public function testResolvingRelativeQSENsBasedOnNamespaceAlias() + { + $fixture = new Resolver(); + + $resolvedType = $fixture->resolve( + 'm\MockInterface', + new Context('phpDocumentor\Reflection', ['m' => '\Mockery']) + ); + + $this->assertInstanceOf('phpDocumentor\Reflection\Fqsen', $resolvedType); + $this->assertSame('\Mockery\MockInterface', (string)$resolvedType); + } + + /** + * @covers ::resolve + * @covers :: + * + * @uses phpDocumentor\Reflection\DocBlock\Context + * @uses phpDocumentor\Reflection\Types\Array_ + * @uses phpDocumentor\Reflection\Types\String + */ + public function testResolvingTypedArrays() + { + $fixture = new Resolver(); + + /** @var Array_ $resolvedType */ + $resolvedType = $fixture->resolve('string[]', new Context('')); + + $this->assertInstanceOf('phpDocumentor\Reflection\Types\Array_', $resolvedType); + $this->assertSame('string[]', (string)$resolvedType); + $this->assertInstanceOf('phpDocumentor\Reflection\Types\Mixed', $resolvedType->getKeyType()); + $this->assertInstanceOf('phpDocumentor\Reflection\Types\String', $resolvedType->getValueType()); + } + + /** + * @covers ::resolve + * @covers :: + * + * @uses phpDocumentor\Reflection\DocBlock\Context + * @uses phpDocumentor\Reflection\Types\Array_ + * @uses phpDocumentor\Reflection\Types\String + */ + public function testResolvingNestedTypedArrays() + { + $fixture = new Resolver(); + + /** @var Array_ $resolvedType */ + $resolvedType = $fixture->resolve('string[][]', new Context('')); + + /** @var Array_ $childValueType */ + $childValueType = $resolvedType->getValueType(); + + $this->assertInstanceOf('phpDocumentor\Reflection\Types\Array_', $resolvedType); + + $this->assertSame('string[][]', (string)$resolvedType); + $this->assertInstanceOf('phpDocumentor\Reflection\Types\Mixed', $resolvedType->getKeyType()); + $this->assertInstanceOf('phpDocumentor\Reflection\Types\Array_', $childValueType); + + $this->assertSame('string[]', (string)$childValueType); + $this->assertInstanceOf('phpDocumentor\Reflection\Types\Mixed', $childValueType->getKeyType()); + $this->assertInstanceOf('phpDocumentor\Reflection\Types\String', $childValueType->getValueType()); + } + + /** + * @covers ::resolve + * @covers :: + * + * @uses phpDocumentor\Reflection\DocBlock\Context + * @uses phpDocumentor\Reflection\Types\Compound + * @uses phpDocumentor\Reflection\Types\String + * @uses phpDocumentor\Reflection\Fqsen + */ + public function testResolvingCompoundTypes() + { + $fixture = new Resolver(); + + /** @var Compound $resolvedType */ + $resolvedType = $fixture->resolve('string|Reflection\DocBlock', new Context('phpDocumentor')); + + $this->assertInstanceOf('phpDocumentor\Reflection\Types\Compound', $resolvedType); + $this->assertSame('string|\phpDocumentor\Reflection\DocBlock', (string)$resolvedType); + $this->assertInstanceOf('phpDocumentor\Reflection\Types\String', $resolvedType->get(0)); + $this->assertInstanceOf('phpDocumentor\Reflection\Fqsen', $resolvedType->get(1)); + } + + /** + * @covers ::resolve + * @uses phpDocumentor\Reflection\DocBlock\Context + * + * @expectedException \InvalidArgumentException + */ + public function testExceptionIsThrownIfTypeIsEmpty() + { + $fixture = new Resolver(); + $fixture->resolve(' ', new Context('')); + } + + /** + * @covers ::resolve + * @uses phpDocumentor\Reflection\DocBlock\Context + * + * @expectedException \InvalidArgumentException + */ + public function testExceptionIsThrownIfTypeIsNotAString() + { + $fixture = new Resolver(); + $fixture->resolve(['a'], new Context('')); + } + + public function provideKeywords() + { + return [ + ['string', 'phpDocumentor\Reflection\Types\String'], + ['int', 'phpDocumentor\Reflection\Types\Integer'], + ['integer', 'phpDocumentor\Reflection\Types\Integer'], + ['float', 'phpDocumentor\Reflection\Types\Float'], + ['double', 'phpDocumentor\Reflection\Types\Float'], + ['bool', 'phpDocumentor\Reflection\Types\Boolean'], + ['boolean', 'phpDocumentor\Reflection\Types\Boolean'], + ['resource', 'phpDocumentor\Reflection\Types\Resource'], + ['null', 'phpDocumentor\Reflection\Types\Null_'], + ['callable', 'phpDocumentor\Reflection\Types\Callable_'], + ['callback', 'phpDocumentor\Reflection\Types\Callable_'], + ['array', 'phpDocumentor\Reflection\Types\Array_'], + ['scalar', 'phpDocumentor\Reflection\Types\Scalar'], + ['object', 'phpDocumentor\Reflection\Types\Object_'], + ['mixed', 'phpDocumentor\Reflection\Types\Mixed'], + ['void', 'phpDocumentor\Reflection\Types\Void'], + ['$this', 'phpDocumentor\Reflection\Types\This'], + ['static', 'phpDocumentor\Reflection\Types\Static_'], + ['self', 'phpDocumentor\Reflection\Types\Self_'], + ]; + } + + public function provideFqsen() + { + return [ + 'namespace' => ['\phpDocumentor\Reflection'], + 'class' => ['\phpDocumentor\Reflection\DocBlock'], + 'function' => ['\DI\object()'], + 'constant' => ['\phpDocumentor\Reflection\GLOBAL_CONSTANT'], + 'classConstant' => ['\phpDocumentor\Reflection\DocBlock::CONSTANT'], + 'property' => ['\phpDocumentor\Reflection\DocBlock::$summary'], + 'method' => ['\phpDocumentor\Reflection\DocBlock::getSummary()'], + ]; + } +} From b54671f51fd990b54e3b6b07cba98a7814439074 Mon Sep 17 00:00:00 2001 From: Mike van Riel Date: Mon, 8 Jun 2015 07:05:32 +0200 Subject: [PATCH 2/4] Introduce Types phpDocumentor is capable of representing a whole series of types, more than PHP supports in fact, and this commit represents those types and a Resolver that is capable of taking a type Expression and resolving that to a series of Value Objects representing those types. --- src/Types/Array_.php | 28 ++- src/Types/Boolean.php | 8 + src/Types/Callable_.php | 8 + src/Types/Collection.php | 113 --------- src/Types/Compound.php | 30 ++- src/Types/Float.php | 8 + src/Types/Integer.php | 5 + src/Types/Mixed.php | 8 + src/Types/Null_.php | 8 + src/Types/Object_.php | 47 ++++ src/Types/Resolver.php | 38 ++- src/Types/Resource.php | 8 + src/Types/Scalar.php | 10 +- src/Types/Self_.php | 10 + src/Types/Static_.php | 15 ++ src/Types/String.php | 8 + src/Types/This.php | 11 + src/Types/Void.php | 11 + .../Reflection/DocBlock/Type/Collection.php | 221 ------------------ .../Reflection/Types/ResolverTest.php | 131 ++++++++++- 20 files changed, 370 insertions(+), 356 deletions(-) delete mode 100644 src/Types/Collection.php delete mode 100644 src/phpDocumentor/Reflection/DocBlock/Type/Collection.php diff --git a/src/Types/Array_.php b/src/Types/Array_.php index ffb364a..cf5d7aa 100644 --- a/src/Types/Array_.php +++ b/src/Types/Array_.php @@ -10,12 +10,21 @@ * @link http://phpdoc.org */ - namespace phpDocumentor\Reflection\Types; +use phpDocumentor\Reflection\Fqsen; use phpDocumentor\Reflection\Type; -class Array_ implements Type +/** + * Represents an array type as described in the PSR-5, the PHPDoc Standard. + * + * An array can be represented in two forms: + * + * 1. Untyped (`array`), where the key and value type is unknown and hence classified as 'Mixed'. + * 2. Types (`string[]`), where the value type is provided by preceding an opening and closing square bracket with a + * type name. + */ +final class Array_ implements Type { /** @var Type */ private $valueType; @@ -23,6 +32,12 @@ class Array_ implements Type /** @var Type */ private $keyType; + /** + * Initializes this representation of an array with the given Type or Fqsen. + * + * @param Type $valueType + * @param Type $keyType + */ public function __construct(Type $valueType = null, Type $keyType = null) { if ($keyType === null) { @@ -37,6 +52,8 @@ class Array_ implements Type } /** + * Returns the type for the keys of this array. + * * @return Type */ public function getKeyType() @@ -45,6 +62,8 @@ class Array_ implements Type } /** + * Returns the value for the keys of this array. + * * @return Type */ public function getValueType() @@ -52,6 +71,11 @@ class Array_ implements Type return $this->valueType; } + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + * + * @return string + */ public function __toString() { if ($this->valueType instanceof Mixed) { diff --git a/src/Types/Boolean.php b/src/Types/Boolean.php index 08db91c..f82b19e 100644 --- a/src/Types/Boolean.php +++ b/src/Types/Boolean.php @@ -14,8 +14,16 @@ namespace phpDocumentor\Reflection\Types; use phpDocumentor\Reflection\Type; +/** + * Value Object representing a Boolean type. + */ final class Boolean implements Type { + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + * + * @return string + */ public function __toString() { return 'bool'; diff --git a/src/Types/Callable_.php b/src/Types/Callable_.php index 4479a93..68ebfbd 100644 --- a/src/Types/Callable_.php +++ b/src/Types/Callable_.php @@ -14,8 +14,16 @@ namespace phpDocumentor\Reflection\Types; use phpDocumentor\Reflection\Type; +/** + * Value Object representing a Callable type. + */ final class Callable_ implements Type { + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + * + * @return string + */ public function __toString() { return 'callable'; diff --git a/src/Types/Collection.php b/src/Types/Collection.php deleted file mode 100644 index 796d9e7..0000000 --- a/src/Types/Collection.php +++ /dev/null @@ -1,113 +0,0 @@ - - * @license http://www.opensource.org/licenses/mit-license.php MIT - * @link http://phpdoc.org - */ - -namespace phpDocumentor\Reflection\Types; - -use phpDocumentor\Reflection\Types\Resolver; -use phpDocumentor\Reflection\DocBlock\Context; - -/** - * Collection - */ -class Collection extends \ArrayObject -{ - /** @var string Definition of the OR operator for types */ - const OPERATOR_OR = '|'; - - /** - * Current invoking location. - * - * This is used to prepend to type with a relative location. - * May also be 'default' or 'global', in which case they are ignored. - * - * @var Context - */ - protected $context = null; - /** - * @var Resolver - */ - private $resolver; - - /** - * Registers the namespace and aliases; uses that to add and expand the given types. - * - * @param string[] $types Array containing a list of types to add to this container. - * @param Context $context - * @param Resolver $resolver - */ - public function __construct(array $types = array(), Context $context = null, Resolver $resolver = null) - { - $this->context = $context ?: new Context(''); - $this->resolver = $resolver ?: new Resolver(); - - foreach ($types as $type) { - $this->add($type); - } - } - - /** - * Returns the current invoking location. - * - * @return Context - */ - public function getContext() - { - return $this->context; - } - - /** - * Adds a new type to the collection and expands it if it contains a - * relative namespace. - * - * If a class in the type contains a relative namespace than this collection - * will try to expand that into a FQCN. - * - * @param string $type A 'Type' as defined in the phpDocumentor - * documentation. - * - * @throws \InvalidArgumentException if a non-string argument is passed. - * - * @see http://phpdoc.org/docs/latest/for-users/types.html for the - * definition of a type. - * - * @return void - */ - public function add($type) - { - if (!is_string($type)) { - throw new \InvalidArgumentException( - 'A type should be represented by a string, received: ' - . var_export($type, true) - ); - } - - // separate the type by the OR operator - $type_parts = explode(self::OPERATOR_OR, $type); - foreach ($type_parts as $part) { - $expanded_type = $this->resolver->resolve($part, $this->context); - if ($expanded_type) { - $this[] = $expanded_type; - } - } - } - - /** - * Returns a string representation of the collection. - * - * @return string The resolved types across the collection, separated with - * {@link self::OPERATOR_OR}. - */ - public function __toString() - { - return implode(self::OPERATOR_OR, $this->getArrayCopy()); - } -} diff --git a/src/Types/Compound.php b/src/Types/Compound.php index 5421ede..ac099c8 100644 --- a/src/Types/Compound.php +++ b/src/Types/Compound.php @@ -10,29 +10,44 @@ * @link http://phpdoc.org */ - namespace phpDocumentor\Reflection\Types; -use phpDocumentor\Reflection\Fqsen; use phpDocumentor\Reflection\Type; +/** + * Value Object representing a Compound Type. + * + * A Compound Type is not so much a special keyword or object reference but is a series of Types that are separated + * using an OR operator (`|`). This combination of types signifies that whatever is associated with this compound type + * may contain a value with any of the given types. + */ final class Compound implements Type { /** @var Type[] */ private $types = []; /** - * @param Type[]|Fqsen[] $types + * Initializes a compound type (i.e. `string|int`) and tests if the provided types all implement the Type interface. + * + * @param Type[] $types */ public function __construct($types) { + foreach ($types as $type) { + if (!$type instanceof Type) { + throw new \InvalidArgumentException('A compound type can only have other types as elements'); + } + } + $this->types = $types; } /** + * Returns the type at the given index. + * * @param integer $index * - * @return null|Type|Fqsen + * @return Type|null */ public function get($index) { @@ -44,6 +59,8 @@ final class Compound implements Type } /** + * Tests if this compound type has a type with the given index. + * * @param integer $index * * @return bool @@ -53,6 +70,11 @@ final class Compound implements Type return isset($this->types[$index]); } + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + * + * @return string + */ public function __toString() { return implode('|', $this->types); diff --git a/src/Types/Float.php b/src/Types/Float.php index a51dcaa..bc32978 100644 --- a/src/Types/Float.php +++ b/src/Types/Float.php @@ -14,8 +14,16 @@ namespace phpDocumentor\Reflection\Types; use phpDocumentor\Reflection\Type; +/** + * Value Object representing a Float. + */ final class Float implements Type { + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + * + * @return string + */ public function __toString() { return 'float'; diff --git a/src/Types/Integer.php b/src/Types/Integer.php index 99c8f96..be4555e 100644 --- a/src/Types/Integer.php +++ b/src/Types/Integer.php @@ -16,6 +16,11 @@ use phpDocumentor\Reflection\Type; final class Integer implements Type { + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + * + * @return string + */ public function __toString() { return 'int'; diff --git a/src/Types/Mixed.php b/src/Types/Mixed.php index 904315b..79695f4 100644 --- a/src/Types/Mixed.php +++ b/src/Types/Mixed.php @@ -14,8 +14,16 @@ namespace phpDocumentor\Reflection\Types; use phpDocumentor\Reflection\Type; +/** + * Value Object representing an unknown, or mixed, type. + */ final class Mixed implements Type { + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + * + * @return string + */ public function __toString() { return 'mixed'; diff --git a/src/Types/Null_.php b/src/Types/Null_.php index 6a78cb8..203b422 100644 --- a/src/Types/Null_.php +++ b/src/Types/Null_.php @@ -14,8 +14,16 @@ namespace phpDocumentor\Reflection\Types; use phpDocumentor\Reflection\Type; +/** + * Value Object representing a null value or type. + */ final class Null_ implements Type { + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + * + * @return string + */ public function __toString() { return 'null'; diff --git a/src/Types/Object_.php b/src/Types/Object_.php index 77cecb0..b337c71 100644 --- a/src/Types/Object_.php +++ b/src/Types/Object_.php @@ -12,12 +12,59 @@ namespace phpDocumentor\Reflection\Types; +use phpDocumentor\Reflection\Fqsen; use phpDocumentor\Reflection\Type; +/** + * Value Object representing an object. + * + * An object can be either typed or untyped. When an object is typed it means that it has an identifier, the FQSEN, + * pointing to an element in PHP. Object types that are untyped do not refer to a specific class but represent objects + * in general. + */ final class Object_ implements Type { + /** @var Fqsen|null */ + private $fqsen; + + /** + * Initializes this object with an optional FQSEN, if not provided this object is considered 'untyped'. + * + * @param Fqsen $fqsen + */ + public function __construct(Fqsen $fqsen = null) + { + if (strpos((string)$fqsen, '::') !== false || strpos((string)$fqsen, '()') !== false) { + throw new \InvalidArgumentException( + 'Object types can only refer to a class, interface or trait but a method, function, constant or ' + . 'property was received: ' . (string)$fqsen + ); + } + + $this->fqsen = $fqsen; + } + + /** + * Returns the FQSEN associated with this object. + * + * @return Fqsen|null + */ + public function getFqsen() + { + return $this->fqsen; + } + + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + * + * @return string + */ public function __toString() { + if ($this->fqsen) { + return (string)$this->fqsen; + } + return 'object'; } } diff --git a/src/Types/Resolver.php b/src/Types/Resolver.php index 451bbb0..b0ed9ea 100644 --- a/src/Types/Resolver.php +++ b/src/Types/Resolver.php @@ -65,7 +65,7 @@ class Resolver * @uses Context::getNamespaceAliases() to check whether the first part of the relative type name should not be * replaced with another namespace. * - * @return Fqsen|Type|null + * @return Type|null */ public function resolve($type, Context $context) { @@ -101,6 +101,32 @@ class Resolver // @codeCoverageIgnoreEnd } + /** + * Adds a keyword to the list of Keywords and associates it with a specific Value Object. + * + * @param string $keyword + * @param string $typeClassName + * + * @return void + */ + public function addKeyword($keyword, $typeClassName) + { + if (!class_exists($typeClassName)) { + throw new \InvalidArgumentException( + 'The Value Object that needs to be created with a keyword "' . $keyword . '" must be an existing class' + . ' but we could not find the class ' . $typeClassName + ); + } + + if (!in_array(Type::class, class_implements($typeClassName))) { + throw new \InvalidArgumentException( + 'The class "' . $typeClassName . '" must implement the interface "phpDocumentor\Reflection\Type"' + ); + } + + $this->keywords[$keyword] = $typeClassName; + } + /** * Detects whether the given type represents an array. * @@ -193,11 +219,11 @@ class Resolver * * @param string $type * - * @return Fqsen + * @return Object_ */ private function resolveFqsen($type) { - return new Fqsen($type); + return new Object_(new Fqsen($type)); } /** @@ -207,7 +233,7 @@ class Resolver * @param string $type * @param Context $context * - * @return Fqsen + * @return Object_ */ private function resolvePartialStructuralElementName($type, Context $context) { @@ -222,12 +248,12 @@ class Resolver $namespace .= self::OPERATOR_NAMESPACE; } - return new Fqsen(self::OPERATOR_NAMESPACE . $namespace . $type); + return new Object_(new Fqsen(self::OPERATOR_NAMESPACE . $namespace . $type)); } $typeParts[0] = $namespaceAliases[$typeParts[0]]; - return new Fqsen(self::OPERATOR_NAMESPACE . implode(self::OPERATOR_NAMESPACE, $typeParts)); + return new Object_(new Fqsen(self::OPERATOR_NAMESPACE . implode(self::OPERATOR_NAMESPACE, $typeParts))); } /** diff --git a/src/Types/Resource.php b/src/Types/Resource.php index 2a8a201..2c2526b 100644 --- a/src/Types/Resource.php +++ b/src/Types/Resource.php @@ -14,8 +14,16 @@ namespace phpDocumentor\Reflection\Types; use phpDocumentor\Reflection\Type; +/** + * Value Object representing the 'resource' Type. + */ final class Resource implements Type { + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + * + * @return string + */ public function __toString() { return 'resource'; diff --git a/src/Types/Scalar.php b/src/Types/Scalar.php index 5dadde8..1e2a660 100644 --- a/src/Types/Scalar.php +++ b/src/Types/Scalar.php @@ -14,8 +14,16 @@ namespace phpDocumentor\Reflection\Types; use phpDocumentor\Reflection\Type; -class Scalar implements Type +/** + * Value Object representing the 'scalar' pseudo-type, which is either a string, integer, float or boolean. + */ +final class Scalar implements Type { + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + * + * @return string + */ public function __toString() { return 'scalar'; diff --git a/src/Types/Self_.php b/src/Types/Self_.php index 933b4b8..1ba3fc5 100644 --- a/src/Types/Self_.php +++ b/src/Types/Self_.php @@ -14,8 +14,18 @@ namespace phpDocumentor\Reflection\Types; use phpDocumentor\Reflection\Type; +/** + * Value Object representing the 'self' type. + * + * Self, as a Type, represents the class in which the associated element was defined. + */ final class Self_ implements Type { + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + * + * @return string + */ public function __toString() { return 'self'; diff --git a/src/Types/Static_.php b/src/Types/Static_.php index 4922fd3..9eb6729 100644 --- a/src/Types/Static_.php +++ b/src/Types/Static_.php @@ -14,8 +14,23 @@ namespace phpDocumentor\Reflection\Types; use phpDocumentor\Reflection\Type; +/** + * Value Object representing the 'static' type. + * + * Self, as a Type, represents the class in which the associated element was called. This differs from self as self does + * not take inheritance into account but static means that the return type is always that of the class of the called + * element. + * + * See the documentation on late static binding in the PHP Documentation for more information on the difference between + * static and self. + */ final class Static_ implements Type { + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + * + * @return string + */ public function __toString() { return 'static'; diff --git a/src/Types/String.php b/src/Types/String.php index 8e4e5a2..ad2c842 100644 --- a/src/Types/String.php +++ b/src/Types/String.php @@ -14,8 +14,16 @@ namespace phpDocumentor\Reflection\Types; use phpDocumentor\Reflection\Type; +/** + * Value Object representing the type 'string'. + */ final class String implements Type { + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + * + * @return string + */ public function __toString() { return 'string'; diff --git a/src/Types/This.php b/src/Types/This.php index d0aec28..c098a93 100644 --- a/src/Types/This.php +++ b/src/Types/This.php @@ -14,8 +14,19 @@ namespace phpDocumentor\Reflection\Types; use phpDocumentor\Reflection\Type; +/** + * Value Object representing the '$this' pseudo-type. + * + * $this, as a Type, represents the instance of the class associated with the element as it was called. $this is + * commonly used when documenting fluent interfaces since it represents that the same object is returned. + */ final class This implements Type { + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + * + * @return string + */ public function __toString() { return '$this'; diff --git a/src/Types/Void.php b/src/Types/Void.php index f323349..6a6156b 100644 --- a/src/Types/Void.php +++ b/src/Types/Void.php @@ -14,8 +14,19 @@ namespace phpDocumentor\Reflection\Types; use phpDocumentor\Reflection\Type; +/** + * Value Object representing the pseudo-type 'void'. + * + * Void is generally only used when working with return types as it signifies that the method intentionally does not + * return any value. + */ final class Void implements Type { + /** + * Returns a rendered output of the Type as it would be used in a DocBlock. + * + * @return string + */ public function __toString() { return 'void'; diff --git a/src/phpDocumentor/Reflection/DocBlock/Type/Collection.php b/src/phpDocumentor/Reflection/DocBlock/Type/Collection.php deleted file mode 100644 index 90ead3f..0000000 --- a/src/phpDocumentor/Reflection/DocBlock/Type/Collection.php +++ /dev/null @@ -1,221 +0,0 @@ - - * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) - * @license http://www.opensource.org/licenses/mit-license.php MIT - * @link http://phpdoc.org - */ - -namespace phpDocumentor\Reflection\DocBlock\Type; - -use phpDocumentor\Reflection\DocBlock\Context; - -/** - * Collection - * - * @author Mike van Riel - * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) - * @license http://www.opensource.org/licenses/mit-license.php MIT - * @link http://phpdoc.org - */ -class Collection extends \ArrayObject -{ - /** @var string Definition of the OR operator for types */ - const OPERATOR_OR = '|'; - - /** @var string Definition of the ARRAY operator for types */ - const OPERATOR_ARRAY = '[]'; - - /** @var string Definition of the NAMESPACE operator in PHP */ - const OPERATOR_NAMESPACE = '\\'; - - /** @var string[] List of recognized keywords */ - protected static $keywords = array( - 'string', 'int', 'integer', 'bool', 'boolean', 'float', 'double', - 'object', 'mixed', 'array', 'resource', 'void', 'null', 'scalar', - 'callback', 'callable', 'false', 'true', 'self', '$this', 'static' - ); - - /** - * Current invoking location. - * - * This is used to prepend to type with a relative location. - * May also be 'default' or 'global', in which case they are ignored. - * - * @var Context - */ - protected $context = null; - - /** - * Registers the namespace and aliases; uses that to add and expand the - * given types. - * - * @param string[] $types Array containing a list of types to add to this - * container. - * @param Context $location The current invoking location. - */ - public function __construct( - array $types = array(), - Context $context = null - ) { - $this->context = null === $context ? new Context() : $context; - - foreach ($types as $type) { - $this->add($type); - } - } - - /** - * Returns the current invoking location. - * - * @return Context - */ - public function getContext() - { - return $this->context; - } - - /** - * Adds a new type to the collection and expands it if it contains a - * relative namespace. - * - * If a class in the type contains a relative namespace than this collection - * will try to expand that into a FQCN. - * - * @param string $type A 'Type' as defined in the phpDocumentor - * documentation. - * - * @throws \InvalidArgumentException if a non-string argument is passed. - * - * @see http://phpdoc.org/docs/latest/for-users/types.html for the - * definition of a type. - * - * @return void - */ - public function add($type) - { - if (!is_string($type)) { - throw new \InvalidArgumentException( - 'A type should be represented by a string, received: ' - .var_export($type, true) - ); - } - - // separate the type by the OR operator - $type_parts = explode(self::OPERATOR_OR, $type); - foreach ($type_parts as $part) { - $expanded_type = $this->expand($part); - if ($expanded_type) { - $this[] = $expanded_type; - } - } - } - - /** - * Returns a string representation of the collection. - * - * @return string The resolved types across the collection, separated with - * {@link self::OPERATOR_OR}. - */ - public function __toString() - { - return implode(self::OPERATOR_OR, $this->getArrayCopy()); - } - - /** - * Analyzes the given type and returns the FQCN variant. - * - * When a type is provided this method checks whether it is not a keyword or - * Fully Qualified Class Name. If so it will use the given namespace and - * aliases to expand the type to a FQCN representation. - * - * This method only works as expected if the namespace and aliases are set; - * no dynamic reflection is being performed here. - * - * @param string $type The relative or absolute type. - * - * @uses getNamespace to determine with what to prefix the type name. - * @uses getNamespaceAliases to check whether the first part of the relative - * type name should not be replaced with another namespace. - * - * @return string - */ - protected function expand($type) - { - $type = trim($type); - if (!$type) { - return ''; - } - - if ($this->isTypeAnArray($type)) { - return $this->expand(substr($type, 0, -2)) . self::OPERATOR_ARRAY; - } - - if ($this->isRelativeType($type) && !$this->isTypeAKeyword($type)) { - $type_parts = explode(self::OPERATOR_NAMESPACE, $type, 2); - - $namespace_aliases = $this->context->getNamespaceAliases(); - // if the first segment is not an alias; prepend namespace name and - // return - if (!isset($namespace_aliases[$type_parts[0]])) { - $namespace = $this->context->getNamespace(); - if ('' !== $namespace) { - $namespace .= self::OPERATOR_NAMESPACE; - } - return self::OPERATOR_NAMESPACE . $namespace . $type; - } - - $type_parts[0] = $namespace_aliases[$type_parts[0]]; - $type = implode(self::OPERATOR_NAMESPACE, $type_parts); - } - - return $type; - } - - /** - * Detects whether the given type represents an array. - * - * @param string $type A relative or absolute type as defined in the - * phpDocumentor documentation. - * - * @return bool - */ - protected function isTypeAnArray($type) - { - return substr($type, -2) === self::OPERATOR_ARRAY; - } - - /** - * Detects whether the given type represents a PHPDoc keyword. - * - * @param string $type A relative or absolute type as defined in the - * phpDocumentor documentation. - * - * @return bool - */ - protected function isTypeAKeyword($type) - { - return in_array(strtolower($type), static::$keywords, true); - } - - /** - * Detects whether the given type represents a relative or absolute path. - * - * This method will detect keywords as being absolute; even though they are - * not preceeded by a namespace separator. - * - * @param string $type A relative or absolute type as defined in the - * phpDocumentor documentation. - * - * @return bool - */ - protected function isRelativeType($type) - { - return ($type[0] !== self::OPERATOR_NAMESPACE) - || $this->isTypeAKeyword($type); - } -} diff --git a/tests/phpDocumentor/Reflection/Types/ResolverTest.php b/tests/phpDocumentor/Reflection/Types/ResolverTest.php index 2c07b9e..6e90643 100644 --- a/tests/phpDocumentor/Reflection/Types/ResolverTest.php +++ b/tests/phpDocumentor/Reflection/Types/ResolverTest.php @@ -12,7 +12,9 @@ namespace phpDocumentor\Reflection\Types; +use Mockery as m; use phpDocumentor\Reflection\DocBlock\Context; +use phpDocumentor\Reflection\Type; /** * @coversDefaultClass phpDocumentor\Reflection\Types\Resolver @@ -26,8 +28,9 @@ class ResolverTest extends \PHPUnit_Framework_TestCase * @covers ::resolve * @covers :: * - * @uses phpDocumentor\Reflection\DocBlock\Context - * @uses phpDocumentor\Reflection\Types\Array_ + * @uses phpDocumentor\Reflection\DocBlock\Context + * @uses phpDocumentor\Reflection\Types\Array_ + * @uses phpDocumentor\Reflection\Types\Object_ * * @dataProvider provideKeywords */ @@ -46,8 +49,9 @@ class ResolverTest extends \PHPUnit_Framework_TestCase * @covers ::resolve * @covers :: * - * @uses phpDocumentor\Reflection\DocBlock\Context - * @uses phpDocumentor\Reflection\Fqsen + * @uses phpDocumentor\Reflection\DocBlock\Context + * @uses phpDocumentor\Reflection\Types\Object_ + * @uses phpDocumentor\Reflection\Fqsen * * @dataProvider provideFqsen */ @@ -55,9 +59,11 @@ class ResolverTest extends \PHPUnit_Framework_TestCase { $fixture = new Resolver(); + /** @var Object_ $resolvedType */ $resolvedType = $fixture->resolve($fqsen, new Context('')); - $this->assertInstanceOf('phpDocumentor\Reflection\Fqsen', $resolvedType); + $this->assertInstanceOf('phpDocumentor\Reflection\Types\Object_', $resolvedType); + $this->assertInstanceOf('phpDocumentor\Reflection\Fqsen', $resolvedType->getFqsen()); $this->assertSame($fqsen, (string)$resolvedType); } @@ -67,15 +73,18 @@ class ResolverTest extends \PHPUnit_Framework_TestCase * @covers :: * * @uses phpDocumentor\Reflection\DocBlock\Context + * @uses phpDocumentor\Reflection\Types\Object_ * @uses phpDocumentor\Reflection\Fqsen */ public function testResolvingRelativeQSENsBasedOnNamespace() { $fixture = new Resolver(); + /** @var Object_ $resolvedType */ $resolvedType = $fixture->resolve('DocBlock', new Context('phpDocumentor\Reflection')); - $this->assertInstanceOf('phpDocumentor\Reflection\Fqsen', $resolvedType); + $this->assertInstanceOf('phpDocumentor\Reflection\Types\Object_', $resolvedType); + $this->assertInstanceOf('phpDocumentor\Reflection\Fqsen', $resolvedType->getFqsen()); $this->assertSame('\phpDocumentor\Reflection\DocBlock', (string)$resolvedType); } @@ -84,18 +93,21 @@ class ResolverTest extends \PHPUnit_Framework_TestCase * @covers :: * * @uses phpDocumentor\Reflection\DocBlock\Context + * @uses phpDocumentor\Reflection\Types\Object_ * @uses phpDocumentor\Reflection\Fqsen */ public function testResolvingRelativeQSENsBasedOnNamespaceAlias() { $fixture = new Resolver(); + /** @var Object_ $resolvedType */ $resolvedType = $fixture->resolve( 'm\MockInterface', new Context('phpDocumentor\Reflection', ['m' => '\Mockery']) ); - $this->assertInstanceOf('phpDocumentor\Reflection\Fqsen', $resolvedType); + $this->assertInstanceOf('phpDocumentor\Reflection\Types\Object_', $resolvedType); + $this->assertInstanceOf('phpDocumentor\Reflection\Fqsen', $resolvedType->getFqsen()); $this->assertSame('\Mockery\MockInterface', (string)$resolvedType); } @@ -156,6 +168,7 @@ class ResolverTest extends \PHPUnit_Framework_TestCase * @uses phpDocumentor\Reflection\DocBlock\Context * @uses phpDocumentor\Reflection\Types\Compound * @uses phpDocumentor\Reflection\Types\String + * @uses phpDocumentor\Reflection\Types\Object_ * @uses phpDocumentor\Reflection\Fqsen */ public function testResolvingCompoundTypes() @@ -167,8 +180,98 @@ class ResolverTest extends \PHPUnit_Framework_TestCase $this->assertInstanceOf('phpDocumentor\Reflection\Types\Compound', $resolvedType); $this->assertSame('string|\phpDocumentor\Reflection\DocBlock', (string)$resolvedType); - $this->assertInstanceOf('phpDocumentor\Reflection\Types\String', $resolvedType->get(0)); - $this->assertInstanceOf('phpDocumentor\Reflection\Fqsen', $resolvedType->get(1)); + + /** @var String $secondType */ + $firstType = $resolvedType->get(0); + + /** @var Object_ $secondType */ + $secondType = $resolvedType->get(1); + + $this->assertInstanceOf('phpDocumentor\Reflection\Types\String', $firstType); + $this->assertInstanceOf('phpDocumentor\Reflection\Types\Object_', $secondType); + $this->assertInstanceOf('phpDocumentor\Reflection\Fqsen', $secondType->getFqsen()); + } + + /** + * This test asserts that the parameter order is correct. + * + * When you pass two arrays separated by the compound operator (i.e. 'integer[]|string[]') then we always split the + * expression in its compound parts and then we parse the types with the array operators. If we were to switch the + * order around then 'integer[]|string[]' would read as an array of string or integer array; which is something + * other than what we intend. + * + * @covers ::resolve + * @covers :: + * + * @uses phpDocumentor\Reflection\DocBlock\Context + * @uses phpDocumentor\Reflection\Types\Compound + * @uses phpDocumentor\Reflection\Types\Array_ + * @uses phpDocumentor\Reflection\Types\Integer + * @uses phpDocumentor\Reflection\Types\String + */ + public function testResolvingCompoundTypesWithTwoArrays() + { + $fixture = new Resolver(); + + /** @var Compound $resolvedType */ + $resolvedType = $fixture->resolve('integer[]|string[]', new Context('')); + + $this->assertInstanceOf('phpDocumentor\Reflection\Types\Compound', $resolvedType); + $this->assertSame('int[]|string[]', (string)$resolvedType); + + /** @var Array_ $firstType */ + $firstType = $resolvedType->get(0); + + /** @var Array_ $secondType */ + $secondType = $resolvedType->get(1); + + $this->assertInstanceOf('phpDocumentor\Reflection\Types\Array_', $firstType); + $this->assertInstanceOf('phpDocumentor\Reflection\Types\Integer', $firstType->getValueType()); + $this->assertInstanceOf('phpDocumentor\Reflection\Types\Array_', $secondType); + $this->assertInstanceOf('phpDocumentor\Reflection\Types\String', $secondType->getValueType()); + } + + /** + * @covers ::addKeyword + * @uses phpDocumentor\Reflection\Types\Resolver::resolve + * @uses phpDocumentor\Reflection\Types\Resolver:: + * @uses phpDocumentor\Reflection\DocBlock\Context + */ + public function testAddingAKeyword() + { + // Assign + $typeMock = m::mock(Type::class); + + // Act + $fixture = new Resolver(); + $fixture->addKeyword('mock', get_class($typeMock)); + + // Assert + $result = $fixture->resolve('mock', new Context('')); + $this->assertInstanceOf(get_class($typeMock), $result); + $this->assertNotSame($typeMock, $result); + } + + /** + * @covers ::addKeyword + * @uses phpDocumentor\Reflection\DocBlock\Context + * @expectedException \InvalidArgumentException + */ + public function testAddingAKeywordFailsIfTypeClassDoesNotExist() + { + $fixture = new Resolver(); + $fixture->addKeyword('mock', 'IDoNotExist'); + } + + /** + * @covers ::addKeyword + * @uses phpDocumentor\Reflection\DocBlock\Context + * @expectedException \InvalidArgumentException + */ + public function testAddingAKeywordFailsIfTypeClassDoesNotImplementTypeInterface() + { + $fixture = new Resolver(); + $fixture->addKeyword('mock', 'stdClass'); } /** @@ -195,6 +298,11 @@ class ResolverTest extends \PHPUnit_Framework_TestCase $fixture->resolve(['a'], new Context('')); } + /** + * Returns a list of keywords and expected classes that are created from them. + * + * @return string[][] + */ public function provideKeywords() { return [ @@ -220,6 +328,11 @@ class ResolverTest extends \PHPUnit_Framework_TestCase ]; } + /** + * Provides a list of FQSENs to test the resolution patterns with. + * + * @return string[][] + */ public function provideFqsen() { return [ From 53a142d6721e76e719fd721f7c161385163e1b4a Mon Sep 17 00:00:00 2001 From: Mike van Riel Date: Mon, 8 Jun 2015 08:02:07 +0200 Subject: [PATCH 3/4] Make resolver final --- src/Types/Resolver.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Types/Resolver.php b/src/Types/Resolver.php index b0ed9ea..024c65e 100644 --- a/src/Types/Resolver.php +++ b/src/Types/Resolver.php @@ -13,10 +13,9 @@ namespace phpDocumentor\Reflection\Types; use phpDocumentor\Reflection\DocBlock\Context; -use phpDocumentor\Reflection\Fqsen; use phpDocumentor\Reflection\Type; -class Resolver +final class Resolver { /** @var string Definition of the ARRAY operator for types */ const OPERATOR_ARRAY = '[]'; From 58d09836c10fd3db5d226e2fa79e32ecdacf85f4 Mon Sep 17 00:00:00 2001 From: Mike van Riel Date: Wed, 10 Jun 2015 14:53:41 +0200 Subject: [PATCH 4/4] Add interface for the DocBlockFactory --- src/DocBlockFactoryInterface.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/DocBlockFactoryInterface.php diff --git a/src/DocBlockFactoryInterface.php b/src/DocBlockFactoryInterface.php new file mode 100644 index 0000000..47c2b83 --- /dev/null +++ b/src/DocBlockFactoryInterface.php @@ -0,0 +1,23 @@ +