You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

445 lines
13 KiB

4 years ago
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\Autoload;
  12. /**
  13. * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
  14. *
  15. * $loader = new \Composer\Autoload\ClassLoader();
  16. *
  17. * // register classes with namespaces
  18. * $loader->add('Symfony\Component', __DIR__.'/component');
  19. * $loader->add('Symfony', __DIR__.'/framework');
  20. *
  21. * // activate the autoloader
  22. * $loader->register();
  23. *
  24. * // to enable searching the include path (eg. for PEAR packages)
  25. * $loader->setUseIncludePath(true);
  26. *
  27. * In this example, if you try to use a class in the Symfony\Component
  28. * namespace or one of its children (Symfony\Component\Console for instance),
  29. * the autoloader will first look for the class under the component/
  30. * directory, and it will then fallback to the framework/ directory if not
  31. * found before giving up.
  32. *
  33. * This class is loosely based on the Symfony UniversalClassLoader.
  34. *
  35. * @author Fabien Potencier <fabien@symfony.com>
  36. * @author Jordi Boggiano <j.boggiano@seld.be>
  37. * @see http://www.php-fig.org/psr/psr-0/
  38. * @see http://www.php-fig.org/psr/psr-4/
  39. */
  40. class ClassLoader
  41. {
  42. // PSR-4
  43. private $prefixLengthsPsr4 = array();
  44. private $prefixDirsPsr4 = array();
  45. private $fallbackDirsPsr4 = array();
  46. // PSR-0
  47. private $prefixesPsr0 = array();
  48. private $fallbackDirsPsr0 = array();
  49. private $useIncludePath = false;
  50. private $classMap = array();
  51. private $classMapAuthoritative = false;
  52. private $missingClasses = array();
  53. private $apcuPrefix;
  54. public function getPrefixes()
  55. {
  56. if (!empty($this->prefixesPsr0)) {
  57. return call_user_func_array('array_merge', $this->prefixesPsr0);
  58. }
  59. return array();
  60. }
  61. public function getPrefixesPsr4()
  62. {
  63. return $this->prefixDirsPsr4;
  64. }
  65. public function getFallbackDirs()
  66. {
  67. return $this->fallbackDirsPsr0;
  68. }
  69. public function getFallbackDirsPsr4()
  70. {
  71. return $this->fallbackDirsPsr4;
  72. }
  73. public function getClassMap()
  74. {
  75. return $this->classMap;
  76. }
  77. /**
  78. * @param array $classMap Class to filename map
  79. */
  80. public function addClassMap(array $classMap)
  81. {
  82. if ($this->classMap) {
  83. $this->classMap = array_merge($this->classMap, $classMap);
  84. } else {
  85. $this->classMap = $classMap;
  86. }
  87. }
  88. /**
  89. * Registers a set of PSR-0 directories for a given prefix, either
  90. * appending or prepending to the ones previously set for this prefix.
  91. *
  92. * @param string $prefix The prefix
  93. * @param array|string $paths The PSR-0 root directories
  94. * @param bool $prepend Whether to prepend the directories
  95. */
  96. public function add($prefix, $paths, $prepend = false)
  97. {
  98. if (!$prefix) {
  99. if ($prepend) {
  100. $this->fallbackDirsPsr0 = array_merge(
  101. (array) $paths,
  102. $this->fallbackDirsPsr0
  103. );
  104. } else {
  105. $this->fallbackDirsPsr0 = array_merge(
  106. $this->fallbackDirsPsr0,
  107. (array) $paths
  108. );
  109. }
  110. return;
  111. }
  112. $first = $prefix[0];
  113. if (!isset($this->prefixesPsr0[$first][$prefix])) {
  114. $this->prefixesPsr0[$first][$prefix] = (array) $paths;
  115. return;
  116. }
  117. if ($prepend) {
  118. $this->prefixesPsr0[$first][$prefix] = array_merge(
  119. (array) $paths,
  120. $this->prefixesPsr0[$first][$prefix]
  121. );
  122. } else {
  123. $this->prefixesPsr0[$first][$prefix] = array_merge(
  124. $this->prefixesPsr0[$first][$prefix],
  125. (array) $paths
  126. );
  127. }
  128. }
  129. /**
  130. * Registers a set of PSR-4 directories for a given namespace, either
  131. * appending or prepending to the ones previously set for this namespace.
  132. *
  133. * @param string $prefix The prefix/namespace, with trailing '\\'
  134. * @param array|string $paths The PSR-4 base directories
  135. * @param bool $prepend Whether to prepend the directories
  136. *
  137. * @throws \InvalidArgumentException
  138. */
  139. public function addPsr4($prefix, $paths, $prepend = false)
  140. {
  141. if (!$prefix) {
  142. // Register directories for the root namespace.
  143. if ($prepend) {
  144. $this->fallbackDirsPsr4 = array_merge(
  145. (array) $paths,
  146. $this->fallbackDirsPsr4
  147. );
  148. } else {
  149. $this->fallbackDirsPsr4 = array_merge(
  150. $this->fallbackDirsPsr4,
  151. (array) $paths
  152. );
  153. }
  154. } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
  155. // Register directories for a new namespace.
  156. $length = strlen($prefix);
  157. if ('\\' !== $prefix[$length - 1]) {
  158. throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
  159. }
  160. $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
  161. $this->prefixDirsPsr4[$prefix] = (array) $paths;
  162. } elseif ($prepend) {
  163. // Prepend directories for an already registered namespace.
  164. $this->prefixDirsPsr4[$prefix] = array_merge(
  165. (array) $paths,
  166. $this->prefixDirsPsr4[$prefix]
  167. );
  168. } else {
  169. // Append directories for an already registered namespace.
  170. $this->prefixDirsPsr4[$prefix] = array_merge(
  171. $this->prefixDirsPsr4[$prefix],
  172. (array) $paths
  173. );
  174. }
  175. }
  176. /**
  177. * Registers a set of PSR-0 directories for a given prefix,
  178. * replacing any others previously set for this prefix.
  179. *
  180. * @param string $prefix The prefix
  181. * @param array|string $paths The PSR-0 base directories
  182. */
  183. public function set($prefix, $paths)
  184. {
  185. if (!$prefix) {
  186. $this->fallbackDirsPsr0 = (array) $paths;
  187. } else {
  188. $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
  189. }
  190. }
  191. /**
  192. * Registers a set of PSR-4 directories for a given namespace,
  193. * replacing any others previously set for this namespace.
  194. *
  195. * @param string $prefix The prefix/namespace, with trailing '\\'
  196. * @param array|string $paths The PSR-4 base directories
  197. *
  198. * @throws \InvalidArgumentException
  199. */
  200. public function setPsr4($prefix, $paths)
  201. {
  202. if (!$prefix) {
  203. $this->fallbackDirsPsr4 = (array) $paths;
  204. } else {
  205. $length = strlen($prefix);
  206. if ('\\' !== $prefix[$length - 1]) {
  207. throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
  208. }
  209. $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
  210. $this->prefixDirsPsr4[$prefix] = (array) $paths;
  211. }
  212. }
  213. /**
  214. * Turns on searching the include path for class files.
  215. *
  216. * @param bool $useIncludePath
  217. */
  218. public function setUseIncludePath($useIncludePath)
  219. {
  220. $this->useIncludePath = $useIncludePath;
  221. }
  222. /**
  223. * Can be used to check if the autoloader uses the include path to check
  224. * for classes.
  225. *
  226. * @return bool
  227. */
  228. public function getUseIncludePath()
  229. {
  230. return $this->useIncludePath;
  231. }
  232. /**
  233. * Turns off searching the prefix and fallback directories for classes
  234. * that have not been registered with the class map.
  235. *
  236. * @param bool $classMapAuthoritative
  237. */
  238. public function setClassMapAuthoritative($classMapAuthoritative)
  239. {
  240. $this->classMapAuthoritative = $classMapAuthoritative;
  241. }
  242. /**
  243. * Should class lookup fail if not found in the current class map?
  244. *
  245. * @return bool
  246. */
  247. public function isClassMapAuthoritative()
  248. {
  249. return $this->classMapAuthoritative;
  250. }
  251. /**
  252. * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
  253. *
  254. * @param string|null $apcuPrefix
  255. */
  256. public function setApcuPrefix($apcuPrefix)
  257. {
  258. $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null;
  259. }
  260. /**
  261. * The APCu prefix in use, or null if APCu caching is not enabled.
  262. *
  263. * @return string|null
  264. */
  265. public function getApcuPrefix()
  266. {
  267. return $this->apcuPrefix;
  268. }
  269. /**
  270. * Registers this instance as an autoloader.
  271. *
  272. * @param bool $prepend Whether to prepend the autoloader or not
  273. */
  274. public function register($prepend = false)
  275. {
  276. spl_autoload_register(array($this, 'loadClass'), true, $prepend);
  277. }
  278. /**
  279. * Unregisters this instance as an autoloader.
  280. */
  281. public function unregister()
  282. {
  283. spl_autoload_unregister(array($this, 'loadClass'));
  284. }
  285. /**
  286. * Loads the given class or interface.
  287. *
  288. * @param string $class The name of the class
  289. * @return bool|null True if loaded, null otherwise
  290. */
  291. public function loadClass($class)
  292. {
  293. if ($file = $this->findFile($class)) {
  294. includeFile($file);
  295. return true;
  296. }
  297. }
  298. /**
  299. * Finds the path to the file where the class is defined.
  300. *
  301. * @param string $class The name of the class
  302. *
  303. * @return string|false The path if found, false otherwise
  304. */
  305. public function findFile($class)
  306. {
  307. // class map lookup
  308. if (isset($this->classMap[$class])) {
  309. return $this->classMap[$class];
  310. }
  311. if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
  312. return false;
  313. }
  314. if (null !== $this->apcuPrefix) {
  315. $file = apcu_fetch($this->apcuPrefix.$class, $hit);
  316. if ($hit) {
  317. return $file;
  318. }
  319. }
  320. $file = $this->findFileWithExtension($class, '.php');
  321. // Search for Hack files if we are running on HHVM
  322. if (false === $file && defined('HHVM_VERSION')) {
  323. $file = $this->findFileWithExtension($class, '.hh');
  324. }
  325. if (null !== $this->apcuPrefix) {
  326. apcu_add($this->apcuPrefix.$class, $file);
  327. }
  328. if (false === $file) {
  329. // Remember that this class does not exist.
  330. $this->missingClasses[$class] = true;
  331. }
  332. return $file;
  333. }
  334. private function findFileWithExtension($class, $ext)
  335. {
  336. // PSR-4 lookup
  337. $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
  338. $first = $class[0];
  339. if (isset($this->prefixLengthsPsr4[$first])) {
  340. $subPath = $class;
  341. while (false !== $lastPos = strrpos($subPath, '\\')) {
  342. $subPath = substr($subPath, 0, $lastPos);
  343. $search = $subPath.'\\';
  344. if (isset($this->prefixDirsPsr4[$search])) {
  345. $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
  346. foreach ($this->prefixDirsPsr4[$search] as $dir) {
  347. if (file_exists($file = $dir . $pathEnd)) {
  348. return $file;
  349. }
  350. }
  351. }
  352. }
  353. }
  354. // PSR-4 fallback dirs
  355. foreach ($this->fallbackDirsPsr4 as $dir) {
  356. if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
  357. return $file;
  358. }
  359. }
  360. // PSR-0 lookup
  361. if (false !== $pos = strrpos($class, '\\')) {
  362. // namespaced class name
  363. $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
  364. . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
  365. } else {
  366. // PEAR-like class name
  367. $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
  368. }
  369. if (isset($this->prefixesPsr0[$first])) {
  370. foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
  371. if (0 === strpos($class, $prefix)) {
  372. foreach ($dirs as $dir) {
  373. if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
  374. return $file;
  375. }
  376. }
  377. }
  378. }
  379. }
  380. // PSR-0 fallback dirs
  381. foreach ($this->fallbackDirsPsr0 as $dir) {
  382. if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
  383. return $file;
  384. }
  385. }
  386. // PSR-0 include paths.
  387. if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
  388. return $file;
  389. }
  390. return false;
  391. }
  392. }
  393. /**
  394. * Scope isolated include.
  395. *
  396. * Prevents access to $this/self from included files.
  397. */
  398. function includeFile($file)
  399. {
  400. include $file;
  401. }