Issue #3124762 by Spokje, dww, johnwebdev, paulocs, piyuesh23, Suresh Prabhu Parkala, Deepak Goyal, catch, larowlan, rpsu, xjm, andypost, alexpott, daffie, longwave, fgm, Wim Leers, anmolgoyal74: Add 'lifecycle' key to .info.yml files

merge-requests/806/head
catch 2021-06-17 11:50:33 +01:00
parent cc5b85edd8
commit 37bd3765c3
32 changed files with 255 additions and 21 deletions

View File

@ -0,0 +1,8 @@
<?php
namespace Drupal\Core\Extension\Exception;
/**
* Exception thrown when the extension is obsolete on install.
*/
class ObsoleteExtensionException extends \Exception {}

View File

@ -0,0 +1,61 @@
<?php
namespace Drupal\Core\Extension;
/**
* Extension lifecycle.
*
* The lifecycle of an extension (module/theme etc) can go through the following
* progression:
* 1. Starts "experimental".
* 2. Stabilizes and goes "stable".
* 3. Eventually (maybe), becomes "deprecated" when being phased out.
* 4. Finally (maybe), becomes "obsolete" and can't be enabled anymore.
*/
final class ExtensionLifecycle {
/**
* The string used to identify the lifecycle in an .info.yml file.
*/
const LIFECYCLE_IDENTIFIER = 'lifecycle';
/**
* Extension is experimental. Warnings will be shown if installed.
*/
const EXPERIMENTAL = 'experimental';
/**
* Extension is stable. This is the default value of any extension.
*/
const STABLE = 'stable';
/**
* Extension is deprecated. Warnings will be shown if still installed.
*/
const DEPRECATED = 'deprecated';
/**
* Extension is obsolete and installation will be prevented.
*/
const OBSOLETE = 'obsolete';
/**
* Determines if a given extension lifecycle string is valid.
*
* @param string $lifecycle
* The lifecycle to validate.
*
* @return bool
* TRUE if the lifecycle is valid, otherwise FALSE.
*/
public static function isValid(string $lifecycle) : bool {
$valid_values = [
self::EXPERIMENTAL,
self::STABLE,
self::DEPRECATED,
self::OBSOLETE,
];
return in_array($lifecycle, $valid_values, TRUE);
}
}

View File

@ -107,6 +107,16 @@ class InfoParserDynamic implements InfoParserInterface {
if (isset($parsed_info['version']) && $parsed_info['version'] === 'VERSION') {
$parsed_info['version'] = \Drupal::VERSION;
}
$parsed_info += [ExtensionLifecycle::LIFECYCLE_IDENTIFIER => ExtensionLifecycle::STABLE];
if (!ExtensionLifecycle::isValid($parsed_info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER])) {
$valid_values = [
ExtensionLifecycle::EXPERIMENTAL,
ExtensionLifecycle::STABLE,
ExtensionLifecycle::DEPRECATED,
ExtensionLifecycle::OBSOLETE,
];
throw new InfoParserException("'lifecycle: {$parsed_info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER]}' is not valid in $filename. Valid values are: '" . implode("', '", $valid_values) . "'.");
}
}
return $parsed_info;
}

View File

@ -20,6 +20,8 @@ interface InfoParserInterface {
* - name: The real name of the module for display purposes. (Required)
* - description: A brief description of the module.
* - type: whether it is for a module or theme. (Required)
* - lifecycle: [experimental|stable|deprecated|obsolete]. A description of
* the current phase in the lifecycle of the module, theme or profile.
*
* Information stored in a module .info.yml file:
* - dependencies: An array of dependency strings. Each is in the form

View File

@ -8,6 +8,7 @@ use Drupal\Core\Database\Connection;
use Drupal\Core\DrupalKernelInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\Exception\ObsoleteExtensionException;
use Drupal\Core\Installer\InstallerKernel;
use Drupal\Core\Serialization\Yaml;
@ -106,6 +107,9 @@ class ModuleInstaller implements ModuleInstallerInterface {
if (!empty($module_data[$module]->info['core_incompatible'])) {
throw new MissingDependencyException("Unable to install modules: module '$module' is incompatible with this version of Drupal core.");
}
if ($module_data[$module]->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::OBSOLETE) {
throw new ObsoleteExtensionException("Unable to install modules: module '$module' is obsolete.");
}
}
if ($enable_dependencies) {
$module_list = $module_list ? array_combine($module_list, $module_list) : [];

View File

@ -1,6 +1,7 @@
name: 'Entity Reference'
type: module
description: 'Deprecated. All the functionality has been moved to Core.'
description: 'Obsolete. All the functionality has been moved to Core.'
lifecycle: obsolete
package: Field types
version: VERSION
hidden: true

View File

@ -17,7 +17,7 @@ class EntityReferenceFileUploadTest extends BrowserTestBase {
use TestFileCreationTrait;
protected static $modules = ['entity_reference', 'node', 'file'];
protected static $modules = ['node', 'file'];
/**
* {@inheritdoc}

View File

@ -2,6 +2,7 @@ name: 'Field Layout'
type: module
description: 'Allows users to configure the display and form display by arranging fields in several columns.'
package: Core (Experimental)
lifecycle: experimental
version: VERSION
dependencies:
- drupal:layout_discovery

View File

@ -4,6 +4,7 @@ namespace Drupal\help\Controller;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\help\HelpSectionManager;
@ -132,7 +133,7 @@ class HelpController extends ControllerBase {
$build['#title'] = $module_name;
$info = $this->moduleExtensionList->getExtensionInfo($name);
if ($info['package'] === 'Core (Experimental)') {
if ($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) {
$this->messenger()->addWarning($this->t('This module is experimental. <a href=":url">Experimental modules</a> are provided for testing purposes only. Use at your own risk.', [':url' => 'https://www.drupal.org/core/experimental']));
}

View File

@ -2,6 +2,7 @@ name: Help Topics
type: module
description: 'Displays help topics provided by themes and modules.'
package: Core (Experimental)
lifecycle: experimental
version: VERSION
dependencies:
- drupal:help

View File

@ -2,6 +2,7 @@
namespace Drupal\Tests\help_topics\Functional;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\Tests\BrowserTestBase;
use Drupal\help_topics\HelpTopicDiscovery;
use PHPUnit\Framework\ExpectationFailedException;
@ -279,7 +280,11 @@ class HelpTopicsSyntaxTest extends BrowserTestBase {
// Find the extensions of this type, even if they are not installed, but
// excluding test ones.
$lister = \Drupal::service('extension.list.' . $type);
foreach (array_keys($lister->getAllAvailableInfo()) as $name) {
foreach ($lister->getAllAvailableInfo() as $name => $info) {
// Skip obsolete modules.
if (isset($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER]) && $info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::OBSOLETE) {
continue;
}
$path = $lister->getPath($name);
// You can tell test modules because they are in package 'Testing', but
// test themes are only known by being found in test directories. So...

View File

@ -3,6 +3,7 @@
namespace Drupal\Tests\jsonapi\Functional;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\Tests\BrowserTestBase;
/**
@ -38,7 +39,7 @@ class TestCoverageTest extends BrowserTestBase {
&& empty($module->info['hidden'])
&& $module->status == FALSE
&& $module->info['package'] !== 'Testing'
&& $module->info['package'] !== 'Core (Experimental)';
&& $module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] !== ExtensionLifecycle::EXPERIMENTAL;
});
$this->container->get('module_installer')->install(array_keys($stable_core_modules));

View File

@ -2,6 +2,7 @@ name: 'Migrate Drupal Multilingual'
type: module
description: 'Provides a requirement for multilingual migrations.'
package: 'Core (Experimental)'
lifecycle: experimental
version: VERSION
dependencies:
- drupal:migrate_drupal

View File

@ -3,6 +3,7 @@
namespace Drupal\Tests\rest\Functional\EntityResource;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\Tests\BrowserTestBase;
/**
@ -42,7 +43,7 @@ class EntityResourceRestTestCoverageTest extends BrowserTestBase {
empty($module->info['hidden']) &&
$module->status == FALSE &&
$module->info['package'] !== 'Testing' &&
$module->info['package'] !== 'Core (Experimental)';
$module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] !== ExtensionLifecycle::EXPERIMENTAL;
});
$this->container->get('module_installer')->install(array_keys($stable_core_modules));

View File

@ -140,8 +140,8 @@ class SearchPageCacheTagsTest extends BrowserTestBase {
*/
public function testSearchTagsBubbling() {
// Install field UI and entity reference modules.
$this->container->get('module_installer')->install(['field_ui', 'entity_reference']);
// Install field UI module.
$this->container->get('module_installer')->install(['field_ui']);
$this->resetAll();
// Creates a new content type that will have an entity reference.

View File

@ -1,6 +1,7 @@
name: Testing
type: module
description: 'Deprecated. SimpleTest has been removed from core.'
description: 'Obsolete. SimpleTest has been removed from core.'
lifecycle: obsolete
package: Core
version: VERSION
hidden: true

View File

@ -6,6 +6,7 @@ use Drupal\Core\Config\PreExistingConfigException;
use Drupal\Core\Config\UnmetDependenciesException;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\Core\Extension\InfoParserException;
use Drupal\Core\Extension\ModuleDependencyMessageTrait;
use Drupal\Core\Extension\ModuleExtensionList;
@ -404,7 +405,7 @@ class ModulesListForm extends FormBase {
elseif (($checkbox = $form_state->getValue(['modules', $name], FALSE)) && $checkbox['enable']) {
$modules['install'][$name] = $data[$name]->info['name'];
// Identify experimental modules.
if ($data[$name]->info['package'] == 'Core (Experimental)') {
if ($data[$name]->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) {
$modules['experimental'][$name] = $data[$name]->info['name'];
}
}
@ -418,7 +419,7 @@ class ModulesListForm extends FormBase {
$modules['install'][$dependency] = $data[$dependency]->info['name'];
// Identify experimental modules.
if ($data[$dependency]->info['package'] == 'Core (Experimental)') {
if ($data[$dependency]->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) {
$modules['experimental'][$dependency] = $data[$dependency]->info['name'];
}
}

View File

@ -15,6 +15,7 @@ use Drupal\Core\Database\Database;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Link;
use Drupal\Core\Site\Settings;
@ -67,7 +68,7 @@ function system_requirements($phase) {
$enabled_modules = \Drupal::moduleHandler()->getModuleList();
foreach ($enabled_modules as $module => $data) {
$info = \Drupal::service('extension.list.module')->getExtensionInfo($module);
if (isset($info['package']) && $info['package'] === 'Core (Experimental)') {
if (isset($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER]) && $info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) {
$experimental_modules[$module] = $info['name'];
}
}

View File

@ -2,4 +2,5 @@ name: 'Experimental Requirements Test'
type: module
description: 'Module in the experimental package to test hook_requirements() with an experimental module.'
package: Core (Experimental)
lifecycle: experimental
version: VERSION

View File

@ -2,4 +2,5 @@ name: 'Experimental Test'
type: module
description: 'Module in the experimental package to test experimental functionality.'
package: Core (Experimental)
lifecycle: experimental
version: 8.y.x-unstable

View File

@ -0,0 +1,6 @@
name: 'System obsolete status test'
type: module
description: 'Support module for testing an obsolete module extension.'
package: Testing
version: VERSION
lifecycle: obsolete

View File

@ -2,6 +2,7 @@
namespace Drupal\Tests\system\Functional\Module;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\workspaces\Entity\Workspace;
@ -104,13 +105,13 @@ class InstallUninstallTest extends ModuleTestBase {
// Install the module.
$edit = [];
$package = $module->info['package'];
$lifecycle = $module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER];
$edit['modules[' . $name . '][enable]'] = TRUE;
$this->drupalGet('admin/modules');
$this->submitForm($edit, 'Install');
// Handle experimental modules, which require a confirmation screen.
if ($package == 'Core (Experimental)') {
if ($lifecycle === ExtensionLifecycle::EXPERIMENTAL) {
$this->assertSession()->pageTextContains('Are you sure you wish to enable experimental modules?');
if (count($modules_to_install) > 1) {
// When there are experimental modules, needed dependencies do not
@ -210,7 +211,7 @@ class InstallUninstallTest extends ModuleTestBase {
foreach ($all_modules as $name => $module) {
$edit['modules[' . $name . '][enable]'] = TRUE;
// Track whether there is at least one experimental module.
if ($module->info['package'] == 'Core (Experimental)') {
if ($module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) {
$experimental = TRUE;
}
}

View File

@ -45,7 +45,6 @@ class EntityReferenceSelectionReferenceableTest extends KernelTestBase {
'system',
'user',
'field',
'entity_reference',
'node',
'entity_test',
];

View File

@ -3,6 +3,7 @@ type: module
description: 'Allows users to stage content or preview a full site by using multiple workspaces on a single site.'
version: VERSION
package: Core (Experimental)
lifecycle: experimental
configure: entity.workspace.collection
dependencies:
- drupal:user

View File

@ -3,6 +3,7 @@ description: Imports the content for the Umami demo.
type: module
version: VERSION
package: 'Core (Experimental)'
lifecycle: experimental
hidden: true
dependencies:
- drupal:field

View File

@ -6,6 +6,7 @@ use Drupal\Core\Config\Entity\ConfigEntityDependency;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\KernelTests\AssertConfigTrait;
use Drupal\KernelTests\FileSystemModuleDiscoveryDataProviderTrait;
use Drupal\KernelTests\KernelTestBase;
@ -165,7 +166,7 @@ class DefaultConfigTest extends KernelTestBase {
}
else {
$info = $this->container->get('extension.list.module')->getExtensionInfo($module);
if (!isset($info['package']) || $info['package'] !== 'Core (Experimental)') {
if (!isset($info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER]) || $info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] !== ExtensionLifecycle::EXPERIMENTAL) {
$this->fail("$config_name provided by $module does not exist after installing all dependencies");
}
}

View File

@ -4,6 +4,7 @@ namespace Drupal\KernelTests\Core\Extension;
use Drupal\Core\Database\Database;
use Drupal\Core\Extension\MissingDependencyException;
use Drupal\Core\Extension\Exception\ObsoleteExtensionException;
use Drupal\KernelTests\KernelTestBase;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
@ -134,4 +135,15 @@ class ModuleInstallerTest extends KernelTestBase {
$this->assertTrue($this->container->get('module_installer')->install(['system_incompatible_core_version_dependencies_test'], FALSE));
}
/**
* Tests trying to install an obsolete module.
*
* @covers ::install
*/
public function testObsoleteInstall() {
$this->expectException(ObsoleteExtensionException::class);
$this->expectExceptionMessage("Unable to install modules: module 'system_status_obsolete_test' is obsolete.");
$this->container->get('module_installer')->install(['system_status_obsolete_test']);
}
}

View File

@ -2,6 +2,7 @@
namespace Drupal\KernelTests\Core\Theme;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\KernelTests\KernelTestBase;
/**
@ -64,7 +65,7 @@ class Stable9LibraryOverrideTest extends KernelTestBase {
$all_modules = array_filter($all_modules, function ($module) {
// Filter contrib, hidden, experimental, already enabled modules, and
// modules in the Testing package.
if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing' || $module->info['package'] == 'Core (Experimental)') {
if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing' || $module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) {
return FALSE;
}
return TRUE;

View File

@ -2,6 +2,7 @@
namespace Drupal\KernelTests\Core\Theme;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\Core\Theme\Registry;
use Drupal\KernelTests\KernelTestBase;
@ -63,7 +64,7 @@ class Stable9TemplateOverrideTest extends KernelTestBase {
$all_modules = array_filter($all_modules, function ($module) {
// Filter contrib, hidden, experimental, already enabled modules, and
// modules in the Testing package.
if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing' || $module->info['package'] == 'Core (Experimental)') {
if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing' || $module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) {
return FALSE;
}
return TRUE;

View File

@ -2,6 +2,7 @@
namespace Drupal\KernelTests\Core\Theme;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\KernelTests\KernelTestBase;
/**
@ -64,7 +65,7 @@ class StableLibraryOverrideTest extends KernelTestBase {
$all_modules = array_filter($all_modules, function ($module) {
// Filter contrib, hidden, experimental, already enabled modules, and
// modules in the Testing package.
if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing' || $module->info['package'] == 'Core (Experimental)') {
if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing' || $module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) {
return FALSE;
}
return TRUE;

View File

@ -2,6 +2,7 @@
namespace Drupal\KernelTests\Core\Theme;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\Core\Theme\Registry;
use Drupal\KernelTests\KernelTestBase;
@ -61,7 +62,7 @@ class StableTemplateOverrideTest extends KernelTestBase {
$all_modules = array_filter($all_modules, function ($module) {
// Filter contrib, hidden, experimental, already enabled modules, and
// modules in the Testing package.
if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing' || $module->info['package'] == 'Core (Experimental)') {
if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing' || $module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::EXPERIMENTAL) {
return FALSE;
}
return TRUE;

View File

@ -2,6 +2,7 @@
namespace Drupal\Tests\Core\Extension;
use Drupal\Core\Extension\ExtensionLifecycle;
use Drupal\Core\Extension\InfoParser;
use Drupal\Core\Extension\InfoParserException;
use Drupal\Tests\UnitTestCase;
@ -691,4 +692,110 @@ INFO;
}
}
/**
* Tests an info file with valid lifecycle values.
*
* @covers ::parse
*
* @dataProvider providerValidLifecycle
*/
public function testValidLifecycle($lifecycle, $expected) {
$info = <<<INFO
package: Core
core: 8.x
version: VERSION
type: module
name: Module for That
INFO;
if (!empty($lifecycle)) {
$info .= "\nlifecycle: $lifecycle\n";
}
vfsStream::setup('modules');
$filename = "lifecycle-$lifecycle.info.yml";
vfsStream::create([
'fixtures' => [
$filename => $info,
],
]);
$info_values = $this->infoParser->parse(vfsStream::url("modules/fixtures/$filename"));
$this->assertSame($expected, $info_values[ExtensionLifecycle::LIFECYCLE_IDENTIFIER]);
}
/**
* Data provider for testValidLifecycle().
*/
public function providerValidLifecycle() {
return [
'empty' => [
'',
ExtensionLifecycle::STABLE,
],
'experimental' => [
ExtensionLifecycle::EXPERIMENTAL,
ExtensionLifecycle::EXPERIMENTAL,
],
'stable' => [
ExtensionLifecycle::STABLE,
ExtensionLifecycle::STABLE,
],
'deprecated' => [
ExtensionLifecycle::DEPRECATED,
ExtensionLifecycle::DEPRECATED,
],
'obsolete' => [
ExtensionLifecycle::OBSOLETE,
ExtensionLifecycle::OBSOLETE,
],
];
}
/**
* Tests an info file with invalid lifecycle values.
*
* @covers ::parse
*
* @dataProvider providerInvalidLifecycle
*/
public function testInvalidLifecycle($lifecycle, $exception_message) {
$info = <<<INFO
package: Core
core: 8.x
version: VERSION
type: module
name: Module for That
INFO;
$info .= "\nlifecycle: $lifecycle\n";
vfsStream::setup('modules');
$filename = "lifecycle-$lifecycle.info.txt";
vfsStream::create([
'fixtures' => [
$filename => $info,
],
]);
$this->expectException('\Drupal\Core\Extension\InfoParserException');
$this->expectExceptionMessage($exception_message);
$info_values = $this->infoParser->parse(vfsStream::url("modules/fixtures/$filename"));
$this->assertEmpty($info_values);
}
/**
* Data provider for testInvalidLifecycle().
*/
public function providerInvalidLifecycle() {
return [
'bogus' => [
'bogus',
"'lifecycle: bogus' is not valid",
],
'two words' => [
'deprecated obsolete',
"'lifecycle: deprecated obsolete' is not valid",
],
'wrong case' => [
'Experimental',
"'lifecycle: Experimental' is not valid",
],
];
}
}