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}

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
305    parent::calculateDependencies();
306    $display_builder = $this->getProfile();
307    $instance = $this->getInstance();
308
309    if ($display_builder && $instance) {
309    if ($display_builder && $instance) {
310      $this->addDependency('config', $display_builder->getConfigDependencyName());
311      $contexts = $instance->getContexts() ?? [];
312
313      foreach ($this->getSources() as $source_data) {
313      foreach ($this->getSources() as $source_data) {
313      foreach ($this->getSources() as $source_data) {
314        /** @var \Drupal\ui_patterns\SourceInterface $source */
315        $source = $this->sourceManager()->getSource('', [], $source_data, $contexts);
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;
320    return $this;
PageLayout->checkAccess
228  public static function checkAccess(string $instance_id, AccountInterface $account): AccessResultInterface {
229    return $account->hasPermission('administer page_layout') ? AccessResult::allowed() : AccessResult::forbidden();
229    return $account->hasPermission('administer page_layout') ? AccessResult::allowed() : AccessResult::forbidden();
229    return $account->hasPermission('administer page_layout') ? AccessResult::allowed() : AccessResult::forbidden();
229    return $account->hasPermission('administer page_layout') ? AccessResult::allowed() : AccessResult::forbidden();
PageLayout->checkInstanceId
150  public static function checkInstanceId(string $instance_id): ?array {
151    if (!\str_starts_with($instance_id, self::getPrefix())) {
152      return NULL;
154    [, $page_layout] = \explode('__', $instance_id);
155
156    return [
157      'page_layout' => $page_layout,
PageLayout->delete
327    if ($this->getInstance()) {
328      $storage = $this->entityTypeManager()->getStorage('display_builder_instance');
329      $storage->delete([$this->getInstance()]);
330    }
331
332    parent::delete();
332    parent::delete();
333  }
PageLayout->getBuilderUrl
165    return Url::fromRoute('entity.page_layout.display_builder', ['page_layout' => $this->id()]);
PageLayout->getConditions
291    if (!isset($this->conditionPluginCollection)) {
294      $manager = \Drupal::service('plugin.manager.condition');
295      $this->conditionPluginCollection = new ConditionPluginCollection($manager, $this->get('conditions'));
296    }
297
298    return $this->conditionPluginCollection;
298    return $this->conditionPluginCollection;
PageLayout->getContextRequirement
144    return 'page';
PageLayout->getDisplayUrlFromInstanceId
185  public static function getDisplayUrlFromInstanceId(string $instance_id): Url {
186    $params = self::checkInstanceId($instance_id);
187
188    if (!$params) {
190      return Url::fromRoute('entity.display_builder_instance.collection');
193    return Url::fromRoute('entity.page_layout.edit_form', $params);
PageLayout->getInitialContext
266    $contexts = [];
267    $contexts = RequirementsContext::addToContext([self::getContextRequirement()], $contexts);
268
269    return $contexts;
PageLayout->getInitialSources
252    $sources = $this->getSources();
253
254    if (empty($sources)) {
256      $sources = DisplayBuilderHelpers::getFixtureDataFromExtension('display_builder_page_layout', 'default_page_layout');
257    }
258
259    return $sources;
259    return $sources;
PageLayout->getInstance
411    if (!$this->getInstanceId()) {
412      return NULL;
415    if (!isset($this->instance)) {
417      $instance = $this->entityTypeManager()->getStorage('display_builder_instance')->load($this->getInstanceId());
418      $this->instance = $instance;
419    }
420
421    return $this->instance;
421    return $this->instance;
PageLayout->getInstanceId
218    if ($this->isNew()) {
219      return NULL;
222    return \sprintf('%s%s', self::getPrefix(), $this->id());
PageLayout->getPluginCollections
136      'conditions' => $this->getConditions(),
PageLayout->getPrefix
128    return 'page_layout__';
PageLayout->getProfile
200    $storage = $this->entityTypeManager()->getStorage('display_builder_profile');
201    $profile_id = $this->get(ConfigFormBuilderInterface::PROFILE_PROPERTY);
202
203    if (!$profile_id) {
204      return NULL;
208    $builder = $storage->load($profile_id);
209
210    return $builder;
PageLayout->getSources
276    return $this->sources;
PageLayout->getUrlFromInstanceId
171  public static function getUrlFromInstanceId(string $instance_id): Url {
172    $params = self::checkInstanceId($instance_id);
173
174    if (!$params) {
176      return Url::fromRoute('entity.display_builder_instance.collection');
179    return Url::fromRoute('entity.page_layout.display_builder', $params);
PageLayout->initInstanceIfMissing
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  }
246  }
PageLayout->isImpactingPageVariantDetection
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)) {
376    if (!$update && $this->status && !empty($this->sources)) {
376    if (!$update && $this->status && !empty($this->sources)) {
377      return TRUE;
381    if (!$update) {
382      return FALSE;
385    $previous = $this->originalEntity;
386
387    // Those properties are impacting AccessControlHandler logic and
388    // PageVariantSubscriber results.
389    foreach (['weight', 'conditions', 'status'] as $property) {
389    foreach (['weight', 'conditions', 'status'] as $property) {
390      if ($this->get($property) !== $previous->get($property)) {
391        return TRUE;
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;
401    return FALSE;
PageLayout->postSave
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)) {
348    if ($this->isImpactingPageVariantDetection($update)) {
355      $entity_type = $this->getEntityType();
356      \Drupal::service('cache_tags.invalidator')->invalidateTags([$entity_type->getConfigPrefix()]);
357    }
358
359    parent::postSave($storage, $update);
359    parent::postSave($storage, $update);
360  }
PageLayout->saveSources
283    $this->sources = $this->getInstance()->getCurrentState();
284    $this->save();
285  }
PageLayout->sourceManager
431    return \Drupal::service('plugin.manager.ui_patterns_source');