Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 154
0.00% covered (danger)
0.00%
0 / 78
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
EntityViewOverride
0.00% covered (danger)
0.00%
0 / 150
0.00% covered (danger)
0.00%
0 / 78
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 22
2352
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 9
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
 getContextRequirement
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
 getBuilderUrl
0.00% covered (danger)
0.00%
0 / 8
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
 checkInstanceId
0.00% covered (danger)
0.00%
0 / 8
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
 getUrlFromInstanceId
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 getDisplayUrlFromInstanceId
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
 getProfile
0.00% covered (danger)
0.00%
0 / 2
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
 getSources
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
 saveSources
0.00% covered (danger)
0.00%
0 / 7
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
 checkAccess
0.00% covered (danger)
0.00%
0 / 5
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
 getInstanceId
0.00% covered (danger)
0.00%
0 / 10
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
 setRevision
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 collectInstances
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getInitializationMessage
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 getInitialSources
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 getInitialContext
0.00% covered (danger)
0.00%
0 / 10
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
 collectInstancesByField
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 time
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
 entityTypeManager
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
 displayBuildableManager
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
 dataConverter
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
 getEntityViewDisplay
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder_entity_view\Plugin\display_builder\Buildable;
6
7use Drupal\Component\Datetime\TimeInterface;
8use Drupal\Core\Access\AccessResult;
9use Drupal\Core\Access\AccessResultInterface;
10use Drupal\Core\Entity\ContentEntityInterface;
11use Drupal\Core\Entity\EntityStorageInterface;
12use Drupal\Core\Entity\EntityTypeManagerInterface;
13use Drupal\Core\Entity\Query\QueryInterface;
14use Drupal\Core\Entity\RevisionableEntityBundleInterface;
15use Drupal\Core\Entity\RevisionLogInterface;
16use Drupal\Core\Field\FieldItemListInterface;
17use Drupal\Core\Plugin\Context\Context;
18use Drupal\Core\Plugin\Context\ContextDefinition;
19use Drupal\Core\Plugin\Context\EntityContext;
20use Drupal\Core\Session\AccountInterface;
21use Drupal\Core\StringTranslation\TranslatableMarkup;
22use Drupal\Core\Url;
23use Drupal\display_builder\Attribute\DisplayBuildable;
24use Drupal\display_builder\DisplayBuildableInterface;
25use Drupal\display_builder\DisplayBuildablePluginBase;
26use Drupal\display_builder\DisplayBuildablePluginManager;
27use Drupal\display_builder\ProfileInterface;
28use Drupal\display_builder_entity_view\BuilderDataConverter;
29use Drupal\display_builder_entity_view\Entity\DisplayBuilderEntityDisplayInterface;
30use Drupal\display_builder_entity_view\Entity\DisplayBuilderOverridableInterface;
31use Drupal\ui_patterns\Plugin\Context\RequirementsContext;
32
33/**
34 * Plugin implementation of the display_buildable.
35 */
36#[DisplayBuildable(
37  id: 'entity_view_override',
38  label: new TranslatableMarkup('Entity view override'),
39  instance_prefix: 'entity_override__',
40)]
41final class EntityViewOverride extends DisplayBuildablePluginBase {
42
43  /**
44   * The time service.
45   */
46  protected ?TimeInterface $time;
47
48  /**
49   * The data converter from Manage Display and Layout Builder.
50   */
51  protected BuilderDataConverter $dataConverter;
52
53  /**
54   * The field items where the override is stored.
55   */
56  private FieldItemListInterface $field;
57
58  /**
59   * The display buildable plugin manager.
60   */
61  private DisplayBuildablePluginManager $displayBuildableManager;
62
63  /**
64   * The overridden display.
65   */
66  private DisplayBuilderEntityDisplayInterface $display;
67
68  /**
69   * {@inheritdoc}
70   */
71  public function __construct(array $configuration, $plugin_id, $plugin_definition) {
72    parent::__construct($configuration, $plugin_id, $plugin_definition);
73    $this->field = $configuration['field'];
74    \assert(\is_string($this->field->getName()));
75    $entity = $this->field->getEntity();
76    $this->display = self::getEntityViewDisplay(
77      $entity->getEntityTypeId(),
78      $entity->bundle(),
79      $this->field->getName(),
80    );
81  }
82
83  /**
84   * {@inheritdoc}
85   */
86  public static function getContextRequirement(): string {
87    return 'content';
88  }
89
90  /**
91   * {@inheritdoc}
92   */
93  public function getBuilderUrl(): Url {
94    \assert(\is_string($this->field->getName()));
95    $entity = $this->field->getEntity();
96    $entity_type_id = $this->display->getTargetEntityTypeId();
97    $parameters = [
98      $entity_type_id => $entity->id(),
99      'view_mode_name' => $this->display->getMode(),
100    ];
101
102    return Url::fromRoute(\sprintf('entity.%s.display_builder.%s', $entity_type_id, $this->display->getMode()), $parameters);
103  }
104
105  /**
106   * {@inheritdoc}
107   */
108  public static function checkInstanceId(string $instance_id): ?array {
109    if (!\str_starts_with($instance_id, self::getPrefix())) {
110      return NULL;
111    }
112    [, $entity_type_id, $entity_id, $field_name] = \explode('__', $instance_id);
113
114    return [
115      'entity_type_id' => $entity_type_id,
116      'entity_id' => $entity_id,
117      'field_name' => $field_name,
118    ];
119  }
120
121  /**
122   * {@inheritdoc}
123   */
124  public static function getUrlFromInstanceId(string $instance_id): Url {
125    [, $entity_type_id, $entity_id, $field_name] = \explode('__', $instance_id);
126
127    $entity = \Drupal::entityTypeManager()->getStorage($entity_type_id)->load($entity_id);
128
129    if (!$entity) {
130      return Url::fromRoute('entity.display_builder_instance.collection');
131    }
132
133    $display = self::getEntityViewDisplay($entity_type_id, $entity->bundle(), $field_name);
134
135    if (!$display) {
136      return Url::fromRoute('entity.display_builder_instance.collection');
137    }
138    $params = [
139      $entity_type_id => $entity_id,
140      'view_mode_name' => $display->getMode(),
141    ];
142
143    $route_name = \sprintf('entity.%s.display_builder.%s', $entity_type_id, $display->getMode());
144
145    return Url::fromRoute($route_name, $params);
146  }
147
148  /**
149   * {@inheritdoc}
150   */
151  public static function getDisplayUrlFromInstanceId(string $instance_id): Url {
152    return Url::fromRoute('<front>');
153  }
154
155  /**
156   * {@inheritdoc}
157   */
158  public function getProfile(): ?ProfileInterface {
159    \assert($this->display instanceof DisplayBuilderOverridableInterface);
160
161    return $this->display->getDisplayBuilderOverrideProfile();
162  }
163
164  /**
165   * {@inheritdoc}
166   */
167  public function getSources(): array {
168    return $this->field->getValue();
169  }
170
171  /**
172   * {@inheritdoc}
173   */
174  public function saveSources(): void {
175    $data = $this->getInstance()->getCurrentState();
176    $entity = $this->field->getEntity();
177
178    if ($entity instanceof ContentEntityInterface) {
179      $this->setRevision($entity);
180    }
181    $entity->save();
182    $this->field->setValue($data);
183    $this->field->getEntity()->save();
184  }
185
186  /**
187   * {@inheritdoc}
188   */
189  public static function checkAccess(string $instance_id, AccountInterface $account): AccessResultInterface {
190    [, $entity_type_id, $entity_id] = \explode('__', $instance_id);
191    $entity = \Drupal::entityTypeManager()->getStorage($entity_type_id)->load($entity_id);
192
193    if (!$entity) {
194      return AccessResult::neutral();
195    }
196
197    return $entity->access('update', $account, TRUE);
198  }
199
200  /**
201   * {@inheritdoc}
202   */
203  public function getInstanceId(): ?string {
204    // Usually an entity is new if no ID exists for it yet.
205    if ($this->field->getEntity()->isNew()) {
206      return NULL;
207    }
208
209    $entity = $this->field->getEntity();
210
211    return \sprintf(
212      '%s%s__%s__%s',
213      self::getPrefix(),
214      $entity->getEntityTypeId(),
215      $entity->id(),
216      $this->field->getName()
217    );
218  }
219
220  /**
221   * Set revision if appropriate.
222   *
223   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
224   *   The entity to set revision if appropriate.
225   */
226  public function setRevision(ContentEntityInterface $entity): void {
227    $bundle = $entity->getBundleEntity();
228
229    if ($bundle instanceof RevisionableEntityBundleInterface
230      && !$bundle->shouldCreateNewRevision()
231    ) {
232      return;
233    }
234
235    $entity->setNewRevision();
236
237    if ($entity instanceof RevisionLogInterface) {
238      $entity->setRevisionLogMessage($this->t('Updated using Display Builder.')->render());
239      $entity->setRevisionCreationTime($this->time()->getCurrentTime());
240    }
241  }
242
243  /**
244   * {@inheritdoc}
245   */
246  public static function collectInstances(?EntityTypeManagerInterface $entityTypeManager = NULL): array {
247    $instances = [];
248    $entityTypeManager = \Drupal::service('entity_type.manager');
249    $storage = $entityTypeManager->getStorage('entity_view_display');
250    $instance_storage = $entityTypeManager->getStorage('display_builder_instance');
251    $entity_query = $entity_storage = [];
252
253    foreach ($storage->loadMultiple() as $display) {
254      /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
255      $display_builder = $display->getThirdPartySettings('display_builder');
256
257      if (!isset($display_builder[DisplayBuildableInterface::OVERRIDE_FIELD_PROPERTY], $display_builder[DisplayBuildableInterface::OVERRIDE_PROFILE_PROPERTY])) {
258        continue;
259      }
260
261      $entity_type = $display->getTargetEntityTypeId();
262      $field_name = $display_builder[DisplayBuildableInterface::OVERRIDE_FIELD_PROPERTY] ?? NULL;
263
264      if (!$field_name) {
265        continue;
266      }
267      $entity_storage[$entity_type] ??= $entityTypeManager->getStorage($entity_type);
268      $entity_query[$entity_type] ??= $entity_storage[$entity_type]->getQuery()->accessCheck(FALSE);
269      $instances = \array_merge($instances, self::collectInstancesByField($field_name, $entity_type, $instance_storage, $entity_query[$entity_type]));
270    }
271
272    return $instances;
273  }
274
275  /**
276   * {@inheritdoc}
277   */
278  protected function getInitializationMessage(): TranslatableMarkup {
279    if ($this->initialDataSource === 'display_builder') {
280      return $this->t('Copy from Entity View Display configuration.');
281    }
282
283    if ($this->initialDataSource === 'layout_builder_override') {
284      return $this->t('Import from Layout Builder override.');
285    }
286
287    return $this->t('Initialization from existing content.');
288  }
289
290  /**
291   * {@inheritdoc}
292   */
293  protected function getInitialSources(): array {
294    $sources = $this->getSources();
295
296    // 1. Keep the existing override value if existing.
297    if (\count($sources) > 0) {
298      return $sources;
299    }
300
301    // 2. Convert the Layout Builder Override if exists.
302    // There is always a single Layout Builder override per bundle: `default`.
303    // There could be many Display Builder overrides per bundle, one for each
304    // display, so we need to check.
305    if ($this->display->getMode() === 'default') {
306      $entity = $this->field->getEntity();
307      // @see: use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage::$FIELD_NAME
308      $field_name = 'layout_builder__layout';
309
310      if ($entity->hasField($field_name) && !$entity->get($field_name)->isEmpty()) {
311        $content = $entity->get($field_name)->first()->getValue();
312        $this->initialDataSource = 'layout_builder_override';
313
314        return $this->dataConverter()->convertFromLayoutBuilder($content);
315      }
316    }
317
318    // 3. Copy entity view display value.
319    \assert(\is_string($this->field->getName()));
320    /** @var \Drupal\display_builder\DisplayBuildableInterface $buildable */
321    $buildable = $this->displayBuildableManager()->createInstance('entity_view', ['entity' => $this->display]);
322
323    if ($buildable->getProfile() !== NULL) {
324      $sources = $buildable->getSources();
325    }
326    $this->initialDataSource = 'display_builder';
327
328    return $sources;
329  }
330
331  /**
332   * {@inheritdoc}
333   */
334  protected function getInitialContext(): array {
335    $entity = $this->field->getEntity();
336    $bundle = $entity->bundle();
337    \assert(\is_string($this->field->getName()));
338
339    $view_mode = $this->display->getMode();
340    $contexts = [
341      'entity' => EntityContext::fromEntity($entity),
342      'bundle' => new Context(ContextDefinition::create('string'), $bundle),
343      'view_mode' => new Context(ContextDefinition::create('string'), $view_mode),
344    ];
345
346    return RequirementsContext::addToContext([self::getContextRequirement()], $contexts);
347  }
348
349  /**
350   * Collect instances by field storage.
351   *
352   * @param string $field_name
353   *   Field name.
354   * @param string $entity_type
355   *   Entity type ID.
356   * @param \Drupal\Core\Entity\EntityStorageInterface $instance_storage
357   *   Instance entity storage handler.
358   * @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
359   *   Entity query handler.
360   *
361   * @return array
362   *   A associative array of Instance entities or null values.
363   */
364  protected static function collectInstancesByField(string $field_name, string $entity_type, EntityStorageInterface $instance_storage, QueryInterface $entity_query): array {
365    $instances = [];
366    $entity_query->exists($field_name);
367    // QueryInterface::execute() returns an integer for count queries or an
368    // array of ids.
369    /** @var array $ids */
370    $ids = $entity_query->execute();
371
372    if (empty($ids)) {
373      return [];
374    }
375
376    foreach ($ids as $id) {
377      $instance_id = \sprintf(
378        '%s%s__%s__%s',
379        self::getPrefix(),
380        $entity_type,
381        $id,
382        $field_name,
383      );
384      // We are OK with keeping the null values if the instance entity
385      // doesn't exists in storage. So the caller can decide to create
386      // the missing Instance entities.
387      $instances[$instance_id] = $instance_storage->load($instance_id);
388    }
389
390    return $instances;
391  }
392
393  /**
394   * Get the time service.
395   *
396   * @return \Drupal\Component\Datetime\TimeInterface
397   *   The time service.
398   */
399  protected function time(): TimeInterface {
400    return $this->time ??= \Drupal::service('datetime.time');
401  }
402
403  /**
404   * Get the entity type manager.
405   *
406   * @return \Drupal\Core\Entity\EntityTypeManagerInterface
407   *   The entity type manager.
408   */
409  protected function entityTypeManager(): EntityTypeManagerInterface {
410    return $this->entityTypeManager ??= \Drupal::service('entity_type.manager');
411  }
412
413  /**
414   * Gets the display buildable manager.
415   *
416   * @return \Drupal\display_builder\DisplayBuildablePluginManager
417   *   The manager for display buildable.
418   */
419  protected function displayBuildableManager(): DisplayBuildablePluginManager {
420    return $this->displayBuildableManager ??= \Drupal::service('plugin.manager.display_buildable');
421  }
422
423  /**
424   * Get the data converter from Manage Display and Layout Builder.
425   *
426   * @return \Drupal\display_builder_entity_view\BuilderDataConverter
427   *   The converter.
428   */
429  protected function dataConverter(): BuilderDataConverter {
430    return $this->dataConverter ??= \Drupal::service('display_builder_entity_view.builder_data_converter');
431  }
432
433  /**
434   * Get entity view display entity.
435   *
436   * @param string $entity_type_id
437   *   Entity type ID.
438   * @param string $bundle
439   *   Entity's bundle which support fields.
440   * @param string $fieldName
441   *   Field name of the display.
442   *
443   * @return \Drupal\display_builder_entity_view\Entity\DisplayBuilderEntityDisplayInterface|null
444   *   The corresponding entity view display.
445   */
446  private static function getEntityViewDisplay(string $entity_type_id, string $bundle, string $fieldName): ?DisplayBuilderEntityDisplayInterface {
447    /** @var \Drupal\display_builder_entity_view\Entity\DisplayBuilderEntityDisplayInterface[] $displays */
448    $displays = \Drupal::entityTypeManager()->getStorage('entity_view_display')->loadByProperties([
449      'targetEntityType' => $entity_type_id,
450    ]);
451
452    foreach ($displays as $display) {
453      if ($display instanceof DisplayBuilderOverridableInterface
454        && $display->getDisplayBuilderOverrideField() === $fieldName
455        && $display->getTargetEntityTypeId()
456        && $display->getTargetBundle() === $bundle
457      ) {
458        return $display;
459      }
460    }
461
462    return NULL;
463  }
464
465}