Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
52.83% covered (warning)
52.83%
56 / 106
88.24% covered (warning)
88.24%
45 / 51
28.36% covered (danger)
28.36%
19 / 67
61.54% covered (warning)
61.54%
8 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageLayout
88.89% covered (warning)
88.89%
56 / 63
88.24% covered (warning)
88.24%
45 / 51
28.36% covered (danger)
28.36%
19 / 67
61.54% covered (warning)
61.54%
8 / 13
384.36
0.00% covered (danger)
0.00%
0 / 1
 getPluginCollections
100.00% covered (success)
100.00%
3 / 3
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
 getConditions
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
 calculateDependencies
80.00% covered (warning)
80.00%
8 / 10
87.50% covered (warning)
87.50%
7 / 8
25.00% covered (danger)
25.00%
2 / 8
0.00% covered (danger)
0.00%
0 / 1
10.75
 delete
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
3 / 3
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
2.50
 postSave
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
7 / 7
37.50% covered (danger)
37.50%
3 / 8
100.00% covered (success)
100.00%
1 / 1
7.91
 getProfile
83.33% covered (warning)
83.33%
5 / 6
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
 getSources
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
 setSources
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%
7 / 7
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 isImpactingPageVariantDetection
81.82% covered (warning)
81.82%
9 / 11
87.50% covered (warning)
87.50%
14 / 16
8.33% covered (danger)
8.33%
3 / 36
0.00% covered (danger)
0.00%
0 / 1
57.30
 displayBuildable
100.00% covered (success)
100.00%
3 / 3
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
 sourceManager
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
 cacheTagsInvalidator
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
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder_page_layout\Entity;
6
7use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
8use Drupal\Core\Condition\ConditionPluginCollection;
9use Drupal\Core\Config\Entity\ConfigEntityBase;
10use Drupal\Core\Entity\Attribute\ConfigEntityType;
11use Drupal\Core\Entity\EntityDeleteForm;
12use Drupal\Core\Entity\EntityStorageInterface;
13use Drupal\Core\StringTranslation\TranslatableMarkup;
14use Drupal\display_builder\DisplayBuildableInterface;
15use Drupal\display_builder\Entity\ProfileInterface;
16use Drupal\display_builder\InstanceInterface;
17use Drupal\display_builder_page_layout\AccessControlHandler;
18use Drupal\display_builder_page_layout\Form\PageLayoutForm;
19use Drupal\display_builder_page_layout\PageLayoutInterface;
20use Drupal\display_builder_page_layout\PageLayoutListBuilder;
21use Drupal\ui_patterns\SourcePluginManager;
22
23/**
24 * Defines the page layout entity type.
25 */
26#[ConfigEntityType(
27  id: 'page_layout',
28  label: new TranslatableMarkup('Page layout'),
29  label_collection: new TranslatableMarkup('Page layouts'),
30  label_singular: new TranslatableMarkup('page layout'),
31  label_plural: new TranslatableMarkup('page layouts'),
32  config_prefix: 'page_layout',
33  entity_keys: [
34    'id' => 'id',
35    'label' => 'label',
36    'weight' => 'weight',
37    'uuid' => 'uuid',
38  ],
39  handlers: [
40    'access' => AccessControlHandler::class,
41    'list_builder' => PageLayoutListBuilder::class,
42    'form' => [
43      'add' => PageLayoutForm::class,
44      'edit' => PageLayoutForm::class,
45      'delete' => EntityDeleteForm::class,
46    ],
47  ],
48  links: [
49    'collection' => '/admin/structure/page-layout',
50    'add-form' => '/admin/structure/page-layout/add',
51    'edit-form' => '/admin/structure/page-layout/{page_layout}',
52    'display-builder' => '/admin/structure/page-layout/{page_layout}/builder',
53    'delete-form' => '/admin/structure/page-layout/{page_layout}/delete',
54    'duplicate-form' => '/admin/structure/page-layout/{page_layout}/duplicate',
55  ],
56  admin_permission: 'administer page_layout',
57  label_count: [
58    'singular' => '@count page layout',
59    'plural' => '@count page layouts',
60  ],
61  config_export: [
62    'id',
63    'label',
64    'weight',
65    DisplayBuildableInterface::PROFILE_PROPERTY,
66    DisplayBuildableInterface::SOURCES_PROPERTY,
67    'conditions',
68  ],
69)]
70final class PageLayout extends ConfigEntityBase implements PageLayoutInterface {
71
72  /**
73   * The ID of the page layout entity.
74   *
75   * This property's type was changed from `string` to `?string` (nullable)
76   * to support the entity duplication process. The original non-nullable type
77   * would cause a fatal error, as a new, duplicated entity does not have an
78   * ID until it is saved.
79   *
80   * @var string|null
81   *   The unique identifier for the page layout.
82   */
83  protected ?string $id;
84
85  /**
86   * The example label.
87   */
88  protected string $label;
89
90  /**
91   * Weight of this page layout when negotiating the page variant.
92   *
93   * The first/lowest that is accessible according to conditions is loaded.
94   *
95   * @var int
96   */
97  protected $weight = 0;
98
99  /**
100   * Display Builder Profile ID.
101   */
102  protected string $profile = '';
103
104  /**
105   * A list of sources plugins.
106   *
107   * @var array
108   */
109  protected $sources = [];
110
111  /**
112   * Condition settings for storage.
113   *
114   * @var array
115   */
116  protected $conditions = [];
117
118  /**
119   * The loaded display builder instance.
120   */
121  protected ?InstanceInterface $instance;
122
123  /**
124   * The conditions plugins for this page.
125   */
126  protected ConditionPluginCollection $conditionPluginCollection;
127
128  /**
129   * {@inheritdoc}
130   */
131  public function getPluginCollections(): array {
132    return [
133      'conditions' => $this->getConditions(),
134    ];
135  }
136
137  /**
138   * {@inheritdoc}
139   */
140  public function getConditions(): ConditionPluginCollection {
141    if (!isset($this->conditionPluginCollection)) {
142      // Static call because EntityBase and descendants don't support
143      // dependency injection.
144      $manager = \Drupal::service('plugin.manager.condition');
145      $this->conditionPluginCollection = new ConditionPluginCollection($manager, $this->get('conditions'));
146    }
147
148    return $this->conditionPluginCollection;
149  }
150
151  /**
152   * {@inheritdoc}
153   */
154  public function calculateDependencies(): PageLayout {
155    parent::calculateDependencies();
156    $display_builder = $this->getProfile();
157    $instance = $this->getInstance();
158
159    if ($display_builder && $instance) {
160      $this->addDependency('config', $display_builder->getConfigDependencyName());
161      $contexts = $instance->getAvailableContexts() ?? [];
162
163      foreach ($this->getSources() as $source_data) {
164        /** @var \Drupal\ui_patterns\SourceInterface $source */
165        $source = $this->sourceManager()->getSource('', [], $source_data, $contexts);
166        $this->addDependencies($source->calculateDependencies());
167      }
168    }
169
170    return $this;
171  }
172
173  /**
174   * {@inheritdoc}
175   */
176  public function delete(): void {
177    if ($this->getInstance()) {
178      $storage = $this->entityTypeManager()->getStorage('display_builder_instance');
179      $storage->delete([$this->getInstance()]);
180    }
181
182    parent::delete();
183  }
184
185  /**
186   * {@inheritdoc}
187   */
188  public function postSave(EntityStorageInterface $storage, $update = TRUE): void {
189    $this->displayBuildable()->initInstanceIfMissing();
190    $instance = $this->getInstance();
191
192    if ($this->isImpactingPageVariantDetection($update)) {
193      // In DisplayBuilderPageVariant we add PageLayout::>getCacheTags() to the
194      // page renderable but it works only for the pages already managed by
195      // Display Builder.
196      // In PageVariantSubscriber::onSelectPageDisplayVariant() we add a custom
197      // tag for the others.
198      /** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type */
199      $entity_type = $this->getEntityType();
200      $this->cacheTagsInvalidator()->invalidateTags([$entity_type->getConfigPrefix()]);
201    }
202
203    // Reset the state once you import a configuration.
204    if ($instance && $this->isSyncing()) {
205      $log = new TranslatableMarkup('Synchronize display from imported configuration');
206      $current_sources = $this->getSources();
207      $instance->setNewPresent($current_sources, $log);
208      $instance->save();
209    }
210    parent::postSave($storage, $update);
211  }
212
213  /**
214   * {@inheritdoc}
215   */
216  public function getProfile(): ?ProfileInterface {
217    $storage = $this->entityTypeManager()->getStorage('display_builder_profile');
218    $profile_id = $this->get(DisplayBuildableInterface::PROFILE_PROPERTY);
219
220    if (!$profile_id) {
221      return NULL;
222    }
223
224    /** @var \Drupal\display_builder\Entity\ProfileInterface $builder */
225    $builder = $storage->load($profile_id);
226
227    return $builder;
228  }
229
230  /**
231   * {@inheritdoc}
232   */
233  public function getSources(): array {
234    return $this->sources;
235  }
236
237  /**
238   * {@inheritdoc}
239   */
240  public function setSources(array $sources): void {
241    $this->sources = $sources;
242  }
243
244  /**
245   * Gets the Display Builder instance.
246   *
247   * @return \Drupal\display_builder\InstanceInterface|null
248   *   A display builder instance.
249   */
250  protected function getInstance(): ?InstanceInterface {
251    if (!$this->displayBuildable()->getInstanceId()) {
252      return NULL;
253    }
254
255    if (!isset($this->instance)) {
256      $instance_id = $this->displayBuildable()->getInstanceId();
257      /** @var \Drupal\display_builder\InstanceInterface|null $instance */
258      $instance = $this->entityTypeManager()->getStorage('display_builder_instance')->load($instance_id);
259      $this->instance = $instance;
260    }
261
262    return $this->instance;
263  }
264
265  /**
266   * Does the page cache need to be flushed?
267   *
268   * Flushing a cache is something to be careful enough. Let's flush only when
269   * needed.
270   *
271   * @param bool $update
272   *   TRUE if the entity has been updated, or FALSE if it has been inserted.
273   *
274   * @return bool
275   *   TRUE if the cache need to be flushed.
276   */
277  private function isImpactingPageVariantDetection(bool $update = TRUE): bool {
278    // A new active page layout has been added.
279    if (!$update && $this->status && !empty($this->sources)) {
280      return TRUE;
281    }
282
283    // Other additions have no impact.
284    if (!$update) {
285      return FALSE;
286    }
287
288    $previous = $this->originalEntity;
289
290    // Those properties are impacting AccessControlHandler logic and
291    // PageVariantSubscriber results.
292    foreach (['weight', 'conditions', 'status'] as $property) {
293      if ($this->get($property) !== $previous->get($property)) {
294        return TRUE;
295      }
296    }
297
298    // A page layout with empty sources is skipped by AccessControlHandler.
299    // This is also altering PageVariantSubscriber results.
300    if (empty($this->sources) !== empty($previous->get('sources'))) {
301      return TRUE;
302    }
303
304    return FALSE;
305  }
306
307  /**
308   * Gets the display buildable manager.
309   *
310   * @return \Drupal\display_builder\DisplayBuildableInterface
311   *   The manager for display buildable.
312   */
313  private function displayBuildable(): DisplayBuildableInterface {
314    /** @var \Drupal\display_builder\DisplayBuildablePluginManager $manager */
315    $manager = \Drupal::service('plugin.manager.display_buildable');
316    /** @var \Drupal\display_builder\DisplayBuildableInterface $buildable */
317    $buildable = $manager->createInstance('page_layout', ['entity' => $this]);
318
319    return $buildable;
320  }
321
322  /**
323   * Gets the UI Patterns Source plugins manager.
324   *
325   * @return \Drupal\ui_patterns\SourcePluginManager
326   *   The manager for source plugins.
327   */
328  private function sourceManager(): SourcePluginManager {
329    return \Drupal::service('plugin.manager.ui_patterns_source');
330  }
331
332  /**
333   * Gets the Cache Tags Invalidator service.
334   *
335   * @return \Drupal\Core\Cache\CacheTagsInvalidatorInterface
336   *   The cache tags invalidator service.
337   */
338  private function cacheTagsInvalidator(): CacheTagsInvalidatorInterface {
339    return \Drupal::service('cache_tags.invalidator');
340  }
341
342}