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