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<mike@phpdoc.org> 9 * @license http://www.opensource.org/licenses/mit-license.php MIT 10 * @link http://phpdoc.org 11 */ 12 13namespace phpDocumentor\Reflection; 14 15use phpDocumentor\Reflection\Types\Array_; 16use phpDocumentor\Reflection\Types\Compound; 17use phpDocumentor\Reflection\Types\Context; 18use phpDocumentor\Reflection\Types\Iterable_; 19use phpDocumentor\Reflection\Types\Nullable; 20use phpDocumentor\Reflection\Types\Object_; 21 22final 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 string[] List of recognized keywords and unto which Value Object they map */ 31 private $keywords = array( 32 'string' => Types\String_::class, 33 'int' => Types\Integer::class, 34 'integer' => Types\Integer::class, 35 'bool' => Types\Boolean::class, 36 'boolean' => Types\Boolean::class, 37 'float' => Types\Float_::class, 38 'double' => Types\Float_::class, 39 'object' => Object_::class, 40 'mixed' => Types\Mixed_::class, 41 'array' => Array_::class, 42 'resource' => Types\Resource_::class, 43 'void' => Types\Void_::class, 44 'null' => Types\Null_::class, 45 'scalar' => Types\Scalar::class, 46 'callback' => Types\Callable_::class, 47 'callable' => Types\Callable_::class, 48 'false' => Types\Boolean::class, 49 'true' => Types\Boolean::class, 50 'self' => Types\Self_::class, 51 '$this' => Types\This::class, 52 'static' => Types\Static_::class, 53 'parent' => Types\Parent_::class, 54 'iterable' => Iterable_::class, 55 ); 56 57 /** @var FqsenResolver */ 58 private $fqsenResolver; 59 60 /** 61 * Initializes this TypeResolver with the means to create and resolve Fqsen objects. 62 * 63 * @param FqsenResolver $fqsenResolver 64 */ 65 public function __construct(FqsenResolver $fqsenResolver = null) 66 { 67 $this->fqsenResolver = $fqsenResolver ?: new FqsenResolver(); 68 } 69 70 /** 71 * Analyzes the given type and returns the FQCN variant. 72 * 73 * When a type is provided this method checks whether it is not a keyword or 74 * Fully Qualified Class Name. If so it will use the given namespace and 75 * aliases to expand the type to a FQCN representation. 76 * 77 * This method only works as expected if the namespace and aliases are set; 78 * no dynamic reflection is being performed here. 79 * 80 * @param string $type The relative or absolute type. 81 * @param Context $context 82 * 83 * @uses Context::getNamespace() to determine with what to prefix the type name. 84 * @uses Context::getNamespaceAliases() to check whether the first part of the relative type name should not be 85 * replaced with another namespace. 86 * 87 * @return Type|null 88 */ 89 public function resolve($type, Context $context = null) 90 { 91 if (!is_string($type)) { 92 throw new \InvalidArgumentException( 93 'Attempted to resolve type but it appeared not to be a string, received: ' . var_export($type, true) 94 ); 95 } 96 97 $type = trim($type); 98 if (!$type) { 99 throw new \InvalidArgumentException('Attempted to resolve "' . $type . '" but it appears to be empty'); 100 } 101 102 if ($context === null) { 103 $context = new Context(''); 104 } 105 106 switch (true) { 107 case $this->isNullableType($type): 108 return $this->resolveNullableType($type, $context); 109 case $this->isKeyword($type): 110 return $this->resolveKeyword($type); 111 case ($this->isCompoundType($type)): 112 return $this->resolveCompoundType($type, $context); 113 case $this->isTypedArray($type): 114 return $this->resolveTypedArray($type, $context); 115 case $this->isFqsen($type): 116 return $this->resolveTypedObject($type); 117 case $this->isPartialStructuralElementName($type): 118 return $this->resolveTypedObject($type, $context); 119 // @codeCoverageIgnoreStart 120 default: 121 // I haven't got the foggiest how the logic would come here but added this as a defense. 122 throw new \RuntimeException( 123 'Unable to resolve type "' . $type . '", there is no known method to resolve it' 124 ); 125 } 126 // @codeCoverageIgnoreEnd 127 } 128 129 /** 130 * Adds a keyword to the list of Keywords and associates it with a specific Value Object. 131 * 132 * @param string $keyword 133 * @param string $typeClassName 134 * 135 * @return void 136 */ 137 public function addKeyword($keyword, $typeClassName) 138 { 139 if (!class_exists($typeClassName)) { 140 throw new \InvalidArgumentException( 141 'The Value Object that needs to be created with a keyword "' . $keyword . '" must be an existing class' 142 . ' but we could not find the class ' . $typeClassName 143 ); 144 } 145 146 if (!in_array(Type::class, class_implements($typeClassName))) { 147 throw new \InvalidArgumentException( 148 'The class "' . $typeClassName . '" must implement the interface "phpDocumentor\Reflection\Type"' 149 ); 150 } 151 152 $this->keywords[$keyword] = $typeClassName; 153 } 154 155 /** 156 * Detects whether the given type represents an array. 157 * 158 * @param string $type A relative or absolute type as defined in the phpDocumentor documentation. 159 * 160 * @return bool 161 */ 162 private function isTypedArray($type) 163 { 164 return substr($type, -2) === self::OPERATOR_ARRAY; 165 } 166 167 /** 168 * Detects whether the given type represents a PHPDoc keyword. 169 * 170 * @param string $type A relative or absolute type as defined in the phpDocumentor documentation. 171 * 172 * @return bool 173 */ 174 private function isKeyword($type) 175 { 176 return in_array(strtolower($type), array_keys($this->keywords), true); 177 } 178 179 /** 180 * Detects whether the given type represents a relative structural element name. 181 * 182 * @param string $type A relative or absolute type as defined in the phpDocumentor documentation. 183 * 184 * @return bool 185 */ 186 private function isPartialStructuralElementName($type) 187 { 188 return ($type[0] !== self::OPERATOR_NAMESPACE) && !$this->isKeyword($type); 189 } 190 191 /** 192 * Tests whether the given type is a Fully Qualified Structural Element Name. 193 * 194 * @param string $type 195 * 196 * @return bool 197 */ 198 private function isFqsen($type) 199 { 200 return strpos($type, self::OPERATOR_NAMESPACE) === 0; 201 } 202 203 /** 204 * Tests whether the given type is a compound type (i.e. `string|int`). 205 * 206 * @param string $type 207 * 208 * @return bool 209 */ 210 private function isCompoundType($type) 211 { 212 return strpos($type, '|') !== false; 213 } 214 215 /** 216 * Test whether the given type is a nullable type (i.e. `?string`) 217 * 218 * @param string $type 219 * 220 * @return bool 221 */ 222 private function isNullableType($type) 223 { 224 return $type[0] === '?'; 225 } 226 227 /** 228 * Resolves the given typed array string (i.e. `string[]`) into an Array object with the right types set. 229 * 230 * @param string $type 231 * @param Context $context 232 * 233 * @return Array_ 234 */ 235 private function resolveTypedArray($type, Context $context) 236 { 237 return new Array_($this->resolve(substr($type, 0, -2), $context)); 238 } 239 240 /** 241 * Resolves the given keyword (such as `string`) into a Type object representing that keyword. 242 * 243 * @param string $type 244 * 245 * @return Type 246 */ 247 private function resolveKeyword($type) 248 { 249 $className = $this->keywords[strtolower($type)]; 250 251 return new $className(); 252 } 253 254 /** 255 * Resolves the given FQSEN string into an FQSEN object. 256 * 257 * @param string $type 258 * @param Context|null $context 259 * 260 * @return Object_ 261 */ 262 private function resolveTypedObject($type, Context $context = null) 263 { 264 return new Object_($this->fqsenResolver->resolve($type, $context)); 265 } 266 267 /** 268 * Resolves a compound type (i.e. `string|int`) into the appropriate Type objects or FQSEN. 269 * 270 * @param string $type 271 * @param Context $context 272 * 273 * @return Compound 274 */ 275 private function resolveCompoundType($type, Context $context) 276 { 277 $types = []; 278 279 foreach (explode('|', $type) as $part) { 280 $types[] = $this->resolve($part, $context); 281 } 282 283 return new Compound($types); 284 } 285 286 /** 287 * Resolve nullable types (i.e. `?string`) into a Nullable type wrapper 288 * 289 * @param string $type 290 * @param Context $context 291 * 292 * @return Nullable 293 */ 294 private function resolveNullableType($type, Context $context) 295 { 296 return new Nullable($this->resolve(ltrim($type, '?'), $context)); 297 } 298} 299