diff --git a/core/includes/install.inc b/core/includes/install.inc
index 7adb15a..eb85afb 100644
--- a/core/includes/install.inc
+++ b/core/includes/install.inc
@@ -5,12 +5,13 @@
* API functions for installing modules and themes.
*/
-use Composer\Semver\Semver;
use Drupal\Component\Utility\Unicode;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\OpCodeCache;
use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Composer\ExtensionComposerDependencies;
+use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\Site\Settings;
@@ -986,8 +987,15 @@ function drupal_requirements_severity(&$requirements) {
function drupal_check_module($module) {
module_load_install($module);
// Check requirements
- $requirements = \Drupal::moduleHandler()->invoke($module, 'requirements', array('install')) ?: [];
- $requirements += drupal_check_composer_requirements($module);
+ $requirements = \Drupal::moduleHandler()->invoke($module, 'requirements', array('install'));
+
+ // Check Composer dependencies.
+ $dependencies = new ExtensionComposerDependencies();
+ // We can't use the module handler here because it only deals with enabled
+ // modules.
+ $extension = new Extension(DRUPAL_ROOT, 'module', drupal_get_filename('module', $module));
+ $requirements[] = $dependencies->extensionComposerRequirements(DRUPAL_ROOT, $extension);
+
if (is_array($requirements) && drupal_requirements_severity($requirements) == REQUIREMENT_ERROR) {
// Print any error messages
foreach ($requirements as $requirement) {
@@ -1005,88 +1013,6 @@ function drupal_check_module($module) {
}
/**
- * Check composer requirements.
- *
- * @param string $module
- * Module name.
- * @return array
- */
-function drupal_check_composer_requirements($module) {
- $module_dir = drupal_get_path('module', $module);
-
- // Check if the modules that are going to be installed have a composer.json
- // file, if they have one, make sure that the configured dependencies are
- // installed. If failures are detected, make sure the dependent modules
- // aren't installed either.
- if (file_exists($module_dir . '/composer.json')) {
-
- $semver = new Semver();
-
- $module_composer_json = json_decode(file_get_contents($module_dir . '/composer.json'));
- $module_package_requirements = $module_composer_json->require;
-
- // Load the actual autoloader being used and determine its filename using
- // reflection. We can determine the vendor directory based on that filename.
- $autoloader = require \Drupal::root() . '/autoload.php';
- $reflector = new ReflectionClass($autoloader);
- $vendor_dir = dirname(dirname($reflector->getFileName()));
-
- // The json in composer/installed.json includes all the installed packages
- // and is more reliable than checking composer.lock, vendors actually have
- // to be installed for this file to even be available.
- if (file_exists($vendor_dir . '/composer/installed.json')) {
- $installed_packages = json_decode(file_get_contents($vendor_dir.'/composer/installed.json'));
- }
- else {
- $installed_packages = [];
- $requirements['composer_dependencies'] = [
- 'description' => t('Not all the modules have their Composer dependencies installed yet. Please find more information on this handbook page.',
- [
- '@handbook' => 'https://www.drupal.org/node/2494073'
- ]),
- 'severity' => REQUIREMENT_ERROR,
- 'title' => $module,
- ];
- }
-
- $check_installed_packages = [];
- $required_packages = [];
-
- foreach ($module_package_requirements as $name => $version) {
- foreach ($installed_packages as $package) {
- if ($package->name === $name && $semver->satisfies($package->version_normalized, $version)) {
- $check_installed_packages[] = $name;
- }
- }
- $required_packages[] = $name;
- }
-
- if ($required_packages !== $check_installed_packages) {
- $requirements['composer_dependencies'] = [
- 'description' => t('Not all the modules have their Composer dependencies installed yet. Please find more information on this handbook page.',
- [
- '@handbook' => 'https://www.drupal.org/node/2494073'
- ]),
- 'severity' => REQUIREMENT_ERROR,
- 'title' => $module,
- ];
- }
- else {
- $requirements['composer_dependencies'] = [
- 'description' => t('All Composer dependencies have been installed.'),
- 'severity' => REQUIREMENT_OK,
- 'value' => t('Never run'),
- 'title' => $module,
- ];
- }
-
- return $requirements;
- }
-
- return [];
-}
-
-/**
* Retrieves information about an installation profile from its .info.yml file.
*
* The information stored in a profile .info.yml file is similar to that stored
diff --git a/core/lib/Drupal/Core/Composer/ExtensionComposerDependencies.php b/core/lib/Drupal/Core/Composer/ExtensionComposerDependencies.php
new file mode 100644
index 0000000..b11f3ae
--- /dev/null
+++ b/core/lib/Drupal/Core/Composer/ExtensionComposerDependencies.php
@@ -0,0 +1,156 @@
+ '1.7.0'.
+ */
+ protected $composerInstalled = [];
+
+ /**
+ * Determine whether Composer-based dependencies are still required.
+ *
+ * @param string $drupal_root
+ * Path to the root of the Drupal installation.
+ * @param \Drupal\Core\Extension\Extension $extension
+ * Extension to check.
+ *
+ * @return array
+ * Requirements array, suitable for hook_requirements().
+ */
+ public function extensionComposerRequirements($drupal_root, Extension $extension) {
+ $requirements = [];
+ if (!$this->dependenciesAreMet($drupal_root, $extension)) {
+ $requirements['composer_dependencies'] = [
+ 'description' => t('Not all the modules have their Composer dependencies installed yet. Please find more information on this handbook page.', [
+ '@handbook' => 'https://www.drupal.org/node/2494073'
+ ]),
+ 'severity' => REQUIREMENT_ERROR,
+ 'title' => $extension->getName(),
+ ];
+ }
+ else {
+ $requirements['composer_dependencies'] = [
+ 'description' => t('All Composer dependencies have been installed.'),
+ 'severity' => REQUIREMENT_OK,
+ 'value' => t('Never run'),
+ 'title' => $extension->getName(),
+ ];
+ }
+ return $requirements;
+ }
+
+ /**
+ * Determine whether the Composer-based dependencies of an extension are met.
+ *
+ * @param string $drupal_root
+ * Path to the root of the Drupal installation.
+ * @param \Drupal\Core\Extension\Extension $extension
+ * Extension to check.
+ *
+ * @return bool
+ * TRUE if this extenion's dependencies are met, FALSE otherwise.
+ */
+ public function dependenciesAreMet($drupal_root, Extension $extension) {
+ // Does the extension have a composer.json file?
+ $extension_composer_json = $drupal_root . '/' . $extension->getPath() . '/composer.json';
+ if (is_readable($extension_composer_json)) {
+ // Populate $composerInstalled.
+ if (empty($this->composerInstalled)) {
+ $installed_json = $drupal_root . '/vendor/composer/installed.json';
+ if (file_exists($installed_json)) {
+ $json = file_get_contents($installed_json, FALSE);
+ $this->composerInstalled = $this->parseInstalledPackages($json);
+ }
+ }
+ // @todo: Determine how to tell if the root package is a dev installation
+ // or not.
+ return $this->extensionDependenciesAreMet(
+ json_decode(file_get_contents($extension_composer_json)), $this->composerInstalled, TRUE
+ );
+ }
+ return TRUE;
+ }
+
+ /**
+ * Performs the dependency check.
+ *
+ * @param object $composer_dependencies
+ * Extension dependencies, parsed from the composer.json file.
+ * @param array $installed_packages
+ * List of installed packages, with package name as key and normalized
+ * version as value.
+ * @param bool $include_dev
+ * Whether or not to include requires-dev.
+ *
+ * @return bool
+ * TRUE if the dependencies are met, FALSE otherwise.
+ */
+ protected function extensionDependenciesAreMet(\stdClass $composer_dependencies, array $installed_packages, $include_dev = FALSE) {
+ // Gather all the requirements.
+ $require = [];
+ if (!empty($composer_dependencies->require)) {
+ $require = (array) $composer_dependencies->require;
+ }
+ if ($include_dev) {
+ // Dev requirements shouldn't be the same as non-dev ones, so we naively
+ // merge them together.
+ if (!empty($composer_dependencies->{'require-dev'})) {
+ $require = array_merge($require, (array) $composer_dependencies->{'require-dev'});
+ }
+ }
+ if (!empty($require)) {
+ // Check each required package against the list of installed packages.
+ $semver = new Semver();
+ foreach ($require as $package_name => $constraint) {
+ if (empty($installed_packages[$package_name])) {
+ return FALSE;
+ }
+ if (!$semver->satisfies($installed_packages[$package_name], $constraint)) {
+ return FALSE;
+ }
+ }
+ }
+ return TRUE;
+ }
+
+ /**
+ * Parse an installed.json file into the useful parts.
+ *
+ * @param string $installed_json
+ * JSON contents of installed.json file.
+ *
+ * @return array
+ * Array with package names as the keys and normalized version as the
+ * values.
+ */
+ protected function parseInstalledPackages($installed_json) {
+ $result = [];
+ foreach (json_decode($installed_json) as $package) {
+ if (isset($package->name) && isset($package->version_normalized)) {
+ $result[$package->name] = $package->version_normalized;
+ }
+ }
+ return $result;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/ExtensionComposerRequirementsException.php b/core/lib/Drupal/Core/Extension/ExtensionComposerRequirementsException.php
index 7256bbf..66eb4f0 100644
--- a/core/lib/Drupal/Core/Extension/ExtensionComposerRequirementsException.php
+++ b/core/lib/Drupal/Core/Extension/ExtensionComposerRequirementsException.php
@@ -8,6 +8,6 @@
namespace Drupal\Core\Extension;
/**
- * Exception thrown when the extension's composer requirements are not installed.
+ * Exception thrown when an extension's Composer requirements are not installed.
*/
-class ExtensionComposerRequirementsException extends \Exception { }
+class ExtensionComposerRequirementsException extends \Exception {}
diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
index 37fdde6..e4c108d 100644
--- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php
+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -10,9 +10,12 @@
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Composer\ExtensionComposerDependencies;
use Drupal\Core\DrupalKernelInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Extension\ExtensionComposerRequirementsException;
/**
* Default implementation of the module installer.
@@ -133,6 +136,9 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
$source_storage = $config_installer->getSourceStorage();
}
$modules_installed = array();
+ // If we reuse the Composer dependency object we'll benefit from it's cache
+ // of installed packages.
+ $composer_dependencies = new ExtensionComposerDependencies();
foreach ($module_list as $module) {
$enabled = $extension_config->get("module.$module") !== NULL;
if (!$enabled) {
@@ -141,8 +147,9 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
throw new ExtensionNameLengthException("Module name '$module' is over the maximum allowed length of " . DRUPAL_EXTENSION_NAME_MAX_LENGTH . ' characters');
}
- $composer_requirements = drupal_check_composer_requirements($module);
- if (isset($composer_requirements['composer_dependencies']) && $composer_requirements['composer_dependencies']['severity'] == REQUIREMENT_ERROR) {
+ // Throw an exception if there are unmet Composer-based dependencies.
+ $extension = new Extension(DRUPAL_ROOT, 'module', drupal_get_filename('module', $module));
+ if (!$composer_dependencies->dependenciesAreMet(DRUPAL_ROOT, $extension)) {
throw new ExtensionComposerRequirementsException("Module '$module' composer requirements are not installed yet.");
}
diff --git a/core/tests/Drupal/Tests/Core/Composer/ExtensionComposerDependenciesTest.php b/core/tests/Drupal/Tests/Core/Composer/ExtensionComposerDependenciesTest.php
new file mode 100644
index 0000000..f5e4ca2
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Composer/ExtensionComposerDependenciesTest.php
@@ -0,0 +1,124 @@
+ '1.0.0'], '[{"name": "vendor/package","version_normalized": "1.0.0"}]'],
+ ];
+ }
+
+ /**
+ * @covers ::parseInstalledPackages
+ *
+ * @dataProvider provideParseInstalled
+ */
+ public function testParseInstalledPackages($expected, $installed) {
+ $dependencies = new ExtensionComposerDependencies();
+
+ $ref_method = new \ReflectionMethod($dependencies, 'parseInstalledPackages');
+ $ref_method->setAccessible(TRUE);
+
+ $this->assertArrayEquals($expected, $ref_method->invoke($dependencies, $installed));
+ }
+
+ /**
+ * @return array
+ * - Boolean showing whether the dependencies are met by the installed
+ * packages.
+ * - Object of dependencies parsed from JSON.
+ * - Array of installed dependencies, with package name as key and version
+ * as value.
+ */
+ public function provideExtensionDependencies() {
+ return [
+ [TRUE, json_decode('{}'), []],
+ [TRUE, json_decode('{"require": {"vendor/package":"1.0.0"}}'), ['vendor/package' => '1.0.0']],
+ [FALSE, json_decode('{"require": {"vendor/package":"~1.0"}}'), ['vendor/package' => '0.0.9']],
+ [FALSE, json_decode('{"require-dev": {"vendor/package":"~1.0"}}'), ['vendor/package' => '0.0.9']],
+ ];
+ }
+
+ /**
+ * @covers ::extensionDependenciesAreMet
+ *
+ * @dataProvider provideExtensionDependencies
+ */
+ public function testExtensionDependenciesAreMet($expected, $extension_dependencies, $installed) {
+ $dependencies = new ExtensionComposerDependencies();
+
+ $ref_method = new \ReflectionMethod($dependencies, 'extensionDependenciesAreMet');
+ $ref_method->setAccessible(TRUE);
+
+ $this->assertEquals($expected, $ref_method->invoke($dependencies, $extension_dependencies, $installed, TRUE));
+ }
+
+ /**
+ * @return array
+ * - TRUE if dependencies are expected to be met, FALSE otherwise.
+ * - Contents of module's composer.json file, or NULL if there is no file.
+ * - Contents of project's installed.json file, or NULL if there is no file.
+ */
+ public function provideDependenciesAreMet() {
+ return [
+ [TRUE, NULL, NULL],
+ [TRUE, NULL, '[]'],
+ [TRUE, '{}', '[]'],
+ [TRUE, '{"require": {"vendor/package":"1.0.0"}}', '[{"name":"vendor/package","version_normalized":"1.0.0"}]'],
+ [FALSE, '{"require": {"vendor/not-installed":"1.0.0"}}', '[{"name":"vendor/package","version_normalized":"1.0.0"}]'],
+ [FALSE, '{"require": {"vendor/package":"~1.1"}}', '[{"name":"vendor/package","version_normalized":"1.0.0"}]'],
+ [FALSE, '{"require-dev": {"vendor/package":"~1.1"}}', '[{"name":"vendor/package","version_normalized":"1.0.0"}]'],
+ ];
+ }
+
+ /**
+ * @covers ::dependenciesAreMet
+ *
+ * @dataProvider provideDependenciesAreMet
+ */
+ public function testDependenciesAreMet($expected, $composer_json, $installed_json) {
+ $structure = [
+ 'modules' => ['some_module' => ['some_module.info.yml' => '']],
+ 'vendor' => ['composer' => []],
+ ];
+ if ($installed_json !== NULL) {
+ $structure['vendor']['composer']['installed.json'] = $installed_json;
+ }
+ if ($composer_json !== NULL) {
+ $structure['modules']['some_module']['composer.json'] = $composer_json;
+ }
+ vfsStream::setup('root', NULL, $structure);
+
+ $extension = new Extension(vfsStream::url('root'), 'module', 'modules/some_module/some_module.info.yml');
+ $dependencies = new ExtensionComposerDependencies();
+
+ $this->assertEquals($expected, $dependencies->dependenciesAreMet(vfsStream::url('root'), $extension));
+ }
+
+}