diff --git a/core/modules/layout_builder/src/Controller/AddSectionController.php b/core/modules/layout_builder/src/Controller/AddSectionController.php index d6771082382bfae78569af177548887e8fa7d28c..fa522b547224a920eacfc60b3c279033242ac3e7 100644 --- a/core/modules/layout_builder/src/Controller/AddSectionController.php +++ b/core/modules/layout_builder/src/Controller/AddSectionController.php @@ -6,6 +6,7 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; +use Drupal\layout_builder\Section; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -63,13 +64,9 @@ public static function create(ContainerInterface $container) { * The controller response. */ public function build(EntityInterface $entity, $delta, $plugin_id) { - /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */ + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ $field_list = $entity->layout_builder__layout; - $field_list->addItem($delta, [ - 'layout' => $plugin_id, - 'layout_settings' => [], - 'section' => [], - ]); + $field_list->insertSection($delta, new Section($plugin_id)); $this->layoutTempstoreRepository->set($entity); diff --git a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php index a4163a40a82151106eeca5b3abcf1fe08e79c44e..959ce1d5b414b2df9a9e38198633e44169ede408 100644 --- a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php +++ b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php @@ -10,8 +10,8 @@ use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; use Drupal\layout_builder\LayoutSectionBuilder; -use Drupal\layout_builder\Field\LayoutSectionItemInterface; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; +use Drupal\layout_builder\Section; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -111,20 +111,20 @@ public function layout(EntityInterface $entity, $is_rebuilding = FALSE) { $entity_id = $entity->id(); $entity_type_id = $entity->getEntityTypeId(); - /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */ + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ $field_list = $entity->layout_builder__layout; // For a new layout override, begin with a single section of one column. - if (!$is_rebuilding && $field_list->isEmpty()) { - $field_list->addItem(0, ['layout' => 'layout_onecol']); + if (!$is_rebuilding && $field_list->count() === 0) { + $field_list->appendSection(new Section('layout_onecol')); $this->layoutTempstoreRepository->set($entity); } $output = []; $count = 0; - foreach ($field_list as $item) { + foreach ($field_list->getSections() as $section) { $output[] = $this->buildAddSectionLink($entity_type_id, $entity_id, $count); - $output[] = $this->buildAdministrativeSection($item, $entity, $count); + $output[] = $this->buildAdministrativeSection($section, $entity, $count); $count++; } $output[] = $this->buildAddSectionLink($entity_type_id, $entity_id, $count); @@ -179,8 +179,8 @@ protected function buildAddSectionLink($entity_type_id, $entity_id, $delta) { /** * Builds the render array for the layout section while editing. * - * @param \Drupal\layout_builder\Field\LayoutSectionItemInterface $item - * The layout section item. + * @param \Drupal\layout_builder\Section $section + * The layout section. * @param \Drupal\Core\Entity\EntityInterface $entity * The entity. * @param int $delta @@ -189,12 +189,12 @@ protected function buildAddSectionLink($entity_type_id, $entity_id, $delta) { * @return array * The render array for a given section. */ - protected function buildAdministrativeSection(LayoutSectionItemInterface $item, EntityInterface $entity, $delta) { + protected function buildAdministrativeSection(Section $section, EntityInterface $entity, $delta) { $entity_type_id = $entity->getEntityTypeId(); $entity_id = $entity->id(); - $layout = $this->layoutManager->createInstance($item->layout, $item->layout_settings); - $build = $this->builder->buildSectionFromLayout($layout, $item->section); + $layout = $section->getLayout(); + $build = $section->toRenderArray(); $layout_definition = $layout->getPluginDefinition(); foreach ($layout_definition->getRegions() as $region => $info) { diff --git a/core/modules/layout_builder/src/Controller/MoveBlockController.php b/core/modules/layout_builder/src/Controller/MoveBlockController.php index d648416d9e418d0e4c94740197937ca60cf8302d..7a841a252d77cc272630bc5507815b5ad7c16c7f 100644 --- a/core/modules/layout_builder/src/Controller/MoveBlockController.php +++ b/core/modules/layout_builder/src/Controller/MoveBlockController.php @@ -69,32 +69,29 @@ public static function create(ContainerInterface $container) { * An AJAX response. */ public function build(EntityInterface $entity, $delta_from, $delta_to, $region_from, $region_to, $block_uuid, $preceding_block_uuid = NULL) { - /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */ - $field = $entity->layout_builder__layout->get($delta_from); - $section = $field->getSection(); + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ + $field_list = $entity->layout_builder__layout; + $section = $field_list->getSection($delta_from); - $block = $section->getBlock($region_from, $block_uuid); - $section->removeBlock($region_from, $block_uuid); + $component = $section->getComponent($block_uuid); + $section->removeComponent($block_uuid); // If the block is moving from one section to another, update the original // section and load the new one. if ($delta_from !== $delta_to) { - $field->updateFromSection($section); - $field = $entity->layout_builder__layout->get($delta_to); - $section = $field->getSection(); + $section = $field_list->getSection($delta_to); } // If a preceding block was specified, insert after that. Otherwise add the // block to the front. + $component->setRegion($region_to); if (isset($preceding_block_uuid)) { - $section->insertBlock($region_to, $block_uuid, $block, $preceding_block_uuid); + $section->insertAfterComponent($preceding_block_uuid, $component); } else { - $section->addBlock($region_to, $block_uuid, $block); + $section->appendComponent($component); } - $field->updateFromSection($section); - $this->layoutTempstoreRepository->set($entity); return $this->rebuildLayout($entity); } diff --git a/core/modules/layout_builder/src/Field/LayoutSectionItemInterface.php b/core/modules/layout_builder/src/Field/LayoutSectionItemInterface.php deleted file mode 100644 index 786b6b4b19acd4f932d7d64c922a46ba99b37613..0000000000000000000000000000000000000000 --- a/core/modules/layout_builder/src/Field/LayoutSectionItemInterface.php +++ /dev/null @@ -1,40 +0,0 @@ -<?php - -namespace Drupal\layout_builder\Field; - -use Drupal\Core\Field\FieldItemInterface; -use Drupal\layout_builder\Section; - -/** - * Defines an interface for the layout section field item. - * - * @internal - * Layout Builder is currently experimental and should only be leveraged by - * experimental modules and development releases of contributed modules. - * See https://www.drupal.org/core/experimental for more information. - * - * @property string layout - * @property array[] layout_settings - * @property array[] section - */ -interface LayoutSectionItemInterface extends FieldItemInterface { - - /** - * Gets a domain object for the layout section. - * - * @return \Drupal\layout_builder\Section - * The layout section. - */ - public function getSection(); - - /** - * Updates the stored value based on the domain object. - * - * @param \Drupal\layout_builder\Section $section - * The layout section. - * - * @return $this - */ - public function updateFromSection(Section $section); - -} diff --git a/core/modules/layout_builder/src/Field/LayoutSectionItemList.php b/core/modules/layout_builder/src/Field/LayoutSectionItemList.php index 933ed6eb974e866bc32cdde910e0409e6e75d5e2..c9a6b71136124ccf2ee16a454f05cbcc8d6a1763 100644 --- a/core/modules/layout_builder/src/Field/LayoutSectionItemList.php +++ b/core/modules/layout_builder/src/Field/LayoutSectionItemList.php @@ -3,6 +3,8 @@ namespace Drupal\layout_builder\Field; use Drupal\Core\Field\FieldItemList; +use Drupal\layout_builder\Section; +use Drupal\layout_builder\SectionStorageInterface; /** * Defines a item list class for layout section fields. @@ -11,22 +13,65 @@ * * @see \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem */ -class LayoutSectionItemList extends FieldItemList implements LayoutSectionItemListInterface { +class LayoutSectionItemList extends FieldItemList implements SectionStorageInterface { /** * {@inheritdoc} */ - public function addItem($index, $value) { - if ($this->get($index)) { - $start = array_slice($this->list, 0, $index); - $end = array_slice($this->list, $index); - $item = $this->createItem($index, $value); + public function insertSection($delta, Section $section) { + if ($this->get($delta)) { + /** @var \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem $item */ + $item = $this->createItem($delta); + $item->section = $section; + + $start = array_slice($this->list, 0, $delta); + $end = array_slice($this->list, $delta); $this->list = array_merge($start, [$item], $end); } else { - $item = $this->appendItem($value); + $this->appendSection($section); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function appendSection(Section $section) { + $this->appendItem()->section = $section; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSections() { + $sections = []; + /** @var \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem $item */ + foreach ($this->list as $delta => $item) { + $sections[$delta] = $item->section; } - return $item; + return $sections; + } + + /** + * {@inheritdoc} + */ + public function getSection($delta) { + /** @var \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem $item */ + if (!$item = $this->get($delta)) { + throw new \OutOfBoundsException(sprintf('Invalid delta "%s" for the "%s" entity', $delta, $this->getEntity()->label())); + } + + return $item->section; + } + + /** + * {@inheritdoc} + */ + public function removeSection($delta) { + $this->removeItem($delta); + return $this; } } diff --git a/core/modules/layout_builder/src/Field/LayoutSectionItemListInterface.php b/core/modules/layout_builder/src/Field/LayoutSectionItemListInterface.php deleted file mode 100644 index 81839d17747b6975efe3912847cab60bcafafaf3..0000000000000000000000000000000000000000 --- a/core/modules/layout_builder/src/Field/LayoutSectionItemListInterface.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php - -namespace Drupal\layout_builder\Field; - -use Drupal\Core\Field\FieldItemListInterface; - -/** - * Defines a item list class for layout section fields. - * - * @internal - * Layout Builder is currently experimental and should only be leveraged by - * experimental modules and development releases of contributed modules. - * See https://www.drupal.org/core/experimental for more information. - * - * @see \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem - */ -interface LayoutSectionItemListInterface extends FieldItemListInterface { - - /** - * {@inheritdoc} - * - * @return \Drupal\layout_builder\Field\LayoutSectionItemInterface|null - * The layout section item, if it exists. - */ - public function get($index); - - /** - * Adds a new item to the list. - * - * If an item exists at the given index, the item at that position and others - * after it are shifted backward. - * - * @param int $index - * The position of the item in the list. - * @param mixed $value - * The value of the item to be stored at the specified position. - * - * @return \Drupal\Core\TypedData\TypedDataInterface - * The item that was appended. - * - * @todo Move to \Drupal\Core\TypedData\ListInterface directly in - * https://www.drupal.org/node/2907417. - */ - public function addItem($index, $value); - -} diff --git a/core/modules/layout_builder/src/Form/AddBlockForm.php b/core/modules/layout_builder/src/Form/AddBlockForm.php index 1757606002c2b2a85209cb6fa65f24bf3e908453..83effd622661ae357b70a8122afd3192f0df89e7 100644 --- a/core/modules/layout_builder/src/Form/AddBlockForm.php +++ b/core/modules/layout_builder/src/Form/AddBlockForm.php @@ -3,6 +3,7 @@ namespace Drupal\layout_builder\Form; use Drupal\layout_builder\Section; +use Drupal\layout_builder\SectionComponent; /** * Provides a form to add a block. @@ -29,7 +30,7 @@ protected function submitLabel() { * {@inheritdoc} */ protected function submitBlock(Section $section, $region, $uuid, array $configuration) { - $section->addBlock($region, $uuid, $configuration); + $section->appendComponent(new SectionComponent($uuid, $region, $configuration)); } } diff --git a/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php b/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php index 7356ef4ccdf18a0abc3670e0aad42ea1c0545694..08ff3174167c374ef2beb59af349ab65f04dbf77 100644 --- a/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php +++ b/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php @@ -246,11 +246,10 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $configuration = $this->block->getConfiguration(); - /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */ - $field = $this->entity->layout_builder__layout->get($this->delta); - $section = $field->getSection(); - $this->submitBlock($section, $this->region, $configuration['uuid'], ['block' => $configuration]); - $field->updateFromSection($section); + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ + $field_list = $this->entity->layout_builder__layout; + $section = $field_list->getSection($this->delta); + $this->submitBlock($section, $this->region, $configuration['uuid'], $configuration); $this->layoutTempstoreRepository->set($this->entity); $form_state->setRedirectUrl($this->entity->toUrl('layout-builder')); diff --git a/core/modules/layout_builder/src/Form/ConfigureSectionForm.php b/core/modules/layout_builder/src/Form/ConfigureSectionForm.php index 17913237d51ddfe6b0beddc63a15ec0f8950e445..6993d218be50caf0131ebb607dcc0366c479dba9 100644 --- a/core/modules/layout_builder/src/Form/ConfigureSectionForm.php +++ b/core/modules/layout_builder/src/Form/ConfigureSectionForm.php @@ -14,6 +14,7 @@ use Drupal\Core\Plugin\PluginWithFormsInterface; use Drupal\layout_builder\Controller\LayoutRebuildTrait; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; +use Drupal\layout_builder\Section; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -121,14 +122,15 @@ public function buildForm(array $form, FormStateInterface $form_state, EntityInt $this->delta = $delta; $this->isUpdate = is_null($plugin_id); - $configuration = []; if ($this->isUpdate) { - /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */ - $field = $this->entity->layout_builder__layout->get($this->delta); - $plugin_id = $field->layout; - $configuration = $field->layout_settings; + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ + $field_list = $this->entity->layout_builder__layout; + $section = $field_list->getSection($this->delta); } - $this->layout = $this->layoutManager->createInstance($plugin_id, $configuration); + else { + $section = new Section($plugin_id); + } + $this->layout = $section->getLayout(); $form['#tree'] = TRUE; $form['layout_settings'] = []; @@ -166,19 +168,13 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $plugin_id = $this->layout->getPluginId(); $configuration = $this->layout->getConfiguration(); - /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */ + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ $field_list = $this->entity->layout_builder__layout; if ($this->isUpdate) { - $field = $field_list->get($this->delta); - $field->layout = $plugin_id; - $field->layout_settings = $configuration; + $field_list->getSection($this->delta)->setLayoutSettings($configuration); } else { - $field_list->addItem($this->delta, [ - 'layout' => $plugin_id, - 'layout_settings' => $configuration, - 'section' => [], - ]); + $field_list->insertSection($this->delta, new Section($plugin_id, $configuration)); } $this->layoutTempstoreRepository->set($this->entity); diff --git a/core/modules/layout_builder/src/Form/RemoveBlockForm.php b/core/modules/layout_builder/src/Form/RemoveBlockForm.php index 139186af66541b656648f3ae3a356fce7e3ca5b1..9c7eb2084dc529776630d10956e96a31af097930 100644 --- a/core/modules/layout_builder/src/Form/RemoveBlockForm.php +++ b/core/modules/layout_builder/src/Form/RemoveBlockForm.php @@ -60,11 +60,9 @@ public function buildForm(array $form, FormStateInterface $form_state, EntityInt * {@inheritdoc} */ protected function handleEntity(EntityInterface $entity, FormStateInterface $form_state) { - /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */ - $field = $entity->layout_builder__layout->get($this->delta); - $section = $field->getSection(); - $section->removeBlock($this->region, $this->uuid); - $field->updateFromSection($section); + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ + $field_list = $this->entity->layout_builder__layout; + $field_list->getSection($this->delta)->removeComponent($this->uuid); } } diff --git a/core/modules/layout_builder/src/Form/UpdateBlockForm.php b/core/modules/layout_builder/src/Form/UpdateBlockForm.php index 2f2aa600e44c7e08d5ed5cd5cf293cce3e372369..3cc36585a11aff0c5724f28506766ee683304f38 100644 --- a/core/modules/layout_builder/src/Form/UpdateBlockForm.php +++ b/core/modules/layout_builder/src/Form/UpdateBlockForm.php @@ -40,14 +40,11 @@ public function getFormId() { * The form array. */ public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL, $delta = NULL, $region = NULL, $uuid = NULL) { - /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */ - $field = $entity->layout_builder__layout->get($delta); - $block = $field->getSection()->getBlock($region, $uuid); - if (empty($block['block']['id'])) { - throw new \InvalidArgumentException('Invalid UUID specified'); - } + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ + $field_list = $entity->layout_builder__layout; + $plugin = $field_list->getSection($delta)->getComponent($uuid)->getPlugin(); - return parent::buildForm($form, $form_state, $entity, $delta, $region, $block['block']['id'], $block['block']); + return parent::buildForm($form, $form_state, $entity, $delta, $region, $plugin->getPluginId(), $plugin->getConfiguration()); } /** @@ -61,7 +58,7 @@ protected function submitLabel() { * {@inheritdoc} */ protected function submitBlock(Section $section, $region, $uuid, array $configuration) { - $section->updateBlock($region, $uuid, $configuration); + $section->getComponent($uuid)->setConfiguration($configuration); } } diff --git a/core/modules/layout_builder/src/LayoutSectionBuilder.php b/core/modules/layout_builder/src/LayoutSectionBuilder.php index 1682974f1134533f61792cf627a14d964beac892..525849d9cef3c12f8d3ba2f7d537133351f55ea1 100644 --- a/core/modules/layout_builder/src/LayoutSectionBuilder.php +++ b/core/modules/layout_builder/src/LayoutSectionBuilder.php @@ -2,14 +2,11 @@ namespace Drupal\layout_builder; -use Drupal\Component\Plugin\Exception\PluginException; use Drupal\Core\Block\BlockManagerInterface; -use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Layout\LayoutInterface; use Drupal\Core\Layout\LayoutPluginManagerInterface; use Drupal\Core\Plugin\Context\ContextHandlerInterface; use Drupal\Core\Plugin\Context\ContextRepositoryInterface; -use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; @@ -17,6 +14,8 @@ * Builds the UI for layout sections. * * @internal + * + * @todo Remove in https://www.drupal.org/project/drupal/issues/2928450. */ class LayoutSectionBuilder { @@ -84,37 +83,21 @@ public function __construct(AccountInterface $account, LayoutPluginManagerInterf * * @param \Drupal\Core\Layout\LayoutInterface $layout * The ID of the layout. - * @param array $section - * An array of configuration, keyed first by region and then by block UUID. + * @param \Drupal\layout_builder\SectionComponent[] $components + * An array of components. * * @return array * The render array for a given section. */ - public function buildSectionFromLayout(LayoutInterface $layout, array $section) { - $cacheability = CacheableMetadata::createFromRenderArray([]); - + public function buildSectionFromLayout(LayoutInterface $layout, array $components) { $regions = []; - $weight = 0; - foreach ($section as $region => $blocks) { - if (!is_array($blocks)) { - throw new \InvalidArgumentException(sprintf('The "%s" region in the "%s" layout has invalid configuration', $region, $layout->getPluginId())); - } - - foreach ($blocks as $uuid => $configuration) { - if (!is_array($configuration) || !isset($configuration['block'])) { - throw new \InvalidArgumentException(sprintf('The block with UUID of "%s" has invalid configuration', $uuid)); - } - - if ($block_output = $this->buildBlock($uuid, $configuration['block'], $cacheability)) { - $block_output['#weight'] = $weight++; - $regions[$region][$uuid] = $block_output; - } + foreach ($components as $component) { + if ($output = $component->toRenderArray()) { + $regions[$component->getRegion()][$component->getUuid()] = $output; } } - $result = $layout->build($regions); - $cacheability->applyTo($result); - return $result; + return $layout->build($regions); } /** @@ -124,78 +107,15 @@ public function buildSectionFromLayout(LayoutInterface $layout, array $section) * The ID of the layout. * @param array $layout_settings * The configuration for the layout. - * @param array $section - * An array of configuration, keyed first by region and then by block UUID. + * @param \Drupal\layout_builder\SectionComponent[] $components + * An array of components. * * @return array * The render array for a given section. */ - public function buildSection($layout_id, array $layout_settings, array $section) { + public function buildSection($layout_id, array $layout_settings, array $components) { $layout = $this->layoutPluginManager->createInstance($layout_id, $layout_settings); - return $this->buildSectionFromLayout($layout, $section); - } - - /** - * Builds the render array for a given block. - * - * @param string $uuid - * The UUID of this block instance. - * @param array $configuration - * An array of configuration relevant to the block instance. Must contain - * the plugin ID with the key 'id'. - * @param \Drupal\Core\Cache\CacheableMetadata $cacheability - * The cacheability metadata. - * - * @return array|null - * The render array representing this block, if accessible. NULL otherwise. - */ - protected function buildBlock($uuid, array $configuration, CacheableMetadata $cacheability) { - $block = $this->getBlock($uuid, $configuration); - - $access = $block->access($this->account, TRUE); - $cacheability->addCacheableDependency($access); - - $block_output = NULL; - if ($access->isAllowed()) { - $block_output = [ - '#theme' => 'block', - '#configuration' => $block->getConfiguration(), - '#plugin_id' => $block->getPluginId(), - '#base_plugin_id' => $block->getBaseId(), - '#derivative_plugin_id' => $block->getDerivativeId(), - 'content' => $block->build(), - ]; - $cacheability->addCacheableDependency($block); - } - return $block_output; - } - - /** - * Gets a block instance. - * - * @param string $uuid - * The UUID of this block instance. - * @param array $configuration - * An array of configuration relevant to the block instance. Must contain - * the plugin ID with the key 'id'. - * - * @return \Drupal\Core\Block\BlockPluginInterface - * The block instance. - * - * @throws \Drupal\Component\Plugin\Exception\PluginException - * Thrown when the configuration parameter does not contain 'id'. - */ - protected function getBlock($uuid, array $configuration) { - if (!isset($configuration['id'])) { - throw new PluginException(sprintf('No plugin ID specified for block with "%s" UUID', $uuid)); - } - - $block = $this->blockManager->createInstance($configuration['id'], $configuration); - if ($block instanceof ContextAwarePluginInterface) { - $contexts = $this->contextRepository->getRuntimeContexts(array_values($block->getContextMapping())); - $this->contextHandler->applyContextMapping($block, $contexts); - } - return $block; + return $this->buildSectionFromLayout($layout, $components); } } diff --git a/core/modules/layout_builder/src/Plugin/DataType/SectionData.php b/core/modules/layout_builder/src/Plugin/DataType/SectionData.php new file mode 100644 index 0000000000000000000000000000000000000000..353c53fe7cca8ad323c3ecc57c220fee253854ce --- /dev/null +++ b/core/modules/layout_builder/src/Plugin/DataType/SectionData.php @@ -0,0 +1,36 @@ +<?php + +namespace Drupal\layout_builder\Plugin\DataType; + +use Drupal\Core\TypedData\TypedData; +use Drupal\layout_builder\Section; + +/** + * Provides a data type wrapping \Drupal\layout_builder\Section. + * + * @DataType( + * id = "layout_section", + * label = @Translation("Layout Section"), + * description = @Translation("A layout section"), + * ) + */ +class SectionData extends TypedData { + + /** + * The section object. + * + * @var \Drupal\layout_builder\Section + */ + protected $value; + + /** + * {@inheritdoc} + */ + public function setValue($value, $notify = TRUE) { + if ($value && !$value instanceof Section) { + throw new \InvalidArgumentException(sprintf('Value assigned to "%s" is not a valid section', $this->getName())); + } + parent::setValue($value, $notify); + } + +} diff --git a/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php b/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php index 4951d01c4b9bca5ad8330adad9bbb9a59f638a99..c321585fdf5536c85f5712c619680399acec47f5 100644 --- a/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php +++ b/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php @@ -78,9 +78,9 @@ public static function create(ContainerInterface $container, array $configuratio public function viewElements(FieldItemListInterface $items, $langcode) { $elements = []; - /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface[] $items */ - foreach ($items as $delta => $item) { - $elements[$delta] = $this->builder->buildSection($item->layout, $item->layout_settings, $item->section); + /** @var \Drupal\layout_builder\SectionStorageInterface $items */ + foreach ($items->getSections() as $delta => $section) { + $elements[$delta] = $section->toRenderArray(); } return $elements; diff --git a/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php index fc1c63413fa1ff7a327312abd6b18ac75dbe2246..2001d1b5ff19fd4901d4ec0e889685546679a658 100644 --- a/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php +++ b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php @@ -7,8 +7,6 @@ use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\TypedData\DataDefinition; -use Drupal\Core\TypedData\MapDataDefinition; -use Drupal\layout_builder\Field\LayoutSectionItemInterface; use Drupal\layout_builder\Section; /** @@ -25,22 +23,16 @@ * no_ui = TRUE, * cardinality = \Drupal\Core\Field\FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED * ) + * + * @property \Drupal\layout_builder\Section section */ -class LayoutSectionItem extends FieldItemBase implements LayoutSectionItemInterface { +class LayoutSectionItem extends FieldItemBase { /** * {@inheritdoc} */ public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { - // Prevent early t() calls by using the TranslatableMarkup. - $properties['layout'] = DataDefinition::create('string') - ->setLabel(new TranslatableMarkup('Layout')) - ->setSetting('case_sensitive', FALSE) - ->setRequired(TRUE); - $properties['layout_settings'] = MapDataDefinition::create('map') - ->setLabel(new TranslatableMarkup('Layout Settings')) - ->setRequired(FALSE); - $properties['section'] = MapDataDefinition::create('map') + $properties['section'] = DataDefinition::create('layout_section') ->setLabel(new TranslatableMarkup('Layout Section')) ->setRequired(FALSE); @@ -73,17 +65,6 @@ public static function mainPropertyName() { public static function schema(FieldStorageDefinitionInterface $field_definition) { $schema = [ 'columns' => [ - 'layout' => [ - 'type' => 'varchar', - 'length' => '255', - 'binary' => FALSE, - ], - 'layout_settings' => [ - 'type' => 'blob', - 'size' => 'normal', - // @todo Address in https://www.drupal.org/node/2914503. - 'serialize' => TRUE, - ], 'section' => [ 'type' => 'blob', 'size' => 'normal', @@ -100,10 +81,8 @@ public static function schema(FieldStorageDefinitionInterface $field_definition) * {@inheritdoc} */ public static function generateSampleValue(FieldDefinitionInterface $field_definition) { - $values['layout'] = 'layout_onecol'; - $values['layout_settings'] = []; // @todo Expand this in https://www.drupal.org/node/2912331. - $values['section'] = []; + $values['section'] = new Section('layout_onecol'); return $values; } @@ -111,22 +90,7 @@ public static function generateSampleValue(FieldDefinitionInterface $field_defin * {@inheritdoc} */ public function isEmpty() { - return empty($this->layout); - } - - /** - * {@inheritdoc} - */ - public function getSection() { - return new Section($this->section); - } - - /** - * {@inheritdoc} - */ - public function updateFromSection(Section $section) { - $this->section = $section->getValue(); - return $this; + return empty($this->section); } } diff --git a/core/modules/layout_builder/src/Section.php b/core/modules/layout_builder/src/Section.php index f5e19003b58a80304b287edb8e65c8fcc3cba099..55204ddc9eda379ac561a265fc9b17286226b03e 100644 --- a/core/modules/layout_builder/src/Section.php +++ b/core/modules/layout_builder/src/Section.php @@ -5,158 +5,303 @@ /** * Provides a domain object for layout sections. * - * A section is a multi-dimensional array, keyed first by region machine name, - * then by block UUID, containing block configuration values. + * A section consists of three parts: + * - The layout plugin ID for the layout applied to the section (for example, + * 'layout_onecol'). + * - An array of settings for the layout plugin. + * - An array of components that can be rendered in the section. + * + * @internal + * Layout Builder is currently experimental and should only be leveraged by + * experimental modules and development releases of contributed modules. + * See https://www.drupal.org/core/experimental for more information. + * + * @see \Drupal\Core\Layout\LayoutDefinition + * @see \Drupal\layout_builder\SectionComponent + * + * @todo Determine whether an interface will be provided for this in + * https://www.drupal.org/project/drupal/issues/2930334. */ class Section { /** - * The section data. + * The layout plugin ID. + * + * @var string + */ + protected $layoutId; + + /** + * The layout plugin settings. * * @var array */ - protected $section; + protected $layoutSettings = []; + + /** + * An array of components, keyed by UUID. + * + * @var \Drupal\layout_builder\SectionComponent[] + */ + protected $components = []; /** * Constructs a new Section. * - * @param array $section - * The section data. + * @param string $layout_id + * The layout plugin ID. + * @param array $layout_settings + * (optional) The layout plugin settings. + * @param \Drupal\layout_builder\SectionComponent[] $components + * (optional) The components. */ - public function __construct(array $section) { - $this->section = $section; + public function __construct($layout_id, array $layout_settings = [], array $components = []) { + $this->layoutId = $layout_id; + $this->layoutSettings = $layout_settings; + foreach ($components as $component) { + $this->setComponent($component); + } } /** - * Returns the value of the section. + * Returns the renderable array for this section. * * @return array - * The section data. + * A renderable array representing the content of the section. */ - public function getValue() { - return $this->section; + public function toRenderArray() { + $regions = []; + foreach ($this->getComponents() as $component) { + if ($output = $component->toRenderArray()) { + $regions[$component->getRegion()][$component->getUuid()] = $output; + } + } + + return $this->getLayout()->build($regions); } /** - * Gets the configuration of a given block from a region. + * Gets the layout plugin for this section. * - * @param string $region - * The region name. - * @param string $uuid - * The UUID of the block to retrieve. + * @return \Drupal\Core\Layout\LayoutInterface + * The layout plugin. + */ + public function getLayout() { + return $this->layoutPluginManager()->createInstance($this->getLayoutId(), $this->getLayoutSettings()); + } + + /** + * Gets the layout plugin ID for this section. * - * @return array - * The block configuration. + * @return string + * The layout plugin ID. * - * @throws \InvalidArgumentException - * Thrown when the expected region or UUID do not exist. + * @internal + * This method should only be used by code responsible for storing the data. */ - public function getBlock($region, $uuid) { - if (!isset($this->section[$region])) { - throw new \InvalidArgumentException('Invalid region'); - } + public function getLayoutId() { + return $this->layoutId; + } - if (!isset($this->section[$region][$uuid])) { - throw new \InvalidArgumentException('Invalid UUID'); - } + /** + * Gets the layout plugin settings for this section. + * + * @return mixed[] + * The layout plugin settings. + * + * @internal + * This method should only be used by code responsible for storing the data. + */ + public function getLayoutSettings() { + return $this->layoutSettings; + } + + /** + * Sets the layout plugin settings for this section. + * + * @param mixed[] $layout_settings + * The layout plugin settings. + * + * @return $this + */ + public function setLayoutSettings(array $layout_settings) { + $this->layoutSettings = $layout_settings; + return $this; + } - return $this->section[$region][$uuid]; + /** + * Returns the components of the section. + * + * @return \Drupal\layout_builder\SectionComponent[] + * The components. + */ + public function getComponents() { + return $this->components; } /** - * Updates the configuration of a given block from a region. + * Gets the component for a given UUID. * - * @param string $region - * The region name. * @param string $uuid - * The UUID of the block to retrieve. - * @param array $configuration - * The block configuration. + * The UUID of the component to retrieve. * - * @return $this + * @return \Drupal\layout_builder\SectionComponent + * The component. * * @throws \InvalidArgumentException - * Thrown when the expected region or UUID do not exist. + * Thrown when the expected UUID does not exist. */ - public function updateBlock($region, $uuid, array $configuration) { - if (!isset($this->section[$region])) { - throw new \InvalidArgumentException('Invalid region'); + public function getComponent($uuid) { + if (!isset($this->components[$uuid])) { + throw new \InvalidArgumentException(sprintf('Invalid UUID "%s"', $uuid)); } - if (!isset($this->section[$region][$uuid])) { - throw new \InvalidArgumentException('Invalid UUID'); - } - - $this->section[$region][$uuid] = $configuration; + return $this->components[$uuid]; + } + /** + * Helper method to set a component. + * + * @param \Drupal\layout_builder\SectionComponent $component + * The component. + * + * @return $this + */ + protected function setComponent(SectionComponent $component) { + $this->components[$component->getUuid()] = $component; return $this; } /** - * Removes a given block from a region. + * Removes a given component from a region. * - * @param string $region - * The region name. * @param string $uuid - * The UUID of the block to remove. + * The UUID of the component to remove. * * @return $this */ - public function removeBlock($region, $uuid) { - unset($this->section[$region][$uuid]); - $this->section = array_filter($this->section); + public function removeComponent($uuid) { + unset($this->components[$uuid]); return $this; } /** - * Adds a block to the front of a region. + * Appends a component to the end of a region. * - * @param string $region - * The region name. - * @param string $uuid - * The UUID of the block to add. - * @param array $configuration - * The block configuration. + * @param \Drupal\layout_builder\SectionComponent $component + * The component being appended. * * @return $this */ - public function addBlock($region, $uuid, array $configuration) { - $this->section += [$region => []]; - $this->section[$region] = array_merge([$uuid => $configuration], $this->section[$region]); + public function appendComponent(SectionComponent $component) { + $component->setWeight($this->getNextHighestWeight($component->getRegion())); + $this->setComponent($component); return $this; } /** - * Inserts a block after a specified existing block in a region. + * Returns the next highest weight of the component in a region. * * @param string $region * The region name. - * @param string $uuid - * The UUID of the block to insert. - * @param array $configuration - * The block configuration. + * + * @return int + * A number higher than the highest weight of the component in the region. + */ + protected function getNextHighestWeight($region) { + $components = $this->getComponentsByRegion($region); + $weights = array_map(function (SectionComponent $component) { + return $component->getWeight(); + }, $components); + return $weights ? max($weights) + 1 : 0; + } + + /** + * Gets the components for a specific region. + * + * @param string $region + * The region name. + * + * @return \Drupal\layout_builder\SectionComponent[] + * An array of components in the specified region, sorted by weight. + */ + protected function getComponentsByRegion($region) { + $components = array_filter($this->getComponents(), function (SectionComponent $component) use ($region) { + return $component->getRegion() === $region; + }); + uasort($components, function (SectionComponent $a, SectionComponent $b) { + return $a->getWeight() > $b->getWeight() ? 1 : -1; + }); + return $components; + } + + /** + * Inserts a component after a specified existing component. + * * @param string $preceding_uuid - * The UUID of the existing block to insert after. + * The UUID of the existing component to insert after. + * @param \Drupal\layout_builder\SectionComponent $component + * The component being inserted. * * @return $this * * @throws \InvalidArgumentException - * Thrown when the expected region does not exist. + * Thrown when the expected UUID does not exist. + */ + public function insertAfterComponent($preceding_uuid, SectionComponent $component) { + // Find the delta of the specified UUID. + $uuids = array_keys($this->getComponentsByRegion($component->getRegion())); + $delta = array_search($preceding_uuid, $uuids, TRUE); + if ($delta === FALSE) { + throw new \InvalidArgumentException(sprintf('Invalid preceding UUID "%s"', $preceding_uuid)); + } + return $this->insertComponent($delta + 1, $component); + } + + /** + * Inserts a component at a specified delta. + * + * @param int $delta + * The zero-based delta in which to insert the component. + * @param \Drupal\layout_builder\SectionComponent $new_component + * The component being inserted. + * + * @return $this + * + * @throws \OutOfBoundsException + * Thrown when the specified delta is invalid. */ - public function insertBlock($region, $uuid, array $configuration, $preceding_uuid) { - if (!isset($this->section[$region])) { - throw new \InvalidArgumentException('Invalid region'); + public function insertComponent($delta, SectionComponent $new_component) { + $components = $this->getComponentsByRegion($new_component->getRegion()); + $count = count($components); + if ($delta > $count) { + throw new \OutOfBoundsException(sprintf('Invalid delta "%s" for the "%s" component', $delta, $new_component->getUuid())); } - $slice_id = array_search($preceding_uuid, array_keys($this->section[$region])); - if ($slice_id === FALSE) { - throw new \InvalidArgumentException('Invalid preceding UUID'); + // If the delta is the end of the list, append the component instead. + if ($delta === $count) { + return $this->appendComponent($new_component); } - $before = array_slice($this->section[$region], 0, $slice_id + 1); - $after = array_slice($this->section[$region], $slice_id + 1); - $this->section[$region] = array_merge($before, [$uuid => $configuration], $after); + // Find the weight of the component that exists at the specified delta. + $weight = array_values($components)[$delta]->getWeight(); + $this->setComponent($new_component->setWeight($weight++)); + + // Increase the weight of every subsequent component. + foreach (array_slice($components, $delta) as $component) { + $component->setWeight($weight++); + } return $this; } + /** + * Wraps the layout plugin manager. + * + * @return \Drupal\Core\Layout\LayoutPluginManagerInterface + * The layout plugin manager. + */ + protected function layoutPluginManager() { + return \Drupal::service('plugin.manager.core.layout'); + } + } diff --git a/core/modules/layout_builder/src/SectionComponent.php b/core/modules/layout_builder/src/SectionComponent.php new file mode 100644 index 0000000000000000000000000000000000000000..caad0a2b61db2f7df9edcbf0ca1bf540f7837d2d --- /dev/null +++ b/core/modules/layout_builder/src/SectionComponent.php @@ -0,0 +1,314 @@ +<?php + +namespace Drupal\layout_builder; + +use Drupal\Component\Plugin\Exception\PluginException; +use Drupal\Core\Block\BlockPluginInterface; +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Plugin\ContextAwarePluginInterface; + +/** + * Provides a value object for a section component. + * + * A component represents the smallest part of a layout (for example, a block). + * Components wrap a renderable plugin, currently using + * \Drupal\Core\Block\BlockPluginInterface, and contain the layout region + * within the section layout where the component will be rendered. + * + * @internal + * Layout Builder is currently experimental and should only be leveraged by + * experimental modules and development releases of contributed modules. + * See https://www.drupal.org/core/experimental for more information. + * + * @see \Drupal\Core\Layout\LayoutDefinition + * @see \Drupal\layout_builder\Section + * @see \Drupal\layout_builder\SectionStorageInterface + * + * @todo Determine whether to retain the name 'component' in + * https://www.drupal.org/project/drupal/issues/2929783. + * @todo Determine whether an interface will be provided for this in + * https://www.drupal.org/project/drupal/issues/2930334. + */ +class SectionComponent { + + /** + * The UUID of the component. + * + * @var string + */ + protected $uuid; + + /** + * The region the component is placed in. + * + * @var string + */ + protected $region; + + /** + * An array of plugin configuration. + * + * @var mixed[] + */ + protected $configuration; + + /** + * The weight of the component. + * + * @var int + */ + protected $weight = 0; + + /** + * Any additional properties and values. + * + * @var mixed[] + */ + protected $additional = []; + + /** + * Constructs a new SectionComponent. + * + * @param string $uuid + * The UUID. + * @param string $region + * The region. + * @param mixed[] $configuration + * The plugin configuration. + * @param mixed[] $additional + * An additional values. + */ + public function __construct($uuid, $region, array $configuration = [], array $additional = []) { + $this->uuid = $uuid; + $this->region = $region; + $this->configuration = $configuration; + $this->additional = $additional; + } + + /** + * Returns the renderable array for this component. + * + * @return array + * A renderable array representing the content of the component. + */ + public function toRenderArray() { + $output = []; + + $plugin = $this->getPlugin(); + // @todo Figure out the best way to unify fields and blocks and components + // in https://www.drupal.org/node/1875974. + if ($plugin instanceof BlockPluginInterface) { + $access = $plugin->access($this->currentUser(), TRUE); + $cacheability = CacheableMetadata::createFromObject($access); + + if ($access->isAllowed()) { + $cacheability->addCacheableDependency($plugin); + // @todo Move this to BlockBase in https://www.drupal.org/node/2931040. + $output = [ + '#theme' => 'block', + '#configuration' => $plugin->getConfiguration(), + '#plugin_id' => $plugin->getPluginId(), + '#base_plugin_id' => $plugin->getBaseId(), + '#derivative_plugin_id' => $plugin->getDerivativeId(), + '#weight' => $this->getWeight(), + 'content' => $plugin->build(), + ]; + } + $cacheability->applyTo($output); + } + return $output; + } + + /** + * Gets any arbitrary property for the component. + * + * @param string $property + * The property to retrieve. + * + * @return mixed + * The value for that property, or NULL if the property does not exist. + */ + public function get($property) { + if (property_exists($this, $property)) { + $value = isset($this->{$property}) ? $this->{$property} : NULL; + } + else { + $value = isset($this->additional[$property]) ? $this->additional[$property] : NULL; + } + return $value; + } + + /** + * Sets a value to an arbitrary property for the component. + * + * @param string $property + * The property to use for the value. + * @param mixed $value + * The value to set. + * + * @return $this + */ + public function set($property, $value) { + if (property_exists($this, $property)) { + $this->{$property} = $value; + } + else { + $this->additional[$property] = $value; + } + return $this; + } + + /** + * Gets the region for the component. + * + * @return string + * The region. + */ + public function getRegion() { + return $this->region; + } + + /** + * Sets the region for the component. + * + * @param string $region + * The region. + * + * @return $this + */ + public function setRegion($region) { + $this->region = $region; + return $this; + } + + /** + * Gets the weight of the component. + * + * @return int + * The zero-based weight of the component. + * + * @throws \UnexpectedValueException + * Thrown if the weight was never set. + */ + public function getWeight() { + return $this->weight; + } + + /** + * Sets the weight of the component. + * + * @param int $weight + * The zero-based weight of the component. + * + * @return $this + */ + public function setWeight($weight) { + $this->weight = $weight; + return $this; + } + + /** + * Gets the component plugin configuration. + * + * @return mixed[] + * The component plugin configuration. + */ + protected function getConfiguration() { + return $this->configuration; + } + + /** + * Sets the plugin configuration. + * + * @param mixed[] $configuration + * The plugin configuration. + * + * @return $this + */ + public function setConfiguration(array $configuration) { + $this->configuration = $configuration; + return $this; + } + + /** + * Gets the plugin ID. + * + * @return string + * The plugin ID. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + * Thrown if the plugin ID cannot be found. + */ + protected function getPluginId() { + if (empty($this->configuration['id'])) { + throw new PluginException(sprintf('No plugin ID specified for component with "%s" UUID', $this->uuid)); + } + return $this->configuration['id']; + } + + /** + * Gets the UUID for this component. + * + * @return string + * The UUID. + */ + public function getUuid() { + return $this->uuid; + } + + /** + * Gets the plugin for this component. + * + * @return \Drupal\Component\Plugin\PluginInspectionInterface + * The plugin. + */ + public function getPlugin() { + $plugin = $this->pluginManager()->createInstance($this->getPluginId(), $this->getConfiguration()); + if ($plugin instanceof ContextAwarePluginInterface) { + $contexts = $this->contextRepository()->getRuntimeContexts(array_values($plugin->getContextMapping())); + $this->contextHandler()->applyContextMapping($plugin, $contexts); + } + return $plugin; + } + + /** + * Wraps the component plugin manager. + * + * @return \Drupal\Core\Block\BlockManagerInterface + * The plugin manager. + */ + protected function pluginManager() { + return \Drupal::service('plugin.manager.block'); + } + + /** + * Wraps the context repository. + * + * @return \Drupal\Core\Plugin\Context\ContextRepositoryInterface + * The context repository. + */ + protected function contextRepository() { + return \Drupal::service('context.repository'); + } + + /** + * Wraps the context handler. + * + * @return \Drupal\Core\Plugin\Context\ContextHandlerInterface + * The context handler. + */ + protected function contextHandler() { + return \Drupal::service('context.handler'); + } + + /** + * Wraps the current user. + * + * @return \Drupal\Core\Session\AccountInterface + * The current user. + */ + protected function currentUser() { + return \Drupal::currentUser(); + } + +} diff --git a/core/modules/layout_builder/src/SectionStorageInterface.php b/core/modules/layout_builder/src/SectionStorageInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..c4da487eba6bf2d8c60ac0b21d9da17a1d1363f7 --- /dev/null +++ b/core/modules/layout_builder/src/SectionStorageInterface.php @@ -0,0 +1,71 @@ +<?php + +namespace Drupal\layout_builder; + +/** + * Defines the interface for an object that stores layout sections. + * + * @internal + * Layout Builder is currently experimental and should only be leveraged by + * experimental modules and development releases of contributed modules. + * See https://www.drupal.org/core/experimental for more information. + * + * @see \Drupal\layout_builder\Section + */ +interface SectionStorageInterface extends \Countable { + + /** + * Gets the layout sections. + * + * @return \Drupal\layout_builder\Section[] + * An array of sections. + */ + public function getSections(); + + /** + * Gets a domain object for the layout section. + * + * @param int $delta + * The delta of the section. + * + * @return \Drupal\layout_builder\Section + * The layout section. + */ + public function getSection($delta); + + /** + * Appends a new section to the end of the list. + * + * @param \Drupal\layout_builder\Section $section + * The section to append. + * + * @return $this + */ + public function appendSection(Section $section); + + /** + * Inserts a new section at a given delta. + * + * If a section exists at the given index, the section at that position and + * others after it are shifted backward. + * + * @param int $delta + * The delta of the section. + * @param \Drupal\layout_builder\Section $section + * The section to insert. + * + * @return $this + */ + public function insertSection($delta, Section $section); + + /** + * Removes the section at the given delta. + * + * @param int $delta + * The delta of the section. + * + * @return $this + */ + public function removeSection($delta); + +} diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php index 61e481c7c7c8d94be39a51309f002e4942cd70a0..90ad29886a4dc801314e1e821ac2d923b787f7b7 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php @@ -4,6 +4,8 @@ use Drupal\Core\Entity\Entity\EntityViewDisplay; use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\layout_builder\Section; +use Drupal\layout_builder\SectionComponent; use Drupal\Tests\BrowserTestBase; /** @@ -56,19 +58,14 @@ public function providerTestLayoutSectionFormatter() { $data['block_with_context'] = [ [ [ - 'layout' => 'layout_onecol', - 'section' => [ - 'content' => [ - 'baz' => [ - 'block' => [ - 'id' => 'test_context_aware', - 'context_mapping' => [ - 'user' => '@user.current_user_context:current_user', - ], - ], + 'section' => new Section('layout_onecol', [], [ + 'baz' => new SectionComponent('baz', 'content', [ + 'id' => 'test_context_aware', + 'context_mapping' => [ + 'user' => '@user.current_user_context:current_user', ], - ], - ], + ]), + ]), ], ], [ @@ -86,16 +83,11 @@ public function providerTestLayoutSectionFormatter() { $data['single_section_single_block'] = [ [ [ - 'layout' => 'layout_onecol', - 'section' => [ - 'content' => [ - 'baz' => [ - 'block' => [ - 'id' => 'system_powered_by_block', - ], - ], - ], - ], + 'section' => new Section('layout_onecol', [], [ + 'baz' => new SectionComponent('baz', 'content', [ + 'id' => 'system_powered_by_block', + ]), + ]), ], ], '.layout--onecol', @@ -107,37 +99,23 @@ public function providerTestLayoutSectionFormatter() { $data['multiple_sections'] = [ [ [ - 'layout' => 'layout_onecol', - 'section' => [ - 'content' => [ - 'baz' => [ - 'block' => [ - 'id' => 'system_powered_by_block', - ], - ], - ], - ], + 'section' => new Section('layout_onecol', [], [ + 'baz' => new SectionComponent('baz', 'content', [ + 'id' => 'system_powered_by_block', + ]), + ]), ], [ - 'layout' => 'layout_twocol', - 'section' => [ - 'first' => [ - 'foo' => [ - 'block' => [ - 'id' => 'test_block_instantiation', - 'display_message' => 'foo text', - ], - ], - ], - 'second' => [ - 'bar' => [ - 'block' => [ - 'id' => 'test_block_instantiation', - 'display_message' => 'bar text', - ], - ], - ], - ], + 'section' => new Section('layout_twocol', [], [ + 'foo' => new SectionComponent('foo', 'first', [ + 'id' => 'test_block_instantiation', + 'display_message' => 'foo text', + ]), + 'bar' => new SectionComponent('bar', 'second', [ + 'id' => 'test_block_instantiation', + 'display_message' => 'bar text', + ]), + ]), ], ], [ @@ -177,16 +155,11 @@ public function testLayoutSectionFormatter($layout_data, $expected_selector, $ex public function testLayoutSectionFormatterAccess() { $node = $this->createSectionNode([ [ - 'layout' => 'layout_onecol', - 'section' => [ - 'content' => [ - 'baz' => [ - 'block' => [ - 'id' => 'test_access', - ], - ], - ], - ], + 'section' => new Section('layout_onecol', [], [ + 'baz' => new SectionComponent('baz', 'content', [ + 'id' => 'test_access', + ]), + ]), ], ]); @@ -216,41 +189,27 @@ public function testMultilingualLayoutSectionFormatter() { $entity = $this->createSectionNode([ [ - 'layout' => 'layout_onecol', - 'section' => [ - 'content' => [ - 'baz' => [ - 'block' => [ - 'id' => 'system_powered_by_block', - ], - ], - ], - ], + 'section' => new Section('layout_onecol', [], [ + 'baz' => new SectionComponent('baz', 'content', [ + 'id' => 'system_powered_by_block', + ]), + ]), ], ]); $entity->addTranslation('es', [ 'title' => 'Translated node title', $this->fieldName => [ [ - 'layout' => 'layout_twocol', - 'section' => [ - 'first' => [ - 'foo' => [ - 'block' => [ - 'id' => 'test_block_instantiation', - 'display_message' => 'foo text', - ], - ], - ], - 'second' => [ - 'bar' => [ - 'block' => [ - 'id' => 'test_block_instantiation', - 'display_message' => 'bar text', - ], - ], - ], - ], + 'section' => new Section('layout_twocol', [], [ + 'foo' => new SectionComponent('foo', 'first', [ + 'id' => 'test_block_instantiation', + 'display_message' => 'foo text', + ]), + 'bar' => new SectionComponent('bar', 'second', [ + 'id' => 'test_block_instantiation', + 'display_message' => 'bar text', + ]), + ]), ], ], ]); diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php index c3a23c78710fb271ef18d1682720e0db39d89ad1..c1b340dea411bdcff16e52593ec0d1f4e1d5b62b 100644 --- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php @@ -8,6 +8,7 @@ use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\KernelTests\KernelTestBase; +use Drupal\layout_builder\Section; /** * Ensures that Layout Builder and Field Layout are compatible with each other. @@ -108,13 +109,9 @@ public function testCompatibility() { $this->assertSame($original_markup, $new_markup); // Add a layout override. - /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */ + /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ $field_list = $entity->layout_builder__layout; - $field_list->appendItem([ - 'layout' => 'layout_onecol', - 'layout_settings' => [], - 'section' => [], - ]); + $field_list->appendSection(new Section('layout_onecol')); $entity->save(); // The rendered entity has now changed. The non-configurable field is shown diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php new file mode 100644 index 0000000000000000000000000000000000000000..4c8ae2e6c9a876720c89060dfd449b7659c9ad9f --- /dev/null +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php @@ -0,0 +1,39 @@ +<?php + +namespace Drupal\Tests\layout_builder\Kernel; + +use Drupal\entity_test\Entity\EntityTestBaseFieldDisplay; + +/** + * Tests the field type for Layout Sections. + * + * @coversDefaultClass \Drupal\layout_builder\Field\LayoutSectionItemList + * + * @group layout_builder + */ +class LayoutSectionItemListTest extends SectionStorageTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'field', + 'text', + ]; + + /** + * {@inheritdoc} + */ + protected function getEntity(array $section_data) { + $this->installEntitySchema('entity_test_base_field_display'); + layout_builder_add_layout_section_field('entity_test_base_field_display', 'entity_test_base_field_display'); + + $entity = EntityTestBaseFieldDisplay::create([ + 'name' => 'The test entity', + 'layout_builder__layout' => $section_data, + ]); + $entity->save(); + return $entity->get('layout_builder__layout'); + } + +} diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemTest.php deleted file mode 100644 index 0e13471c155814143b703687a788165ceb967cef..0000000000000000000000000000000000000000 --- a/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemTest.php +++ /dev/null @@ -1,89 +0,0 @@ -<?php - -namespace Drupal\Tests\layout_builder\Kernel; - -use Drupal\Core\Field\FieldItemInterface; -use Drupal\Core\Field\FieldItemListInterface; -use Drupal\entity_test\Entity\EntityTest; -use Drupal\layout_builder\Field\LayoutSectionItemInterface; -use Drupal\layout_builder\Field\LayoutSectionItemListInterface; -use Drupal\Tests\field\Kernel\FieldKernelTestBase; - -/** - * Tests the field type for Layout Sections. - * - * @group layout_builder - */ -class LayoutSectionItemTest extends FieldKernelTestBase { - - /** - * Modules to enable. - * - * @var array - */ - public static $modules = ['layout_builder', 'layout_discovery']; - - /** - * Tests using entity fields of the layout section field type. - */ - public function testLayoutSectionItem() { - layout_builder_add_layout_section_field('entity_test', 'entity_test'); - - $entity = EntityTest::create(); - /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */ - $field_list = $entity->layout_builder__layout; - - // Test sample item generation. - $field_list->generateSampleItems(); - $this->entityValidateAndSave($entity); - - $field = $field_list->get(0); - $this->assertInstanceOf(LayoutSectionItemInterface::class, $field); - $this->assertInstanceOf(FieldItemInterface::class, $field); - $this->assertSame('section', $field->mainPropertyName()); - $this->assertSame('layout_onecol', $field->layout); - $this->assertSame([], $field->layout_settings); - $this->assertSame([], $field->section); - } - - /** - * {@inheritdoc} - */ - public function testLayoutSectionItemList() { - layout_builder_add_layout_section_field('entity_test', 'entity_test'); - - $entity = EntityTest::create(); - /** @var \Drupal\layout_builder\Field\LayoutSectionItemListInterface $field_list */ - $field_list = $entity->layout_builder__layout; - $this->assertInstanceOf(LayoutSectionItemListInterface::class, $field_list); - $this->assertInstanceOf(FieldItemListInterface::class, $field_list); - $entity->save(); - - $field_list->appendItem(['layout' => 'layout_twocol']); - $field_list->appendItem(['layout' => 'layout_onecol']); - $field_list->appendItem(['layout' => 'layout_threecol_25_50_25']); - $this->assertSame([ - ['layout' => 'layout_twocol'], - ['layout' => 'layout_onecol'], - ['layout' => 'layout_threecol_25_50_25'], - ], $field_list->getValue()); - - $field_list->addItem(1, ['layout' => 'layout_threecol_33_34_33']); - $this->assertSame([ - ['layout' => 'layout_twocol'], - ['layout' => 'layout_threecol_33_34_33'], - ['layout' => 'layout_onecol'], - ['layout' => 'layout_threecol_25_50_25'], - ], $field_list->getValue()); - - $field_list->addItem($field_list->count(), ['layout' => 'layout_twocol_bricks']); - $this->assertSame([ - ['layout' => 'layout_twocol'], - ['layout' => 'layout_threecol_33_34_33'], - ['layout' => 'layout_onecol'], - ['layout' => 'layout_threecol_25_50_25'], - ['layout' => 'layout_twocol_bricks'], - ], $field_list->getValue()); - } - -} diff --git a/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php b/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php new file mode 100644 index 0000000000000000000000000000000000000000..b8e8b3462c35f351334c2f1e1292705dc9bca418 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php @@ -0,0 +1,156 @@ +<?php + +namespace Drupal\Tests\layout_builder\Kernel; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\layout_builder\Section; +use Drupal\layout_builder\SectionComponent; + +/** + * Provides a base class for testing implementations of section storage. + */ +abstract class SectionStorageTestBase extends KernelTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'layout_builder', + 'layout_discovery', + 'layout_test', + 'user', + 'entity_test', + ]; + + /** + * The section storage implementation. + * + * @var \Drupal\layout_builder\SectionStorageInterface + */ + protected $sectionStorage; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $section_data = [ + [ + 'section' => new Section('layout_test_plugin', [], [ + 'first-uuid' => new SectionComponent('first-uuid', 'content'), + ]), + ], + [ + 'section' => new Section('layout_test_plugin', ['setting_1' => 'bar'], [ + 'second-uuid' => new SectionComponent('second-uuid', 'content'), + ]), + ], + ]; + $this->sectionStorage = $this->getEntity($section_data); + } + + /** + * Sets up the section storage entity. + * + * @param array $section_data + * An array of section data. + * + * @return \Drupal\Core\Entity\EntityInterface + * The entity. + */ + abstract protected function getEntity(array $section_data); + + /** + * @covers ::getSections + */ + public function testGetSections() { + $expected = [ + new Section('layout_test_plugin', [], [ + 'first-uuid' => new SectionComponent('first-uuid', 'content'), + ]), + new Section('layout_test_plugin', ['setting_1' => 'bar'], [ + 'second-uuid' => new SectionComponent('second-uuid', 'content'), + ]), + ]; + $this->assertSections($expected); + } + + /** + * @covers ::getSection + */ + public function testGetSection() { + $this->assertInstanceOf(Section::class, $this->sectionStorage->getSection(0)); + } + + /** + * @covers ::getSection + */ + public function testGetSectionInvalidDelta() { + $this->setExpectedException(\OutOfBoundsException::class, 'Invalid delta "2" for the "The test entity"'); + $this->sectionStorage->getSection(2); + } + + /** + * @covers ::insertSection + */ + public function testInsertSection() { + $expected = [ + new Section('layout_test_plugin', [], [ + 'first-uuid' => new SectionComponent('first-uuid', 'content'), + ]), + new Section('setting_1'), + new Section('layout_test_plugin', ['setting_1' => 'bar'], [ + 'second-uuid' => new SectionComponent('second-uuid', 'content'), + ]), + ]; + + $this->sectionStorage->insertSection(1, new Section('setting_1')); + $this->assertSections($expected); + } + + /** + * @covers ::appendSection + */ + public function testAppendSection() { + $expected = [ + new Section('layout_test_plugin', [], [ + 'first-uuid' => new SectionComponent('first-uuid', 'content'), + ]), + new Section('layout_test_plugin', ['setting_1' => 'bar'], [ + 'second-uuid' => new SectionComponent('second-uuid', 'content'), + ]), + new Section('foo'), + ]; + + $this->sectionStorage->appendSection(new Section('foo')); + $this->assertSections($expected); + } + + /** + * @covers ::removeSection + */ + public function testRemoveSection() { + $expected = [ + new Section('layout_test_plugin', ['setting_1' => 'bar'], [ + 'second-uuid' => new SectionComponent('second-uuid', 'content'), + ]), + ]; + + $this->sectionStorage->removeSection(0); + $this->assertSections($expected); + } + + /** + * Asserts that the field list has the expected sections. + * + * @param \Drupal\layout_builder\Section[] $expected + * The expected sections. + */ + protected function assertSections(array $expected) { + $result = $this->sectionStorage->getSections(); + $this->assertEquals($expected, $result); + $this->assertSame(array_keys($expected), array_keys($result)); + } + +} diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutSectionBuilderTest.php b/core/modules/layout_builder/tests/src/Unit/LayoutSectionBuilderTest.php index 3e5b2ffad148a47c1ff54354b0befdbb698d0d7b..28f22557e75bb2afa0aab82600a058b0571bfc4c 100644 --- a/core/modules/layout_builder/tests/src/Unit/LayoutSectionBuilderTest.php +++ b/core/modules/layout_builder/tests/src/Unit/LayoutSectionBuilderTest.php @@ -7,6 +7,8 @@ use Drupal\Core\Block\BlockManagerInterface; use Drupal\Core\Block\BlockPluginInterface; use Drupal\Core\Cache\Cache; +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\Layout\LayoutDefinition; use Drupal\Core\Layout\LayoutInterface; use Drupal\Core\Layout\LayoutPluginManagerInterface; use Drupal\Core\Plugin\Context\ContextHandlerInterface; @@ -14,6 +16,7 @@ use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\Session\AccountInterface; use Drupal\layout_builder\LayoutSectionBuilder; +use Drupal\layout_builder\SectionComponent; use Drupal\Tests\UnitTestCase; use Prophecy\Argument; @@ -86,7 +89,16 @@ protected function setUp() { $this->layoutSectionBuilder = new LayoutSectionBuilder($this->account->reveal(), $this->layoutPluginManager->reveal(), $this->blockManager->reveal(), $this->contextHandler->reveal(), $this->contextRepository->reveal()); $this->layout = $this->prophesize(LayoutInterface::class); + $this->layout->getPluginDefinition()->willReturn(new LayoutDefinition([])); + $this->layout->build(Argument::type('array'))->willReturnArgument(0); $this->layoutPluginManager->createInstance('layout_onecol', [])->willReturn($this->layout->reveal()); + + $container = new ContainerBuilder(); + $container->set('current_user', $this->account->reveal()); + $container->set('plugin.manager.block', $this->blockManager->reveal()); + $container->set('context.handler', $this->contextHandler->reveal()); + $container->set('context.repository', $this->contextRepository->reveal()); + \Drupal::setContainer($container); } /** @@ -102,8 +114,12 @@ public function testBuildSection() { '#base_plugin_id' => 'block_plugin_id', '#derivative_plugin_id' => NULL, 'content' => $block_content, + '#cache' => [ + 'contexts' => [], + 'tags' => [], + 'max-age' => -1, + ], ]; - $this->layout->build(['content' => ['some_uuid' => $render_array]])->willReturnArgument(0); $block = $this->prophesize(BlockPluginInterface::class); $this->blockManager->createInstance('block_plugin_id', ['id' => 'block_plugin_id'])->willReturn($block->reveal()); @@ -120,20 +136,9 @@ public function testBuildSection() { $block->getConfiguration()->willReturn([]); $section = [ - 'content' => [ - 'some_uuid' => [ - 'block' => [ - 'id' => 'block_plugin_id', - ], - ], - ], + new SectionComponent('some_uuid', 'content', ['id' => 'block_plugin_id']), ]; $expected = [ - '#cache' => [ - 'contexts' => [], - 'tags' => [], - 'max-age' => -1, - ], 'content' => [ 'some_uuid' => $render_array, ], @@ -146,7 +151,6 @@ public function testBuildSection() { * @covers ::buildSection */ public function testBuildSectionAccessDenied() { - $this->layout->build([])->willReturn([]); $block = $this->prophesize(BlockPluginInterface::class); $this->blockManager->createInstance('block_plugin_id', ['id' => 'block_plugin_id'])->willReturn($block->reveal()); @@ -156,21 +160,19 @@ public function testBuildSectionAccessDenied() { $block->build()->shouldNotBeCalled(); $section = [ + new SectionComponent('some_uuid', 'content', ['id' => 'block_plugin_id']), + ]; + $expected = [ 'content' => [ 'some_uuid' => [ - 'block' => [ - 'id' => 'block_plugin_id', + '#cache' => [ + 'contexts' => [], + 'tags' => [], + 'max-age' => -1, ], ], ], ]; - $expected = [ - '#cache' => [ - 'contexts' => [], - 'tags' => [], - 'max-age' => -1, - ], - ]; $result = $this->layoutSectionBuilder->buildSection('layout_onecol', [], $section); $this->assertEquals($expected, $result); } @@ -179,23 +181,14 @@ public function testBuildSectionAccessDenied() { * @covers ::buildSection */ public function testBuildSectionEmpty() { - $this->layout->build([])->willReturn([]); - $section = []; - $expected = [ - '#cache' => [ - 'contexts' => [], - 'tags' => [], - 'max-age' => -1, - ], - ]; + $expected = []; $result = $this->layoutSectionBuilder->buildSection('layout_onecol', [], $section); $this->assertEquals($expected, $result); } /** * @covers ::buildSection - * @covers ::getBlock */ public function testContextAwareBlock() { $render_array = [ @@ -206,8 +199,12 @@ public function testContextAwareBlock() { '#base_plugin_id' => 'block_plugin_id', '#derivative_plugin_id' => NULL, 'content' => [], + '#cache' => [ + 'contexts' => [], + 'tags' => [], + 'max-age' => -1, + ], ]; - $this->layout->build(['content' => ['some_uuid' => $render_array]])->willReturnArgument(0); $block = $this->prophesize(BlockPluginInterface::class)->willImplement(ContextAwarePluginInterface::class); $this->blockManager->createInstance('block_plugin_id', ['id' => 'block_plugin_id'])->willReturn($block->reveal()); @@ -228,20 +225,9 @@ public function testContextAwareBlock() { $this->contextHandler->applyContextMapping($block->reveal(), [])->shouldBeCalled(); $section = [ - 'content' => [ - 'some_uuid' => [ - 'block' => [ - 'id' => 'block_plugin_id', - ], - ], - ], + new SectionComponent('some_uuid', 'content', ['id' => 'block_plugin_id']), ]; $expected = [ - '#cache' => [ - 'contexts' => [], - 'tags' => [], - 'max-age' => -1, - ], 'content' => [ 'some_uuid' => $render_array, ], @@ -252,50 +238,10 @@ public function testContextAwareBlock() { /** * @covers ::buildSection - * @covers ::getBlock */ public function testBuildSectionMissingPluginId() { - $section = [ - 'content' => [ - 'some_uuid' => [ - 'block' => [], - ], - ], - ]; - $this->setExpectedException(PluginException::class, 'No plugin ID specified for block with "some_uuid" UUID'); - $this->layoutSectionBuilder->buildSection('layout_onecol', [], $section); - } - - /** - * @covers ::buildSection - * - * @dataProvider providerTestBuildSectionMalformedData - */ - public function testBuildSectionMalformedData($section, $message) { - $this->layout->build(Argument::type('array'))->willReturnArgument(0); - $this->layout->getPluginId()->willReturn('the_plugin_id'); - $this->setExpectedException(\InvalidArgumentException::class, $message); - $this->layoutSectionBuilder->buildSection('layout_onecol', [], $section); - } - - /** - * Provides test data for ::testBuildSectionMalformedData(). - */ - public function providerTestBuildSectionMalformedData() { - $data = []; - $data['invalid_region'] = [ - ['content' => 'bar'], - 'The "content" region in the "the_plugin_id" layout has invalid configuration', - ]; - $data['invalid_configuration'] = [ - ['content' => ['some_uuid' => 'bar']], - 'The block with UUID of "some_uuid" has invalid configuration', - ]; - $data['invalid_blocks'] = [ - ['content' => ['some_uuid' => []]], - 'The block with UUID of "some_uuid" has invalid configuration', - ]; - return $data; + $this->setExpectedException(PluginException::class, 'No plugin ID specified for component with "some_uuid" UUID'); + $this->layoutSectionBuilder->buildSection('layout_onecol', [], [new SectionComponent('some_uuid', 'content')]); } } diff --git a/core/modules/layout_builder/tests/src/Unit/SectionTest.php b/core/modules/layout_builder/tests/src/Unit/SectionTest.php index 33a338706e27ec78aace18dc38969453ae03363e..eff239507d2b111c7717474e483e4706ad58f071 100644 --- a/core/modules/layout_builder/tests/src/Unit/SectionTest.php +++ b/core/modules/layout_builder/tests/src/Unit/SectionTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\layout_builder\Unit; use Drupal\layout_builder\Section; +use Drupal\layout_builder\SectionComponent; use Drupal\Tests\UnitTestCase; /** @@ -24,242 +25,158 @@ class SectionTest extends UnitTestCase { protected function setUp() { parent::setUp(); - $this->section = new Section([ - 'empty-region' => [], - 'some-region' => [ - 'existing-uuid' => [ - 'block' => [ - 'id' => 'existing-block-id', - ], - ], - ], - 'ordered-region' => [ - 'first-uuid' => [ - 'block' => [ - 'id' => 'first-block-id', - ], - ], - 'second-uuid' => [ - 'block' => [ - 'id' => 'second-block-id', - ], - ], - ], + $this->section = new Section('layout_onecol', [], [ + new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']), + (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(3), + (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(2), ]); } /** * @covers ::__construct - * @covers ::getValue + * @covers ::setComponent + * @covers ::getComponents */ - public function testGetValue() { + public function testGetComponents() { $expected = [ - 'empty-region' => [], - 'some-region' => [ - 'existing-uuid' => [ - 'block' => [ - 'id' => 'existing-block-id', - ], - ], - ], - 'ordered-region' => [ - 'first-uuid' => [ - 'block' => [ - 'id' => 'first-block-id', - ], - ], - 'second-uuid' => [ - 'block' => [ - 'id' => 'second-block-id', - ], - ], - ], + 'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0), + 'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(3), + 'first-uuid' => (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(2), ]; - $result = $this->section->getValue(); - $this->assertSame($expected, $result); - } - /** - * @covers ::getBlock - */ - public function testGetBlockInvalidRegion() { - $this->setExpectedException(\InvalidArgumentException::class, 'Invalid region'); - $this->section->getBlock('invalid-region', 'existing-uuid'); + $this->assertComponents($expected, $this->section); } /** - * @covers ::getBlock + * @covers ::getComponent */ - public function testGetBlockInvalidUuid() { - $this->setExpectedException(\InvalidArgumentException::class, 'Invalid UUID'); - $this->section->getBlock('some-region', 'invalid-uuid'); + public function testGetComponentInvalidUuid() { + $this->setExpectedException(\InvalidArgumentException::class, 'Invalid UUID "invalid-uuid"'); + $this->section->getComponent('invalid-uuid'); } /** - * @covers ::getBlock + * @covers ::getComponent */ - public function testGetBlock() { - $expected = ['block' => ['id' => 'existing-block-id']]; + public function testGetComponent() { + $expected = new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']); - $block = $this->section->getBlock('some-region', 'existing-uuid'); - $this->assertSame($expected, $block); + $this->assertEquals($expected, $this->section->getComponent('existing-uuid')); } /** - * @covers ::removeBlock + * @covers ::removeComponent + * @covers ::getComponentsByRegion */ - public function testRemoveBlock() { - $this->section->removeBlock('some-region', 'existing-uuid'); + public function testRemoveComponent() { $expected = [ - 'ordered-region' => [ - 'first-uuid' => [ - 'block' => [ - 'id' => 'first-block-id', - ], - ], - 'second-uuid' => [ - 'block' => [ - 'id' => 'second-block-id', - ], - ], - ], + 'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0), + 'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(3), ]; - $this->assertSame($expected, $this->section->getValue()); + + $this->section->removeComponent('first-uuid'); + $this->assertComponents($expected, $this->section); } /** - * @covers ::addBlock + * @covers ::appendComponent + * @covers ::getNextHighestWeight + * @covers ::getComponentsByRegion */ - public function testAddBlock() { - $this->section->addBlock('some-region', 'new-uuid', []); + public function testAppendComponent() { $expected = [ - 'empty-region' => [], - 'some-region' => [ - 'new-uuid' => [], - 'existing-uuid' => [ - 'block' => [ - 'id' => 'existing-block-id', - ], - ], - ], - 'ordered-region' => [ - 'first-uuid' => [ - 'block' => [ - 'id' => 'first-block-id', - ], - ], - 'second-uuid' => [ - 'block' => [ - 'id' => 'second-block-id', - ], - ], - ], + 'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0), + 'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(3), + 'first-uuid' => (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(2), + 'new-uuid' => (new SectionComponent('new-uuid', 'some-region', []))->setWeight(1), ]; - $this->assertSame($expected, $this->section->getValue()); + + $this->section->appendComponent(new SectionComponent('new-uuid', 'some-region')); + $this->assertComponents($expected, $this->section); } /** - * @covers ::insertBlock + * @covers ::insertAfterComponent */ - public function testInsertBlock() { - $this->section->insertBlock('ordered-region', 'new-uuid', [], 'first-uuid'); + public function testInsertAfterComponent() { $expected = [ - 'empty-region' => [], - 'some-region' => [ - 'existing-uuid' => [ - 'block' => [ - 'id' => 'existing-block-id', - ], - ], - ], - 'ordered-region' => [ - 'first-uuid' => [ - 'block' => [ - 'id' => 'first-block-id', - ], - ], - 'new-uuid' => [], - 'second-uuid' => [ - 'block' => [ - 'id' => 'second-block-id', - ], - ], - ], + 'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0), + 'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(4), + 'first-uuid' => (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(2), + 'new-uuid' => (new SectionComponent('new-uuid', 'ordered-region', []))->setWeight(3), ]; - $this->assertSame($expected, $this->section->getValue()); + + $this->section->insertAfterComponent('first-uuid', new SectionComponent('new-uuid', 'ordered-region')); + $this->assertComponents($expected, $this->section); } /** - * @covers ::insertBlock + * @covers ::insertAfterComponent */ - public function testInsertBlockInvalidRegion() { - $this->setExpectedException(\InvalidArgumentException::class, 'Invalid region'); - $this->section->insertBlock('invalid-region', 'new-uuid', [], 'first-uuid'); + public function testInsertAfterComponentValidUuidRegionMismatch() { + $this->setExpectedException(\InvalidArgumentException::class, 'Invalid preceding UUID "existing-uuid"'); + $this->section->insertAfterComponent('existing-uuid', new SectionComponent('new-uuid', 'ordered-region')); } /** - * @covers ::insertBlock + * @covers ::insertAfterComponent */ - public function testInsertBlockInvalidUuid() { - $this->setExpectedException(\InvalidArgumentException::class, 'Invalid preceding UUID'); - $this->section->insertBlock('ordered-region', 'new-uuid', [], 'invalid-uuid'); + public function testInsertAfterComponentInvalidUuid() { + $this->setExpectedException(\InvalidArgumentException::class, 'Invalid preceding UUID "invalid-uuid"'); + $this->section->insertAfterComponent('invalid-uuid', new SectionComponent('new-uuid', 'ordered-region')); } /** - * @covers ::updateBlock + * @covers ::insertComponent + * @covers ::getComponentsByRegion */ - public function testUpdateBlock() { - $this->section->updateBlock('some-region', 'existing-uuid', [ - 'block' => [ - 'id' => 'existing-block-id', - 'settings' => [ - 'foo' => 'bar', - ], - ], - ]); + public function testInsertComponent() { + $expected = [ + 'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0), + 'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(4), + 'first-uuid' => (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(3), + 'new-uuid' => (new SectionComponent('new-uuid', 'ordered-region', []))->setWeight(2), + ]; + + $this->section->insertComponent(0, new SectionComponent('new-uuid', 'ordered-region')); + $this->assertComponents($expected, $this->section); + } + /** + * @covers ::insertComponent + */ + public function testInsertComponentAppend() { $expected = [ - 'empty-region' => [], - 'some-region' => [ - 'existing-uuid' => [ - 'block' => [ - 'id' => 'existing-block-id', - 'settings' => [ - 'foo' => 'bar', - ], - ], - ], - ], - 'ordered-region' => [ - 'first-uuid' => [ - 'block' => [ - 'id' => 'first-block-id', - ], - ], - 'second-uuid' => [ - 'block' => [ - 'id' => 'second-block-id', - ], - ], - ], + 'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0), + 'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(3), + 'first-uuid' => (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(2), + 'new-uuid' => (new SectionComponent('new-uuid', 'ordered-region', []))->setWeight(4), ]; - $this->assertSame($expected, $this->section->getValue()); + + $this->section->insertComponent(2, new SectionComponent('new-uuid', 'ordered-region')); + $this->assertComponents($expected, $this->section); } /** - * @covers ::updateBlock + * @covers ::insertComponent */ - public function testUpdateBlockInvalidRegion() { - $this->setExpectedException(\InvalidArgumentException::class, 'Invalid region'); - $this->section->updateBlock('invalid-region', 'new-uuid', []); + public function testInsertComponentInvalidDelta() { + $this->setExpectedException(\OutOfBoundsException::class, 'Invalid delta "7" for the "new-uuid" component'); + $this->section->insertComponent(7, new SectionComponent('new-uuid', 'ordered-region')); } /** - * @covers ::updateBlock + * Asserts that the section has the expected components. + * + * @param \Drupal\layout_builder\SectionComponent[] $expected + * The expected sections. + * @param \Drupal\layout_builder\Section $section + * The section storage to check. */ - public function testUpdateBlockInvalidUuid() { - $this->setExpectedException(\InvalidArgumentException::class, 'Invalid UUID'); - $this->section->updateBlock('ordered-region', 'new-uuid', []); + protected function assertComponents(array $expected, Section $section) { + $result = $section->getComponents(); + $this->assertEquals($expected, $result); + $this->assertSame(array_keys($expected), array_keys($result)); } }