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