From 316893ad2c797f9bebda02b0854d5af49e1f6b98 Mon Sep 17 00:00:00 2001 From: Marius Hein Date: Fri, 12 Jul 2013 15:00:59 +0200 Subject: [PATCH] Add new autoloader implementation New namespace implementation created to load application code like forms with this autoloader. Consumpting services can register their own, multiple namespaces. Overlapping namespaces matched by closest name. refs #4407 --- application/forms/TestForm.php | 30 +++ .../Application/ApplicationBootstrap.php | 50 ++++- library/Icinga/Application/Loader.php | 181 ++++++++------- library/Icinga/Application/Modules/Module.php | 207 ++++++++++++++++-- .../monitoring/application/forms/TestForm.php | 30 +++ .../library/Icinga/Application/LoaderTest.php | 156 +++++++++---- 6 files changed, 495 insertions(+), 159 deletions(-) create mode 100644 application/forms/TestForm.php create mode 100644 modules/monitoring/application/forms/TestForm.php diff --git a/application/forms/TestForm.php b/application/forms/TestForm.php new file mode 100644 index 000000000..0015cba25 --- /dev/null +++ b/application/forms/TestForm.php @@ -0,0 +1,30 @@ + + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Form; + +class TestForm +{ +} diff --git a/library/Icinga/Application/ApplicationBootstrap.php b/library/Icinga/Application/ApplicationBootstrap.php index c3d5534ed..12cbf4fac 100755 --- a/library/Icinga/Application/ApplicationBootstrap.php +++ b/library/Icinga/Application/ApplicationBootstrap.php @@ -1,10 +1,28 @@ + * @author Icinga Development Team */ +// {{{ICINGA_LICENSE_HEADER}}} + namespace Icinga\Application; use Icinga\Application\Modules\Manager as ModuleManager; @@ -54,26 +72,25 @@ abstract class ApplicationBootstrap * Constructor * * The constructor is protected to avoid incorrect usage - * - * @return void */ protected function __construct($configDir) { $this->checkPrerequisites(); - $this->libdir = realpath(dirname(dirname(dirname(__FILE__)))); - require $this->libdir . '/Icinga/Application/Loader.php'; + $this->libdir = realpath(__DIR__. '/../..'); if (! defined('ICINGA_LIBDIR')) { define('ICINGA_LIBDIR', $this->libdir); } + // TODO: Make appdir configurable for packagers - $this->appdir = realpath(dirname($this->libdir) . '/application'); + $this->appdir = realpath($this->libdir. '/../application'); + if (! defined('ICINGA_APPDIR')) { define('ICINGA_APPDIR', $this->appdir); } - $this->loader = Loader::register(); + $this->registerAutoloader(); $this->registerZendAutoloader(); Benchmark::measure('Bootstrap, autoloader registered'); @@ -102,6 +119,10 @@ abstract class ApplicationBootstrap return $this->moduleManager; } + /** + * Getter for class loader + * @return Loader + */ public function getLoader() { return $this->loader; @@ -160,6 +181,17 @@ abstract class ApplicationBootstrap return $obj; } + public function registerAutoloader() + { + require $this->libdir. '/Icinga/Exception/ProgrammingError.php'; + require $this->libdir. '/Icinga/Application/Loader.php'; + + $this->loader = new Loader(); + $this->loader->registerNamespace('Icinga', $this->libdir. '/Icinga'); + $this->loader->registerNamespace('Icinga\\Form', $this->appdir. '/forms'); + $this->loader->register(); + } + /** * Register the Zend Autoloader * diff --git a/library/Icinga/Application/Loader.php b/library/Icinga/Application/Loader.php index 7b25f7ded..b89bbd0fc 100755 --- a/library/Icinga/Application/Loader.php +++ b/library/Icinga/Application/Loader.php @@ -1,71 +1,73 @@ + * @author Icinga Development Team */ +// {{{ICINGA_LICENSE_HEADER}}} + namespace Icinga\Application; -use Icinga\Application\Log; +use Icinga\Exception\ProgrammingError; -/** - * This class provides a simple Autoloader - * - * It takes care of loading classes in the Icinga namespace. You shouldn't need - * to manually instantiate this class, as bootstrapping code will do so for you. - * - * Usage example: - * - * - * Icinga\Application\Loader::register(); - * - * - * @copyright Copyright (c) 2013 Icinga-Web Team - * @author Icinga-Web Team - * @package Icinga\Application - * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License - */ class Loader { - const NS = '\\'; - protected $moduleDirs = array(); - - private static $instance; + /** + * Namespace separator + */ + const NAMESPACE_SEPARATOR = '\\'; /** - * Register the Icinga autoloader - * - * You could also call getInstance(), this alias function is here to make - * code look better - * - * @return self + * List of namespaces + * @var array */ - public static function register() + private $namespaces = array(); + + public function __destruct() { - return self::getInstance(); + $this->unRegister(); } /** - * Singleton - * - * Registers the Icinga autoloader if not already been done - * - * @return self + * Register new namespace for directory + * @param string $namespace + * @param string $directory + * @throws \Icinga\Exception\ProgrammingError */ - public static function getInstance() + public function registerNamespace($namespace, $directory) { - if (self::$instance === null) { - self::$instance = new Loader(); - self::$instance->registerAutoloader(); + if (!is_dir($directory)) { + throw new ProgrammingError('Directory does not exist: '. $directory); } - return self::$instance; + + $this->namespaces[$namespace] = $directory; } - public function addModule($name, $dir) + /** + * Test if a namespace exists + * @param string $namespace + * @return bool + */ + public function hasNamespace($namespace) { - $this->moduleDirs[ucfirst($name)] = $dir; - return $this; + return array_key_exists($namespace, $this->namespaces); } /** @@ -73,59 +75,72 @@ class Loader * * Ignores all but classes in the Icinga namespace. * + * @param string $class * @return boolean */ public function loadClass($class) { - if (strpos($class, 'Icinga' . self::NS) === false) { - return false; - } - $file = str_replace(self::NS, '/', $class) . '.php'; - $file = ICINGA_LIBDIR . '/' . $file; - if (! @is_file($file)) { - $parts = preg_split('~\\\~', $class); - array_shift($parts); - $module = $parts[0]; - if (array_key_exists($module, $this->moduleDirs)) { - $file = $this->moduleDirs[$module] - . '/' - . implode('/', $parts) . '.php'; - if (@is_file($file)) { - require_once $file; - return true; - } + $namespace = $this->getNamespaceForClass($class); + + if ($namespace) { + $file = $this->namespaces[$namespace] + . '/' + . preg_replace('/^'. preg_quote($namespace). '/', '', $class); + + $file = (str_replace(self::NAMESPACE_SEPARATOR, '/', $file). '.php'); + if (@file_exists($file)) { + require_once $file; + return true; } - // Log::debug('File ' . $file . ' not found'); - return false; } - require_once $file; - return true; + + return false; + } + + /** + * Test if we have a registered namespaces for this class + * + * Return is the longest match in the array found + * + * @param string $className + * @return bool|string + */ + private function getNamespaceForClass($className) + { + $testNamespace = ''; + $testLength = 0; + + foreach ($this->namespaces as $namespace => $directory) { + $stub = preg_replace('/^'. preg_quote($namespace). '/', '', $className); + $length = strlen($className) - strlen($stub); + if ($length > $testLength) { + $testLength = $length; + $testNamespace = $namespace; + } + } + + if ($testLength > 0) { + return $testNamespace; + } + + return false; } /** * Effectively registers the autoloader the PHP/SPL way - * - * @return void */ - protected function registerAutoloader() + public function register() { - // Not adding ourselves to include_path right now, MAY be faster - /*set_include_path(implode(PATH_SEPARATOR, array( - realpath(dirname(dirname(__FILE__))), - get_include_path(), - )));*/ - spl_autoload_register(array($this, 'loadClass')); + // Think about to add class pathes to php include path + // this could be faster (tg) + spl_autoload_register(array(&$this, 'loadClass')); } /** - * Constructor - * - * Singleton usage is enforced, you are also not allowed to overwrite this - * function - * - * @return void + * Detach autoloader from spl registration */ - final private function __construct() + public function unRegister() { + spl_autoload_unregister(array(&$this, 'loadClass')); } } diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php index 878345b92..e95e4d805 100644 --- a/library/Icinga/Application/Modules/Module.php +++ b/library/Icinga/Application/Modules/Module.php @@ -1,39 +1,112 @@ + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} namespace Icinga\Application\Modules; use Icinga\Application\ApplicationBootstrap; -use Icinga\Application\Icinga; +use Icinga\Application\Config; use Icinga\Web\Hook; use Zend_Controller_Router_Route as Route; class Module { - protected $name; - protected $basedir; - protected $cssdir; - protected $libdir; - protected $localedir; - protected $controllerdir; - protected $registerscript; - protected $app; + /** + * Module name + * @var string + */ + private $name; + + /** + * Base directory of module + * @var string + */ + private $basedir; + + /** + * Directory for styles + * @var string + */ + private $cssdir; + + /** + * Library directory + * @var string + */ + private $libdir; + + /** + * Directory containing translations + * @var string + */ + private $localedir; + + /** + * Directory where controllers reside + * @var string + */ + private $controllerdir; + + /** + * Directory containing form implementations + * @var string + */ + private $formdir; + + /** + * Module bootstrapping script + * @var string + */ + private $registerscript; + + /** + * Icinga application + * @var \Icinga\Application\ApplicationBootstrap + */ + private $app; public function __construct(ApplicationBootstrap $app, $name, $basedir) { $this->app = $app; $this->name = $name; $this->basedir = $basedir; - $this->cssdir = $basedir . '/public/css'; - $this->libdir = $basedir . '/library'; - $this->configdir = $basedir . '/config'; - $this->localedir = $basedir . '/application/locale'; - $this->controllerdir = $basedir . '/application/controllers'; - $this->registerscript = $basedir . '/register.php'; + $this->cssdir = $basedir. '/public/css'; + $this->libdir = $basedir. '/library'; + $this->configdir = $basedir. '/config'; + $this->localedir = $basedir. '/application/locale'; + $this->formdir = $basedir. '/application/forms'; + $this->controllerdir = $basedir. '/application/controllers'; + $this->registerscript = $basedir. '/register.php'; } + /** + * Register module + * @return bool + */ public function register() { - $this->registerLibrary() + $this->registerAutoloader() ->registerWebIntegration() ->runRegisterScript(); return true; @@ -56,26 +129,73 @@ class Module return $manager->getModule($name); } + /** + * Test if module provide css + * @return bool + */ public function hasCss() { return file_exists($this->getCssFilename()); } + /** + * Getter for module name + */ + public function getName() + { + return $this->name; + } + + /** + * Getter for css file name + * @return string + */ public function getCssFilename() { return $this->cssdir . '/module.less'; } + /** + * Getter for base directory + * @return string + */ public function getBaseDir() { return $this->basedir; } + /** + * Getter for library directory + * @return string + */ + public function getLibDir() + { + return $this->libdir; + } + + /** + * Getter for configuration directory + * @return string + */ public function getConfigDir() { return $this->configdir; } + /** + * Getter for form directory + * @return string + */ + public function getFormDir() + { + return $this->formdir; + } + + /** + * Getter for module config object + * @param null|string $file + * @return Config + */ public function getConfig($file = null) { return $this->app @@ -83,14 +203,31 @@ class Module ->module($this->name, $file); } - protected function registerLibrary() + /** + * Register new namespaces on the autoloader + * @return Module + */ + protected function registerAutoloader() { - if (file_exists($this->libdir) && is_dir($this->libdir)) { - $this->app->getLoader()->addModule($this->name, $this->libdir); + if (is_dir($this->getBaseDir()) && is_dir($this->getLibDir())) { + $moduleName = ucfirst($this->getName()); + $moduleLibraryDir = $this->getLibDir(). '/'. $moduleName; + + $this->app->getLoader()->registerNamespace($moduleName, $moduleLibraryDir); + + // TODO(mh): Old namespace for convenience (#4409). Should be removed immediately + $this->app->getLoader()->registerNamespace('Icinga\\'. $moduleName, $moduleLibraryDir); + + $this->app->getLoader()->registerNamespace($moduleName. '\\Form', $this->getFormDir()); } + return $this; } + /** + * Bind text domain for i18n + * @return Module + */ protected function registerLocales() { if (file_exists($this->localedir) && is_dir($this->localedir)) { @@ -99,9 +236,16 @@ class Module return $this; } + /** + * Register web integration + * + * Add controller directory to mvc + * + * @return Module + */ protected function registerWebIntegration() { - if (! $this->app->isWeb()) { + if (!$this->app->isWeb()) { return $this; } @@ -118,6 +262,10 @@ class Module return $this; } + /** + * Register menu entries + * @return Module + */ protected function registerMenuEntries() { $cfg = $this->app @@ -130,6 +278,10 @@ class Module return $this; } + /** + * Register routes for web access + * @return Module + */ protected function registerRoutes() { $this->app->frontController()->getRouter()->addRoute( @@ -157,6 +309,10 @@ class Module return $this; } + /** + * Run module bootstrap script + * @return Module + */ protected function runRegisterScript() { if (file_exists($this->registerscript) @@ -166,9 +322,16 @@ class Module return $this; } - protected function registerHook($name, $class) + /** + * Register hook + * @param string $name + * @param string $class + * @param string $key + * @return Module + */ + protected function registerHook($name, $key, $class) { - Hook::register($name, $class); + Hook::register($name, $key, $class); return $this; } } diff --git a/modules/monitoring/application/forms/TestForm.php b/modules/monitoring/application/forms/TestForm.php new file mode 100644 index 000000000..feff5653b --- /dev/null +++ b/modules/monitoring/application/forms/TestForm.php @@ -0,0 +1,30 @@ + + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Monitoring\Form; + +class TestForm +{ +} diff --git a/test/php/library/Icinga/Application/LoaderTest.php b/test/php/library/Icinga/Application/LoaderTest.php index 867efbc12..2f81ec8fd 100644 --- a/test/php/library/Icinga/Application/LoaderTest.php +++ b/test/php/library/Icinga/Application/LoaderTest.php @@ -1,6 +1,12 @@ markTestIncomplete('testSetModuleDir is not implemented yet'); + return true; + } +} + +EOD; + + protected function setUp() + { + $tempDir = sys_get_temp_dir(); + $this->baseDir = tempnam($tempDir, 'icinga2-web'); + system('mkdir -p '. $this->baseDir. dirname(self::$classFile)); + file_put_contents($this->baseDir. self::$classFile, self::$classContent); + } + + protected function tearDown() + { + system('rm -rf '. $this->baseDir); + } + + + public function testObjectCreation1() + { + $loader = new Loader(); + $loader->register(); + + $check = false; + foreach (spl_autoload_functions() as $functions) { + if (is_array($functions) && $functions[0] === $loader) { + if ($functions[1] === 'loadClass') { + $check = true; + } + } + } + $this->assertTrue($check); + + $loader->unRegister(); + + $check = true; + foreach (spl_autoload_functions() as $functions) { + if (is_array($functions) && $functions[0] === $loader) { + if ($functions[1] === 'loadClass') { + $check = false; + } + } + } + $this->assertTrue($check); + } + + public function testNamespaces() + { + $loader = new Loader(); + $loader->registerNamespace('Test\\Laola', '/tmp'); + $loader->registerNamespace('Dings\\Var', '/var/tmp'); + + $this->assertTrue($loader->hasNamespace('Dings\\Var')); + $this->assertTrue($loader->hasNamespace('Test\\Laola')); + } + + /** + * Important: Test must run before testClassLoad + * + * Because testClassLoads loads the real code into stack + */ + public function testLoadInterface() + { + $classFile = $this->baseDir. self::$classFile; + $this->assertFileExists($classFile); + + $loader = new Loader(); + $loader->registerNamespace('My\\Library', dirname($classFile)); + $this->assertFalse($loader->loadClass('DOES\\NOT\\EXISTS')); + $this->assertTrue($loader->loadClass('My\\Library\\TestStruct')); + } + + public function testClassLoad() + { + $classFile = $this->baseDir. self::$classFile; + $this->assertFileExists($classFile); + + $loader = new Loader(); + $loader->registerNamespace('My\\Library', dirname($classFile)); + $loader->register(); + + $o = new \My\Library\TestStruct(); + $this->assertTrue($o->testFlag()); + } + + /** + * @expectedException Icinga\Exception\ProgrammingError + * @expectedExceptionMessage Directory does not exist: /trullalla/123 + */ + public function testNonexistingDirectory() + { + $loader = new Loader(); + $loader->registerNamespace('My\\Library', '/trullalla/123'); } - - /** - * Test for Loader::AddModule() - * - **/ - public function testAddModule() - { - $this->markTestIncomplete('testAddModule is not implemented yet'); - } - - /** - * Test for Loader::LoadClass() - * - **/ - public function testLoadClass() - { - $this->markTestIncomplete('testLoadClass is not implemented yet'); - } - - /** - * Test for Loader::Register() - * Note: This method is static! - * - **/ - public function testRegister() - { - $this->markTestIncomplete('testRegister is not implemented yet'); - } - - /** - * Test for Loader::GetInstance() - * Note: This method is static! - * - **/ - public function testGetInstance() - { - $this->markTestIncomplete('testGetInstance is not implemented yet'); - } - }