Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
46.97% covered (danger)
46.97%
62 / 132
66.67% covered (warning)
66.67%
46 / 69
35.38% covered (danger)
35.38%
23 / 65
50.00% covered (danger)
50.00%
11 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageLayout
68.89% covered (warning)
68.89%
62 / 90
66.67% covered (warning)
66.67%
46 / 69
35.38% covered (danger)
35.38%
23 / 65
50.00% covered (danger)
50.00%
11 / 22
616.85
0.00% covered (danger)
0.00%
0 / 1
 getPrefix
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
 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
 getContextRequirement
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
 checkInstanceId
0.00% covered (danger)
0.00%
0 / 6
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
 getBuilderUrl
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
 getUrlFromInstanceId
0.00% covered (danger)
0.00%
0 / 4
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
 getDisplayUrlFromInstanceId
0.00% covered (danger)
0.00%
0 / 4
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
 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
 getInstanceId
100.00% covered (success)
100.00%
3 / 3
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
 checkAccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 initInstanceIfMissing
100.00% covered (success)
100.00%
5 / 5
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
 getInitialSources
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
 getInitialContext
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
 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
 saveSources
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
 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
85.71% covered (warning)
85.71%
6 / 7
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
7.46
 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
 isImpactingPageVariantDetection
63.64% covered (warning)
63.64%
7 / 11
69.23% covered (warning)
69.23%
9 / 13
8.00% covered (danger)
8.00%
2 / 25
0.00% covered (danger)
0.00%
0 / 1
57.84
 getInstance
100.00% covered (success)
100.00%
6 / 6
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
 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\Access\AccessResult;
8use Drupal\Core\Access\AccessResultInterface;
9use Drupal\Core\Condition\ConditionPluginCollection;
10use Drupal\Core\Config\Entity\ConfigEntityBase;
11use Drupal\Core\Entity\Attribute\ConfigEntityType;
12use Drupal\Core\Entity\EntityDeleteForm;
13use Drupal\Core\Entity\EntityStorageInterface;
14use Drupal\Core\Session\AccountInterface;
15use Drupal\Core\StringTranslation\TranslatableMarkup;
16use Drupal\Core\Url;
17use Drupal\display_builder\ConfigFormBuilderInterface;
18use Drupal\display_builder\DisplayBuilderHelpers;
19use Drupal\display_builder\InstanceInterface;
20use Drupal\display_builder\ProfileInterface;
21use Drupal\display_builder_page_layout\AccessControlHandler;
22use Drupal\display_builder_page_layout\Form\PageLayoutForm;
23use Drupal\display_builder_page_layout\PageLayoutInterface;
24use Drupal\display_builder_page_layout\PageLayoutListBuilder;
25use Drupal\ui_patterns\Plugin\Context\RequirementsContext;
26use Drupal\ui_patterns\SourcePluginManager;
27
28/**
29 * Defines the page layout entity type.
30 */
31#[ConfigEntityType(
32  id: 'page_layout',
33  label: new TranslatableMarkup('Page layout'),
34  label_collection: new TranslatableMarkup('Page layouts'),
35  label_singular: new TranslatableMarkup('page layout'),
36  label_plural: new TranslatableMarkup('page layouts'),
37  config_prefix: 'page_layout',
38  entity_keys: [
39    'id' => 'id',
40    'label' => 'label',
41    'weight' => 'weight',
42    'uuid' => 'uuid',
43  ],
44  handlers: [
45    'access' => AccessControlHandler::class,
46    'list_builder' => PageLayoutListBuilder::class,
47    'form' => [
48      'add' => PageLayoutForm::class,
49      'edit' => PageLayoutForm::class,
50      'delete' => EntityDeleteForm::class,
51    ],
52  ],
53  links: [
54    'collection' => '/admin/structure/page-layout',
55    'add-form' => '/admin/structure/page-layout/add',
56    'edit-form' => '/admin/structure/page-layout/{page_layout}',
57    'display-builder' => '/admin/structure/page-layout/{page_layout}/builder',
58    'delete-form' => '/admin/structure/page-layout/{page_layout}/delete',
59  ],
60  admin_permission: 'administer page_layout',
61  label_count: [
62    'singular' => '@count page layout',
63    'plural' => '@count page layouts',
64  ],
65  config_export: [
66    'id',
67    'label',
68    'weight',
69    ConfigFormBuilderInterface::PROFILE_PROPERTY,
70    ConfigFormBuilderInterface::SOURCES_PROPERTY,
71    'conditions',
72  ],
73)]
74final class PageLayout extends ConfigEntityBase implements PageLayoutInterface {
75
76  /**
77   * The example ID.
78   */
79  protected string $id;
80
81  /**
82   * The example label.
83   */
84  protected string $label;
85
86  /**
87   * Weight of this page layout when negotiating the page variant.
88   *
89   * The first/lowest that is accessible according to conditions is loaded.
90   *
91   * @var int
92   */
93  protected $weight = 0;
94
95  /**
96   * Display Builder Profile ID.
97   */
98  protected string $profile = '';
99
100  /**
101   * A list of sources plugins.
102   *
103   * @var array
104   */
105  protected $sources = [];
106
107  /**
108   * Condition settings for storage.
109   *
110   * @var array
111   */
112  protected $conditions = [];
113
114  /**
115   * The loaded display builder instance.
116   */
117  protected ?InstanceInterface $instance;
118
119  /**
120   * The conditions plugins for this page.
121   */
122  private ConditionPluginCollection $conditionPluginCollection;
123
124  /**
125   * {@inheritdoc}
126   */
127  public static function getPrefix(): string {
128    return 'page_layout__';
129  }
130
131  /**
132   * {@inheritdoc}
133   */
134  public function getPluginCollections(): array {
135    return [
136      'conditions' => $this->getConditions(),
137    ];
138  }
139
140  /**
141   * {@inheritdoc}
142   */
143  public static function getContextRequirement(): string {
144    return 'page';
145  }
146
147  /**
148   * {@inheritdoc}
149   */
150  public static function checkInstanceId(string $instance_id): ?array {
151    if (!\str_starts_with($instance_id, self::getPrefix())) {
152      return NULL;
153    }
154    [, $page_layout] = \explode('__', $instance_id);
155
156    return [
157      'page_layout' => $page_layout,
158    ];
159  }
160
161  /**
162   * {@inheritdoc}
163   */
164  public function getBuilderUrl(): Url {
165    return Url::fromRoute('entity.page_layout.display_builder', ['page_layout' => $this->id()]);
166  }
167
168  /**
169   * {@inheritdoc}
170   */
171  public static function getUrlFromInstanceId(string $instance_id): Url {
172    $params = self::checkInstanceId($instance_id);
173
174    if (!$params) {
175      // Fallback to the list of instances.
176      return Url::fromRoute('entity.display_builder_instance.collection');
177    }
178
179    return Url::fromRoute('entity.page_layout.display_builder', $params);
180  }
181
182  /**
183   * {@inheritdoc}
184   */
185  public static function getDisplayUrlFromInstanceId(string $instance_id): Url {
186    $params = self::checkInstanceId($instance_id);
187
188    if (!$params) {
189      // Fallback to the list of instances.
190      return Url::fromRoute('entity.display_builder_instance.collection');
191    }
192
193    return Url::fromRoute('entity.page_layout.edit_form', $params);
194  }
195
196  /**
197   * {@inheritdoc}
198   */
199  public function getProfile(): ?ProfileInterface {
200    $storage = $this->entityTypeManager()->getStorage('display_builder_profile');
201    $profile_id = $this->get(ConfigFormBuilderInterface::PROFILE_PROPERTY);
202
203    if (!$profile_id) {
204      return NULL;
205    }
206
207    /** @var \Drupal\display_builder\ProfileInterface $builder */
208    $builder = $storage->load($profile_id);
209
210    return $builder;
211  }
212
213  /**
214   * {@inheritdoc}
215   */
216  public function getInstanceId(): ?string {
217    // Usually an entity is new if no ID exists for it yet.
218    if ($this->isNew()) {
219      return NULL;
220    }
221
222    return \sprintf('%s%s', self::getPrefix(), $this->id());
223  }
224
225  /**
226   * {@inheritdoc}
227   */
228  public static function checkAccess(string $instance_id, AccountInterface $account): AccessResultInterface {
229    return $account->hasPermission('administer page_layout') ? AccessResult::allowed() : AccessResult::forbidden();
230  }
231
232  /**
233   * {@inheritdoc}
234   */
235  public function initInstanceIfMissing(): void {
236    /** @var \Drupal\display_builder\InstanceStorage $storage */
237    $storage = $this->entityTypeManager()->getStorage('display_builder_instance');
238
239    /** @var \Drupal\display_builder\InstanceInterface $instance */
240    $instance = $storage->load($this->getInstanceId());
241
242    if (!$instance) {
243      $instance = $storage->createFromImplementation($this);
244      $instance->save();
245    }
246  }
247
248  /**
249   * {@inheritdoc}
250   */
251  public function getInitialSources(): array {
252    $sources = $this->getSources();
253
254    if (empty($sources)) {
255      // Fallback to a fixture mimicking the standard page layout.
256      $sources = DisplayBuilderHelpers::getFixtureDataFromExtension('display_builder_page_layout', 'default_page_layout');
257    }
258
259    return $sources;
260  }
261
262  /**
263   * {@inheritdoc}
264   */
265  public function getInitialContext(): array {
266    $contexts = [];
267    $contexts = RequirementsContext::addToContext([self::getContextRequirement()], $contexts);
268
269    return $contexts;
270  }
271
272  /**
273   * {@inheritdoc}
274   */
275  public function getSources(): array {
276    return $this->sources;
277  }
278
279  /**
280   * {@inheritdoc}
281   */
282  public function saveSources(): void {
283    $this->sources = $this->getInstance()->getCurrentState();
284    $this->save();
285  }
286
287  /**
288   * {@inheritdoc}
289   */
290  public function getConditions(): ConditionPluginCollection {
291    if (!isset($this->conditionPluginCollection)) {
292      // Static call because EntityBase and descendants don't support
293      // dependency injection.
294      $manager = \Drupal::service('plugin.manager.condition');
295      $this->conditionPluginCollection = new ConditionPluginCollection($manager, $this->get('conditions'));
296    }
297
298    return $this->conditionPluginCollection;
299  }
300
301  /**
302   * {@inheritdoc}
303   */
304  public function calculateDependencies(): PageLayout {
305    parent::calculateDependencies();
306    $display_builder = $this->getProfile();
307    $instance = $this->getInstance();
308
309    if ($display_builder && $instance) {
310      $this->addDependency('config', $display_builder->getConfigDependencyName());
311      $contexts = $instance->getContexts() ?? [];
312
313      foreach ($this->getSources() as $source_data) {
314        /** @var \Drupal\ui_patterns\SourceInterface $source */
315        $source = $this->sourceManager()->getSource('', [], $source_data, $contexts);
316        $this->addDependencies($source->calculateDependencies());
317      }
318    }
319
320    return $this;
321  }
322
323  /**
324   * {@inheritdoc}
325   */
326  public function delete(): void {
327    if ($this->getInstance()) {
328      $storage = $this->entityTypeManager()->getStorage('display_builder_instance');
329      $storage->delete([$this->getInstance()]);
330    }
331
332    parent::delete();
333  }
334
335  /**
336   * {@inheritdoc}
337   */
338  public function postSave(EntityStorageInterface $storage, $update = TRUE): void {
339    $this->initInstanceIfMissing();
340    $instance = $this->getInstance();
341
342    // Save the profile in the instance if changed.
343    if ($instance->getProfile()->id() !== $this->profile) {
344      $instance->setProfile($this->profile);
345      $instance->save();
346    }
347
348    if ($this->isImpactingPageVariantDetection($update)) {
349      // In DisplayBuilderPageVariant we add PageLayout::>getCacheTags() to the
350      // page renderable but it works only for the pages already managed by
351      // Display Builder.
352      // In PageVariantSubscriber::onSelectPageDisplayVariant() we add a custom
353      // tag for the others.
354      /** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type */
355      $entity_type = $this->getEntityType();
356      \Drupal::service('cache_tags.invalidator')->invalidateTags([$entity_type->getConfigPrefix()]);
357    }
358
359    parent::postSave($storage, $update);
360  }
361
362  /**
363   * Does the page cache need to be flushed?
364   *
365   * Flushing a cache is something to be careful enough. Let's flush only when
366   * needed.
367   *
368   * @param bool $update
369   *   TRUE if the entity has been updated, or FALSE if it has been inserted.
370   *
371   * @return bool
372   *   TRUE if the cache need to be flushed.
373   */
374  private function isImpactingPageVariantDetection(bool $update = TRUE): bool {
375    // A new active page layout has been added.
376    if (!$update && $this->status && !empty($this->sources)) {
377      return TRUE;
378    }
379
380    // Other additions have no impact.
381    if (!$update) {
382      return FALSE;
383    }
384
385    $previous = $this->originalEntity;
386
387    // Those properties are impacting AccessControlHandler logic and
388    // PageVariantSubscriber results.
389    foreach (['weight', 'conditions', 'status'] as $property) {
390      if ($this->get($property) !== $previous->get($property)) {
391        return TRUE;
392      }
393    }
394
395    // A page layout with empty sources is skipped by AccessControlHandler.
396    // This is also altering PageVariantSubscriber results.
397    if (empty($this->sources) !== empty($previous->get('sources'))) {
398      return TRUE;
399    }
400
401    return FALSE;
402  }
403
404  /**
405   * Gets the Display Builder instance.
406   *
407   * @return \Drupal\display_builder\InstanceInterface|null
408   *   A display builder instance.
409   */
410  private function getInstance(): ?InstanceInterface {
411    if (!$this->getInstanceId()) {
412      return NULL;
413    }
414
415    if (!isset($this->instance)) {
416      /** @var \Drupal\display_builder\InstanceInterface|null $instance */
417      $instance = $this->entityTypeManager()->getStorage('display_builder_instance')->load($this->getInstanceId());
418      $this->instance = $instance;
419    }
420
421    return $this->instance;
422  }
423
424  /**
425   * Gets the UI Patterns Source plugins manager.
426   *
427   * @return \Drupal\ui_patterns\SourcePluginManager
428   *   The manager for source plugins.
429   */
430  private function sourceManager(): SourcePluginManager {
431    return \Drupal::service('plugin.manager.ui_patterns_source');
432  }
433
434}