Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 66
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
DisplayBuilderPageVariant
0.00% covered (danger)
0.00%
0 / 63
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 6
306
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
2
 create
0.00% covered (danger)
0.00%
0 / 9
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
2
 build
0.00% covered (danger)
0.00%
0 / 34
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
42
 setMainContent
0.00% covered (danger)
0.00%
0 / 2
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
2
 setTitle
0.00% covered (danger)
0.00%
0 / 2
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
2
 replaceTitleAndContent
0.00% covered (danger)
0.00%
0 / 11
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder_page_layout\Plugin\DisplayVariant;
6
7use Drupal\Component\Render\MarkupInterface;
8use Drupal\Core\Cache\CacheableMetadata;
9use Drupal\Core\Display\Attribute\PageDisplayVariant;
10use Drupal\Core\Display\PageVariantInterface;
11use Drupal\Core\Display\VariantBase;
12use Drupal\Core\Entity\EntityTypeManagerInterface;
13use Drupal\Core\Extension\ExtensionList;
14use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
15use Drupal\Core\StringTranslation\TranslatableMarkup;
16use Drupal\Core\Theme\Registry;
17use Drupal\display_builder\DisplayBuilderHelpers;
18use Drupal\ui_patterns\Element\ComponentElementBuilder;
19use Symfony\Component\DependencyInjection\ContainerInterface;
20
21/**
22 * A variant for pages managed by Display Builder Page Layout.
23 */
24#[PageDisplayVariant(
25  id: 'display_builder',
26  admin_label: new TranslatableMarkup('Display Builder page')
27)]
28class DisplayBuilderPageVariant extends VariantBase implements ContainerFactoryPluginInterface, PageVariantInterface {
29
30  private const SOURCE_CONTENT_ID = 'main_page_content';
31
32  private const SOURCE_TITLE_ID = 'page_title';
33
34  /**
35   * The render array representing the main content.
36   */
37  protected array $mainContent;
38
39  /**
40   * The page title.
41   *
42   * Can be a string (plain title), Markup or a render array (formatted title).
43   */
44  protected array|MarkupInterface|string $title;
45
46  /**
47   * Component element builder.
48   */
49  protected ComponentElementBuilder $componentElementBuilder;
50
51  /**
52   * The entity type manager.
53   */
54  protected EntityTypeManagerInterface $entityTypeManager;
55
56  /**
57   * The theme registry.
58   */
59  protected Registry $themeRegistry;
60
61  /**
62   * The list of modules.
63   */
64  protected ExtensionList $modules;
65
66  public function __construct(
67    array $configuration,
68    $plugin_id,
69    $plugin_definition,
70    ComponentElementBuilder $component_element_builder,
71    EntityTypeManagerInterface $entity_type_manager,
72    Registry $theme_registry,
73    ExtensionList $modules,
74  ) {
75    parent::__construct($configuration, $plugin_id, $plugin_definition);
76    $this->componentElementBuilder = $component_element_builder;
77    $this->entityTypeManager = $entity_type_manager;
78    $this->themeRegistry = $theme_registry;
79    $this->modules = $modules;
80  }
81
82  /**
83   * {@inheritdoc}
84   */
85  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
86    return new static(
87      $configuration,
88      $plugin_id,
89      $plugin_definition,
90      $container->get('ui_patterns.component_element_builder'),
91      $container->get('entity_type.manager'),
92      $container->get('theme.registry'),
93      $container->get('extension.list.module'),
94    );
95  }
96
97  /**
98   * {@inheritdoc}
99   */
100  public function build(): array {
101    /** @var \Drupal\display_builder_page_layout\AccessControlHandler $access_control */
102    $access_control = $this->entityTypeManager->getAccessControlHandler('page_layout');
103    $page_layout = $access_control->loadCurrentPageLayout();
104
105    if (!$page_layout) {
106      // This is not supposed to happen. PageVariantSubscriber must not load
107      // this plugin, and fallback to Block Layout if there is no suitable
108      // Page Layout entities.
109      // @todo raise an error
110      return [];
111    }
112
113    // We alter the registry here instead of implementing
114    // hook_theme_registry_alter in order keep the alteration specific to each
115    // page.
116    $theme_registry = $this->themeRegistry->get();
117    $template_uri = $this->modules->getPath('display_builder_page_layout') . '/templates';
118    $runtime = $this->themeRegistry->getRuntime();
119    $theme_registry['page']['path'] = $template_uri;
120    $runtime->set('page', $theme_registry['page']);
121    $theme_registry['region']['path'] = $template_uri;
122    $runtime->set('region', $theme_registry['region']);
123
124    // Also skip the related template suggestions.
125    foreach (\array_keys($theme_registry) as $renderable_id) {
126      if (\str_starts_with($renderable_id, 'page__') || \str_starts_with($renderable_id, 'region__')) {
127        $runtime->delete($renderable_id);
128      }
129    }
130
131    $sources = $page_layout->getSources();
132    $instance_id = $page_layout->getInstanceId();
133    $this->replaceTitleAndContent($sources, $this->title, $this->mainContent);
134
135    /** @var \Drupal\display_builder\InstanceInterface $instance */
136    $instance = $this->entityTypeManager->getStorage('display_builder_instance')->load($instance_id);
137    $contexts = $instance->getContexts() ?? [];
138    $data = [];
139
140    foreach ($sources as $source) {
141      $build = $this->componentElementBuilder->buildSource($data, 'content', [], $source, $contexts);
142      $data[] = $build['#slots']['content'][0] ?? [];
143    }
144
145    $cache = new CacheableMetadata();
146    $cache->addCacheableDependency($this);
147    // See also: PageLayout::postSave().
148    // See also: PageVariantSubscriber::onSelectPageDisplayVariant()
149    $cache->addCacheTags($page_layout->getCacheTags());
150
151    $build = [
152      'display_builder_content' => [
153        'data' => $data,
154        '#weight' => -800,
155      ],
156    ];
157    $cache->applyTo($build);
158
159    return $build;
160  }
161
162  /**
163   * {@inheritdoc}
164   */
165  public function setMainContent(array $main_content): self {
166    $this->mainContent = $main_content;
167
168    return $this;
169  }
170
171  /**
172   * {@inheritdoc}
173   */
174  public function setTitle($title): self {
175    $this->title = $title;
176
177    return $this;
178  }
179
180  /**
181   * Replace title and content blocks.
182   *
183   * @param array $data
184   *   The Display Builder data to alter.
185   * @param mixed $title
186   *   The title to set.
187   * @param array|null $content
188   *   The content to set.
189   *
190   * @todo replace multiple in one pass?
191   */
192  private function replaceTitleAndContent(array &$data, mixed $title, ?array $content): void {
193    if ($content !== NULL) {
194      DisplayBuilderHelpers::findArrayReplaceSource($data, ['source_id' => self::SOURCE_CONTENT_ID], $content);
195    }
196
197    // Try to handle specific title cases.
198    if (\is_string($title)) {
199      $title = $title;
200    }
201    elseif ($title instanceof MarkupInterface) {
202      $title = (string) $title;
203    }
204    elseif (isset($title['#markup'])) {
205      $title = $title['#markup'];
206    }
207
208    if ($title !== NULL && \is_string($title)) {
209      // @todo avoid arbitrary classes.
210      $title = ['#markup' => '<h1 class="title page-title">' . $title . '</h1>'];
211      DisplayBuilderHelpers::findArrayReplaceSource($data, ['source_id' => self::SOURCE_TITLE_ID], $title);
212    }
213  }
214
215}