Commit 2597462d authored by catch's avatar catch

Issue #2949351 by alexpott, Lendude, tim.plunkett, dawehner: Add a helper...

Issue #2949351 by alexpott, Lendude, tim.plunkett, dawehner: Add a helper class to make updating configuration simple
parent 7e110101
<?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'];
}
}
......@@ -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}
*/
......
......@@ -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;
});
}
<?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');
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment