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.

411 line
12KB

  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. public function getPrefixes()
  54. {
  55. if (!empty($this->prefixesPsr0)) {
  56. return call_user_func_array('array_merge', $this->prefixesPsr0);
  57. }
  58. return array();
  59. }
  60. public function getPrefixesPsr4()
  61. {
  62. return $this->prefixDirsPsr4;
  63. }
  64. public function getFallbackDirs()
  65. {
  66. return $this->fallbackDirsPsr0;
  67. }
  68. public function getFallbackDirsPsr4()
  69. {
  70. return $this->fallbackDirsPsr4;
  71. }
  72. public function getClassMap()
  73. {
  74. return $this->classMap;
  75. }
  76. /**
  77. * @param array $classMap Class to filename map
  78. */
  79. public function addClassMap(array $classMap)
  80. {
  81. if ($this->classMap) {
  82. $this->classMap = array_merge($this->classMap, $classMap);
  83. } else {
  84. $this->classMap = $classMap;
  85. }
  86. }
  87. /**
  88. * Registers a set of PSR-0 directories for a given prefix, either
  89. * appending or prepending to the ones previously set for this prefix.
  90. *
  91. * @param string $prefix The prefix
  92. * @param array|string $paths The PSR-0 root directories
  93. * @param bool $prepend Whether to prepend the directories
  94. */
  95. public function add($prefix, $paths, $prepend = false)
  96. {
  97. if (!$prefix) {
  98. if ($prepend) {
  99. $this->fallbackDirsPsr0 = array_merge(
  100. (array) $paths,
  101. $this->fallbackDirsPsr0
  102. );
  103. } else {
  104. $this->fallbackDirsPsr0 = array_merge(
  105. $this->fallbackDirsPsr0,
  106. (array) $paths
  107. );
  108. }
  109. return;
  110. }
  111. $first = $prefix[0];
  112. if (!isset($this->prefixesPsr0[$first][$prefix])) {
  113. $this->prefixesPsr0[$first][$prefix] = (array) $paths;
  114. return;
  115. }
  116. if ($prepend) {
  117. $this->prefixesPsr0[$first][$prefix] = array_merge(
  118. (array) $paths,
  119. $this->prefixesPsr0[$first][$prefix]
  120. );
  121. } else {
  122. $this->prefixesPsr0[$first][$prefix] = array_merge(
  123. $this->prefixesPsr0[$first][$prefix],
  124. (array) $paths
  125. );
  126. }
  127. }
  128. /**
  129. * Registers a set of PSR-4 directories for a given namespace, either
  130. * appending or prepending to the ones previously set for this namespace.
  131. *
  132. * @param string $prefix The prefix/namespace, with trailing '\\'
  133. * @param array|string $paths The PSR-4 base directories
  134. * @param bool $prepend Whether to prepend the directories
  135. *
  136. * @throws \InvalidArgumentException
  137. */
  138. public function addPsr4($prefix, $paths, $prepend = false)
  139. {
  140. if (!$prefix) {
  141. // Register directories for the root namespace.
  142. if ($prepend) {
  143. $this->fallbackDirsPsr4 = array_merge(
  144. (array) $paths,
  145. $this->fallbackDirsPsr4
  146. );
  147. } else {
  148. $this->fallbackDirsPsr4 = array_merge(
  149. $this->fallbackDirsPsr4,
  150. (array) $paths
  151. );
  152. }
  153. } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
  154. // Register directories for a new namespace.
  155. $length = strlen($prefix);
  156. if ('\\' !== $prefix[$length - 1]) {
  157. throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
  158. }
  159. $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
  160. $this->prefixDirsPsr4[$prefix] = (array) $paths;
  161. } elseif ($prepend) {
  162. // Prepend directories for an already registered namespace.
  163. $this->prefixDirsPsr4[$prefix] = array_merge(
  164. (array) $paths,
  165. $this->prefixDirsPsr4[$prefix]
  166. );
  167. } else {
  168. // Append directories for an already registered namespace.
  169. $this->prefixDirsPsr4[$prefix] = array_merge(
  170. $this->prefixDirsPsr4[$prefix],
  171. (array) $paths
  172. );
  173. }
  174. }
  175. /**
  176. * Registers a set of PSR-0 directories for a given prefix,
  177. * replacing any others previously set for this prefix.
  178. *
  179. * @param string $prefix The prefix
  180. * @param array|string $paths The PSR-0 base directories
  181. */
  182. public function set($prefix, $paths)
  183. {
  184. if (!$prefix) {
  185. $this->fallbackDirsPsr0 = (array) $paths;
  186. } else {
  187. $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
  188. }
  189. }
  190. /**
  191. * Registers a set of PSR-4 directories for a given namespace,
  192. * replacing any others previously set for this namespace.
  193. *
  194. * @param string $prefix The prefix/namespace, with trailing '\\'
  195. * @param array|string $paths The PSR-4 base directories
  196. *
  197. * @throws \InvalidArgumentException
  198. */
  199. public function setPsr4($prefix, $paths)
  200. {
  201. if (!$prefix) {
  202. $this->fallbackDirsPsr4 = (array) $paths;
  203. } else {
  204. $length = strlen($prefix);
  205. if ('\\' !== $prefix[$length - 1]) {
  206. throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
  207. }
  208. $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
  209. $this->prefixDirsPsr4[$prefix] = (array) $paths;
  210. }
  211. }
  212. /**
  213. * Turns on searching the include path for class files.
  214. *
  215. * @param bool $useIncludePath
  216. */
  217. public function setUseIncludePath($useIncludePath)
  218. {
  219. $this->useIncludePath = $useIncludePath;
  220. }
  221. /**
  222. * Can be used to check if the autoloader uses the include path to check
  223. * for classes.
  224. *
  225. * @return bool
  226. */
  227. public function getUseIncludePath()
  228. {
  229. return $this->useIncludePath;
  230. }
  231. /**
  232. * Turns off searching the prefix and fallback directories for classes
  233. * that have not been registered with the class map.
  234. *
  235. * @param bool $classMapAuthoritative
  236. */
  237. public function setClassMapAuthoritative($classMapAuthoritative)
  238. {
  239. $this->classMapAuthoritative = $classMapAuthoritative;
  240. }
  241. /**
  242. * Should class lookup fail if not found in the current class map?
  243. *
  244. * @return bool
  245. */
  246. public function isClassMapAuthoritative()
  247. {
  248. return $this->classMapAuthoritative;
  249. }
  250. /**
  251. * Registers this instance as an autoloader.
  252. *
  253. * @param bool $prepend Whether to prepend the autoloader or not
  254. */
  255. public function register($prepend = false)
  256. {
  257. spl_autoload_register(array($this, 'loadClass'), true, $prepend);
  258. }
  259. /**
  260. * Unregisters this instance as an autoloader.
  261. */
  262. public function unregister()
  263. {
  264. spl_autoload_unregister(array($this, 'loadClass'));
  265. }
  266. /**
  267. * Loads the given class or interface.
  268. *
  269. * @param string $class The name of the class
  270. * @return bool|null True if loaded, null otherwise
  271. */
  272. public function loadClass($class)
  273. {
  274. if ($file = $this->findFile($class)) {
  275. includeFile($file);
  276. return true;
  277. }
  278. }
  279. /**
  280. * Finds the path to the file where the class is defined.
  281. *
  282. * @param string $class The name of the class
  283. *
  284. * @return string|false The path if found, false otherwise
  285. */
  286. public function findFile($class)
  287. {
  288. // class map lookup
  289. if (isset($this->classMap[$class])) {
  290. return $this->classMap[$class];
  291. }
  292. if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
  293. return false;
  294. }
  295. $file = $this->findFileWithExtension($class, '.php');
  296. // Search for Hack files if we are running on HHVM
  297. if (false === $file && defined('HHVM_VERSION')) {
  298. $file = $this->findFileWithExtension($class, '.hh');
  299. }
  300. if (false === $file) {
  301. // Remember that this class does not exist.
  302. $this->missingClasses[$class] = true;
  303. }
  304. return $file;
  305. }
  306. private function findFileWithExtension($class, $ext)
  307. {
  308. // PSR-4 lookup
  309. $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
  310. $first = $class[0];
  311. if (isset($this->prefixLengthsPsr4[$first])) {
  312. foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
  313. if (0 === strpos($class, $prefix)) {
  314. foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
  315. if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
  316. return $file;
  317. }
  318. }
  319. }
  320. }
  321. }
  322. // PSR-4 fallback dirs
  323. foreach ($this->fallbackDirsPsr4 as $dir) {
  324. if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
  325. return $file;
  326. }
  327. }
  328. // PSR-0 lookup
  329. if (false !== $pos = strrpos($class, '\\')) {
  330. // namespaced class name
  331. $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
  332. . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
  333. } else {
  334. // PEAR-like class name
  335. $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
  336. }
  337. if (isset($this->prefixesPsr0[$first])) {
  338. foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
  339. if (0 === strpos($class, $prefix)) {
  340. foreach ($dirs as $dir) {
  341. if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
  342. return $file;
  343. }
  344. }
  345. }
  346. }
  347. }
  348. // PSR-0 fallback dirs
  349. foreach ($this->fallbackDirsPsr0 as $dir) {
  350. if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
  351. return $file;
  352. }
  353. }
  354. // PSR-0 include paths.
  355. if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
  356. return $file;
  357. }
  358. return false;
  359. }
  360. }
  361. /**
  362. * Scope isolated include.
  363. *
  364. * Prevents access to $this/self from included files.
  365. */
  366. function includeFile($file)
  367. {
  368. include $file;
  369. }