From 22c3041e128bd2d10d37672bdf5c7c1eb4112b12 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 25 Jun 2010 14:24:04 +0200 Subject: [PATCH] [feature/auto-loading] Added a phpBB autoloader with caching support. phpBB class name lookups follow these rules: - All classes are prefixed with phpbb_ - All classes reside in includes/ or a subdirectory thereof - Directories must not contain underscores - The class name is separated into parts by underscores, the parts are checked from first to last, until one is found which is not a directory, all remaining parts make up the file name. If no parts are left, the last directory name is used. Examples: directory structure: includes/ class_name.php dir/ class_name.php dir.php subdir/ class_name.php lookups: phpbb_class_name -> includes/class_name.php phpbb_dir_class_name -> includes/dir/class_name.php phpbb_dir -> includes/dir/dir.php phpbb_dir_subdir_class_name -> includes/dir/subdir/class_name.php Optionally the class can be supplied with a cache instance, either in the constructor or via set_cache() at a later time. This allows for the lookups to be cached, so the directories do not have to be traveresed on every request. This makes it necessary for the cache and its dependency to continue to be loaded the old way - without autoloading. The code will not be changed to use autoloading, but it will rather only be used for new classes where applicable. PHPBB3-9682 --- phpBB/includes/class_loader.php | 162 ++++++++++++++++++ tests/all_tests.php | 2 + tests/class_loader/all_tests.php | 41 +++++ tests/class_loader/cache_mock.php | 29 ++++ tests/class_loader/class_loader_test.php | 65 +++++++ tests/class_loader/includes/class_name.php | 6 + .../class_loader/includes/dir/class_name.php | 6 + .../includes/dir/subdir/class_name.php | 6 + 8 files changed, 317 insertions(+) create mode 100644 phpBB/includes/class_loader.php create mode 100644 tests/class_loader/all_tests.php create mode 100644 tests/class_loader/cache_mock.php create mode 100644 tests/class_loader/class_loader_test.php create mode 100644 tests/class_loader/includes/class_name.php create mode 100644 tests/class_loader/includes/dir/class_name.php create mode 100644 tests/class_loader/includes/dir/subdir/class_name.php diff --git a/phpBB/includes/class_loader.php b/phpBB/includes/class_loader.php new file mode 100644 index 0000000000..c70351b437 --- /dev/null +++ b/phpBB/includes/class_loader.php @@ -0,0 +1,162 @@ +phpbb_root_path = $phpbb_root_path; + $this->php_ext = $php_ext; + + $this->set_cache($cache); + } + + /** + * Provide the class loader with a cache to store paths. If set to null, the + * the class loader will resolve paths by checking for the existance of every + * directory in the class name every time. + * + * @param acm $cache An implementation of the phpBB cache interface. + */ + public function set_cache($cache = null) + { + if ($cache) + { + $this->cached_paths = $cache->get('class_loader'); + + if ($this->cached_paths === false) + { + $this->cached_paths = array(); + } + } + + $this->cache = $cache; + } + + /** + * Registers the class loader as an autoloader using SPL. + */ + public function register() + { + spl_autoload_register(array($this, 'load_class')); + } + + /** + * Removes the class loader from the SPL autoloader stack. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'load_class')); + } + + /** + * Resolves a phpBB class name to a relative path which can be included. + * + * @param string $class The class name to resolve, must have a phpbb_ + * prefix + * @return string|bool A relative path to the file containing the + * class or false if looking it up failed. + */ + public function resolve_path($class) + { + $path_prefix = $this->phpbb_root_path . 'includes/'; + + if (isset($this->cached_paths[$class])) + { + return $path_prefix . $this->cached_paths[$class] . $this->php_ext; + } + + if (!preg_match('/phpbb_[a-zA-Z0-9_]+/', $class)) + { + return false; + } + + $parts = explode('_', substr($class, 6)); + + $dirs = ''; + + for ($i = 0; is_dir($path_prefix . $dirs . $parts[$i]) && $i < sizeof($parts); $i++) + { + $dirs .= $parts[$i] . '/'; + } + + // no file name left => use last dir name as file name + if ($i == sizeof($parts)) + { + $parts[] = $parts[$i - 1]; + } + + $relative_path = $dirs . implode(array_slice($parts, $i, sizeof($parts) - $i), '_'); + + if (!file_exists($path_prefix . $relative_path . $this->php_ext)) + { + return false; + } + + if ($this->cache) + { + $this->cached_paths[$class] = $relative_path; + $this->cache->put('class_loader', $this->cached_paths); + } + + return $path_prefix . $relative_path . $this->php_ext; + } + + /** + * Resolves a class name to a path and then includes it. + * + * @param string $class The class name which is being loaded. + */ + public function load_class($class) + { + if (substr($class, 0, 6) === 'phpbb_') + { + $path = $this->resolve_path($class); + + if ($path) + { + require $path; + } + } + } +} diff --git a/tests/all_tests.php b/tests/all_tests.php index 938b17cf26..4eee950860 100644 --- a/tests/all_tests.php +++ b/tests/all_tests.php @@ -15,6 +15,7 @@ if (!defined('PHPUnit_MAIN_METHOD')) require_once 'test_framework/framework.php'; require_once 'PHPUnit/TextUI/TestRunner.php'; +require_once 'class_loader/all_tests.php'; require_once 'utf/all_tests.php'; require_once 'request/all_tests.php'; require_once 'security/all_tests.php'; @@ -38,6 +39,7 @@ class phpbb_all_tests { $suite = new PHPUnit_Framework_TestSuite('phpBB'); + $suite->addTest(phpbb_class_loader_all_tests::suite()); $suite->addTest(phpbb_utf_all_tests::suite()); $suite->addTest(phpbb_request_all_tests::suite()); $suite->addTest(phpbb_security_all_tests::suite()); diff --git a/tests/class_loader/all_tests.php b/tests/class_loader/all_tests.php new file mode 100644 index 0000000000..451a1b02c2 --- /dev/null +++ b/tests/class_loader/all_tests.php @@ -0,0 +1,41 @@ +addTestSuite('phpbb_class_loader_test'); + + return $suite; + } +} + +if (PHPUnit_MAIN_METHOD == 'phpbb_class_loader_all_tests::main') +{ + phpbb_class_loader_all_tests::main(); +} + diff --git a/tests/class_loader/cache_mock.php b/tests/class_loader/cache_mock.php new file mode 100644 index 0000000000..c8069fa9cc --- /dev/null +++ b/tests/class_loader/cache_mock.php @@ -0,0 +1,29 @@ +variables[$var_name])) + { + return $this->variables[$var_name]; + } + + return false; + } + + function put($var_name, $value) + { + $this->variables[$var_name] = $value; + } +} \ No newline at end of file diff --git a/tests/class_loader/class_loader_test.php b/tests/class_loader/class_loader_test.php new file mode 100644 index 0000000000..37c11657c4 --- /dev/null +++ b/tests/class_loader/class_loader_test.php @@ -0,0 +1,65 @@ +assertEquals( + $prefix . 'class_name.php', + $class_loader->resolve_path('phpbb_class_name'), + 'Top level class' + ); + $this->assertEquals( + $prefix . 'dir/class_name.php', + $class_loader->resolve_path('phpbb_dir_class_name'), + 'Class in a directory' + ); + $this->assertEquals( + $prefix . 'dir/subdir/class_name.php', + $class_loader->resolve_path('phpbb_dir_subdir_class_name'), + 'Class in a sub-directory' + ); + } + + public function test_resolve_cached() + { + $cache = new phpbb_cache_mock; + $cache->put('class_loader', array('phpbb_a_cached_name' => 'a/cached_name')); + + $prefix = 'class_loader/'; + $class_loader = new phpbb_class_loader($prefix, '.php', $cache); + + $prefix .= 'includes/'; + + $this->assertEquals( + $prefix . 'dir/class_name.php', + $class_loader->resolve_path('phpbb_dir_class_name'), + 'Class in a directory' + ); + + $this->assertEquals( + $prefix . 'a/cached_name.php', + $class_loader->resolve_path('phpbb_a_cached_name'), + 'Class in a directory' + ); + } +} diff --git a/tests/class_loader/includes/class_name.php b/tests/class_loader/includes/class_name.php new file mode 100644 index 0000000000..e941173cdd --- /dev/null +++ b/tests/class_loader/includes/class_name.php @@ -0,0 +1,6 @@ +