diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 74af7c6..23d0840 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -10,7 +10,8 @@ use Drupal\Core\DrupalKernel; use Drupal\Core\Database\Database; use Drupal\Core\DependencyInjection\ContainerBuilder; -use Symfony\Component\ClassLoader\ClassLoader; +use Drupal\Core\ClassLoader\ClassLoaderInterface; +use Drupal\Core\ClassLoader\ClassLoader; use Symfony\Component\ClassLoader\ApcClassLoader; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Container; @@ -2750,64 +2751,72 @@ function arg($index = NULL, $path = NULL) { * classes, interfaces, and traits (PHP 5.4 and later). It's only dependency * is DRUPAL_ROOT. Otherwise it may be called as early as possible. * - * @param $class_loader + * @param string $loader_name * The name of class loader to use. This can be used to change the class * loader class when calling drupal_classloader() from settings.php. It is * ignored otherwise. - * - * @return \Symfony\Component\ClassLoader\ClassLoader + * @param ClassLoaderInterface $custom_loader + * Loader object. + * This can be called from settings.php to replace the class loader. + * @param boolean $add_composer_dir + * Whether to register core and vendor namespaces from composer's + * autoload_classes.php and autoload_namespaces.php. This can be set to FALSE, + * if the namespaces are already registered in the $custom_loader object. + * + * @throws Exception + * @return ClassLoaderInterface * A ClassLoader class instance (or extension thereof). */ -function drupal_classloader($class_loader = NULL) { +function drupal_classloader($loader_name = NULL, ClassLoaderInterface $custom_loader = NULL, $add_composer_dir = TRUE) { // By default, use the ClassLoader which is best for development, as it does // not break when code is moved on the file system. However, as it is slow, // allow to use the APC class loader in production. static $loader; if (!isset($loader)) { - - // Include the Symfony ClassLoader for loading PSR-0-compatible classes. - require_once DRUPAL_ROOT . '/core/vendor/symfony/class-loader/Symfony/Component/ClassLoader/ClassLoader.php'; - $loader = new ClassLoader(); - - // Register the class loader. - // When configured to use APC, the ApcClassLoader is registered instead. - // Note that ApcClassLoader decorates ClassLoader and only provides the - // findFile() method, but none of the others. The actual registry is still - // in ClassLoader. - if (!isset($class_loader)) { - $class_loader = settings()->get('class_loader', 'default'); - } - if ($class_loader === 'apc') { - require_once DRUPAL_ROOT . '/core/vendor/symfony/class-loader/Symfony/Component/ClassLoader/ApcClassLoader.php'; - $apc_loader = new ApcClassLoader('drupal.' . drupal_get_hash_salt(), $loader); - $apc_loader->register(); + if (isset($custom_loader)) { + // Use a custom loader. + $loader = $custom_loader; + // Do not call ->register() on the custom loader, to support custom + // decorators. } else { - $loader->register(); - } + // Include the Drupal ClassLoader for loading PSR-0-compatible classes. + require_once DRUPAL_ROOT . '/core/lib/Drupal/Core/ClassLoader/ClassLoaderInterface.php'; + require_once DRUPAL_ROOT . '/core/lib/Drupal/Core/ClassLoader/AbstractClassLoader.php'; + require_once DRUPAL_ROOT . '/core/lib/Drupal/Core/ClassLoader/ClassLoader.php'; + $loader = new ClassLoader(); + + // Determine whether to use a cache decorator for the loader. + if (!isset($loader_name)) { + $loader_name = settings()->get('class_loader', 'default'); + } - // Register namespaces for vendor libraries managed by Composer. - $prefixes_and_namespaces = require DRUPAL_ROOT . '/core/vendor/composer/autoload_namespaces.php'; - $loader->addPrefixes($prefixes_and_namespaces); + // Register either the decorator, or the loader itself. + if ($loader_name === 'apc') { + // Create the APC decorator, and register its loadClass() method on the + // spl autoload stack. + require_once DRUPAL_ROOT . '/core/vendor/symfony/class-loader/Symfony/Component/ClassLoader/ApcClassLoader.php'; + $apc_loader = new ApcClassLoader('drupal.' . drupal_get_hash_salt(), $loader); + $apc_loader->register(); + } + else { + // Register the undecorated class loader. + $loader->register(); + } + } - // Register the loader with PHP. - $loader->register(); + if ($add_composer_dir) { + // Register namespaces for vendor libraries managed by Composer. + // This always happens on the loader itself, not on the APC decorator. + $loader->composerVendorDir(DRUPAL_ROOT . '/core/vendor', FALSE); + } + } + elseif ($custom_loader) { + throw new \Exception('Custom loader cannot be set, if a loader already exists.'); } - return $loader; -} -/** - * Registers an additional namespace. - * - * @param string $name - * The namespace component to register; e.g., 'node'. - * @param string $path - * The relative path to the Drupal component in the filesystem. - */ -function drupal_classloader_register($name, $path) { - $loader = drupal_classloader(); - $loader->addPrefix('Drupal\\' . $name, DRUPAL_ROOT . '/' . $path . '/lib'); + return $loader; } /** diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 5217e37..7a14084 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -951,7 +951,15 @@ function install_display_output($output, $install_state) { $active_task = $install_state['installation_finished'] ? NULL : $install_state['active_task']; drupal_add_region_content('sidebar_first', theme('task_list', array('items' => install_tasks_to_display($install_state), 'active' => $active_task, 'variant' => 'install'))); } - print theme('install_page', array('content' => $output)); + if (function_exists('theme')) { + print theme('install_page', array('content' => $output)); + } + elseif (is_string($output)) { + print $output; + } + else { + var_dump($output); + } exit; } diff --git a/core/includes/module.inc b/core/includes/module.inc index cdd3ddd..1214f01 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -135,7 +135,7 @@ function system_list_reset() { */ function system_register($type, $name, $uri) { drupal_get_filename($type, $name, $uri); - drupal_classloader_register($name, dirname($uri)); + drupal_classloader()->addDrupalExtension($name, dirname($uri)); } /** diff --git a/core/lib/Drupal/Core/ClassLoader/AbstractClassLoader.php b/core/lib/Drupal/Core/ClassLoader/AbstractClassLoader.php new file mode 100644 index 0000000..2ca717c --- /dev/null +++ b/core/lib/Drupal/Core/ClassLoader/AbstractClassLoader.php @@ -0,0 +1,115 @@ +classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + public function addMultiple($prefixes, $prepend = FALSE) { + foreach ($prefixes as $prefix => $paths) { + $this->add($prefix, $paths, $prepend); + } + } + + public function composerVendorDir($dir, $complete = TRUE) { + if ($complete) { + // This does not also register classmap and namespaces, + // but also set include paths, and include some files by default. + /** + * @var \Composer\Autoload\ClassLoader $composerLoader + */ + $composerLoader = $dir . '/autoload.php'; + $this->add('', $composerLoader->getFallbackDirs()); + $this->setUseIncludePath($composerLoader->getUseIncludePath()); + $prefixes = $composerLoader->getPrefixes(); + $classMap = $composerLoader->getClassMap(); + } + else { + $prefixes = require $dir . '/composer/autoload_namespaces.php'; + $classMap = require $dir . '/composer/autoload_classmap.php'; + } + foreach ($prefixes as $prefix => $path) { + $this->add($prefix, $path); + } + if ($classMap) { + $this->addClassMap($classMap); + } + } + + /** + * @inheritdoc + */ + public function addDrupalExtensionsByRelativeFilePath($extensions, $prepend = FALSE) { + foreach ($extensions as $extension_name => $relativeFilePath) { + $this->addDrupalExtension($extension_name, dirname($relativeFilePath), $prepend); + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() { + return $this->useIncludePath; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class + * The name of the class + * @return bool|null + * True if loaded, null otherwise + */ + public function loadClass($class) { + if ($file = $this->findFile($class)) { + include $file; + return true; + } + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/ClassLoader/ClassLoader.php b/core/lib/Drupal/Core/ClassLoader/ClassLoader.php new file mode 100644 index 0000000..973ba1d --- /dev/null +++ b/core/lib/Drupal/Core/ClassLoader/ClassLoader.php @@ -0,0 +1,130 @@ +fallbackDirs = array_merge( + (array) $paths, + $this->fallbackDirs + ); + } else { + $this->fallbackDirs = array_merge( + $this->fallbackDirs, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixes[$first][$prefix])) { + $this->prefixes[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixes[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixes[$first][$prefix] + ); + } else { + $this->prefixes[$first][$prefix] = array_merge( + $this->prefixes[$first][$prefix], + (array) $paths + ); + } + } + + /** + * @param $extensionName + * @param $relativeExtensionDir + * @param bool $prepend + */ + public function addDrupalExtension($extensionName, $relativeExtensionDir, $prepend = false) { + $this->add('Drupal\\' . $extensionName . '\\', + DRUPAL_ROOT . DIRECTORY_SEPARATOR . $relativeExtensionDir . '/lib', + $prepend); + } + + /** + * @param $extensionName + * @param $relativeExtensionDir + * @param bool $prepend + */ + public function addDrupalExtensionTests($extensionName, $relativeExtensionDir, $prepend = false) { + $this->add('Drupal\\' . $extensionName . '\Tests\\', + DRUPAL_ROOT . DIRECTORY_SEPARATOR . $relativeExtensionDir . '/tests', + $prepend); + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) { + // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 + if ('\\' == $class[0]) { + $class = substr($class, 1); + } + + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $classPath = strtr(substr($class, 0, $pos), '\\', DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + $className = substr($class, $pos + 1); + } else { + // PEAR-like class name + $classPath = null; + $className = $class; + } + + $classPath .= strtr($className, '_', DIRECTORY_SEPARATOR) . '.php'; + + $first = $class[0]; + if (isset($this->prefixes[$first])) { + foreach ($this->prefixes[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) { + return $dir . DIRECTORY_SEPARATOR . $classPath; + } + } + } + } + } + + foreach ($this->fallbackDirs as $dir) { + if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) { + return $dir . DIRECTORY_SEPARATOR . $classPath; + } + } + + if ($this->useIncludePath && $file = stream_resolve_include_path($classPath)) { + return $file; + } + + return $this->classMap[$class] = false; + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/ClassLoader/ClassLoaderInterface.php b/core/lib/Drupal/Core/ClassLoader/ClassLoaderInterface.php new file mode 100644 index 0000000..cdfdb3a --- /dev/null +++ b/core/lib/Drupal/Core/ClassLoader/ClassLoaderInterface.php @@ -0,0 +1,87 @@ +environment = $environment; $this->booted = false; $this->classLoader = $class_loader; @@ -233,7 +233,7 @@ public function discoverServiceProviders() { $this->moduleList = isset($module_list['enabled']) ? $module_list['enabled'] : array(); } $module_filenames = $this->getModuleFileNames(); - $this->registerNamespaces($this->getModuleNamespaces($module_filenames)); + $this->classLoader->addDrupalExtensionsByRelativeFilePath($module_filenames); // Load each module's serviceProvider class. foreach ($this->moduleList as $module => $weight) { @@ -417,8 +417,7 @@ protected function initializeContainer() { // All namespaces must be registered before we attempt to use any service // from the container. $container_modules = $this->container->getParameter('container.modules'); - $namespaces_before = $this->classLoader->getPrefixes(); - $this->registerNamespaces($this->getModuleNamespaces($container_modules)); + $this->classLoader->addDrupalExtensionsByRelativeFilePath($container_modules); // If 'container.modules' is wrong, the container must be rebuilt. if (!isset($this->moduleList)) { @@ -427,13 +426,21 @@ protected function initializeContainer() { if (array_keys($this->moduleList) !== array_keys($container_modules)) { $persist = $this->getServicesToPersist(); unset($this->container); - // Revert the class loader to its prior state. However, - // registerNamespaces() performs a merge rather than replace, so to - // effectively remove erroneous registrations, we must replace them with - // empty arrays. - $namespaces_after = $this->classLoader->getPrefixes(); - $namespaces_before += array_fill_keys(array_diff(array_keys($namespaces_after), array_keys($namespaces_before)), array()); - $this->registerNamespaces($namespaces_before); + // @todo Revert the class loader? + // At this point, previous versions did attempt to revert the + // class loader to its previous state, with the intention to remove + // erroneous registrations. + // However, we can assume that this did not work, because + // \Symfony\Component\ClassLoader\ClassLoader does not support removal + // of registered namespaces, it only allows adding them. + // The other question is, whether removal of namespace registrations + // is desirable in the first place. Some of the classes might already + // be included, and removal of the namespace cannot undo that. + // A more interesting case is if a module has moved to a different + // directory. This case is not even detected in the if() clause above. + // + // Conclusion: A revert mechanic can be added, but needs to be + // discussed first. } } @@ -521,7 +528,7 @@ protected function buildContainer() { $container->setParameter('container.namespaces', $namespaces); // Register synthetic services. - $container->register('class_loader', 'Symfony\Component\ClassLoader\ClassLoader')->setSynthetic(TRUE); + $container->register('class_loader', 'Drupal\Core\ClassLoader\ClassLoaderInterface')->setSynthetic(TRUE); $container->register('kernel', 'Symfony\Component\HttpKernel\KernelInterface')->setSynthetic(TRUE); $container->register('service_container', 'Symfony\Component\DependencyInjection\ContainerInterface')->setSynthetic(TRUE); $yaml_loader = new YamlFileLoader($container); @@ -656,11 +663,4 @@ protected function getModuleNamespaces($moduleFileNames) { } return $namespaces; } - - /** - * Registers a list of namespaces. - */ - protected function registerNamespaces(array $namespaces = array()) { - $this->classLoader->addPrefixes($namespaces); - } } diff --git a/core/lib/Drupal/Core/Extension/UpdateModuleHandler.php b/core/lib/Drupal/Core/Extension/UpdateModuleHandler.php index 0e5a57b..04c76dc 100644 --- a/core/lib/Drupal/Core/Extension/UpdateModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/UpdateModuleHandler.php @@ -125,7 +125,7 @@ public function enable($module_list, $enable_dependencies = TRUE) { system_list_reset(); $this->moduleList[$module] = drupal_get_filename('module', $module); $this->load($module); - drupal_classloader_register($module, dirname($this->moduleList[$module])); + drupal_classloader()->addDrupalExtension($module, dirname($this->moduleList[$module])); // @todo Figure out what to do about hook_install() and hook_enable(). } return $old_schema; diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module index 587b6c0..1beb719 100644 --- a/core/modules/simpletest/simpletest.module +++ b/core/modules/simpletest/simpletest.module @@ -1,5 +1,6 @@ array('dir' => 'themes', 'extension' => 'info'), 'profile' => array('dir' => 'profiles', 'extension' => 'profile'), ); + $loader = drupal_classloader(); foreach ($types as $type => $info) { $matches = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.' . $info['extension'] . '$/', $info['dir']); foreach ($matches as $name => $file) { - drupal_classloader_register($name, dirname($file->uri)); - drupal_classloader()->addPrefix('Drupal\\' . $name . '\\Tests', DRUPAL_ROOT . '/' . dirname($file->uri) . '/tests'); + $loader->addDrupalExtension($name, dirname($file->uri)); + $loader->addDrupalExtensionTests($name, dirname($file->uri)); // While being there, prime drupal_get_filename(). drupal_get_filename($type, $name, $file->uri); } } // Register the core test directory so we can find \Drupal\UnitTestCase. - drupal_classloader()->addPrefix('Drupal\\Tests', DRUPAL_ROOT . '/core/tests'); - - // Manually register phpunit prefixes because they use a classmap instead of a - // prefix. This can be safely removed if we move to using composer's - // autoloader with a classmap. - drupal_classloader()->addPrefixes(array( - 'PHPUnit' => DRUPAL_ROOT . '/core/vendor/phpunit/phpunit', - 'File_Iterator' => DRUPAL_ROOT . '/core/vendor/phpunit/php-file-iterator/', - 'PHP_Timer' => DRUPAL_ROOT . '/core/vendor/phpunit/php-timer/', - )); + $loader->add('Drupal\\Tests\\', DRUPAL_ROOT . '/core/tests'); } /** diff --git a/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/DrupalKernelTest.php b/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/DrupalKernelTest.php index db60d18..60851d6 100644 --- a/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/DrupalKernelTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/DrupalKernelTest.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\DrupalKernel; +use Drupal\Core\ClassLoader\ClassLoaderInterface; use Drupal\Core\DrupalKernel; use Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage; use Drupal\Component\PhpStorage\FileReadOnlyStorage; @@ -79,8 +80,7 @@ function testCompileDIC() { $this->assertTrue($is_compiled_container); // Test that our synthetic services are there. $classloader = $container->get('class_loader'); - $refClass = new ReflectionClass($classloader); - $this->assertTrue($refClass->hasMethod('loadClass'), 'Container has a classloader'); + $this->assertTrue($classloader instanceof ClassLoaderInterface, 'Container has a classloader'); // We make this assertion here purely to show that the new container below // is functioning correctly, i.e. we get a brand new ContainerBuilder @@ -107,8 +107,7 @@ function testCompileDIC() { $this->assertTrue($container->has('service_provider_test_class')); // Test that our synthetic services are there. $classloader = $container->get('class_loader'); - $refClass = new ReflectionClass($classloader); - $this->assertTrue($refClass->hasMethod('loadClass'), 'Container has a classloader'); + $this->assertTrue($classloader instanceof ClassLoaderInterface, 'Container has a classloader'); // Check that the location of the new module is registered. $modules = $container->getParameter('container.modules'); $this->assertEqual($modules['service_provider_test'], drupal_get_filename('module', 'service_provider_test'));