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