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