wire = $wire; spl_autoload_register(array($this, 'loadClass')); } /** * Normalize a path * * @param string $path * @return string * @since 3.0.152 * */ protected function path($path) { if(DIRECTORY_SEPARATOR !== '/') $path = str_replace(DIRECTORY_SEPARATOR, '/', $path); return rtrim($path, '/') . '/'; } /** * Add a recognized file extension for PHP files * * Note: ".php" is already assumed, so does not need to be added. * * #pw-advanced * * @param string $ext * */ public function addExtension($ext) { if(strpos($ext, '.') !== 0) $ext = ".$ext"; if(!in_array($ext, $this->extensions)) $this->extensions[] = $ext; } /** * Map a class suffix to a path * * This is used as a helper/fallback and class is not required to be in given path, * but the path will be added as another to check when not found in namespace path(s). * * @param string $suffix Case sensitive suffix specific to class name (not namespace). * @param string $path * */ public function addSuffix($suffix, $path) { if(!isset($this->suffixes[$suffix])) $this->suffixes[$suffix] = array(); if(!empty($path) && is_dir($path)) $this->suffixes[$suffix][] = $this->path($path); } /** * Map a class prefix to a path * * This is used as a helper/fallback and class is not required to be in given path, * but the path will be added as another to check when not found in namespace path(s). * * @param string $prefix Case sensitive prefix specific to class name (not namespace). * @param string $path * */ public function addPrefix($prefix, $path) { if(!isset($this->prefixes[$prefix])) $this->prefixes[$prefix] = array(); if(!empty($path) && is_dir($path)) $this->prefixes[$prefix][] = $this->path($path); } /** * Add a namespace to point to a path root * * Multiple root paths may be specified for a single namespace by calling this method more than once. * * ~~~~~ * $classLoader->addNamespace('HelloWorld', '/path/to/that/'); * ~~~~~ * * @param string $namespace * @param string $path Full system path * */ public function addNamespace($namespace, $path) { if(!isset(self::$namespaces[$namespace])) self::$namespaces[$namespace] = array(); $path = $this->path($path); if(!in_array($path, self::$namespaces[$namespace])) self::$namespaces[$namespace][] = $path; } /** * Return array of paths for the given namespace, or empty array if none found * * @param string $namespace * @return array of paths or empty array if none found * */ public function getNamespace($namespace) { return isset(self::$namespaces[$namespace]) ? self::$namespaces[$namespace] : array(); } /** * Return true if namespace is defined with paths or false if not * * @param string $namespace * @return bool * */ public function hasNamespace($namespace) { return isset(self::$namespaces[$namespace]); } /** * Remove defined paths (or single path) for given namespace * * @param string $namespace * @param string $path Optionally specify path to remove (default=remove all) * */ public function removeNamespace($namespace, $path = '') { if(strlen($path)) { $key = array_search($path, self::$namespaces[$namespace]); if($key !== false) unset(self::$namespaces[$namespace][$key]); } else { unset(self::$namespaces[$namespace]); } } /** * Find filename for given class name (primarily for API testing/debugging purposes) * * @param string $className Class name with namespace * @return bool|string Returns file on success or boolean false when not found * @since 3.0.152 * */ public function findClassFile($className) { $this->findFile = true; $this->loadClass($className); $file = is_string($this->findFile) ? $this->findFile : false; $this->findFile = false; return $file; } /** * Load the file for the given class * * #pw-advanced * * @param string $className * */ public function loadClass($className) { static $level = 0; static $levelHistory = array(); $level++; if($this->modules === null && $this->wire) { $this->modules = $this->wire->wire('modules'); } if($this->debug === null && $this->wire) { $this->debug = $this->wire->wire('config')->debug; } if($this->debug) { $_className = str_replace(__NAMESPACE__ . '\\', '', $className); $levelHistoryStr = count($levelHistory) ? ' (via ' . implode(' > ', $levelHistory) . ')' : ''; $levelHistory[] = $_className; } else { $levelHistoryStr = ''; $_className = ''; } $found = false; $_parts = array(); if(__NAMESPACE__) { $parts = explode("\\", $className); $name = array_pop($parts); $namespace = implode("\\", $parts); } else { $_parts = array(); if(strpos($className, "\\") !== false) { $parts = explode("\\", $className); $name = array_pop($parts); $namespace = implode("\\", $parts); } else { $name = $className; $namespace = "\\"; } } $_namespace = $namespace; // original and unmodified namespace if($this->modules && $this->modules->isModule($className)) { if($this->modules->includeModule($name)) { // success, and Modules class just included it if($this->findFile === true) { $this->findFile = $this->modules->getModuleFile($name); } if($this->debug) { $this->debugLog[$_className] = "Handled by modules loader" . $levelHistoryStr; array_pop($levelHistory); } $level--; return; } } while($namespace && !isset(self::$namespaces[$namespace])) { $_parts[] = array_pop($parts); $namespace = implode("\\", $parts); } if($namespace) { $paths = self::$namespaces[$namespace]; $dir = count($_parts) ? implode('/', array_reverse($_parts)) . '/' : ''; $found = $this->findClassInPaths($name, $paths, $dir); } if(!$found && $this->modules && $_namespace) { // if namespace is for a known module, see if we can find a file in that module’s directory // with the same name as the request class // @todo psr-4 support for these $path = $this->modules->getNamespacePath($_namespace); if($path) $found = $this->findClassInPaths($name, $path); } if(!$found && (!empty($this->prefixes) || !empty($this->suffixes))) { $found = $this->findInPrefixSuffixPaths($name); } if($found) { /** @noinspection PhpIncludeInspection */ include_once($found); if($this->debug) { $file = $this->wire ? str_replace($this->wire->wire('config')->paths->root, '/', $found) : $found; $this->debugLog[$_className] = $file . $levelHistoryStr; } if($this->findFile === true && $level === 1) { $this->findFile = $found; } } else if($this->debug) { $this->debugLog[$_className] = "Unable to locate file for this class" . $levelHistoryStr; } $level--; if($this->debug) array_pop($levelHistory); } /** * Find class file among given paths and return full pathname to file if found * * @param string $name Class name without namespace * @param string|array $paths Path(s) to check * @param string $dir Optional directory string to append to each path, must not start with slash but must end with slash, i.e. "dir/" * @return string|bool Returns full path+filename when found or boolean false when not found * @since 3.0.152 * */ protected function findClassInPaths($name, $paths, $dir = '') { $found = false; if(!is_array($paths)) $paths = array($paths); foreach($paths as $path) { foreach($this->extensions as $ext) { $file = "$path$dir$name$ext"; if(!is_file($file)) continue; $found = $file; break; } if($found) break; } return $found; } /** * Check prefix and suffix definition paths for given class name and return file if found * * @param string $name Class name without namespace * @return bool|string Returns filename on success or boolean false if not found * @since 3.0.152 * */ protected function findInPrefixSuffixPaths($name) { $found = false; foreach(array('prefixes', 'suffixes') as $type) { foreach($this->$type as $fix => $paths) { // if class exactly matches prefix/suffix, it is the full class name and not allowed if($name === $fix || empty($fix)) continue; // determine where the prefix/suffix appears in the class name $pos = strpos($name, $fix); // prefix/suffix does not appear in class name if($pos === false) continue; if($type === 'prefixes') { // prefixes: class name must begin with prefix if($pos !== 0) continue; } else { // suffixes: class name must end with suffix if(substr($name, -1 * strlen($fix)) !== $fix) continue; } // if still here then we have a class name that matches a prefix/suffix, check if in path $found = $this->findClassInPaths($name, $paths); if($found) break; } if($found) break; } return $found; } /** * Enable or disable debug mode * * #pw-internal * * @param bool $debug * */ public function setDebug($debug) { $this->debug = (bool) $debug; } /** * Get log of debug events * * #pw-internal * * @return array of strings * */ public function getDebugLog() { return $this->debugLog; } }