diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index aa0fbcb..6ecf9fe 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -7,6 +7,7 @@ use Drupal\Core\DrupalKernel; use Drupal\Core\Database\Database; use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\DrupalModuleClassLoader; use Symfony\Component\ClassLoader\ClassLoader; use Symfony\Component\ClassLoader\ApcClassLoader; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -3065,7 +3066,7 @@ function arg($index = NULL, $path = NULL) { } /** - * Initializes and returns the class loader. + * Initializes and returns the class loader for PSR-0 libraries. * * The class loader is responsible for lazy-loading all PSR-0 compatible * classes, interfaces, and traits (PHP 5.4 and later). It's only dependency @@ -3119,16 +3120,48 @@ function drupal_classloader($class_loader = NULL) { } /** - * Registers an additional namespace. + * Initializes and returns the class loader for modules. + * + * Drupal modules follow a variation on PSR-0 directory structures that is + * more flexible to avoid extensive nested directories. Instead of strictly + * following directory structures, Drupal modules are allowed to be located + * in any of the standard Drupal locations such as /modules, /core/modules, or + * /sites/example.com/modules. All such locations are considered to be directly + * under the \Drupal vendor namespace. + * + * @param $class_loader + * The name of class loader to use. This can be used to change the class + * loader class when calling module_classloader() from settings.php. It is + * ignored otherwise. + * + * @return \Drupal\Core\DrupalModuleClassLoader + * A DrupalModuleClassLoader class instance (or extension thereof). + */ +function module_classloader() { + static $loader; + if (!isset($loader)) { + // Include the DrupalModuleClassLoader for loading classes contained in + // variable Drupal directory structures. + require_once DRUPAL_ROOT . '/core/lib/Drupal/Core/DrupalModuleClassLoader.php'; + + // Create and register the loader with PHP. + $loader = new DrupalModuleClassLoader(); + $loader->register(); + } + return $loader; +} + +/** + * Registers an additional namespace for a module. * * @param string $name - * The namespace component to register; e.g., 'node'. + * The module 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'); +function module_classloader_register($name, $path) { + $loader = module_classloader(); + $loader->addModule($name, DRUPAL_ROOT . '/' . $path . '/lib'); } /** diff --git a/core/includes/install.inc b/core/includes/install.inc index 391750f..a2d742b 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -961,7 +961,7 @@ function st($string, array $args = array(), array $options = array()) { if (!empty($files)) { // Register locale classes with the classloader. Locale module is not // yet enabled at this stage, so this is not happening automatically. - drupal_classloader_register('locale', drupal_get_path('module', 'locale')); + module_classloader_register('locale', drupal_get_path('module', 'locale')); $strings = Gettext::filesToArray($install_state['parameters']['langcode'], $files); } } diff --git a/core/includes/module.inc b/core/includes/module.inc index 11f8244..baabfb8 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -138,7 +138,7 @@ function system_list_reset() { */ function system_register($type, $name, $uri) { drupal_get_filename($type, $name, $uri); - drupal_classloader_register($name, dirname($uri)); + module_classloader_register($name, dirname($uri)); } /** diff --git a/core/lib/Drupal/Component/Plugin/Discovery/AnnotatedClassDiscovery.php b/core/lib/Drupal/Component/Plugin/Discovery/AnnotatedClassDiscovery.php index 377bf4a..72ba1da 100644 --- a/core/lib/Drupal/Component/Plugin/Discovery/AnnotatedClassDiscovery.php +++ b/core/lib/Drupal/Component/Plugin/Discovery/AnnotatedClassDiscovery.php @@ -57,6 +57,10 @@ class AnnotatedClassDiscovery implements DiscoveryInterface { * Defaults to 'Drupal\Component\Annotation\Plugin'. */ function __construct($plugin_namespaces = array(), $annotation_namespaces = array(), $plugin_definition_annotation_name = 'Drupal\Component\Annotation\Plugin') { + $plugin_namespaces += array( + 'psr0' => array(), + 'modules' => array(), + ); $this->pluginNamespaces = $plugin_namespaces; $this->annotationNamespaces = $annotation_namespaces; $this->pluginDefinitionAnnotationName = $plugin_definition_annotation_name; @@ -75,38 +79,62 @@ public function getDefinition($plugin_id) { */ public function getDefinitions() { $definitions = array(); - $reader = new AnnotationReader(); - // Prevent @endlink from being parsed as an annotation. - $reader->addGlobalIgnoredName('endlink'); // Register the namespaces of classes that can be used for annotations. AnnotationRegistry::registerAutoloadNamespaces($this->getAnnotationNamespaces()); // Search for classes within all PSR-0 namespace locations. - foreach ($this->getPluginNamespaces() as $namespace => $dirs) { - foreach ($dirs as $dir) { - $dir .= DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $namespace); - if (file_exists($dir)) { - foreach (new DirectoryIterator($dir) as $fileinfo) { - // @todo Once core requires 5.3.6, use $fileinfo->getExtension(). - if (pathinfo($fileinfo->getFilename(), PATHINFO_EXTENSION) == 'php') { - $class = $namespace . '\\' . $fileinfo->getBasename('.php'); - - // The filename is already known, so there is no need to find the - // file. However, StaticReflectionParser needs a finder, so use a - // mock version. - $finder = MockFileFinder::create($fileinfo->getPathName()); - $parser = new StaticReflectionParser($class, $finder); - - if ($annotation = $reader->getClassAnnotation($parser->getReflectionClass(), $this->pluginDefinitionAnnotationName)) { - // AnnotationInterface::get() returns the array definition - // instead of requiring us to work with the annotation object. - $definition = $annotation->get(); - $definition['class'] = $class; - $definitions[$definition['id']] = $definition; - } - } - } + $namespaces = $this->getPluginNamespaces(); + foreach ($namespaces['psr0'] as $namespace => $directories) { + foreach ($directories as $directory) { + $directory .= DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $namespace); + if (file_exists($directory)) { + $definitions += $this->parseDirectory($namespace, $directory); + } + } + } + // Search for all classes within Drupal module locations. + foreach ($namespaces['modules'] as $namespace => $directories) { + foreach ($directories as $directory) { + if (file_exists($directory)) { + $definitions += $this->parseDirectory($namespace, $directory); + } + } + } + return $definitions; + } + + /** + * Parse all the class annotations in a given directory. + * + * @param $directory string + * The path to a directory whose class annotations will be parsed. + * + * @return array + * An array of all the annotations from the parsed files. + */ + protected function parseDirectory($namespace, $directory) { + $reader = new AnnotationReader(); + // Prevent @endlink from being parsed as an annotation. + $reader->addGlobalIgnoredName('endlink'); + + $definitions = array(); + foreach (new DirectoryIterator($directory) as $fileinfo) { + if ($fileinfo->getExtension() == 'php') { + $class = $namespace . '\\' . $fileinfo->getBasename('.php'); + + // The filename is already known, so there is no need to find the + // file. However, StaticReflectionParser needs a finder, so use a + // mock version. + $finder = MockFileFinder::create($fileinfo->getPathName()); + $parser = new StaticReflectionParser($class, $finder); + + if ($annotation = $reader->getClassAnnotation($parser->getReflectionClass(), $this->pluginDefinitionAnnotationName)) { + // AnnotationInterface::get() returns the array definition + // instead of requiring us to work with the annotation object. + $definition = $annotation->get(); + $definition['class'] = $class; + $definitions[$definition['id']] = $definition; } } } @@ -114,7 +142,7 @@ public function getDefinitions() { } /** - * Returns an array of PSR-0 namespaces to search for plugin classes. + * Returns an array of namespaces to search for plugin classes. */ protected function getPluginNamespaces() { return $this->pluginNamespaces; diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index e600313..295cab4 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -12,6 +12,7 @@ use Drupal\Core\CoreBundle; use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\DependencyInjection\YamlFileLoader; +use Drupal\Core\DrupalModuleClassLoader; use Symfony\Component\ClassLoader\ClassLoader; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; @@ -66,13 +67,20 @@ class DrupalKernel extends Kernel implements DrupalKernelInterface { protected $storage; /** - * The classloader object. + * The classloader object for PSR-0 libraries. * * @var \Symfony\Component\ClassLoader\ClassLoader */ protected $classLoader; /** + * The classloader object for loading module classes. + * + * @var \Drupal\Core\DrupalModuleClassLoader + */ + protected $drupalModuleClassLoader; + + /** * Config storage object used for reading enabled modules configuration. * * @var \Drupal\Core\Config\StorageInterface @@ -130,6 +138,7 @@ class DrupalKernel extends Kernel implements DrupalKernelInterface { public function __construct($environment, $debug, ClassLoader $class_loader, $allow_dumping = TRUE) { parent::__construct($environment, $debug); $this->classLoader = $class_loader; + $this->drupalModuleClassLoader = module_classloader(); $this->allowDumping = $allow_dumping; } @@ -180,7 +189,7 @@ public function registerBundles() { $this->moduleList = isset($module_list['enabled']) ? $module_list['enabled'] : array(); } $module_filenames = $this->getModuleFileNames(); - $this->registerNamespaces($this->getModuleNamespaces($module_filenames)); + $this->drupalModuleClassLoader->addModules($this->getModuleNamespaces($module_filenames)); // Load each module's bundle class. foreach ($this->moduleList as $module => $weight) { @@ -319,8 +328,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->drupalModuleClassLoader->addModules($this->getModuleNamespaces($container_modules)); // If 'container.modules' is wrong, the container must be rebuilt. if (!isset($this->moduleList)) { @@ -329,13 +337,9 @@ 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); + // Reset the module class loader and start with a fresh set of modules. + $this->drupalModuleClassLoader->resetModules(); + $this->drupalModuleClassLoader->addModules($this->getModuleNamespaces($this->moduleList)); } } @@ -350,6 +354,7 @@ protected function initializeContainer() { $this->container->set('kernel', $this); // Set the class loader which was registered as a synthetic service. $this->container->set('class_loader', $this->classLoader); + $this->container->set('drupal_module_class_loader', $this->drupalModuleClassLoader); // If we have a request set it back to the new container. if (isset($request)) { $this->container->enterScope('request'); @@ -399,13 +404,15 @@ protected function buildContainer() { $container->setParameter('container.modules', $this->getModuleFileNames()); // Get a list of namespaces and put it onto the container. - $namespaces = $this->getModuleNamespaces($this->getModuleFileNames()); - $namespaces['Drupal\Core'] = DRUPAL_ROOT . '/core/lib'; - $namespaces['Drupal\Component'] = DRUPAL_ROOT . '/core/lib'; + $namespaces = array(); + $namespaces['psr0']['Drupal\Core'] = DRUPAL_ROOT . '/core/lib'; + $namespaces['psr0']['Drupal\Component'] = DRUPAL_ROOT . '/core/lib'; + $namespaces['modules'] = $this->getModuleNamespaces($this->getModuleFileNames()); $container->setParameter('container.namespaces', $namespaces); // Register synthetic services. $container->register('class_loader', 'Symfony\Component\ClassLoader\ClassLoader')->setSynthetic(TRUE); + $container->register('drupal_module_class_loader', 'Drupal\Core\DrupalModuleClassLoader')->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); @@ -498,15 +505,8 @@ protected function getModuleFileNames() { protected function getModuleNamespaces($moduleFileNames) { $namespaces = array(); foreach ($moduleFileNames as $module => $filename) { - $namespaces["Drupal\\$module"] = DRUPAL_ROOT . '/' . dirname($filename) . '/lib'; - } + $namespaces[$module] = DRUPAL_ROOT . '/' . dirname($filename) . '/lib'; + }; return $namespaces; } - - /** - * Registers a list of namespaces. - */ - protected function registerNamespaces(array $namespaces = array()) { - $this->classLoader->addPrefixes($namespaces); - } } diff --git a/core/lib/Drupal/Core/DrupalModuleClassLoader.php b/core/lib/Drupal/Core/DrupalModuleClassLoader.php new file mode 100644 index 0000000..36c56ac --- /dev/null +++ b/core/lib/Drupal/Core/DrupalModuleClassLoader.php @@ -0,0 +1,123 @@ +modules; + } + + /** + * Resets the list of modules in the autoloader registry. + */ + public function resetModules() { + $this->modules = array(); + } + + /** + * Adds modules to the autoloader registry. + * + * @param array $modules + * An array of modules to add, keyed by the module name and with a value + * is either an individual string, or an array of strings for that module's + * classes. + */ + public function addModules(array $modules) { + foreach ($modules as $module => $path) { + $this->addModule($module, $path); + } + } + + /** + * Registers a set of classes + * + * @param string $module_name + * The module name being registered. + * @param array|string $paths + * The location(s) of the classes under this module's namespace. + */ + public function addModule($module, $paths) { + if (isset($this->modules[$module])) { + $this->modules[$module] = array_merge($this->modules[$module], (array) $paths); + } + else { + $this->modules[$module] = (array) $paths; + } + } + + /** + * Registers this instance as an autoloader. + * + * @param Boolean $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 Boolean|null + * True, if loaded. + */ + public function loadClass($class) { + if ($file = $this->findFile($class)) { + require $file; + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class + * The name of the class. + * + * @return string|null + * The path, if found. + */ + public function findFile($class) { + // We only deal with Drupal namespaced classes. + if (strpos($class, 'Drupal\\') !== 0) { + return; + } + + // Get the module name by checking the string between the first two slashes. + $second_slash = strpos($class, '\\', 7); + $module_name = substr($class, 7, $second_slash - 7); + + // Convert the classname to a file path. + $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, $second_slash)) . '.php'; + + // Check if that class exists within any of the registered module paths. + if (isset($this->modules[$module_name])) { + foreach ($this->modules[$module_name] as $dir) { + if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) { + return $dir . DIRECTORY_SEPARATOR . $classPath; + } + } + } + } +} diff --git a/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php b/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php index 8ed9440..b2c3ffa 100644 --- a/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php +++ b/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php @@ -40,13 +40,21 @@ class AnnotatedClassDiscovery extends ComponentAnnotatedClassDiscovery { * plugins will not be removed until the next request. */ function __construct($owner, $type, array $root_namespaces = array(), $annotation_namespaces = array(), $plugin_definition_annotation_name = 'Drupal\Component\Annotation\Plugin') { + $root_namespaces += array( + 'psr0' => array(), + 'modules' => array(), + ); $annotation_namespaces += array( 'Drupal\Component\Annotation' => DRUPAL_ROOT . '/core/lib', 'Drupal\Core\Annotation' => DRUPAL_ROOT . '/core/lib', ); + // Adjust the namespaces to be specific to the plugin type we're parsing. $plugin_namespaces = array(); - foreach ($root_namespaces as $namespace => $dir) { - $plugin_namespaces["$namespace\\Plugin\\{$owner}\\{$type}"] = array($dir); + foreach ($root_namespaces['psr0'] as $namespace => $dir) { + $plugin_namespaces['psr0']["$namespace\\Plugin\\{$owner}\\{$type}"] = array($dir); + } + foreach ($root_namespaces['modules'] as $module_name => $dir) { + $plugin_namespaces['modules']["Drupal\\$module_name\\Plugin\\{$owner}\\{$type}"] = array("$dir/Plugin/{$owner}/{$type}"); } parent::__construct($plugin_namespaces, $annotation_namespaces, $plugin_definition_annotation_name); }