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