diff core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php @ 5:12f9dff5fda9 tip

Update to Drupal core 8.7.1
author Chris Cannam
date Thu, 09 May 2019 15:34:47 +0100
parents a9cd425dd02b
children
line wrap: on
line diff
--- a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php	Thu Feb 28 13:11:55 2019 +0000
+++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php	Thu May 09 15:34:47 2019 +0100
@@ -2,29 +2,34 @@
 
 namespace Drupal\layout_builder\Entity;
 
+use Drupal\Component\Plugin\ConfigurableInterface;
+use Drupal\Component\Plugin\DerivativeInspectionInterface;
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\Cache\CacheableMetadata;
 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\Plugin\Context\EntityContext;
+use Drupal\Core\Render\Element;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\layout_builder\LayoutEntityHelperTrait;
 use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
+use Drupal\layout_builder\QuickEditIntegration;
 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.
- *
- * @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 LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements LayoutEntityDisplayInterface {
 
   use SectionStorageTrait;
+  use LayoutEntityHelperTrait;
 
   /**
    * The entity field manager.
@@ -48,7 +53,7 @@
    * {@inheritdoc}
    */
   public function isOverridable() {
-    return $this->getThirdPartySetting('layout_builder', 'allow_custom', FALSE);
+    return $this->isLayoutBuilderEnabled() && $this->getThirdPartySetting('layout_builder', 'allow_custom', FALSE);
   }
 
   /**
@@ -56,6 +61,10 @@
    */
   public function setOverridable($overridable = TRUE) {
     $this->setThirdPartySetting('layout_builder', 'allow_custom', $overridable);
+    // Enable Layout Builder if it's not already enabled and overriding.
+    if ($overridable && !$this->isLayoutBuilderEnabled()) {
+      $this->enableLayoutBuilder();
+    }
     return $this;
   }
 
@@ -63,6 +72,12 @@
    * {@inheritdoc}
    */
   public function isLayoutBuilderEnabled() {
+    // To prevent infinite recursion, Layout Builder must not be enabled for the
+    // '_custom' view mode that is used for on-the-fly rendering of fields in
+    // isolation from the entity.
+    if ($this->getOriginalMode() === static::CUSTOM_MODE) {
+      return FALSE;
+    }
     return (bool) $this->getThirdPartySetting('layout_builder', 'enabled');
   }
 
@@ -94,7 +109,14 @@
    * {@inheritdoc}
    */
   protected function setSections(array $sections) {
-    $this->setThirdPartySetting('layout_builder', 'sections', array_values($sections));
+    // Third-party settings must be completely unset instead of stored as an
+    // empty array.
+    if (!$sections) {
+      $this->unsetThirdPartySetting('layout_builder', 'sections');
+    }
+    else {
+      $this->setThirdPartySetting('layout_builder', 'sections', array_values($sections));
+    }
     return $this;
   }
 
@@ -133,9 +155,7 @@
       }
       else {
         // When being disabled, remove all existing section data.
-        while (count($this) > 0) {
-          $this->removeSection(0);
-        }
+        $this->removeAllSections();
       }
     }
   }
@@ -186,6 +206,7 @@
           'type' => 'layout_section',
           'locked' => TRUE,
         ]);
+        $field_storage->setTranslatable(FALSE);
         $field_storage->save();
       }
 
@@ -194,6 +215,7 @@
         'bundle' => $bundle,
         'label' => t('Layout'),
       ]);
+      $field->setTranslatable(FALSE);
       $field->save();
     }
   }
@@ -234,31 +256,20 @@
    */
   public function buildMultiple(array $entities) {
     $build_list = parent::buildMultiple($entities);
-    if (!$this->isLayoutBuilderEnabled()) {
-      return $build_list;
-    }
 
-    /** @var \Drupal\Core\Entity\EntityInterface $entity */
     foreach ($entities as $id => $entity) {
-      $sections = $this->getRuntimeSections($entity);
-      if ($sections) {
+      $build_list[$id]['_layout_builder'] = $this->buildSections($entity);
+
+      // If there are any sections, remove all fields with configurable display
+      // from the existing build. These fields are replicated within sections as
+      // field blocks by ::setComponent().
+      if (!Element::isEmpty($build_list[$id]['_layout_builder'])) {
         foreach ($build_list[$id] as $name => $build_part) {
           $field_definition = $this->getFieldDefinition($name);
           if ($field_definition && $field_definition->isDisplayConfigurable($this->displayContext)) {
             unset($build_list[$id][$name]);
           }
         }
-
-        // Bypass ::getContexts() in order to use the runtime entity, not a
-        // sample entity.
-        $contexts = $this->contextRepository()->getAvailableContexts();
-        $label = new TranslatableMarkup('@entity being viewed', [
-          '@entity' => $entity->getEntityType()->getSingularLabel(),
-        ]);
-        $contexts['layout_builder.entity'] = EntityContext::fromEntity($entity, $label);
-        foreach ($sections as $delta => $section) {
-          $build_list[$id]['_layout_builder'][$delta] = $section->toRenderArray($contexts);
-        }
       }
     }
 
@@ -266,6 +277,56 @@
   }
 
   /**
+   * Builds the render array for the sections of a given entity.
+   *
+   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
+   *   The entity.
+   *
+   * @return array
+   *   The render array representing the sections of the entity.
+   */
+  protected function buildSections(FieldableEntityInterface $entity) {
+    $contexts = $this->getContextsForEntity($entity);
+    // @todo Remove in https://www.drupal.org/project/drupal/issues/3018782.
+    $label = new TranslatableMarkup('@entity being viewed', [
+      '@entity' => $entity->getEntityType()->getSingularLabel(),
+    ]);
+    $contexts['layout_builder.entity'] = EntityContext::fromEntity($entity, $label);
+
+    $cacheability = new CacheableMetadata();
+    $storage = $this->sectionStorageManager()->findByContext($contexts, $cacheability);
+
+    $build = [];
+    if ($storage) {
+      foreach ($storage->getSections() as $delta => $section) {
+        $build[$delta] = $section->toRenderArray($contexts);
+      }
+    }
+    // The render array is built based on decisions made by @SectionStorage
+    // plugins and therefore it needs to depend on the accumulated
+    // cacheability of those decisions.
+    $cacheability->applyTo($build);
+    return $build;
+  }
+
+  /**
+   * Gets the available contexts for a given entity.
+   *
+   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
+   *   The entity.
+   *
+   * @return \Drupal\Core\Plugin\Context\ContextInterface[]
+   *   An array of context objects for a given entity.
+   */
+  protected function getContextsForEntity(FieldableEntityInterface $entity) {
+    return [
+      'view_mode' => new Context(ContextDefinition::create('string'), $this->getMode()),
+      'entity' => EntityContext::fromEntity($entity),
+      'display' => EntityContext::fromEntity($this),
+    ] + $this->contextRepository()->getAvailableContexts();
+  }
+
+  /**
    * Gets the runtime sections for a given entity.
    *
    * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
@@ -273,13 +334,20 @@
    *
    * @return \Drupal\layout_builder\Section[]
    *   The sections.
+   *
+   * @deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0.
+   *   \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::findByContext()
+   *   should be used instead. See https://www.drupal.org/node/3022574.
    */
   protected function getRuntimeSections(FieldableEntityInterface $entity) {
-    if ($this->isOverridable() && !$entity->get(OverridesSectionStorage::FIELD_NAME)->isEmpty()) {
-      return $entity->get(OverridesSectionStorage::FIELD_NAME)->getSections();
-    }
-
-    return $this->getSections();
+    @trigger_error('\Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::getRuntimeSections() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::findByContext() should be used instead. See https://www.drupal.org/node/3022574.', E_USER_DEPRECATED);
+    // For backwards compatibility, mimic the functionality of ::buildSections()
+    // by constructing a cacheable metadata object and retrieving the
+    // entity-based contexts.
+    $cacheability = new CacheableMetadata();
+    $contexts = $this->getContextsForEntity($entity);
+    $storage = $this->sectionStorageManager()->findByContext($contexts, $cacheability);
+    return $storage ? $storage->getSections() : [];
   }
 
   /**
@@ -399,4 +467,94 @@
     return $this->getSection(0);
   }
 
+  /**
+   * Gets the section storage manager.
+   *
+   * @return \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
+   *   The section storage manager.
+   */
+  private function sectionStorageManager() {
+    return \Drupal::service('plugin.manager.layout_builder.section_storage');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getComponent($name) {
+    if ($this->isLayoutBuilderEnabled() && $section_component = $this->getQuickEditSectionComponent() ?: $this->getSectionComponentForFieldName($name)) {
+      $plugin = $section_component->getPlugin();
+      if ($plugin instanceof ConfigurableInterface) {
+        $configuration = $plugin->getConfiguration();
+        if (isset($configuration['formatter'])) {
+          return $configuration['formatter'];
+        }
+      }
+    }
+    return parent::getComponent($name);
+  }
+
+  /**
+   * Returns the Quick Edit formatter settings.
+   *
+   * @return \Drupal\layout_builder\SectionComponent|null
+   *   The section component if it is available.
+   *
+   * @see \Drupal\layout_builder\QuickEditIntegration::entityViewAlter()
+   * @see \Drupal\quickedit\MetadataGenerator::generateFieldMetadata()
+   */
+  private function getQuickEditSectionComponent() {
+    // To determine the Quick Edit view_mode ID we need an originalMode set.
+    if ($original_mode = $this->getOriginalMode()) {
+      $parts = explode('-', $original_mode);
+      // The Quick Edit view mode ID is created by
+      // \Drupal\layout_builder\QuickEditIntegration::entityViewAlter()
+      // concatenating together the information we need to retrieve the Layout
+      // Builder component. It follows the structure prescribed by the
+      // documentation of hook_quickedit_render_field().
+      if (count($parts) === 6 && $parts[0] === 'layout_builder') {
+        list(, $delta, $component_uuid, $entity_id) = QuickEditIntegration::deconstructViewModeId($original_mode);
+        $entity = $this->entityTypeManager()->getStorage($this->getTargetEntityTypeId())->load($entity_id);
+        $sections = $this->getEntitySections($entity);
+        if (isset($sections[$delta])) {
+          $component = $sections[$delta]->getComponent($component_uuid);
+          $plugin = $component->getPlugin();
+          // We only care about FieldBlock because these are only components
+          // that provide Quick Edit integration: Quick Edit enables in-place
+          // editing of fields of entities, not of anything else.
+          if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'field_block') {
+            return $component;
+          }
+        }
+      }
+    }
+    return NULL;
+  }
+
+  /**
+   * Gets the component for a given field name if any.
+   *
+   * @param string $field_name
+   *   The field name.
+   *
+   * @return \Drupal\layout_builder\SectionComponent|null
+   *   The section component if it is available.
+   */
+  private function getSectionComponentForFieldName($field_name) {
+    // Loop through every component until the first match is found.
+    foreach ($this->getSections() as $section) {
+      foreach ($section->getComponents() as $component) {
+        $plugin = $component->getPlugin();
+        if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'field_block') {
+          // FieldBlock derivative IDs are in the format
+          // [entity_type]:[bundle]:[field].
+          list(, , $field_block_field_name) = explode(PluginBase::DERIVATIVE_SEPARATOR, $plugin->getDerivativeId());
+          if ($field_block_field_name === $field_name) {
+            return $component;
+          }
+        }
+      }
+    }
+    return NULL;
+  }
+
 }