xref: /web-bugs/src/Autoloader.php (revision f762db34)
1<?php
2
3namespace App;
4
5/**
6 * This is a PSR-4 autoloader based on the example implementation by the PHP-FIG
7 * at https://www.php-fig.org/psr/psr-4/. It includes an optional functionality
8 * of allowing multiple base directories for a single namespace prefix. A
9 * separate implementation besides the Composer's autoloader is done for cases
10 * when Composer is not available on the server environment such as production.
11 * It also provides loading non-PSR-4 compliant classes.
12 *
13 * Given a foo-bar package of classes in the file system at the following
14 * paths ...
15 *
16 *     /path/to/packages/foo-bar/
17 *         src/
18 *             Baz.php             # Foo\Bar\Baz
19 *             Qux/
20 *                 Quux.php        # Foo\Bar\Qux\Quux
21 *         tests/
22 *             BazTest.php         # Foo\Bar\BazTest
23 *             Qux/
24 *                 QuuxTest.php    # Foo\Bar\Qux\QuuxTest
25 *
26 * ... add the path to the class files for the \Foo\Bar\ namespace prefix
27 * as follows:
28 *
29 *      <?php
30 *      // Instantiate the loader to registers the SPL autoload
31 *      $loader = new App\Autoloader;
32 *
33 *      // Register the base directories for the namespace prefix
34 *      $loader->addNamespace('Foo\Bar', '/path/to/packages/foo-bar/src');
35 *      $loader->addNamespace('Foo\Bar', '/path/to/packages/foo-bar/tests');
36 *
37 * The following line would cause the autoloader to attempt to load the
38 * \Foo\Bar\Qux\Quux class from /path/to/packages/foo-bar/src/Qux/Quux.php:
39 *
40 *      <?php
41 *      new \Foo\Bar\Qux\Quux;
42 *
43 * The following line would cause the autoloader to attempt to load the
44 * \Foo\Bar\Qux\QuuxTest class from /path/to/packages/foo-bar/tests/Qux/QuuxTest.php:
45 *
46 *      <?php
47 *      new \Foo\Bar\Qux\QuuxTest;
48 */
49class Autoloader
50{
51    /**
52     * An associative array with namespace prefixes as keys and values of arrays
53     * of base directories for classes in that namespace.
54     */
55    protected $prefixes = [];
56
57    /**
58     * An associative array of classes as keys and their paths as values.
59     */
60    protected $classmap = [];
61
62    /**
63     * Class constructor that registers loader with a SPL autoloader stack.
64     */
65    public function __construct()
66    {
67        spl_autoload_register([$this, 'load']);
68    }
69
70    /**
71     * Adds a base directory for a namespace prefix.
72     *
73     * @param string $prefix The namespace prefix.
74     * @param string $baseDir A base directory for class files in the
75     * namespace.
76     * @param bool $prepend If true, prepend the base directory to the stack
77     * instead of appending it; this causes it to be searched first rather
78     * than last.
79     */
80    public function addNamespace($prefix, $baseDir, $prepend = false)
81    {
82        // normalize namespace prefix
83        $prefix = trim($prefix, '\\') . '\\';
84
85        // normalize the base directory with a trailing separator
86        $baseDir = rtrim($baseDir, '\\/') . '/';
87
88        // initialize the namespace prefix array
89        if (isset($this->prefixes[$prefix]) === false) {
90            $this->prefixes[$prefix] = [];
91        }
92
93        // retain the base directory for the namespace prefix
94        if ($prepend) {
95            array_unshift($this->prefixes[$prefix], $baseDir);
96        } else {
97            array_push($this->prefixes[$prefix], $baseDir);
98        }
99    }
100
101    /**
102     * Add a classmap. Classmap is a simplistic imitation of the Composer's
103     * classmap autoloading.
104     */
105    public function addClassmap($class, $path)
106    {
107        $this->classmap[$class] = $path;
108    }
109
110    /**
111     * Loads the class file for a given class name.
112     *
113     * @param string $class The fully-qualified class name.
114     * @return mixed The mapped file name on success, or boolean false on
115     * failure.
116     */
117    public function load($class)
118    {
119        // the current namespace prefix
120        $prefix = $class;
121
122        // Work backwards through the namespace names of the fully-qualified
123        // class name to find a mapped file name
124        while (false !== $pos = strrpos($prefix, '\\')) {
125
126            // retain the trailing namespace separator in the prefix
127            $prefix = substr($class, 0, $pos + 1);
128
129            // the rest is the relative class name
130            $relativeClass = substr($class, $pos + 1);
131
132            // try to load a mapped file for the prefix and relative class
133            $mappedFile = $this->loadMappedFile($prefix, $relativeClass);
134            if ($mappedFile) {
135                return $mappedFile;
136            }
137
138            // Remove the trailing namespace separator for the next iteration
139            // of strrpos()
140            $prefix = rtrim($prefix, '\\');
141        }
142
143        // Check if file is maybe in classmap
144        if (!empty($this->classmap[$class])) {
145            return $this->requireFile($this->classmap[$class]) ? $this->classmap[$class] : false;
146        }
147
148        // Mapped file not found
149        return false;
150    }
151
152    /**
153     * Load the mapped file for a namespace prefix and relative class.
154     *
155     * @param string $prefix The namespace prefix.
156     * @param string $relativeClass The relative class name.
157     * @return mixed Boolean false if no mapped file can be loaded, or the
158     * name of the mapped file that was loaded.
159     */
160    protected function loadMappedFile($prefix, $relativeClass)
161    {
162        // are there any base directories for this namespace prefix?
163        if (isset($this->prefixes[$prefix]) === false) {
164            return false;
165        }
166
167        // Look through base directories for this namespace prefix
168        foreach ($this->prefixes[$prefix] as $baseDir) {
169            // replace the namespace prefix with the base directory,
170            // replace namespace separators with directory separators
171            // in the relative class name, append with .php
172            $file = $baseDir
173                  . str_replace('\\', '/', $relativeClass)
174                  . '.php';
175
176            // If the mapped file exists, require it
177            if ($this->requireFile($file)) {
178                return $file;
179            }
180        }
181
182        // Mapped file not found
183        return false;
184    }
185
186    /**
187     * If a file exists, require it from the file system.
188     *
189     * @param string $file The file to require.
190     * @return bool True if the file exists, false if not.
191     */
192    protected function requireFile($file)
193    {
194        if (file_exists($file)) {
195            require_once $file;
196            return true;
197        }
198
199        return false;
200    }
201}
202