Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
70.42% covered (warning)
70.42%
50 / 71
60.00% covered (warning)
60.00%
12 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageLayout
70.42% covered (warning)
70.42%
50 / 71
60.00% covered (warning)
60.00%
12 / 20
63.91
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
1
 getPluginCollections
100.00% covered (success)
100.00%
3 / 3
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
1
 checkInstanceId
0.00% covered (danger)
0.00%
0 / 6
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
2
 getUrlFromInstanceId
0.00% covered (danger)
0.00%
0 / 4
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 / 1
6
 getProfile
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 getInstanceId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 initInstanceIfMissing
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getInitialSources
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getInitialContext
100.00% covered (success)
100.00%
3 / 3
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
1
 saveSources
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getConditions
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 calculateDependencies
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
4.13
 delete
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 postSave
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getInstance
100.00% covered (success)
100.00%
6 / 6
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
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\Core\Url;
14use Drupal\display_builder\ConfigFormBuilderInterface;
15use Drupal\display_builder\DisplayBuilderHelpers;
16use Drupal\display_builder\InstanceInterface;
17use Drupal\display_builder\ProfileInterface;
18use Drupal\display_builder_page_layout\AccessControlHandler;
19use Drupal\display_builder_page_layout\Form\PageLayoutForm;
20use Drupal\display_builder_page_layout\PageLayoutInterface;
21use Drupal\display_builder_page_layout\PageLayoutListBuilder;
22use Drupal\ui_patterns\Plugin\Context\RequirementsContext;
23use Drupal\ui_patterns\SourcePluginManager;
24
25/**
26 * Defines the page layout entity type.
27 */
28#[ConfigEntityType(
29  id: 'page_layout',
30  label: new TranslatableMarkup('Page layout'),
31  label_collection: new TranslatableMarkup('Page layouts'),
32  label_singular: new TranslatableMarkup('page layout'),
33  label_plural: new TranslatableMarkup('page layouts'),
34  config_prefix: 'page_layout',
35  entity_keys: [
36    'id' => 'id',
37    'label' => 'label',
38    'weight' => 'weight',
39    'uuid' => 'uuid',
40  ],
41  handlers: [
42    'access' => AccessControlHandler::class,
43    'list_builder' => PageLayoutListBuilder::class,
44    'form' => [
45      'add' => PageLayoutForm::class,
46      'edit' => PageLayoutForm::class,
47      'delete' => EntityDeleteForm::class,
48    ],
49  ],
50  links: [
51    'collection' => '/admin/structure/page-layout',
52    'add-form' => '/admin/structure/page-layout/add',
53    'edit-form' => '/admin/structure/page-layout/{page_layout}',
54    'display-builder' => '/admin/structure/page-layout/{page_layout}/builder',
55    'delete-form' => '/admin/structure/page-layout/{page_layout}/delete',
56  ],
57  admin_permission: 'administer page_layout',
58  label_count: [
59    'singular' => '@count page layout',
60    'plural' => '@count page layouts',
61  ],
62  config_export: [
63    'id',
64    'label',
65    'weight',
66    ConfigFormBuilderInterface::PROFILE_PROPERTY,
67    ConfigFormBuilderInterface::SOURCES_PROPERTY,
68    'conditions',
69  ],
70)]
71final class PageLayout extends ConfigEntityBase implements PageLayoutInterface {
72
73  /**
74   * The example ID.
75   */
76  protected string $id;
77
78  /**
79   * The example label.
80   */
81  protected string $label;
82
83  /**
84   * Weight of this page layout when negotiating the page variant.
85   *
86   * The first/lowest that is accessible according to conditions is loaded.
87   *
88   * @var int
89   */
90  protected $weight = 0;
91
92  /**
93   * Display Builder ID.
94   */
95  protected string $display_builder = '';
96
97  /**
98   * A list of sources plugins.
99   *
100   * @var array
101   */
102  protected $sources = [];
103
104  /**
105   * Condition settings for storage.
106   *
107   * @var array
108   */
109  protected $conditions = [];
110
111  /**
112   * The loaded display builder instance.
113   */
114  protected ?InstanceInterface $instance;
115
116  /**
117   * The conditions plugins for this page.
118   */
119  private ConditionPluginCollection $conditionPluginCollection;
120
121  /**
122   * {@inheritdoc}
123   */
124  public static function getPrefix(): string {
125    return 'page_layout__';
126  }
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 static function getContextRequirement(): string {
141    return 'page';
142  }
143
144  /**
145   * {@inheritdoc}
146   */
147  public static function checkInstanceId(string $instance_id): ?array {
148    if (!\str_starts_with($instance_id, self::getPrefix())) {
149      return NULL;
150    }
151    [, $page_layout] = \explode('__', $instance_id);
152
153    return [
154      'page_layout' => $page_layout,
155    ];
156  }
157
158  /**
159   * {@inheritdoc}
160   */
161  public function getBuilderUrl(): Url {
162    return Url::fromRoute('entity.page_layout.display_builder', ['page_layout' => $this->id()]);
163  }
164
165  /**
166   * {@inheritdoc}
167   */
168  public static function getUrlFromInstanceId(string $instance_id): Url {
169    $params = self::checkInstanceId($instance_id);
170
171    if (!$params) {
172      // Fallback to the list of instances.
173      return Url::fromRoute('entity.display_builder_instance.collection');
174    }
175
176    return Url::fromRoute('entity.page_layout.display_builder', $params);
177  }
178
179  /**
180   * {@inheritdoc}
181   */
182  public static function getDisplayUrlFromInstanceId(string $instance_id): Url {
183    $params = self::checkInstanceId($instance_id);
184
185    if (!$params) {
186      // Fallback to the list of instances.
187      return Url::fromRoute('entity.display_builder_instance.collection');
188    }
189
190    return Url::fromRoute('entity.page_layout.edit_form', $params);
191  }
192
193  /**
194   * {@inheritdoc}
195   */
196  public function getProfile(): ?ProfileInterface {
197    $storage = $this->entityTypeManager()->getStorage('display_builder_profile');
198    $profile_id = $this->get(ConfigFormBuilderInterface::PROFILE_PROPERTY);
199
200    if (!$profile_id) {
201      return NULL;
202    }
203
204    /** @var \Drupal\display_builder\ProfileInterface $builder */
205    $builder = $storage->load($profile_id);
206
207    return $builder;
208  }
209
210  /**
211   * {@inheritdoc}
212   */
213  public function getInstanceId(): ?string {
214    // Usually an entity is new if no ID exists for it yet.
215    if ($this->isNew()) {
216      return NULL;
217    }
218
219    return \sprintf('%s%s', self::getPrefix(), $this->id());
220  }
221
222  /**
223   * {@inheritdoc}
224   */
225  public function initInstanceIfMissing(): void {
226    /** @var \Drupal\display_builder\InstanceStorage $storage */
227    $storage = $this->entityTypeManager()->getStorage('display_builder_instance');
228
229    /** @var \Drupal\display_builder\InstanceInterface $instance */
230    $instance = $storage->load($this->getInstanceId());
231
232    if (!$instance) {
233      $instance = $storage->createFromImplementation($this);
234      $instance->save();
235    }
236  }
237
238  /**
239   * {@inheritdoc}
240   */
241  public function getInitialSources(): array {
242    $sources = $this->getSources();
243
244    if (empty($sources)) {
245      // Fallback to a fixture mimicking the standard page layout.
246      $sources = DisplayBuilderHelpers::getFixtureDataFromExtension('display_builder_page_layout', 'default_page_layout');
247    }
248
249    return $sources;
250  }
251
252  /**
253   * {@inheritdoc}
254   */
255  public function getInitialContext(): array {
256    $contexts = [];
257    $contexts = RequirementsContext::addToContext([self::getContextRequirement()], $contexts);
258
259    return $contexts;
260  }
261
262  /**
263   * {@inheritdoc}
264   */
265  public function getSources(): array {
266    return $this->sources;
267  }
268
269  /**
270   * {@inheritdoc}
271   */
272  public function saveSources(): void {
273    $this->sources = $this->getInstance()->getCurrentState();
274    $this->save();
275  }
276
277  /**
278   * {@inheritdoc}
279   */
280  public function getConditions(): ConditionPluginCollection {
281    if (!isset($this->conditionPluginCollection)) {
282      // Static call because EntityBase and descendants don't support
283      // dependency injection.
284      $manager = \Drupal::service('plugin.manager.condition');
285      $this->conditionPluginCollection = new ConditionPluginCollection($manager, $this->get('conditions'));
286    }
287
288    return $this->conditionPluginCollection;
289  }
290
291  /**
292   * {@inheritdoc}
293   */
294  public function calculateDependencies(): PageLayout {
295    parent::calculateDependencies();
296    $display_builder = $this->getProfile();
297    $instance = $this->getInstance();
298
299    if ($display_builder && $instance) {
300      $this->addDependency('config', $display_builder->getConfigDependencyName());
301      $contexts = $instance->getContexts() ?? [];
302
303      foreach ($this->getSources() as $source_data) {
304        /** @var \Drupal\ui_patterns\SourceInterface $source */
305        $source = $this->sourceManager()->getSource('', [], $source_data, $contexts);
306        $this->addDependencies($source->calculateDependencies());
307      }
308    }
309
310    return $this;
311  }
312
313  /**
314   * {@inheritdoc}
315   */
316  public function delete(): void {
317    if ($this->getInstance()) {
318      $storage = $this->entityTypeManager()->getStorage('display_builder_instance');
319      $storage->delete([$this->getInstance()]);
320    }
321
322    parent::delete();
323  }
324
325  /**
326   * {@inheritdoc}
327   */
328  public function postSave(EntityStorageInterface $storage, $update = TRUE): void {
329    $this->initInstanceIfMissing();
330    parent::postSave($storage, $update);
331  }
332
333  /**
334   * Gets the Display Builder instance.
335   *
336   * @return \Drupal\display_builder\InstanceInterface|null
337   *   A display builder instance.
338   */
339  private function getInstance(): ?InstanceInterface {
340    if (!$this->getInstanceId()) {
341      return NULL;
342    }
343
344    if (!isset($this->instance)) {
345      /** @var \Drupal\display_builder\InstanceInterface|null $instance */
346      $instance = $this->entityTypeManager()->getStorage('display_builder_instance')->load($this->getInstanceId());
347      $this->instance = $instance;
348    }
349
350    return $this->instance;
351  }
352
353  /**
354   * Gets the UI Patterns Source plugins manager.
355   *
356   * @return \Drupal\ui_patterns\SourcePluginManager
357   *   The manager for source plugins.
358   */
359  private function sourceManager(): SourcePluginManager {
360    return \Drupal::service('plugin.manager.ui_patterns_source');
361  }
362
363}