diff --git a/core/includes/install.inc b/core/includes/install.inc index 2d246b5c84269eb7c0cbbc3fc7e8b48644c1bdcf..fb0e10777b8e3f61bb126684351c67df0cbd77aa 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -9,7 +9,9 @@ use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Database\Database; use Drupal\Core\Extension\Dependency; +use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\ExtensionDiscovery; +use Drupal\Core\Extension\InstallRequirementsInterface; use Drupal\Core\Installer\InstallerKernel; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -616,6 +618,8 @@ function drupal_check_profile($profile) { if (function_exists($function)) { $requirements = array_merge($requirements, $function('install')); } + + $requirements = array_merge($requirements, install_check_class_requirements($module_list[$module])); } } @@ -625,6 +629,9 @@ function drupal_check_profile($profile) { $requirements = array_merge($requirements, $function('install')); } + $extension = \Drupal::service('extension.list.profile')->get($profile); + $requirements = array_merge($requirements, install_check_class_requirements($extension)); + return $requirements; } @@ -660,13 +667,16 @@ function drupal_requirements_severity(&$requirements) { function drupal_check_module($module) { /** @var \Drupal\Core\Extension\ModuleExtensionList $module_list */ $module_list = \Drupal::service('extension.list.module'); - $file = DRUPAL_ROOT . '/' . $module_list->getPath($module) . "/$module.install"; + $extension = $module_list->get($module); + $file = \Drupal::root() . '/' . $extension->getPath() . "/$module.install"; if (is_file($file)) { require_once $file; } // Check requirements - $requirements = \Drupal::moduleHandler()->invoke($module, 'requirements', ['install']); - if (is_array($requirements) && drupal_requirements_severity($requirements) == REQUIREMENT_ERROR) { + $requirements = \Drupal::moduleHandler()->invoke($module, 'requirements', ['install']) ?? []; + $requirements = array_merge($requirements, install_check_class_requirements($extension)); + + if (!empty($requirements) && drupal_requirements_severity($requirements) == REQUIREMENT_ERROR) { // Print any error messages foreach ($requirements as $requirement) { if (isset($requirement['severity']) && $requirement['severity'] == REQUIREMENT_ERROR) { @@ -790,3 +800,41 @@ function install_profile_info($profile, $langcode = 'en') { } return $cache[$profile][$langcode]; } + +/** + * Checks a module or profile requirements. + * + * See \Drupal\Core\Extension\InstallRequirementsInterface for more information. + * + * @param \Drupal\Core\Extension $extension + * The extension to check install requirements for. + * + * @return array + * The requirements. + * + * @internal + */ +function install_check_class_requirements(Extension $extension): array { + $extension_path = $extension->getPath(); + $extension_name = $extension->getName(); + $dir = \Drupal::root() . "/$extension_path/src/Install/Requirements"; + $requirements = []; + if (is_dir($dir)) { + $fileSystemIterator = new FilesystemIterator($dir); + foreach ($fileSystemIterator as $fileInfo) { + if ($fileInfo->isFile() && $fileInfo->getExtension() === 'php') { + $filename = $fileInfo->getFilename(); + $requirements_path = $dir . '/' . $filename; + require_once $requirements_path; + + $namespace = "Drupal\\$extension_name\\Install\\Requirements"; + $class_name = $namespace . '\\' . $fileInfo->getBasename('.php'); + if (class_exists($class_name) && class_implements($class_name, InstallRequirementsInterface::class)) { + $requirements = $class_name::getRequirements(); + } + } + } + + } + return $requirements; +} diff --git a/core/lib/Drupal/Core/Extension/InstallRequirementsInterface.php b/core/lib/Drupal/Core/Extension/InstallRequirementsInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..ca1ad6afd6736db007e2b0617e5b3692d2619ef9 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/InstallRequirementsInterface.php @@ -0,0 +1,48 @@ +<?php + +namespace Drupal\Core\Extension; + +/** + * Provides method for checking requirements during install time. + */ +interface InstallRequirementsInterface { + + /** + * Check installation requirements. + * + * Classes implementing this must be in the Install/Requirements namespace. + * For example src/Install/Requirements/ModuleNameRequirements.php. + * + * During the 'install' phase, modules can for example assert that + * library or server versions are available or sufficient. + * Note that the installation of a module can happen during installation of + * Drupal itself (by install.php) with an installation profile or later by hand. + * As a consequence, install-time requirements must be checked without access + * to the full Drupal API, because it is not available during install.php. + * If a requirement has a severity of REQUIREMENT_ERROR, install.php will abort + * or at least the module will not install. + * Other severity levels have no effect on the installation. + * Module dependencies do not belong to these installation requirements, + * but should be defined in the module's .info.yml file. + * + * During installation, if you need to load a class from your module, + * you'll need to include the class file directly. + * + * @return array + * An associative array where the keys are arbitrary but must be unique (it + * is suggested to use the module short name as a prefix) and the values are + * themselves associative arrays with the following elements: + * - title: The name of the requirement. + * - value: This should only be used for version numbers, do not set it if + * not applicable. + * - description: The description of the requirement/status. + * - severity: (optional) The requirement's result/severity level, one of: + * - REQUIREMENT_INFO: For info only. + * - REQUIREMENT_OK: The requirement is satisfied. + * - REQUIREMENT_WARNING: The requirement failed with a warning. + * - REQUIREMENT_ERROR: The requirement failed with an error. + * Defaults to REQUIREMENT_OK when installing. + */ + public static function getRequirements(): array; + +} diff --git a/core/modules/system/tests/modules/module_install_requirements/module_install_requirements.info.yml b/core/modules/system/tests/modules/module_install_requirements/module_install_requirements.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..dbbeb86129afafd26968910c40d0753e0837b5d1 --- /dev/null +++ b/core/modules/system/tests/modules/module_install_requirements/module_install_requirements.info.yml @@ -0,0 +1,5 @@ +name: 'Test Install Requirements Class' +type: module +description: 'Support module for testing install requirements.' +package: Testing +version: VERSION diff --git a/core/modules/system/tests/modules/module_install_requirements/src/Install/Requirements/ModuleInstallRequirements.php b/core/modules/system/tests/modules/module_install_requirements/src/Install/Requirements/ModuleInstallRequirements.php new file mode 100644 index 0000000000000000000000000000000000000000..32bc2fef1e02ec273c26e0a8ca018eb320a36853 --- /dev/null +++ b/core/modules/system/tests/modules/module_install_requirements/src/Install/Requirements/ModuleInstallRequirements.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\module_install_requirements\Install\Requirements; + +use Drupal\Core\Extension\InstallRequirementsInterface; + +/** + * Provides method for checking requirements during install time. + */ +class ModuleInstallRequirements implements InstallRequirementsInterface { + + /** + * {@inheritdoc} + */ + public static function getRequirements(): array { + $GLOBALS['module_install_requirements'] = 'module_install_requirements'; + + return []; + } + +} diff --git a/core/modules/system/tests/modules/module_install_unmet_requirements/module_install_unmet_requirements.info.yml b/core/modules/system/tests/modules/module_install_unmet_requirements/module_install_unmet_requirements.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..dec6ba7e9e8571db87ee0889749d239627830ae2 --- /dev/null +++ b/core/modules/system/tests/modules/module_install_unmet_requirements/module_install_unmet_requirements.info.yml @@ -0,0 +1,5 @@ +name: 'Test Install Requirements Class' +type: module +description: 'Support module for testing install unmet requirements.' +package: Testing +version: VERSION diff --git a/core/modules/system/tests/modules/module_install_unmet_requirements/src/Install/Requirements/ModuleInstallUnmetRequirementsRequirements.php b/core/modules/system/tests/modules/module_install_unmet_requirements/src/Install/Requirements/ModuleInstallUnmetRequirementsRequirements.php new file mode 100644 index 0000000000000000000000000000000000000000..4d7367ff414131aae9cb898e176cf698d4073627 --- /dev/null +++ b/core/modules/system/tests/modules/module_install_unmet_requirements/src/Install/Requirements/ModuleInstallUnmetRequirementsRequirements.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\module_install_unmet_requirements\Install\Requirements; + +use Drupal\Core\Extension\InstallRequirementsInterface; + +/** + * Provides method for checking requirements during install time. + */ +class ModuleInstallUnmetRequirementsRequirements implements InstallRequirementsInterface { + + /** + * {@inheritdoc} + */ + public static function getRequirements(): array { + $requirements['testing_requirements'] = [ + 'title' => t('Testing requirements'), + 'severity' => REQUIREMENT_ERROR, + 'description' => t('Testing requirements failed requirements.'), + ]; + + return $requirements; + } + +} diff --git a/core/profiles/tests/profile_install_requirements/profile_install_requirements.info.yml b/core/profiles/tests/profile_install_requirements/profile_install_requirements.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..f6e04e5021c7fe35c2a7f8ad689101814acb0da5 --- /dev/null +++ b/core/profiles/tests/profile_install_requirements/profile_install_requirements.info.yml @@ -0,0 +1,5 @@ +name: 'Test Install Requirements' +type: profile +hidden: true +description: 'Support profile for testing install requirements.' +version: VERSION diff --git a/core/profiles/tests/profile_install_requirements/src/Install/Requirements/ProfileInstallRequirementsRequirements.php b/core/profiles/tests/profile_install_requirements/src/Install/Requirements/ProfileInstallRequirementsRequirements.php new file mode 100644 index 0000000000000000000000000000000000000000..9a010b930f7c31d55fc2f9ae9768bb886ca9ec71 --- /dev/null +++ b/core/profiles/tests/profile_install_requirements/src/Install/Requirements/ProfileInstallRequirementsRequirements.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\profile_install_requirements\Install\Requirements; + +use Drupal\Core\Extension\InstallRequirementsInterface; + +/** + * Provides method for checking requirements during install time. + */ +class ProfileInstallRequirementsRequirements implements InstallRequirementsInterface { + + /** + * {@inheritdoc} + */ + public static function getRequirements(): array { + $requirements['testing_requirements'] = [ + 'title' => t('Testing requirements'), + 'severity' => REQUIREMENT_ERROR, + 'description' => t('Testing requirements failed requirements.'), + ]; + + return $requirements; + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Installer/ProfileRequirementsTest.php b/core/tests/Drupal/FunctionalTests/Installer/ProfileRequirementsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..6a6e556944b7015e054c0587345f2ee9a76ffc82 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Installer/ProfileRequirementsTest.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\FunctionalTests\Installer; + +/** + * Tests installing a profile that implements InstallRequirementsInterface. + * + * @group Installer + */ +class ProfileRequirementsTest extends InstallerTestBase { + + /** + * {@inheritdoc} + */ + protected $profile = 'profile_install_requirements'; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUpSettings(): void { + // This form will never be reached. + } + + /** + * {@inheritdoc} + */ + protected function setUpRequirementsProblem(): void { + // The parent method asserts that there are no requirements errors, but + // this test expects a requirements error in the test method below. + // Therefore, we override this method to suppress the parent's assertions. + } + + /** + * {@inheritdoc} + */ + protected function setUpSite(): void { + // This form will never be reached. + } + + /** + * Test Requirements are picked up. + */ + public function testRequirementsFailure(): void { + $this->assertSession()->pageTextContains('Testing requirements failed requirements.'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Installer/InstallRequirementsTest.php b/core/tests/Drupal/KernelTests/Core/Installer/InstallRequirementsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8c05cfd7dc66c159d175dbc64c09515abe838e3d --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Installer/InstallRequirementsTest.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Installer; + +use Drupal\KernelTests\KernelTestBase; + +/** + * Tests for install requirements. + * + * @group Installer + */ +class InstallRequirementsTest extends KernelTestBase { + + /** + * Confirm installer checks requirements in designated classes. + */ + public function testRequirements(): void { + require_once 'core/includes/install.inc'; + + $this->assertFalse(isset($GLOBALS['module_install_requirements'])); + drupal_check_module('module_install_requirements'); + $this->assertTrue(isset($GLOBALS['module_install_requirements'])); + } + + /** + * Tests that the installer returns false if module requirements are not met. + */ + public function testRequirementsFailure(): void { + require_once 'core/includes/install.inc'; + $this->assertFalse(drupal_check_module('module_install_unmet_requirements')); + } + +}