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