diff --git a/core/lib/Drupal/Core/Config/Action/Plugin/ConfigAction/SetProperties.php b/core/lib/Drupal/Core/Config/Action/Plugin/ConfigAction/SetProperties.php new file mode 100644 index 0000000000000000000000000000000000000000..0ec3c7fc3253aa06d86f5728bb9fbfb467292741 --- /dev/null +++ b/core/lib/Drupal/Core/Config/Action/Plugin/ConfigAction/SetProperties.php @@ -0,0 +1,80 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Config\Action\Plugin\ConfigAction; + +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Config\Action\Attribute\ConfigAction; +use Drupal\Core\Config\Action\ConfigActionException; +use Drupal\Core\Config\Action\ConfigActionPluginInterface; +use Drupal\Core\Config\ConfigManagerInterface; +use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * @internal + * This API is experimental. + */ +#[ConfigAction( + id: 'setProperties', + admin_label: new TranslatableMarkup('Set property of a config entity'), + entity_types: ['*'], +)] +final class SetProperties implements ConfigActionPluginInterface, ContainerFactoryPluginInterface { + + public function __construct( + private readonly ConfigManagerInterface $configManager, + ) {} + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $container->get(ConfigManagerInterface::class), + ); + } + + /** + * {@inheritdoc} + */ + public function apply(string $configName, mixed $values): void { + $entity = $this->configManager->loadConfigEntityByName($configName); + assert($entity instanceof ConfigEntityInterface); + + assert(is_array($values)); + assert(!array_is_list($values)); + + // Don't allow the ID or UUID to be changed. + $entity_keys = $entity->getEntityType()->getKeys(); + $forbidden_keys = array_filter([ + $entity_keys['id'], + $entity_keys['uuid'], + ]); + + foreach ($values as $property_name => $value) { + if (in_array($property_name, $forbidden_keys, TRUE)) { + throw new ConfigActionException("Entity key '$property_name' cannot be changed by the setProperties config action."); + } + $parts = explode('.', $property_name); + + $property_value = $entity->get($parts[0]); + if (count($parts) > 1) { + if (isset($property_value) && !is_array($property_value)) { + throw new ConfigActionException('The setProperties config action can only set nested values on arrays.'); + } + $property_value ??= []; + NestedArray::setValue($property_value, array_slice($parts, 1), $value); + } + else { + $property_value = $value; + } + $entity->set($parts[0], $property_value); + } + $entity->save(); + } + +} diff --git a/core/lib/Drupal/Core/Config/Action/Plugin/ConfigAction/SimpleConfigUpdate.php b/core/lib/Drupal/Core/Config/Action/Plugin/ConfigAction/SimpleConfigUpdate.php index 2e9064f3a3fc75be4183b1e64ffb0589dee41f82..8df05ae91c4df895fe65aae52d7f142a70abeb93 100644 --- a/core/lib/Drupal/Core/Config/Action/Plugin/ConfigAction/SimpleConfigUpdate.php +++ b/core/lib/Drupal/Core/Config/Action/Plugin/ConfigAction/SimpleConfigUpdate.php @@ -8,6 +8,7 @@ use Drupal\Core\Config\Action\ConfigActionException; use Drupal\Core\Config\Action\ConfigActionPluginInterface; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Config\ConfigManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -22,31 +23,31 @@ )] final class SimpleConfigUpdate implements ConfigActionPluginInterface, ContainerFactoryPluginInterface { - /** - * Constructs a SimpleConfigUpdate object. - * - * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory - * The config factory. - */ public function __construct( - protected readonly ConfigFactoryInterface $configFactory, - ) { - } + private readonly ConfigFactoryInterface $configFactory, + private readonly ConfigManagerInterface $configManager, + ) {} /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { - return new static($container->get('config.factory')); + return new static( + $container->get(ConfigFactoryInterface::class), + $container->get(ConfigManagerInterface::class), + ); } /** * {@inheritdoc} */ public function apply(string $configName, mixed $value): void { + if ($this->configManager->getEntityTypeIdByName($configName)) { + // @todo Make this an exception in https://www.drupal.org/node/3515544. + @trigger_error('Using the simpleConfigUpdate config action on config entities is deprecated in drupal:11.2.0 and throws an exception in drupal:12.0.0. Use the setProperties action instead. See https://www.drupal.org/node/3515543', E_USER_DEPRECATED); + } + $config = $this->configFactory->getEditable($configName); - // @todo https://www.drupal.org/i/3439713 Should we error if this is a - // config entity? if ($config->isNew()) { throw new ConfigActionException(sprintf('Config %s does not exist so can not be updated', $configName)); } diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/ConfigActionValidationTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/ConfigActionValidationTest.php index e5df2b824b19f558da601441790e733a18b93b0e..871e0775777033fb52b7088a6a739ed6cf6e97ec 100644 --- a/core/tests/Drupal/KernelTests/Core/Recipe/ConfigActionValidationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Recipe/ConfigActionValidationTest.php @@ -77,7 +77,7 @@ public function testConfigActionsAreValidated(string $entity_type_id): void { config: actions: $config_name: - simpleConfigUpdate: + setProperties: $label_key: '' YAML; diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/EntityMethodConfigActionsTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/EntityMethodConfigActionsTest.php index 8c08c3b8c848adde724af93690dd3202705f8189..f1ae746ddd025389d1e50bacf2a0e8b2d275730c 100644 --- a/core/tests/Drupal/KernelTests/Core/Recipe/EntityMethodConfigActionsTest.php +++ b/core/tests/Drupal/KernelTests/Core/Recipe/EntityMethodConfigActionsTest.php @@ -4,21 +4,27 @@ namespace Drupal\KernelTests\Core\Recipe; +use Drupal\block\Entity\Block; +use Drupal\Core\Config\Action\ConfigActionException; use Drupal\Core\Config\Action\ConfigActionManager; use Drupal\Core\Entity\EntityDisplayRepositoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Extension\ThemeInstallerInterface; use Drupal\entity_test\Entity\EntityTestBundle; use Drupal\KernelTests\KernelTestBase; +use Drupal\Tests\block\Traits\BlockCreationTrait; /** * @group Recipe */ class EntityMethodConfigActionsTest extends KernelTestBase { + use BlockCreationTrait; + /** * {@inheritdoc} */ - protected static $modules = ['config_test', 'entity_test', 'system']; + protected static $modules = ['block', 'config_test', 'entity_test', 'system']; /** * The configuration action manager. @@ -172,4 +178,90 @@ public function testRemoveComponentFromDisplay(string $action_name): void { $this->assertFalse($this->configActionManager->hasDefinition($plugin_id)); } + /** + * Test setting a nested property on a config entity. + */ + public function testSetNestedProperty(): void { + $this->container->get(ThemeInstallerInterface::class) + ->install(['claro']); + $block = $this->placeBlock('local_tasks_block', ['theme' => 'claro']); + + $this->configActionManager->applyAction( + 'setProperties', + $block->getConfigDependencyName(), + ['settings.label' => 'Magic!'], + ); + $settings = Block::load($block->id())->get('settings'); + $this->assertSame('Magic!', $settings['label']); + + // If the property is not nested, it should still work. + $settings['label'] = 'Mundane'; + $this->configActionManager->applyAction( + 'setProperties', + $block->getConfigDependencyName(), + ['settings' => $settings], + ); + $settings = Block::load($block->id())->get('settings'); + $this->assertSame('Mundane', $settings['label']); + + // We can use this to set a scalar property normally. + $this->configActionManager->applyAction( + 'setProperties', + $block->getConfigDependencyName(), + ['region' => 'highlighted'], + ); + $this->assertSame('highlighted', Block::load($block->id())->getRegion()); + + // We should get an exception if we try to set a nested value on a property + // that isn't an array. + $this->expectException(ConfigActionException::class); + $this->expectExceptionMessage('The setProperties config action can only set nested values on arrays.'); + $this->configActionManager->applyAction( + 'setProperties', + $block->getConfigDependencyName(), + ['theme.name' => 'stark'], + ); + } + + /** + * Tests that the setProperties action refuses to modify entity IDs or UUIDs. + * + * @testWith ["id"] + * ["uuid"] + */ + public function testSetPropertiesWillNotChangeEntityKeys(string $key): void { + $view_display = $this->container->get(EntityDisplayRepositoryInterface::class) + ->getViewDisplay('entity_test_with_bundle', 'test'); + $this->assertFalse($view_display->isNew()); + + $property_name = $view_display->getEntityType()->getKey($key); + $this->assertNotEmpty($property_name); + + $this->expectException(ConfigActionException::class); + $this->expectExceptionMessage("Entity key '$property_name' cannot be changed by the setProperties config action."); + $this->configActionManager->applyAction( + 'setProperties', + $view_display->getConfigDependencyName(), + [$property_name => '12345'], + ); + } + + /** + * Tests that the simpleConfigUpdate action cannot be used on entities. + * + * @group legacy + */ + public function testSimpleConfigUpdateFailsOnEntities(): void { + $view_display = $this->container->get(EntityDisplayRepositoryInterface::class) + ->getViewDisplay('entity_test_with_bundle', 'test'); + $view_display->save(); + + $this->expectDeprecation('Using the simpleConfigUpdate config action on config entities is deprecated in drupal:11.2.0 and throws an exception in drupal:12.0.0. Use the setProperties action instead. See https://www.drupal.org/node/3515543'); + $this->configActionManager->applyAction( + 'simpleConfigUpdate', + $view_display->getConfigDependencyName(), + ['hidden.uid' => TRUE], + ); + } + }