From 64f631c128da52e499431977a851870e919997e8 Mon Sep 17 00:00:00 2001 From: John Okely Date: Mon, 1 Aug 2016 14:50:31 +0800 Subject: [PATCH] MDL-53016 classloader: Load PSR-0 and PSR-4 separately, add unit tests --- lib/classes/component.php | 89 ++++-- lib/tests/component_test.php | 290 ++++++++++++++++++ .../overlap/subnamespace/example.php | 35 +++ .../overlap/subnamespace/example2.php | 33 ++ lib/tests/fixtures/component/psr0/main.php | 33 ++ .../component/psr0/subnamespace/example.php | 33 ++ .../component/psr0/subnamespace/slashes.php | 35 +++ lib/tests/fixtures/component/psr4/main.php | 35 +++ .../component/psr4/subnamespace/example.php | 35 +++ .../psr4/subnamespace/underscore_example.php | 35 +++ 10 files changed, 622 insertions(+), 31 deletions(-) create mode 100644 lib/tests/fixtures/component/overlap/subnamespace/example.php create mode 100644 lib/tests/fixtures/component/overlap/subnamespace/example2.php create mode 100644 lib/tests/fixtures/component/psr0/main.php create mode 100644 lib/tests/fixtures/component/psr0/subnamespace/example.php create mode 100644 lib/tests/fixtures/component/psr0/subnamespace/slashes.php create mode 100644 lib/tests/fixtures/component/psr4/main.php create mode 100644 lib/tests/fixtures/component/psr4/subnamespace/example.php create mode 100644 lib/tests/fixtures/component/psr4/subnamespace/underscore_example.php diff --git a/lib/classes/component.php b/lib/classes/component.php index 6a64155dd3e..519606c2a11 100644 --- a/lib/classes/component.php +++ b/lib/classes/component.php @@ -67,9 +67,12 @@ class core_component { protected static $version = null; /** @var array list of the files to map. */ protected static $filestomap = array('lib.php', 'settings.php'); - /** @var array associative array of PRS-4 and PSR-0 namespaces and corresponding paths. */ - protected static $psrnamespaces = array( - 'Horde' => 'lib/horde/framework/Horde', + /** @var array associative array of PSR-0 namespaces and corresponding paths. */ + protected static $psr0namespaces = array( + 'Horde' => 'lib/horde/framework/Horde' + ); + /** @var array associative array of PRS-4 namespaces and corresponding paths. */ + protected static $psr4namespaces = array( ); /** @@ -109,53 +112,77 @@ class core_component { return; } - if (self::psr_classloader($classname)) { + $file = self::psr_classloader($classname); + // If the file is found, require it. + if (!empty($file)) { + require($file); return; } } /** - * Load a class from our defined PSR-0 or PSR-4 standard namespaces on - * demand. + * Return the path to a class from our defined PSR-0 or PSR-4 standard namespaces on + * demand. Only returns paths to files that exist. * * Adapated from http://www.php-fig.org/psr/psr-4/examples/ and made PSR-0 * compatible. * - * @param string $class the name fo the class. - * @return bool true if class was loaded. + * @param string $class the name of the class. + * @return string|bool The full path to the file defining the class. Or false if it could not be resolved or does not exist. */ protected static function psr_classloader($class) { - global $CFG; - - // Iterate through each namespace prefix. - foreach (self::$psrnamespaces as $prefix => $path) { - // Does the class use the namespace prefix? - $len = strlen($prefix); - if (strncmp($prefix, $class, $len) !== 0) { - // No, move to the next prefix. - continue; + // Iterate through each PSR-4 namespace prefix. + foreach (self::$psr4namespaces as $prefix => $path) { + $file = self::get_class_file($class, $prefix, $path, array('\\')); + if (!empty($file) && file_exists($file)) { + return $file; } + } - $path = $CFG->dirroot . DIRECTORY_SEPARATOR . $path; - - // Get the relative class name. - $relativeclass = substr($class, $len); - - // Replace the namespace prefix with the base directory, replace namespace - // separators or _ with directory separators in the relative class name, append - // with .php. - $file = $path. str_replace(array('\\', '_'), DIRECTORY_SEPARATOR, $relativeclass) . '.php'; - - // If the file exists, require it. - if (file_exists($file)) { - require($file); - return true; + // Iterate through each PSR-0 namespace prefix. + foreach (self::$psr0namespaces as $prefix => $path) { + $file = self::get_class_file($class, $prefix, $path, array('\\', '_')); + if (!empty($file) && file_exists($file)) { + return $file; } } return false; } + /** + * Return the path to the class based on the given namespace prefix and path it corresponds to. + * + * Will return the path even if the file does not exist. Check the file esists before requiring. + * + * @param string $class the name of the class. + * @param string $prefix The namespace prefix used to identify the base directory of the source files. + * @param string $path The relative path to the base directory of the source files. + * @param string[] $separators The characters that should be used for separating. + * @return string|bool The full path to the file defining the class. Or false if it could not be resolved. + */ + protected static function get_class_file($class, $prefix, $path, $separators) { + global $CFG; + + // Does the class use the namespace prefix? + $len = strlen($prefix); + if (strncmp($prefix, $class, $len) !== 0) { + // No, move to the next prefix. + return false; + } + $path = $CFG->dirroot . '/' . $path; + + // Get the relative class name. + $relativeclass = substr($class, $len); + + // Replace the namespace prefix with the base directory, replace namespace + // separators with directory separators in the relative class name, append + // with .php. + $file = $path . str_replace($separators, '/', $relativeclass) . '.php'; + + return $file; + } + /** * Initialise caches, always call before accessing self:: caches. diff --git a/lib/tests/component_test.php b/lib/tests/component_test.php index b3294117219..21750ec483c 100644 --- a/lib/tests/component_test.php +++ b/lib/tests/component_test.php @@ -36,6 +36,25 @@ class core_component_testcase extends advanced_testcase { // always verify that it does not collide with any existing add-on modules and subplugins!!! const SUBSYSTEMCOUNT = 65; + public function setUp() { + $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces'); + $psr0namespaces->setAccessible(true); + $this->oldpsr0namespaces = $psr0namespaces->getValue(null); + + $psr4namespaces = new ReflectionProperty('core_component', 'psr4namespaces'); + $psr4namespaces->setAccessible(true); + $this->oldpsr4namespaces = $psr4namespaces->getValue(null); + } + public function tearDown() { + $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces'); + $psr0namespaces->setAccessible(true); + $psr0namespaces->setValue(null, $this->oldpsr0namespaces); + + $psr4namespaces = new ReflectionProperty('core_component', 'psr4namespaces'); + $psr4namespaces->setAccessible(true); + $psr4namespaces->setValue(null, $this->oldpsr4namespaces); + } + public function test_get_core_subsystems() { global $CFG; @@ -469,4 +488,275 @@ class core_component_testcase extends advanced_testcase { $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', '\\output\\myprofile\\')); $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', '\\output\\myprofile')); } + + /** + * Data provider for classloader test + */ + public function classloader_provider() { + global $CFG; + + // As part of these tests, we Check that there are no unexpected problems with overlapping PSR namespaces. + // This is not in the spec, but may come up in some libraries using both namespaces and PEAR-style class names. + // If problems arise we can remove this test, but will need to add a warning. + // Normalise to forward slash for testing purposes. + $directory = str_replace('\\', '/', $CFG->dirroot) . "/lib/tests/fixtures/component/"; + + $psr0 = [ + 'psr0' => 'lib/tests/fixtures/component/psr0', + 'overlap' => 'lib/tests/fixtures/component/overlap' + ]; + $psr4 = [ + 'psr4' => 'lib/tests/fixtures/component/psr4', + 'overlap' => 'lib/tests/fixtures/component/overlap' + ]; + return [ + 'PSR-0 Classloading - Root' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr0_main', + 'includedfiles' => "{$directory}psr0/main.php", + ], + 'PSR-0 Classloading - Sub namespace - underscores' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr0_subnamespace_example', + 'includedfiles' => "{$directory}psr0/subnamespace/example.php", + ], + 'PSR-0 Classloading - Sub namespace - slashes' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr0\\subnamespace\\slashes', + 'includedfiles' => "{$directory}psr0/subnamespace/slashes.php", + ], + 'PSR-4 Classloading - Root' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr4\\main', + 'includedfiles' => "{$directory}psr4/main.php", + ], + 'PSR-4 Classloading - Sub namespace' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr4\\subnamespace\\example', + 'includedfiles' => "{$directory}psr4/subnamespace/example.php", + ], + 'PSR-4 Classloading - Ensure underscores are not converted to paths' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr4\\subnamespace\\underscore_example', + 'includedfiles' => "{$directory}psr4/subnamespace/underscore_example.php", + ], + 'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'overlap\\subnamespace\\example', + 'includedfiles' => "{$directory}overlap/subnamespace/example.php", + ], + 'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'overlap_subnamespace_example2', + 'includedfiles' => "{$directory}overlap/subnamespace/example2.php", + ], + ]; + } + + /** + * Test the classloader. + * + * @dataProvider classloader_provider + * @param array $psr0 The PSR-0 namespaces to be used in the test. + * @param array $psr4 The PSR-4 namespaces to be used in the test. + * @param string $classname The name of the class to attempt to load. + * @param string $includedfiles The file expected to be loaded. + */ + public function test_classloader($psr0, $psr4, $classname, $includedfiles) { + $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces'); + $psr0namespaces->setAccessible(true); + $psr0namespaces->setValue(null, $psr0); + + $psr4namespaces = new ReflectionProperty('core_component', 'psr4namespaces'); + $psr4namespaces->setAccessible(true); + $psr4namespaces->setValue(null, $psr4); + + core_component::classloader($classname); + if (DIRECTORY_SEPARATOR != '/') { + // Denormalise the expected path so that we can quickly compare with get_included_files. + $includedfiles = str_replace('/', DIRECTORY_SEPARATOR, $includedfiles); + } + $this->assertContains($includedfiles, get_included_files()); + $this->assertTrue(class_exists($classname, false)); + } + + /** + * Data provider for psr_classloader test + */ + public function psr_classloader_provider() { + global $CFG; + + // As part of these tests, we Check that there are no unexpected problems with overlapping PSR namespaces. + // This is not in the spec, but may come up in some libraries using both namespaces and PEAR-style class names. + // If problems arise we can remove this test, but will need to add a warning. + // Normalise to forward slash for testing purposes. + $directory = str_replace('\\', '/', $CFG->dirroot) . "/lib/tests/fixtures/component/"; + + $psr0 = [ + 'psr0' => 'lib/tests/fixtures/component/psr0', + 'overlap' => 'lib/tests/fixtures/component/overlap' + ]; + $psr4 = [ + 'psr4' => 'lib/tests/fixtures/component/psr4', + 'overlap' => 'lib/tests/fixtures/component/overlap' + ]; + return [ + 'PSR-0 Classloading - Root' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr0_main', + 'file' => "{$directory}psr0/main.php", + ], + 'PSR-0 Classloading - Sub namespace - underscores' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr0_subnamespace_example', + 'file' => "{$directory}psr0/subnamespace/example.php", + ], + 'PSR-0 Classloading - Sub namespace - slashes' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr0\\subnamespace\\slashes', + 'file' => "{$directory}psr0/subnamespace/slashes.php", + ], + 'PSR-0 Classloading - non-existant file' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr0_subnamespace_nonexistant_file', + 'file' => false, + ], + 'PSR-4 Classloading - Root' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr4\\main', + 'file' => "{$directory}psr4/main.php", + ], + 'PSR-4 Classloading - Sub namespace' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr4\\subnamespace\\example', + 'file' => "{$directory}psr4/subnamespace/example.php", + ], + 'PSR-4 Classloading - Ensure underscores are not converted to paths' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr4\\subnamespace\\underscore_example', + 'file' => "{$directory}psr4/subnamespace/underscore_example.php", + ], + 'PSR-4 Classloading - non-existant file' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'psr4\\subnamespace\\nonexistant', + 'file' => false, + ], + 'Overlap - Ensure no unexpected problems with PSR-4 when overlapping namespaces.' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'overlap\\subnamespace\\example', + 'file' => "{$directory}overlap/subnamespace/example.php", + ], + 'Overlap - Ensure no unexpected problems with PSR-0 overlapping namespaces.' => [ + 'psr0' => $psr0, + 'psr4' => $psr4, + 'classname' => 'overlap_subnamespace_example2', + 'file' => "{$directory}overlap/subnamespace/example2.php", + ], + ]; + } + + /** + * Test the PSR classloader. + * + * @dataProvider psr_classloader_provider + * @param array $psr0 The PSR-0 namespaces to be used in the test. + * @param array $psr4 The PSR-4 namespaces to be used in the test. + * @param string $classname The name of the class to attempt to load. + * @param string|bool $file The expected file corresponding to the class or false for nonexistant. + */ + public function test_psr_classloader($psr0, $psr4, $classname, $file) { + $psr0namespaces = new ReflectionProperty('core_component', 'psr0namespaces'); + $psr0namespaces->setAccessible(true); + $psr0namespaces->setValue(null, $psr0); + + $psr4namespaces = new ReflectionProperty('core_component', 'psr4namespaces'); + $psr4namespaces->setAccessible(true); + $oldpsr4namespaces = $psr4namespaces->getValue(null); + $psr4namespaces->setValue(null, $psr4); + + $component = new ReflectionClass('core_component'); + $psrclassloader = $component->getMethod('psr_classloader'); + $psrclassloader->setAccessible(true); + + $returnvalue = $psrclassloader->invokeArgs(null, array($classname)); + // Normalise to forward slashes for testing comparison. + if ($returnvalue) { + $returnvalue = str_replace('\\', '/', $returnvalue); + } + $this->assertEquals($file, $returnvalue); + } + + /** + * Data provider for get_class_file test + */ + public function get_class_file_provider() { + global $CFG; + + return [ + 'Getting a file with underscores' => [ + 'classname' => 'Test_With_Underscores', + 'prefix' => "Test", + 'path' => 'test/src', + 'separators' => ['_'], + 'result' => $CFG->dirroot . "/test/src/With/Underscores.php", + ], + 'Getting a file with slashes' => [ + 'classname' => 'Test\\With\\Slashes', + 'prefix' => "Test", + 'path' => 'test/src', + 'separators' => ['\\'], + 'result' => $CFG->dirroot . "/test/src/With/Slashes.php", + ], + 'Getting a file with multiple namespaces' => [ + 'classname' => 'Test\\With\\Multiple\\Namespaces', + 'prefix' => "Test\\With", + 'path' => 'test/src', + 'separators' => ['\\'], + 'result' => $CFG->dirroot . "/test/src/Multiple/Namespaces.php", + ], + 'Getting a file with multiple namespaces' => [ + 'classname' => 'Nonexistant\\Namespace\\Test', + 'prefix' => "Test", + 'path' => 'test/src', + 'separators' => ['\\'], + 'result' => false, + ], + ]; + } + + /** + * Test the PSR classloader. + * + * @dataProvider get_class_file_provider + * @param string $classname the name of the class. + * @param string $prefix The namespace prefix used to identify the base directory of the source files. + * @param string $path The relative path to the base directory of the source files. + * @param string[] $separators The characters that should be used for separating. + * @param string|bool $result The expected result to be returned from get_class_file. + */ + public function test_get_class_file($classname, $prefix, $path, $separators, $result) { + $component = new ReflectionClass('core_component'); + $psrclassloader = $component->getMethod('get_class_file'); + $psrclassloader->setAccessible(true); + + $file = $psrclassloader->invokeArgs(null, array($classname, $prefix, $path, $separators)); + $this->assertEquals($result, $file); + } } diff --git a/lib/tests/fixtures/component/overlap/subnamespace/example.php b/lib/tests/fixtures/component/overlap/subnamespace/example.php new file mode 100644 index 00000000000..9ac41a4a3df --- /dev/null +++ b/lib/tests/fixtures/component/overlap/subnamespace/example.php @@ -0,0 +1,35 @@ +. + +/** + * Example file declaring a class + * + * @package core + * @copyright 2016 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace overlap\subnamespace; + +/** + * Example Test Class + * + * @package core + * @copyright 2016 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class example { +} diff --git a/lib/tests/fixtures/component/overlap/subnamespace/example2.php b/lib/tests/fixtures/component/overlap/subnamespace/example2.php new file mode 100644 index 00000000000..dada85f8477 --- /dev/null +++ b/lib/tests/fixtures/component/overlap/subnamespace/example2.php @@ -0,0 +1,33 @@ +. + +/** + * Example file declaring a class + * + * @package core + * @copyright 2016 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Example Test Class + * + * @package core + * @copyright 2016 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class overlap_subnamespace_example2 { +} diff --git a/lib/tests/fixtures/component/psr0/main.php b/lib/tests/fixtures/component/psr0/main.php new file mode 100644 index 00000000000..7e70a2e95a2 --- /dev/null +++ b/lib/tests/fixtures/component/psr0/main.php @@ -0,0 +1,33 @@ +. + +/** + * Example file declaring a class + * + * @package core + * @copyright 2016 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Example Test Class + * + * @package core + * @copyright 2016 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class psr0_main { +} diff --git a/lib/tests/fixtures/component/psr0/subnamespace/example.php b/lib/tests/fixtures/component/psr0/subnamespace/example.php new file mode 100644 index 00000000000..f0a89c9c64a --- /dev/null +++ b/lib/tests/fixtures/component/psr0/subnamespace/example.php @@ -0,0 +1,33 @@ +. + +/** + * Example file declaring a class + * + * @package core + * @copyright 2016 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Example Test Class + * + * @package core + * @copyright 2016 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class psr0_subnamespace_example { +} diff --git a/lib/tests/fixtures/component/psr0/subnamespace/slashes.php b/lib/tests/fixtures/component/psr0/subnamespace/slashes.php new file mode 100644 index 00000000000..a63c0b8bcfb --- /dev/null +++ b/lib/tests/fixtures/component/psr0/subnamespace/slashes.php @@ -0,0 +1,35 @@ +. + +/** + * Example file declaring a class + * + * @package core + * @copyright 2016 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace psr0\subnamespace; + +/** + * Example Test Class + * + * @package core + * @copyright 2016 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class slashes { +} diff --git a/lib/tests/fixtures/component/psr4/main.php b/lib/tests/fixtures/component/psr4/main.php new file mode 100644 index 00000000000..4c46bde2585 --- /dev/null +++ b/lib/tests/fixtures/component/psr4/main.php @@ -0,0 +1,35 @@ +. + +/** + * Example file declaring a class + * + * @package core + * @copyright 2016 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + namespace PSR4; + +/** + * Example Test Class + * + * @package core + * @copyright 2016 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class main { +} diff --git a/lib/tests/fixtures/component/psr4/subnamespace/example.php b/lib/tests/fixtures/component/psr4/subnamespace/example.php new file mode 100644 index 00000000000..f2f93da8bd3 --- /dev/null +++ b/lib/tests/fixtures/component/psr4/subnamespace/example.php @@ -0,0 +1,35 @@ +. + +/** + * Example file declaring a class + * + * @package core + * @copyright 2016 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace psr4\subnamespace; + +/** + * Example Test Class + * + * @package core + * @copyright 2016 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class example { +} diff --git a/lib/tests/fixtures/component/psr4/subnamespace/underscore_example.php b/lib/tests/fixtures/component/psr4/subnamespace/underscore_example.php new file mode 100644 index 00000000000..5723f104719 --- /dev/null +++ b/lib/tests/fixtures/component/psr4/subnamespace/underscore_example.php @@ -0,0 +1,35 @@ +. + +/** + * Example file declaring a class + * + * @package core + * @copyright 2016 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace psr4\subnamespace; + +/** + * Example Test Class + * + * @package core + * @copyright 2016 John Okely + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class underscore_example { +}