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