Issue #2926914 by tim.plunkett, xjm, larowlan, tedbow, EclipseGc: Rewrite \Drupal\layout_builder\Section to represent the entire section, not just the block info

8.5.x
xjm 2017-12-19 09:25:18 -10:00
parent 3cf0815a54
commit 2e16f2a35d
25 changed files with 1150 additions and 831 deletions

View File

@ -6,6 +6,7 @@ use Drupal\Core\DependencyInjection\ClassResolverInterface;
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 @@ class AddSectionController implements ContainerInjectionInterface {
* 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);

View File

@ -10,8 +10,8 @@ use Drupal\Core\Plugin\PluginFormInterface;
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 @@ class LayoutBuilderController implements ContainerInjectionInterface {
$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 @@ class LayoutBuilderController implements ContainerInjectionInterface {
/**
* 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 @@ class LayoutBuilderController implements ContainerInjectionInterface {
* @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) {

View File

@ -69,32 +69,29 @@ class MoveBlockController implements ContainerInjectionInterface {
* 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);
}

View File

@ -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);
}

View File

@ -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 @@ use Drupal\Core\Field\FieldItemList;
*
* @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 $item;
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 $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;
}
}

View File

@ -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);
}

View File

@ -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 @@ class AddBlockForm extends ConfigureBlockFormBase {
* {@inheritdoc}
*/
protected function submitBlock(Section $section, $region, $uuid, array $configuration) {
$section->addBlock($region, $uuid, $configuration);
$section->appendComponent(new SectionComponent($uuid, $region, $configuration));
}
}

View File

@ -246,11 +246,10 @@ abstract class ConfigureBlockFormBase extends FormBase {
$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'));

View File

@ -14,6 +14,7 @@ use Drupal\Core\Plugin\PluginFormInterface;
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 @@ class ConfigureSectionForm extends FormBase {
$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 @@ class ConfigureSectionForm extends FormBase {
$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);

View File

@ -60,11 +60,9 @@ class RemoveBlockForm extends LayoutRebuildConfirmFormBase {
* {@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);
}
}

View File

@ -40,14 +40,11 @@ class UpdateBlockForm extends ConfigureBlockFormBase {
* 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 @@ class UpdateBlockForm extends ConfigureBlockFormBase {
* {@inheritdoc}
*/
protected function submitBlock(Section $section, $region, $uuid, array $configuration) {
$section->updateBlock($region, $uuid, $configuration);
$section->getComponent($uuid)->setConfiguration($configuration);
}
}

View File

@ -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 @@ use Drupal\Core\StringTranslation\StringTranslationTrait;
* Builds the UI for layout sections.
*
* @internal
*
* @todo Remove in https://www.drupal.org/project/drupal/issues/2928450.
*/
class LayoutSectionBuilder {
@ -84,37 +83,21 @@ class LayoutSectionBuilder {
*
* @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 @@ class LayoutSectionBuilder {
* 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);
}
}

View File

@ -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);
}
}

View File

@ -78,9 +78,9 @@ class LayoutSectionFormatter extends FormatterBase implements ContainerFactoryPl
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;

View File

@ -7,8 +7,6 @@ use Drupal\Core\Field\FieldItemBase;
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 @@ use Drupal\layout_builder\Section;
* 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 @@ class LayoutSectionItem extends FieldItemBase implements LayoutSectionItemInterf
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 @@ class LayoutSectionItem extends FieldItemBase implements LayoutSectionItemInterf
* {@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 @@ class LayoutSectionItem extends FieldItemBase implements LayoutSectionItemInterf
* {@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);
}
}

View File

@ -5,158 +5,303 @@ namespace Drupal\layout_builder;
/**
* 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.
*
* @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 string
* The layout plugin ID.
*
* @internal
* This method should only be used by code responsible for storing the data.
*/
public function getLayoutId() {
return $this->layoutId;
}
/**
* 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;
}
/**
* Returns the components of the section.
*
* @return \Drupal\layout_builder\SectionComponent[]
* The components.
*/
public function getComponents() {
return $this->components;
}
/**
* Gets the component for a given UUID.
*
* @param string $region
* The region name.
* @param string $uuid
* The UUID of the block to retrieve.
* The UUID of the component to retrieve.
*
* @return array
* The block configuration.
* @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 getBlock($region, $uuid) {
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');
}
return $this->section[$region][$uuid];
return $this->components[$uuid];
}
/**
* Updates the configuration of a given block from a region.
* Helper method to set a component.
*
* @param string $region
* The region name.
* @param string $uuid
* The UUID of the block to retrieve.
* @param array $configuration
* The block configuration.
* @param \Drupal\layout_builder\SectionComponent $component
* The component.
*
* @return $this
*
* @throws \InvalidArgumentException
* Thrown when the expected region or UUID do not exist.
*/
public function updateBlock($region, $uuid, array $configuration) {
if (!isset($this->section[$region])) {
throw new \InvalidArgumentException('Invalid region');
}
if (!isset($this->section[$region][$uuid])) {
throw new \InvalidArgumentException('Invalid UUID');
}
$this->section[$region][$uuid] = $configuration;
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 insertBlock($region, $uuid, array $configuration, $preceding_uuid) {
if (!isset($this->section[$region])) {
throw new \InvalidArgumentException('Invalid region');
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 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');
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -4,6 +4,8 @@ namespace Drupal\Tests\layout_builder\Functional;
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 @@ class LayoutSectionTest extends BrowserTestBase {
$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 @@ class LayoutSectionTest extends BrowserTestBase {
$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 @@ class LayoutSectionTest extends BrowserTestBase {
$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 @@ class LayoutSectionTest extends BrowserTestBase {
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 @@ class LayoutSectionTest extends BrowserTestBase {
$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',
]),
]),
],
],
]);

View File

@ -8,6 +8,7 @@ use Drupal\entity_test\Entity\EntityTestBaseFieldDisplay;
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 @@ class LayoutBuilderFieldLayoutCompatibilityTest extends KernelTestBase {
$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

View File

@ -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');
}
}

View File

@ -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());
}
}

View File

@ -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));
}
}

View File

@ -7,6 +7,8 @@ use Drupal\Core\Access\AccessResult;
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\Context\ContextRepositoryInterface;
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 @@ class LayoutSectionBuilderTest extends UnitTestCase {
$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 @@ class LayoutSectionBuilderTest extends UnitTestCase {
'#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 @@ class LayoutSectionBuilderTest extends UnitTestCase {
$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 @@ class LayoutSectionBuilderTest extends UnitTestCase {
* @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,19 +160,17 @@ class LayoutSectionBuilderTest extends UnitTestCase {
$block->build()->shouldNotBeCalled();
$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' => [
'#cache' => [
'contexts' => [],
'tags' => [],
'max-age' => -1,
],
],
],
];
$result = $this->layoutSectionBuilder->buildSection('layout_onecol', [], $section);
@ -179,23 +181,14 @@ class LayoutSectionBuilderTest extends UnitTestCase {
* @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 @@ class LayoutSectionBuilderTest extends UnitTestCase {
'#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 @@ class LayoutSectionBuilderTest extends UnitTestCase {
$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 @@ class LayoutSectionBuilderTest extends UnitTestCase {
/**
* @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')]);
}
}

View File

@ -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);
$this->assertComponents($expected, $this->section);
}
/**
* @covers ::getBlock
* @covers ::getComponent
*/
public function testGetBlockInvalidRegion() {
$this->setExpectedException(\InvalidArgumentException::class, 'Invalid region');
$this->section->getBlock('invalid-region', 'existing-uuid');
public function testGetComponentInvalidUuid() {
$this->setExpectedException(\InvalidArgumentException::class, 'Invalid UUID "invalid-uuid"');
$this->section->getComponent('invalid-uuid');
}
/**
* @covers ::getBlock
* @covers ::getComponent
*/
public function testGetBlockInvalidUuid() {
$this->setExpectedException(\InvalidArgumentException::class, 'Invalid UUID');
$this->section->getBlock('some-region', 'invalid-uuid');
public function testGetComponent() {
$expected = new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']);
$this->assertEquals($expected, $this->section->getComponent('existing-uuid'));
}
/**
* @covers ::getBlock
* @covers ::removeComponent
* @covers ::getComponentsByRegion
*/
public function testGetBlock() {
$expected = ['block' => ['id' => 'existing-block-id']];
$block = $this->section->getBlock('some-region', 'existing-uuid');
$this->assertSame($expected, $block);
}
/**
* @covers ::removeBlock
*/
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 = [
'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(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->assertSame($expected, $this->section->getValue());
$this->section->insertComponent(0, 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 testInsertComponentAppend() {
$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(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->section->insertComponent(2, new SectionComponent('new-uuid', 'ordered-region'));
$this->assertComponents($expected, $this->section);
}
/**
* @covers ::updateBlock
* @covers ::insertComponent
*/
public function testUpdateBlockInvalidUuid() {
$this->setExpectedException(\InvalidArgumentException::class, 'Invalid UUID');
$this->section->updateBlock('ordered-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'));
}
/**
* 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.
*/
protected function assertComponents(array $expected, Section $section) {
$result = $section->getComponents();
$this->assertEquals($expected, $result);
$this->assertSame(array_keys($expected), array_keys($result));
}
}