diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index 80339c1f70e591efaa20d4e3aac5c9808f64b13c..b1ecb5d594d0025412591e9a9056202f406423af 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -73,9 +73,8 @@ function layout_builder_form_entity_form_display_edit_form_alter(&$form, FormSta */ function layout_builder_field_config_insert(FieldConfigInterface $field_config) { // Clear the sample entity for this entity type and bundle. - /** @var \Drupal\Core\TempStore\SharedTempStore $tempstore */ - $tempstore = \Drupal::service('tempstore.shared')->get('layout_builder.sample_entity'); - $tempstore->delete($field_config->getTargetEntityTypeId() . '.' . $field_config->getTargetBundle()); + $sample_entity_generator = \Drupal::service('layout_builder.sample_entity_generator'); + $sample_entity_generator->delete($field_config->getTargetEntityTypeId(), $field_config->getTargetBundle()); \Drupal::service('plugin.manager.block')->clearCachedDefinitions(); } @@ -84,8 +83,7 @@ function layout_builder_field_config_insert(FieldConfigInterface $field_config) */ function layout_builder_field_config_delete(FieldConfigInterface $field_config) { // Clear the sample entity for this entity type and bundle. - /** @var \Drupal\Core\TempStore\SharedTempStore $tempstore */ - $tempstore = \Drupal::service('tempstore.shared')->get('layout_builder.sample_entity'); - $tempstore->delete($field_config->getTargetEntityTypeId() . '.' . $field_config->getTargetBundle()); + $sample_entity_generator = \Drupal::service('layout_builder.sample_entity_generator'); + $sample_entity_generator->delete($field_config->getTargetEntityTypeId(), $field_config->getTargetBundle()); \Drupal::service('plugin.manager.block')->clearCachedDefinitions(); } diff --git a/core/modules/layout_builder/layout_builder.routing.yml b/core/modules/layout_builder/layout_builder.routing.yml index 98465538625e108da9f8bf1f80c133b4984cabdb..322e2301c11b9a69f37ae588b581174f53c5bd47 100644 --- a/core/modules/layout_builder/layout_builder.routing.yml +++ b/core/modules/layout_builder/layout_builder.routing.yml @@ -115,6 +115,3 @@ layout_builder.move_block: parameters: section_storage: layout_builder_tempstore: TRUE - -route_callbacks: - - 'layout_builder.routes:getRoutes' diff --git a/core/modules/layout_builder/layout_builder.services.yml b/core/modules/layout_builder/layout_builder.services.yml index 38933073d965a097cb4b2d57ad33e5357d8cb86b..778a6d5fa60ce8ef3ec92e025bf5d1ae7d19e39c 100644 --- a/core/modules/layout_builder/layout_builder.services.yml +++ b/core/modules/layout_builder/layout_builder.services.yml @@ -6,9 +6,12 @@ services: class: Drupal\layout_builder\Access\LayoutSectionAccessCheck tags: - { name: access_check, applies_to: _has_layout_section } + plugin.manager.layout_builder.section_storage: + class: Drupal\layout_builder\SectionStorage\SectionStorageManager + parent: default_plugin_manager layout_builder.routes: class: Drupal\layout_builder\Routing\LayoutBuilderRoutes - arguments: ['@entity_type.manager', '@entity_field.manager'] + arguments: ['@plugin.manager.layout_builder.section_storage'] tags: - { name: event_subscriber } layout_builder.route_enhancer: @@ -17,17 +20,14 @@ services: - { name: route_enhancer } layout_builder.param_converter: class: Drupal\layout_builder\Routing\LayoutTempstoreParamConverter - arguments: ['@layout_builder.tempstore_repository', '@class_resolver'] + arguments: ['@layout_builder.tempstore_repository', '@plugin.manager.layout_builder.section_storage'] tags: - { name: paramconverter, priority: 10 } - layout_builder.section_storage_param_converter.defaults: - class: Drupal\layout_builder\Routing\SectionStorageDefaultsParamConverter - arguments: ['@entity.manager'] - layout_builder.section_storage_param_converter.overrides: - class: Drupal\layout_builder\Routing\SectionStorageOverridesParamConverter - arguments: ['@entity.manager'] cache_context.layout_builder_is_active: class: Drupal\layout_builder\Cache\LayoutBuilderIsActiveCacheContext arguments: ['@current_route_match'] tags: - { name: cache.context} + layout_builder.sample_entity_generator: + class: Drupal\layout_builder\Entity\LayoutBuilderSampleEntityGenerator + arguments: ['@tempstore.shared', '@entity_type.manager'] diff --git a/core/modules/layout_builder/src/Annotation/SectionStorage.php b/core/modules/layout_builder/src/Annotation/SectionStorage.php new file mode 100644 index 0000000000000000000000000000000000000000..42f4a47fe677acc888be293bd715e849ad28174d --- /dev/null +++ b/core/modules/layout_builder/src/Annotation/SectionStorage.php @@ -0,0 +1,32 @@ +<?php + +namespace Drupal\layout_builder\Annotation; + +use Drupal\Component\Annotation\Plugin; +use Drupal\layout_builder\SectionStorage\SectionStorageDefinition; + +/** + * Defines a Section Storage type annotation object. + * + * @see \Drupal\layout_builder\SectionStorage\SectionStorageManager + * @see plugin_api + * + * @Annotation + */ +class SectionStorage extends Plugin { + + /** + * The plugin ID. + * + * @var string + */ + public $id; + + /** + * {@inheritdoc} + */ + public function get() { + return new SectionStorageDefinition($this->definition); + } + +} diff --git a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php index 9642c4e2a70a3d17070dbe5048e78fd4dced384e..8f7666f579a570920a045d9bc610523688afe262 100644 --- a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php +++ b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php @@ -305,7 +305,7 @@ public function saveLayout(SectionStorageInterface $section_storage) { $this->messenger->addMessage($this->t('The layout has been saved.')); } - return new RedirectResponse($section_storage->getCanonicalUrl()->setAbsolute()->toString()); + return new RedirectResponse($section_storage->getRedirectUrl()->setAbsolute()->toString()); } /** @@ -322,7 +322,7 @@ public function cancelLayout(SectionStorageInterface $section_storage) { $this->messenger->addMessage($this->t('The changes to the layout have been discarded.')); - return new RedirectResponse($section_storage->getCanonicalUrl()->setAbsolute()->toString()); + return new RedirectResponse($section_storage->getRedirectUrl()->setAbsolute()->toString()); } } diff --git a/core/modules/layout_builder/src/DefaultsSectionStorageInterface.php b/core/modules/layout_builder/src/DefaultsSectionStorageInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..9397c6cea58506536528c28282e1335c63592097 --- /dev/null +++ b/core/modules/layout_builder/src/DefaultsSectionStorageInterface.php @@ -0,0 +1,33 @@ +<?php + +namespace Drupal\layout_builder; + +/** + * Defines an interface for an object that stores layout sections for defaults. + * + * @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. + */ +interface DefaultsSectionStorageInterface extends SectionStorageInterface { + + /** + * Determines if the defaults allow custom overrides. + * + * @return bool + * TRUE if custom overrides are allowed, FALSE otherwise. + */ + public function isOverridable(); + + /** + * Sets the defaults to allow or disallow overrides. + * + * @param bool $overridable + * TRUE if the display should allow overrides, FALSE otherwise. + * + * @return $this + */ + public function setOverridable($overridable = TRUE); + +} diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php index 9ed5f73ecb5e874994ff7dde3f31afaa39b472c0..2582d5c7ed0c9f3e5f18dbbe0a6c8982d74f520f 100644 --- a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php @@ -5,18 +5,17 @@ use Drupal\Component\Plugin\DependentPluginInterface; use Drupal\Component\Plugin\PluginInspectionInterface; use Drupal\Component\Utility\NestedArray; -use Drupal\Core\Entity\ContentEntityStorageInterface; use Drupal\Core\Entity\Entity\EntityViewDisplay as BaseEntityViewDisplay; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Plugin\Context\Context; use Drupal\Core\Plugin\Context\ContextDefinition; use Drupal\Core\StringTranslation\TranslatableMarkup; -use Drupal\Core\Url; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\layout_builder\Section; use Drupal\layout_builder\SectionComponent; +use Drupal\layout_builder\SectionStorage\SectionStorageTrait; /** * Provides an entity view display entity that has a layout. @@ -28,6 +27,8 @@ */ class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements LayoutEntityDisplayInterface { + use SectionStorageTrait; + /** * {@inheritdoc} */ @@ -51,104 +52,13 @@ public function getSections() { } /** - * Store the information for all sections. - * - * @param \Drupal\layout_builder\Section[] $sections - * The sections information. - * - * @return $this + * {@inheritdoc} */ protected function setSections(array $sections) { $this->setThirdPartySetting('layout_builder', 'sections', array_values($sections)); return $this; } - /** - * {@inheritdoc} - */ - public function count() { - return count($this->getSections()); - } - - /** - * {@inheritdoc} - */ - public function getSection($delta) { - if (!$this->hasSection($delta)) { - throw new \OutOfBoundsException(sprintf('Invalid delta "%s" for the "%s" entity', $delta, $this->id())); - } - - return $this->getSections()[$delta]; - } - - /** - * Sets the section for the given delta on the display. - * - * @param int $delta - * The delta of the section. - * @param \Drupal\layout_builder\Section $section - * The layout section. - * - * @return $this - */ - protected function setSection($delta, Section $section) { - $sections = $this->getSections(); - $sections[$delta] = $section; - $this->setSections($sections); - return $this; - } - - /** - * {@inheritdoc} - */ - public function appendSection(Section $section) { - $delta = $this->count(); - - $this->setSection($delta, $section); - return $this; - } - - /** - * {@inheritdoc} - */ - public function insertSection($delta, Section $section) { - if ($this->hasSection($delta)) { - $sections = $this->getSections(); - // @todo Use https://www.drupal.org/node/66183 once resolved. - $start = array_slice($sections, 0, $delta); - $end = array_slice($sections, $delta); - $this->setSections(array_merge($start, [$section], $end)); - } - else { - $this->appendSection($section); - } - return $this; - } - - /** - * {@inheritdoc} - */ - public function removeSection($delta) { - $sections = $this->getSections(); - unset($sections[$delta]); - $this->setSections($sections); - return $this; - } - - /** - * Indicates if there is a section at the specified delta. - * - * @param int $delta - * The delta of the section. - * - * @return bool - * TRUE if there is a section for this delta, FALSE otherwise. - */ - protected function hasSection($delta) { - $sections = $this->getSections(); - return isset($sections[$delta]); - } - /** * {@inheritdoc} */ @@ -255,50 +165,6 @@ public function buildMultiple(array $entities) { return $build_list; } - /** - * {@inheritdoc} - */ - public function getContexts() { - $entity = $this->getSampleEntity($this->getTargetEntityTypeId(), $this->getTargetBundle()); - $context_label = new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()]); - - // @todo Use EntityContextDefinition after resolving - // https://www.drupal.org/node/2932462. - $contexts = []; - $contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", $context_label), $entity); - return $contexts; - } - - /** - * Returns a sample entity. - * - * @param string $entity_type_id - * The entity type ID. - * @param string $bundle_id - * The bundle ID. - * - * @return \Drupal\Core\Entity\EntityInterface - * An entity. - */ - protected function getSampleEntity($entity_type_id, $bundle_id) { - /** @var \Drupal\Core\TempStore\SharedTempStore $tempstore */ - $tempstore = \Drupal::service('tempstore.shared')->get('layout_builder.sample_entity'); - if ($entity = $tempstore->get("$entity_type_id.$bundle_id")) { - return $entity; - } - - $entity_storage = $this->entityTypeManager()->getStorage($entity_type_id); - if (!$entity_storage instanceof ContentEntityStorageInterface) { - throw new \InvalidArgumentException(sprintf('The "%s" entity storage is not supported', $entity_type_id)); - } - - $entity = $entity_storage->createWithSampleValues($bundle_id); - // Mark the sample entity as being a preview. - $entity->in_preview = TRUE; - $tempstore->set("$entity_type_id.$bundle_id", $entity); - return $entity; - } - /** * Gets the runtime sections for a given entity. * @@ -328,51 +194,6 @@ public function label() { return new TranslatableMarkup('@bundle @label', ['@bundle' => $bundle_label, '@label' => $target_entity_type->getPluralLabel()]); } - /** - * {@inheritdoc} - */ - public static function getStorageType() { - return 'defaults'; - } - - /** - * {@inheritdoc} - */ - public function getStorageId() { - return $this->id(); - } - - /** - * {@inheritdoc} - */ - public function getCanonicalUrl() { - return Url::fromRoute("entity.entity_view_display.{$this->getTargetEntityTypeId()}.view_mode", $this->getRouteParameters()); - } - - /** - * {@inheritdoc} - */ - public function getLayoutBuilderUrl() { - return Url::fromRoute("entity.entity_view_display.{$this->getTargetEntityTypeId()}.layout_builder", $this->getRouteParameters()); - } - - /** - * Returns the route parameters needed to build routes for this entity. - * - * @return string[] - * An array of route parameters. - */ - protected function getRouteParameters() { - $route_parameters = []; - - $entity_type = $this->entityTypeManager()->getDefinition($this->getTargetEntityTypeId()); - $bundle_parameter_key = $entity_type->getBundleEntityType() ?: 'bundle'; - $route_parameters[$bundle_parameter_key] = $this->getTargetBundle(); - - $route_parameters['view_mode_name'] = $this->getMode(); - return $route_parameters; - } - /** * {@inheritdoc} */ diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderSampleEntityGenerator.php b/core/modules/layout_builder/src/Entity/LayoutBuilderSampleEntityGenerator.php new file mode 100644 index 0000000000000000000000000000000000000000..9f606070e3fd8ea914811c0511e71deccf1f8450 --- /dev/null +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderSampleEntityGenerator.php @@ -0,0 +1,91 @@ +<?php + +namespace Drupal\layout_builder\Entity; + +use Drupal\Core\Entity\ContentEntityStorageInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\TempStore\SharedTempStoreFactory; + +/** + * Generates a sample entity for use by the Layout Builder. + * + * @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. + */ +class LayoutBuilderSampleEntityGenerator { + + /** + * The shared tempstore factory. + * + * @var \Drupal\Core\TempStore\SharedTempStoreFactory + */ + protected $tempStoreFactory; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * LayoutBuilderSampleEntityGenerator constructor. + * + * @param \Drupal\Core\TempStore\SharedTempStoreFactory $temp_store_factory + * The tempstore factory. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ + public function __construct(SharedTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $entity_type_manager) { + $this->tempStoreFactory = $temp_store_factory; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * Gets a sample entity for a given entity type and bundle. + * + * @param string $entity_type_id + * The entity type ID. + * @param string $bundle_id + * The bundle ID. + * + * @return \Drupal\Core\Entity\EntityInterface + * An entity. + */ + public function get($entity_type_id, $bundle_id) { + $tempstore = $this->tempStoreFactory->get('layout_builder.sample_entity'); + if ($entity = $tempstore->get("$entity_type_id.$bundle_id")) { + return $entity; + } + + $entity_storage = $this->entityTypeManager->getStorage($entity_type_id); + if (!$entity_storage instanceof ContentEntityStorageInterface) { + throw new \InvalidArgumentException(sprintf('The "%s" entity storage is not supported', $entity_type_id)); + } + + $entity = $entity_storage->createWithSampleValues($bundle_id); + // Mark the sample entity as being a preview. + $entity->in_preview = TRUE; + $tempstore->set("$entity_type_id.$bundle_id", $entity); + return $entity; + } + + /** + * Deletes a sample entity for a given entity type and bundle. + * + * @param string $entity_type_id + * The entity type ID. + * @param string $bundle_id + * The bundle ID. + * + * @return $this + */ + public function delete($entity_type_id, $bundle_id) { + $tempstore = $this->tempStoreFactory->get('layout_builder.sample_entity'); + $tempstore->delete("$entity_type_id.$bundle_id"); + return $this; + } + +} diff --git a/core/modules/layout_builder/src/Entity/LayoutEntityDisplayInterface.php b/core/modules/layout_builder/src/Entity/LayoutEntityDisplayInterface.php index 1affc53b399418fe6edf519ff6fdde0a0b2a535d..b8fd21d822b224dcb266ff7f45b8644b03f4c50f 100644 --- a/core/modules/layout_builder/src/Entity/LayoutEntityDisplayInterface.php +++ b/core/modules/layout_builder/src/Entity/LayoutEntityDisplayInterface.php @@ -3,7 +3,7 @@ namespace Drupal\layout_builder\Entity; use Drupal\Core\Entity\Display\EntityDisplayInterface; -use Drupal\layout_builder\SectionStorageInterface; +use Drupal\layout_builder\SectionListInterface; /** * Provides an interface for entity displays that have layout. @@ -13,7 +13,7 @@ * experimental modules and development releases of contributed modules. * See https://www.drupal.org/core/experimental for more information. */ -interface LayoutEntityDisplayInterface extends EntityDisplayInterface, SectionStorageInterface { +interface LayoutEntityDisplayInterface extends EntityDisplayInterface, SectionListInterface { /** * Determines if the display allows custom overrides. diff --git a/core/modules/layout_builder/src/Field/LayoutSectionItemList.php b/core/modules/layout_builder/src/Field/LayoutSectionItemList.php index f67f90eba446fd230b5bb0608d3746d21148b002..a2ceca1373819bc00c91a9d323385c8e5cab1b45 100644 --- a/core/modules/layout_builder/src/Field/LayoutSectionItemList.php +++ b/core/modules/layout_builder/src/Field/LayoutSectionItemList.php @@ -3,13 +3,8 @@ namespace Drupal\layout_builder\Field; use Drupal\Core\Field\FieldItemList; -use Drupal\Core\Plugin\Context\Context; -use Drupal\Core\Plugin\Context\ContextDefinition; -use Drupal\Core\StringTranslation\TranslatableMarkup; -use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; -use Drupal\layout_builder\OverridesSectionStorageInterface; -use Drupal\layout_builder\Section; -use Drupal\layout_builder\SectionStorageInterface; +use Drupal\layout_builder\SectionListInterface; +use Drupal\layout_builder\SectionStorage\SectionStorageTrait; /** * Defines a item list class for layout section fields. @@ -18,35 +13,9 @@ * * @see \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem */ -class LayoutSectionItemList extends FieldItemList implements SectionStorageInterface, OverridesSectionStorageInterface { +class LayoutSectionItemList extends FieldItemList implements SectionListInterface { - /** - * {@inheritdoc} - */ - 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; - - // @todo Use https://www.drupal.org/node/66183 once resolved. - $start = array_slice($this->list, 0, $delta); - $end = array_slice($this->list, $delta); - $this->list = array_merge($start, [$item], $end); - } - else { - $this->appendSection($section); - } - return $this; - } - - /** - * {@inheritdoc} - */ - public function appendSection(Section $section) { - $this->appendItem()->section = $section; - return $this; - } + use SectionStorageTrait; /** * {@inheritdoc} @@ -63,84 +32,18 @@ public function getSections() { /** * {@inheritdoc} */ - public function getSection($delta) { + protected function setSections(array $sections) { + $this->list = []; + $sections = array_values($sections); /** @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())); + foreach ($sections as $section) { + $item = $this->appendItem(); + $item->section = $section; } - return $item->section; - } - - /** - * {@inheritdoc} - */ - public function removeSection($delta) { - $this->removeItem($delta); return $this; } - /** - * {@inheritdoc} - */ - public function getContexts() { - $entity = $this->getEntity(); - // @todo Use EntityContextDefinition after resolving - // https://www.drupal.org/node/2932462. - $contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()])), $entity); - return $contexts; - } - - /** - * {@inheritdoc} - */ - public static function getStorageType() { - return 'overrides'; - } - - /** - * {@inheritdoc} - */ - public function getStorageId() { - $entity = $this->getEntity(); - return $entity->getEntityTypeId() . ':' . $entity->id(); - } - - /** - * {@inheritdoc} - */ - public function label() { - return $this->getEntity()->label(); - } - - /** - * {@inheritdoc} - */ - public function save() { - return $this->getEntity()->save(); - } - - /** - * {@inheritdoc} - */ - public function getCanonicalUrl() { - return $this->getEntity()->toUrl('canonical'); - } - - /** - * {@inheritdoc} - */ - public function getLayoutBuilderUrl() { - return $this->getEntity()->toUrl('layout-builder'); - } - - /** - * {@inheritdoc} - */ - public function getDefaultSectionStorage() { - return LayoutBuilderEntityViewDisplay::collectRenderDisplay($this->getEntity(), 'default'); - } - /** * {@inheritdoc} */ diff --git a/core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php b/core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php index b2eb700d350e36cce96f30bdd52ddf8dad415649..63ec398eec25a73404447aa177a70a01232151a7 100644 --- a/core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php +++ b/core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php @@ -6,6 +6,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\field_ui\Form\EntityViewDisplayEditForm; use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface; +use Drupal\layout_builder\SectionStorageInterface; /** * Edit form for the LayoutBuilderEntityViewDisplay entity type. @@ -24,6 +25,21 @@ class LayoutBuilderEntityViewDisplayForm extends EntityViewDisplayEditForm { */ protected $entity; + /** + * The storage section. + * + * @var \Drupal\layout_builder\SectionStorageInterface + */ + protected $sectionStorage; + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL) { + $this->sectionStorage = $section_storage; + return parent::buildForm($form, $form_state); + } + /** * {@inheritdoc} */ @@ -40,7 +56,7 @@ public function form(array $form, FormStateInterface $form_state) { '#title' => $this->t('Manage layout'), '#weight' => -10, '#attributes' => ['class' => ['button']], - '#url' => $this->entity->getLayoutBuilderUrl(), + '#url' => $this->sectionStorage->getLayoutBuilderUrl(), ]; // @todo Expand to work for all view modes in diff --git a/core/modules/layout_builder/src/OverridesSectionStorageInterface.php b/core/modules/layout_builder/src/OverridesSectionStorageInterface.php index 0d6fd2b05645cb74f7619b4633dd9947b10b26dd..76e15b5a1aef7ca5db67e80ed8b96d8220569c19 100644 --- a/core/modules/layout_builder/src/OverridesSectionStorageInterface.php +++ b/core/modules/layout_builder/src/OverridesSectionStorageInterface.php @@ -10,7 +10,7 @@ * experimental modules and development releases of contributed modules. * See https://www.drupal.org/core/experimental for more information. */ -interface OverridesSectionStorageInterface { +interface OverridesSectionStorageInterface extends SectionStorageInterface { /** * Returns the corresponding defaults section storage for this override. diff --git a/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php b/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php index 94160a7736f5d3b1f31cdcfb0cb0663fde74a850..c2046c0fd407170ab26a48d02c47b8f201bd9960 100644 --- a/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php +++ b/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php @@ -52,59 +52,52 @@ public static function create(ContainerInterface $container, $base_plugin_id) { public function getDerivativeDefinitions($base_plugin_definition) { foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) { // Overrides. - $this->derivatives["entity.$entity_type_id.layout_builder"] = $base_plugin_definition + [ - 'route_name' => "entity.$entity_type_id.layout_builder", + $this->derivatives["layout_builder.overrides.$entity_type_id.view"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.overrides.$entity_type_id.view", 'weight' => 15, 'title' => $this->t('Layout'), 'base_route' => "entity.$entity_type_id.canonical", - 'entity_type_id' => $entity_type_id, 'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id], ]; - $this->derivatives["entity.$entity_type_id.layout_builder_save"] = $base_plugin_definition + [ - 'route_name' => "entity.$entity_type_id.layout_builder_save", + $this->derivatives["layout_builder.overrides.$entity_type_id.save"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.overrides.$entity_type_id.save", 'title' => $this->t('Save Layout'), - 'parent_id' => "layout_builder_ui:entity.$entity_type_id.layout_builder", - 'entity_type_id' => $entity_type_id, + 'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view", 'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id], ]; - $this->derivatives["entity.$entity_type_id.layout_builder_cancel"] = $base_plugin_definition + [ - 'route_name' => "entity.$entity_type_id.layout_builder_cancel", + $this->derivatives["layout_builder.overrides.$entity_type_id.cancel"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.overrides.$entity_type_id.cancel", 'title' => $this->t('Cancel Layout'), - 'parent_id' => "layout_builder_ui:entity.$entity_type_id.layout_builder", - 'entity_type_id' => $entity_type_id, + 'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view", 'weight' => 5, 'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id], ]; // @todo This link should be conditionally displayed, see // https://www.drupal.org/node/2917777. - $this->derivatives["entity.$entity_type_id.layout_builder_revert"] = $base_plugin_definition + [ - 'route_name' => "entity.$entity_type_id.layout_builder_revert", + $this->derivatives["layout_builder.overrides.$entity_type_id.revert"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.overrides.$entity_type_id.revert", 'title' => $this->t('Revert to defaults'), - 'parent_id' => "layout_builder_ui:entity.$entity_type_id.layout_builder", - 'entity_type_id' => $entity_type_id, + 'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view", 'weight' => 10, 'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id], ]; // Defaults. - $this->derivatives["entity.entity_view_display.$entity_type_id.layout_builder"] = $base_plugin_definition + [ - 'route_name' => "entity.entity_view_display.$entity_type_id.layout_builder", + $this->derivatives["layout_builder.defaults.$entity_type_id.view"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.defaults.$entity_type_id.view", 'title' => $this->t('Manage layout'), - 'base_route' => "entity.entity_view_display.$entity_type_id.layout_builder", - 'entity_type_id' => $entity_type_id, + 'base_route' => "layout_builder.defaults.$entity_type_id.view", ]; - $this->derivatives["entity.entity_view_display.$entity_type_id.layout_builder_save"] = $base_plugin_definition + [ - 'route_name' => "entity.entity_view_display.$entity_type_id.layout_builder_save", + $this->derivatives["layout_builder.defaults.$entity_type_id.save"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.defaults.$entity_type_id.save", 'title' => $this->t('Save Layout'), - 'parent_id' => "layout_builder_ui:entity.entity_view_display.$entity_type_id.layout_builder", - 'entity_type_id' => $entity_type_id, + 'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view", ]; - $this->derivatives["entity.entity_view_display.$entity_type_id.layout_builder_cancel"] = $base_plugin_definition + [ - 'route_name' => "entity.entity_view_display.$entity_type_id.layout_builder_cancel", + $this->derivatives["layout_builder.defaults.$entity_type_id.cancel"] = $base_plugin_definition + [ + 'route_name' => "layout_builder.defaults.$entity_type_id.cancel", 'title' => $this->t('Cancel Layout'), 'weight' => 5, - 'parent_id' => "layout_builder_ui:entity.entity_view_display.$entity_type_id.layout_builder", - 'entity_type_id' => $entity_type_id, + 'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view", ]; } diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php new file mode 100644 index 0000000000000000000000000000000000000000..a83ca049f54db6188d835d7215811a1d00d408b5 --- /dev/null +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/DefaultsSectionStorage.php @@ -0,0 +1,295 @@ +<?php + +namespace Drupal\layout_builder\Plugin\SectionStorage; + +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Plugin\Context\Context; +use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\Url; +use Drupal\field_ui\FieldUI; +use Drupal\layout_builder\DefaultsSectionStorageInterface; +use Drupal\layout_builder\Entity\LayoutBuilderSampleEntityGenerator; +use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface; +use Drupal\layout_builder\SectionListInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Routing\RouteCollection; + +/** + * Defines the 'defaults' section storage type. + * + * @SectionStorage( + * id = "defaults", + * ) + * + * @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. + */ +class DefaultsSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, DefaultsSectionStorageInterface { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The entity type bundle info. + * + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface + */ + protected $entityTypeBundleInfo; + + /** + * {@inheritdoc} + * + * @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface + */ + protected $sectionList; + + /** + * The sample entity generator. + * + * @var \Drupal\layout_builder\Entity\LayoutBuilderSampleEntityGenerator + */ + protected $sampleEntityGenerator; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, LayoutBuilderSampleEntityGenerator $sample_entity_generator) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + $this->entityTypeManager = $entity_type_manager; + $this->entityTypeBundleInfo = $entity_type_bundle_info; + $this->sampleEntityGenerator = $sample_entity_generator; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('entity_type.bundle.info'), + $container->get('layout_builder.sample_entity_generator') + ); + } + + /** + * {@inheritdoc} + */ + public function setSectionList(SectionListInterface $section_list) { + if (!$section_list instanceof LayoutEntityDisplayInterface) { + throw new \InvalidArgumentException('Defaults expect a display-based section list'); + } + + return parent::setSectionList($section_list); + } + + /** + * Gets the entity storing the overrides. + * + * @return \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface + * The entity storing the defaults. + */ + protected function getDisplay() { + return $this->getSectionList(); + } + + /** + * {@inheritdoc} + */ + public function getStorageId() { + return $this->getDisplay()->id(); + } + + /** + * {@inheritdoc} + */ + public function getRedirectUrl() { + return Url::fromRoute("entity.entity_view_display.{$this->getDisplay()->getTargetEntityTypeId()}.view_mode", $this->getRouteParameters()); + } + + /** + * {@inheritdoc} + */ + public function getLayoutBuilderUrl() { + return Url::fromRoute("layout_builder.{$this->getStorageType()}.{$this->getDisplay()->getTargetEntityTypeId()}.view", $this->getRouteParameters()); + } + + /** + * Provides the route parameters needed to generate a URL for this object. + * + * @return mixed[] + * An associative array of parameter names and values. + */ + protected function getRouteParameters() { + $display = $this->getDisplay(); + $entity_type = $this->entityTypeManager->getDefinition($display->getTargetEntityTypeId()); + $route_parameters = FieldUI::getRouteBundleParameter($entity_type, $display->getTargetBundle()); + $route_parameters['view_mode_name'] = $display->getMode(); + return $route_parameters; + } + + /** + * {@inheritdoc} + */ + public function buildRoutes(RouteCollection $collection) { + foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) { + // Try to get the route from the current collection. + if (!$entity_route = $collection->get($entity_type->get('field_ui_base_route'))) { + continue; + } + + $path = $entity_route->getPath() . '/display-layout/{view_mode_name}'; + + $defaults = []; + $defaults['entity_type_id'] = $entity_type_id; + // If the entity type has no bundles and it doesn't use {bundle} in its + // admin path, use the entity type. + if (strpos($path, '{bundle}') === FALSE) { + if (!$entity_type->hasKey('bundle')) { + $defaults['bundle'] = $entity_type_id; + } + else { + $defaults['bundle_key'] = $entity_type->getBundleEntityType(); + } + } + + $requirements = []; + $requirements['_field_ui_view_mode_access'] = 'administer ' . $entity_type_id . ' display'; + + $options = $entity_route->getOptions(); + $options['_admin_route'] = FALSE; + + $this->buildLayoutRoutes($collection, $this->getPluginDefinition(), $path, $defaults, $requirements, $options, $entity_type_id); + + $route_names = [ + "entity.entity_view_display.{$entity_type_id}.default", + "entity.entity_view_display.{$entity_type_id}.view_mode", + ]; + foreach ($route_names as $route_name) { + if (!$route = $collection->get($route_name)) { + continue; + } + + $route->addDefaults([ + 'section_storage_type' => $this->getStorageType(), + 'section_storage' => '', + ] + $defaults); + $parameters['section_storage']['layout_builder_tempstore'] = TRUE; + $parameters = NestedArray::mergeDeep($parameters, $route->getOption('parameters') ?: []); + $route->setOption('parameters', $parameters); + } + } + } + + /** + * Returns an array of relevant entity types. + * + * @return \Drupal\Core\Entity\EntityTypeInterface[] + * An array of entity types. + */ + protected function getEntityTypes() { + return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) { + return $entity_type->hasLinkTemplate('layout-builder') && $entity_type->get('field_ui_base_route'); + }); + } + + /** + * {@inheritdoc} + */ + public function extractIdFromRoute($value, $definition, $name, array $defaults) { + if (is_string($value) && strpos($value, '.') !== FALSE) { + return $value; + } + + // If a bundle is not provided but a value corresponding to the bundle key + // is, use that for the bundle value. + if (empty($defaults['bundle']) && isset($defaults['bundle_key']) && !empty($defaults[$defaults['bundle_key']])) { + $defaults['bundle'] = $defaults[$defaults['bundle_key']]; + } + + if (!empty($defaults['entity_type_id']) && !empty($defaults['bundle']) && !empty($defaults['view_mode_name'])) { + return $defaults['entity_type_id'] . '.' . $defaults['bundle'] . '.' . $defaults['view_mode_name']; + } + } + + /** + * {@inheritdoc} + */ + public function getSectionListFromId($id) { + if (strpos($id, '.') === FALSE) { + throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType())); + } + + $storage = $this->entityTypeManager->getStorage('entity_view_display'); + // If the display does not exist, create a new one. + if (!$display = $storage->load($id)) { + list($entity_type_id, $bundle, $view_mode) = explode('.', $id, 3); + $display = $storage->create([ + 'targetEntityType' => $entity_type_id, + 'bundle' => $bundle, + 'mode' => $view_mode, + 'status' => TRUE, + ]); + } + return $display; + } + + /** + * {@inheritdoc} + */ + public function getContexts() { + $display = $this->getDisplay(); + $entity = $this->sampleEntityGenerator->get($display->getTargetEntityTypeId(), $display->getTargetBundle()); + $context_label = new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()]); + + // @todo Use EntityContextDefinition after resolving + // https://www.drupal.org/node/2932462. + $contexts = []; + $contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", $context_label), $entity); + return $contexts; + } + + /** + * {@inheritdoc} + */ + public function label() { + return $this->getDisplay()->label(); + } + + /** + * {@inheritdoc} + */ + public function save() { + return $this->getDisplay()->save(); + } + + /** + * {@inheritdoc} + */ + public function isOverridable() { + return $this->getDisplay()->isOverridable(); + } + + /** + * {@inheritdoc} + */ + public function setOverridable($overridable = TRUE) { + $this->getDisplay()->setOverridable($overridable); + return $this; + } + +} diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php new file mode 100644 index 0000000000000000000000000000000000000000..58adb5503d256dcc41dd4600b6e753295b74ed1d --- /dev/null +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php @@ -0,0 +1,234 @@ +<?php + +namespace Drupal\layout_builder\Plugin\SectionStorage; + +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Plugin\Context\Context; +use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\Url; +use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; +use Drupal\layout_builder\OverridesSectionStorageInterface; +use Drupal\layout_builder\SectionListInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Routing\RouteCollection; + +/** + * Defines the 'overrides' section storage type. + * + * @SectionStorage( + * id = "overrides", + * ) + * + * @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. + */ +class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, OverridesSectionStorageInterface { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The entity field manager. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface + */ + protected $entityFieldManager; + + /** + * {@inheritdoc} + * + * @var \Drupal\layout_builder\SectionListInterface|\Drupal\Core\Field\FieldItemListInterface + */ + protected $sectionList; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + $this->entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('entity_field.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function setSectionList(SectionListInterface $section_list) { + if (!$section_list instanceof FieldItemListInterface) { + throw new \InvalidArgumentException('Overrides expect a field-based section list'); + } + + return parent::setSectionList($section_list); + } + + /** + * Gets the entity storing the overrides. + * + * @return \Drupal\Core\Entity\FieldableEntityInterface + * The entity storing the overrides. + */ + protected function getEntity() { + return $this->getSectionList()->getEntity(); + } + + /** + * {@inheritdoc} + */ + public function getStorageId() { + $entity = $this->getEntity(); + return $entity->getEntityTypeId() . '.' . $entity->id(); + } + + /** + * {@inheritdoc} + */ + public function extractIdFromRoute($value, $definition, $name, array $defaults) { + if (strpos($value, '.') !== FALSE) { + return $value; + } + + if (isset($defaults['entity_type_id']) && !empty($defaults[$defaults['entity_type_id']])) { + $entity_type_id = $defaults['entity_type_id']; + $entity_id = $defaults[$entity_type_id]; + return $entity_type_id . '.' . $entity_id; + } + } + + /** + * {@inheritdoc} + */ + public function getSectionListFromId($id) { + if (strpos($id, '.') !== FALSE) { + list($entity_type_id, $entity_id) = explode('.', $id, 2); + $entity = $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id); + if ($entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout')) { + return $entity->get('layout_builder__layout'); + } + } + throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType())); + } + + /** + * {@inheritdoc} + */ + public function buildRoutes(RouteCollection $collection) { + foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) { + $defaults = []; + $defaults['entity_type_id'] = $entity_type_id; + + $requirements = []; + if ($this->hasIntegerId($entity_type)) { + $requirements[$entity_type_id] = '\d+'; + } + + $options = []; + // Ensure that upcasting is run in the correct order. + $options['parameters']['section_storage'] = []; + $options['parameters'][$entity_type_id]['type'] = 'entity:' . $entity_type_id; + + $template = $entity_type->getLinkTemplate('layout-builder'); + $this->buildLayoutRoutes($collection, $this->getPluginDefinition(), $template, $defaults, $requirements, $options, $entity_type_id); + } + } + + /** + * Determines if this entity type's ID is stored as an integer. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * An entity type. + * + * @return bool + * TRUE if this entity type's ID key is always an integer, FALSE otherwise. + */ + protected function hasIntegerId(EntityTypeInterface $entity_type) { + $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id()); + return $field_storage_definitions[$entity_type->getKey('id')]->getType() === 'integer'; + } + + /** + * Returns an array of relevant entity types. + * + * @return \Drupal\Core\Entity\EntityTypeInterface[] + * An array of entity types. + */ + protected function getEntityTypes() { + return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) { + return $entity_type->hasLinkTemplate('layout-builder'); + }); + } + + /** + * {@inheritdoc} + */ + public function getDefaultSectionStorage() { + return LayoutBuilderEntityViewDisplay::collectRenderDisplay($this->getEntity(), 'default'); + } + + /** + * {@inheritdoc} + */ + public function getRedirectUrl() { + return $this->getEntity()->toUrl('canonical'); + } + + /** + * {@inheritdoc} + */ + public function getLayoutBuilderUrl() { + $entity = $this->getEntity(); + $route_parameters[$entity->getEntityTypeId()] = $entity->id(); + return Url::fromRoute("layout_builder.{$this->getStorageType()}.{$this->getEntity()->getEntityTypeId()}.view", $route_parameters); + } + + /** + * {@inheritdoc} + */ + public function getContexts() { + $entity = $this->getEntity(); + // @todo Use EntityContextDefinition after resolving + // https://www.drupal.org/node/2932462. + $contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()])), $entity); + return $contexts; + } + + /** + * {@inheritdoc} + */ + public function label() { + return $this->getEntity()->label(); + } + + /** + * {@inheritdoc} + */ + public function save() { + return $this->getEntity()->save(); + } + +} diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php b/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php new file mode 100644 index 0000000000000000000000000000000000000000..c419060c45b8ac068a63ae13f5cc118a42a2302d --- /dev/null +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/SectionStorageBase.php @@ -0,0 +1,106 @@ +<?php + +namespace Drupal\layout_builder\Plugin\SectionStorage; + +use Drupal\Core\Plugin\PluginBase; +use Drupal\layout_builder\Routing\LayoutBuilderRoutesTrait; +use Drupal\layout_builder\Section; +use Drupal\layout_builder\SectionListInterface; +use Drupal\layout_builder\SectionStorageInterface; + +/** + * Provides a base class for Section Storage types. + * + * @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. + */ +abstract class SectionStorageBase extends PluginBase implements SectionStorageInterface { + + use LayoutBuilderRoutesTrait; + + /** + * The section storage instance. + * + * @var \Drupal\layout_builder\SectionListInterface|null + */ + protected $sectionList; + + /** + * {@inheritdoc} + */ + public function setSectionList(SectionListInterface $section_list) { + $this->sectionList = $section_list; + return $this; + } + + /** + * Gets the section list. + * + * @return \Drupal\layout_builder\SectionListInterface + * The section list. + * + * @throws \RuntimeException + * Thrown if ::setSectionList() is not called first. + */ + protected function getSectionList() { + if (!$this->sectionList) { + throw new \RuntimeException(sprintf('%s::setSectionList() must be called first', static::class)); + } + return $this->sectionList; + } + + /** + * {@inheritdoc} + */ + public function getStorageType() { + return $this->getPluginId(); + } + + /** + * {@inheritdoc} + */ + public function count() { + return $this->getSectionList()->count(); + } + + /** + * {@inheritdoc} + */ + public function getSections() { + return $this->getSectionList()->getSections(); + } + + /** + * {@inheritdoc} + */ + public function getSection($delta) { + return $this->getSectionList()->getSection($delta); + } + + /** + * {@inheritdoc} + */ + public function appendSection(Section $section) { + $this->getSectionList()->appendSection($section); + return $this; + } + + /** + * {@inheritdoc} + */ + public function insertSection($delta, Section $section) { + $this->getSectionList()->insertSection($delta, $section); + return $this; + } + + /** + * {@inheritdoc} + */ + public function removeSection($delta) { + $this->getSectionList()->removeSection($delta); + return $this; + } + +} diff --git a/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php index 500a380a11bed717b2a7be6f30fc2ccbe1c03b0a..1046579458e0ff14bf10b552d6bb9e4feeded769 100644 --- a/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php +++ b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php @@ -2,14 +2,9 @@ namespace Drupal\layout_builder\Routing; -use Drupal\Component\Utility\NestedArray; -use Drupal\Core\Entity\EntityFieldManagerInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Routing\RouteBuildEvent; use Drupal\Core\Routing\RoutingEvents; -use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; -use Drupal\layout_builder\Field\LayoutSectionItemList; +use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -19,61 +14,21 @@ */ class LayoutBuilderRoutes implements EventSubscriberInterface { - use LayoutBuilderRoutesTrait; - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - /** - * The entity field manager. + * The section storage manager. * - * @var \Drupal\Core\Entity\EntityFieldManagerInterface + * @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface */ - protected $entityFieldManager; + protected $sectionStorageManager; /** * Constructs a new LayoutBuilderRoutes. * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager - * The entity field manager. + * @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager + * The section storage manager. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager) { - $this->entityTypeManager = $entity_type_manager; - $this->entityFieldManager = $entity_field_manager; - } - - /** - * Generates layout builder routes. - * - * @return \Symfony\Component\Routing\Route[] - * An array of route objects. - */ - public function getRoutes() { - $routes = []; - - foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) { - $defaults = []; - $defaults['entity_type_id'] = $entity_type_id; - - $requirements = []; - if ($this->hasIntegerId($entity_type)) { - $requirements[$entity_type_id] = '\d+'; - } - - $options = []; - $options['parameters']['section_storage']['layout_builder_tempstore'] = TRUE; - $options['parameters'][$entity_type_id]['type'] = 'entity:' . $entity_type_id; - - $template = $entity_type->getLinkTemplate('layout-builder'); - $routes += $this->buildRoute(LayoutSectionItemList::class, 'entity.' . $entity_type_id, $template, $defaults, $requirements, $options); - } - return $routes; + public function __construct(SectionStorageManagerInterface $section_storage_manager) { + $this->sectionStorageManager = $section_storage_manager; } /** @@ -84,70 +39,11 @@ public function getRoutes() { */ public function onAlterRoutes(RouteBuildEvent $event) { $collection = $event->getRouteCollection(); - foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) { - if ($route_name = $entity_type->get('field_ui_base_route')) { - // Try to get the route from the current collection. - if (!$entity_route = $collection->get($route_name)) { - continue; - } - $path = $entity_route->getPath() . '/display-layout/{view_mode_name}'; - - $defaults = []; - $defaults['entity_type_id'] = $entity_type_id; - // If the entity type has no bundles and it doesn't use {bundle} in its - // admin path, use the entity type. - if (strpos($path, '{bundle}') === FALSE) { - if (!$entity_type->hasKey('bundle')) { - $defaults['bundle'] = $entity_type_id; - } - else { - $defaults['bundle_key'] = $entity_type->getBundleEntityType(); - } - } - - $requirements = []; - $requirements['_field_ui_view_mode_access'] = 'administer ' . $entity_type_id . ' display'; - - $options['parameters']['section_storage']['layout_builder_tempstore'] = TRUE; - // Merge the entity route options in after Layout Builder's. - $options = NestedArray::mergeDeep($options, $entity_route->getOptions()); - // Disable the admin route flag after merging in entity route options. - $options['_admin_route'] = FALSE; - - $routes = $this->buildRoute(LayoutBuilderEntityViewDisplay::class, 'entity.entity_view_display.' . $entity_type_id, $path, $defaults, $requirements, $options); - foreach ($routes as $name => $route) { - $collection->add($name, $route); - } - } + foreach ($this->sectionStorageManager->getDefinitions() as $plugin_id => $definition) { + $this->sectionStorageManager->loadEmpty($plugin_id)->buildRoutes($collection); } } - /** - * Determines if this entity type's ID is stored as an integer. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * An entity type. - * - * @return bool - * TRUE if this entity type's ID key is always an integer, FALSE otherwise. - */ - protected function hasIntegerId(EntityTypeInterface $entity_type) { - $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id()); - return $field_storage_definitions[$entity_type->getKey('id')]->getType() === 'integer'; - } - - /** - * Returns an array of relevant entity types. - * - * @return \Drupal\Core\Entity\EntityTypeInterface[] - * An array of entity types. - */ - protected function getEntityTypes() { - return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) { - return $entity_type->hasLinkTemplate('layout-builder'); - }); - } - /** * {@inheritdoc} */ diff --git a/core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php index da9e1c1759f15ee632978856c97543778c2f0047..febedbe31efdc98b95e84a4565d0d06f7726c52a 100644 --- a/core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php +++ b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php @@ -2,9 +2,11 @@ namespace Drupal\layout_builder\Routing; +use Drupal\Component\Utility\NestedArray; use Drupal\layout_builder\OverridesSectionStorageInterface; -use Drupal\layout_builder\SectionStorageInterface; +use Drupal\layout_builder\SectionStorage\SectionStorageDefinition; use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; /** * Provides a trait for building routes for a Layout Builder UI. @@ -19,36 +21,41 @@ trait LayoutBuilderRoutesTrait { /** * Builds the layout routes for the given values. * - * @param string $class - * The class defining the section storage. - * @param string $route_name_prefix - * The prefix to use for the route name. + * @param \Symfony\Component\Routing\RouteCollection $collection + * The route collection. + * @param \Drupal\layout_builder\SectionStorage\SectionStorageDefinition $definition + * The definition of the section storage. * @param string $path * The path patten for the routes. * @param array $defaults - * An array of default parameter values. + * (optional) An array of default parameter values. * @param array $requirements - * An array of requirements for parameters. + * (optional) An array of requirements for parameters. * @param array $options - * An array of options. - * - * @return \Symfony\Component\Routing\Route[] - * An array of route objects. + * (optional) An array of options. + * @param string $route_name_prefix + * (optional) The prefix to use for the route name. */ - protected function buildRoute($class, $route_name_prefix, $path, array $defaults, array $requirements, array $options) { - $routes = []; - - if (!is_subclass_of($class, SectionStorageInterface::class)) { - return $routes; - } - - $defaults['section_storage_type'] = $class::getStorageType(); + protected function buildLayoutRoutes(RouteCollection $collection, SectionStorageDefinition $definition, $path, array $defaults = [], array $requirements = [], array $options = [], $route_name_prefix = '') { + $type = $definition->id(); + $defaults['section_storage_type'] = $type; // Provide an empty value to allow the section storage to be upcast. $defaults['section_storage'] = ''; // Trigger the layout builder access check. $requirements['_has_layout_section'] = 'true'; // Trigger the layout builder RouteEnhancer. $options['_layout_builder'] = TRUE; + // Trigger the layout builder param converter. + $parameters['section_storage']['layout_builder_tempstore'] = TRUE; + // Merge the passed in options in after Layout Builder's parameters. + $options = NestedArray::mergeDeep(['parameters' => $parameters], $options); + + if ($route_name_prefix) { + $route_name_prefix = "layout_builder.$type.$route_name_prefix"; + } + else { + $route_name_prefix = "layout_builder.$type"; + } $main_defaults = $defaults; $main_defaults['is_rebuilding'] = FALSE; @@ -58,7 +65,7 @@ protected function buildRoute($class, $route_name_prefix, $path, array $defaults ->setDefaults($main_defaults) ->setRequirements($requirements) ->setOptions($options); - $routes["{$route_name_prefix}.layout_builder"] = $route; + $collection->add("$route_name_prefix.view", $route); $save_defaults = $defaults; $save_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout'; @@ -66,7 +73,7 @@ protected function buildRoute($class, $route_name_prefix, $path, array $defaults ->setDefaults($save_defaults) ->setRequirements($requirements) ->setOptions($options); - $routes["{$route_name_prefix}.layout_builder_save"] = $route; + $collection->add("$route_name_prefix.save", $route); $cancel_defaults = $defaults; $cancel_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout'; @@ -74,19 +81,17 @@ protected function buildRoute($class, $route_name_prefix, $path, array $defaults ->setDefaults($cancel_defaults) ->setRequirements($requirements) ->setOptions($options); - $routes["{$route_name_prefix}.layout_builder_cancel"] = $route; + $collection->add("$route_name_prefix.cancel", $route); - if (is_subclass_of($class, OverridesSectionStorageInterface::class)) { + if (is_subclass_of($definition->getClass(), OverridesSectionStorageInterface::class)) { $revert_defaults = $defaults; $revert_defaults['_form'] = '\Drupal\layout_builder\Form\RevertOverridesForm'; $route = (new Route("$path/revert")) ->setDefaults($revert_defaults) ->setRequirements($requirements) ->setOptions($options); - $routes["{$route_name_prefix}.layout_builder_revert"] = $route; + $collection->add("$route_name_prefix.revert", $route); } - - return $routes; } } diff --git a/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php b/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php index 228a786764d6c902d77d077d1114315b74022dc2..263b767f729871845c3d36c120752cf520e97e97 100644 --- a/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php +++ b/core/modules/layout_builder/src/Routing/LayoutTempstoreParamConverter.php @@ -2,9 +2,9 @@ namespace Drupal\layout_builder\Routing; -use Drupal\Core\DependencyInjection\ClassResolverInterface; use Drupal\Core\ParamConverter\ParamConverterInterface; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; +use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; use Symfony\Component\Routing\Route; /** @@ -22,59 +22,33 @@ class LayoutTempstoreParamConverter implements ParamConverterInterface { protected $layoutTempstoreRepository; /** - * The class resolver. + * The section storage manager. * - * @var \Drupal\Core\DependencyInjection\ClassResolverInterface + * @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface */ - protected $classResolver; + protected $sectionStorageManager; /** * Constructs a new LayoutTempstoreParamConverter. * * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository * The layout tempstore repository. - * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver - * The class resolver. + * @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager + * The section storage manager. */ - public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, ClassResolverInterface $class_resolver) { + public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, SectionStorageManagerInterface $section_storage_manager) { $this->layoutTempstoreRepository = $layout_tempstore_repository; - $this->classResolver = $class_resolver; + $this->sectionStorageManager = $section_storage_manager; } /** * {@inheritdoc} */ public function convert($value, $definition, $name, array $defaults) { - if ($converter = $this->getParamConverterFromDefaults($defaults)) { - if ($object = $converter->convert($value, $definition, $name, $defaults)) { - // Pass the result of the storage param converter through the - // tempstore repository. - return $this->layoutTempstoreRepository->get($object); - } - } - } - - /** - * Gets a param converter based on the provided defaults. - * - * @param array $defaults - * The route defaults array. - * - * @return \Drupal\layout_builder\Routing\SectionStorageParamConverterInterface|null - * A section storage param converter if found, NULL otherwise. - */ - protected function getParamConverterFromDefaults(array $defaults) { - // If a storage type was specified, get the corresponding param converter. - if (isset($defaults['section_storage_type'])) { - try { - $converter = $this->classResolver->getInstanceFromDefinition('layout_builder.section_storage_param_converter.' . $defaults['section_storage_type']); - } - catch (\InvalidArgumentException $e) { - $converter = NULL; - } - - if ($converter instanceof SectionStorageParamConverterInterface) { - return $converter; + if (isset($defaults['section_storage_type']) && $this->sectionStorageManager->hasDefinition($defaults['section_storage_type'])) { + if ($section_storage = $this->sectionStorageManager->loadFromRoute($defaults['section_storage_type'], $value, $definition, $name, $defaults)) { + // Pass the plugin through the tempstore repository. + return $this->layoutTempstoreRepository->get($section_storage); } } } diff --git a/core/modules/layout_builder/src/Routing/SectionStorageDefaultsParamConverter.php b/core/modules/layout_builder/src/Routing/SectionStorageDefaultsParamConverter.php deleted file mode 100644 index 2f886102c3e7f8786530ca032ce930711789378d..0000000000000000000000000000000000000000 --- a/core/modules/layout_builder/src/Routing/SectionStorageDefaultsParamConverter.php +++ /dev/null @@ -1,53 +0,0 @@ -<?php - -namespace Drupal\layout_builder\Routing; - -use Drupal\Core\ParamConverter\EntityConverter; - -/** - * Provides a param converter for defaults-based section storage. - * - * @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. - */ -class SectionStorageDefaultsParamConverter extends EntityConverter implements SectionStorageParamConverterInterface { - - /** - * {@inheritdoc} - */ - public function convert($value, $definition, $name, array $defaults) { - if (!$value) { - // If a bundle is not provided but a value corresponding to the bundle key - // is, use that for the bundle value. - if (empty($defaults['bundle']) && isset($defaults['bundle_key']) && !empty($defaults[$defaults['bundle_key']])) { - $defaults['bundle'] = $defaults[$defaults['bundle_key']]; - } - - if (empty($defaults['entity_type_id']) && empty($defaults['bundle']) && empty($defaults['view_mode_name'])) { - return NULL; - } - - $value = $defaults['entity_type_id'] . '.' . $defaults['bundle'] . '.' . $defaults['view_mode_name']; - } - if (!$display = parent::convert($value, $definition, $name, $defaults)) { - list($entity_type_id, $bundle, $view_mode) = explode('.', $value); - $display = $this->entityManager->getStorage('entity_view_display')->create([ - 'targetEntityType' => $entity_type_id, - 'bundle' => $bundle, - 'mode' => $view_mode, - 'status' => TRUE, - ]); - } - return $display; - } - - /** - * {@inheritdoc} - */ - protected function getEntityTypeFromDefaults($definition, $name, array $defaults) { - return 'entity_view_display'; - } - -} diff --git a/core/modules/layout_builder/src/Routing/SectionStorageOverridesParamConverter.php b/core/modules/layout_builder/src/Routing/SectionStorageOverridesParamConverter.php deleted file mode 100644 index 8d8ae58059fba94c7b16f16bd3f091a7bd2d8939..0000000000000000000000000000000000000000 --- a/core/modules/layout_builder/src/Routing/SectionStorageOverridesParamConverter.php +++ /dev/null @@ -1,70 +0,0 @@ -<?php - -namespace Drupal\layout_builder\Routing; - -use Drupal\Core\Entity\FieldableEntityInterface; -use Drupal\Core\ParamConverter\EntityConverter; - -/** - * Provides a param converter for overrides-based section storage. - */ -class SectionStorageOverridesParamConverter extends EntityConverter implements SectionStorageParamConverterInterface { - - /** - * {@inheritdoc} - */ - public function convert($value, $definition, $name, array $defaults) { - $entity_id = $this->getEntityIdFromDefaults($value, $defaults); - $entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults); - if (!$entity_id || !$entity_type_id) { - return NULL; - } - - $entity = parent::convert($entity_id, $definition, $name, $defaults); - if ($entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout')) { - return $entity->get('layout_builder__layout'); - } - } - - /** - * Determines the entity ID given a parameter value and route defaults. - * - * @param string $value - * The parameter value. - * @param array $defaults - * The route defaults array. - * - * @return string|null - * The entity ID if it exists, NULL otherwise. - */ - protected function getEntityIdFromDefaults($value, array $defaults) { - $entity_id = NULL; - // Layout Builder routes will have this parameter in the form of - // 'entity_type_id:entity_id'. - if (strpos($value, ':') !== FALSE) { - list(, $entity_id) = explode(':', $value); - } - // Overridden routes have the entity ID available in the defaults. - elseif (isset($defaults['entity_type_id']) && !empty($defaults[$defaults['entity_type_id']])) { - $entity_id = $defaults[$defaults['entity_type_id']]; - } - return $entity_id; - } - - /** - * {@inheritdoc} - */ - protected function getEntityTypeFromDefaults($definition, $name, array $defaults) { - // Layout Builder routes will have this parameter in the form of - // 'entity_type_id:entity_id'. - if (isset($defaults[$name]) && strpos($defaults[$name], ':') !== FALSE) { - list($entity_type_id) = explode(':', $defaults[$name], 2); - return $entity_type_id; - } - // Overridden routes have the entity type ID available in the defaults. - elseif (isset($defaults['entity_type_id'])) { - return $defaults['entity_type_id']; - } - } - -} diff --git a/core/modules/layout_builder/src/Routing/SectionStorageParamConverterInterface.php b/core/modules/layout_builder/src/Routing/SectionStorageParamConverterInterface.php deleted file mode 100644 index 955b6735132205d68f1a7b416cacc54e0c208f4a..0000000000000000000000000000000000000000 --- a/core/modules/layout_builder/src/Routing/SectionStorageParamConverterInterface.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php - -namespace Drupal\layout_builder\Routing; - -/** - * Defines the interface of a param converter for section storage. - * - * A service implementing this interface must have a service ID prefixed with - * 'layout_builder.section_storage_param_converter.', followed by the section - * storage type. - * - * @see \Drupal\Core\ParamConverter\ParamConverterInterface - * @see \Drupal\layout_builder\SectionStorageInterface::getStorageType() - */ -interface SectionStorageParamConverterInterface { - - /** - * Converts path variables to their corresponding objects. - * - * @param mixed $value - * The raw value. - * @param mixed $definition - * The parameter definition provided in the route options. - * @param string $name - * The name of the parameter. - * @param array $defaults - * The route defaults array. - * - * @return \Drupal\layout_builder\SectionStorageInterface|null - * The section storage if it could be loaded, or NULL otherwise. - */ - public function convert($value, $definition, $name, array $defaults); - -} diff --git a/core/modules/layout_builder/src/SectionListInterface.php b/core/modules/layout_builder/src/SectionListInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..8df586a7aae6ec0f7becd8846234e16f582c3b15 --- /dev/null +++ b/core/modules/layout_builder/src/SectionListInterface.php @@ -0,0 +1,74 @@ +<?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 SectionListInterface extends \Countable { + + /** + * Gets the layout sections. + * + * @return \Drupal\layout_builder\Section[] + * A sequentially and numerically keyed array of section objects. + */ + 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. + * + * As sections are stored sequentially and numerically this will re-key every + * subsequent section, shifting them forward. + * + * @param int $delta + * The delta of the section. + * + * @return $this + */ + public function removeSection($delta); + +} diff --git a/core/modules/layout_builder/src/SectionStorage/SectionStorageDefinition.php b/core/modules/layout_builder/src/SectionStorage/SectionStorageDefinition.php new file mode 100644 index 0000000000000000000000000000000000000000..61b975a471825d53af63e6804d6696bf578f905b --- /dev/null +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageDefinition.php @@ -0,0 +1,75 @@ +<?php + +namespace Drupal\layout_builder\SectionStorage; + +use Drupal\Component\Plugin\Definition\PluginDefinition; + +/** + * Provides section storage type plugin definition. + * + * @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. + */ +class SectionStorageDefinition extends PluginDefinition { + + /** + * Any additional properties and values. + * + * @var array + */ + protected $additional = []; + + /** + * LayoutDefinition constructor. + * + * @param array $definition + * An array of values from the annotation. + */ + public function __construct(array $definition = []) { + foreach ($definition as $property => $value) { + $this->set($property, $value); + } + } + + /** + * Gets any arbitrary property. + * + * @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. + * + * @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; + } + +} diff --git a/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php new file mode 100644 index 0000000000000000000000000000000000000000..18147cd1d8157ead6499c8d20c74cb4a387e57b4 --- /dev/null +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManager.php @@ -0,0 +1,71 @@ +<?php + +namespace Drupal\layout_builder\SectionStorage; + +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\layout_builder\Annotation\SectionStorage; +use Drupal\layout_builder\SectionStorageInterface; + +/** + * Provides the Section Storage type plugin manager. + * + * @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. + */ +class SectionStorageManager extends DefaultPluginManager implements SectionStorageManagerInterface { + + /** + * Constructs a new SectionStorageManager object. + * + * @param \Traversable $namespaces + * An object that implements \Traversable which contains the root paths + * keyed by the corresponding namespace to look for plugin implementations. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * Cache backend instance to use. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler to invoke the alter hook with. + */ + public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { + parent::__construct('Plugin/SectionStorage', $namespaces, $module_handler, SectionStorageInterface::class, SectionStorage::class); + + $this->alterInfo('layout_builder_section_storage'); + $this->setCacheBackend($cache_backend, 'layout_builder_section_storage_plugins'); + } + + /** + * {@inheritdoc} + */ + public function loadEmpty($id) { + return $this->createInstance($id); + } + + /** + * {@inheritdoc} + */ + public function loadFromStorageId($type, $id) { + /** @var \Drupal\layout_builder\SectionStorageInterface $plugin */ + $plugin = $this->createInstance($type); + return $plugin->setSectionList($plugin->getSectionListFromId($id)); + } + + /** + * {@inheritdoc} + */ + public function loadFromRoute($type, $value, $definition, $name, array $defaults) { + /** @var \Drupal\layout_builder\SectionStorageInterface $plugin */ + $plugin = $this->createInstance($type); + if ($id = $plugin->extractIdFromRoute($value, $definition, $name, $defaults)) { + try { + return $plugin->setSectionList($plugin->getSectionListFromId($id)); + } + catch (\InvalidArgumentException $e) { + // Intentionally empty. + } + } + } + +} diff --git a/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..3b269fcbba839240b4998401a7dcefc999c65434 --- /dev/null +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageManagerInterface.php @@ -0,0 +1,65 @@ +<?php + +namespace Drupal\layout_builder\SectionStorage; + +use Drupal\Component\Plugin\Discovery\DiscoveryInterface; + +/** + * Provides the interface for a plugin manager of section storage types. + * + * @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. + */ +interface SectionStorageManagerInterface extends DiscoveryInterface { + + /** + * Loads a section storage with no associated section list. + * + * @param string $id + * The ID of the section storage being instantiated. + * + * @return \Drupal\layout_builder\SectionStorageInterface + * The section storage. + */ + public function loadEmpty($id); + + /** + * Loads a section storage populated with an existing section list. + * + * @param string $type + * The section storage type. + * @param string $id + * The section list ID. + * + * @return \Drupal\layout_builder\SectionStorageInterface + * The section storage. + * + * @throws \InvalidArgumentException + * Thrown if the ID is invalid. + */ + public function loadFromStorageId($type, $id); + + /** + * Loads a section storage populated with a section list derived from a route. + * + * @param string $type + * The section storage type. + * @param string $value + * The raw value. + * @param mixed $definition + * The parameter definition provided in the route options. + * @param string $name + * The name of the parameter. + * @param array $defaults + * The route defaults array. + * + * @return \Drupal\layout_builder\SectionStorageInterface|null + * The section storage if it could be loaded, or NULL otherwise. + * + * @see \Drupal\Core\ParamConverter\ParamConverterInterface::convert() + */ + public function loadFromRoute($type, $value, $definition, $name, array $defaults); + +} diff --git a/core/modules/layout_builder/src/SectionStorage/SectionStorageTrait.php b/core/modules/layout_builder/src/SectionStorage/SectionStorageTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..9d942c7ad85982898e0480f3f7cac4d075b2f83e --- /dev/null +++ b/core/modules/layout_builder/src/SectionStorage/SectionStorageTrait.php @@ -0,0 +1,114 @@ +<?php + +namespace Drupal\layout_builder\SectionStorage; + +use Drupal\layout_builder\Section; + +/** + * Provides a trait for storing sections on an object. + * + * @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. + */ +trait SectionStorageTrait { + + /** + * Stores the information for all sections. + * + * Implementations of this method are expected to call array_values() to rekey + * the list of sections. + * + * @param \Drupal\layout_builder\Section[] $sections + * An array of section objects. + * + * @return $this + */ + abstract protected function setSections(array $sections); + + /** + * {@inheritdoc} + */ + public function count() { + return count($this->getSections()); + } + + /** + * {@inheritdoc} + */ + public function getSection($delta) { + if (!$this->hasSection($delta)) { + throw new \OutOfBoundsException(sprintf('Invalid delta "%s"', $delta)); + } + + return $this->getSections()[$delta]; + } + + /** + * Sets the section for the given delta on the display. + * + * @param int $delta + * The delta of the section. + * @param \Drupal\layout_builder\Section $section + * The layout section. + * + * @return $this + */ + protected function setSection($delta, Section $section) { + $sections = $this->getSections(); + $sections[$delta] = $section; + $this->setSections($sections); + return $this; + } + + /** + * {@inheritdoc} + */ + public function appendSection(Section $section) { + $delta = $this->count(); + + $this->setSection($delta, $section); + return $this; + } + + /** + * {@inheritdoc} + */ + public function insertSection($delta, Section $section) { + if ($this->hasSection($delta)) { + // @todo Use https://www.drupal.org/node/66183 once resolved. + $start = array_slice($this->getSections(), 0, $delta); + $end = array_slice($this->getSections(), $delta); + $this->setSections(array_merge($start, [$section], $end)); + } + else { + $this->appendSection($section); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function removeSection($delta) { + $sections = $this->getSections(); + unset($sections[$delta]); + $this->setSections($sections); + return $this; + } + + /** + * Indicates if there is a section at the specified delta. + * + * @param int $delta + * The delta of the section. + * + * @return bool + * TRUE if there is a section for this delta, FALSE otherwise. + */ + protected function hasSection($delta) { + return isset($this->getSections()[$delta]); + } + +} diff --git a/core/modules/layout_builder/src/SectionStorageInterface.php b/core/modules/layout_builder/src/SectionStorageInterface.php index cc1efaba2a62586a318e804c7184f8e38095d5aa..e7fbb086b89da585cfde215c14e84d7ef468491c 100644 --- a/core/modules/layout_builder/src/SectionStorageInterface.php +++ b/core/modules/layout_builder/src/SectionStorageInterface.php @@ -2,100 +2,124 @@ namespace Drupal\layout_builder; +use Drupal\Component\Plugin\PluginInspectionInterface; +use Symfony\Component\Routing\RouteCollection; + /** - * Defines the interface for an object that stores layout sections. + * Defines an interface for Section Storage type plugins. * * @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 { +interface SectionStorageInterface extends SectionListInterface, PluginInspectionInterface { /** - * Gets the layout sections. + * Returns an identifier for this storage. * - * @return \Drupal\layout_builder\Section[] - * A sequentially and numerically keyed array of section objects. + * @return string + * The unique identifier for this storage. */ - public function getSections(); + public function getStorageId(); /** - * Gets a domain object for the layout section. + * Returns the type of this storage. * - * @param int $delta - * The delta of the section. + * Used in conjunction with the storage ID. * - * @return \Drupal\layout_builder\Section - * The layout section. + * @return string + * The type of storage. */ - public function getSection($delta); + public function getStorageType(); /** - * Appends a new section to the end of the list. + * Sets the section list on the storage. * - * @param \Drupal\layout_builder\Section $section - * The section to append. + * @param \Drupal\layout_builder\SectionListInterface $section_list + * The section list. * * @return $this + * + * @internal + * This should only be called during section storage instantiation. */ - public function appendSection(Section $section); + public function setSectionList(SectionListInterface $section_list); /** - * Inserts a new section at a given delta. + * Derives the section list from the storage ID. * - * If a section exists at the given index, the section at that position and - * others after it are shifted backward. + * @param string $id + * The storage ID, see ::getStorageId(). * - * @param int $delta - * The delta of the section. - * @param \Drupal\layout_builder\Section $section - * The section to insert. + * @return \Drupal\layout_builder\SectionListInterface + * The section list. * - * @return $this + * @throws \InvalidArgumentException + * Thrown if the ID is invalid. + * + * @internal + * This should only be called during section storage instantiation. */ - public function insertSection($delta, Section $section); + public function getSectionListFromId($id); /** - * Removes the section at the given delta. + * Provides the routes needed for Layout Builder UI. * - * As sections are stored sequentially and numerically this will re-key every - * subsequent section, shifting them forward. + * Allows the plugin to add or alter routes during the route building process. + * \Drupal\layout_builder\Routing\LayoutBuilderRoutesTrait is provided for the + * typical use case of building a standard Layout Builder UI. * - * @param int $delta - * The delta of the section. + * @param \Symfony\Component\Routing\RouteCollection $collection + * The route collection. * - * @return $this + * @see \Drupal\Core\Routing\RoutingEvents::ALTER */ - public function removeSection($delta); + public function buildRoutes(RouteCollection $collection); /** - * Provides any available contexts for the object using the sections. + * Gets the URL used when redirecting away from the Layout Builder UI. * - * @return \Drupal\Core\Plugin\Context\ContextInterface[] - * The array of context objects. + * @return \Drupal\Core\Url + * The URL object. */ - public function getContexts(); + public function getRedirectUrl(); /** - * Returns an identifier for this storage. + * Gets the URL used to display the Layout Builder UI. * - * @return string - * The unique identifier for this storage. + * @return \Drupal\Core\Url + * The URL object. */ - public function getStorageId(); + public function getLayoutBuilderUrl(); /** - * Returns the type of this storage. - * - * Used in conjunction with the storage ID. + * Configures the plugin based on route values. + * + * @param mixed $value + * The raw value. + * @param mixed $definition + * The parameter definition provided in the route options. + * @param string $name + * The name of the parameter. + * @param array $defaults + * The route defaults array. + * + * @return string|null + * The section storage ID if it could be extracted, NULL otherwise. + * + * @internal + * This should only be called during section storage instantiation. + */ + public function extractIdFromRoute($value, $definition, $name, array $defaults); + + /** + * Provides any available contexts for the object using the sections. * - * @return string - * The type of storage. + * @return \Drupal\Core\Plugin\Context\ContextInterface[] + * The array of context objects. */ - public static function getStorageType(); + public function getContexts(); /** * Gets the label for the object using the sections. @@ -114,20 +138,4 @@ public function label(); */ public function save(); - /** - * Returns a URL for viewing the object using the sections. - * - * @return \Drupal\Core\Url - * The URL object. - */ - public function getCanonicalUrl(); - - /** - * Returns a URL to edit the sections in the Layout Builder UI. - * - * @return \Drupal\Core\Url - * The URL object. - */ - public function getLayoutBuilderUrl(); - } diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php index e0dfdc7f429d16b99c74b63994099715da2769d0..0437c1ca8538b198746a989ba22040d1763fde30 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php @@ -169,10 +169,11 @@ public function providerTestLayoutSectionFormatter() { public function testLayoutSectionFormatter($layout_data, $expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache) { $node = $this->createSectionNode($layout_data); - $this->drupalGet($node->toUrl('canonical')); + $canonical_url = $node->toUrl('canonical'); + $this->drupalGet($canonical_url); $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache); - $this->drupalGet($node->toUrl('layout-builder')); + $this->drupalGet($canonical_url->toString() . '/layout'); $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, 'UNCACHEABLE'); } @@ -255,7 +256,7 @@ public function testLayoutPageTitle() { $this->drupalPlaceBlock('page_title_block'); $node = $this->createSectionNode([]); - $this->drupalGet($node->toUrl('layout-builder')); + $this->drupalGet($node->toUrl('canonical')->toString() . '/layout'); $this->assertSession()->titleEquals('Edit layout for The node title | Drupal'); $this->assertEquals('Edit layout for The node title', $this->cssSelect('h1.page-title')[0]->getText()); } @@ -274,7 +275,8 @@ public function testLayoutUrlNoSectionField() { ], ]); $node->save(); - $this->drupalGet($node->toUrl('layout-builder')); + + $this->drupalGet($node->toUrl('canonical')->toString() . '/layout'); $this->assertSession()->statusCodeEquals(404); } diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayTest.php index 034a76628e3cec5cb566cb212e15583745f257d0..a554fb526f93582976755b47438e1c5714bdeb7a 100644 --- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayTest.php +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayTest.php @@ -31,14 +31,6 @@ protected function getSectionStorage(array $section_data) { return $display; } - /** - * @covers ::getSection - */ - public function testGetSectionInvalidDelta() { - $this->setExpectedException(\OutOfBoundsException::class, 'Invalid delta "2" for the "entity_test.entity_test.default"'); - $this->sectionStorage->getSection(2); - } - /** * Tests that configuration schema enforces valid values. */ diff --git a/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php b/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php index 151cf736f974f27e8d74fbc53f2434f76bc9e625..1879977633e3abc9b192d8fafc41c22f082c3311 100644 --- a/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php +++ b/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php @@ -83,7 +83,7 @@ public function testGetSection() { * @covers ::getSection */ public function testGetSectionInvalidDelta() { - $this->setExpectedException(\OutOfBoundsException::class, 'Invalid delta "2" for the "The test entity"'); + $this->setExpectedException(\OutOfBoundsException::class, 'Invalid delta "2"'); $this->sectionStorage->getSection(2); } diff --git a/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php b/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1aa1c16cf4ab8386274e50e9d50dfc80d242bcf7 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php @@ -0,0 +1,339 @@ +<?php + +namespace Drupal\Tests\layout_builder\Unit; + +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityType; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\layout_builder\Entity\LayoutBuilderSampleEntityGenerator; +use Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage; +use Drupal\layout_builder\SectionStorage\SectionStorageDefinition; +use Drupal\Tests\UnitTestCase; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @coversDefaultClass \Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage + * + * @group layout_builder + */ +class DefaultsSectionStorageTest extends UnitTestCase { + + /** + * The plugin. + * + * @var \Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage + */ + protected $plugin; + + /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityTypeManager; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $entity_type_bundle_info = $this->prophesize(EntityTypeBundleInfoInterface::class); + $sample_entity_generator = $this->prophesize(LayoutBuilderSampleEntityGenerator::class); + + $definition = new SectionStorageDefinition([ + 'id' => 'defaults', + 'class' => DefaultsSectionStorage::class, + ]); + $this->plugin = new DefaultsSectionStorage([], '', $definition, $this->entityTypeManager->reveal(), $entity_type_bundle_info->reveal(), $sample_entity_generator->reveal()); + } + + /** + * @covers ::extractIdFromRoute + * + * @dataProvider providerTestExtractIdFromRoute + */ + public function testExtractIdFromRoute($expected, $value, array $defaults) { + $result = $this->plugin->extractIdFromRoute($value, [], 'the_parameter_name', $defaults); + $this->assertSame($expected, $result); + } + + /** + * Provides data for ::testExtractIdFromRoute(). + */ + public function providerTestExtractIdFromRoute() { + $data = []; + $data['with value'] = [ + 'foo.bar.baz', + 'foo.bar.baz', + [], + ]; + $data['empty value, without bundle'] = [ + 'my_entity_type.bundle_name.default', + '', + [ + 'entity_type_id' => 'my_entity_type', + 'view_mode_name' => 'default', + 'bundle_key' => 'my_bundle', + 'my_bundle' => 'bundle_name', + ], + ]; + $data['empty value, with bundle'] = [ + 'my_entity_type.bundle_name.default', + '', + [ + 'entity_type_id' => 'my_entity_type', + 'view_mode_name' => 'default', + 'bundle' => 'bundle_name', + ], + ]; + $data['without value, empty defaults'] = [ + NULL, + '', + [], + ]; + return $data; + } + + /** + * @covers ::getSectionListFromId + * + * @dataProvider providerTestGetSectionListFromId + */ + public function testGetSectionListFromId($success, $expected_entity_id, $value) { + if ($expected_entity_id) { + $entity_storage = $this->prophesize(EntityStorageInterface::class); + $entity_storage->load($expected_entity_id)->willReturn('the_return_value'); + + $this->entityTypeManager->getDefinition('entity_view_display')->willReturn(new EntityType(['id' => 'entity_view_display'])); + $this->entityTypeManager->getStorage('entity_view_display')->willReturn($entity_storage->reveal()); + } + else { + $this->entityTypeManager->getDefinition('entity_view_display')->shouldNotBeCalled(); + $this->entityTypeManager->getStorage('entity_view_display')->shouldNotBeCalled(); + } + + if (!$success) { + $this->setExpectedException(\InvalidArgumentException::class); + } + + $result = $this->plugin->getSectionListFromId($value); + if ($success) { + $this->assertEquals('the_return_value', $result); + } + } + + /** + * Provides data for ::testGetSectionListFromId(). + */ + public function providerTestGetSectionListFromId() { + $data = []; + $data['with value'] = [ + TRUE, + 'foo.bar.baz', + 'foo.bar.baz', + ]; + $data['without value, empty defaults'] = [ + FALSE, + NULL, + '', + ]; + return $data; + } + + /** + * @covers ::getSectionListFromId + */ + public function testGetSectionListFromIdCreate() { + $expected = 'the_return_value'; + $value = 'foo.bar.baz'; + $expected_create_values = [ + 'targetEntityType' => 'foo', + 'bundle' => 'bar', + 'mode' => 'baz', + 'status' => TRUE, + ]; + $entity_storage = $this->prophesize(EntityStorageInterface::class); + $entity_storage->load($value)->willReturn(NULL); + $entity_storage->create($expected_create_values)->willReturn($expected); + + $this->entityTypeManager->getDefinition('entity_view_display')->willReturn(new EntityType(['id' => 'entity_view_display'])); + $this->entityTypeManager->getStorage('entity_view_display')->willReturn($entity_storage->reveal()); + + $result = $this->plugin->getSectionListFromId($value); + $this->assertSame($expected, $result); + } + + /** + * @covers ::buildRoutes + * @covers ::getEntityTypes + */ + public function testBuildRoutes() { + $entity_types = []; + $entity_types['no_link_template'] = new EntityType(['id' => 'no_link_template']); + $entity_types['unknown_field_ui_route'] = new EntityType([ + 'id' => 'unknown_field_ui_route', + 'links' => ['layout-builder' => '/entity/{entity}/layout'], + 'entity_keys' => ['id' => 'id'], + 'field_ui_base_route' => 'unknown', + ]); + $entity_types['with_bundle_key'] = new EntityType([ + 'id' => 'with_bundle_key', + 'links' => ['layout-builder' => '/entity/{entity}/layout'], + 'entity_keys' => ['id' => 'id', 'bundle' => 'bundle'], + 'bundle_entity_type' => 'my_bundle_type', + 'field_ui_base_route' => 'known', + ]); + $entity_types['with_bundle_parameter'] = new EntityType([ + 'id' => 'with_bundle_parameter', + 'links' => ['layout-builder' => '/entity/{entity}/layout'], + 'entity_keys' => ['id' => 'id'], + 'field_ui_base_route' => 'with_bundle', + ]); + $this->entityTypeManager->getDefinitions()->willReturn($entity_types); + + $expected = [ + 'known' => new Route('/admin/entity/whatever', [], [], ['_admin_route' => TRUE]), + 'with_bundle' => new Route('/admin/entity/{bundle}'), + 'layout_builder.defaults.with_bundle_key.view' => new Route( + '/admin/entity/whatever/display-layout/{view_mode_name}', + [ + 'entity_type_id' => 'with_bundle_key', + 'bundle_key' => 'my_bundle_type', + 'section_storage_type' => 'defaults', + 'section_storage' => '', + 'is_rebuilding' => FALSE, + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', + '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', + ], + [ + '_field_ui_view_mode_access' => 'administer with_bundle_key display', + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + ], + '_layout_builder' => TRUE, + '_admin_route' => FALSE, + ] + ), + 'layout_builder.defaults.with_bundle_key.save' => new Route( + '/admin/entity/whatever/display-layout/{view_mode_name}/save', + [ + 'entity_type_id' => 'with_bundle_key', + 'bundle_key' => 'my_bundle_type', + 'section_storage_type' => 'defaults', + 'section_storage' => '', + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', + ], + [ + '_field_ui_view_mode_access' => 'administer with_bundle_key display', + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + ], + '_layout_builder' => TRUE, + '_admin_route' => FALSE, + ] + ), + 'layout_builder.defaults.with_bundle_key.cancel' => new Route( + '/admin/entity/whatever/display-layout/{view_mode_name}/cancel', + [ + 'entity_type_id' => 'with_bundle_key', + 'bundle_key' => 'my_bundle_type', + 'section_storage_type' => 'defaults', + 'section_storage' => '', + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', + ], + [ + '_field_ui_view_mode_access' => 'administer with_bundle_key display', + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + ], + '_layout_builder' => TRUE, + '_admin_route' => FALSE, + ] + ), + 'layout_builder.defaults.with_bundle_parameter.view' => new Route( + '/admin/entity/{bundle}/display-layout/{view_mode_name}', + [ + 'entity_type_id' => 'with_bundle_parameter', + 'section_storage_type' => 'defaults', + 'section_storage' => '', + 'is_rebuilding' => FALSE, + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', + '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', + ], + [ + '_field_ui_view_mode_access' => 'administer with_bundle_parameter display', + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + ], + '_layout_builder' => TRUE, + '_admin_route' => FALSE, + ] + ), + 'layout_builder.defaults.with_bundle_parameter.save' => new Route( + '/admin/entity/{bundle}/display-layout/{view_mode_name}/save', + [ + 'entity_type_id' => 'with_bundle_parameter', + 'section_storage_type' => 'defaults', + 'section_storage' => '', + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', + ], + [ + '_field_ui_view_mode_access' => 'administer with_bundle_parameter display', + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + ], + '_layout_builder' => TRUE, + '_admin_route' => FALSE, + ] + ), + 'layout_builder.defaults.with_bundle_parameter.cancel' => new Route( + '/admin/entity/{bundle}/display-layout/{view_mode_name}/cancel', + [ + 'entity_type_id' => 'with_bundle_parameter', + 'section_storage_type' => 'defaults', + 'section_storage' => '', + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', + ], + [ + '_field_ui_view_mode_access' => 'administer with_bundle_parameter display', + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + ], + '_layout_builder' => TRUE, + '_admin_route' => FALSE, + ] + ), + ]; + + $collection = new RouteCollection(); + $collection->add('known', new Route('/admin/entity/whatever', [], [], ['_admin_route' => TRUE])); + $collection->add('with_bundle', new Route('/admin/entity/{bundle}')); + + $this->plugin->buildRoutes($collection); + $this->assertEquals($expected, $collection->all()); + $this->assertSame(array_keys($expected), array_keys($collection->all())); + } + +} diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutBuilderRoutesTest.php b/core/modules/layout_builder/tests/src/Unit/LayoutBuilderRoutesTest.php index 961b870429880582cbc30b44100550922ddc5e7f..e4b20bd840c9f37e778718c3c9c0cb9380b36a75 100644 --- a/core/modules/layout_builder/tests/src/Unit/LayoutBuilderRoutesTest.php +++ b/core/modules/layout_builder/tests/src/Unit/LayoutBuilderRoutesTest.php @@ -2,13 +2,13 @@ namespace Drupal\Tests\layout_builder\Unit; -use Drupal\Core\Entity\EntityFieldManagerInterface; -use Drupal\Core\Entity\EntityType; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Routing\RouteBuildEvent; use Drupal\layout_builder\Routing\LayoutBuilderRoutes; +use Drupal\layout_builder\SectionStorage\SectionStorageDefinition; +use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; +use Drupal\layout_builder\SectionStorageInterface; use Drupal\Tests\UnitTestCase; +use Prophecy\Argument; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -19,6 +19,13 @@ */ class LayoutBuilderRoutesTest extends UnitTestCase { + /** + * The Layout Builder route builder. + * + * @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface + */ + protected $sectionStorageManager; + /** * The Layout Builder route builder. * @@ -32,685 +39,46 @@ class LayoutBuilderRoutesTest extends UnitTestCase { protected function setUp() { parent::setUp(); - $entity_types = []; - $entity_types['no_link_template'] = new EntityType(['id' => 'no_link_template']); - $entity_types['with_link_template'] = new EntityType([ - 'id' => 'with_link_template', - 'links' => ['layout-builder' => '/entity/{entity}/layout'], - 'entity_keys' => ['id' => 'id'], - 'field_ui_base_route' => 'unknown', - ]); - $entity_types['with_integer_id'] = new EntityType([ - 'id' => 'with_integer_id', - 'links' => ['layout-builder' => '/entity/{entity}/layout'], - 'entity_keys' => ['id' => 'id'], - ]); - $entity_types['with_field_ui_route'] = new EntityType([ - 'id' => 'with_field_ui_route', - 'links' => ['layout-builder' => '/entity/{entity}/layout'], - 'entity_keys' => ['id' => 'id'], - 'field_ui_base_route' => 'known', - ]); - $entity_types['with_bundle_key'] = new EntityType([ - 'id' => 'with_field_ui_route', - 'links' => ['layout-builder' => '/entity/{entity}/layout'], - 'entity_keys' => ['id' => 'id', 'bundle' => 'bundle'], - 'bundle_entity_type' => 'my_bundle_type', - 'field_ui_base_route' => 'known', - ]); - $entity_types['with_bundle_parameter'] = new EntityType([ - 'id' => 'with_bundle_parameter', - 'links' => ['layout-builder' => '/entity/{entity}/layout'], - 'entity_keys' => ['id' => 'id'], - 'field_ui_base_route' => 'with_bundle', - ]); - $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); - $entity_type_manager->getDefinitions()->willReturn($entity_types); - - $string_id = $this->prophesize(FieldStorageDefinitionInterface::class); - $string_id->getType()->willReturn('string'); - $integer_id = $this->prophesize(FieldStorageDefinitionInterface::class); - $integer_id->getType()->willReturn('integer'); - $entity_field_manager = $this->prophesize(EntityFieldManagerInterface::class); - $entity_field_manager->getFieldStorageDefinitions('no_link_template')->shouldNotBeCalled(); - $entity_field_manager->getFieldStorageDefinitions('with_link_template')->willReturn(['id' => $string_id->reveal()]); - $entity_field_manager->getFieldStorageDefinitions('with_integer_id')->willReturn(['id' => $integer_id->reveal()]); - $entity_field_manager->getFieldStorageDefinitions('with_field_ui_route')->willReturn(['id' => $integer_id->reveal()]); - $entity_field_manager->getFieldStorageDefinitions('with_bundle_parameter')->willReturn(['id' => $integer_id->reveal()]); - - $this->routeBuilder = new LayoutBuilderRoutes($entity_type_manager->reveal(), $entity_field_manager->reveal()); + $this->sectionStorageManager = $this->prophesize(SectionStorageManagerInterface::class); + $this->routeBuilder = new LayoutBuilderRoutes($this->sectionStorageManager->reveal()); } /** - * @covers ::getRoutes - * @covers ::buildRoute - * @covers ::hasIntegerId - * @covers ::getEntityTypes + * @covers ::onAlterRoutes */ - public function testGetRoutes() { + public function testOnAlterRoutes() { $expected = [ - 'entity.with_link_template.layout_builder' => new Route( - '/entity/{entity}/layout', - [ - 'entity_type_id' => 'with_link_template', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - 'is_rebuilding' => FALSE, - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', - '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', - ], - [ - '_has_layout_section' => 'true', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_link_template' => ['type' => 'entity:with_link_template'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_link_template.layout_builder_save' => new Route( - '/entity/{entity}/layout/save', - [ - 'entity_type_id' => 'with_link_template', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', - ], - [ - '_has_layout_section' => 'true', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_link_template' => ['type' => 'entity:with_link_template'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_link_template.layout_builder_cancel' => new Route( - '/entity/{entity}/layout/cancel', - [ - 'entity_type_id' => 'with_link_template', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', - ], - [ - '_has_layout_section' => 'true', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_link_template' => ['type' => 'entity:with_link_template'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_link_template.layout_builder_revert' => new Route( - '/entity/{entity}/layout/revert', - [ - 'entity_type_id' => 'with_link_template', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_form' => '\Drupal\layout_builder\Form\RevertOverridesForm', - ], - [ - '_has_layout_section' => 'true', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_link_template' => ['type' => 'entity:with_link_template'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_integer_id.layout_builder' => new Route( - '/entity/{entity}/layout', - [ - 'entity_type_id' => 'with_integer_id', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - 'is_rebuilding' => FALSE, - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', - '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', - ], - [ - '_has_layout_section' => 'true', - 'with_integer_id' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_integer_id' => ['type' => 'entity:with_integer_id'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_integer_id.layout_builder_save' => new Route( - '/entity/{entity}/layout/save', - [ - 'entity_type_id' => 'with_integer_id', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', - ], - [ - '_has_layout_section' => 'true', - 'with_integer_id' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_integer_id' => ['type' => 'entity:with_integer_id'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_integer_id.layout_builder_cancel' => new Route( - '/entity/{entity}/layout/cancel', - [ - 'entity_type_id' => 'with_integer_id', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', - ], - [ - '_has_layout_section' => 'true', - 'with_integer_id' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_integer_id' => ['type' => 'entity:with_integer_id'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_integer_id.layout_builder_revert' => new Route( - '/entity/{entity}/layout/revert', - [ - 'entity_type_id' => 'with_integer_id', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_form' => '\Drupal\layout_builder\Form\RevertOverridesForm', - ], - [ - '_has_layout_section' => 'true', - 'with_integer_id' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_integer_id' => ['type' => 'entity:with_integer_id'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_field_ui_route.layout_builder' => new Route( - '/entity/{entity}/layout', - [ - 'entity_type_id' => 'with_field_ui_route', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - 'is_rebuilding' => FALSE, - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', - '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', - ], - [ - '_has_layout_section' => 'true', - 'with_field_ui_route' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_field_ui_route' => ['type' => 'entity:with_field_ui_route'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_field_ui_route.layout_builder_save' => new Route( - '/entity/{entity}/layout/save', - [ - 'entity_type_id' => 'with_field_ui_route', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', - ], - [ - '_has_layout_section' => 'true', - 'with_field_ui_route' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_field_ui_route' => ['type' => 'entity:with_field_ui_route'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_field_ui_route.layout_builder_cancel' => new Route( - '/entity/{entity}/layout/cancel', - [ - 'entity_type_id' => 'with_field_ui_route', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', - ], - [ - '_has_layout_section' => 'true', - 'with_field_ui_route' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_field_ui_route' => ['type' => 'entity:with_field_ui_route'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_field_ui_route.layout_builder_revert' => new Route( - '/entity/{entity}/layout/revert', - [ - 'entity_type_id' => 'with_field_ui_route', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_form' => '\Drupal\layout_builder\Form\RevertOverridesForm', - ], - [ - '_has_layout_section' => 'true', - 'with_field_ui_route' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_field_ui_route' => ['type' => 'entity:with_field_ui_route'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_bundle_key.layout_builder' => new Route( - '/entity/{entity}/layout', - [ - 'entity_type_id' => 'with_bundle_key', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - 'is_rebuilding' => FALSE, - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', - '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', - ], - [ - '_has_layout_section' => 'true', - 'with_bundle_key' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_bundle_key' => ['type' => 'entity:with_bundle_key'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_bundle_key.layout_builder_save' => new Route( - '/entity/{entity}/layout/save', - [ - 'entity_type_id' => 'with_bundle_key', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', - ], - [ - '_has_layout_section' => 'true', - 'with_bundle_key' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_bundle_key' => ['type' => 'entity:with_bundle_key'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_bundle_key.layout_builder_cancel' => new Route( - '/entity/{entity}/layout/cancel', - [ - 'entity_type_id' => 'with_bundle_key', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', - ], - [ - '_has_layout_section' => 'true', - 'with_bundle_key' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_bundle_key' => ['type' => 'entity:with_bundle_key'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_bundle_key.layout_builder_revert' => new Route( - '/entity/{entity}/layout/revert', - [ - 'entity_type_id' => 'with_bundle_key', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_form' => '\Drupal\layout_builder\Form\RevertOverridesForm', - ], - [ - '_has_layout_section' => 'true', - 'with_bundle_key' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_bundle_key' => ['type' => 'entity:with_bundle_key'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_bundle_parameter.layout_builder' => new Route( - '/entity/{entity}/layout', - [ - 'entity_type_id' => 'with_bundle_parameter', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - 'is_rebuilding' => FALSE, - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', - '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', - ], - [ - '_has_layout_section' => 'true', - 'with_bundle_parameter' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_bundle_parameter' => ['type' => 'entity:with_bundle_parameter'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_bundle_parameter.layout_builder_save' => new Route( - '/entity/{entity}/layout/save', - [ - 'entity_type_id' => 'with_bundle_parameter', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', - ], - [ - '_has_layout_section' => 'true', - 'with_bundle_parameter' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_bundle_parameter' => ['type' => 'entity:with_bundle_parameter'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_bundle_parameter.layout_builder_cancel' => new Route( - '/entity/{entity}/layout/cancel', - [ - 'entity_type_id' => 'with_bundle_parameter', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', - ], - [ - '_has_layout_section' => 'true', - 'with_bundle_parameter' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_bundle_parameter' => ['type' => 'entity:with_bundle_parameter'], - ], - '_layout_builder' => TRUE, - ] - ), - 'entity.with_bundle_parameter.layout_builder_revert' => new Route( - '/entity/{entity}/layout/revert', - [ - 'entity_type_id' => 'with_bundle_parameter', - 'section_storage_type' => 'overrides', - 'section_storage' => '', - '_form' => '\Drupal\layout_builder\Form\RevertOverridesForm', - ], - [ - '_has_layout_section' => 'true', - 'with_bundle_parameter' => '\d+', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - 'with_bundle_parameter' => ['type' => 'entity:with_bundle_parameter'], - ], - '_layout_builder' => TRUE, - ] - ), + 'test_route1' => new Route('/test/path1'), + 'test_route_shared' => new Route('/test/path/shared2'), + 'test_route2' => new Route('/test/path2'), ]; - $this->assertEquals($expected, $this->routeBuilder->getRoutes()); - } + $section_storage_first = $this->prophesize(SectionStorageInterface::class); + $section_storage_first->buildRoutes(Argument::type(RouteCollection::class))->shouldBeCalled()->will(function ($args) { + /** @var \Symfony\Component\Routing\RouteCollection $collection */ + $collection = $args[0]; + $collection->add('test_route_shared', new Route('/test/path/shared1')); + $collection->add('test_route1', new Route('/test/path1')); + }); + $section_storage_second = $this->prophesize(SectionStorageInterface::class); + $section_storage_second->buildRoutes(Argument::type(RouteCollection::class))->shouldBeCalled()->will(function ($args) { + /** @var \Symfony\Component\Routing\RouteCollection $collection */ + $collection = $args[0]; + $collection->add('test_route_shared', new Route('/test/path/shared2')); + $collection->add('test_route2', new Route('/test/path2')); + }); + + $this->sectionStorageManager->loadEmpty('first')->willReturn($section_storage_first->reveal()); + $this->sectionStorageManager->loadEmpty('second')->willReturn($section_storage_second->reveal()); + $definitions['first'] = new SectionStorageDefinition(); + $definitions['second'] = new SectionStorageDefinition(); + $this->sectionStorageManager->getDefinitions()->willReturn($definitions); - /** - * @covers ::onAlterRoutes - * @covers ::buildRoute - * @covers ::hasIntegerId - * @covers ::getEntityTypes - */ - public function testOnAlterRoutes() { $collection = new RouteCollection(); - $collection->add('known', new Route('/admin/entity/whatever', [], [], ['_admin_route' => TRUE])); - $collection->add('with_bundle', new Route('/admin/entity/{bundle}')); $event = new RouteBuildEvent($collection); - - $expected = [ - 'known' => new Route('/admin/entity/whatever', [], [], ['_admin_route' => TRUE]), - 'with_bundle' => new Route('/admin/entity/{bundle}'), - 'entity.entity_view_display.with_field_ui_route.layout_builder' => new Route( - '/admin/entity/whatever/display-layout/{view_mode_name}', - [ - 'entity_type_id' => 'with_field_ui_route', - 'bundle' => 'with_field_ui_route', - 'section_storage_type' => 'defaults', - 'section_storage' => '', - 'is_rebuilding' => FALSE, - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', - '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', - ], - [ - '_field_ui_view_mode_access' => 'administer with_field_ui_route display', - '_has_layout_section' => 'true', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - ], - '_layout_builder' => TRUE, - '_admin_route' => FALSE, - ] - ), - 'entity.entity_view_display.with_field_ui_route.layout_builder_save' => new Route( - '/admin/entity/whatever/display-layout/{view_mode_name}/save', - [ - 'entity_type_id' => 'with_field_ui_route', - 'bundle' => 'with_field_ui_route', - 'section_storage_type' => 'defaults', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', - ], - [ - '_field_ui_view_mode_access' => 'administer with_field_ui_route display', - '_has_layout_section' => 'true', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - ], - '_layout_builder' => TRUE, - '_admin_route' => FALSE, - ] - ), - 'entity.entity_view_display.with_field_ui_route.layout_builder_cancel' => new Route( - '/admin/entity/whatever/display-layout/{view_mode_name}/cancel', - [ - 'entity_type_id' => 'with_field_ui_route', - 'bundle' => 'with_field_ui_route', - 'section_storage_type' => 'defaults', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', - ], - [ - '_field_ui_view_mode_access' => 'administer with_field_ui_route display', - '_has_layout_section' => 'true', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - ], - '_layout_builder' => TRUE, - '_admin_route' => FALSE, - ] - ), - 'entity.entity_view_display.with_bundle_key.layout_builder' => new Route( - '/admin/entity/whatever/display-layout/{view_mode_name}', - [ - 'entity_type_id' => 'with_bundle_key', - 'bundle_key' => 'my_bundle_type', - 'section_storage_type' => 'defaults', - 'section_storage' => '', - 'is_rebuilding' => FALSE, - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', - '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', - ], - [ - '_field_ui_view_mode_access' => 'administer with_bundle_key display', - '_has_layout_section' => 'true', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - ], - '_layout_builder' => TRUE, - '_admin_route' => FALSE, - ] - ), - 'entity.entity_view_display.with_bundle_key.layout_builder_save' => new Route( - '/admin/entity/whatever/display-layout/{view_mode_name}/save', - [ - 'entity_type_id' => 'with_bundle_key', - 'bundle_key' => 'my_bundle_type', - 'section_storage_type' => 'defaults', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', - ], - [ - '_field_ui_view_mode_access' => 'administer with_bundle_key display', - '_has_layout_section' => 'true', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - ], - '_layout_builder' => TRUE, - '_admin_route' => FALSE, - ] - ), - 'entity.entity_view_display.with_bundle_key.layout_builder_cancel' => new Route( - '/admin/entity/whatever/display-layout/{view_mode_name}/cancel', - [ - 'entity_type_id' => 'with_bundle_key', - 'bundle_key' => 'my_bundle_type', - 'section_storage_type' => 'defaults', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', - ], - [ - '_field_ui_view_mode_access' => 'administer with_bundle_key display', - '_has_layout_section' => 'true', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - ], - '_layout_builder' => TRUE, - '_admin_route' => FALSE, - ] - ), - 'entity.entity_view_display.with_bundle_parameter.layout_builder' => new Route( - '/admin/entity/{bundle}/display-layout/{view_mode_name}', - [ - 'entity_type_id' => 'with_bundle_parameter', - 'section_storage_type' => 'defaults', - 'section_storage' => '', - 'is_rebuilding' => FALSE, - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', - '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', - ], - [ - '_field_ui_view_mode_access' => 'administer with_bundle_parameter display', - '_has_layout_section' => 'true', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - ], - '_layout_builder' => TRUE, - '_admin_route' => FALSE, - ] - ), - 'entity.entity_view_display.with_bundle_parameter.layout_builder_save' => new Route( - '/admin/entity/{bundle}/display-layout/{view_mode_name}/save', - [ - 'entity_type_id' => 'with_bundle_parameter', - 'section_storage_type' => 'defaults', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', - ], - [ - '_field_ui_view_mode_access' => 'administer with_bundle_parameter display', - '_has_layout_section' => 'true', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - ], - '_layout_builder' => TRUE, - '_admin_route' => FALSE, - ] - ), - 'entity.entity_view_display.with_bundle_parameter.layout_builder_cancel' => new Route( - '/admin/entity/{bundle}/display-layout/{view_mode_name}/cancel', - [ - 'entity_type_id' => 'with_bundle_parameter', - 'section_storage_type' => 'defaults', - 'section_storage' => '', - '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', - ], - [ - '_field_ui_view_mode_access' => 'administer with_bundle_parameter display', - '_has_layout_section' => 'true', - ], - [ - 'parameters' => [ - 'section_storage' => ['layout_builder_tempstore' => TRUE], - ], - '_layout_builder' => TRUE, - '_admin_route' => FALSE, - ] - ), - ]; - $this->routeBuilder->onAlterRoutes($event); - $this->assertEquals($expected, $event->getRouteCollection()->all()); + $this->assertEquals($expected, $collection->all()); + $this->assertSame(array_keys($expected), array_keys($collection->all())); } } diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreParamConverterTest.php b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreParamConverterTest.php index 3c3c9f40653a6348590b6d0aa7a00670fe6b5eb8..5f8dc051e3765e978ea7a8ed39fa7d95f41c4935 100644 --- a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreParamConverterTest.php +++ b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreParamConverterTest.php @@ -2,10 +2,9 @@ namespace Drupal\Tests\layout_builder\Unit; -use Drupal\Core\DependencyInjection\ClassResolverInterface; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; use Drupal\layout_builder\Routing\LayoutTempstoreParamConverter; -use Drupal\layout_builder\Routing\SectionStorageParamConverterInterface; +use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; use Drupal\layout_builder\SectionStorageInterface; use Drupal\Tests\UnitTestCase; @@ -18,23 +17,23 @@ class LayoutTempstoreParamConverterTest extends UnitTestCase { /** * @covers ::convert - * @covers ::getParamConverterFromDefaults */ public function testConvert() { $layout_tempstore_repository = $this->prophesize(LayoutTempstoreRepositoryInterface::class); - $class_resolver = $this->prophesize(ClassResolverInterface::class); - $param_converter = $this->prophesize(SectionStorageParamConverterInterface::class); - $converter = new LayoutTempstoreParamConverter($layout_tempstore_repository->reveal(), $class_resolver->reveal()); + $section_storage_manager = $this->prophesize(SectionStorageManagerInterface::class); + $converter = new LayoutTempstoreParamConverter($layout_tempstore_repository->reveal(), $section_storage_manager->reveal()); + + $section_storage = $this->prophesize(SectionStorageInterface::class); $value = 'some_value'; $definition = ['layout_builder_tempstore' => TRUE]; $name = 'the_parameter_name'; $defaults = ['section_storage_type' => 'my_type']; - $section_storage = $this->prophesize(SectionStorageInterface::class); $expected = 'the_return_value'; - $class_resolver->getInstanceFromDefinition('layout_builder.section_storage_param_converter.my_type')->willReturn($param_converter->reveal()); - $param_converter->convert($value, $definition, $name, $defaults)->willReturn($section_storage->reveal()); + $section_storage_manager->hasDefinition('my_type')->willReturn(TRUE); + $section_storage_manager->loadFromRoute('my_type', $value, $definition, $name, $defaults)->willReturn($section_storage); + $layout_tempstore_repository->get($section_storage->reveal())->willReturn($expected); $result = $converter->convert($value, $definition, $name, $defaults); @@ -43,19 +42,19 @@ public function testConvert() { /** * @covers ::convert - * @covers ::getParamConverterFromDefaults */ public function testConvertNoType() { $layout_tempstore_repository = $this->prophesize(LayoutTempstoreRepositoryInterface::class); - $class_resolver = $this->prophesize(ClassResolverInterface::class); - $converter = new LayoutTempstoreParamConverter($layout_tempstore_repository->reveal(), $class_resolver->reveal()); + $section_storage_manager = $this->prophesize(SectionStorageManagerInterface::class); + $converter = new LayoutTempstoreParamConverter($layout_tempstore_repository->reveal(), $section_storage_manager->reveal()); $value = 'some_value'; $definition = ['layout_builder_tempstore' => TRUE]; $name = 'the_parameter_name'; $defaults = ['section_storage_type' => NULL]; - $class_resolver->getInstanceFromDefinition()->shouldNotBeCalled(); + $section_storage_manager->hasDefinition()->shouldNotBeCalled(); + $section_storage_manager->loadFromRoute()->shouldNotBeCalled(); $layout_tempstore_repository->get()->shouldNotBeCalled(); $result = $converter->convert($value, $definition, $name, $defaults); @@ -64,19 +63,19 @@ public function testConvertNoType() { /** * @covers ::convert - * @covers ::getParamConverterFromDefaults */ public function testConvertInvalidConverter() { $layout_tempstore_repository = $this->prophesize(LayoutTempstoreRepositoryInterface::class); - $class_resolver = $this->prophesize(ClassResolverInterface::class); - $converter = new LayoutTempstoreParamConverter($layout_tempstore_repository->reveal(), $class_resolver->reveal()); + $section_storage_manager = $this->prophesize(SectionStorageManagerInterface::class); + $converter = new LayoutTempstoreParamConverter($layout_tempstore_repository->reveal(), $section_storage_manager->reveal()); $value = 'some_value'; $definition = ['layout_builder_tempstore' => TRUE]; $name = 'the_parameter_name'; $defaults = ['section_storage_type' => 'invalid']; - $class_resolver->getInstanceFromDefinition('layout_builder.section_storage_param_converter.invalid')->willThrow(\InvalidArgumentException::class); + $section_storage_manager->hasDefinition('invalid')->willReturn(FALSE); + $section_storage_manager->loadFromRoute()->shouldNotBeCalled(); $layout_tempstore_repository->get()->shouldNotBeCalled(); $result = $converter->convert($value, $definition, $name, $defaults); diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php index 5d1f691271d5b3280cdbe868089df9b51e2a6f25..0c74d01763290cc9d33daeda8ea5b2f8d4fc2894 100644 --- a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php +++ b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php @@ -2,12 +2,11 @@ namespace Drupal\Tests\layout_builder\Unit; +use Drupal\Core\TempStore\SharedTempStore; +use Drupal\Core\TempStore\SharedTempStoreFactory; use Drupal\layout_builder\LayoutTempstoreRepository; -use Drupal\layout_builder\Section; use Drupal\layout_builder\SectionStorageInterface; use Drupal\Tests\UnitTestCase; -use Drupal\Core\TempStore\SharedTempStore; -use Drupal\Core\TempStore\SharedTempStoreFactory; /** * @coversDefaultClass \Drupal\layout_builder\LayoutTempstoreRepository @@ -19,7 +18,9 @@ class LayoutTempstoreRepositoryTest extends UnitTestCase { * @covers ::get */ public function testGetEmptyTempstore() { - $section_storage = new TestSectionStorage(); + $section_storage = $this->prophesize(SectionStorageInterface::class); + $section_storage->getStorageType()->willReturn('my_storage_type'); + $section_storage->getStorageId()->willReturn('my_storage_id'); $tempstore = $this->prophesize(SharedTempStore::class); $tempstore->get('my_storage_id')->shouldBeCalled(); @@ -29,34 +30,38 @@ public function testGetEmptyTempstore() { $repository = new LayoutTempstoreRepository($tempstore_factory->reveal()); - $result = $repository->get($section_storage); - $this->assertSame($section_storage, $result); + $result = $repository->get($section_storage->reveal()); + $this->assertSame($section_storage->reveal(), $result); } /** * @covers ::get */ public function testGetLoadedTempstore() { - $section_storage = new TestSectionStorage(); + $section_storage = $this->prophesize(SectionStorageInterface::class); + $section_storage->getStorageType()->willReturn('my_storage_type'); + $section_storage->getStorageId()->willReturn('my_storage_id'); - $tempstore_section_storage = new TestSectionStorage(); + $tempstore_section_storage = $this->prophesize(SectionStorageInterface::class); $tempstore = $this->prophesize(SharedTempStore::class); - $tempstore->get('my_storage_id')->willReturn(['section_storage' => $tempstore_section_storage]); + $tempstore->get('my_storage_id')->willReturn(['section_storage' => $tempstore_section_storage->reveal()]); $tempstore_factory = $this->prophesize(SharedTempStoreFactory::class); $tempstore_factory->get('layout_builder.section_storage.my_storage_type')->willReturn($tempstore->reveal()); $repository = new LayoutTempstoreRepository($tempstore_factory->reveal()); - $result = $repository->get($section_storage); - $this->assertSame($tempstore_section_storage, $result); - $this->assertNotSame($section_storage, $result); + $result = $repository->get($section_storage->reveal()); + $this->assertSame($tempstore_section_storage->reveal(), $result); + $this->assertNotSame($section_storage->reveal(), $result); } /** * @covers ::get */ public function testGetInvalidEntry() { - $section_storage = new TestSectionStorage(); + $section_storage = $this->prophesize(SectionStorageInterface::class); + $section_storage->getStorageType()->willReturn('my_storage_type'); + $section_storage->getStorageId()->willReturn('my_storage_id'); $tempstore = $this->prophesize(SharedTempStore::class); $tempstore->get('my_storage_id')->willReturn(['section_storage' => 'this_is_not_an_entity']); @@ -67,85 +72,7 @@ public function testGetInvalidEntry() { $repository = new LayoutTempstoreRepository($tempstore_factory->reveal()); $this->setExpectedException(\UnexpectedValueException::class, 'The entry with storage type "my_storage_type" and ID "my_storage_id" is invalid'); - $repository->get($section_storage); - } - -} - -/** - * Provides a test implementation of section storage. - * - * @todo This works around https://github.com/phpspec/prophecy/issues/119. - */ -class TestSectionStorage implements SectionStorageInterface { - - /** - * {@inheritdoc} - */ - public static function getStorageType() { - return 'my_storage_type'; + $repository->get($section_storage->reveal()); } - /** - * {@inheritdoc} - */ - public function getStorageId() { - return 'my_storage_id'; - } - - /** - * {@inheritdoc} - */ - public function count() {} - - /** - * {@inheritdoc} - */ - public function getSections() {} - - /** - * {@inheritdoc} - */ - public function getSection($delta) {} - - /** - * {@inheritdoc} - */ - public function appendSection(Section $section) {} - - /** - * {@inheritdoc} - */ - public function insertSection($delta, Section $section) {} - - /** - * {@inheritdoc} - */ - public function removeSection($delta) {} - - /** - * {@inheritdoc} - */ - public function getContexts() {} - - /** - * {@inheritdoc} - */ - public function label() {} - - /** - * {@inheritdoc} - */ - public function save() {} - - /** - * {@inheritdoc} - */ - public function getCanonicalUrl() {} - - /** - * {@inheritdoc} - */ - public function getLayoutBuilderUrl() {} - } diff --git a/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php b/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php new file mode 100644 index 0000000000000000000000000000000000000000..711ce5d40b477726311ee71f42c3928a205bcf52 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php @@ -0,0 +1,363 @@ +<?php + +namespace Drupal\Tests\layout_builder\Unit; + +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityType; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; +use Drupal\layout_builder\SectionStorage\SectionStorageDefinition; +use Drupal\Tests\UnitTestCase; +use Prophecy\Argument; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @coversDefaultClass \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage + * + * @group layout_builder + */ +class OverridesSectionStorageTest extends UnitTestCase { + + /** + * The plugin. + * + * @var \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage + */ + protected $plugin; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The entity field manager. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface + */ + protected $entityFieldManager; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $this->entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class); + + $definition = new SectionStorageDefinition([ + 'id' => 'overrides', + 'class' => OverridesSectionStorage::class, + ]); + $this->plugin = new OverridesSectionStorage([], 'overrides', $definition, $this->entityTypeManager->reveal(), $this->entityFieldManager->reveal()); + } + + /** + * @covers ::extractIdFromRoute + * + * @dataProvider providerTestExtractIdFromRoute + */ + public function testExtractIdFromRoute($expected, $value, array $defaults) { + $result = $this->plugin->extractIdFromRoute($value, [], 'the_parameter_name', $defaults); + $this->assertSame($expected, $result); + } + + /** + * Provides data for ::testExtractIdFromRoute(). + */ + public function providerTestExtractIdFromRoute() { + $data = []; + $data['with value, with layout'] = [ + 'my_entity_type.entity_with_layout', + 'my_entity_type.entity_with_layout', + [], + ]; + $data['with value, without layout'] = [ + NULL, + 'my_entity_type', + [], + ]; + $data['empty value, populated defaults'] = [ + 'my_entity_type.entity_with_layout', + '', + [ + 'entity_type_id' => 'my_entity_type', + 'my_entity_type' => 'entity_with_layout', + ], + ]; + $data['empty value, empty defaults'] = [ + NULL, + '', + [], + ]; + return $data; + } + + /** + * @covers ::getSectionListFromId + * + * @dataProvider providerTestGetSectionListFromId + */ + public function testGetSectionListFromId($success, $expected_entity_type_id, $id) { + $defaults['the_parameter_name'] = $id; + + if ($expected_entity_type_id) { + $entity_storage = $this->prophesize(EntityStorageInterface::class); + + $entity_without_layout = $this->prophesize(FieldableEntityInterface::class); + $entity_without_layout->hasField('layout_builder__layout')->willReturn(FALSE); + $entity_without_layout->get('layout_builder__layout')->shouldNotBeCalled(); + $entity_storage->load('entity_without_layout')->willReturn($entity_without_layout->reveal()); + + $entity_with_layout = $this->prophesize(FieldableEntityInterface::class); + $entity_with_layout->hasField('layout_builder__layout')->willReturn(TRUE); + $entity_with_layout->get('layout_builder__layout')->willReturn('the_return_value'); + $entity_storage->load('entity_with_layout')->willReturn($entity_with_layout->reveal()); + + $this->entityTypeManager->getStorage($expected_entity_type_id)->willReturn($entity_storage->reveal()); + } + else { + $this->entityTypeManager->getStorage(Argument::any())->shouldNotBeCalled(); + } + + if (!$success) { + $this->setExpectedException(\InvalidArgumentException::class); + } + + $result = $this->plugin->getSectionListFromId($id); + if ($success) { + $this->assertEquals('the_return_value', $result); + } + } + + /** + * Provides data for ::testGetSectionListFromId(). + */ + public function providerTestGetSectionListFromId() { + $data = []; + $data['with value, with layout'] = [ + TRUE, + 'my_entity_type', + 'my_entity_type.entity_with_layout', + ]; + $data['with value, without layout'] = [ + FALSE, + 'my_entity_type', + 'my_entity_type.entity_without_layout', + ]; + $data['empty value, empty defaults'] = [ + FALSE, + NULL, + '', + ]; + return $data; + } + + /** + * @covers ::buildRoutes + * @covers ::hasIntegerId + * @covers ::getEntityTypes + */ + public function testBuildRoutes() { + $entity_types = []; + + $entity_types['no_link_template'] = new EntityType(['id' => 'no_link_template']); + $this->entityFieldManager->getFieldStorageDefinitions('no_link_template')->shouldNotBeCalled(); + + $entity_types['with_string_id'] = new EntityType([ + 'id' => 'with_string_id', + 'links' => ['layout-builder' => '/entity/{entity}/layout'], + 'entity_keys' => ['id' => 'id'], + ]); + $string_id = $this->prophesize(FieldStorageDefinitionInterface::class); + $string_id->getType()->willReturn('string'); + $this->entityFieldManager->getFieldStorageDefinitions('with_string_id')->willReturn(['id' => $string_id->reveal()]); + + $entity_types['with_integer_id'] = new EntityType([ + 'id' => 'with_integer_id', + 'links' => ['layout-builder' => '/entity/{entity}/layout'], + 'entity_keys' => ['id' => 'id'], + ]); + $integer_id = $this->prophesize(FieldStorageDefinitionInterface::class); + $integer_id->getType()->willReturn('integer'); + $this->entityFieldManager->getFieldStorageDefinitions('with_integer_id')->willReturn(['id' => $integer_id->reveal()]); + + $this->entityTypeManager->getDefinitions()->willReturn($entity_types); + + $expected = [ + 'layout_builder.overrides.with_string_id.view' => new Route( + '/entity/{entity}/layout', + [ + 'entity_type_id' => 'with_string_id', + 'section_storage_type' => 'overrides', + 'section_storage' => '', + 'is_rebuilding' => FALSE, + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', + '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', + ], + [ + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + 'with_string_id' => ['type' => 'entity:with_string_id'], + ], + '_layout_builder' => TRUE, + ] + ), + 'layout_builder.overrides.with_string_id.save' => new Route( + '/entity/{entity}/layout/save', + [ + 'entity_type_id' => 'with_string_id', + 'section_storage_type' => 'overrides', + 'section_storage' => '', + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', + ], + [ + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + 'with_string_id' => ['type' => 'entity:with_string_id'], + ], + '_layout_builder' => TRUE, + ] + ), + 'layout_builder.overrides.with_string_id.cancel' => new Route( + '/entity/{entity}/layout/cancel', + [ + 'entity_type_id' => 'with_string_id', + 'section_storage_type' => 'overrides', + 'section_storage' => '', + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', + ], + [ + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + 'with_string_id' => ['type' => 'entity:with_string_id'], + ], + '_layout_builder' => TRUE, + ] + ), + 'layout_builder.overrides.with_string_id.revert' => new Route( + '/entity/{entity}/layout/revert', + [ + 'entity_type_id' => 'with_string_id', + 'section_storage_type' => 'overrides', + 'section_storage' => '', + '_form' => '\Drupal\layout_builder\Form\RevertOverridesForm', + ], + [ + '_has_layout_section' => 'true', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + 'with_string_id' => ['type' => 'entity:with_string_id'], + ], + '_layout_builder' => TRUE, + ] + ), + 'layout_builder.overrides.with_integer_id.view' => new Route( + '/entity/{entity}/layout', + [ + 'entity_type_id' => 'with_integer_id', + 'section_storage_type' => 'overrides', + 'section_storage' => '', + 'is_rebuilding' => FALSE, + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', + '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', + ], + [ + '_has_layout_section' => 'true', + 'with_integer_id' => '\d+', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + 'with_integer_id' => ['type' => 'entity:with_integer_id'], + ], + '_layout_builder' => TRUE, + ] + ), + 'layout_builder.overrides.with_integer_id.save' => new Route( + '/entity/{entity}/layout/save', + [ + 'entity_type_id' => 'with_integer_id', + 'section_storage_type' => 'overrides', + 'section_storage' => '', + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', + ], + [ + '_has_layout_section' => 'true', + 'with_integer_id' => '\d+', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + 'with_integer_id' => ['type' => 'entity:with_integer_id'], + ], + '_layout_builder' => TRUE, + ] + ), + 'layout_builder.overrides.with_integer_id.cancel' => new Route( + '/entity/{entity}/layout/cancel', + [ + 'entity_type_id' => 'with_integer_id', + 'section_storage_type' => 'overrides', + 'section_storage' => '', + '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', + ], + [ + '_has_layout_section' => 'true', + 'with_integer_id' => '\d+', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + 'with_integer_id' => ['type' => 'entity:with_integer_id'], + ], + '_layout_builder' => TRUE, + ] + ), + 'layout_builder.overrides.with_integer_id.revert' => new Route( + '/entity/{entity}/layout/revert', + [ + 'entity_type_id' => 'with_integer_id', + 'section_storage_type' => 'overrides', + 'section_storage' => '', + '_form' => '\Drupal\layout_builder\Form\RevertOverridesForm', + ], + [ + '_has_layout_section' => 'true', + 'with_integer_id' => '\d+', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + 'with_integer_id' => ['type' => 'entity:with_integer_id'], + ], + '_layout_builder' => TRUE, + ] + ), + ]; + + $collection = new RouteCollection(); + $this->plugin->buildRoutes($collection); + $this->assertEquals($expected, $collection->all()); + $this->assertSame(array_keys($expected), array_keys($collection->all())); + } + +} diff --git a/core/modules/layout_builder/tests/src/Unit/SectionStorageDefaultsParamConverterTest.php b/core/modules/layout_builder/tests/src/Unit/SectionStorageDefaultsParamConverterTest.php deleted file mode 100644 index 8afde9a05558e91c18e0d3be5976666b30d97181..0000000000000000000000000000000000000000 --- a/core/modules/layout_builder/tests/src/Unit/SectionStorageDefaultsParamConverterTest.php +++ /dev/null @@ -1,134 +0,0 @@ -<?php - -namespace Drupal\Tests\layout_builder\Unit; - -use Drupal\Core\Entity\EntityManagerInterface; -use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\Core\Entity\EntityType; -use Drupal\layout_builder\Routing\SectionStorageDefaultsParamConverter; -use Drupal\Tests\UnitTestCase; - -/** - * @coversDefaultClass \Drupal\layout_builder\Routing\SectionStorageDefaultsParamConverter - * - * @group layout_builder - */ -class SectionStorageDefaultsParamConverterTest extends UnitTestCase { - - /** - * The converter. - * - * @var \Drupal\layout_builder\Routing\SectionStorageDefaultsParamConverter - */ - protected $converter; - - /** - * The entity manager. - * - * @var \Drupal\Core\Entity\EntityManagerInterface - */ - protected $entityManager; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - $this->entityManager = $this->prophesize(EntityManagerInterface::class); - $this->converter = new SectionStorageDefaultsParamConverter($this->entityManager->reveal()); - } - - /** - * @covers ::convert - * @covers ::getEntityTypeFromDefaults - * - * @dataProvider providerTestConvert - */ - public function testConvert($success, $expected_entity_id, $value, array $defaults) { - if ($expected_entity_id) { - $entity_storage = $this->prophesize(EntityStorageInterface::class); - $entity_storage->load($expected_entity_id)->willReturn('the_return_value'); - - $this->entityManager->getDefinition('entity_view_display')->willReturn(new EntityType(['id' => 'entity_view_display'])); - $this->entityManager->getStorage('entity_view_display')->willReturn($entity_storage->reveal()); - } - else { - $this->entityManager->getDefinition('entity_view_display')->shouldNotBeCalled(); - $this->entityManager->getStorage('entity_view_display')->shouldNotBeCalled(); - } - - $result = $this->converter->convert($value, [], 'the_parameter_name', $defaults); - if ($success) { - $this->assertEquals('the_return_value', $result); - } - else { - $this->assertNull($result); - } - } - - /** - * Provides data for ::testConvert(). - */ - public function providerTestConvert() { - $data = []; - $data['with value'] = [ - TRUE, - 'some_value', - 'some_value', - [], - ]; - $data['empty value, without bundle'] = [ - TRUE, - 'my_entity_type.bundle_name.default', - '', - [ - 'entity_type_id' => 'my_entity_type', - 'view_mode_name' => 'default', - 'bundle_key' => 'my_bundle', - 'my_bundle' => 'bundle_name', - ], - ]; - $data['empty value, with bundle'] = [ - TRUE, - 'my_entity_type.bundle_name.default', - '', - [ - 'entity_type_id' => 'my_entity_type', - 'view_mode_name' => 'default', - 'bundle' => 'bundle_name', - ], - ]; - $data['without value, empty defaults'] = [ - FALSE, - NULL, - '', - [], - ]; - return $data; - } - - /** - * @covers ::convert - */ - public function testConvertCreate() { - $expected = 'the_return_value'; - $value = 'foo.bar.baz'; - $expected_create_values = [ - 'targetEntityType' => 'foo', - 'bundle' => 'bar', - 'mode' => 'baz', - 'status' => TRUE, - ]; - $entity_storage = $this->prophesize(EntityStorageInterface::class); - $entity_storage->load($value)->willReturn(NULL); - $entity_storage->create($expected_create_values)->willReturn($expected); - - $this->entityManager->getDefinition('entity_view_display')->willReturn(new EntityType(['id' => 'entity_view_display'])); - $this->entityManager->getStorage('entity_view_display')->willReturn($entity_storage->reveal()); - - $result = $this->converter->convert($value, [], 'the_parameter_name', []); - $this->assertSame($expected, $result); - } - -} diff --git a/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php b/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..392cd8939ebed61081d53950a127850c72b133bd --- /dev/null +++ b/core/modules/layout_builder/tests/src/Unit/SectionStorageManagerTest.php @@ -0,0 +1,100 @@ +<?php + +namespace Drupal\Tests\layout_builder\Unit; + +use Drupal\Component\Plugin\Factory\FactoryInterface; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\layout_builder\SectionListInterface; +use Drupal\layout_builder\SectionStorage\SectionStorageManager; +use Drupal\layout_builder\SectionStorageInterface; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\layout_builder\SectionStorage\SectionStorageManager + * + * @group layout_builder + */ +class SectionStorageManagerTest extends UnitTestCase { + + /** + * The section storage manager. + * + * @var \Drupal\layout_builder\SectionStorage\SectionStorageManager + */ + protected $manager; + + /** + * The plugin. + * + * @var \Drupal\layout_builder\SectionStorageInterface + */ + protected $plugin; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $cache = $this->prophesize(CacheBackendInterface::class); + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + $this->manager = new SectionStorageManager(new \ArrayObject(), $cache->reveal(), $module_handler->reveal()); + + $this->plugin = $this->prophesize(SectionStorageInterface::class); + + $factory = $this->prophesize(FactoryInterface::class); + $factory->createInstance('the_plugin_id', [])->willReturn($this->plugin->reveal()); + $reflection_property = new \ReflectionProperty($this->manager, 'factory'); + $reflection_property->setAccessible(TRUE); + $reflection_property->setValue($this->manager, $factory->reveal()); + } + + /** + * @covers ::loadEmpty + */ + public function testLoadEmpty() { + $result = $this->manager->loadEmpty('the_plugin_id'); + $this->assertInstanceOf(SectionStorageInterface::class, $result); + } + + /** + * @covers ::loadFromStorageId + */ + public function testLoadFromStorageId() { + $section_list = $this->prophesize(SectionListInterface::class); + $this->plugin->setSectionList($section_list->reveal())->will(function () { + return $this; + }); + $this->plugin->getSectionListFromId('the_storage_id')->willReturn($section_list->reveal()); + + $result = $this->manager->loadFromStorageId('the_plugin_id', 'the_storage_id'); + $this->assertInstanceOf(SectionStorageInterface::class, $result); + } + + /** + * @covers ::loadFromRoute + */ + public function testLoadFromRoute() { + $section_list = $this->prophesize(SectionListInterface::class); + $this->plugin->extractIdFromRoute('the_value', [], 'the_parameter_name', [])->willReturn('the_storage_id'); + $this->plugin->getSectionListFromId('the_storage_id')->willReturn($section_list->reveal()); + $this->plugin->setSectionList($section_list->reveal())->will(function () { + return $this; + }); + + $result = $this->manager->loadFromRoute('the_plugin_id', 'the_value', [], 'the_parameter_name', []); + $this->assertInstanceOf(SectionStorageInterface::class, $result); + } + + /** + * @covers ::loadFromRoute + */ + public function testLoadFromRouteNull() { + $this->plugin->extractIdFromRoute('the_value', [], 'the_parameter_name', ['_route' => 'the_route_name'])->willReturn(NULL); + + $result = $this->manager->loadFromRoute('the_plugin_id', 'the_value', [], 'the_parameter_name', ['_route' => 'the_route_name']); + $this->assertNull($result); + } + +} diff --git a/core/modules/layout_builder/tests/src/Unit/SectionStorageOverridesParamConverterTest.php b/core/modules/layout_builder/tests/src/Unit/SectionStorageOverridesParamConverterTest.php deleted file mode 100644 index f0c01468e59b1b3578e57e6fb909957b21d434b4..0000000000000000000000000000000000000000 --- a/core/modules/layout_builder/tests/src/Unit/SectionStorageOverridesParamConverterTest.php +++ /dev/null @@ -1,119 +0,0 @@ -<?php - -namespace Drupal\Tests\layout_builder\Unit; - -use Drupal\Core\Entity\EntityManagerInterface; -use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\Core\Entity\EntityType; -use Drupal\Core\Entity\FieldableEntityInterface; -use Drupal\layout_builder\Routing\SectionStorageOverridesParamConverter; -use Drupal\Tests\UnitTestCase; -use Prophecy\Argument; - -/** - * @coversDefaultClass \Drupal\layout_builder\Routing\SectionStorageOverridesParamConverter - * - * @group layout_builder - */ -class SectionStorageOverridesParamConverterTest extends UnitTestCase { - - /** - * The converter. - * - * @var \Drupal\layout_builder\Routing\SectionStorageOverridesParamConverter - */ - protected $converter; - - /** - * The entity manager. - * - * @var \Drupal\Core\Entity\EntityManagerInterface - */ - protected $entityManager; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - $this->entityManager = $this->prophesize(EntityManagerInterface::class); - $this->converter = new SectionStorageOverridesParamConverter($this->entityManager->reveal()); - } - - /** - * @covers ::convert - * @covers ::getEntityTypeFromDefaults - * @covers ::getEntityIdFromDefaults - * - * @dataProvider providerTestConvert - */ - public function testConvert($success, $expected_entity_type_id, $value, array $defaults) { - $defaults['the_parameter_name'] = $value; - - if ($expected_entity_type_id) { - $entity_storage = $this->prophesize(EntityStorageInterface::class); - - $entity_without_layout = $this->prophesize(FieldableEntityInterface::class); - $entity_without_layout->hasField('layout_builder__layout')->willReturn(FALSE); - $entity_without_layout->get('layout_builder__layout')->shouldNotBeCalled(); - $entity_storage->load('entity_without_layout')->willReturn($entity_without_layout->reveal()); - - $entity_with_layout = $this->prophesize(FieldableEntityInterface::class); - $entity_with_layout->hasField('layout_builder__layout')->willReturn(TRUE); - $entity_with_layout->get('layout_builder__layout')->willReturn('the_return_value'); - $entity_storage->load('entity_with_layout')->willReturn($entity_with_layout->reveal()); - - $this->entityManager->getDefinition($expected_entity_type_id)->willReturn(new EntityType(['id' => 'entity_view_display'])); - $this->entityManager->getStorage($expected_entity_type_id)->willReturn($entity_storage->reveal()); - } - else { - $this->entityManager->getDefinition(Argument::any())->shouldNotBeCalled(); - $this->entityManager->getStorage(Argument::any())->shouldNotBeCalled(); - } - - $result = $this->converter->convert($value, [], 'the_parameter_name', $defaults); - if ($success) { - $this->assertEquals('the_return_value', $result); - } - else { - $this->assertNull($result); - } - } - - /** - * Provides data for ::testConvert(). - */ - public function providerTestConvert() { - $data = []; - $data['with value, with layout'] = [ - TRUE, - 'my_entity_type', - 'my_entity_type:entity_with_layout', - [], - ]; - $data['with value, without layout'] = [ - FALSE, - 'my_entity_type', - 'my_entity_type:entity_without_layout', - [], - ]; - $data['empty value, populated defaults'] = [ - TRUE, - 'my_entity_type', - '', - [ - 'entity_type_id' => 'my_entity_type', - 'my_entity_type' => 'entity_with_layout', - ], - ]; - $data['empty value, empty defaults'] = [ - FALSE, - NULL, - '', - [], - ]; - return $data; - } - -}