Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
50.94% covered (warning)
50.94%
54 / 106
53.33% covered (warning)
53.33%
40 / 75
24.29% covered (danger)
24.29%
17 / 70
37.50% covered (danger)
37.50%
6 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
EntityViewDisplayTrait
50.94% covered (warning)
50.94%
54 / 106
53.33% covered (warning)
53.33%
40 / 75
24.29% covered (danger)
24.29%
17 / 70
37.50% covered (danger)
37.50%
6 / 16
845.55
0.00% covered (danger)
0.00%
0 / 1
 calculateDependencies
92.31% covered (success)
92.31%
12 / 13
90.00% covered (success)
90.00%
9 / 10
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
8.12
 isDisplayBuilderEnabled
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 onDependencyRemoval
91.67% covered (success)
91.67%
11 / 12
87.50% covered (warning)
87.50%
7 / 8
20.00% covered (danger)
20.00%
1 / 5
0.00% covered (danger)
0.00%
0 / 1
12.19
 getDisplayBuilderOverrideField
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDisplayBuilderOverrideProfile
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 isDisplayBuilderOverridable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 postSave
61.11% covered (warning)
61.11%
11 / 18
62.50% covered (warning)
62.50%
10 / 16
10.00% covered (danger)
10.00%
3 / 30
0.00% covered (danger)
0.00%
0 / 1
54.66
 delete
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 buildMultiple
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 isOverrideOfCurrentDisplay
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 getContextsForEntity
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 contextRepository
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getInstance
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 displayBuildable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 loadDisplayBuilder
80.00% covered (warning)
80.00%
4 / 5
66.67% covered (warning)
66.67%
2 / 3
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 buildSources
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder_entity_view\Entity;
6
7use Drupal\Core\Cache\CacheableMetadata;
8use Drupal\Core\Entity\EntityStorageInterface;
9use Drupal\Core\Entity\FieldableEntityInterface;
10use Drupal\Core\Plugin\Context\Context;
11use Drupal\Core\Plugin\Context\ContextDefinition;
12use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
13use Drupal\Core\Plugin\Context\EntityContext;
14use Drupal\display_builder\DisplayBuildableInterface;
15use Drupal\display_builder\InstanceInterface;
16use Drupal\display_builder\ProfileInterface;
17use Drupal\display_builder_entity_view\Plugin\display_builder\Buildable\EntityViewOverride;
18
19/**
20 * Common methods for entity view display.
21 */
22trait EntityViewDisplayTrait {
23
24  /**
25   * Calculates dependencies for the display builder.
26   *
27   * @return $this
28   *   The current instance.
29   *
30   * @see \Drupal\Core\Entity\Display\EntityViewDisplayInterface
31   */
32  public function calculateDependencies(): self {
33    parent::calculateDependencies();
34
35    if (!$this->displayBuildable()->getInstanceId()) {
36      // If there is no instance ID, we cannot calculate dependencies.
37      return $this;
38    }
39
40    /** @var \Drupal\display_builder\InstanceInterface $instance */
41    $instance = $this->entityTypeManager->getStorage('display_builder_instance')->load($this->displayBuildable()->getInstanceId());
42
43    if (!$instance) {
44      return $this;
45    }
46    $contexts = $instance->getContexts();
47
48    if (!$contexts) {
49      return $this;
50    }
51
52    foreach ($this->displayBuildable()->getSources() as $source_data) {
53      /** @var \Drupal\ui_patterns\SourceInterface $source */
54      $source = $this->sourcePluginManager->getSource('', [], $source_data, $contexts);
55      $this->addDependencies($source->calculateDependencies());
56    }
57
58    return $this;
59  }
60
61  /**
62   * Is display builder enabled?
63   *
64   * @return bool
65   *   The display builder is enabled if there is a Display Builder entity.
66   *
67   * @see \Drupal\display_builder_entity_view\DisplayBuilderEntityDisplayInterface
68   */
69  public function isDisplayBuilderEnabled(): bool {
70    // Display Builder must not be enabled for the '_custom' view mode that is
71    // used for on-the-fly rendering of fields in isolation from the entity.
72    if ($this->getOriginalMode() === static::CUSTOM_MODE) {
73      return FALSE;
74    }
75
76    return (bool) $this->displayBuildable()->getProfile();
77  }
78
79  /**
80   * Handler for when dependencies are removed.
81   *
82   * @param array $dependencies
83   *   The dependencies that were removed.
84   *
85   * @return bool
86   *   TRUE if the display can be overridden, FALSE otherwise.
87   *
88   * @see \Drupal\Core\Entity\Display\EntityViewDisplayInterface
89   */
90  public function onDependencyRemoval(array $dependencies): bool {
91    $changed = parent::onDependencyRemoval($dependencies);
92
93    // Loop through all sources and determine if the removed dependencies are
94    // used by their plugins.
95    /** @var \Drupal\display_builder\InstanceInterface $instance */
96    $instance = $this->getInstance();
97
98    // @todo not working when content entity type is deleted.
99    if (!$instance) {
100      return TRUE;
101    }
102
103    $contexts = $instance->getContexts();
104
105    foreach ($this->displayBuildable()->getSources() as $source_data) {
106      /** @var \Drupal\ui_patterns\SourceInterface $source */
107      $source = $this->sourcePluginManager->getSource('', [], $source_data, $contexts);
108      $source_dependencies = $source->calculateDependencies();
109      $source_removed_dependencies = $this->getPluginRemovedDependencies($source_dependencies, $dependencies);
110
111      if ($source_removed_dependencies) {
112        // @todo Allow the plugins to react to their dependency removal in
113        // https://www.drupal.org/project/drupal/issues/2579743.
114        // $this->removeSource($delta);
115        $changed = TRUE;
116      }
117    }
118
119    return $changed;
120  }
121
122  /**
123   * Returns the field name used to store overridden displays.
124   *
125   * @return string|null
126   *   The field name used to store overridden displays, or NULL if not set.
127   *
128   * @see \Drupal\display_builder_entity_view\Entity\DisplayBuilderOverridableInterface
129   */
130  public function getDisplayBuilderOverrideField(): ?string {
131    return $this->getThirdPartySetting('display_builder', DisplayBuildableInterface::OVERRIDE_FIELD_PROPERTY);
132  }
133
134  /**
135   * Returns the display builder override profile.
136   *
137   * @return \Drupal\display_builder\ProfileInterface|null
138   *   The display builder override profile, or NULL if not set.
139   *
140   * @see \Drupal\display_builder_entity_view\Entity\DisplayBuilderOverridableInterface
141   */
142  public function getDisplayBuilderOverrideProfile(): ?ProfileInterface {
143    $display_builder_id = $this->getThirdPartySetting('display_builder', DisplayBuildableInterface::OVERRIDE_PROFILE_PROPERTY);
144
145    if ($display_builder_id === NULL) {
146      return NULL;
147    }
148
149    return $this->loadDisplayBuilder($display_builder_id);
150  }
151
152  /**
153   * Returns TRUE if the display can be overridden.
154   *
155   * @return bool
156   *   TRUE if the display can be overridden, FALSE otherwise.
157   *
158   * @see \Drupal\display_builder_entity_view\Entity\DisplayBuilderOverridableInterface
159   */
160  public function isDisplayBuilderOverridable(): bool {
161    return !empty($this->getDisplayBuilderOverrideField())
162      && $this->getDisplayBuilderOverrideProfile() !== NULL;
163  }
164
165  /**
166   * Post-save operations for the display builder.
167   *
168   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
169   *   The entity storage.
170   * @param bool $update
171   *   Whether the entity is being updated.
172   *
173   * @see \Drupal\Core\Entity\Display\EntityViewDisplayInterface
174   */
175  public function postSave(EntityStorageInterface $storage, $update = TRUE): void {
176    if ($profile = $this->displayBuildable()->getProfile()) {
177      $this->displayBuildable()->initInstanceIfMissing();
178
179      // Save the profile in the instance if changed.
180      $instance = $this->getInstance();
181      $profile_id = (string) $profile->id();
182
183      if ($instance && ($instance->getProfile()?->id() !== $profile_id)) {
184        $instance->setProfile($profile_id);
185      }
186      $instance->save();
187    }
188
189    // Do also overrides.
190    if ($profile = $this->getDisplayBuilderOverrideProfile()) {
191      $profile_id = (string) $profile->id();
192      $storage = $this->entityTypeManager->getStorage('display_builder_instance');
193
194      foreach ($storage->loadMultiple() as $override) {
195        /** @var \Drupal\display_builder\InstanceInterface $override */
196        if (!$this->isOverrideOfCurrentDisplay($override)) {
197          continue;
198        }
199
200        if ($override->getProfile()->id() === $profile_id) {
201          continue;
202        }
203        $override->setProfile($profile_id);
204        $override->save();
205      }
206    }
207
208    parent::postSave($storage, $update);
209  }
210
211  /**
212   * Deletes the display builder instance if it exists.
213   *
214   * @see \Drupal\Core\Entity\Display\EntityViewDisplayInterface
215   */
216  public function delete(): void {
217    if ($instance = $this->getInstance()) {
218      $instance->delete();
219    }
220    parent::delete();
221  }
222
223  /**
224   * Builds a renderable array for the components of a set of entities.
225   *
226   * @param \Drupal\Core\Entity\FieldableEntityInterface[] $entities
227   *   The entities being displayed.
228   *
229   * @return array
230   *   A renderable array for the entities, indexed by the same keys as the
231   *   $entities array parameter.
232   *
233   * @see \Drupal\Core\Entity\Display\EntityViewDisplayInterface
234   */
235  public function buildMultiple(array $entities): array {
236    $build_list = parent::buildMultiple($entities);
237
238    // If no display builder enabled, stop here and return:
239    // - 'Manage Display' build if this trait is used in EntityViewDisplay
240    // - 'Layout Builder' build if used in LayoutBuilderEntityViewDisplay.
241    if (!$this->isDisplayBuilderEnabled()) {
242      // This is also preventing the availability of Display Builder overrides
243      // when Display Builder is not used for the entity view display.
244      // @todo Is it something we want to keep like that?
245      // @see https://www.drupal.org/project/display_builder/issues/3540048
246      return $build_list;
247    }
248
249    foreach ($entities as $id => $entity) {
250      $sources = [];
251
252      if ($this->isDisplayBuilderOverridable()) {
253        $display_builder_field = $this->getDisplayBuilderOverrideField();
254        $overridden_field = $entity->get($display_builder_field);
255        /** @var \Drupal\display_builder\DisplayBuildableInterface $buildable */
256        $buildable = $this->displayBuildableManager->createInstance('entity_view_override', ['field' => $overridden_field]);
257        $sources = $buildable->getSources();
258      }
259
260      // If the overridden field is empty fallback to the entity view.
261      if (\count($sources) === 0) {
262        $sources = $this->displayBuildable()->getSources();
263      }
264
265      // We clear the display because we only want our renderable.
266      $build_list[$id] = [];
267      // @see entity.html.twig
268      $build_list[$id]['content'] = $this->buildSources($entity, $sources);
269    }
270
271    return $build_list;
272  }
273
274  /**
275   * Check if the instance is overriding this display.
276   *
277   * @param \Drupal\display_builder\InstanceInterface $instance
278   *   A list of display builder instances.
279   *
280   * @return bool
281   *   Is the instance overriding this display?
282   */
283  protected function isOverrideOfCurrentDisplay(InstanceInterface $instance): bool {
284    $parts = EntityViewOverride::checkInstanceId((string) $instance->id());
285
286    if (!$parts) {
287      return FALSE;
288    }
289
290    if ($parts['entity_type_id'] !== $this->getTargetEntityTypeId()) {
291      return FALSE;
292    }
293
294    if ($parts['field_name'] !== $this->getDisplayBuilderOverrideField()) {
295      return FALSE;
296    }
297
298    return TRUE;
299  }
300
301  /**
302   * Gets the available contexts for a given entity.
303   *
304   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
305   *   The entity.
306   *
307   * @return \Drupal\Core\Plugin\Context\ContextInterface[]
308   *   An array of context objects for a given entity.
309   */
310  protected function getContextsForEntity(FieldableEntityInterface $entity): array {
311    $available_context_ids = \array_keys($this->contextRepository()->getAvailableContexts());
312
313    return [
314      'view_mode' => new Context(ContextDefinition::create('string'), $this->getMode()),
315      'entity' => EntityContext::fromEntity($entity),
316      'display' => EntityContext::fromEntity($this),
317    ] + $this->contextRepository()->getRuntimeContexts($available_context_ids);
318  }
319
320  /**
321   * Wraps the context repository service.
322   *
323   * @return \Drupal\Core\Plugin\Context\ContextRepositoryInterface
324   *   The context repository service.
325   */
326  protected function contextRepository(): ContextRepositoryInterface {
327    return \Drupal::service('context.repository');
328  }
329
330  /**
331   * Gets the Display Builder instance.
332   *
333   * @return \Drupal\display_builder\InstanceInterface|null
334   *   A display builder instance.
335   */
336  protected function getInstance(): ?InstanceInterface {
337    $instance_id = $this->displayBuildable()->getInstanceId();
338    /** @var \Drupal\display_builder\InstanceInterface|null $instance */
339    $instance = $this->entityTypeManager->getStorage('display_builder_instance')->load($instance_id);
340    $this->instance = $instance;
341
342    return $this->instance;
343  }
344
345  /**
346   * Gets the display buildable manager.
347   *
348   * @return \Drupal\display_builder\DisplayBuildableInterface
349   *   The manager for display buildable.
350   */
351  protected function displayBuildable(): DisplayBuildableInterface {
352    /** @var \Drupal\display_builder\DisplayBuildableInterface $buildable */
353    $buildable = $this->displayBuildableManager->createInstance('entity_view', ['entity' => $this]);
354
355    return $buildable;
356  }
357
358  /**
359   * Loads display builder by id.
360   *
361   * @param string $display_builder_id
362   *   The display builder ID.
363   *
364   * @return \Drupal\display_builder\ProfileInterface|null
365   *   The display builder, or NULL if not found.
366   */
367  private function loadDisplayBuilder(string $display_builder_id): ?ProfileInterface {
368    if (empty($display_builder_id)) {
369      return NULL;
370    }
371    $storage = $this->entityTypeManager->getStorage('display_builder_profile');
372
373    /** @var \Drupal\display_builder\ProfileInterface $display_builder */
374    $display_builder = $storage->load($display_builder_id);
375
376    return $display_builder;
377  }
378
379  /**
380   * Builds the render array for the sources of a given entity.
381   *
382   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
383   *   The entity.
384   * @param array $sources
385   *   The sources to build.
386   *
387   * @return array
388   *   The render array representing the sources of the entity.
389   */
390  private function buildSources(FieldableEntityInterface $entity, array $sources): array {
391    $contexts = $this->getContextsForEntity($entity);
392    $cacheability = new CacheableMetadata();
393    $fake_build = [];
394
395    foreach ($sources as $source_data) {
396      $fake_build = $this->componentElementBuilder->buildSource($fake_build, 'content', [], $source_data, $contexts);
397    }
398    $build = $fake_build['#slots']['content'] ?? [];
399    $build['#cache'] = $fake_build['#cache'] ?? [];
400    // The render array is built based on decisions made by SourceStorage
401    // plugins, and therefore it needs to depend on the accumulated
402    // cacheability of those decisions.
403    $cacheability->applyTo($build);
404
405    return $build;
406  }
407
408}