<?php
/**
- * Horde Autoloader.
+ * Horde_Autoloader
*
+ * Manages an application's class name to file name mapping conventions. One or
+ * more class-to-filename mappers are defined, and are searched in LIFO order.
+ *
+ * @author Bob Mckee <bmckee@bywires.com>
+ * @author Chuck Hagenbuch <chuck@horde.org>
* @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);
}
}
--- /dev/null
+<?php
+/**
+ * Horde_Autoloader_ClassPathMapper
+ *
+ * Interface for class loaders
+ *
+ * @author Bob Mckee <bmckee@bywires.com>
+ * @category Horde
+ * @package Horde_Autoloader
+ */
+interface Horde_Autoloader_ClassPathMapper
+{
+ public function mapToPath($className);
+}
--- /dev/null
+<?php
+/**
+ * Maps classes to paths following the PHP Framework Interop Group PSR-0
+ * reference implementation. Under this guideline, the following rules apply:
+ *
+ * Each namespace separator is converted to a DIRECTORY_SEPARATOR when loading from the file system.
+ * Each "_" character in the CLASS NAME is converted to a DIRECTORY_SEPARATOR. The "_" character has no special meaning in the namespace.
+ * The fully-qualified namespace and class is suffixed with ".php" when loading from the file system.
+ *
+ * Examples:
+ *
+ * \Doctrine\Common\IsolatedClassLoader => /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 <chuck@horde.org>
+ * @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;
+ }
+}
--- /dev/null
+<?php
+/**
+ * Load classes from a specific path matching a specific prefix.
+ *
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ * @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;
+ }
+}
<dir name="lib">
<dir name="Horde">
<file name="Autoloader.php" role="php" />
+ <dir name="Autoloader">
+ <file name="ClassPathMapper.php" role="php" />
+ <dir name="ClassPathMapper">
+ <file name="Application.php" role="php" />
+ <file name="Default.php" role="php" />
+ <file name="Prefix.php" role="php" />
+ </dir> <!-- /lib/Horde/Autoloader/ClassPathMapper -->
+ </dir> <!-- /lib/Horde/Autoloader -->
</dir> <!-- /lib/Horde -->
</dir> <!-- /lib -->
</dir> <!-- / -->
</dependencies>
<phprelease>
<filelist>
+ <install name="lib/Horde/Autoloader/ClassPathMapper/Application.php" as="Horde/Autoloader/ClassPathMapper/Application.php" />
+ <install name="lib/Horde/Autoloader/ClassPathMapper/Default.php" as="Horde/Autoloader/ClassPathMapper/Default.php" />
+ <install name="lib/Horde/Autoloader/ClassPathMapper/Prefix.php" as="Horde/Autoloader/ClassPathMapper/Prefix.php" />
+ <install name="lib/Horde/Autoloader/ClassPathMapper.php" as="Horde/Autoloader/ClassPathMapper.php" />
<install name="lib/Horde/Autoloader.php" as="Horde/Autoloader.php" />
</filelist>
</phprelease>
--- /dev/null
+<?php
+/**
+ * @package Horde_Autoloader
+ * @subpackage UnitTests
+ */
+
+/**
+ * Define the main method
+ */
+if (!defined('PHPUnit_MAIN_METHOD')) {
+ define('PHPUnit_MAIN_METHOD', 'Horde_Autoloader_AllTests::main');
+}
+
+/**
+ * Prepare the test setup.
+ */
+require_once 'Horde/Test/AllTests.php';
+
+/**
+ * @package Horde_Autoloader
+ * @subpackage UnitTests
+ */
+class Horde_Autoloader_AllTests extends Horde_Test_AllTests
+{
+}
+
+Horde_Autoloader_AllTests::init('Horde_Autoloader', __FILE__);
+
+if (PHPUnit_MAIN_METHOD == 'Horde_Autoloader_AllTests::main') {
+ Horde_Autoloader_AllTests::main();
+}
--- /dev/null
+<?php
+class Horde_Autoloader_AutoloaderTest extends PHPUnit_Framework_TestCase
+{
+ private $_autoloader;
+
+ public function setUp()
+ {
+ $this->_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;
+ }
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Autoloader
+ */
+class Horde_Autoloader_ClassPathMapper_DefaultTest extends PHPUnit_Framework_TestCase
+{
+ private $_mapper;
+
+ public function setUp()
+ {
+ $this->_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)
+ );
+ }
+}
--- /dev/null
+<?php
+/**
+ * @category Horde
+ * @package Horde_Autoloader
+ */
+class Horde_Autoloader_ClassPathMapper_PrefixTest extends PHPUnit_Framework_TestCase
+{
+ private $_mapper;
+
+ public function setUp()
+ {
+ $this->_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)
+ );
+ }
+}