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