├── README ├── autoloadManager.php └── test ├── 001.phpt ├── 002.phpt ├── 003.phpt ├── 004.phpt ├── 005.phpt ├── 006.phpt ├── 007.phpt ├── src ├── a.php └── b.php └── testcache.php /README: -------------------------------------------------------------------------------- 1 | _______________________ 2 | 3 | AutoLoad Manager README 4 | _______________________ 5 | 6 | 7 | AUTHORS & CONTACT 8 | ================= 9 | 10 | Al-Fallouji Bashar 11 | - bashar@alfallouji.com 12 | 13 | Charron Pierrick 14 | - pierrick@webstart.fr 15 | 16 | 17 | DOCUMENTATION & DOWNLOAD 18 | ======================== 19 | 20 | Latest version is available on github at : 21 | - http://github.com/alfallouji/PHP-Autoload-Manager/ 22 | 23 | Documentation can be found on : 24 | - http://bashar.alfallouji.com/PHP-Autoload-Manager/ 25 | 26 | 27 | LICENSE 28 | ======= 29 | 30 | This Code is released under the GNU LGPL 31 | 32 | Please do not change the header of the file(s). 33 | 34 | This library is free software; you can redistribute it and/or modify it 35 | under the terms of the GNU Lesser General Public License as published 36 | by the Free Software Foundation; either version 2 of the License, or 37 | (at your option) any later version. 38 | 39 | This library is distributed in the hope that it will be useful, but 40 | WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 41 | or FITNESS FOR A PARTICULAR PURPOSE. 42 | 43 | See the GNU Lesser General Public License for more details. 44 | 45 | 46 | DESCRIPTION 47 | =========== 48 | 49 | AutoLoad Manager is a generic autoloader that can be used with any 50 | framework or library. 51 | 52 | Using the PHP tokenizer mechanism, it will parse folder(s) and discover 53 | the different classes and interfaces defined. 54 | 55 | The big advantage of using this autoloadManager is that it will allow 56 | you to implement whatever naming rules you want and may have mutliple 57 | classes in one file (if you want to have a such feature). 58 | 59 | So basically, you don’t have anymore to implement some complex naming rules 60 | between the filename and the classname. You may organize the way you want 61 | your API (may have as many subfolders as you want, or have multiple 62 | API folders, etc.). 63 | 64 | 65 | How does it work ? 66 | ================== 67 | 68 | It will scan any given folder and find any defined PHP classes or interfaces. 69 | It will then create an hashtable that will reference what class can be 70 | found in what file. This hash table is serialized and cached in a file. 71 | 72 | Whenever, your program or script will look for a non-existing class, 73 | the autoloadManager will look on that hash table and load the file if it exists. 74 | 75 | A fallback mechanism can be used also in a development environment that will 76 | try to rescan all the folders once more (this mechanism is usefull when 77 | you are often adding new classes to your program). 78 | 79 | 80 | How can I use it ? 81 | ================== 82 | 83 | First, you will have simply to load the autoloadManager class into your script. 84 | 85 | include('api/autoloadManager.php'); 86 | 87 | 88 | Secondly, you will have to instanciate your autoloadManager and define 89 | the path where the autoloadManager will store the file containing the 90 | hash table. 91 | 92 | $autoloadManager = new AutoloadManager(); 93 | $autoloadManager->setSaveFile('./autoload.php'); 94 | 95 | Then, you have the four main features offered by this script. 96 | 97 | 1. Register the loadClass function: 98 | 99 | $autoloadManager->register(); 100 | 101 | 2. Add a folder to process: 102 | 103 | $autoloadManager->addFolder('{YOUR_FOLDER_PATH}'); 104 | 105 | 106 | For instance, if your classes are found in ‘/var/www/myProject/lib’ and 107 | ‘/var/www/myProject/includes’, then you can do something like this. 108 | 109 | $autoloadManager->addFolder('/var/www/myProject/lib'); 110 | $autoloadManager->addFolder('/var/www/myProject/includes'); 111 | $autoloadManager->register(); 112 | 113 | 3. Add or remove file extensions to scan: 114 | 115 | By default the autoloadManager will parse all .php and .inc files. You can 116 | modify this behavior by using the setFileRegex method as bellow 117 | 118 | $autoloadManager->setFileRegex('/\.php$/'); 119 | 120 | 121 | 4. Exclude a folder from the process list: 122 | 123 | $autoloadManager->excludeFolder('/var/www/myProject/includes/lib1'); 124 | -------------------------------------------------------------------------------- /autoloadManager.php: -------------------------------------------------------------------------------- 1 | register(); 47 | * Add a folder to process: $autoloader->addFolder('{YOUR_FOLDER_PATH}'); 48 | * 49 | * Read documentation for more information. 50 | */ 51 | class autoloadManager 52 | { 53 | /** 54 | * Constants used by the checkClass method 55 | * @var int 56 | */ 57 | const CLASS_NOT_FOUND = 0; 58 | const CLASS_EXISTS = 1; 59 | const CLASS_IS_NULL = 2; 60 | 61 | /** 62 | * Constants used for the scan options 63 | * @var int 64 | */ 65 | const SCAN_NEVER = 0; // 0b000 66 | const SCAN_ONCE = 1; // 0b001 67 | const SCAN_ALWAYS = 3; // 0b011 68 | const SCAN_CACHE = 4; // 0b100 69 | 70 | /** 71 | * Folders that should be parsed 72 | * @var array 73 | */ 74 | private $_folders = array(); 75 | 76 | /** 77 | * Excluded folders 78 | * @var array 79 | */ 80 | private $_excludedFolders = array(); 81 | 82 | /** 83 | * Classes and their matching filename 84 | * @var array 85 | */ 86 | private $_classes = array(); 87 | 88 | /** 89 | * Scan files matching this regex 90 | * @var string 91 | */ 92 | private $_filesRegex = '/\.(inc|php)$/'; 93 | 94 | /** 95 | * Save path (Default is null) 96 | * @var string 97 | */ 98 | private $_saveFile = null; 99 | 100 | /** 101 | * Scan options 102 | * @var Integer (options) 103 | */ 104 | private $_scanOptions = self::SCAN_ONCE; 105 | 106 | /** 107 | * Constructor 108 | * 109 | * @param string $saveFile Path where autoload files will be saved 110 | * @param int $scanOptions Scan options 111 | * @return void 112 | */ 113 | public function __construct($saveFile = null, $scanOptions = self::SCAN_ONCE) 114 | { 115 | $this->setSaveFile($saveFile); 116 | $this->setScanOptions($scanOptions); 117 | } 118 | 119 | /** 120 | * Get the path where autoload files are saved 121 | * 122 | * @return string path where autoload files will be saved 123 | */ 124 | public function getSaveFile() 125 | { 126 | return $this->_saveFile; 127 | } 128 | 129 | /** 130 | * Set the path where autoload files are saved 131 | * 132 | * @param string $path path where autoload files will be saved 133 | */ 134 | public function setSaveFile($pathToFile) 135 | { 136 | $this->_saveFile = $pathToFile; 137 | if ($this->_saveFile && file_exists($this->_saveFile)) 138 | { 139 | $this->_classes = include($this->_saveFile); 140 | } 141 | } 142 | 143 | /** 144 | * Set the file regex 145 | * 146 | * @param string 147 | */ 148 | public function setFileRegex($regex) 149 | { 150 | $this->_filesRegex = $regex; 151 | } 152 | 153 | /** 154 | * Set the file extensions 155 | * 156 | * Another method to set up the $_filesRegex 157 | * 158 | * @param string|array allowed extension string or array with extension strings 159 | * @return void 160 | */ 161 | public function setAllowedFileExtensions($extensions) 162 | { 163 | $regex = '/\.'; 164 | if (is_array($extensions)) 165 | { 166 | $regex .= '(' . implode('|', $extensions) . ')'; 167 | } 168 | else { 169 | $regex .= $extensions; 170 | } 171 | 172 | $this->_filesRegex = $regex . '$/'; 173 | } 174 | 175 | /** 176 | * Add a new folder to parse 177 | * 178 | * @param string $path Root path to process 179 | */ 180 | public function addFolder($path) 181 | { 182 | if ($realpath = realpath($path) and is_dir($realpath)) 183 | { 184 | $this->_folders[] = $realpath; 185 | } 186 | else 187 | { 188 | throw new Exception('Failed to open dir : ' . $path); 189 | } 190 | } 191 | 192 | /** 193 | * Exclude a folder from the parsing 194 | * 195 | * @param string $path Folder to exclude 196 | */ 197 | public function excludeFolder($path) 198 | { 199 | if ($realpath = realpath($path) and is_dir($realpath)) 200 | { 201 | $this->_excludedFolders[] = $realpath . DIRECTORY_SEPARATOR; 202 | } 203 | else 204 | { 205 | throw new Exception('Failed to open dir : ' . $path); 206 | } 207 | } 208 | 209 | /** 210 | * Checks if the class has been defined 211 | * 212 | * @param string $className Name of the class 213 | * @return bool true if class exists, false otherwise. 214 | */ 215 | public function classExists($className) 216 | { 217 | return self::CLASS_EXISTS === $this->checkClass($className, $this->_classes); 218 | } 219 | 220 | /** 221 | * Set the scan options 222 | * 223 | * @param int $options scan options. 224 | * @return void 225 | */ 226 | public function setScanOptions($options) 227 | { 228 | $this->_scanOptions = $options; 229 | } 230 | 231 | /** 232 | * Get the scan options. 233 | * 234 | * @return int 235 | */ 236 | public function getScanOptions() 237 | { 238 | return $this->_scanOptions; 239 | } 240 | 241 | /** 242 | * Method used by the spl_autoload_register 243 | * 244 | * @param string $className Name of the class 245 | * @return void 246 | */ 247 | public function loadClass($className) 248 | { 249 | $className = strtolower($className); 250 | // check if the class already exists in the cache file 251 | $loaded = $this->checkClass($className, $this->_classes); 252 | if (!$loaded && (self::SCAN_ONCE & $this->_scanOptions)) 253 | { 254 | // parse the folders returns the list of all the classes 255 | // in the application 256 | $this->refresh(); 257 | 258 | // recheck if the class exists again in the reloaded classes 259 | $loaded = $this->checkClass($className, $this->_classes); 260 | if (!$loaded && (self::SCAN_CACHE & $this->_scanOptions)) 261 | { 262 | // set it to null to flag that it was not found 263 | // This behaviour fixes the problem with infinite 264 | // loop if we have a class_exists() for an inexistant 265 | // class. 266 | $this->_classes[$className] = null; 267 | } 268 | 269 | // write to a single file 270 | if ($this->getSaveFile()) 271 | { 272 | $this->saveToFile($this->_classes); 273 | } 274 | 275 | // scan just once per call 276 | if (!($this->_scanOptions & 2)) 277 | { 278 | $this->_scanOptions = $this->_scanOptions & ~ self::SCAN_ONCE; 279 | } 280 | } 281 | } 282 | 283 | /** 284 | * checks if a className exists in the class array 285 | * 286 | * @param mixed $className the classname to check 287 | * @param array $classes an array of classes 288 | * @return int errorCode 1 if the class exists 289 | * 2 if the class exists and is null 290 | * (there have been an attempt done) 291 | * 0 if the class does not exist 292 | */ 293 | private function checkClass($className, array $classes) 294 | { 295 | if (isset($classes[$className])) 296 | { 297 | require $classes[$className]; 298 | return self::CLASS_EXISTS; 299 | } 300 | elseif (array_key_exists($className, $classes)) 301 | { 302 | return self::CLASS_IS_NULL; 303 | } 304 | return self::CLASS_NOT_FOUND; 305 | } 306 | 307 | 308 | /** 309 | * Parse every registred folders, regenerate autoload files and update the $_classes 310 | * 311 | * @return array Array containing all the classes found 312 | */ 313 | private function parseFolders() 314 | { 315 | $classesArray = array(); 316 | foreach ($this->_folders as $folder) 317 | { 318 | $classesArray = array_merge($classesArray, $this->parseFolder($folder)); 319 | } 320 | return $classesArray; 321 | } 322 | 323 | /** 324 | * Parse folder and update $_classes array 325 | * 326 | * @param string $folder Folder to process 327 | * @return array Array containing all the classes found 328 | */ 329 | private function parseFolder($folder) 330 | { 331 | $classes = array(); 332 | $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($folder)); 333 | 334 | foreach ($files as $file) 335 | { 336 | if ($file->isFile() && preg_match($this->_filesRegex, $file->getFilename())) 337 | { 338 | foreach ($this->_excludedFolders as $folder) 339 | { 340 | $len = strlen($folder); 341 | if (0 === strncmp($folder, $file->getPathname(), $len)) 342 | { 343 | continue 2; 344 | } 345 | } 346 | 347 | if ($classNames = $this->getClassesFromFile($file->getPathname())) 348 | { 349 | foreach ($classNames as $className) 350 | { 351 | // Adding class to map 352 | $classes[$className] = $file->getPathname(); 353 | } 354 | } 355 | } 356 | } 357 | return $classes; 358 | } 359 | 360 | /** 361 | * Extract the classname contained inside the php file 362 | * 363 | * @param string $file Filename to process 364 | * @return array Array of classname(s) and interface(s) found in the file 365 | */ 366 | private function getClassesFromFile($file) 367 | { 368 | $namespace = null; 369 | $classes = array(); 370 | $tokens = token_get_all(file_get_contents($file)); 371 | $nbtokens = count($tokens); 372 | 373 | for ($i = 0 ; $i < $nbtokens ; $i++) 374 | { 375 | switch ($tokens[$i][0]) 376 | { 377 | case T_NAMESPACE: 378 | $namespace = null; 379 | $i+=2; 380 | while ($tokens[$i][0] === T_STRING || $tokens[$i][0] === T_NS_SEPARATOR) 381 | { 382 | $namespace .= $tokens[$i++][1]; 383 | } 384 | break; 385 | case T_INTERFACE: 386 | case T_CLASS: 387 | case T_TRAIT: 388 | if(($tokens[$i][0] === T_CLASS) && $tokens[$i-1][0] === T_DOUBLE_COLON) 389 | { 390 | continue(2); 391 | } 392 | 393 | $i+=2; 394 | if ($namespace) 395 | { 396 | $classes[] = strtolower($namespace . '\\' . $tokens[$i][1]); 397 | } 398 | else 399 | { 400 | $classes[] = strtolower($tokens[$i][1]); 401 | } 402 | break; 403 | } 404 | } 405 | 406 | return $classes; 407 | } 408 | 409 | /** 410 | * Generate a file containing an array. 411 | * File is generated under the _savePath folder. 412 | * 413 | * @param array $classes Contains all the classes found and the corresponding filename (e.g. {$className} => {fileName}) 414 | * @param string $folder Folder to process 415 | * @return void 416 | */ 417 | private function saveToFile(array $classes) 418 | { 419 | // Write header and comment 420 | $content = 'getSaveFile(), $content); 433 | } 434 | 435 | /** 436 | * Returns previously registered classes 437 | * 438 | * @return array the list of registered classes 439 | */ 440 | public function getRegisteredClasses() 441 | { 442 | return $this->_classes; 443 | } 444 | 445 | /** 446 | * Refreshes an already generated cache file 447 | * This solves problems with previously unexistant classes that 448 | * have been made available after. 449 | * The optimize functionnality will look at all null values of 450 | * the available classes and does a new parse. if it founds that 451 | * there are classes that has been made available, it will update 452 | * the file. 453 | * 454 | * @return bool true if there has been a change to the array, false otherwise 455 | */ 456 | public function refresh() 457 | { 458 | $existantClasses = $this->_classes; 459 | $nullClasses = array_filter($existantClasses, array('self','_getNullElements')); 460 | $newClasses = $this->parseFolders(); 461 | 462 | // $newClasses will override $nullClasses if the same key exists 463 | // this allows new added classes (that were flagged as null) to be 464 | // added 465 | $this->_classes = array_merge($nullClasses, $newClasses); 466 | return true; 467 | } 468 | 469 | /** 470 | * Generate the autoload file 471 | * 472 | * @return void 473 | */ 474 | public function generate() 475 | { 476 | if ($this->getSaveFile()) 477 | { 478 | $this->refresh(); 479 | $this->saveToFile($this->_classes); 480 | } 481 | } 482 | 483 | /** 484 | * returns null elements (used in an array filter) 485 | * 486 | * @param mixed $element the element to check 487 | * @return bool true if element is null, false otherwise 488 | */ 489 | private function _getNullElements($element) 490 | { 491 | return null === $element; 492 | } 493 | 494 | /** 495 | * Registers this autoloadManager on the SPL autoload stack. 496 | * 497 | * @return void 498 | */ 499 | public function register() 500 | { 501 | spl_autoload_register(array($this, 'loadClass')); 502 | } 503 | 504 | /** 505 | * Removes this autoloadManager from the SPL autoload stack. 506 | * 507 | * @return void 508 | */ 509 | public function unregister() 510 | { 511 | spl_autoload_unregister(array($this, 'loadClass')); 512 | } 513 | } 514 | -------------------------------------------------------------------------------- /test/001.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | autoloadManager - Test no-cache SCAN_ONCE 3 | --FILE-- 4 | addFolder(__DIR__ . '/src/'); 9 | $autoloadManager->register(); 10 | 11 | $a = new A; 12 | $c = new C; 13 | ?> 14 | --EXPECTF-- 15 | Fatal error: Class 'C' not found in %s on line %d 16 | 17 | -------------------------------------------------------------------------------- /test/002.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | autoloadManager - Test cache SCAN_NEVER 3 | --FILE-- 4 | register(); 9 | 10 | $a = new A; 11 | $b = new B; 12 | ?> 13 | --EXPECTF-- 14 | Fatal error: Class 'B' not found in %s on line %d 15 | 16 | -------------------------------------------------------------------------------- /test/003.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | autoloadManager - Test cache SCAN_ONCE 3 | --FILE-- 4 | addFolder(__DIR__ . '/src'); 9 | $autoloadManager->register(); 10 | 11 | $a = new A; 12 | $b = new B; 13 | 14 | var_dump(include(__DIR__ . '/cache.php')); 15 | unlink(__DIR__ . '/cache.php'); 16 | 17 | ?> 18 | --EXPECTF-- 19 | array(2) { 20 | ["b"]=> 21 | string(%d) "%s/src/b.php" 22 | ["a"]=> 23 | string(%d) "%s/src/a.php" 24 | } 25 | 26 | -------------------------------------------------------------------------------- /test/004.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | autoloadManager - Test cache SCAN_ONCE 3 | --FILE-- 4 | addFolder(__DIR__ . '/src'); 9 | $autoloadManager->register(); 10 | 11 | class_exists('A'); 12 | 13 | $time1 = filemtime(__DIR__ . '/cache.php'); 14 | 15 | sleep(2); 16 | class_exists('C'); 17 | 18 | $time2 = filemtime(__DIR__ . '/cache.php'); 19 | 20 | var_dump($time1 == $time2); 21 | unlink(__DIR__ . '/cache.php'); 22 | 23 | ?> 24 | --EXPECTF-- 25 | bool(true) 26 | -------------------------------------------------------------------------------- /test/005.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | autoloadManager - Test cache SCAN_ALWAYS 3 | --FILE-- 4 | addFolder(__DIR__ . '/src'); 9 | $autoloadManager->register(); 10 | 11 | class_exists('A'); 12 | 13 | $time1 = filemtime(__DIR__ . '/cache.php'); 14 | 15 | sleep(2); 16 | class_exists('C'); 17 | 18 | $time2 = filemtime(__DIR__ . '/cache.php'); 19 | 20 | var_dump($time1 == $time2); 21 | unlink(__DIR__ . '/cache.php'); 22 | 23 | ?> 24 | --EXPECTF-- 25 | bool(false) 26 | -------------------------------------------------------------------------------- /test/006.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | autoloadManager - Test cache SCAN_CACHE 3 | --FILE-- 4 | addFolder(__DIR__ . '/src'); 9 | $autoloadManager->register(); 10 | 11 | class_exists('C'); 12 | 13 | $time1 = filemtime(__DIR__ . '/cache.php'); 14 | 15 | sleep(2); 16 | class_exists('C'); 17 | 18 | $time2 = filemtime(__DIR__ . '/cache.php'); 19 | 20 | var_dump($time1 == $time2); 21 | unlink(__DIR__ . '/cache.php'); 22 | 23 | ?> 24 | --EXPECTF-- 25 | bool(true) 26 | -------------------------------------------------------------------------------- /test/007.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | autoloadManager - Test case sensitivity 3 | --FILE-- 4 | addFolder(__DIR__ . '/src/'); 9 | $autoloadManager->register(); 10 | 11 | $a = new a; 12 | echo 'DONE!'; 13 | ?> 14 | --EXPECTF-- 15 | DONE! 16 | -------------------------------------------------------------------------------- /test/src/a.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/src/a.php' 12 | ); 13 | --------------------------------------------------------------------------------