* @version 1.5.0 * @copyright Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2017 Core Developer Team * @license GNU GPL 3.0 or any newer version * @link http://www.shipsimu.org * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * ---------------------------------- * 1.5 * - Namespace scheme Project\Package[\SubPackage...] is fully supported and * throws an InvalidArgumentException if not present. The last part will be * always the class' name. * 1.4 * - Some comments improved, other minor improvements * 1.3 * - Constructor is now empty and factory method 'createClassLoader' is created * - renamed loadClasses to scanClassPath * - Added initLoader() * 1.2 * - ClassLoader rewritten to PHP SPL's own RecursiveIteratorIterator class * 1.1 * - loadClasses rewritten to fix some notices * 1.0 * - Initial release * ---------------------------------- */ class ClassLoader { /** * Instance of this class */ private static $selfInstance = NULL; /** * Array with all found classes */ private $foundClasses = array(); /** * List of loaded classes */ private $loadedClasses = array(); /** * Suffix with extension for all class files */ private $prefix = 'class_'; /** * Suffix with extension for all class files */ private $suffix = '.php'; /** * A list for directory names (no leading/trailing slashes!) which not be scanned by the path scanner * @see scanLocalPath */ private $ignoreList = array(); /** * Debug this class loader? (true = yes, false = no) */ private $debug = false; /** * Whether the file list is cached */ private $listCached = false; /** * Wethe class content has been cached */ private $classesCached = false; /** * SplFileInfo for the list cache */ private $listCacheFile = NULL; /** * SplFileInfo for class content */ private $classCacheFile = NULL; /** * Counter for loaded include files */ private $total = 0; /** * By default the class loader is strict with naming-convention check */ private static $strictNamingConvention = true; /** * Framework/application paths for classes, etc. */ private static $frameworkPaths = array( 'exceptions', // Exceptions 'interfaces', // Interfaces 'classes', // Classes 'middleware' // The middleware ); /** * Registered paths where test classes can be found. These are all relative * to base_path . */ private static $testPaths = array(); /** * The protected constructor. Please use the factory method below, or use * getSelfInstance() for singleton * * @return void */ protected function __construct () { // This is empty for now } /** * The destructor makes it sure all caches got flushed * * @return void */ public function __destruct () { // Skip here if dev-mode if (defined('DEVELOPER')) { return; } // END - if // Skip here if already cached if ($this->listCached === false) { // Writes the cache file of our list away $cacheContent = json_encode($this->foundClasses); // Open cache instance $fileObject = $this->listCacheFile->openFile('w'); // And write whole list $fileObject->fwrite($cacheContent); } // END - if // Skip here if already cached if ($this->classesCached === false) { // Generate a full-cache of all classes $cacheContent = ''; foreach (array_keys($this->loadedClasses) as $fileInstance) { // Open file $fileObject = $fileInstance->openFile('r'); // Load the file // @TODO Add some uglifying code (compress) here $cacheContent .= $fileObject->fread($fileInstance->getSize()); } // END - foreach // Open file $fileObject = $this->classCacheFile->openFile('w'); // And write it away $fileObject->fwrite($cacheContent); } // END - if } /** * Creates an instance of this class loader for given configuration instance * * @param $configInstance Configuration class instance * @return void */ public static final function createClassLoader (FrameworkConfiguration $configInstance) { // Get a new instance $loaderInstance = new ClassLoader(); // Init the instance $loaderInstance->initLoader($configInstance); // Return the prepared instance return $loaderInstance; } /** * Scans for all framework classes, exceptions and interfaces. * * @return void */ public static function scanFrameworkClasses () { // Trace message //* NOISY-DEBUG: */ printf('[%s:%d]: CALLED!' . PHP_EOL, __METHOD__, __LINE__); // Get loader instance $loaderInstance = self::getSelfInstance(); // Get config instance $configInstance = FrameworkBootstrap::getConfigurationInstance(); // Load all classes foreach (self::$frameworkPaths as $shortPath) { // Debug message //* NOISY-DEBUG: */ printf('[%s:%d]: shortPath=%s' . PHP_EOL, __METHOD__, __LINE__, $shortPath); // Generate full path from it $realPathName = realpath(sprintf( '%smain%s%s%s', $configInstance->getConfigEntry('framework_base_path'), DIRECTORY_SEPARATOR, $shortPath, DIRECTORY_SEPARATOR )); // Debug message //* NOISY-DEBUG: */ printf('[%s:%d]: realPathName=%s' . PHP_EOL, __METHOD__, __LINE__, $realPathName); // Is it not false and accessible? if (is_bool($realPathName)) { // Skip this continue; } elseif (!is_readable($realPathName)) { // @TODO Throw exception instead of break break; } // Try to load the framework classes $loaderInstance->scanClassPath($realPathName); } // END - foreach // Trace message //* NOISY-DEBUG: */ printf('[%s:%d]: EXIT!' . PHP_EOL, __METHOD__, __LINE__); } /** * Scans for application's classes, etc. * * @return void */ public static function scanApplicationClasses () { // Trace message //* NOISY-DEBUG: */ printf('[%s:%d]: CALLED!' . PHP_EOL, __METHOD__, __LINE__); // Get loader instance $loaderInstance = self::getSelfInstance(); // Get config instance $configInstance = FrameworkBootstrap::getConfigurationInstance(); // Load all classes for the application foreach (self::$frameworkPaths as $shortPath) { // Debug message //* NOISY-DEBUG: */ printf('[%s:%d]: shortPath=%s' . PHP_EOL, __METHOD__, __LINE__, $shortPath); // Create path name $pathName = realpath(sprintf( '%s%s%s%s%s', $configInstance->getConfigEntry('application_base_path'), DIRECTORY_SEPARATOR, $configInstance->getConfigEntry('detected_app_name'), DIRECTORY_SEPARATOR, $shortPath )); // Debug message //* NOISY-DEBUG: */ printf('[%s:%d]: pathName[%s]=%s' . PHP_EOL, __METHOD__, __LINE__, gettype($pathName), $pathName); // Is the path readable? if (is_dir($pathName)) { // Try to load the application classes $loaderInstance->scanClassPath($pathName); } // END - if } // END - foreach // Trace message //* NOISY-DEBUG: */ printf('[%s:%d]: EXIT!' . PHP_EOL, __METHOD__, __LINE__); } /** * Scans for test classes, etc. * * @return void */ public static function scanTestsClasses () { // Trace message //* NOISY-DEBUG: */ printf('[%s:%d]: CALLED!' . PHP_EOL, __METHOD__, __LINE__); // Get config instance $configInstance = FrameworkBootstrap::getConfigurationInstance(); // Load all classes for the application foreach (self::$testPaths as $shortPath) { // Debug message //* NOISY-DEBUG: */ printf('[%s:%d]: shortPath=%s' . PHP_EOL, __METHOD__, __LINE__, $shortPath); // Construct path name $pathName = sprintf( '%s%s%s', $configInstance->getConfigEntry('root_base_path'), DIRECTORY_SEPARATOR, $shortPath ); // Debug message //* NOISY-DEBUG: */ printf('[%s:%d]: pathName[%s]=%s - BEFORE!' . PHP_EOL, __METHOD__, __LINE__, gettype($pathName), $pathName); // Try to find it $realPathName = realpath($pathName); // Debug message //* NOISY-DEBUG: */ printf('[%s:%d]: realPathName[%s]=%s - AFTER!' . PHP_EOL, __METHOD__, __LINE__, gettype($realPathName), $realPathName); // Is the path readable? if ((is_dir($realPathName)) && (is_readable($realPathName))) { // Try to load the application classes ClassLoader::getSelfInstance()->scanClassPath($realPathName); } // END - if } // END - foreach // Trace message //* NOISY-DEBUG: */ printf('[%s:%d]: EXIT!' . PHP_EOL, __METHOD__, __LINE__); } /** * Enables or disables strict naming-convention tests on class loading * * @param $strictNamingConvention Whether to strictly check naming-convention * @return void */ public static function enableStrictNamingConventionCheck ($strictNamingConvention = true) { self::$strictNamingConvention = $strictNamingConvention; } /** * Registeres given relative path where test classes reside. For regular * framework uses, they should not be loaded (and used). * * @param $relativePath Relative path to test classes * @return void */ public static function registerTestsPath ($relativePath) { // Trace message //* NOISY-DEBUG: */ printf('[%s:%d]: relativePath=%s - CALLED!' . PHP_EOL, __METHOD__, __LINE__, $relativePath); // "Register" it self::$testPaths[$relativePath] = $relativePath; // Trace message //* NOISY-DEBUG: */ printf('[%s:%d]: EXIT!' . PHP_EOL, __METHOD__, __LINE__); } /** * Autoload-function * * @param $className Name of the class to load * @return void */ public static function autoLoad ($className) { // Trace message //* NOISY-DEBUG: */ printf('[%s:%d]: className=%s - CALLED!' . PHP_EOL, __METHOD__, __LINE__, $className); // Try to include this class self::getSelfInstance()->loadClassFile($className); // Trace message //* NOISY-DEBUG: */ printf('[%s:%d]: EXIT!' . PHP_EOL, __METHOD__, __LINE__); } /** * Singleton getter for an instance of this class * * @return $selfInstance A singleton instance of this class */ public static final function getSelfInstance () { // Is the instance there? if (is_null(self::$selfInstance)) { // Get a new one self::$selfInstance = ClassLoader::createClassLoader(FrameworkBootstrap::getConfigurationInstance()); } // END - if // Return the instance return self::$selfInstance; } /** * Scans recursively a local path for class files which must have a prefix and a suffix as given by $this->suffix and $this->prefix * * @param $basePath The relative base path to 'framework_base_path' constant for all classes * @param $ignoreList An optional list (array forced) of directory and file names which shall be ignored * @return void */ public function scanClassPath ($basePath, array $ignoreList = array() ) { // Is a list has been restored from cache, don't read it again if ($this->listCached === true) { // Abort here return; } // END - if // Keep it in class for later usage $this->ignoreList = $ignoreList; /* * Ignore .htaccess by default as it is for protection of directories * on Apache servers. */ array_push($ignoreList, '.htaccess'); /* * Set base directory which holds all our classes, an absolute path * should be used here so is_dir(), is_file() and so on will always * find the correct files and dirs. */ $basePath2 = realpath($basePath); // If the basePath is false it is invalid if ($basePath2 === false) { /* @TODO: Do not exit here. */ exit(__METHOD__ . ': Cannot read ' . $basePath . ' !' . PHP_EOL); } else { // Set base path $basePath = $basePath2; } // Get a new iterator //* NOISY-DEBUG: */ printf('[%s:%d] basePath=%s' . PHP_EOL, __METHOD__, __LINE__, $basePath); $iteratorInstance = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basePath), RecursiveIteratorIterator::CHILD_FIRST); // Load all entries while ($iteratorInstance->valid()) { // Get current entry $currentEntry = $iteratorInstance->current(); // Get filename from iterator which is the class' name (according naming-convention) $fileName = $currentEntry->getFileName(); // Current entry must be a file, not smaller than 100 bytes and not on ignore list if ((!$currentEntry->isFile()) || (in_array($fileName, $this->ignoreList)) || ($currentEntry->getSize() < 100)) { // Advance to next entry $iteratorInstance->next(); // Skip non-file entries //* NOISY-DEBUG: */ printf('[%s:%d] SKIP: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName); continue; } // END - if // Is this file wanted? //* NOISY-DEBUG: */ printf('[%s:%d] FOUND: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName); if ((substr($fileName, 0, strlen($this->prefix)) == $this->prefix) && (substr($fileName, -strlen($this->suffix), strlen($this->suffix)) == $this->suffix)) { // Add it to the list //* NOISY-DEBUG: */ printf('[%s:%d] ADD: %s,currentEntry=%s' . PHP_EOL, __METHOD__, __LINE__, $fileName, $currentEntry); $this->foundClasses[$fileName] = $currentEntry; } else { // Not added //* NOISY-DEBUG: */ printf('[%s:%d] NOT ADDED: %s,currentEntry=%s' . PHP_EOL, __METHOD__, __LINE__, $fileName, $currentEntry); } // Advance to next entry //* NOISY-DEBUG: */ printf('[%s:%d] NEXT: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName); $iteratorInstance->next(); } // END - while } /** * Getter for total include counter * * @return $total Total loaded include files */ public final function getTotal () { return $this->total; } /** * Getter for a printable list of included main/interfaces/exceptions * * @param $includeList A printable include list */ public function getPrintableIncludeList () { // Prepare the list $includeList = ''; foreach ($this->loadedClasses as $classFile) { $includeList .= basename($classFile) . '
' . PHP_EOL; } // END - foreach // And return it return $includeList; } /** * Initializes our loader class * * @param $configInstance Configuration class instance * @return void */ private function initLoader (FrameworkConfiguration $configInstance) { // Set configuration instance $this->configInstance = $configInstance; // Construct the FQFN for the cache if (!defined('DEVELOPER')) { $this->listCacheFile = new SplFileInfo($this->configInstance->getConfigEntry('local_database_path') . 'list-' . $this->configInstance->getConfigEntry('detected_app_name') . '.cache'); $this->classCacheFile = new SplFileInfo($this->configInstance->getConfigEntry('local_database_path') . 'class-' . $this->configInstance->getConfigEntry('detected_app_name') . '.cache'); } // END - if // Set suffix and prefix from configuration $this->suffix = $configInstance->getConfigEntry('class_suffix'); $this->prefix = $configInstance->getConfigEntry('class_prefix'); // Set own instance self::$selfInstance = $this; // Skip here if no dev-mode if (defined('DEVELOPER')) { return; } // END - if // Is the cache there? if (FrameworkBootstrap::isReadableFile($this->listCacheFile)) { // Load and convert it $this->foundClasses = json_decode(file_get_contents($this->listCacheFile->getPathname())); // List has been restored from cache! $this->listCached = true; } // END - if // Does the class cache exist? if (FrameworkBootstrap::isReadableFile($this->classCacheFile)) { // Then include it FrameworkBootstrap::loadInclude($this->classCacheFile); // Mark the class cache as loaded $this->classesCached = true; } // END - if } /** * Tries to find the given class in our list. This method ignores silently * missing classes or interfaces. So if you use class_exists() this method * does not interrupt your program. * * @param $className The class that shall be loaded * @return void * @throws InvalidArgumentException If strict-checking is enabled and class name is not following naming-convention */ private function loadClassFile ($className) { // Trace message //* NOISY-DEBUG: */ printf('[%s:%d] className=%s - CALLED!' . PHP_EOL, __METHOD__, __LINE__, $className); // The class name should contain at least 2 back-slashes, so split at them $classNameParts = explode("\\", $className); // At least 3 parts should be there if ((self::$strictNamingConvention === true) && (count($classNameParts) < 5)) { // Namespace scheme is: Project\Package[\SubPackage...] throw new InvalidArgumentException(sprintf('Class name "%s" is not conform to naming-convention: Tld\Domain\Project\Package[\SubPackage...]\SomeFooBar', $className)); } // END - if // Get last element $shortClassName = array_pop($classNameParts); // Create a name with prefix and suffix $fileName = sprintf('%s%s%s', $this->prefix, $shortClassName, $this->suffix); // Now look it up in our index //* NOISY-DEBUG: */ printf('[%s:%d] ISSET: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName); if ((isset($this->foundClasses[$fileName])) && (!isset($this->loadedClasses[$this->foundClasses[$fileName]->getPathname()]))) { // File is found and not loaded so load it only once //* NOISY-DEBUG: */ printf('[%s:%d] LOAD: %s - START' . PHP_EOL, __METHOD__, __LINE__, $fileName); FrameworkBootstrap::loadInclude($this->foundClasses[$fileName]); //* NOISY-DEBUG: */ printf('[%s:%d] LOAD: %s - END' . PHP_EOL, __METHOD__, __LINE__, $fileName); // Count this loaded class/interface/exception $this->total++; // Mark this class as loaded for other purposes than loading it. $this->loadedClasses[$this->foundClasses[$fileName]->getPathname()] = true; // Remove it from classes list so it won't be found twice. //* NOISY-DEBUG: */ printf('[%s:%d] UNSET: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName); unset($this->foundClasses[$fileName]); // Developer mode excludes caching (better debugging) if (!defined('DEVELOPER')) { // Reset cache //* NOISY-DEBUG: */ printf('[%s:%d] classesCached=false' . PHP_EOL, __METHOD__, __LINE__); $this->classesCached = false; } // END - if } else { // Not found //* NOISY-DEBUG: */ printf('[%s:%d] 404: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName); } } }