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}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

PageLayout->calculateDependencies
154    parent::calculateDependencies();
155    $display_builder = $this->getProfile();
156    $instance = $this->getInstance();
157
158    if ($display_builder && $instance) {
158    if ($display_builder && $instance) {
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) {
162      foreach ($this->displayBuildable()->getSources() as $source_data) {
162      foreach ($this->displayBuildable()->getSources() as $source_data) {
163        /** @var \Drupal\ui_patterns\SourceInterface $source */
164        $source = $this->sourceManager()->getSource('', [], $source_data, $contexts);
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;
169    return $this;
170  }
PageLayout->delete
176    if ($this->getInstance()) {
177      $storage = $this->entityTypeManager()->getStorage('display_builder_instance');
178      $storage->delete([$this->getInstance()]);
179    }
180
181    parent::delete();
181    parent::delete();
182  }
PageLayout->displayBuildable
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  }
PageLayout->getConditions
140    if (!isset($this->conditionPluginCollection)) {
143      $manager = \Drupal::service('plugin.manager.condition');
144      $this->conditionPluginCollection = new ConditionPluginCollection($manager, $this->get('conditions'));
145    }
146
147    return $this->conditionPluginCollection;
147    return $this->conditionPluginCollection;
148  }
PageLayout->getInstance
249    if (!$this->displayBuildable()->getInstanceId()) {
250      return NULL;
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;
260    return $this->instance;
261  }
PageLayout->getPluginCollections
132      'conditions' => $this->getConditions(),
133    ];
134  }
PageLayout->getProfile
215    $storage = $this->entityTypeManager()->getStorage('display_builder_profile');
216    $profile_id = $this->get(DisplayBuildableInterface::PROFILE_PROPERTY);
217
218    if (!$profile_id) {
219      return NULL;
223    $builder = $storage->load($profile_id);
224
225    return $builder;
226  }
PageLayout->getSources
232    return $this->sources;
233  }
PageLayout->isImpactingPageVariantDetection
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)) {
277    if (!$update && $this->status && !empty($this->sources)) {
277    if (!$update && $this->status && !empty($this->sources)) {
277    if (!$update && $this->status && !empty($this->sources)) {
277    if (!$update && $this->status && !empty($this->sources)) {
278      return TRUE;
282    if (!$update) {
283      return FALSE;
286    $previous = $this->originalEntity;
287
288    // Those properties are impacting AccessControlHandler logic and
289    // PageVariantSubscriber results.
290    foreach (['weight', 'conditions', 'status'] as $property) {
290    foreach (['weight', 'conditions', 'status'] as $property) {
291      if ($this->get($property) !== $previous->get($property)) {
292        return TRUE;
290    foreach (['weight', 'conditions', 'status'] as $property) {
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;
302    return FALSE;
303  }
PageLayout->postSave
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)) {
197    if ($this->isImpactingPageVariantDetection($update)) {
204      $entity_type = $this->getEntityType();
205      \Drupal::service('cache_tags.invalidator')->invalidateTags([$entity_type->getConfigPrefix()]);
206    }
207
208    parent::postSave($storage, $update);
208    parent::postSave($storage, $update);
209  }
PageLayout->setSources
238  public function setSources(array $sources): void {
239    $this->sources = $sources;
240  }
PageLayout->sourceManager
327    return \Drupal::service('plugin.manager.ui_patterns_source');
328  }