diff --git a/core/lib/Drupal/Core/Config/Action/Attribute/ActionMethod.php b/core/lib/Drupal/Core/Config/Action/Attribute/ActionMethod.php index f5f9b0de5ec9cc2f1f250720ce4f16e82a2672bc..56e388072fa9708f1cbcd00e1fd027bbc969bb4c 100644 --- a/core/lib/Drupal/Core/Config/Action/Attribute/ActionMethod.php +++ b/core/lib/Drupal/Core/Config/Action/Attribute/ActionMethod.php @@ -5,7 +5,9 @@ namespace Drupal\Core\Config\Action\Attribute; // cspell:ignore inflector +use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; use Drupal\Core\Config\Action\Exists; +use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\StringTranslation\TranslatableMarkup; /** @@ -30,12 +32,23 @@ final class ActionMethod { * ID. For example, if the method is called 'addArray' this can be set to * 'addMultipleArrays'. Set to FALSE if a pluralized version does not make * logical sense. + * @param string|null $name + * The name of the action, if it should differ from the method name. Will be + * pluralized if $pluralize is TRUE. Must follow the rules for a valid PHP + * function name (e.g., no spaces, no Unicode characters, etc.). If used, + * the actual name of the method will NOT be available as an action name. + * + * @see https://www.php.net/manual/en/functions.user-defined.php */ public function __construct( public readonly Exists $exists = Exists::ErrorIfNotExists, public readonly TranslatableMarkup|string $adminLabel = '', public readonly bool|string $pluralize = TRUE, + public readonly ?string $name = NULL, ) { + if ($name && !preg_match(ExtensionDiscovery::PHP_FUNCTION_PATTERN, $name)) { + throw new InvalidPluginDefinitionException('entity_method', sprintf("'%s' is not a valid PHP function name.", $name)); + } } } diff --git a/core/lib/Drupal/Core/Config/Action/Plugin/ConfigAction/Deriver/EntityMethodDeriver.php b/core/lib/Drupal/Core/Config/Action/Plugin/ConfigAction/Deriver/EntityMethodDeriver.php index 2577d8d7b923847be2d54a61abfd8fb4a16f8cf1..b1c5ccff3841e7da2cdc2f31357b49048e013976 100644 --- a/core/lib/Drupal/Core/Config/Action/Plugin/ConfigAction/Deriver/EntityMethodDeriver.php +++ b/core/lib/Drupal/Core/Config/Action/Plugin/ConfigAction/Deriver/EntityMethodDeriver.php @@ -102,15 +102,16 @@ private function processMethod(\ReflectionMethod $method, ActionMethod $action_a 'pluralized' => FALSE, ]; $derivative['entity_types'] = [$entity_type->id()]; + $action_name = $action_attribute->name ?: $method->name; // Build a config action identifier from the entity type's config // prefix and the method name. For example, the Role entity adds a // 'user.role:grantPermission' action. - $this->addDerivative($method->name, $entity_type, $derivative, $method->name); + $this->addDerivative($action_name, $entity_type, $derivative, $method->name); $pluralized_name = match(TRUE) { is_string($action_attribute->pluralize) => $action_attribute->pluralize, $action_attribute->pluralize === FALSE => '', - default => $this->inflector->pluralize($method->name)[0] + default => $this->inflector->pluralize($action_name)[0] }; // Add a pluralized version of the plugin. if (strlen($pluralized_name) > 0) { diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php index 22e56ac3bc6462cd70d03677e7f3795959c1f81b..41fb1d630b14e31a7bd5bd62c3d2673786363378 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -160,6 +160,7 @@ public function get($property_name) { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set a value'), pluralize: 'setMultiple')] public function set($property_name, $value) { if ($this instanceof EntityWithPluginCollectionInterface && !$this->isSyncing()) { $plugin_collections = $this->getPluginCollections(); @@ -177,6 +178,7 @@ public function set($property_name, $value) { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Enable'), pluralize: FALSE)] public function enable() { return $this->setStatus(TRUE); } @@ -184,6 +186,7 @@ public function enable() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Disable'), pluralize: FALSE)] public function disable() { return $this->setStatus(FALSE); } diff --git a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php index 4951ef32d72d8121910cf698823fbf2889be1101..c1b80ac8487b5f7bca85746e30250cfd02e153a7 100644 --- a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php +++ b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php @@ -373,6 +373,7 @@ public function setComponent($name, array $options = []) { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Hide component'), name: 'hideComponent')] public function removeComponent($name) { $this->hidden[$name] = TRUE; unset($this->content[$name]); diff --git a/core/lib/Drupal/Core/Field/FieldConfigBase.php b/core/lib/Drupal/Core/Field/FieldConfigBase.php index 988f510729483dc7927b18573c8c02e24055c251..56cd5b497a02a798ad199aca8f6da8fa1469edd3 100644 --- a/core/lib/Drupal/Core/Field/FieldConfigBase.php +++ b/core/lib/Drupal/Core/Field/FieldConfigBase.php @@ -329,7 +329,7 @@ public function getLabel() { /** * {@inheritdoc} */ - #[ActionMethod(adminLabel: new TranslatableMarkup('Change field label'))] + #[ActionMethod(adminLabel: new TranslatableMarkup('Set field label'), pluralize: FALSE)] public function setLabel($label) { $this->label = $label; return $this; @@ -345,6 +345,7 @@ public function getDescription() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set field description'), pluralize: FALSE)] public function setDescription($description) { $this->description = $description; return $this; @@ -361,6 +362,7 @@ public function isTranslatable() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set whether field is translatable'), pluralize: FALSE)] public function setTranslatable($translatable) { $this->translatable = $translatable; return $this; @@ -376,6 +378,7 @@ public function getSettings() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set field settings'), pluralize: FALSE)] public function setSettings(array $settings) { $this->settings = $settings + $this->settings; return $this; @@ -411,6 +414,7 @@ public function isRequired() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set whether field is required'), pluralize: FALSE)] public function setRequired($required) { $this->required = $required; return $this; @@ -443,6 +447,7 @@ public function getDefaultValueLiteral() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set default value'), pluralize: FALSE)] public function setDefaultValue($value) { $this->default_value = $this->normalizeValue($value, $this->getFieldStorageDefinition()->getMainPropertyName()); return $this; diff --git a/core/modules/block/src/Entity/Block.php b/core/modules/block/src/Entity/Block.php index 72184ed9ae3149b9040f883b006bbb19f11cc339..61c24fcead372f9f566d2dad80b49a97e3835d18 100644 --- a/core/modules/block/src/Entity/Block.php +++ b/core/modules/block/src/Entity/Block.php @@ -312,7 +312,7 @@ protected function conditionPluginManager() { /** * {@inheritdoc} */ - #[ActionMethod(adminLabel: new TranslatableMarkup('Set region'), pluralize: FALSE)] + #[ActionMethod(adminLabel: new TranslatableMarkup('Set block region'), pluralize: FALSE)] public function setRegion($region) { $this->region = $region; return $this; @@ -321,7 +321,7 @@ public function setRegion($region) { /** * {@inheritdoc} */ - #[ActionMethod(adminLabel: new TranslatableMarkup('Set weight'), pluralize: FALSE)] + #[ActionMethod(adminLabel: new TranslatableMarkup('Set block weight'), pluralize: FALSE)] public function setWeight($weight) { $this->weight = $weight; return $this; diff --git a/core/modules/block/tests/src/Kernel/ConfigActionsTest.php b/core/modules/block/tests/src/Kernel/ConfigActionsTest.php index e4078943f9b5d782128c8d3516af8afbb9a8bc59..5fa62b12082d602c647f76bbbe5062a308a1ff9e 100644 --- a/core/modules/block/tests/src/Kernel/ConfigActionsTest.php +++ b/core/modules/block/tests/src/Kernel/ConfigActionsTest.php @@ -11,6 +11,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ThemeInstallerInterface; use Drupal\KernelTests\KernelTestBase; +use Drupal\Tests\block\Traits\BlockCreationTrait; /** * @covers \Drupal\block\Plugin\ConfigAction\PlaceBlock @@ -19,10 +20,12 @@ */ class ConfigActionsTest extends KernelTestBase { + use BlockCreationTrait; + /** * {@inheritdoc} */ - protected static $modules = ['block', 'user', 'system']; + protected static $modules = ['block', 'system', 'user']; private readonly ConfigActionManager $configActionManager; @@ -40,21 +43,41 @@ protected function setUp(): void { ->set('default', 'olivero') ->set('admin', 'claro') ->save(); - $this->configActionManager = $this->container->get('plugin.manager.config_action'); } + public function testEntityMethodActions(): void { + $block = $this->placeBlock('system_messages_block', ['theme' => 'olivero']); + $this->assertSame('content', $block->getRegion()); + $this->assertSame(0, $block->getWeight()); + + $this->configActionManager->applyAction( + 'entity_method:block.block:setRegion', + $block->getConfigDependencyName(), + 'highlighted', + ); + $this->configActionManager->applyAction( + 'entity_method:block.block:setWeight', + $block->getConfigDependencyName(), + -10, + ); + + $block = Block::load($block->id()); + $this->assertSame('highlighted', $block->getRegion()); + $this->assertSame(-10, $block->getWeight()); + } + /** * @testWith ["placeBlockInDefaultTheme"] * ["placeBlockInAdminTheme"] */ - public function testActionOnlyWorksOnBlocks(string $action): void { + public function testPlaceBlockActionOnlyWorksOnBlocks(string $action): void { $this->expectException(PluginNotFoundException::class); $this->expectExceptionMessage("The \"$action\" plugin does not exist."); $this->configActionManager->applyAction($action, 'user.role.anonymous', []); } - public function testExistingBlockIsNotChanged(): void { + public function testPlaceBlockActionDoesNotChangeExistingBlock(): void { $extant_region = Block::load('olivero_powered')->getRegion(); $this->assertNotSame('content', $extant_region); diff --git a/core/modules/contact/src/Entity/ContactForm.php b/core/modules/contact/src/Entity/ContactForm.php index 514455b4d7d964b04c76d604c1d33cdd963ed52a..16c6d1cc7d9e9a3640669d6e4f5b151ec43c6043 100644 --- a/core/modules/contact/src/Entity/ContactForm.php +++ b/core/modules/contact/src/Entity/ContactForm.php @@ -2,8 +2,10 @@ namespace Drupal\contact\Entity; +use Drupal\Core\Config\Action\Attribute\ActionMethod; use Drupal\Core\Config\Entity\ConfigEntityBundleBase; use Drupal\contact\ContactFormInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Url; /** @@ -117,6 +119,7 @@ public function getMessage() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set contact form message'), pluralize: FALSE)] public function setMessage($message) { $this->message = $message; return $this; @@ -132,6 +135,7 @@ public function getRecipients() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set recipients'), pluralize: FALSE)] public function setRecipients($recipients) { $this->recipients = $recipients; return $this; @@ -160,6 +164,7 @@ public function getRedirectUrl() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set redirect path'), pluralize: FALSE)] public function setRedirectPath($redirect) { $this->redirect = $redirect; return $this; @@ -175,6 +180,7 @@ public function getReply() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set auto-reply message'), pluralize: FALSE)] public function setReply($reply) { $this->reply = $reply; return $this; @@ -190,6 +196,7 @@ public function getWeight() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set weight'), pluralize: FALSE)] public function setWeight($weight) { $this->weight = $weight; return $this; diff --git a/core/modules/contact/tests/src/Kernel/ConfigActionsTest.php b/core/modules/contact/tests/src/Kernel/ConfigActionsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a9a2eaada66baece899125dfd66ed83b3b3d374d --- /dev/null +++ b/core/modules/contact/tests/src/Kernel/ConfigActionsTest.php @@ -0,0 +1,74 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\contact\Kernel; + +use Drupal\contact\Entity\ContactForm; +use Drupal\Core\Config\Action\ConfigActionManager; +use Drupal\KernelTests\KernelTestBase; + +/** + * @group contact + */ +class ConfigActionsTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['contact', 'system', 'user']; + + private readonly ConfigActionManager $configActionManager; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installConfig('contact'); + $this->configActionManager = $this->container->get('plugin.manager.config_action'); + } + + public function testConfigActions(): void { + $form = ContactForm::load('personal'); + $this->assertSame('Your message has been sent.', $form->getMessage()); + $this->assertEmpty($form->getRecipients()); + $this->assertSame('/', $form->getRedirectUrl()->toString()); + $this->assertEmpty($form->getReply()); + $this->assertSame(0, $form->getWeight()); + + $this->configActionManager->applyAction( + 'entity_method:contact.form:setMessage', + $form->getConfigDependencyName(), + 'Fly, little message!', + ); + $this->configActionManager->applyAction( + 'entity_method:contact.form:setRecipients', + $form->getConfigDependencyName(), + ['ben@deep.space', 'jake@deep.space'], + ); + $this->configActionManager->applyAction( + 'entity_method:contact.form:setRedirectPath', + $form->getConfigDependencyName(), + '/admin/appearance', + ); + $this->configActionManager->applyAction( + 'entity_method:contact.form:setReply', + $form->getConfigDependencyName(), + "From hell's heart, I reply to thee.", + ); + $this->configActionManager->applyAction( + 'entity_method:contact.form:setWeight', + $form->getConfigDependencyName(), + -10, + ); + + $form = ContactForm::load($form->id()); + $this->assertSame('Fly, little message!', $form->getMessage()); + $this->assertSame(['ben@deep.space', 'jake@deep.space'], $form->getRecipients()); + $this->assertSame('/admin/appearance', $form->getRedirectUrl()->toString()); + $this->assertSame("From hell's heart, I reply to thee.", $form->getReply()); + $this->assertSame(-10, $form->getWeight()); + } + +} diff --git a/core/modules/field/tests/src/Kernel/ConfigActionsTest.php b/core/modules/field/tests/src/Kernel/ConfigActionsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..70a06101c6b5f005f9076d60775a4ccf95660129 --- /dev/null +++ b/core/modules/field/tests/src/Kernel/ConfigActionsTest.php @@ -0,0 +1,106 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\field\Kernel; + +use Drupal\Core\Config\Action\ConfigActionManager; +use Drupal\entity_test\Entity\EntityTestBundle; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\KernelTests\KernelTestBase; + +/** + * @group field + */ +class ConfigActionsTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['entity_test', 'field']; + + private readonly ConfigActionManager $configActionManager; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + EntityTestBundle::create([ + 'id' => 'test', + 'label' => $this->randomString(), + ])->save(); + + $this->configActionManager = $this->container->get('plugin.manager.config_action'); + } + + public function testConfigActions(): void { + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'test', + 'type' => 'boolean', + 'entity_type' => 'entity_test_with_bundle', + ]); + $field_storage->save(); + + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'test', + ]); + $field->save(); + + $this->assertTrue($field->isTranslatable()); + $this->assertFalse($field->isRequired()); + $this->assertSame('On', (string) $field->getSetting('on_label')); + $this->assertSame('Off', (string) $field->getSetting('off_label')); + $this->assertEmpty($field->getDefaultValueLiteral()); + + $this->configActionManager->applyAction( + 'entity_method:field.field:setLabel', + $field->getConfigDependencyName(), + 'Not what you were expecting!', + ); + $this->configActionManager->applyAction( + 'entity_method:field.field:setDescription', + $field->getConfigDependencyName(), + "Any ol' nonsense can go here.", + ); + $this->configActionManager->applyAction( + 'entity_method:field.field:setTranslatable', + $field->getConfigDependencyName(), + FALSE, + ); + $this->configActionManager->applyAction( + 'entity_method:field.field:setRequired', + $field->getConfigDependencyName(), + TRUE, + ); + $this->configActionManager->applyAction( + 'entity_method:field.field:setSettings', + $field->getConfigDependencyName(), + [ + 'on_label' => 'Zap!', + 'off_label' => 'Zing!', + ], + ); + $this->configActionManager->applyAction( + 'entity_method:field.field:setDefaultValue', + $field->getConfigDependencyName(), + [ + 'value' => FALSE, + ], + ); + + $field = FieldConfig::load($field->id()); + $this->assertNotEmpty($field); + $this->assertSame('Not what you were expecting!', $field->getLabel()); + $this->assertSame("Any ol' nonsense can go here.", $field->getDescription()); + $this->assertFalse($field->isTranslatable()); + $this->assertTrue($field->isRequired()); + $this->assertSame('Zap!', $field->getSetting('on_label')); + $this->assertSame('Zing!', $field->getSetting('off_label')); + $this->assertSame([['value' => 0]], $field->getDefaultValueLiteral()); + } + +} diff --git a/core/modules/image/src/Entity/ImageStyle.php b/core/modules/image/src/Entity/ImageStyle.php index abb6160dffd71612230183c602d8968ca9ebdea7..33d80b1dad6084beba13650afbc616c9ccab3982 100644 --- a/core/modules/image/src/Entity/ImageStyle.php +++ b/core/modules/image/src/Entity/ImageStyle.php @@ -3,6 +3,7 @@ namespace Drupal\image\Entity; use Drupal\Core\Cache\Cache; +use Drupal\Core\Config\Action\Attribute\ActionMethod; use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\Core\Entity\EntityStorageInterface; @@ -12,6 +13,7 @@ use Drupal\Core\Routing\RequestHelper; use Drupal\Core\Site\Settings; use Drupal\Core\StreamWrapper\StreamWrapperManager; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Url; use Drupal\image\ImageEffectPluginCollection; use Drupal\image\ImageEffectInterface; @@ -417,6 +419,7 @@ public function getPluginCollections() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Add an image effect'))] public function addImageEffect(array $configuration) { $configuration['uuid'] = $this->uuidGenerator()->generate(); $this->getEffects()->addInstanceId($configuration['uuid'], $configuration); diff --git a/core/modules/image/tests/src/Kernel/ConfigActionsTest.php b/core/modules/image/tests/src/Kernel/ConfigActionsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e8391499c471ea93ecb8f642d154be21259fc904 --- /dev/null +++ b/core/modules/image/tests/src/Kernel/ConfigActionsTest.php @@ -0,0 +1,45 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\image\Kernel; + +use Drupal\Core\Config\Action\ConfigActionManager; +use Drupal\image\Entity\ImageStyle; +use Drupal\KernelTests\KernelTestBase; + +/** + * @group image + */ +class ConfigActionsTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['image', 'system']; + + private readonly ConfigActionManager $configActionManager; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installConfig(['image', 'system']); + $this->configActionManager = $this->container->get('plugin.manager.config_action'); + } + + public function testConfigActions(): void { + $style = ImageStyle::load('large'); + $this->assertCount(2, $style->getEffects()); + + $this->configActionManager->applyAction( + 'entity_method:image.style:addImageEffect', + $style->getConfigDependencyName(), + ['id' => 'image_desaturate', 'weight' => 1], + ); + + $this->assertCount(3, ImageStyle::load('large')->getEffects()); + } + +} diff --git a/core/modules/language/src/Entity/ConfigurableLanguage.php b/core/modules/language/src/Entity/ConfigurableLanguage.php index c8f246bf91c40e9a866f79c4e3e1bfdb39ad8fc4..a8235aa4fe2eddd7b5a8aaeaeba16114fa95bbd6 100644 --- a/core/modules/language/src/Entity/ConfigurableLanguage.php +++ b/core/modules/language/src/Entity/ConfigurableLanguage.php @@ -2,9 +2,11 @@ namespace Drupal\language\Entity; +use Drupal\Core\Config\Action\Attribute\ActionMethod; use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Language\LanguageManager; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\language\ConfigurableLanguageManager; use Drupal\language\ConfigurableLanguageManagerInterface; use Drupal\language\Exception\DeleteDefaultLanguageException; @@ -224,6 +226,7 @@ public function getName() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set Language name'), pluralize: FALSE)] public function setName($name) { $this->label = $name; @@ -254,6 +257,7 @@ public function getWeight() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set weight'), pluralize: FALSE)] public function setWeight($weight) { $this->weight = $weight; return $this; diff --git a/core/modules/language/tests/src/Kernel/ConfigActionsTest.php b/core/modules/language/tests/src/Kernel/ConfigActionsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..537678199bf15326149b7a2cfc7184ade51ac4d7 --- /dev/null +++ b/core/modules/language/tests/src/Kernel/ConfigActionsTest.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\language\Kernel; + +use Drupal\Core\Config\Action\ConfigActionManager; +use Drupal\KernelTests\KernelTestBase; +use Drupal\language\Entity\ConfigurableLanguage; + +/** + * @group language + */ +class ConfigActionsTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['language']; + + private readonly ConfigActionManager $configActionManager; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installConfig('language'); + $this->configActionManager = $this->container->get('plugin.manager.config_action'); + } + + public function testConfigActions(): void { + $language = ConfigurableLanguage::load('en'); + $this->assertSame('English', $language->getName()); + $this->assertSame(0, $language->getWeight()); + + $this->configActionManager->applyAction( + 'entity_method:language.entity:setName', + $language->getConfigDependencyName(), + 'Wacky language', + ); + $this->configActionManager->applyAction( + 'entity_method:language.entity:setWeight', + $language->getConfigDependencyName(), + 39, + ); + + $language = ConfigurableLanguage::load('en'); + $this->assertSame('Wacky language', $language->getName()); + $this->assertSame(39, $language->getWeight()); + } + +} diff --git a/core/modules/media/src/Entity/MediaType.php b/core/modules/media/src/Entity/MediaType.php index b0047fbeca413c45bd154ff987f2a0d8aca2c2e5..e80b2d3a1a38456fcde793c645c94b4c1fdc0239 100644 --- a/core/modules/media/src/Entity/MediaType.php +++ b/core/modules/media/src/Entity/MediaType.php @@ -2,9 +2,11 @@ namespace Drupal\media\Entity; +use Drupal\Core\Config\Action\Attribute\ActionMethod; use Drupal\Core\Config\Entity\ConfigEntityBundleBase; use Drupal\Core\Entity\EntityWithPluginCollectionInterface; use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\media\MediaTypeInterface; /** @@ -168,6 +170,7 @@ public function getDescription() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set description'), pluralize: FALSE)] public function setDescription($description) { return $this->set('description', $description); } @@ -237,6 +240,7 @@ public function getFieldMap() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set field mapping'), pluralize: FALSE)] public function setFieldMap(array $map) { return $this->set('field_map', $map); } diff --git a/core/modules/media/tests/src/Kernel/ConfigActionsTest.php b/core/modules/media/tests/src/Kernel/ConfigActionsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9ec9d21495ce54a28e3fa01c0e109030fb14ec7f --- /dev/null +++ b/core/modules/media/tests/src/Kernel/ConfigActionsTest.php @@ -0,0 +1,56 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\media\Kernel; + +use Drupal\Core\Config\Action\ConfigActionManager; +use Drupal\Core\Extension\ModuleInstallerInterface; +use Drupal\KernelTests\KernelTestBase; +use Drupal\media\Entity\MediaType; + +/** + * @group media + */ +class ConfigActionsTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['media']; + + private readonly ConfigActionManager $configActionManager; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->container->get(ModuleInstallerInterface::class)->install([ + 'media_test_type', + ]); + $this->configActionManager = $this->container->get('plugin.manager.config_action'); + } + + public function testConfigActions(): void { + $media_type = MediaType::load('test'); + $this->assertSame('Test type.', $media_type->getDescription()); + $this->assertSame(['metadata_attribute' => 'field_attribute_config_test'], $media_type->getFieldMap()); + + $this->configActionManager->applyAction( + 'entity_method:media.type:setDescription', + $media_type->getConfigDependencyName(), + 'Changed by a config action...', + ); + $this->configActionManager->applyAction( + 'entity_method:media.type:setFieldMap', + $media_type->getConfigDependencyName(), + ['foo' => 'baz'], + ); + + $media_type = MediaType::load('test'); + $this->assertSame('Changed by a config action...', $media_type->getDescription()); + $this->assertSame(['foo' => 'baz'], $media_type->getFieldMap()); + } + +} diff --git a/core/modules/node/src/Entity/NodeType.php b/core/modules/node/src/Entity/NodeType.php index a6a826f62975868489a8cded7b1b126c2b8155f8..934b4a2c38597b3dd936d93229b695628d42cefc 100644 --- a/core/modules/node/src/Entity/NodeType.php +++ b/core/modules/node/src/Entity/NodeType.php @@ -2,8 +2,10 @@ namespace Drupal\node\Entity; +use Drupal\Core\Config\Action\Attribute\ActionMethod; use Drupal\Core\Config\Entity\ConfigEntityBundleBase; use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\node\NodeTypeInterface; /** @@ -128,6 +130,7 @@ public function isLocked() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Automatically create new revisions'), pluralize: FALSE)] public function setNewRevision($new_revision) { $this->new_revision = $new_revision; } @@ -142,6 +145,7 @@ public function displaySubmitted() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set whether to display submission information'), pluralize: FALSE)] public function setDisplaySubmitted($display_submitted) { $this->display_submitted = $display_submitted; } @@ -156,6 +160,7 @@ public function getPreviewMode() { /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Set preview mode'), pluralize: FALSE)] public function setPreviewMode($preview_mode) { $this->preview_mode = $preview_mode; } diff --git a/core/modules/node/tests/src/Kernel/ConfigActionsTest.php b/core/modules/node/tests/src/Kernel/ConfigActionsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e9164b0295fecbe633abc043a129bb98ef591a96 --- /dev/null +++ b/core/modules/node/tests/src/Kernel/ConfigActionsTest.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\node\Kernel; + +use Drupal\Core\Config\Action\ConfigActionManager; +use Drupal\KernelTests\KernelTestBase; +use Drupal\node\Entity\NodeType; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; + +/** + * @group node + */ +class ConfigActionsTest extends KernelTestBase { + + use ContentTypeCreationTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = ['field', 'node', 'system', 'text', 'user']; + + private readonly ConfigActionManager $configActionManager; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installConfig('node'); + $this->configActionManager = $this->container->get('plugin.manager.config_action'); + } + + public function testConfigActions(): void { + $node_type = $this->createContentType(); + + $this->assertTrue($node_type->shouldCreateNewRevision()); + $this->assertSame(DRUPAL_OPTIONAL, $node_type->getPreviewMode()); + $this->assertTrue($node_type->displaySubmitted()); + + $this->configActionManager->applyAction( + 'entity_method:node.type:setNewRevision', + $node_type->getConfigDependencyName(), + FALSE, + ); + $this->configActionManager->applyAction( + 'entity_method:node.type:setPreviewMode', + $node_type->getConfigDependencyName(), + DRUPAL_REQUIRED, + ); + $this->configActionManager->applyAction( + 'entity_method:node.type:setDisplaySubmitted', + $node_type->getConfigDependencyName(), + FALSE, + ); + + $node_type = NodeType::load($node_type->id()); + $this->assertFalse($node_type->shouldCreateNewRevision()); + $this->assertSame(DRUPAL_REQUIRED, $node_type->getPreviewMode()); + $this->assertFalse($node_type->displaySubmitted()); + } + +} diff --git a/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml index 31aa1d614ca48cda1527816cb4a4df6ecb9d4778..eaf57abec3b379141bcf61b57998c238e4117ce7 100644 --- a/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml +++ b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml @@ -5,6 +5,12 @@ core.entity_view_display.*.*.*.third_party.entity_test: foo: type: string label: 'Label for foo' + noun: + type: string + label: 'A noun' + verb: + type: string + label: 'A verb' field.storage_settings.shape: type: mapping diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php index 42d26e0e0a0a131b23231c3a3d31fcd7a77af578..baf0a45b856c25782a2eaab41cdd63b115ab9e36 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php @@ -60,6 +60,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setDescription(t('The name of the test entity.')) ->setTranslatable(TRUE) ->setSetting('max_length', 32) + ->setDisplayConfigurable('view', TRUE) ->setDisplayOptions('view', [ 'label' => 'hidden', 'type' => 'string', diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/EntityMethodConfigActionsTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/EntityMethodConfigActionsTest.php index 5590fe0e75ad6a020c81d0601000bceee2b43053..4b68ee2855c550e3541e4e08808fe8b553fb23f7 100644 --- a/core/tests/Drupal/KernelTests/Core/Recipe/EntityMethodConfigActionsTest.php +++ b/core/tests/Drupal/KernelTests/Core/Recipe/EntityMethodConfigActionsTest.php @@ -4,32 +4,23 @@ namespace Drupal\KernelTests\Core\Recipe; +use Drupal\Core\Config\Action\ConfigActionManager; use Drupal\Core\Entity\EntityDisplayRepositoryInterface; -use Drupal\Core\Recipe\RecipeRunner; -use Drupal\FunctionalTests\Core\Recipe\RecipeTestTrait; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\entity_test\Entity\EntityTestBundle; use Drupal\KernelTests\KernelTestBase; -use Drupal\Tests\node\Traits\ContentTypeCreationTrait; /** * @group Recipe */ class EntityMethodConfigActionsTest extends KernelTestBase { - use ContentTypeCreationTrait; - use RecipeTestTrait; - /** * {@inheritdoc} */ - protected static $modules = [ - 'field', - 'layout_builder', - 'layout_discovery', - 'node', - 'system', - 'text', - 'user', - ]; + protected static $modules = ['config_test', 'entity_test', 'system']; + + private readonly ConfigActionManager $configActionManager; /** * {@inheritdoc} @@ -37,58 +28,145 @@ class EntityMethodConfigActionsTest extends KernelTestBase { protected function setUp(): void { parent::setUp(); - $this->installConfig('node'); - $this->createContentType(['type' => 'test']); + EntityTestBundle::create([ + 'id' => 'test', + 'label' => $this->randomString(), + ])->save(); $this->container->get(EntityDisplayRepositoryInterface::class) - ->getViewDisplay('node', 'test', 'full') + ->getViewDisplay('entity_test_with_bundle', 'test') ->save(); + + $this->configActionManager = $this->container->get('plugin.manager.config_action'); } public function testSetSingleThirdPartySetting(): void { - $recipe = <<<YAML -name: Third-party setting -config: - actions: - core.entity_view_display.node.test.full: - setThirdPartySetting: - module: layout_builder - key: enabled - value: true -YAML; - $recipe = $this->createRecipe($recipe); - RecipeRunner::processRecipe($recipe); + $this->configActionManager->applyAction( + 'entity_method:core.entity_view_display:setThirdPartySetting', + 'core.entity_view_display.entity_test_with_bundle.test.default', + [ + 'module' => 'entity_test', + 'key' => 'verb', + 'value' => 'Save', + ], + ); /** @var \Drupal\Core\Config\Entity\ThirdPartySettingsInterface $display */ $display = $this->container->get(EntityDisplayRepositoryInterface::class) - ->getViewDisplay('node', 'test', 'full'); - $this->assertTrue($display->getThirdPartySetting('layout_builder', 'enabled')); + ->getViewDisplay('entity_test_with_bundle', 'test'); + $this->assertSame('Save', $display->getThirdPartySetting('entity_test', 'verb')); } public function testSetMultipleThirdPartySettings(): void { - $recipe = <<<YAML -name: Third-party setting -config: - actions: - core.entity_view_display.node.test.full: - setThirdPartySettings: - - - module: layout_builder - key: enabled - value: true - - - module: layout_builder - key: allow_custom - value: true -YAML; - $recipe = $this->createRecipe($recipe); - RecipeRunner::processRecipe($recipe); + $this->configActionManager->applyAction( + 'entity_method:core.entity_view_display:setThirdPartySettings', + 'core.entity_view_display.entity_test_with_bundle.test.default', + [ + [ + 'module' => 'entity_test', + 'key' => 'noun', + 'value' => 'Spaceship', + ], + [ + 'module' => 'entity_test', + 'key' => 'verb', + 'value' => 'Explode', + ], + ], + ); /** @var \Drupal\Core\Config\Entity\ThirdPartySettingsInterface $display */ $display = $this->container->get(EntityDisplayRepositoryInterface::class) - ->getViewDisplay('node', 'test', 'full'); - $this->assertTrue($display->getThirdPartySetting('layout_builder', 'enabled')); - $this->assertTrue($display->getThirdPartySetting('layout_builder', 'allow_custom')); + ->getViewDisplay('entity_test_with_bundle', 'test'); + $this->assertSame('Spaceship', $display->getThirdPartySetting('entity_test', 'noun')); + $this->assertSame('Explode', $display->getThirdPartySetting('entity_test', 'verb')); + } + + /** + * @testWith ["set", {"property_name": "protected_property", "value": "Here be sandworms..."}] + * ["setMultiple", [{"property_name": "protected_property", "value": "Here be sandworms..."}, {"property_name": "label", "value": "New face"}]] + */ + public function testSet(string $action_name, array $value): void { + $storage = $this->container->get(EntityTypeManagerInterface::class) + ->getStorage('config_test'); + + $entity = $storage->create([ + 'id' => 'foo', + 'label' => 'Behold!', + 'protected_property' => 'Here be dragons...', + ]); + $this->assertSame('Behold!', $entity->get('label')); + $this->assertSame('Here be dragons...', $entity->get('protected_property')); + $entity->save(); + + $this->configActionManager->applyAction( + "entity_method:config_test.dynamic:$action_name", + $entity->getConfigDependencyName(), + $value, + ); + + $expected_values = array_is_list($value) ? $value : [$value]; + $entity = $storage->load('foo'); + foreach ($expected_values as ['property_name' => $name, 'value' => $value]) { + $this->assertSame($value, $entity->get($name)); + } + } + + /** + * @testWith [true, "setStatus", false, false] + * [false, "setStatus", true, true] + * [true, "disable", [], false] + * [false, "enable", [], true] + */ + public function testSetStatus(bool $initial_status, string $action_name, array|bool $value, bool $expected_status): void { + $storage = $this->container->get(EntityTypeManagerInterface::class) + ->getStorage('config_test'); + + $entity = $storage->create([ + 'id' => 'foo', + 'label' => 'Behold!', + 'status' => $initial_status, + ]); + $this->assertSame($initial_status, $entity->status()); + $entity->save(); + + $this->configActionManager->applyAction( + "entity_method:config_test.dynamic:$action_name", + $entity->getConfigDependencyName(), + $value, + ); + + $this->assertSame($expected_status, $storage->load('foo')->status()); + } + + /** + * @testWith ["hideComponent"] + * ["hideComponents"] + */ + public function testRemoveComponentFromDisplay(string $action_name): void { + $this->assertStringStartsWith('hideComponent', $action_name); + + /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $repository */ + $repository = $this->container->get(EntityDisplayRepositoryInterface::class); + + $view_display = $repository->getViewDisplay('entity_test_with_bundle', 'test'); + $this->assertIsArray($view_display->getComponent('name')); + + // The `hideComponent` action is an alias for `removeComponent`, proving + // that entity methods can be aliased. + $this->configActionManager->applyAction( + "entity_method:core.entity_view_display:$action_name", + $view_display->getConfigDependencyName(), + $action_name === 'hideComponents' ? ['name'] : 'name', + ); + + $view_display = $repository->getViewDisplay('entity_test_with_bundle', 'test'); + $this->assertNull($view_display->getComponent('name')); + + // The underlying action name should not be available. It should be hidden + // by the alias. + $plugin_id = str_replace('hide', 'remove', $action_name); + $this->assertFalse($this->configActionManager->hasDefinition($plugin_id)); } } diff --git a/core/tests/Drupal/Tests/Core/Config/Action/ActionMethodAttributeTest.php b/core/tests/Drupal/Tests/Core/Config/Action/ActionMethodAttributeTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a9dabacb5a5d7e2c26d320f13ba7e7e245b69bc5 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Config/Action/ActionMethodAttributeTest.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\Core\Config\Action; + +use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; +use Drupal\Core\Config\Action\Attribute\ActionMethod; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\Core\Config\Action\Attribute\ActionMethod + * @group Config + */ +class ActionMethodAttributeTest extends UnitTestCase { + + /** + * @covers ::__construct + */ + public function testInvalidFunctionName(): void { + $name = "hello Goodbye"; + $this->expectException(InvalidPluginDefinitionException::class); + $this->expectExceptionMessage("'$name' is not a valid PHP function name."); + new ActionMethod(name: $name); + } + +}