From 282a5f7768aa36031bcf86cae7d98246ac145230 Mon Sep 17 00:00:00 2001
From: Roderik Muit <35514-roderik@users.noreply.drupalcode.org>
Date: Fri, 27 Sep 2024 07:45:52 +0000
Subject: [PATCH] Issue #3475612 by roderik, fago: Config dependencies are not
 properly calculated

---
 src/Entity/EntityCeDisplay.php          | 175 ++++++++++++++++++++++--
 src/Entity/EntityCeDisplayInterface.php |  12 +-
 2 files changed, 174 insertions(+), 13 deletions(-)

diff --git a/src/Entity/EntityCeDisplay.php b/src/Entity/EntityCeDisplay.php
index 2fdd89a..7c77026 100644
--- a/src/Entity/EntityCeDisplay.php
+++ b/src/Entity/EntityCeDisplay.php
@@ -3,8 +3,13 @@
 namespace Drupal\custom_elements\Entity;
 
 use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
 use Drupal\Core\Entity\EntityDisplayBase;
+use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\Query\QueryInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Plugin\DefaultLazyPluginCollection as DefaultLazyPluginCollectionAlias;
 use Drupal\custom_elements\CustomElementGeneratorTrait;
 
@@ -48,9 +53,6 @@ class EntityCeDisplay extends EntityDisplayBase implements EntityCeDisplayInterf
   /**
    * Whether this display is enabled or not.
    *
-   * If the entity (form) display is disabled, we'll fall back to the 'default'
-   * display.
-   *
    * @var bool
    */
   protected $status = TRUE;
@@ -60,21 +62,36 @@ class EntityCeDisplay extends EntityDisplayBase implements EntityCeDisplayInterf
    *
    * @var string
    */
-  protected $customElementName;
+  protected string $customElementName = '';
 
   /**
    * Whether to build using layout (if enabled in appropriate display).
    *
    * @var bool
    */
-  protected $useLayoutBuilder = FALSE;
+  protected bool $useLayoutBuilder = FALSE;
 
   /**
    * Whether to build using processors instead of display components.
    *
    * @var bool
    */
-  protected $forceAutoProcessing = FALSE;
+  protected bool $forceAutoProcessing = FALSE;
+
+  /**
+   * The entity display repository.
+   */
+  protected EntityDisplayRepositoryInterface $entityDisplayRepository;
+
+  /**
+   * The entity field manager.
+   */
+  protected EntityFieldManagerInterface $entityFieldManager;
+
+  /**
+   * The module handler.
+   */
+  protected ModuleHandlerInterface $moduleHandler;
 
   /**
    * {@inheritdoc}
@@ -126,14 +143,14 @@ class EntityCeDisplay extends EntityDisplayBase implements EntityCeDisplayInterf
   /**
    * {@inheritdoc}
    */
-  public function getCustomElementName() {
+  public function getCustomElementName(): string {
     return $this->customElementName;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function setCustomElementName($name) {
+  public function setCustomElementName($name): self {
     $this->set('customElementName', $name);
     return $this;
   }
@@ -261,7 +278,7 @@ class EntityCeDisplay extends EntityDisplayBase implements EntityCeDisplayInterf
     if (!$initialized) {
       // Enable components that are enabled in the regular entity_view_display.
       /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $entity_view_display */
-      $entity_view_display = \Drupal::service('entity_display.repository')
+      $entity_view_display = $this->getEntityDisplayRepository()
         ->getViewDisplay($this->targetEntityType, $this->bundle, $this->originalMode);
       $field_definitions = $this->getFieldDefinitions();
 
@@ -322,7 +339,8 @@ class EntityCeDisplay extends EntityDisplayBase implements EntityCeDisplayInterf
   public function preSave(EntityStorageInterface $storage) {
     // Skip over parent method: we don't have regions. Only sort content.
     ksort($this->content);
-    // Ensure the hidden property is not NULL, parent code requires it.
+    // Accommodate for 'hidden' being NULL in config objects created by
+    // 3.0-alpha versions of this module.
     if (!isset($this->hidden)) {
       $this->hidden = [];
     }
@@ -338,6 +356,101 @@ class EntityCeDisplay extends EntityDisplayBase implements EntityCeDisplayInterf
     return ConfigEntityBase::toArray();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    // Skip over parent method: fields need to be calculated differently.
+    ConfigEntityBase::calculateDependencies();
+
+    // Depend on the bundle.
+    $target_entity_type = $this->entityTypeManager()->getDefinition($this->targetEntityType);
+    $bundle_config_dependency = $target_entity_type->getBundleConfigDependency($this->bundle);
+    $this->addDependency($bundle_config_dependency['type'], $bundle_config_dependency['name']);
+
+    // Depend on fields: names are in 'field_name' properties instead of keys.
+    if ($this->getModuleHandler()->moduleExists('field')) {
+      $fieldnames_as_keys = array_flip(array_filter(array_map(
+        fn($component) => $component['field_name'] ?? NULL,
+        $this->getComponents()
+      )));
+      $field_definitions = $this->getEntityFieldManager()->getFieldDefinitions($this->targetEntityType, $this->bundle);
+      foreach (array_intersect_key($field_definitions, $fieldnames_as_keys) as $field_definition) {
+        if ($field_definition instanceof ConfigEntityInterface && $field_definition->getEntityTypeId() == 'field_config') {
+          $this->addDependency('config', $field_definition->getConfigDependencyName());
+        }
+      }
+    }
+
+    // Depend on configured modes.
+    if ($this->mode != 'default') {
+      $mode_entity = $this->entityTypeManager()->getStorage('entity_' . $this->displayContext . '_mode')->load($target_entity_type->id() . '.' . $this->mode);
+      $this->addDependency('config', $mode_entity->getConfigDependencyName());
+    }
+
+    // Depend on related entity view displays.
+    foreach ($this->getConfigDependencyEntityViewDisplays() as $display) {
+      $this->addDependency('config', $display->getConfigDependencyName());
+    }
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfigDependencyEntityViewDisplays(): array {
+    $displays = [];
+    if ($this->getUseLayoutBuilder()) {
+      // Building a custom element depends on the "use layout builder" setting
+      // in the corresponding entity view display. To guarantee consistent
+      // output, define it as a dependency.
+      if ($this->mode !== 'default') {
+        // It's either the display with the same view mode, or the default.
+        // entityDisplayRepository::>getViewDisplay() cannot check if a display
+        // actually exists. Do the loading by ourselves.
+        $displays = $this->entityTypeManager()
+          ->getStorage('entity_view_display')
+          ->loadMultiple([
+            $this->targetEntityType . '.' . $this->bundle . '.' . $this->mode,
+            $this->targetEntityType . '.' . $this->bundle . '.default',
+          ]);
+        // Disabled displays are not dependencies. (If no displays are enabled,
+        // Core auto-generates the default ones, so we have 0 dependencies.)
+        $displays = array_filter(
+          $displays,
+          fn($display) => $display->status()
+        );
+        if (count($displays) > 1) {
+          unset($displays[$this->targetEntityType . '.' . $this->bundle . '.default']);
+        }
+      }
+      else {
+        // This entity is used for all view modes that have no own CE display,
+        // so all corresponding entity view displays are also dependencies, if
+        // enabled.
+        $query = $this->getEntityQuery('entity_view_display')
+          ->condition('id', $this->targetEntityType . '.' . $this->bundle . '.', 'STARTS_WITH')
+          ->condition('status', TRUE)
+          ->condition('id', $this->targetEntityType . '.' . $this->bundle . '.default', '<>');
+        $other_active_display_ids = $query->execute();
+        if ($other_active_display_ids) {
+          // Filter out view modes with their own CE display.
+          $active_ce_display_ids = $this->getEntityQuery('entity_ce_display')
+            ->condition('id', $this->targetEntityType . '.' . $this->bundle . '.', 'STARTS_WITH')
+            ->condition('status', TRUE)
+            ->execute();
+          $other_active_display_ids = array_diff($other_active_display_ids, $active_ce_display_ids);
+          if ($other_active_display_ids) {
+            $storage = $this->entityTypeManager()->getStorage('entity_view_display');
+            $displays += $storage->loadMultiple($other_active_display_ids);
+          }
+        }
+      }
+    }
+    return $displays;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -345,10 +458,50 @@ class EntityCeDisplay extends EntityDisplayBase implements EntityCeDisplayInterf
     // Override parent method: do not filter field definitions, all fields'
     // display is configurable.
     if (!isset($this->fieldDefinitions)) {
-      $this->fieldDefinitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($this->targetEntityType, $this->bundle);
+      $this->fieldDefinitions = $this->getEntityFieldManager()->getFieldDefinitions($this->targetEntityType, $this->bundle);
     }
 
     return $this->fieldDefinitions;
   }
 
+  /**
+   * Gets an entity query.
+   *
+   * @param string $entity_type
+   *   The entity type for which the query object should be returned.
+   */
+  protected function getEntityQuery($entity_type): QueryInterface {
+    return $this->entityTypeManager()->getStorage($entity_type)->getQuery();
+  }
+
+  /**
+   * Gets the entity field manager.
+   */
+  protected function getEntityFieldManager(): EntityFieldManagerInterface {
+    if (!isset($this->entityFieldManager)) {
+      $this->entityFieldManager = \Drupal::service('entity_field.manager');
+    }
+    return $this->entityFieldManager;
+  }
+
+  /**
+   * Gets the entity display repository.
+   */
+  protected function getEntityDisplayRepository(): EntityDisplayRepositoryInterface {
+    if (!isset($this->entityDisplayRepository)) {
+      $this->entityDisplayRepository = \Drupal::service('entity_display.repository');
+    }
+    return $this->entityDisplayRepository;
+  }
+
+  /**
+   * Gets the module handler.
+   */
+  protected function getModuleHandler(): ModuleHandlerInterface {
+    if (!isset($this->moduleHandler)) {
+      $this->moduleHandler = \Drupal::moduleHandler();
+    }
+    return $this->moduleHandler;
+  }
+
 }
diff --git a/src/Entity/EntityCeDisplayInterface.php b/src/Entity/EntityCeDisplayInterface.php
index e4b3a85..a6dd6df 100644
--- a/src/Entity/EntityCeDisplayInterface.php
+++ b/src/Entity/EntityCeDisplayInterface.php
@@ -61,7 +61,7 @@ interface EntityCeDisplayInterface extends EntityDisplayInterface {
    * @return string
    *   The entity type id.
    */
-  public function getCustomElementName();
+  public function getCustomElementName(): string;
 
   /**
    * Sets the custom element name to be displayed.
@@ -71,7 +71,7 @@ interface EntityCeDisplayInterface extends EntityDisplayInterface {
    *
    * @return $this
    */
-  public function setCustomElementName(string $name);
+  public function setCustomElementName(string $name): self;
 
   /**
    * {@inheritDoc}
@@ -81,6 +81,14 @@ interface EntityCeDisplayInterface extends EntityDisplayInterface {
    */
   public function getRenderer($field_name);
 
+  /**
+   * Gets the entity view displays that are this entity's config dependencies.
+   *
+   * @return \Drupal\Core\Entity\Display\EntityDisplayInterface[]
+   *   Entity views displays which this CE display depends on.
+   */
+  public function getConfigDependencyEntityViewDisplays(): array;
+
   /**
    * Sets the originally requested view mode, when building a CE display.
    */
-- 
GitLab