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