1<?php 2 3/* 4 * This file is part of Composer. 5 * 6 * (c) Nils Adermann <naderman@naderman.de> 7 * Jordi Boggiano <j.boggiano@seld.be> 8 * 9 * For the full copyright and license information, please view the LICENSE 10 * file that was distributed with this source code. 11 */ 12 13namespace Composer\Autoload; 14 15/** 16 * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. 17 * 18 * $loader = new \Composer\Autoload\ClassLoader(); 19 * 20 * // register classes with namespaces 21 * $loader->add('Symfony\Component', __DIR__.'/component'); 22 * $loader->add('Symfony', __DIR__.'/framework'); 23 * 24 * // activate the autoloader 25 * $loader->register(); 26 * 27 * // to enable searching the include path (eg. for PEAR packages) 28 * $loader->setUseIncludePath(true); 29 * 30 * In this example, if you try to use a class in the Symfony\Component 31 * namespace or one of its children (Symfony\Component\Console for instance), 32 * the autoloader will first look for the class under the component/ 33 * directory, and it will then fallback to the framework/ directory if not 34 * found before giving up. 35 * 36 * This class is loosely based on the Symfony UniversalClassLoader. 37 * 38 * @author Fabien Potencier <fabien@symfony.com> 39 * @author Jordi Boggiano <j.boggiano@seld.be> 40 * @see https://www.php-fig.org/psr/psr-0/ 41 * @see https://www.php-fig.org/psr/psr-4/ 42 */ 43class ClassLoader 44{ 45 /** @var ?string */ 46 private $vendorDir; 47 48 // PSR-4 49 /** 50 * @var array[] 51 * @psalm-var array<string, array<string, int>> 52 */ 53 private $prefixLengthsPsr4 = array(); 54 /** 55 * @var array[] 56 * @psalm-var array<string, array<int, string>> 57 */ 58 private $prefixDirsPsr4 = array(); 59 /** 60 * @var array[] 61 * @psalm-var array<string, string> 62 */ 63 private $fallbackDirsPsr4 = array(); 64 65 // PSR-0 66 /** 67 * @var array[] 68 * @psalm-var array<string, array<string, string[]>> 69 */ 70 private $prefixesPsr0 = array(); 71 /** 72 * @var array[] 73 * @psalm-var array<string, string> 74 */ 75 private $fallbackDirsPsr0 = array(); 76 77 /** @var bool */ 78 private $useIncludePath = false; 79 80 /** 81 * @var string[] 82 * @psalm-var array<string, string> 83 */ 84 private $classMap = array(); 85 86 /** @var bool */ 87 private $classMapAuthoritative = false; 88 89 /** 90 * @var bool[] 91 * @psalm-var array<string, bool> 92 */ 93 private $missingClasses = array(); 94 95 /** @var ?string */ 96 private $apcuPrefix; 97 98 /** 99 * @var self[] 100 */ 101 private static $registeredLoaders = array(); 102 103 /** 104 * @param ?string $vendorDir 105 */ 106 public function __construct($vendorDir = null) 107 { 108 $this->vendorDir = $vendorDir; 109 } 110 111 /** 112 * @return string[] 113 */ 114 public function getPrefixes() 115 { 116 if (!empty($this->prefixesPsr0)) { 117 return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); 118 } 119 120 return array(); 121 } 122 123 /** 124 * @return array[] 125 * @psalm-return array<string, array<int, string>> 126 */ 127 public function getPrefixesPsr4() 128 { 129 return $this->prefixDirsPsr4; 130 } 131 132 /** 133 * @return array[] 134 * @psalm-return array<string, string> 135 */ 136 public function getFallbackDirs() 137 { 138 return $this->fallbackDirsPsr0; 139 } 140 141 /** 142 * @return array[] 143 * @psalm-return array<string, string> 144 */ 145 public function getFallbackDirsPsr4() 146 { 147 return $this->fallbackDirsPsr4; 148 } 149 150 /** 151 * @return string[] Array of classname => path 152 * @psalm-var array<string, string> 153 */ 154 public function getClassMap() 155 { 156 return $this->classMap; 157 } 158 159 /** 160 * @param string[] $classMap Class to filename map 161 * @psalm-param array<string, string> $classMap 162 * 163 * @return void 164 */ 165 public function addClassMap(array $classMap) 166 { 167 if ($this->classMap) { 168 $this->classMap = array_merge($this->classMap, $classMap); 169 } else { 170 $this->classMap = $classMap; 171 } 172 } 173 174 /** 175 * Registers a set of PSR-0 directories for a given prefix, either 176 * appending or prepending to the ones previously set for this prefix. 177 * 178 * @param string $prefix The prefix 179 * @param string[]|string $paths The PSR-0 root directories 180 * @param bool $prepend Whether to prepend the directories 181 * 182 * @return void 183 */ 184 public function add($prefix, $paths, $prepend = false) 185 { 186 if (!$prefix) { 187 if ($prepend) { 188 $this->fallbackDirsPsr0 = array_merge( 189 (array) $paths, 190 $this->fallbackDirsPsr0 191 ); 192 } else { 193 $this->fallbackDirsPsr0 = array_merge( 194 $this->fallbackDirsPsr0, 195 (array) $paths 196 ); 197 } 198 199 return; 200 } 201 202 $first = $prefix[0]; 203 if (!isset($this->prefixesPsr0[$first][$prefix])) { 204 $this->prefixesPsr0[$first][$prefix] = (array) $paths; 205 206 return; 207 } 208 if ($prepend) { 209 $this->prefixesPsr0[$first][$prefix] = array_merge( 210 (array) $paths, 211 $this->prefixesPsr0[$first][$prefix] 212 ); 213 } else { 214 $this->prefixesPsr0[$first][$prefix] = array_merge( 215 $this->prefixesPsr0[$first][$prefix], 216 (array) $paths 217 ); 218 } 219 } 220 221 /** 222 * Registers a set of PSR-4 directories for a given namespace, either 223 * appending or prepending to the ones previously set for this namespace. 224 * 225 * @param string $prefix The prefix/namespace, with trailing '\\' 226 * @param string[]|string $paths The PSR-4 base directories 227 * @param bool $prepend Whether to prepend the directories 228 * 229 * @throws \InvalidArgumentException 230 * 231 * @return void 232 */ 233 public function addPsr4($prefix, $paths, $prepend = false) 234 { 235 if (!$prefix) { 236 // Register directories for the root namespace. 237 if ($prepend) { 238 $this->fallbackDirsPsr4 = array_merge( 239 (array) $paths, 240 $this->fallbackDirsPsr4 241 ); 242 } else { 243 $this->fallbackDirsPsr4 = array_merge( 244 $this->fallbackDirsPsr4, 245 (array) $paths 246 ); 247 } 248 } elseif (!isset($this->prefixDirsPsr4[$prefix])) { 249 // Register directories for a new namespace. 250 $length = strlen($prefix); 251 if ('\\' !== $prefix[$length - 1]) { 252 throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 253 } 254 $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 255 $this->prefixDirsPsr4[$prefix] = (array) $paths; 256 } elseif ($prepend) { 257 // Prepend directories for an already registered namespace. 258 $this->prefixDirsPsr4[$prefix] = array_merge( 259 (array) $paths, 260 $this->prefixDirsPsr4[$prefix] 261 ); 262 } else { 263 // Append directories for an already registered namespace. 264 $this->prefixDirsPsr4[$prefix] = array_merge( 265 $this->prefixDirsPsr4[$prefix], 266 (array) $paths 267 ); 268 } 269 } 270 271 /** 272 * Registers a set of PSR-0 directories for a given prefix, 273 * replacing any others previously set for this prefix. 274 * 275 * @param string $prefix The prefix 276 * @param string[]|string $paths The PSR-0 base directories 277 * 278 * @return void 279 */ 280 public function set($prefix, $paths) 281 { 282 if (!$prefix) { 283 $this->fallbackDirsPsr0 = (array) $paths; 284 } else { 285 $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; 286 } 287 } 288 289 /** 290 * Registers a set of PSR-4 directories for a given namespace, 291 * replacing any others previously set for this namespace. 292 * 293 * @param string $prefix The prefix/namespace, with trailing '\\' 294 * @param string[]|string $paths The PSR-4 base directories 295 * 296 * @throws \InvalidArgumentException 297 * 298 * @return void 299 */ 300 public function setPsr4($prefix, $paths) 301 { 302 if (!$prefix) { 303 $this->fallbackDirsPsr4 = (array) $paths; 304 } else { 305 $length = strlen($prefix); 306 if ('\\' !== $prefix[$length - 1]) { 307 throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 308 } 309 $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 310 $this->prefixDirsPsr4[$prefix] = (array) $paths; 311 } 312 } 313 314 /** 315 * Turns on searching the include path for class files. 316 * 317 * @param bool $useIncludePath 318 * 319 * @return void 320 */ 321 public function setUseIncludePath($useIncludePath) 322 { 323 $this->useIncludePath = $useIncludePath; 324 } 325 326 /** 327 * Can be used to check if the autoloader uses the include path to check 328 * for classes. 329 * 330 * @return bool 331 */ 332 public function getUseIncludePath() 333 { 334 return $this->useIncludePath; 335 } 336 337 /** 338 * Turns off searching the prefix and fallback directories for classes 339 * that have not been registered with the class map. 340 * 341 * @param bool $classMapAuthoritative 342 * 343 * @return void 344 */ 345 public function setClassMapAuthoritative($classMapAuthoritative) 346 { 347 $this->classMapAuthoritative = $classMapAuthoritative; 348 } 349 350 /** 351 * Should class lookup fail if not found in the current class map? 352 * 353 * @return bool 354 */ 355 public function isClassMapAuthoritative() 356 { 357 return $this->classMapAuthoritative; 358 } 359 360 /** 361 * APCu prefix to use to cache found/not-found classes, if the extension is enabled. 362 * 363 * @param string|null $apcuPrefix 364 * 365 * @return void 366 */ 367 public function setApcuPrefix($apcuPrefix) 368 { 369 $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; 370 } 371 372 /** 373 * The APCu prefix in use, or null if APCu caching is not enabled. 374 * 375 * @return string|null 376 */ 377 public function getApcuPrefix() 378 { 379 return $this->apcuPrefix; 380 } 381 382 /** 383 * Registers this instance as an autoloader. 384 * 385 * @param bool $prepend Whether to prepend the autoloader or not 386 * 387 * @return void 388 */ 389 public function register($prepend = false) 390 { 391 spl_autoload_register(array($this, 'loadClass'), true, $prepend); 392 393 if (null === $this->vendorDir) { 394 return; 395 } 396 397 if ($prepend) { 398 self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; 399 } else { 400 unset(self::$registeredLoaders[$this->vendorDir]); 401 self::$registeredLoaders[$this->vendorDir] = $this; 402 } 403 } 404 405 /** 406 * Unregisters this instance as an autoloader. 407 * 408 * @return void 409 */ 410 public function unregister() 411 { 412 spl_autoload_unregister(array($this, 'loadClass')); 413 414 if (null !== $this->vendorDir) { 415 unset(self::$registeredLoaders[$this->vendorDir]); 416 } 417 } 418 419 /** 420 * Loads the given class or interface. 421 * 422 * @param string $class The name of the class 423 * @return true|null True if loaded, null otherwise 424 */ 425 public function loadClass($class) 426 { 427 if ($file = $this->findFile($class)) { 428 includeFile($file); 429 430 return true; 431 } 432 433 return null; 434 } 435 436 /** 437 * Finds the path to the file where the class is defined. 438 * 439 * @param string $class The name of the class 440 * 441 * @return string|false The path if found, false otherwise 442 */ 443 public function findFile($class) 444 { 445 // class map lookup 446 if (isset($this->classMap[$class])) { 447 return $this->classMap[$class]; 448 } 449 if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { 450 return false; 451 } 452 if (null !== $this->apcuPrefix) { 453 $file = apcu_fetch($this->apcuPrefix.$class, $hit); 454 if ($hit) { 455 return $file; 456 } 457 } 458 459 $file = $this->findFileWithExtension($class, '.php'); 460 461 // Search for Hack files if we are running on HHVM 462 if (false === $file && defined('HHVM_VERSION')) { 463 $file = $this->findFileWithExtension($class, '.hh'); 464 } 465 466 if (null !== $this->apcuPrefix) { 467 apcu_add($this->apcuPrefix.$class, $file); 468 } 469 470 if (false === $file) { 471 // Remember that this class does not exist. 472 $this->missingClasses[$class] = true; 473 } 474 475 return $file; 476 } 477 478 /** 479 * Returns the currently registered loaders indexed by their corresponding vendor directories. 480 * 481 * @return self[] 482 */ 483 public static function getRegisteredLoaders() 484 { 485 return self::$registeredLoaders; 486 } 487 488 /** 489 * @param string $class 490 * @param string $ext 491 * @return string|false 492 */ 493 private function findFileWithExtension($class, $ext) 494 { 495 // PSR-4 lookup 496 $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; 497 498 $first = $class[0]; 499 if (isset($this->prefixLengthsPsr4[$first])) { 500 $subPath = $class; 501 while (false !== $lastPos = strrpos($subPath, '\\')) { 502 $subPath = substr($subPath, 0, $lastPos); 503 $search = $subPath . '\\'; 504 if (isset($this->prefixDirsPsr4[$search])) { 505 $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); 506 foreach ($this->prefixDirsPsr4[$search] as $dir) { 507 if (file_exists($file = $dir . $pathEnd)) { 508 return $file; 509 } 510 } 511 } 512 } 513 } 514 515 // PSR-4 fallback dirs 516 foreach ($this->fallbackDirsPsr4 as $dir) { 517 if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { 518 return $file; 519 } 520 } 521 522 // PSR-0 lookup 523 if (false !== $pos = strrpos($class, '\\')) { 524 // namespaced class name 525 $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) 526 . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); 527 } else { 528 // PEAR-like class name 529 $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; 530 } 531 532 if (isset($this->prefixesPsr0[$first])) { 533 foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { 534 if (0 === strpos($class, $prefix)) { 535 foreach ($dirs as $dir) { 536 if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 537 return $file; 538 } 539 } 540 } 541 } 542 } 543 544 // PSR-0 fallback dirs 545 foreach ($this->fallbackDirsPsr0 as $dir) { 546 if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 547 return $file; 548 } 549 } 550 551 // PSR-0 include paths. 552 if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { 553 return $file; 554 } 555 556 return false; 557 } 558} 559 560/** 561 * Scope isolated include. 562 * 563 * Prevents access to $this/self from included files. 564 * 565 * @param string $file 566 * @return void 567 * @private 568 */ 569function includeFile($file) 570{ 571 include $file; 572} 573