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:
13: namespace 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: */
43: class 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: