diff -u b/core/includes/install.inc b/core/includes/install.inc --- b/core/includes/install.inc +++ b/core/includes/install.inc @@ -12,6 +12,7 @@ use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Extension\ModuleHandler; +use Drupal\Core\Extension\ProfileHandler; use Drupal\Core\Site\Settings; /** @@ -1063,18 +1064,7 @@ $cache = &drupal_static(__FUNCTION__, array()); if (!isset($cache[$profile][$langcode])) { - // Set defaults for module info. - $defaults = array( - 'dependencies' => array(), - 'themes' => array('stark'), - 'description' => '', - 'version' => NULL, - 'hidden' => FALSE, - 'php' => DRUPAL_MINIMUM_PHP, - ); - $profile_file = drupal_get_path('profile', $profile) . "/$profile.info.yml"; - $info = \Drupal::service('info_parser')->parse($profile_file); - $info += $defaults; + $info = \Drupal::service('profile_handler')->getProfileInfo($profile); // drupal_required_modules() includes the current profile as a dependency. // Remove that dependency, since a module cannot depend on itself. @@ -1082,27 +1072,6 @@ $locale = !empty($langcode) && $langcode != 'en' ? array('locale') : array(); - // Get the base profile chain. - $base_profile_name = !empty($info['base profile']['name']) ? $info['base profile']['name'] : - (!empty($info['base profile']) ? $info['base profile'] : ''); - if ($base_profile_name) { - $base_profile = install_profile_info($base_profile_name, $langcode); - - // Ensure all dependencies are cleanly merged. - $info['dependencies'] = array_merge($info['dependencies'], $base_profile['dependencies']); - // If there are dependency excludes from the base apply them now. - if (!empty($info['base profile']['excluded_dependencies'])) { - $info['dependencies'] = array_diff($info['dependencies'], $info['base profile']['excluded_dependencies']); - } - // Ensure there's no circular dependency. - $info['dependencies'] = array_diff($info['dependencies'], array($profile)); - } - - // Ensure the same dependency notation as in modules can be used. - array_walk($info['dependencies'], function(&$dependency) { - $dependency = ModuleHandler::parseDependency($dependency)['name']; - }); - $info['dependencies'] = array_unique(array_merge($required, $info['dependencies'], $locale)); $cache[$profile][$langcode] = $info; diff -u b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php --- b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php +++ b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php @@ -86,14 +86,6 @@ $modules = $extensions['module']; // Remove the install profile as this is handled later. unset($modules[$install_profile]); - $profiles = $listing->scan('profile'); - foreach ($profiles as $profile_name => $profile_object) { - // Prime the drupal_get_filename() static cache with the profile info - // file location so we can use drupal_get_path() on the active profile - // during the module scan. - // @todo Remove as part of https://www.drupal.org/node/2186491 - drupal_get_filename('profile', $profile_name, $profile_object->getPathname()); - } $module_list_scan = $listing->scan('module'); $module_list = array(); foreach (array_keys($modules) as $module) { @@ -114,10 +106,12 @@ } if ($this->includeProfile) { - // The install profile can override module default configuration. We do - // this by replacing the config file path from the module/theme with the - // install profile version if there are any duplicates. + // The install profile (and any parent profiles) can override module + // default configuration. We do this by replacing the config file path + // from the module/theme with the install profile version if there are + // any duplicates. if (isset($profile)) { + // Get the profile and any parents. $profiles = \Drupal::service('profile_handler')->getProfiles($profile); foreach ($profiles as $extension) { $profile_folders = $this->getComponentNames(array($extension)); diff -u b/core/lib/Drupal/Core/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php --- b/core/lib/Drupal/Core/Config/InstallStorage.php +++ b/core/lib/Drupal/Core/Config/InstallStorage.php @@ -151,24 +151,12 @@ if (!isset($this->folders)) { $this->folders = array(); $this->folders += $this->getCoreNames(); + // Get dependent profiles and add the extension components. + $this->folders += $this->getComponentNames(\Drupal::service('profile_handler')->getProfiles()); // Perform an ExtensionDiscovery scan as we cannot use drupal_get_path() // yet because the system module may not yet be enabled during install. // @todo Remove as part of https://www.drupal.org/node/2186491 $listing = new ExtensionDiscovery(\Drupal::root()); - $profiles = $listing->scan('profile'); - foreach ($profiles as $profile_name => $extension) { - // Prime the drupal_get_filename() static cache with the profile info - // file location so we can use drupal_get_path() on the active profile - // during the module scan. - // @todo Remove as part of https://www.drupal.org/node/2186491 - drupal_get_filename('profile', $profile_name, $extension->getPathname()); - } - // Now that we can fetch the path, get dependent profiles and add - // the extensions. - $profiles = \Drupal::service('profile_handler')->getProfiles(); - foreach ($profiles as $extension) { - $this->folders += $this->getComponentNames(array($extension)); - } // @todo Remove as part of https://www.drupal.org/node/2186491 $this->folders += $this->getComponentNames($listing->scan('module')); $this->folders += $this->getComponentNames($listing->scan('theme')); diff -u b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php --- b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -5,6 +5,7 @@ use Drupal\Component\FileCache\FileCacheFactory; use Drupal\Core\DrupalKernel; use Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator; +use Drupal\Core\Extension\ProfileHandlerInterface; use Drupal\Core\Site\Settings; use Symfony\Component\HttpFoundation\Request; @@ -99,6 +100,13 @@ protected $sitePath; /** + * The profile handler object. + * + * @var \Drupal\Core\Extension\ProfileHandlerInterface + */ + protected $profileHandler; + + /** * Constructs a new ExtensionDiscovery object. * * @param string $root @@ -109,12 +117,20 @@ * The available profile directories * @param string $site_path * The path to the site. + * @param Drupal\Core\Extension\ProfileHandlerInterface + * The Profile Handler instance to use. */ - public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL, $site_path = NULL) { + public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL, $site_path = NULL, $profile_handler = NULL) { $this->root = $root; $this->fileCache = $use_file_cache ? FileCacheFactory::get('extension_discovery') : NULL; $this->profileDirectories = $profile_directories; $this->sitePath = $site_path; + if (!isset($profile_handler) && \Drupal::hasService('profile_handler')) { + $this->profileHandler = \Drupal::service('profile_handler'); + } + else { + $this->profileHandler = $profile_handler; + } } /** @@ -250,8 +266,8 @@ if ($profile) { // ExtensionDiscovery can be called without a service container. // (@drupalKernel::moduleData) so check if profile_handler is available. - if (\Drupal::hasService('profile_handler')) { - $profiles = \Drupal::service('profile_handler')->getProfiles($profile); + if (isset($this->profileHandler)) { + $profiles = $this->profileHandler->getProfiles($profile); $profile_directories = array_map(function($extension) { return $extension->getPath(); }, $profiles); diff -u b/core/lib/Drupal/Core/Extension/ProfileHandler.php b/core/lib/Drupal/Core/Extension/ProfileHandler.php --- b/core/lib/Drupal/Core/Extension/ProfileHandler.php +++ b/core/lib/Drupal/Core/Extension/ProfileHandler.php @@ -2,21 +2,33 @@ namespace Drupal\Core\Extension; -use Drupal\Core\Site\Settings; - /** * Class that manages profiles in a Drupal installation. */ class ProfileHandler implements ProfileHandlerInterface { /** - * Static state cache. + * Cache for getProfiles. * * @var array */ protected $cache = array(); /** + * Cache for processing info files. + * + * @var array + */ + protected $info_cache = array(); + + /** + * Whether we have primed the filename cache. + * + * @var bool + */ + protected $scan_cache = FALSE; + + /** * The app root. * * @var string @@ -31,6 +43,13 @@ protected $infoParser; /** + * Local variable used to set profile weights + * + * @var int + */ + private $weight; + + /** * Constructs a new ProfileHandler. * * @param string $root @@ -46,59 +65,177 @@ /** + * Return the full path to a profile. + * + * Wrapper around drupal_get_path. If profile path is not available yet + * we call scan('profile') and prime the cache. + * + * @param string $profile + * Name of the profile. + */ + protected function getProfilePath($profile) { + // Check to see if system_rebuild_module_data cache is primed. + // @todo Remove as part of https://www.drupal.org/node/2186491. + $modules_cache = &drupal_static('system_rebuild_module_data'); + if (!$this->scan_cache && !isset($modules_cache)) { + $listing = new ExtensionDiscovery(\Drupal::root()); + // Find installation profiles. This needs to happen before performing a + // module scan as the module scan requires knowing what the active profile is. + // @todo Remove as part of https://www.drupal.org/node/2186491. + $profiles = $listing->scan('profile'); + foreach ($profiles as $profile_name => $extension) { + // Prime the drupal_get_filename() static cache with the profile info file + // location so we can use drupal_get_path() on the active profile during + // the module scan. + // @todo Remove as part of https://www.drupal.org/node/2186491. + drupal_get_filename('profile', $profile_name, $extension->getPathname()); + } + $this->scan_cache = TRUE; + } + return drupal_get_path('profile', $profile); + } + + /** * {@inheritdoc} */ - public function getProfiles($profile = NULL) { - static $weight; + public function getProfileInfo($profile) { + // Even though info_parser caches the info array, we need to also cache + // this since it is recursive. + if (!isset($this->info_cache[$profile])) { + + // Set defaults for profile info. + $defaults = array( + 'dependencies' => array(), + 'themes' => array('stark'), + 'description' => '', + 'version' => NULL, + 'hidden' => FALSE, + 'php' => DRUPAL_MINIMUM_PHP, + ); + + $profile_path = $this->getProfilePath($profile); + $profile_file = $profile_path . "/$profile.info.yml"; + $info = $this->infoParser->parse($profile_file); + $info += $defaults; + + $profile_list = array(); + // Get the base profile dependencies. + $base_profile_name = ProfileHandler::getProfileBaseName($info); + if ($base_profile_name) { + $base_info = $this->getProfileInfo($base_profile_name); + $profile_list += $base_info['profile_list']; + + // Ensure all dependencies are cleanly merged. + $info['dependencies'] = array_merge($info['dependencies'], $base_info['dependencies']); + // If there are dependency excludes from the base apply them now. + if (!empty($info['base profile']['excluded_dependencies'])) { + $info['dependencies'] = array_diff($info['dependencies'], $info['base profile']['excluded_dependencies']); + } + // Ensure there's no circular dependency. + $info['dependencies'] = array_diff($info['dependencies'], array($profile)); + } + $profile_list[$profile] = $profile; + $info['profile_list'] = $profile_list; + + // Ensure the same dependency notation as in modules can be used. + array_walk($info['dependencies'], function(&$dependency) { + $dependency = ModuleHandler::parseDependency($dependency)['name']; + }); + + // Installation profiles are hidden by default, unless explicitly specified + // otherwise in the .info.yml file. + $info['hidden'] = isset($info['hidden']) ? $info['hidden'] : TRUE; + + // Add a default distribution name if the profile did not provide one. + // @see install_profile_info() + // @see drupal_install_profile_distribution_name() + if (!isset($info['distribution']['name'])) { + $info['distribution']['name'] = 'Drupal'; + } + + $this->info_cache[$profile] = $info; + } + return $this->info_cache[$profile]; + } + + /** + * Create an Extension object for a profile. + * + * @param string $profile + * The name of the profile. + * + * @return \Drupal\Core\Extension\Extension + * The extension object for the profile + * Properties added to extension: + * info: The parsed info.yml data. + * origin: The directory origin as used in ExtensionDiscovery. + */ + protected function getProfileExtension($profile) { + $profile_info = $this->getProfileInfo($profile); + + $type = $profile_info['type']; + $profile_path = $this->getProfilePath($profile); + $profile_file = $profile_path . "/$profile.info.yml"; + $filename = file_exists($profile_path . "/$profile.$type") ? "$profile.$type" : NULL; + $extension = new Extension($this->root, $type, $profile_file, $filename); + + $extension->info = $profile_info; + $extension->origin = ''; + return $extension; + } + + /** + * Get a list of dependent profile names. + * + * @param string $profile + * Name of profile. + * + * @return string[] + * An associative array of profile names, keyed by profile name + * in descending order of their dependencies. + * (parent profiles first, main profile last) + */ + protected function getProfileList($profile) { + $profile_info = $this->getProfileInfo($profile); + return $profile_info['profile_list']; + } + + /** + * {@inheritdoc} + */ + public function getProfiles($profile = NULL) { if (empty($profile)) { $profile = drupal_get_profile(); } if (!isset($this->cache[$profile])) { $profiles = array(); - // Starting weight for profiles ensures their hooks run last. - $weight = 1000; // Check if a valid profile name was given. if (!empty($profile)) { - // We can't use install_profile_info() because that could trigger an - // endless loop. So we read this on our own. - $profile_path = drupal_get_path('profile', $profile); - $profile_file = $profile_path . "/$profile.info.yml"; - $profile_info = $this->infoParser->parse($profile_file); - - // Check of the profile has a base profile and if so add it - recursion. - $base_profile_name = !empty($profile_info['base profile']['name']) ? $profile_info['base profile']['name'] : - (!empty($profile_info['base profile']) ? $profile_info['base profile'] : ''); - if ($base_profile_name) { - $profiles += $this->getProfiles($base_profile_name); - $weight++; - } + $list = $this->getProfileList($profile); - $type = $profile_info['type']; - $filename = file_exists($profile_path . "/$profile.$type") ? "$profile.$type" : NULL; - $extension = new Extension($this->root, $type, $profile_file, $filename); - - $info_defaults = array(); - - // Installation profiles are hidden by default, unless explicitly specified - // otherwise in the .info.yml file. - $info_defaults['hidden'] = isset($profile_info['hidden']) ? $profile_info['hidden'] : TRUE; - - $extension->info = $profile_info + $info_defaults; - $extension->weight = $weight; - $extension->origin = ''; - - // Pass needed defaults to info separately so - // _system_rebuild_module_data can merge it with the parsed info. - // @todo Determine why _system_rebuild_module_data needs freshly - // parsed info data and cannot use cached info. See issue: - // https://www.drupal.org/node/1356276#comment-11464109. - $extension->info_defaults = $info_defaults; + // Starting weight for profiles ensures their hooks run last. + $weight = 1000; - // Add requested profile as last one. - $profiles[$profile] = $extension; + // Loop through profile list and create Extension objects. + $profiles = array(); + foreach ($list as $profile_name) { + $extension = $this->getProfileExtension($profile_name); + $extension->weight = $weight; + $weight++; + $profiles[$profile_name] = $extension; + } } $this->cache[$profile] = $profiles; } return $this->cache[$profile]; } + /** + * {@inheritdoc} + */ + static function getProfileBaseName($info) { + return !empty($info['base profile']['name']) + ? $info['base profile']['name'] + : (!empty($info['base profile']) ? $info['base profile'] : ''); + } + } diff -u b/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php b/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php --- b/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php +++ b/core/lib/Drupal/Core/Extension/ProfileHandlerInterface.php @@ -10,14 +10,54 @@ /** - * Returns a list of related installation profiles. + * Retrieve the info array for a profile. + * + * Parse and process the profile info.yml file. + * Processing steps: + * 1) Ensure default keys are set. + * 2) Recursively collect dependencies from parent profiles. + * 3) Exclude dependencies explicitly mentioned in + * $info['base profile']['exclude_dependencies'] + * 4) Add the $info['profile_list'] list of dependent profiles. * * @param string $profile - * Name of profile. If none is specified, use the current profile. + * Name of profile. * * @return array - * List of installation profile extensions keyed by profile name + * The processed $info array. + */ + public function getProfileInfo($profile); + + /** + * Returns a list of dependent installation profiles. + * + * @param string $profile + * Name of profile. If none is specified, use the current profile. + * + * @return \Drupal\Core\Extension\Extension[] + * An associative array of Extension objects, keyed by profile name * in descending order of their dependencies. * (parent profiles first, main profile last) */ public function getProfiles($profile = NULL); + /** + * Return the name of a profile from its info. + * + * The info array can reference a "base profile" in two ways: + * 1) Along with excluded dependency data: + * base profile: + * name: base_profile_name + * excluded_dependencies: + * - array of excluded module dependencies + * 2) or, a shortcut of just the name: + * base profile: base_profile_name + * + * @param array $info + * The parsed info.yml array for the profile. + * + * @return string + * The name of the base (parent) profile. + * + */ + static function getProfileBaseName($info); + } diff -u b/core/modules/system/system.module b/core/modules/system/system.module --- b/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -959,24 +959,12 @@ function _system_rebuild_module_data() { $listing = new ExtensionDiscovery(\Drupal::root()); - // Find installation profiles. This needs to happen before performing a - // module scan as the module scan requires knowing what the active profile is. - // @todo Remove as part of https://www.drupal.org/node/2186491. - $profiles = $listing->scan('profile'); - foreach ($profiles as $profile_name => $extension) { - // Prime the drupal_get_filename() static cache with the profile info file - // location so we can use drupal_get_path() on the active profile during - // the module scan. - // @todo Remove as part of https://www.drupal.org/node/2186491. - drupal_get_filename('profile', $profile_name, $extension->getPathname()); - } - // Find modules. $modules = $listing->scan('module'); // Find profiles. - $profiles = \Drupal::service('profile_handler')->getProfiles(); - $modules = array_merge($modules, $profiles); + $profile_handler = \Drupal::service('profile_handler'); + $modules = array_merge($modules, $profile_handler->getProfiles()); // Set defaults for module info. $defaults = array( @@ -990,13 +978,13 @@ // Read info files for each module. foreach ($modules as $key => $module) { // Look for the info file. - // @todo Determine why we must freshly parse the info array instead of - // using any existing $module->info. See issue: - // https://www.drupal.org/node/1356276#comment-11464109. - $module->info = \Drupal::service('info_parser')->parse($module->getPathname()); - // Add any defaults stored in the extension (see ProfileHandler) - if (!empty($module->info_defaults)) { - $module->info = $module->info + $module->info_defaults; + // @todo each extension handler should be responsible for loading the + // info data. For now we just have a ProfileHandler. + if ($module->getType() == 'profile') { + $module->info = $profile_handler->getProfileInfo($module->getName()); + } + else { + $module->info = \Drupal::service('info_parser')->parse($module->getPathname()); } // Add the info file modification time, so it becomes available for @@ -1024,12 +1012,6 @@ if ($profile && isset($modules[$profile])) { // The installation profile is required, if it's a valid module. $modules[$profile]->info['required'] = TRUE; - // Add a default distribution name if the profile did not provide one. - // @see install_profile_info() - // @see drupal_install_profile_distribution_name() - if (!isset($modules[$profile]->info['distribution']['name'])) { - $modules[$profile]->info['distribution']['name'] = 'Drupal'; - } } return $modules;