diff --git a/core/modules/layout_builder/layout_builder.post_update.php b/core/modules/layout_builder/layout_builder.post_update.php index 00033ae19a..a6e7bbd055 100644 --- a/core/modules/layout_builder/layout_builder.post_update.php +++ b/core/modules/layout_builder/layout_builder.post_update.php @@ -8,6 +8,7 @@ use Drupal\Component\Plugin\ConfigurablePluginInterface; use Drupal\Component\Plugin\ContextAwarePluginInterface; use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\Core\Site\Settings; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface; use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; @@ -65,55 +66,91 @@ function layout_builder_post_update_add_extra_fields(&$sandbox = NULL) { } /** - * Remove the layout_builder.entity context mapping from existing components. + * Removes the context mapping from all components in a section list. + * + * @param \Drupal\layout_builder\SectionListInterface $section_list + * The section list. + * + * @return bool + * TRUE if the section list has been updated, FALSE otherwise. */ -function layout_builder_post_update_remove_magic_context_mapping(&$sandbox = NULL) { - // Provide a generic callback for updating a section list to remove the - // 'layout_builder.entity' context mapping. - $callback = function (SectionListInterface $section_list) { - $result = FALSE; - foreach ($section_list->getSections() as $section) { - foreach ($section->getComponents() as $component) { - $block = $component->getPlugin(); - if ($block instanceof ContextAwarePluginInterface && $block instanceof ConfigurablePluginInterface) { - $context_mapping = $block->getContextMapping(); - if (isset($context_mapping['entity']) && $context_mapping['entity'] === 'layout_builder.entity') { - unset($context_mapping['entity']); - $block->setContextMapping($context_mapping); - $component->setConfiguration($block->getConfiguration()); - $result = TRUE; - } +function _layout_builder_remove_context_mapping(SectionListInterface $section_list) { + $result = FALSE; + foreach ($section_list->getSections() as $section) { + foreach ($section->getComponents() as $component) { + $block = $component->getPlugin(); + if ($block instanceof ContextAwarePluginInterface && $block instanceof ConfigurablePluginInterface) { + $context_mapping = $block->getContextMapping(); + if (isset($context_mapping['entity']) && $context_mapping['entity'] === 'layout_builder.entity') { + unset($context_mapping['entity']); + $block->setContextMapping($context_mapping); + $component->setConfiguration($block->getConfiguration()); + $result = TRUE; } } } - return $result; - }; + } + return $result; +} - // Apply the callback to all entity view displays. - \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'entity_view_display', $callback); +/** + * Remove the layout_builder.entity context mapping from config entities. + */ +function layout_builder_post_update_remove_context_mapping_from_config(&$sandbox = NULL) { + \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'entity_view_display', '_layout_builder_remove_context_mapping'); +} - // Find all entity types that have Layout Builder overrides enabled. - $entity_type_ids = array_unique(array_filter(array_map(function (LayoutEntityDisplayInterface $display) { - return $display->isOverridable() ? $display->getTargetEntityTypeId() : FALSE; - }, LayoutBuilderEntityViewDisplay::loadMultiple()))); +/** + * Remove the layout_builder.entity context mapping from content entities. + */ +function layout_builder_post_update_remove_context_mapping_from_content(&$sandbox = NULL) { + if (!isset($sandbox['callback'])) { + $sandbox['callback'] = '_layout_builder_remove_context_mapping'; + } - // Apply the callback to all overridden entities. - foreach ($entity_type_ids as $entity_type_id) { - $storage = \Drupal::entityTypeManager()->getStorage($entity_type_id); - if (!isset($sandbox[$entity_type_id])) { + // On the first batch, compute the total number of entities to be processed. + if (!isset($sandbox['total'])) { + // Find all entity types that have Layout Builder overrides enabled. + $entity_type_ids = array_unique(array_filter(array_map(function (LayoutEntityDisplayInterface $display) { + return $display->isOverridable() ? $display->getTargetEntityTypeId() : FALSE; + }, LayoutBuilderEntityViewDisplay::loadMultiple()))); + + // Track the current progress as well as the total number of entities. + $sandbox['progress'] = 0; + $sandbox['total'] = 0; + foreach ($entity_type_ids as $entity_type_id) { + $storage = \Drupal::entityTypeManager()->getStorage($entity_type_id); // Find all entities of this type that have a non-empty override field. - $sandbox[$entity_type_id]['entities'] = $storage->getQuery() + $entity_ids = $storage->getQuery() ->exists(OverridesSectionStorage::FIELD_NAME) ->accessCheck(FALSE) ->execute(); - $sandbox[$entity_type_id]['count'] = count($sandbox[$entity_type_id]['entities']); + $sandbox['total'] += count($entity_ids); + $sandbox['entity_ids'][$entity_type_id] = $entity_ids; } - $entities = $storage->loadMultiple(array_splice($sandbox[$entity_type_id]['entities'], 0, 50)); + } + + if (!empty($sandbox['entity_ids'])) { + // Find the first entity type to be processed. + $entity_type_id = key($sandbox['entity_ids']); + + // Remove the first batch of entity IDs from the list and process them. + $batch_size = Settings::get('entity_update_batch_size', 50); + $ids = array_splice($sandbox['entity_ids'][$entity_type_id], 0, $batch_size); + $entities = \Drupal::entityTypeManager()->getStorage($entity_type_id)->loadMultiple($ids); foreach ($entities as $entity) { $list = $entity->get(OverridesSectionStorage::FIELD_NAME); - if (call_user_func($callback, $list)) { + if (call_user_func($sandbox['callback'], $list)) { $entity->save(); } + $sandbox['progress']++; + } + // Remove the entity type from the list once all its entities have been + // processed. + if (empty($sandbox['entity_ids'][$entity_type_id])) { + unset($sandbox['entity_ids'][$entity_type_id]); } } + + $sandbox['#finished'] = empty($sandbox['total']) ? 1 : ($sandbox['progress'] / $sandbox['total']); } diff --git a/core/modules/layout_builder/tests/src/Kernel/RemoveContextMappingUpdateTest.php b/core/modules/layout_builder/tests/src/Kernel/RemoveContextMappingUpdateTest.php new file mode 100644 index 0000000000..c0f169bb53 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Kernel/RemoveContextMappingUpdateTest.php @@ -0,0 +1,216 @@ +installSchema('system', [ + 'sequences', + 'key_value', + 'key_value_expire', + ]); + $this->installSchema('node', ['node_access']); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installConfig('filter'); + $this->installConfig('node'); + + include_once __DIR__ . '/../../../layout_builder.post_update.php'; + } + + /** + * @covers \layout_builder_post_update_remove_context_mapping_from_content + */ + public function testUpdate() { + $section_data = [new Section('layout_onecol')]; + // Create 5 nodes each of two different bundles. + foreach (['bundle1', 'bundle2'] as $bundle) { + $this->createContentType(['type' => $bundle]); + LayoutBuilderEntityViewDisplay::load("node.$bundle.default") + ->enableLayoutBuilder() + ->setOverridable() + ->save(); + for ($i = 0; $i < 5; $i++) { + $this->createNode([ + 'type' => $bundle, + OverridesSectionStorage::FIELD_NAME => $section_data, + ]); + } + } + + // Create 5 users with layout overrides. + LayoutBuilderEntityViewDisplay::create([ + 'targetEntityType' => 'user', + 'bundle' => 'user', + 'mode' => 'default', + 'status' => TRUE, + ])->enableLayoutBuilder() + ->setOverridable() + ->save(); + for ($i = 0; $i < 5; $i++) { + $user = $this->createUser(); + $user->get(OverridesSectionStorage::FIELD_NAME)->setValue($section_data); + $user->save(); + } + + $settings = Settings::getInstance() ? Settings::getAll() : []; + $settings['entity_update_batch_size'] = 4; + new Settings($settings); + + $sandbox = []; + $sandbox['callback'] = function (SectionListInterface $list) { + $list->removeSection(0); + return TRUE; + }; + + // Run the first batch. + layout_builder_post_update_remove_context_mapping_from_content($sandbox); + $this->assertSame(15, $sandbox['total']); + $this->assertSame(4, $sandbox['progress']); + $this->assertSame(4 / 15, $sandbox['#finished']); + // Only the first 4 nodes have been updated. + $nodes = Node::loadMultiple(); + for ($i = 1; $i <= 4; $i++) { + $this->assertEntityUpdated($nodes[$i]); + } + for ($i = 5; $i <= 10; $i++) { + $this->assertEntityNotUpdated($nodes[$i]); + } + // None of the users have been updated. + $users = User::loadMultiple(); + foreach ($users as $user) { + $this->assertEntityNotUpdated($user); + } + + // Run the second batch. + layout_builder_post_update_remove_context_mapping_from_content($sandbox); + $this->assertSame(15, $sandbox['total']); + $this->assertSame(8, $sandbox['progress']); + $this->assertSame(8 / 15, $sandbox['#finished']); + // All nodes of the first bundle have been updated, and 3 of the second one. + $nodes = Node::loadMultiple(); + for ($i = 1; $i <= 8; $i++) { + $this->assertEntityUpdated($nodes[$i]); + } + for ($i = 9; $i <= 10; $i++) { + $this->assertEntityNotUpdated($nodes[$i]); + } + // None of the users have been updated. + $users = User::loadMultiple(); + foreach ($users as $user) { + $this->assertEntityNotUpdated($user); + } + + // Run the third batch. + layout_builder_post_update_remove_context_mapping_from_content($sandbox); + $this->assertSame(15, $sandbox['total']); + $this->assertSame(10, $sandbox['progress']); + $this->assertSame(10 / 15, $sandbox['#finished']); + // All nodes have been updated. + $nodes = Node::loadMultiple(); + foreach ($nodes as $node) { + $this->assertEntityUpdated($node); + } + // None of the users have been updated. + $users = User::loadMultiple(); + foreach ($users as $user) { + $this->assertEntityNotUpdated($user); + } + + // Run the fourth batch. + layout_builder_post_update_remove_context_mapping_from_content($sandbox); + $this->assertSame(15, $sandbox['total']); + $this->assertSame(14, $sandbox['progress']); + $this->assertSame(14 / 15, $sandbox['#finished']); + // All nodes have been updated. + $nodes = Node::loadMultiple(); + foreach ($nodes as $node) { + $this->assertEntityUpdated($node); + } + // Four of the users have been updated. + $users = User::loadMultiple(); + for ($i = 1; $i <= 4; $i++) { + $this->assertEntityUpdated($users[$i]); + } + $this->assertEntityNotUpdated($users[5]); + + // Run the final batch. + layout_builder_post_update_remove_context_mapping_from_content($sandbox); + $this->assertSame(15, $sandbox['total']); + $this->assertSame(15, $sandbox['progress']); + $this->assertSame(1, $sandbox['#finished']); + // All nodes have been updated. + $nodes = Node::loadMultiple(); + foreach ($nodes as $node) { + $this->assertEntityUpdated($node); + } + // All of the users have been updated. + $users = User::loadMultiple(); + foreach ($users as $user) { + $this->assertEntityUpdated($user); + } + } + + /** + * Asserts than an entity has been updated. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The fieldable entity to check. + */ + private function assertEntityUpdated(FieldableEntityInterface $entity) { + $this->assertTrue($entity->get(OverridesSectionStorage::FIELD_NAME)->isEmpty()); + } + + /** + * Asserts than an entity has not been updated. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The fieldable entity to check. + */ + private function assertEntityNotUpdated(FieldableEntityInterface $entity) { + $this->assertFalse($entity->get(OverridesSectionStorage::FIELD_NAME)->isEmpty()); + } + +}