From 05b7388a180c2b18d95cf44c3037a0d0896deb28 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Thu, 12 Nov 2015 17:33:45 +0100 Subject: [PATCH] ClassLoader: completely refactored --- library/Icinga/Application/ClassLoader.php | 243 ++++++++++++++++++++- 1 file changed, 231 insertions(+), 12 deletions(-) diff --git a/library/Icinga/Application/ClassLoader.php b/library/Icinga/Application/ClassLoader.php index 3247d3250..28dd633fe 100644 --- a/library/Icinga/Application/ClassLoader.php +++ b/library/Icinga/Application/ClassLoader.php @@ -3,6 +3,8 @@ namespace Icinga\Application; +use Zend_Loader_Autoloader; + /** * PSR-4 class loader */ @@ -13,6 +15,40 @@ class ClassLoader */ const NAMESPACE_SEPARATOR = '\\'; + /** + * Icinga Web 2 module namespace prefix + */ + const MODULE_PREFIX = 'Icinga\\Module\\'; + + /** + * Icinga Web 2 module namespace prefix length + * + * Helps to make substr/strpos operations even faster + */ + const MODULE_PREFIX_LENGTH = 14; + + /** + * A hardcoded class/subdir map for application ns prefixes + * + * When a module registers with an application directory, those + * namespace prefixes (after the module prefix) will be looked up + * in the corresponding application subdirectories + * + * @var array + */ + protected $applicationPrefixes = array( + 'Clicommands' => 'clicommands', + 'Controllers' => 'controllers', + 'Forms' => 'forms' + ); + + /** + * Whether we already instantiated the ZF autoloader + * + * @var boolean + */ + protected $gotZend = false; + /** * Namespaces * @@ -20,18 +56,33 @@ class ClassLoader */ private $namespaces = array(); + /** + * Application directories + * + * @var array + */ + private $applicationDirectories = array(); + /** * Register a base directory for a namespace prefix * + * Application directory is optional and provides additional lookup + * logic for hardcoded namespaces like "Forms" + * * @param string $namespace * @param string $directory + * @param string $appDirectory * * @return $this */ - public function registerNamespace($namespace, $directory) + public function registerNamespace($namespace, $directory, $appDirectory = null) { $this->namespaces[$namespace] = $directory; + if ($appDirectory !== null) { + $this->applicationDirectories[$namespace] = $appDirectory; + } + return $this; } @@ -56,21 +107,177 @@ class ClassLoader */ public function getSourceFile($class) { + if ($file = $this->getModuleSourceFile($class)) { + return $file; + } + foreach ($this->namespaces as $namespace => $dir) { if ($class === strstr($class, $namespace)) { - $classPath = str_replace( - self::NAMESPACE_SEPARATOR, - DIRECTORY_SEPARATOR, - substr($class, strlen($namespace)) - ) . '.php'; - if (file_exists($file = $dir . $classPath)) { - return $file; - } + return $this->buildClassFilename($class, $namespace); } } + return null; } + /** + * Get the source file of the given module class or interface + * + * @param string $class Module class or interface name + * + * @return string|null + */ + protected function getModuleSourceFile($class) + { + if (! $this->classBelongsToModule($class)) { + return null; + } + + $modules = Icinga::app()->getModuleManager(); + $namespace = $this->extractModuleNamespace($class); + + if ($this->hasNamespace($namespace)) { + + return $this->buildClassFilename($class, $namespace); + + } elseif (! $modules->loadedAllEnabledModules()) { + + $moduleName = $this->extractModuleName($class); + + if ($modules->hasEnabled($moduleName)) { + $modules->loadModule($moduleName); + + return $this->buildClassFilename($class, $namespace); + } + } + + return null; + } + + /** + * Extract the Icinga module namespace from a given namespaced class name + * + * Does no validation, prefix must have been checked before + * + * @return string + */ + protected function extractModuleNamespace($class) + { + return substr( + $class, + 0, + strpos($class, self::NAMESPACE_SEPARATOR, self::MODULE_PREFIX_LENGTH + 1) + ); + } + + /** + * Extract the Icinga module name from a given namespaced class name + * + * Does no validation, prefix must have been checked before + * + * @return string + */ + protected function extractModuleName($class) + { + return lcfirst( + substr( + $class, + self::MODULE_PREFIX_LENGTH, + strpos( + $class, + self::NAMESPACE_SEPARATOR, + self::MODULE_PREFIX_LENGTH + 1 + ) - self::MODULE_PREFIX_LENGTH + ) + ); + } + + /** + * Whether the given class name belongs to a module namespace + * + * @return boolean + */ + protected function classBelongsToModule($class) + { + return substr($class, 0, self::MODULE_PREFIX_LENGTH) === self::MODULE_PREFIX; + } + + /** + * Prepare a filename string for the given class + * + * Expects the given namespace to be registered with a path name + * + * @return string + */ + protected function buildClassFilename($class, $namespace) + { + $relNs = substr($class, strlen($namespace) + 1); + + if ($this->namespaceHasApplictionDirectory($namespace)) { + $prefixSeparator = strpos($relNs, self::NAMESPACE_SEPARATOR); + $prefix = substr($relNs, 0, $prefixSeparator); + + if ($this->isApplicationPrefix($prefix)) { + return $this->applicationDirectories[$namespace] + . DIRECTORY_SEPARATOR + . $this->applicationPrefixes[$prefix] + . $this->classToRelativePhpFilename(substr($relNs, $prefixSeparator)); + } + } + + return $this->namespaces[$namespace] . DIRECTORY_SEPARATOR . $this->classToRelativePhpFilename($relNs); + } + + /** + * Return the relative file name for the given (namespaces) class + * + * @param string $class + * + * @return string + */ + protected function classToRelativePhpFilename($class) + { + return str_replace( + self::NAMESPACE_SEPARATOR, + DIRECTORY_SEPARATOR, + $class + ) . '.php'; + } + + /** + * Whether given prefix (Forms, Controllers...) makes part of "application" + * + * @param string $prefix + * + * @return boolean + */ + protected function isApplicationPrefix($prefix) + { + return array_key_exists($prefix, $this->applicationPrefixes); + } + + /** + * Whether the given namespace registered an application directory + * + * @return boolean + */ + protected function namespaceHasApplictionDirectory($namespace) + { + return array_key_exists($namespace, $this->applicationDirectories); + } + + /** + * Require ZF autoloader + * + * @return Zend_Loader_Autoloader + */ + protected function requireZendAutoloader() + { + require_once 'Zend/Loader/Autoloader.php'; + $this->gotZend = true; + return Zend_Loader_Autoloader::getInstance(); + } + /** * Load the given class or interface * @@ -80,10 +287,22 @@ class ClassLoader */ public function loadClass($class) { - if ($file = $this->getSourceFile($class)) { - require $file; - return true; + // We are aware of the Zend_ prefix and lazyload it's autoloader. + // Return as fast as possible if we already did so. + if (substr($class, 0, 5) === 'Zend_') { + if (! $this->gotZend) { + $this->requireZendAutoloader(); + } + return false; } + + if ($file = $this->getSourceFile($class)) { + if (file_exists($file)) { + require $file; + return true; + } + } + return false; }