* @version 1.7.0 * @copyright Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2023 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.7.0 * - "Cached" more like config instance and root/application base path for * shorter call stacks and lesser methods invoked * - More debug logging * 1.6.0 * - This class loader is now 100% singleton, no other instance is really * required, therefore the factory method can be removed safely * - renamed initLoader() to initClassLoader() * - An instance of a FrameworkConfiguration is no longer set here as this * violated the rule that there shall be no instance set of that class * - .htaccess is marked as deprecated as this should be no longer done * - scanClassPath() is now protected, please use other scan*() methods * 1.5.0 * - 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.0 * - Some comments improved, other minor improvements * 1.3.0 * - Constructor is now empty and factory method 'createClassLoader' is created * - renamed loadClasses to scanClassPath * - Added initLoader() * 1.2.0 * - ClassLoader rewritten to PHP SPL's own RecursiveIteratorIterator class * 1.1.0 * - loadClasses rewritten to fix some notices * 1.0.0 * - Initial release * ---------------------------------- */ final class ClassLoader { /** * Instance of this class */ private static $selfInstance = NULL; /** * Instance of a FrameworkConfiguration class */ private static $configInstance = NULL; /** * Cached configuration entry 'is_developer_mode_enabled' */ private static $developerModeEnabled = NULL; /** * Array with all valid but pending for loading file names (class, * interfaces, traits must start with below prefix). */ private $pendingFiles = []; /** * List of loaded classes */ private $loadedClasses = []; /** * 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 = []; /** * 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 = [ 'classes', // Classes 'exceptions', // Exceptions 'interfaces', // Interfaces 'middleware', // The middleware 'traits', // Traits ]; /** * Registered paths where test classes can be found. These are all relative * to base_path . */ private static $testPaths = []; /** * Cached includes that needs to be flushed */ private $flushCache = []; /** * The protected constructor. Please use the factory method below, or use * getSelfInstance() for singleton * * @return void */ private function __construct () { // Is developerModeEnabled set? //* NOISY-DEBUG: */ printf('[%s:%d]: CONSTRUCTED!' . PHP_EOL, __METHOD__, __LINE__); if (is_null(self::$developerModeEnabled)) { // Cache config instance self::$configInstance = FrameworkBootstrap::getConfigurationInstance(); // Cache config entry self::$developerModeEnabled = self::$configInstance->isEnabled('developer_mode'); } // Trace message //* NOISY-DEBUG: */ printf('[%s:%d]: self::developerModeEnabled=%d - EXIT!' . PHP_EOL, __METHOD__, __LINE__, intval(self::$developerModeEnabled)); } /** * The destructor makes it sure all caches got flushed * * @return void */ public function __destruct () { // Skip here if dev-mode //* NOISY-DEBUG: */ printf('[%s:%d]: self::developerModeEnabled=%d - DESTRUCTED!' . PHP_EOL, __METHOD__, __LINE__, intval(self::$developerModeEnabled)); if (self::$developerModeEnabled) { // Is enabled, don't cache //* NOISY-DEBUG: */ printf('[%s:%d]: Developer mode enabled, not caching classes - EXIT!' . PHP_EOL, __METHOD__, __LINE__); return; } // Init content $cacheContent = ''; // Skip here if already cached //* NOISY-DEBUG: */ printf('[%s:%d]: this->listCached=%d' . PHP_EOL, __METHOD__, __LINE__, intval($this->listCached)); if ($this->listCached === false) { // Writes the cache file of our list away $cacheContent = json_encode($this->pendingFiles); // Open cache instance //* NOISY-DEBUG: */ printf('[%s:%d]: cacheContent()=%d' . PHP_EOL, __METHOD__, __LINE__, strlen($cacheContent)); $fileObject = $this->listCacheFile->openFile('w'); // And write whole list $fileObject->fwrite($cacheContent); // Close it //* NOISY-DEBUG: */ printf('[%s:%d]: Closing file %s ...' . PHP_EOL, __METHOD__, __LINE__, $fileObject->getPathName()); unset($fileObject); } // Init content $cacheContent = ''; // Skip here if already cached //* NOISY-DEBUG: */ printf('[%s:%d]: this->classesCached=%d' . PHP_EOL, __METHOD__, __LINE__, intval($this->classesCached)); if ($this->classesCached === false) { // Generate a full-cache of all classes //* NOISY-DEBUG: */ printf('[%s:%d]: this->flushCache()=%d' . PHP_EOL, __METHOD__, __LINE__, count($this->flushCache)); foreach ($this->flushCache as $key => $fileInstance) { // Open file //* NOISY-DEBUG: */ printf('[%s:%d]: key=%s,fileInstance[]=%s' . PHP_EOL, __METHOD__, __LINE__, $key, gettype($fileInstance)); $fileObject = $fileInstance->openFile('r'); // Load the file // @TODO Add some uglifying code (compress) here //* NOISY-DEBUG: */ printf('[%s:%d]: Adding fileInstance->size=%d bytes ...' . PHP_EOL, __METHOD__, __LINE__, $fileInstance->getSize()); $cacheContent .= $fileObject->fread($fileInstance->getSize()); } // Open file //* NOISY-DEBUG: */ printf('[%s:%d]: cacheContent()=%d' . PHP_EOL, __METHOD__, __LINE__, strlen($cacheContent)); $fileObject = $this->classCacheFile->openFile('w'); // And write it away $fileObject->fwrite($cacheContent); // Close it //* NOISY-DEBUG: */ printf('[%s:%d]: Closing file %s ...' . PHP_EOL, __METHOD__, __LINE__, $fileObject->getPathName()); unset($fileObject); } // Trace message //* NOISY-DEBUG: */ printf('[%s:%d]: EXIT!' . PHP_EOL, __METHOD__, __LINE__); } /** * Scans for all framework classes, exceptions and interfaces. * * @return void */ public static function scanFrameworkClasses () { // Get loader instance //* NOISY-DEBUG: */ printf('[%s:%d]: CALLED!' . PHP_EOL, __METHOD__, __LINE__); $loaderInstance = self::getSelfInstance(); // "Cache" configuration instance and framework base path $frameworkBasePath = self::$configInstance->getConfigEntry('framework_base_path'); // Load all classes //* NOISY-DEBUG: */ printf('[%s:%d]: frameworkBasePath=%s,self::$frameworkPaths()=%d,' . PHP_EOL, __METHOD__, __LINE__, $frameworkBasePath, count(self::$frameworkPaths)); foreach (self::$frameworkPaths as $shortPath) { // Generate full path from it //* NOISY-DEBUG: */ printf('[%s:%d]: shortPath[%s]=%s' . PHP_EOL, __METHOD__, __LINE__, gettype($shortPath), $shortPath); $realPathName = realpath(sprintf( '%smain%s%s%s', $frameworkBasePath, DIRECTORY_SEPARATOR, $shortPath, DIRECTORY_SEPARATOR )); // Is it not false and accessible? //* NOISY-DEBUG: */ printf('[%s:%d]: realPathName[%s]=%s' . PHP_EOL, __METHOD__, __LINE__, gettype($realPathName), $realPathName); if (is_bool($realPathName)) { // Skip this, it is not accessible continue; } elseif (!is_readable($realPathName)) { // @TODO Throw exception instead of break break; } // Try to load the framework classes $loaderInstance->scanClassPath($realPathName); } // Trace message //* NOISY-DEBUG: */ printf('[%s:%d]: EXIT!' . PHP_EOL, __METHOD__, __LINE__); } /** * Scans for application's classes, etc. * * @return void * @throws UnexpectedValueException If a given path isn't one or not readable */ public static function scanApplicationClasses () { // Get loader instance //* NOISY-DEBUG: */ printf('[%s:%d]: CALLED!' . PHP_EOL, __METHOD__, __LINE__); $loaderInstance = self::getSelfInstance(); // "Cache" application base path $basePath = self::$configInstance->getConfigEntry('application_base_path'); // Load all classes for the application //* NOISY-DEBUG: */ printf('[%s:%d]: self::frameworkPaths()=%d,basePath=%s' . PHP_EOL, __METHOD__, __LINE__, count(self::$frameworkPaths), $basePath); foreach (self::$frameworkPaths as $shortPath) { // Create path name //* NOISY-DEBUG: */ printf('[%s:%d]: shortPath=%s' . PHP_EOL, __METHOD__, __LINE__, $shortPath); $realPathName = realpath(sprintf( '%s%s%s%s%s', $basePath, DIRECTORY_SEPARATOR, FrameworkBootstrap::getDetectedApplicationName(), DIRECTORY_SEPARATOR, $shortPath )); // Is the path readable? //* NOISY-DEBUG: */ printf('[%s:%d]: realPathName[%s]=%s - AFTER!' . PHP_EOL, __METHOD__, __LINE__, gettype($realPathName), $realPathName); if (!is_string($realPathName)) { // Skip this cone //* NOISY-DEBUG: */ printf('[%s:%d]: realPathName[]=%s - SKIPPED!' . PHP_EOL, __METHOD__, __LINE__, gettype($realPathName)); continue; } elseif (!is_dir($realPathName)) { // Is not a directory throw new UnexpectedValueException(sprintf('realPathName=%s is not a directory', $realPathName), FrameworkInterface::EXCEPTION_UNEXPECTED_VALUE); } elseif (!is_readable($realPathName)) { // Not readable throw new UnexpectedValueException(sprintf('realPathName=%s cannot be read from', $realPathName), FrameworkInterface::EXCEPTION_UNEXPECTED_VALUE); } // Try to load the application classes //* NOISY-DEBUG: */ printf('[%s:%d]: Scanning for classes/interfaces at realPathName=%s ...' . PHP_EOL, __METHOD__, __LINE__, $realPathName); $loaderInstance->scanClassPath($realPathName); } // 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 (bool $strictNamingConvention = true) { self::$strictNamingConvention = $strictNamingConvention; } /** * Autoload-function * * @param $className Name of the class to load * @return void * @throws InvalidArgumentException If the class' name does not contain a namespace: Tld\Domain\Project is AT LEAST recommended! */ public static function autoLoad (string $className) { // Validate parameter //* NOISY-DEBUG: */ printf('[%s:%d] className=%s - CALLED!' . PHP_EOL, __METHOD__, __LINE__, $className); if (empty($className)) { // Should not be empty throw new InvalidArgumentException('Parameter "className" is empty', FrameworkInterface::EXCEPTION_INVALID_ARGUMENT); } // The class name MUST be at least Tld\Domain\Project\Package\SomeFooBar so split at them $classNameParts = explode("\\", $className); // At least 3 parts should be there //* NOISY-DEBUG: */ printf('[%s:%d]: self::strictNamingConvention=%d,classNameParts()=%d' . PHP_EOL, __METHOD__, __LINE__, intval(self::$strictNamingConvention), count($classNameParts)); if ((self::$strictNamingConvention === true) && (count($classNameParts) < 5)) { // Namespace scheme is: Tld\Domain\Project\Package[\SubPackage...] throw new InvalidArgumentException(sprintf('Class name "%s" is not conform to naming-convention: Tld\Domain\Project\Package[\SubPackage...]\SomeFooBar', $className)); } // Try to include this class //* NOISY-DEBUG: */ printf('[%s:%d]: Invoking self->loadClassFile(%s) ...' . PHP_EOL, __METHOD__, __LINE__, $className); 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? //* NOISY-DEBUG: */ printf('[%s:%d]: self::selfInstance[]=%s - CALLED!' . PHP_EOL, __METHOD__, __LINE__, gettype(self::$selfInstance)); if (is_null(self::$selfInstance)) { // Get a new one and initialize it //* NOISY-DEBUG: */ printf('[%s:%d]: Initializing class loader ...' . PHP_EOL, __METHOD__, __LINE__); self::$selfInstance = new ClassLoader(); self::$selfInstance->initClassLoader(); } // Return the instance //* NOISY-DEBUG: */ printf('[%s:%d]: self::selfInstance=%s - EXIT!' . PHP_EOL, __METHOD__, __LINE__, get_class(self::$selfInstance)); return self::$selfInstance; } /** * 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 (array_keys($this->loadedClasses) as $classFile) { $includeList .= basename($classFile) . '
' . PHP_EOL; } // And return it return $includeList; } /** * 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 * @throws InvalidArgumentException If a parameter is invalid */ protected function scanClassPath (string $basePath, array $ignoreList = [] ) { // Is a list has been restored from cache, don't read it again //* NOISY-DEBUG: */ printf('[%s:%d] basePath=%s,ignoreList()=%d - CALLED!' . PHP_EOL, __METHOD__, __LINE__, $basePath, count($ignoreList)); if (empty($basePath)) { // Throw IAE throw new InvalidArgumentException('Parameter "basePath" is empty', FrameworkInterface::EXCEPTION_INVALID_ARGUMENT); } elseif ($this->listCached === true) { // Abort here //* NOISY-DEBUG: */ printf('[%s:%d] this->listCache=true - EXIT!' . PHP_EOL, __METHOD__, __LINE__); return; } // Keep it in class for later usage, but flip index<->value $this->ignoreList = array_flip($ignoreList); /* * 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. */ $basePath = realpath($basePath); // If the basePath is false it is invalid //* NOISY-DEBUG: */ printf('[%s:%d] basePath[%s]=%s' . PHP_EOL, __METHOD__, __LINE__, gettype($basePath), $basePath); if (!is_string($basePath)) { /* @TODO: Do not exit here. */ exit(__METHOD__ . ': Cannot read ' . $basePath . ' !' . PHP_EOL); } // 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) //* NOISY-DEBUG: */ printf('[%s:%d] currentEntry=%s,currentEntry->size=%d' . PHP_EOL, __METHOD__, __LINE__, $currentEntry->__toString(), $currentEntry->getSize()); $fileName = $currentEntry->getFilename(); // Current entry must be a file, not smaller than 100 bytes and not on ignore list //* NOISY-DEBUG: */ printf('[%s:%d] fileName=%s' . PHP_EOL, __METHOD__, __LINE__, $fileName); if (!$currentEntry->isFile() || isset($this->ignoreList[$fileName]) || $currentEntry->getSize() < 100) { // Advance to next entry $iteratorInstance->next(); // Skip non-file entries //* NOISY-DEBUG: */ printf('[%s:%d] SKIP: fileName=%s' . PHP_EOL, __METHOD__, __LINE__, $fileName); continue; } // Is this file wanted? //* NOISY-DEBUG: */ printf('[%s:%d] FOUND: fileName=%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: fileName=%s,currentEntry=%s' . PHP_EOL, __METHOD__, __LINE__, $fileName, $currentEntry); $this->pendingFiles[$fileName] = $currentEntry; } else { // Not added //* NOISY-DEBUG: */ printf('[%s:%d] NOT ADDED: fileName=%s,currentEntry=%s' . PHP_EOL, __METHOD__, __LINE__, $fileName, $currentEntry); } // Advance to next entry //* NOISY-DEBUG: */ printf('[%s:%d] NEXT: fileName=%s' . PHP_EOL, __METHOD__, __LINE__, $fileName); $iteratorInstance->next(); } // Trace message //* NOISY-DEBUG: */ printf('[%s:%d]: EXIT!' . PHP_EOL, __METHOD__, __LINE__); } /** * Initializes our loader class * * @return void */ private function initClassLoader () { // Set suffix and prefix from configuration //* NOISY-DEBUG: */ printf('[%s:%d]: CALLED!' . PHP_EOL, __METHOD__, __LINE__); $this->suffix = self::$configInstance->getConfigEntry('class_suffix'); $this->prefix = self::$configInstance->getConfigEntry('class_prefix'); // Set own instance //* NOISY-DEBUG: */ printf('[%s:%d]: this->suffix=%s,this->prefix=%s' . PHP_EOL, __METHOD__, __LINE__, $this->suffix, $this->prefix); self::$selfInstance = $this; // Skip here if no dev-mode //* NOISY-DEBUG: */ printf('[%s:%d]: self::developerModeEnabled=%d' . PHP_EOL, __METHOD__, __LINE__, intval(self::$developerModeEnabled)); if (self::$developerModeEnabled) { // Developer mode is enabled //* NOISY-DEBUG: */ printf('[%s:%d]: Developer mode is enabled - EXIT!' . PHP_EOL, __METHOD__, __LINE__); return; } // Init cache instances //* NOISY-DEBUG: */ printf('[%s:%d]: Initializing cache file instances ...' . PHP_EOL, __METHOD__, __LINE__); $this->listCacheFile = new SplFileInfo(self::$configInstance->getConfigEntry('local_database_path') . 'list-' . FrameworkBootstrap::getDetectedApplicationName() . '.cache'); $this->classCacheFile = new SplFileInfo(self::$configInstance->getConfigEntry('local_database_path') . 'class-' . FrameworkBootstrap::getDetectedApplicationName() . '.cache'); // Is the cache there? //* NOISY-DEBUG: */ printf('[%s:%d]: Checking this->listCacheFile=%s ...' . PHP_EOL, __METHOD__, __LINE__, $this->listCacheFile); if (FrameworkBootstrap::isReadableFile($this->listCacheFile)) { // Load and convert it //* NOISY-DEBUG: */ printf('[%s:%d]: Loading %s ...' . PHP_EOL, __METHOD__, __LINE__, $this->listCacheFile); $this->pendingFiles = json_decode(file_get_contents($this->listCacheFile->getPathname())); // List has been restored from cache! $this->listCached = true; } // Does the class cache exist? //* NOISY-DEBUG: */ printf('[%s:%d]: Checking this->classCacheFile=%s ...' . PHP_EOL, __METHOD__, __LINE__, $this->classCacheFile); if (FrameworkBootstrap::isReadableFile($this->classCacheFile)) { // Then include it //* NOISY-DEBUG: */ printf('[%s:%d]: Invoking FrameworkBootstrap::loadInclude(%s) ...' . PHP_EOL, __METHOD__, __LINE__, $this->classCacheFile); FrameworkBootstrap::loadInclude($this->classCacheFile); // Mark the class cache as loaded $this->classesCached = true; } // Trace message //* NOISY-DEBUG: */ printf('[%s:%d]: EXIT!' . PHP_EOL, __METHOD__, __LINE__); } /** * Tries to find the given class in our list. It will ignore already loaded * (to the program available) class/interface/trait files. But you SHOULD * save below "expensive" code by pre-checking this->loadedClasses[], if * possible. * * @param $className The class that shall be loaded * @return void */ private function loadClassFile (string $className) { // The class name should contain at least 2 back-slashes, so split at them //* NOISY-DEBUG: */ printf('[%s:%d]: className=%s - CALLED!' . PHP_EOL, __METHOD__, __LINE__, $className); $classNameParts = explode("\\", $className); // Get last element //* NOISY-DEBUG: */ printf('[%s:%d]: classNameParts()=%d' . PHP_EOL, __METHOD__, __LINE__, count($classNameParts)); $shortClassName = array_pop($classNameParts); // Create a name with prefix and suffix //* NOISY-DEBUG: */ printf('[%s:%d]: this->prefix=%s,shortClassName=%s,this->suffix=%s - CALLED!' . PHP_EOL, __METHOD__, __LINE__, $this->prefix, $shortClassName, $this->suffix); $fileName = sprintf('%s%s%s', $this->prefix, $shortClassName, $this->suffix); // Now look it up in our index //* DEBUG-DIE: */ die(sprintf('[%s:%d]: this->pendingFiles=%s', __METHOD__, __LINE__, print_r($this->pendingFiles, TRUE))); //* NOISY-DEBUG: */ printf('[%s:%d] ISSET: fileName=%s' . PHP_EOL, __METHOD__, __LINE__, $fileName); if ((isset($this->pendingFiles[$fileName])) && (!isset($this->loadedClasses[$this->pendingFiles[$fileName]->getPathname()]))) { // File is found and not loaded so load it only once //* NOISY-DEBUG: */ printf('[%s:%d] LOAD: fileName=%s - START' . PHP_EOL, __METHOD__, __LINE__, $fileName); FrameworkBootstrap::loadInclude($this->pendingFiles[$fileName]); //* NOISY-DEBUG: */ printf('[%s:%d] LOAD: fileName=%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. //* NOISY-DEBUG: */ printf('[%s:%d] LOAD: fileName=%s marked as loaded ...' . PHP_EOL, __METHOD__, __LINE__, $fileName); $this->loadedClasses[$this->pendingFiles[$fileName]->getPathname()] = true; // Developer mode excludes caching (better debugging) //* NOISY-DEBUG: */ printf('[%s:%d] self::developerModeEnabled=%d' . PHP_EOL, __METHOD__, __LINE__, intval(self::$developerModeEnabled)); if (!self::$developerModeEnabled) { // Reset cache and mark file for flushing //* NOISY-DEBUG: */ printf('[%s:%d] Setting this->classesCached=false ...' . PHP_EOL, __METHOD__, __LINE__); $this->classesCached = false; $this->flushCache[$fileName] = $this->pendingFiles[$fileName]; } // Remove it from classes list so it won't be found twice. //* NOISY-DEBUG: */ printf('[%s:%d] UNSET: fileName=%s' . PHP_EOL, __METHOD__, __LINE__, $fileName); unset($this->pendingFiles[$fileName]); } else { // Not found //* NOISY-DEBUG: */ printf('[%s:%d] 404: fileName=%s' . PHP_EOL, __METHOD__, __LINE__, $fileName); } // Trace message //* NOISY-DEBUG: */ printf('[%s:%d] EXIT!' . PHP_EOL, __METHOD__, __LINE__); } }