From 77b7ed58d745b2a404d2c6f45600449626f57130 Mon Sep 17 00:00:00 2001 From: Chuck Hagenbuch Date: Fri, 4 Jun 2010 15:37:13 -0400 Subject: [PATCH] New Autoloader that uses modular ClassPathMappers to map classes to filenames, rather than hardcoding a lot of logic into the main Autoloader class. Also allows more flexibility since mappers can have any logic they need in them. --- framework/Autoloader/lib/Horde/Autoloader.php | 225 ++++----------------- .../lib/Horde/Autoloader/ClassPathMapper.php | 14 ++ .../Horde/Autoloader/ClassPathMapper/Default.php | 35 ++++ .../Horde/Autoloader/ClassPathMapper/Prefix.php | 35 ++++ framework/Autoloader/package.xml | 12 ++ .../Autoloader/test/Horde/Autoloader/AllTests.php | 31 +++ .../test/Horde/Autoloader/AutoloaderTest.php | 122 +++++++++++ .../Autoloader/ClassPathMapper/DefaultTest.php | 39 ++++ .../Autoloader/ClassPathMapper/PrefixTest.php | 34 ++++ 9 files changed, 364 insertions(+), 183 deletions(-) create mode 100644 framework/Autoloader/lib/Horde/Autoloader/ClassPathMapper.php create mode 100644 framework/Autoloader/lib/Horde/Autoloader/ClassPathMapper/Default.php create mode 100644 framework/Autoloader/lib/Horde/Autoloader/ClassPathMapper/Prefix.php create mode 100644 framework/Autoloader/test/Horde/Autoloader/AllTests.php create mode 100644 framework/Autoloader/test/Horde/Autoloader/AutoloaderTest.php create mode 100644 framework/Autoloader/test/Horde/Autoloader/ClassPathMapper/DefaultTest.php create mode 100644 framework/Autoloader/test/Horde/Autoloader/ClassPathMapper/PrefixTest.php diff --git a/framework/Autoloader/lib/Horde/Autoloader.php b/framework/Autoloader/lib/Horde/Autoloader.php index 08e42cc78..8eda39c39 100644 --- a/framework/Autoloader/lib/Horde/Autoloader.php +++ b/framework/Autoloader/lib/Horde/Autoloader.php @@ -1,224 +1,83 @@ + * @author Chuck Hagenbuch * @category Horde * @package Horde_Autoloader - * @license http://www.gnu.org/copyleft/lesser.html */ class Horde_Autoloader { - /** - * Patterns that match classes we can load. - * - * @var array - */ - protected static $_classPatterns = array(); - - /** - * The include path cache. - * - * @var array - */ - protected static $_includeCache = null; - - /** - * Callbacks. - * - * @var array - */ - protected static $_callbacks = array(); + private $_mappers = array(); + private $_callbacks = array(); - /** - * Autoload implementation automatically registered with - * spl_autoload_register. - * - * We ignore E_WARNINGS when trying to include files so that if our - * autoloader doesn't find a file, we pass on to the next autoloader (if - * any) or to the PHP class not found error. We don't want to suppress all - * errors, though, or else we'll end up silencing parse errors or - * redefined class name errors, making debugging especially difficult. - * - * @param string $class Class name to load (or interface). - */ - public static function loadClass($class) + public function loadClass($className) { - /* Search in class patterns first. */ - foreach (self::$_classPatterns as $classPattern) { - list($pattern, $replace) = $classPattern; - - if (!is_null($replace) && - preg_match($pattern, $class, $matches, PREG_OFFSET_CAPTURE)) { - if (strcasecmp($matches[0][0], $class) === 0) { - $relativePath = $replace . '/' . $class; - } else { - $relativePath = str_replace(array('\\', '_'), '/', substr($class, 0, $matches[0][1])) . - $replace . - str_replace(array('\\', '_'), '/', substr($class, $matches[0][1] + strlen($matches[0][0]))); - } - - if (self::_loadClass($class, $relativePath)) { - return true; + if ($path = $this->mapToPath($className)) { + if ($this->_include($path)) { + $className = strtolower($className); + if (isset($this->_callbacks[$className])) { + call_user_func($this->_callbacks[$className]); } + return true; } } - /* Do a final search in the include path. */ - $relativePath = str_replace(array('\\', '_'), '/', $class); - return self::_loadClass($class, $relativePath); + return false; } - /** - * Try to load the class from each path in the include_path. - * - * @param string $class The loaded classname. - * @param string $relativePath TODO - */ - protected static function _loadClass($class, $relativePath) + public function addClassPathMapper(Horde_Autoloader_ClassPathMapper $mapper) { - $result = false; - - if (substr($relativePath, 0, 1) == '/') { - $result = self::_loadClassUsingAbsolutePath($relativePath); - } else { - if (is_null(self::$_includeCache)) { - self::_cacheIncludePath(); - } - - foreach (self::$_includeCache as $includePath) { - if (self::_loadClassUsingAbsolutePath($includePath . DIRECTORY_SEPARATOR . $relativePath)) { - $result = true; - break; - } - } - } - - if (!$result) { - return false; - } - - $class = strtolower($class); - if (isset(self::$_callbacks[$class])) { - call_user_func(self::$_callbacks[$class]); - } - - return true; + array_unshift($this->_mappers, $mapper); + return $this; } /** - * Include a PHP file using an absolute path, suppressing E_WARNING and - * E_DEPRECATED errors, but letting fatal/parse errors come through. - * - * @param string $absolutePath TODO + * Add a callback to run when a class is loaded through loadClass(). * - * @TODO Remove E_DEPRECATED masking + * @param string $class The classname. + * @param mixed $callback The callback to run when the class is loaded. */ - protected static function _loadClassUsingAbsolutePath($absolutePath) + public function addCallback($class, $callback) { - $err_mask = E_ALL ^ E_WARNING; - if (defined('E_DEPRECATED')) { - $err_mask = $err_mask ^ E_DEPRECATED; - } - $oldErrorReporting = error_reporting($err_mask); - $included = include $absolutePath . '.php'; - error_reporting($oldErrorReporting); - return $included; + $this->_callbacks[strtolower($class)] = $callback; } - /** - * Add a new path to the include_path we're loading from. - * - * @param string $path The directory to add. - * @param boolean $prepend Add to the beginning of the stack? - * - * @return string The new include_path. - */ - public static function addClassPath($path, $prepend = true) + public function registerAutoloader() { - if ($path == '.') { - throw new Exception('"." is not allowed in the include_path. Use an absolute path instead.'); - } - $path = realpath($path); - - if (is_null(self::$_includeCache)) { - self::_cacheIncludePath(); + // Register the autoloader in a way to play well with as many + // configurations as possible. + spl_autoload_register(array($this, 'loadClass')); + if (function_exists('__autoload')) { + spl_autoload_register('__autoload'); } - - if (in_array($path, self::$_includeCache)) { - // The path is already present in our stack; don't re-add it. - return implode(PATH_SEPARATOR, self::$_includeCache); - } - - if ($prepend) { - array_unshift(self::$_includeCache, $path); - } else { - self::$_includeCache[] = $path; - } - - $include_path = implode(PATH_SEPARATOR, self::$_includeCache); - set_include_path($include_path); - - return $include_path; } /** - * Add a new class pattern. - * - * @param string $pattern The class pattern to add. - * @param string $replace The substitution pattern. All '_' and '\' - * strings in a classname will be converted to - * directory separators. If the entire pattern - * is matched, the matched text will be appended - * to the replacement string (allows for a single - * base class file to live within the include - * directory). + * Search registered mappers in LIFO order. */ - public static function addClassPattern($pattern, $replace = null) + public function mapToPath($className) { - if (strlen($replace)) { - $replace = rtrim($replace, '/') . '/'; + foreach ($this->_mappers as $mapper) { + if ($path = $mapper->mapToPath($className)) { + if ($this->_fileExists($path)) { + return $path; + } + } } - self::$_classPatterns[] = array($pattern, $replace); } - /** - * Add a callback to run when a class is loaded through loadClass(). - * - * @param string $class The classname. - * @param mixed $callback The callback to run when the class is loaded. - */ - public static function addCallback($class, $callback) + protected function _include($path) { - self::$_callbacks[strtolower($class)] = $callback; + return (bool)include $path; } - /** - * TODO - */ - protected static function _cacheIncludePath() - { - self::$_includeCache = array(); - foreach (explode(PATH_SEPARATOR, get_include_path()) as $path) { - if ($path == '.') { continue; } - $path = realpath($path); - if ($path) { - self::$_includeCache[] = $path; - } - } - } - -} - -/* Register the autoloader in a way to play well with as many configurations - * as possible. */ -if (function_exists('spl_autoload_register')) { - spl_autoload_register(array('Horde_Autoloader', 'loadClass')); - if (function_exists('__autoload')) { - spl_autoload_register('__autoload'); - } -} elseif (!function_exists('__autoload')) { - function __autoload($class) + protected function _fileExists($path) { - return Horde_Autoloader::loadClass($class); + return file_exists($path); } } diff --git a/framework/Autoloader/lib/Horde/Autoloader/ClassPathMapper.php b/framework/Autoloader/lib/Horde/Autoloader/ClassPathMapper.php new file mode 100644 index 000000000..b5735e4a0 --- /dev/null +++ b/framework/Autoloader/lib/Horde/Autoloader/ClassPathMapper.php @@ -0,0 +1,14 @@ + + * @category Horde + * @package Horde_Autoloader + */ +interface Horde_Autoloader_ClassPathMapper +{ + public function mapToPath($className); +} diff --git a/framework/Autoloader/lib/Horde/Autoloader/ClassPathMapper/Default.php b/framework/Autoloader/lib/Horde/Autoloader/ClassPathMapper/Default.php new file mode 100644 index 000000000..4003f1dd8 --- /dev/null +++ b/framework/Autoloader/lib/Horde/Autoloader/ClassPathMapper/Default.php @@ -0,0 +1,35 @@ + /path/to/project/lib/vendor/Doctrine/Common/IsolatedClassLoader.php + * \namespace\package\Class_Name => /path/to/project/lib/vendor/namespace/package/Class/Name.php + * \namespace\package_name\Class_Name => /path/to/project/lib/vendor/namespace/package_name/Class/Name.php + * + * @author Chuck Hagenbuch + * @category Horde + * @package Horde_Autoloader + */ +class Horde_Autoloader_ClassPathMapper_Default implements Horde_Autoloader_ClassPathMapper +{ + private $_includePath; + + public function __construct($includePath) + { + $this->_includePath = $includePath; + } + + public function mapToPath($className) + { + // @FIXME: Follow reference implementation + $relativePath = str_replace(array('\\', '_'), DIRECTORY_SEPARATOR, $className) . '.php'; + return $this->_includePath . DIRECTORY_SEPARATOR . $relativePath; + } +} diff --git a/framework/Autoloader/lib/Horde/Autoloader/ClassPathMapper/Prefix.php b/framework/Autoloader/lib/Horde/Autoloader/ClassPathMapper/Prefix.php new file mode 100644 index 000000000..7cd1ab5ee --- /dev/null +++ b/framework/Autoloader/lib/Horde/Autoloader/ClassPathMapper/Prefix.php @@ -0,0 +1,35 @@ + + * @category Horde + * @package Horde_Autoloader + */ +class Horde_Autoloader_ClassPathMapper_Prefix implements Horde_Autoloader_ClassPathMapper +{ + private $_pattern; + private $_includePath; + + public function __construct($pattern, $includePath) + { + $this->_pattern = $pattern; + $this->_includePath = $includePath; + } + + public function mapToPath($className) + { + if (preg_match($this->_pattern, $className, $matches, PREG_OFFSET_CAPTURE)) { + if (strcasecmp($matches[0][0], $className) === 0) { + return "$this->_includePath/$className.php"; + } else { + return str_replace(array('\\', '_'), '/', substr($className, 0, $matches[0][1])) . + $this->_includePath . '/' . + str_replace(array('\\', '_'), '/', substr($className, $matches[0][1] + strlen($matches[0][0]))) . + '.php'; + } + } + + return false; + } +} diff --git a/framework/Autoloader/package.xml b/framework/Autoloader/package.xml index 8b4b0c90e..66c12e01f 100644 --- a/framework/Autoloader/package.xml +++ b/framework/Autoloader/package.xml @@ -40,6 +40,14 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + + + @@ -56,6 +64,10 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + diff --git a/framework/Autoloader/test/Horde/Autoloader/AllTests.php b/framework/Autoloader/test/Horde/Autoloader/AllTests.php new file mode 100644 index 000000000..20af76db3 --- /dev/null +++ b/framework/Autoloader/test/Horde/Autoloader/AllTests.php @@ -0,0 +1,31 @@ +_autoloader = new Horde_Autoloader_TestHarness(); + } + + public function testInitialStateShouldYeildNoMatches() + { + $this->assertNull($this->_autoloader->mapToPath('The_Class_Name')); + } + + public function testInitialStateShouldNotLoadAnyFiles() + { + $this->assertFalse($this->_autoloader->loadClass('The_Class_Name')); + } + + public function testShouldNotMapClassIfMapperDoesNotReturnAPath() + { + $this->_autoloader->addClassPathMapper($this->_getUnsuccessfulMapperMock()); + $this->assertNull($this->_autoloader->mapToPath('The_Class_Name')); + } + + public function testShouldLoadPathMapperDoesNotReturnAPath() + { + $this->_autoloader->addClassPathMapper($this->_getUnsuccessfulMapperMock()); + $this->assertFalse($this->_autoloader->loadClass('The_Class_Name')); + } + + public function testShouldMapClassIfAMapperReturnsAPath() + { + // trick the autoloader into thinking the returned path exists + $this->_autoloader->setFileExistsResponse(true); + + $this->_autoloader->addClassPathMapper($this->_getSuccessfulMapperMock()); + + $this->assertEquals('The/Class/Name.php', $this->_autoloader->mapToPath('The_Class_Name')); + } + + public function testShouldNotMapClassIfAMapperReturnsAPathThatDoesNotExist() + { + // trick the autoloader into thinking the returned path does not exist + $this->_autoloader->setFileExistsResponse(false); + + $this->_autoloader->addClassPathMapper($this->_getSuccessfulMapperMock()); + + $this->assertNull($this->_autoloader->mapToPath('The_Class_Name')); + } + + public function testShouldLoadFileIfMapperReturnsAValidPath() + { + // trick the autoloader into thinking the returned path exists and was included + $this->_autoloader->setFileExistsResponse(true); + $this->_autoloader->setIncludeResponse(true); + + $this->_autoloader->addClassPathMapper($this->_getSuccessfulMapperMock()); + + $this->assertTrue($this->_autoloader->loadClass('The_Class_Name')); + } + + public function testShouldLoadFileIfMapperReturnsAValidPathButIncludingItFails() + { + // trick the autoloader into thinking the returned path exists and was included + $this->_autoloader->setFileExistsResponse(true); + $this->_autoloader->setIncludeResponse(false); + + $this->_autoloader->addClassPathMapper($this->_getSuccessfulMapperMock()); + + $this->assertFalse($this->_autoloader->loadClass('The_Class_Name')); + } + + private function _getSuccessfulMapperMock() + { + $mapper = $this->getMock('Horde_Autoloader_ClassPathMapper', array('mapToPath')); + $mapper->expects($this->once()) + ->method('mapToPath') + ->with($this->equalTo('The_Class_Name')) + ->will($this->returnValue('The/Class/Name.php')); + + return $mapper; + } + + private function _getUnsuccessfulMapperMock() + { + $mapper = $this->getMock('Horde_Autoloader_ClassPathMapper', array('mapToPath')); + $mapper->expects($this->once()) + ->method('mapToPath') + ->with($this->equalTo('The_Class_Name')) + ->will($this->returnValue(null)); + + return $mapper; + } +} + +class Horde_Autoloader_TestHarness extends Horde_Autoloader +{ + private $_includeResponse; + private $_fileExistsResponse; + + public function setIncludeResponse($bool) + { + $this->_includeResponse = $bool; + } + + public function setFileExistsResponse($bool) + { + $this->_fileExistsResponse = $bool; + } + + protected function _include($path) + { + return $this->_includeResponse; + } + + protected function _fileExists($path) + { + return $this->_fileExistsResponse; + } +} diff --git a/framework/Autoloader/test/Horde/Autoloader/ClassPathMapper/DefaultTest.php b/framework/Autoloader/test/Horde/Autoloader/ClassPathMapper/DefaultTest.php new file mode 100644 index 000000000..6be850ccd --- /dev/null +++ b/framework/Autoloader/test/Horde/Autoloader/ClassPathMapper/DefaultTest.php @@ -0,0 +1,39 @@ +_mapper = new Horde_Autoloader_ClassPathMapper_Default('dir'); + } + + public function providerClassNames() + { + return array( + array('Module_Action_Suffix', 'dir/Module/Action/Suffix.php'), + array('MyModule_Action_Suffix', 'dir/MyModule/Action/Suffix.php'), + array('Module_MyAction_Suffix', 'dir/Module/MyAction/Suffix.php'), + array('MyModule_MyAction_Suffix', 'dir/MyModule/MyAction/Suffix.php'), + array('Module\Action\Suffix', 'dir/Module/Action/Suffix.php'), + array('MyModule\Action\Suffix', 'dir/MyModule/Action/Suffix.php'), + array('Module\MyAction\Suffix', 'dir/Module/MyAction/Suffix.php'), + array('MyModule\MyAction\Suffix', 'dir/MyModule/MyAction/Suffix.php'), + ); + } + + /** + * @dataProvider providerClassNames + */ + public function testShouldMapClassToPath($className, $classPath) + { + $this->assertEquals( + $classPath, + $this->_mapper->mapToPath($className) + ); + } +} diff --git a/framework/Autoloader/test/Horde/Autoloader/ClassPathMapper/PrefixTest.php b/framework/Autoloader/test/Horde/Autoloader/ClassPathMapper/PrefixTest.php new file mode 100644 index 000000000..f3f6eb703 --- /dev/null +++ b/framework/Autoloader/test/Horde/Autoloader/ClassPathMapper/PrefixTest.php @@ -0,0 +1,34 @@ +_mapper = new Horde_Autoloader_ClassPathMapper_Prefix('/^App(?:$|_)/i', 'dir'); + } + + public function providerClassNames() + { + return array( + array('App', 'dir/App.php'), + array('App_Foo', 'dir/Foo.php'), + array('App_Foo_Bar', 'dir/Foo/Bar.php'), + ); + } + + /** + * @dataProvider providerClassNames + */ + public function testShouldMapClassToPath($className, $classPath) + { + $this->assertEquals( + $classPath, + $this->_mapper->mapToPath($className) + ); + } +} -- 2.11.0