diff --git a/core/modules/layout_builder/layout_builder.post_update.php b/core/modules/layout_builder/layout_builder.post_update.php index 1fcd74d437..bc14fc10b9 100644 --- a/core/modules/layout_builder/layout_builder.post_update.php +++ b/core/modules/layout_builder/layout_builder.post_update.php @@ -5,8 +5,13 @@ * Post update functions for Layout Builder. */ +use Drupal\Component\Plugin\ConfigurablePluginInterface; +use Drupal\Component\Plugin\ContextAwarePluginInterface; use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface; +use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; +use Drupal\layout_builder\SectionListInterface; /** * Rebuild plugin dependencies for all entity view displays. @@ -58,3 +63,57 @@ function layout_builder_post_update_add_extra_fields(&$sandbox = NULL) { return $result; }); } + +/** + * Remove the layout_builder.entity context mapping from existing components. + */ +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; + } + } + } + } + return $result; + }; + + // Apply the callback to all entity view displays. + \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'entity_view_display', $callback); + + // Find all entity types that have Layout Builder overrides enabled. + $entity_type_ids = array_filter(array_map(function (LayoutEntityDisplayInterface $display) { + return $display->isOverridable() ? $display->getTargetEntityTypeId() : FALSE; + }, LayoutBuilderEntityViewDisplay::loadMultiple())); + + // 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])) { + // Find all entities of this type that have a non-empty override field. + $sandbox[$entity_type_id]['entities'] = $storage->getQuery() + ->exists(OverridesSectionStorage::FIELD_NAME) + ->accessCheck(FALSE) + ->execute(); + $sandbox[$entity_type_id]['count'] = count($sandbox[$entity_type_id]['entities']); + } + $entities = $storage->loadMultiple(array_splice($sandbox[$entity_type_id]['entities'], 0, 50)); + foreach ($entities as $entity) { + $list = $entity->get(OverridesSectionStorage::FIELD_NAME); + if (call_user_func($callback, $list)) { + $entity->save(); + } + } + } +} diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php index 14899a816f..da847a65a4 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php @@ -15,8 +15,6 @@ use Drupal\field_ui\FieldUI; use Drupal\layout_builder\DefaultsSectionStorageInterface; use Drupal\layout_builder\Entity\LayoutBuilderSampleEntityGenerator; -use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface; -use Drupal\layout_builder\SectionListInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Routing\RouteCollection; diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php index 33cf559f5c..1439820b15 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php @@ -7,14 +7,12 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; -use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\Context\EntityContext; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; use Drupal\layout_builder\OverridesSectionStorageInterface; -use Drupal\layout_builder\SectionListInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Routing\RouteCollection; diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php b/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php index 5927701ed5..b7de4a6012 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php @@ -21,7 +21,17 @@ abstract class SectionStorageBase extends ContextAwarePluginBase implements Sect use LayoutBuilderRoutesTrait; /** - * {@inheritdoc} + * Sets the section list on the storage. + * + * @param \Drupal\layout_builder\SectionListInterface $section_list + * The section list. + * + * @internal + * This should only be called during section storage instantiation. + * + * @deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. The + * section list should be derived from context. See + * https://www.drupal.org/node/3016262. */ public function setSectionList(SectionListInterface $section_list) { @trigger_error('\Drupal\layout_builder\SectionStorageInterface::setSectionList() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. The section list should be derived from context. See https://www.drupal.org/node/3016262.', E_USER_DEPRECATED); diff --git a/core/modules/layout_builder/src/SectionStorageInterface.php b/core/modules/layout_builder/src/SectionStorageInterface.php index b79a702de3..48e5c3f342 100644 --- a/core/modules/layout_builder/src/SectionStorageInterface.php +++ b/core/modules/layout_builder/src/SectionStorageInterface.php @@ -35,23 +35,6 @@ public function getStorageId(); */ public function getStorageType(); - /** - * Sets the section list on the storage. - * - * @param \Drupal\layout_builder\SectionListInterface $section_list - * The section list. - * - * @return $this - * - * @internal - * This should only be called during section storage instantiation. - * - * @deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. The - * section list should be derived from context. See - * https://www.drupal.org/node/3016262. - */ - public function setSectionList(SectionListInterface $section_list); - /** * Derives the section list from the storage ID. * diff --git a/core/modules/layout_builder/tests/fixtures/update/layout-builder-field-block.php b/core/modules/layout_builder/tests/fixtures/update/layout-builder-field-block.php new file mode 100644 index 0000000000..51325dcda0 --- /dev/null +++ b/core/modules/layout_builder/tests/fixtures/update/layout-builder-field-block.php @@ -0,0 +1,278 @@ + 'layout_onecol', + 'layout_settings' => [], + 'components' => [ + 'some-uuid' => [ + 'uuid' => 'some-uuid', + 'region' => 'content', + 'configuration' => [ + 'id' => 'field_block:node:article:body', + 'label' => 'Body', + 'provider' => 'layout_builder', + 'label_display' => 'visible', + 'formatter' => [ + 'label' => 'above', + 'type' => 'text_default', + 'settings' => [], + 'third_party_settings' => [], + ], + 'context_mapping' => [ + 'entity' => 'layout_builder.entity', + ], + ], + 'additional' => [], + 'weight' => 0, + ], + ], +]; +$section_string = serialize(Section::fromArray($section_array)); + +$connection = Database::getConnection(); + +// Enable Layout Builder on an existing entity view display. +$display = $connection->select('config') + ->fields('config', ['data']) + ->condition('collection', '') + ->condition('name', 'core.entity_view_display.node.article.default') + ->execute() + ->fetchField(); +$display = unserialize($display); +$display['third_party_settings']['layout_builder']['enabled'] = TRUE; +$display['third_party_settings']['layout_builder']['allow_custom'] = TRUE; +$display['third_party_settings']['layout_builder']['sections'][] = $section_array; +$connection->update('config') + ->fields([ + 'data' => serialize($display), + 'collection' => '', + 'name' => 'core.entity_view_display.node.article.default', + ]) + ->condition('collection', '') + ->condition('name', 'core.entity_view_display.node.article.default') + ->execute(); + +// Add the layout builder field and field storage. +$connection->insert('config') + ->fields([ + 'collection', + 'name', + 'data', + ]) + ->values([ + 'collection' => '', + 'name' => 'field.field.node.article.layout_builder__layout', + 'data' => 'a:16:{s:4:"uuid";s:36:"3a7fb64f-d1cf-4fd5-bd07-9f81d893021a";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:2:{s:6:"config";a:2:{i:0;s:41:"field.storage.node.layout_builder__layout";i:1;s:17:"node.type.article";}s:6:"module";a:1:{i:0;s:14:"layout_builder";}}s:2:"id";s:35:"node.article.layout_builder__layout";s:10:"field_name";s:22:"layout_builder__layout";s:11:"entity_type";s:4:"node";s:6:"bundle";s:7:"article";s:5:"label";s:6:"Layout";s:11:"description";s:0:"";s:8:"required";b:0;s:12:"translatable";b:1;s:13:"default_value";a:0:{}s:22:"default_value_callback";s:0:"";s:8:"settings";a:0:{}s:10:"field_type";s:14:"layout_section";}', + ]) + ->values([ + 'collection' => '', + 'name' => 'field.storage.node.layout_builder__layout', + 'data' => 'a:16:{s:4:"uuid";s:36:"65b11331-3cd9-4c45-b7a3-6bcfbfd56c6e";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:1:{s:6:"module";a:2:{i:0;s:14:"layout_builder";i:1;s:4:"node";}}s:2:"id";s:27:"node.layout_builder__layout";s:10:"field_name";s:22:"layout_builder__layout";s:11:"entity_type";s:4:"node";s:4:"type";s:14:"layout_section";s:8:"settings";a:0:{}s:6:"module";s:14:"layout_builder";s:6:"locked";b:1;s:11:"cardinality";i:1;s:12:"translatable";b:1;s:7:"indexes";a:0:{}s:22:"persist_with_no_fields";b:0;s:14:"custom_storage";b:0;}', + ]) + ->execute(); +$connection->insert('key_value') + ->fields([ + 'collection', + 'name', + 'value', + ]) + ->values([ + 'collection' => 'config.entity.key_store.field_config', + 'name' => 'uuid:3a7fb64f-d1cf-4fd5-bd07-9f81d893021a', + 'value' => 'a:1:{i:0;s:47:"field.field.node.article.layout_builder__layout";}', + ]) + ->values([ + 'collection' => 'config.entity.key_store.field_storage_config', + 'name' => 'uuid:65b11331-3cd9-4c45-b7a3-6bcfbfd56c6e', + 'value' => 'a:1:{i:0;s:41:"field.storage.node.layout_builder__layout";}', + ]) + ->values([ + 'collection' => 'entity.storage_schema.sql', + 'name' => 'node.field_schema_data.layout_builder__layout', + 'value' => 'a:2:{s:28:"node__layout_builder__layout";a:4:{s:11:"description";s:51:"Data storage for node field layout_builder__layout.";s:6:"fields";a:7:{s:6:"bundle";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:128;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:88:"The field instance bundle to which this row belongs, used when deleting a field instance";}s:7:"deleted";a:5:{s:4:"type";s:3:"int";s:4:"size";s:4:"tiny";s:8:"not null";b:1;s:7:"default";i:0;s:11:"description";s:60:"A boolean indicating whether this data item has been deleted";}s:9:"entity_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:38:"The entity id this data is attached to";}s:11:"revision_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:47:"The entity revision id this data is attached to";}s:8:"langcode";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:32;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:37:"The language code for this data item.";}s:5:"delta";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:67:"The sequence number for this data item, used for multi-value fields";}s:30:"layout_builder__layout_section";a:4:{s:4:"type";s:4:"blob";s:4:"size";s:6:"normal";s:9:"serialize";b:1;s:8:"not null";b:0;}}s:11:"primary key";a:4:{i:0;s:9:"entity_id";i:1;s:7:"deleted";i:2;s:5:"delta";i:3;s:8:"langcode";}s:7:"indexes";a:2:{s:6:"bundle";a:1:{i:0;s:6:"bundle";}s:11:"revision_id";a:1:{i:0;s:11:"revision_id";}}}s:37:"node_revision__layout_builder__layout";a:4:{s:11:"description";s:63:"Revision archive storage for node field layout_builder__layout.";s:6:"fields";a:7:{s:6:"bundle";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:128;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:88:"The field instance bundle to which this row belongs, used when deleting a field instance";}s:7:"deleted";a:5:{s:4:"type";s:3:"int";s:4:"size";s:4:"tiny";s:8:"not null";b:1;s:7:"default";i:0;s:11:"description";s:60:"A boolean indicating whether this data item has been deleted";}s:9:"entity_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:38:"The entity id this data is attached to";}s:11:"revision_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:47:"The entity revision id this data is attached to";}s:8:"langcode";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:32;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:37:"The language code for this data item.";}s:5:"delta";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:67:"The sequence number for this data item, used for multi-value fields";}s:30:"layout_builder__layout_section";a:4:{s:4:"type";s:4:"blob";s:4:"size";s:6:"normal";s:9:"serialize";b:1;s:8:"not null";b:0;}}s:11:"primary key";a:5:{i:0;s:9:"entity_id";i:1;s:11:"revision_id";i:2;s:7:"deleted";i:3;s:5:"delta";i:4;s:8:"langcode";}s:7:"indexes";a:2:{s:6:"bundle";a:1:{i:0;s:6:"bundle";}s:11:"revision_id";a:1:{i:0;s:11:"revision_id";}}}}', + ]) + ->execute(); +$connection->update('key_value') + ->fields([ + 'collection' => 'entity.definitions.bundle_field_map', + 'name' => 'node', + 'value' => 'a:5:{s:11:"field_image";a:2:{s:4:"type";s:5:"image";s:7:"bundles";a:1:{s:7:"article";s:7:"article";}}s:7:"comment";a:2:{s:4:"type";s:7:"comment";s:7:"bundles";a:1:{s:7:"article";s:7:"article";}}s:10:"field_tags";a:2:{s:4:"type";s:16:"entity_reference";s:7:"bundles";a:1:{s:7:"article";s:7:"article";}}s:4:"body";a:2:{s:4:"type";s:17:"text_with_summary";s:7:"bundles";a:2:{s:4:"page";s:4:"page";s:7:"article";s:7:"article";}}s:22:"layout_builder__layout";a:2:{s:4:"type";s:14:"layout_section";s:7:"bundles";a:1:{s:7:"article";s:7:"article";}}}', + ]) + ->condition('collection', 'entity.definitions.bundle_field_map') + ->condition('name', 'node') + ->execute(); + +// Create tables for the layout builder field. +$connection->schema()->createTable('node__layout_builder__layout', [ + 'fields' => [ + 'bundle' => [ + 'type' => 'varchar_ascii', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ], + 'deleted' => [ + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'tiny', + 'default' => '0', + ], + 'entity_id' => [ + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ], + 'revision_id' => [ + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ], + 'langcode' => [ + 'type' => 'varchar_ascii', + 'not null' => TRUE, + 'length' => '32', + 'default' => '', + ], + 'delta' => [ + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ], + 'layout_builder__layout_section' => [ + 'type' => 'blob', + 'not null' => FALSE, + 'size' => 'normal', + ], + ], + 'primary key' => [ + 'entity_id', + 'deleted', + 'delta', + 'langcode', + ], + 'indexes' => [ + 'bundle' => [ + 'bundle', + ], + 'revision_id' => [ + 'revision_id', + ], + ], + 'mysql_character_set' => 'utf8mb4', +]); +$connection->schema()->createTable('node_revision__layout_builder__layout', [ + 'fields' => [ + 'bundle' => [ + 'type' => 'varchar_ascii', + 'not null' => TRUE, + 'length' => '128', + 'default' => '', + ], + 'deleted' => [ + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'tiny', + 'default' => '0', + ], + 'entity_id' => [ + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ], + 'revision_id' => [ + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ], + 'langcode' => [ + 'type' => 'varchar_ascii', + 'not null' => TRUE, + 'length' => '32', + 'default' => '', + ], + 'delta' => [ + 'type' => 'int', + 'not null' => TRUE, + 'size' => 'normal', + 'unsigned' => TRUE, + ], + 'layout_builder__layout_section' => [ + 'type' => 'blob', + 'not null' => FALSE, + 'size' => 'normal', + ], + ], + 'primary key' => [ + 'entity_id', + 'revision_id', + 'deleted', + 'delta', + 'langcode', + ], + 'indexes' => [ + 'bundle' => [ + 'bundle', + ], + 'revision_id' => [ + 'revision_id', + ], + ], + 'mysql_character_set' => 'utf8mb4', +]); +// Add the layout data to the node. +$connection->insert('node__layout_builder__layout') + ->fields([ + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'langcode', + 'delta', + 'layout_builder__layout_section', + ]) + ->values([ + 'bundle' => 'article', + 'deleted' => '0', + 'entity_id' => '1', + 'revision_id' => '1', + 'langcode' => 'en', + 'delta' => '0', + 'layout_builder__layout_section' => $section_string, + ]) + ->execute(); +$connection->insert('node_revision__layout_builder__layout') + ->fields([ + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'langcode', + 'delta', + 'layout_builder__layout_section', + ]) + ->values([ + 'bundle' => 'article', + 'deleted' => '0', + 'entity_id' => '1', + 'revision_id' => '1', + 'langcode' => 'en', + 'delta' => '0', + 'layout_builder__layout_section' => $section_string, + ]) + ->execute(); diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/SimpleConfigSectionStorage.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/SimpleConfigSectionStorage.php index a7b3e58e65..dea461306f 100644 --- a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/SimpleConfigSectionStorage.php +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Plugin/SectionStorage/SimpleConfigSectionStorage.php @@ -198,14 +198,6 @@ public function getContextsDuringPreview() { return $this->getContexts(); } - /** - * {@inheritdoc} - */ - public function setSectionList(SectionListInterface $section_list) { - @trigger_error('\Drupal\layout_builder\SectionStorageInterface::setSectionList() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. The section list should be derived from context. See https://www.drupal.org/node/3016262.', E_USER_DEPRECATED); - return $this; - } - /** * {@inheritdoc} */ diff --git a/core/modules/layout_builder/tests/src/Functional/Update/LayoutBuilderContextMappingUpdatePathTest.php b/core/modules/layout_builder/tests/src/Functional/Update/LayoutBuilderContextMappingUpdatePathTest.php new file mode 100644 index 0000000000..35e7044dac --- /dev/null +++ b/core/modules/layout_builder/tests/src/Functional/Update/LayoutBuilderContextMappingUpdatePathTest.php @@ -0,0 +1,44 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.filled.standard.php.gz', + __DIR__ . '/../../../fixtures/update/layout-builder.php', + __DIR__ . '/../../../fixtures/update/layout-builder-field-block.php', + ]; + } + + /** + * Tests the upgrade path for enabling Layout Builder. + */ + public function testRunUpdates() { + $assert_session = $this->assertSession(); + + $this->runUpdates(); + + // Ensure that both defaults and overrides continue to function as expected. + $this->drupalLogin($this->rootUser); + $this->drupalGet('admin/structure/types/manage/article/display-layout/default'); + $assert_session->statusCodeEquals(200); + $assert_session->elementExists('css', '.field--name-body'); + $this->drupalGet('node/1'); + $assert_session->statusCodeEquals(200); + $assert_session->elementExists('css', '.field--name-body'); + } + +}