1<?php 2 3declare(strict_types=1); 4 5/** 6 * This file is part of phpDocumentor. 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 * 11 * @link http://phpdoc.org 12 */ 13 14namespace phpDocumentor\Reflection\Types; 15 16use ArrayIterator; 17use InvalidArgumentException; 18use ReflectionClass; 19use ReflectionClassConstant; 20use ReflectionMethod; 21use ReflectionParameter; 22use ReflectionProperty; 23use Reflector; 24use RuntimeException; 25use UnexpectedValueException; 26use function define; 27use function defined; 28use function file_exists; 29use function file_get_contents; 30use function get_class; 31use function in_array; 32use function is_string; 33use function strrpos; 34use function substr; 35use function token_get_all; 36use function trim; 37use const T_AS; 38use const T_CLASS; 39use const T_CURLY_OPEN; 40use const T_DOLLAR_OPEN_CURLY_BRACES; 41use const T_NAMESPACE; 42use const T_NS_SEPARATOR; 43use const T_STRING; 44use const T_USE; 45 46if (!defined('T_NAME_QUALIFIED')) { 47 define('T_NAME_QUALIFIED', 'T_NAME_QUALIFIED'); 48} 49 50if (!defined('T_NAME_FULLY_QUALIFIED')) { 51 define('T_NAME_FULLY_QUALIFIED', 'T_NAME_FULLY_QUALIFIED'); 52} 53 54/** 55 * Convenience class to create a Context for DocBlocks when not using the Reflection Component of phpDocumentor. 56 * 57 * For a DocBlock to be able to resolve types that use partial namespace names or rely on namespace imports we need to 58 * provide a bit of context so that the DocBlock can read that and based on it decide how to resolve the types to 59 * Fully Qualified names. 60 * 61 * @see Context for more information. 62 */ 63final class ContextFactory 64{ 65 /** The literal used at the end of a use statement. */ 66 private const T_LITERAL_END_OF_USE = ';'; 67 68 /** The literal used between sets of use statements */ 69 private const T_LITERAL_USE_SEPARATOR = ','; 70 71 /** 72 * Build a Context given a Class Reflection. 73 * 74 * @see Context for more information on Contexts. 75 */ 76 public function createFromReflector(Reflector $reflector) : Context 77 { 78 if ($reflector instanceof ReflectionClass) { 79 //phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable 80 /** @var ReflectionClass<object> $reflector */ 81 82 return $this->createFromReflectionClass($reflector); 83 } 84 85 if ($reflector instanceof ReflectionParameter) { 86 return $this->createFromReflectionParameter($reflector); 87 } 88 89 if ($reflector instanceof ReflectionMethod) { 90 return $this->createFromReflectionMethod($reflector); 91 } 92 93 if ($reflector instanceof ReflectionProperty) { 94 return $this->createFromReflectionProperty($reflector); 95 } 96 97 if ($reflector instanceof ReflectionClassConstant) { 98 return $this->createFromReflectionClassConstant($reflector); 99 } 100 101 throw new UnexpectedValueException('Unhandled \Reflector instance given: ' . get_class($reflector)); 102 } 103 104 private function createFromReflectionParameter(ReflectionParameter $parameter) : Context 105 { 106 $class = $parameter->getDeclaringClass(); 107 if (!$class) { 108 throw new InvalidArgumentException('Unable to get class of ' . $parameter->getName()); 109 } 110 111 //phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable 112 /** @var ReflectionClass<object> $class */ 113 114 return $this->createFromReflectionClass($class); 115 } 116 117 private function createFromReflectionMethod(ReflectionMethod $method) : Context 118 { 119 //phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable 120 /** @var ReflectionClass<object> $class */ 121 $class = $method->getDeclaringClass(); 122 123 return $this->createFromReflectionClass($class); 124 } 125 126 private function createFromReflectionProperty(ReflectionProperty $property) : Context 127 { 128 //phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable 129 /** @var ReflectionClass<object> $class */ 130 $class = $property->getDeclaringClass(); 131 132 return $this->createFromReflectionClass($class); 133 } 134 135 private function createFromReflectionClassConstant(ReflectionClassConstant $constant) : Context 136 { 137 //phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable 138 /** @var ReflectionClass<object> $class */ 139 $class = $constant->getDeclaringClass(); 140 141 return $this->createFromReflectionClass($class); 142 } 143 144 /** 145 * @param ReflectionClass<object> $class 146 */ 147 private function createFromReflectionClass(ReflectionClass $class) : Context 148 { 149 $fileName = $class->getFileName(); 150 $namespace = $class->getNamespaceName(); 151 152 if (is_string($fileName) && file_exists($fileName)) { 153 $contents = file_get_contents($fileName); 154 if ($contents === false) { 155 throw new RuntimeException('Unable to read file "' . $fileName . '"'); 156 } 157 158 return $this->createForNamespace($namespace, $contents); 159 } 160 161 return new Context($namespace, []); 162 } 163 164 /** 165 * Build a Context for a namespace in the provided file contents. 166 * 167 * @see Context for more information on Contexts. 168 * 169 * @param string $namespace It does not matter if a `\` precedes the namespace name, 170 * this method first normalizes. 171 * @param string $fileContents The file's contents to retrieve the aliases from with the given namespace. 172 */ 173 public function createForNamespace(string $namespace, string $fileContents) : Context 174 { 175 $namespace = trim($namespace, '\\'); 176 $useStatements = []; 177 $currentNamespace = ''; 178 $tokens = new ArrayIterator(token_get_all($fileContents)); 179 180 while ($tokens->valid()) { 181 $currentToken = $tokens->current(); 182 switch ($currentToken[0]) { 183 case T_NAMESPACE: 184 $currentNamespace = $this->parseNamespace($tokens); 185 break; 186 case T_CLASS: 187 // Fast-forward the iterator through the class so that any 188 // T_USE tokens found within are skipped - these are not 189 // valid namespace use statements so should be ignored. 190 $braceLevel = 0; 191 $firstBraceFound = false; 192 while ($tokens->valid() && ($braceLevel > 0 || !$firstBraceFound)) { 193 $currentToken = $tokens->current(); 194 if ($currentToken === '{' 195 || in_array($currentToken[0], [T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES], true)) { 196 if (!$firstBraceFound) { 197 $firstBraceFound = true; 198 } 199 200 ++$braceLevel; 201 } 202 203 if ($currentToken === '}') { 204 --$braceLevel; 205 } 206 207 $tokens->next(); 208 } 209 210 break; 211 case T_USE: 212 if ($currentNamespace === $namespace) { 213 $useStatements += $this->parseUseStatement($tokens); 214 } 215 216 break; 217 } 218 219 $tokens->next(); 220 } 221 222 return new Context($namespace, $useStatements); 223 } 224 225 /** 226 * Deduce the name from tokens when we are at the T_NAMESPACE token. 227 * 228 * @param ArrayIterator<int, string|array{0:int,1:string,2:int}> $tokens 229 */ 230 private function parseNamespace(ArrayIterator $tokens) : string 231 { 232 // skip to the first string or namespace separator 233 $this->skipToNextStringOrNamespaceSeparator($tokens); 234 235 $name = ''; 236 $acceptedTokens = [T_STRING, T_NS_SEPARATOR, T_NAME_QUALIFIED]; 237 while ($tokens->valid() && in_array($tokens->current()[0], $acceptedTokens, true)) { 238 $name .= $tokens->current()[1]; 239 $tokens->next(); 240 } 241 242 return $name; 243 } 244 245 /** 246 * Deduce the names of all imports when we are at the T_USE token. 247 * 248 * @param ArrayIterator<int, string|array{0:int,1:string,2:int}> $tokens 249 * 250 * @return string[] 251 * 252 * @psalm-return array<string, string> 253 */ 254 private function parseUseStatement(ArrayIterator $tokens) : array 255 { 256 $uses = []; 257 258 while ($tokens->valid()) { 259 $this->skipToNextStringOrNamespaceSeparator($tokens); 260 261 $uses += $this->extractUseStatements($tokens); 262 $currentToken = $tokens->current(); 263 if ($currentToken[0] === self::T_LITERAL_END_OF_USE) { 264 return $uses; 265 } 266 } 267 268 return $uses; 269 } 270 271 /** 272 * Fast-forwards the iterator as longs as we don't encounter a T_STRING or T_NS_SEPARATOR token. 273 * 274 * @param ArrayIterator<int, string|array{0:int,1:string,2:int}> $tokens 275 */ 276 private function skipToNextStringOrNamespaceSeparator(ArrayIterator $tokens) : void 277 { 278 while ($tokens->valid()) { 279 $currentToken = $tokens->current(); 280 if (in_array($currentToken[0], [T_STRING, T_NS_SEPARATOR], true)) { 281 break; 282 } 283 284 if ($currentToken[0] === T_NAME_QUALIFIED) { 285 break; 286 } 287 288 if (defined('T_NAME_FULLY_QUALIFIED') && $currentToken[0] === T_NAME_FULLY_QUALIFIED) { 289 break; 290 } 291 292 $tokens->next(); 293 } 294 } 295 296 /** 297 * Deduce the namespace name and alias of an import when we are at the T_USE token or have not reached the end of 298 * a USE statement yet. This will return a key/value array of the alias => namespace. 299 * 300 * @param ArrayIterator<int, string|array{0:int,1:string,2:int}> $tokens 301 * 302 * @return string[] 303 * 304 * @psalm-suppress TypeDoesNotContainType 305 * 306 * @psalm-return array<string, string> 307 */ 308 private function extractUseStatements(ArrayIterator $tokens) : array 309 { 310 $extractedUseStatements = []; 311 $groupedNs = ''; 312 $currentNs = ''; 313 $currentAlias = ''; 314 $state = 'start'; 315 316 while ($tokens->valid()) { 317 $currentToken = $tokens->current(); 318 $tokenId = is_string($currentToken) ? $currentToken : $currentToken[0]; 319 $tokenValue = is_string($currentToken) ? null : $currentToken[1]; 320 switch ($state) { 321 case 'start': 322 switch ($tokenId) { 323 case T_STRING: 324 case T_NS_SEPARATOR: 325 $currentNs .= (string) $tokenValue; 326 $currentAlias = $tokenValue; 327 break; 328 case T_NAME_QUALIFIED: 329 case T_NAME_FULLY_QUALIFIED: 330 $currentNs .= (string) $tokenValue; 331 $currentAlias = substr( 332 (string) $tokenValue, 333 (int) (strrpos((string) $tokenValue, '\\')) + 1 334 ); 335 break; 336 case T_CURLY_OPEN: 337 case '{': 338 $state = 'grouped'; 339 $groupedNs = $currentNs; 340 break; 341 case T_AS: 342 $state = 'start-alias'; 343 break; 344 case self::T_LITERAL_USE_SEPARATOR: 345 case self::T_LITERAL_END_OF_USE: 346 $state = 'end'; 347 break; 348 default: 349 break; 350 } 351 352 break; 353 case 'start-alias': 354 switch ($tokenId) { 355 case T_STRING: 356 $currentAlias = $tokenValue; 357 break; 358 case self::T_LITERAL_USE_SEPARATOR: 359 case self::T_LITERAL_END_OF_USE: 360 $state = 'end'; 361 break; 362 default: 363 break; 364 } 365 366 break; 367 case 'grouped': 368 switch ($tokenId) { 369 case T_STRING: 370 case T_NS_SEPARATOR: 371 $currentNs .= (string) $tokenValue; 372 $currentAlias = $tokenValue; 373 break; 374 case T_AS: 375 $state = 'grouped-alias'; 376 break; 377 case self::T_LITERAL_USE_SEPARATOR: 378 $state = 'grouped'; 379 $extractedUseStatements[(string) $currentAlias] = $currentNs; 380 $currentNs = $groupedNs; 381 $currentAlias = ''; 382 break; 383 case self::T_LITERAL_END_OF_USE: 384 $state = 'end'; 385 break; 386 default: 387 break; 388 } 389 390 break; 391 case 'grouped-alias': 392 switch ($tokenId) { 393 case T_STRING: 394 $currentAlias = $tokenValue; 395 break; 396 case self::T_LITERAL_USE_SEPARATOR: 397 $state = 'grouped'; 398 $extractedUseStatements[(string) $currentAlias] = $currentNs; 399 $currentNs = $groupedNs; 400 $currentAlias = ''; 401 break; 402 case self::T_LITERAL_END_OF_USE: 403 $state = 'end'; 404 break; 405 default: 406 break; 407 } 408 } 409 410 if ($state === 'end') { 411 break; 412 } 413 414 $tokens->next(); 415 } 416 417 if ($groupedNs !== $currentNs) { 418 $extractedUseStatements[(string) $currentAlias] = $currentNs; 419 } 420 421 return $extractedUseStatements; 422 } 423} 424