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 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 */
43class 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 (file_exists($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 (file_exists($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 (file_exists($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 (file_exists($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 */
410function includeFile($file)
411{
412    include $file;
413}
414