From 2597462d8d66ecd3543173a50b3cb638a7088eae Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org> Date: Wed, 11 Apr 2018 13:17:00 +0100 Subject: [PATCH] Issue #2949351 by alexpott, Lendude, tim.plunkett, dawehner: Add a helper class to make updating configuration simple --- .../Config/Entity/ConfigEntityUpdater.php | 119 ++++++++++++++++ .../config_test/src/Entity/ConfigTest.php | 11 ++ core/modules/views/views.post_update.php | 26 ++-- .../Config/Entity/ConfigEntityUpdaterTest.php | 127 ++++++++++++++++++ 4 files changed, 266 insertions(+), 17 deletions(-) create mode 100644 core/lib/Drupal/Core/Config/Entity/ConfigEntityUpdater.php create mode 100644 core/tests/Drupal/KernelTests/Core/Config/Entity/ConfigEntityUpdaterTest.php diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityUpdater.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityUpdater.php new file mode 100644 index 000000000000..37e5fb1aa87b --- /dev/null +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityUpdater.php @@ -0,0 +1,119 @@ +<?php + +namespace Drupal\Core\Config\Entity; + +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * A utility class to make updating configuration entities simple. + * + * Use this in a post update function like so: + * @code + * // Update the dependencies of all Vocabulary configuration entities. + * \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'taxonomy_vocabulary'); + * @endcode + * + * The number of entities processed in each batch is determined by the + * 'entity_update_batch_size' setting. + * + * @see default.settings.php + */ +class ConfigEntityUpdater implements ContainerInjectionInterface { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The number of entities to process in each batch. + * @var int + */ + protected $batchSize; + + /** + * ConfigEntityUpdater constructor. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param int $batch_size + * The number of entities to process in each batch. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, $batch_size) { + $this->entityTypeManager = $entity_type_manager; + $this->batchSize = $batch_size; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('settings')->get('entity_update_batch_size', 50) + ); + } + + /** + * Updates configuration entities as part of a Drupal update. + * + * @param array $sandbox + * Stores information for batch updates. + * @param string $entity_type_id + * The configuration entity type ID. For example, 'view' or 'vocabulary'. + * @param callable $callback + * (optional) A callback to determine if a configuration entity should be + * saved. The callback will be passed each entity of the provided type that + * exists. The callback should not save an entity itself. Return TRUE to + * save an entity. The callback can make changes to an entity. Note that all + * changes should comply with schema as an entity's data will not be + * validated against schema on save to avoid unexpected errors. If a + * callback is not provided, the default behaviour is to update the + * dependencies if required. + * + * @see hook_post_update_NAME() + * + * @api + * + * @throws \InvalidArgumentException + * Thrown when the provided entity type ID is not a configuration entity + * type. + */ + public function update(array &$sandbox, $entity_type_id, callable $callback = NULL) { + $storage = $this->entityTypeManager->getStorage($entity_type_id); + $sandbox_key = 'config_entity_updater:' . $entity_type_id; + if (!isset($sandbox[$sandbox_key])) { + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + if (!($entity_type instanceof ConfigEntityTypeInterface)) { + throw new \InvalidArgumentException("The provided entity type ID '$entity_type_id' is not a configuration entity type"); + } + $sandbox[$sandbox_key]['entities'] = $storage->getQuery()->accessCheck(FALSE)->execute(); + $sandbox[$sandbox_key]['count'] = count($sandbox[$sandbox_key]['entities']); + } + + // The default behaviour is to fix dependencies. + if ($callback === NULL) { + $callback = function ($entity) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */ + $original_dependencies = $entity->getDependencies(); + return $original_dependencies !== $entity->calculateDependencies()->getDependencies(); + }; + } + + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */ + $entities = $storage->loadMultiple(array_splice($sandbox[$sandbox_key]['entities'], 0, $this->batchSize)); + foreach ($entities as $entity) { + if (call_user_func($callback, $entity)) { + $entity->trustData(); + $entity->save(); + } + } + + $sandbox['#finished'] = empty($sandbox[$sandbox_key]['entities']) ? 1 : ($sandbox[$sandbox_key]['count'] - count($sandbox[$sandbox_key]['entities'])) / $sandbox[$sandbox_key]['count']; + } + +} diff --git a/core/modules/config/tests/config_test/src/Entity/ConfigTest.php b/core/modules/config/tests/config_test/src/Entity/ConfigTest.php index ee0e35a2cd6f..d1a7249bede9 100644 --- a/core/modules/config/tests/config_test/src/Entity/ConfigTest.php +++ b/core/modules/config/tests/config_test/src/Entity/ConfigTest.php @@ -113,6 +113,17 @@ public static function postDelete(EntityStorageInterface $storage, array $entiti } } + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + parent::calculateDependencies(); + if ($module = \Drupal::state()->get('config_test_new_dependency', FALSE)) { + $this->addDependency('module', $module); + } + return $this; + } + /** * {@inheritdoc} */ diff --git a/core/modules/views/views.post_update.php b/core/modules/views/views.post_update.php index f1030c71c6fe..237cb806de50 100644 --- a/core/modules/views/views.post_update.php +++ b/core/modules/views/views.post_update.php @@ -5,6 +5,7 @@ * Post update functions for Views. */ +use Drupal\Core\Config\Entity\ConfigEntityUpdater; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\views\Entity\View; use Drupal\views\Plugin\views\filter\NumericFilter; @@ -352,23 +353,14 @@ function views_post_update_views_data_table_dependencies(&$sandbox = NULL) { * Fix cache max age for table displays. */ function views_post_update_table_display_cache_max_age(&$sandbox = NULL) { - $storage = \Drupal::entityTypeManager()->getStorage('view'); - if (!isset($sandbox['views'])) { - $sandbox['views'] = $storage->getQuery()->accessCheck(FALSE)->execute(); - $sandbox['count'] = count($sandbox['views']); - } - - for ($i = 0; $i < 10 && count($sandbox['views']); $i++) { - $view_id = array_shift($sandbox['views']); - if ($view = $storage->load($view_id)) { - $displays = $view->get('display'); - foreach ($displays as $display_name => &$display) { - if (isset($display['display_options']['style']['type']) && $display['display_options']['style']['type'] === 'table') { - $view->save(); - } + \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function ($view) { + /** @var \Drupal\views\ViewEntityInterface $view */ + $displays = $view->get('display'); + foreach ($displays as $display_name => &$display) { + if (isset($display['display_options']['style']['type']) && $display['display_options']['style']['type'] === 'table') { + return TRUE; } } - } - - $sandbox['#finished'] = empty($sandbox['views']) ? 1 : ($sandbox['count'] - count($sandbox['views'])) / $sandbox['count']; + return FALSE; + }); } diff --git a/core/tests/Drupal/KernelTests/Core/Config/Entity/ConfigEntityUpdaterTest.php b/core/tests/Drupal/KernelTests/Core/Config/Entity/ConfigEntityUpdaterTest.php new file mode 100644 index 000000000000..5aae17f7d64c --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Config/Entity/ConfigEntityUpdaterTest.php @@ -0,0 +1,127 @@ +<?php + +namespace Drupal\KernelTests\Core\Config\Entity; + +use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\Core\Site\Settings; +use Drupal\KernelTests\KernelTestBase; + +/** + * Tests \Drupal\Core\Config\Entity\ConfigEntityUpdater. + * + * @coversDefaultClass \Drupal\Core\Config\Entity\ConfigEntityUpdater + * @group config + */ +class ConfigEntityUpdaterTest extends KernelTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = ['config_test']; + + /** + * @covers ::update + */ + public function testUpdate() { + // Create some entities to update. + $storage = $this->container->get('entity_type.manager')->getStorage('config_test'); + for ($i = 0; $i < 15; $i++) { + $entity_id = 'config_test_' . $i; + $storage->create(['id' => $entity_id, 'label' => $entity_id])->save(); + } + + // Set up the updater. + $sandbox = []; + $settings = Settings::getInstance() ? Settings::getAll() : []; + $settings['entity_update_batch_size'] = 10; + new Settings($settings); + $updater = $this->container->get('class_resolver')->getInstanceFromDefinition(ConfigEntityUpdater::class); + + $callback = function ($config_entity) { + /** @var \Drupal\config_test\Entity\ConfigTest $config_entity */ + $number = (int) str_replace('config_test_', '', $config_entity->id()); + // Only update even numbered entities. + if ($number % 2 == 0) { + $config_entity->set('label', $config_entity->label . ' (updated)'); + return TRUE; + } + return FALSE; + }; + + // This should run against the first 10 entities. The even numbered labels + // will have been updated. + $updater->update($sandbox, 'config_test', $callback); + $entities = $storage->loadMultiple(); + $this->assertEquals('config_test_8 (updated)', $entities['config_test_8']->label()); + $this->assertEquals('config_test_9', $entities['config_test_9']->label()); + $this->assertEquals('config_test_10', $entities['config_test_10']->label()); + $this->assertEquals('config_test_14', $entities['config_test_14']->label()); + $this->assertEquals(15, $sandbox['config_entity_updater:config_test']['count']); + $this->assertCount(5, $sandbox['config_entity_updater:config_test']['entities']); + $this->assertEquals(10 / 15, $sandbox['#finished']); + + // Update the rest. + $updater->update($sandbox, 'config_test', $callback); + $entities = $storage->loadMultiple(); + $this->assertEquals('config_test_8 (updated)', $entities['config_test_8']->label()); + $this->assertEquals('config_test_9', $entities['config_test_9']->label()); + $this->assertEquals('config_test_10 (updated)', $entities['config_test_10']->label()); + $this->assertEquals('config_test_14 (updated)', $entities['config_test_14']->label()); + $this->assertEquals(1, $sandbox['#finished']); + $this->assertCount(0, $sandbox['config_entity_updater:config_test']['entities']); + } + + /** + * @covers ::update + */ + public function testUpdateDefaultCallback() { + // Create some entities to update. + $storage = $this->container->get('entity_type.manager')->getStorage('config_test'); + for ($i = 0; $i < 15; $i++) { + $entity_id = 'config_test_' . $i; + $storage->create(['id' => $entity_id, 'label' => $entity_id])->save(); + } + + // Set up the updater. + $sandbox = []; + $settings = Settings::getInstance() ? Settings::getAll() : []; + $settings['entity_update_batch_size'] = 9; + new Settings($settings); + $updater = $this->container->get('class_resolver')->getInstanceFromDefinition(ConfigEntityUpdater::class); + // Cause a dependency to be added during an update. + \Drupal::state()->set('config_test_new_dependency', 'added_dependency'); + + // This should run against the first 10 entities. + $updater->update($sandbox, 'config_test'); + $entities = $storage->loadMultiple(); + $this->assertEquals(['added_dependency'], $entities['config_test_7']->getDependencies()['module']); + $this->assertEquals(['added_dependency'], $entities['config_test_8']->getDependencies()['module']); + $this->assertEquals([], $entities['config_test_9']->getDependencies()); + $this->assertEquals([], $entities['config_test_14']->getDependencies()); + $this->assertEquals(15, $sandbox['config_entity_updater:config_test']['count']); + $this->assertCount(6, $sandbox['config_entity_updater:config_test']['entities']); + $this->assertEquals(9 / 15, $sandbox['#finished']); + + // Update the rest. + $updater->update($sandbox, 'config_test'); + $entities = $storage->loadMultiple(); + $this->assertEquals(['added_dependency'], $entities['config_test_9']->getDependencies()['module']); + $this->assertEquals(['added_dependency'], $entities['config_test_14']->getDependencies()['module']); + $this->assertEquals(1, $sandbox['#finished']); + $this->assertCount(0, $sandbox['config_entity_updater:config_test']['entities']); + } + + /** + * @covers ::update + */ + public function testUpdateException() { + $this->enableModules(['entity_test']); + $this->setExpectedException(\InvalidArgumentException::class, 'The provided entity type ID \'entity_test_mul_changed\' is not a configuration entity type'); + $updater = $this->container->get('class_resolver')->getInstanceFromDefinition(ConfigEntityUpdater::class); + $sandbox = []; + $updater->update($sandbox, 'entity_test_mul_changed'); + } + +} -- GitLab