Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
63.31% covered (warning)
63.31%
88 / 139
57.75% covered (warning)
57.75%
41 / 71
16.85% covered (danger)
16.85%
15 / 89
35.00% covered (danger)
35.00%
7 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
DisplayBuildablePluginBase
63.31% covered (warning)
63.31%
88 / 139
57.75% covered (warning)
57.75%
41 / 71
16.85% covered (danger)
16.85%
15 / 89
35.00% covered (danger)
35.00%
7 / 20
1156.83
0.00% covered (danger)
0.00%
0 / 1
 create
100.00% covered (success)
100.00%
5 / 5
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
 getPrefix
0.00% covered (danger)
0.00%
0 / 3
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
 label
0.00% covered (danger)
0.00%
0 / 2
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
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
 getInstance
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 getInstanceId
0.00% covered (danger)
0.00%
0 / 3
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
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
 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
 getContextRequirement
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
 buildInstanceForm
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
15 / 15
10.53% covered (danger)
10.53%
4 / 38
100.00% covered (success)
100.00%
1 / 1
53.84
 getAllowedProfiles
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
6 / 6
25.00% covered (danger)
25.00%
1 / 4
100.00% covered (success)
100.00%
1 / 1
6.80
 isAllowed
100.00% covered (success)
100.00%
7 / 7
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
 createDisplayBuilderInstance
0.00% covered (danger)
0.00%
0 / 21
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
 getInitializationMessage
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
 getInitialContext
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
 getInitialSources
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
 buildSelect
92.31% covered (success)
92.31%
24 / 26
75.00% covered (warning)
75.00%
9 / 12
15.00% covered (danger)
15.00%
3 / 20
0.00% covered (danger)
0.00%
0 / 1
28.11
 buildLink
100.00% covered (success)
100.00%
15 / 15
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
 buildDisabledSelect
100.00% covered (success)
100.00%
9 / 9
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
 isCurrentUserAllowedToAdministrate
100.00% covered (success)
100.00%
1 / 1
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
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder;
6
7use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
8use Drupal\Core\Entity\EntityInterface;
9use Drupal\Core\Entity\EntityTypeManagerInterface;
10use Drupal\Core\Extension\ModuleHandlerInterface;
11use Drupal\Core\Plugin\PluginBase;
12use Drupal\Core\Session\AccountInterface;
13use Drupal\Core\Session\AccountProxyInterface;
14use Drupal\Core\StringTranslation\TranslatableMarkup;
15use Drupal\Core\Url;
16use Drupal\display_builder\Attribute\DisplayBuildable;
17use Drupal\display_builder\Entity\Instance;
18use Drupal\display_builder\Entity\ProfileInterface;
19use Symfony\Component\DependencyInjection\ContainerInterface;
20
21/**
22 * Base class for display_buildable plugins.
23 */
24abstract class DisplayBuildablePluginBase extends PluginBase implements DisplayBuildableInterface {
25
26  /**
27   * The entity type manager.
28   */
29  protected EntityTypeManagerInterface $entityTypeManager;
30
31  /**
32   * The loaded display builder instance.
33   */
34  protected ?InstanceInterface $instance;
35
36  /**
37   * Current user.
38   */
39  protected AccountProxyInterface $currentUser;
40
41  /**
42   * Module handler.
43   */
44  protected ModuleHandlerInterface $moduleHandler;
45
46  /**
47   * A tiny hint to remember where the initial data comes from.
48   *
49   * See ::getInitialSources() and ::getInitializationMessage().
50   */
51  protected string $initialDataSource = '';
52
53  /**
54   * {@inheritdoc}
55   */
56  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
57    $instance = new static($configuration, $plugin_id, $plugin_definition);
58    $instance->entityTypeManager = $container->get('entity_type.manager');
59    $instance->currentUser = $container->get('current_user');
60    $instance->moduleHandler = $container->get('module_handler');
61
62    return $instance;
63  }
64
65  /**
66   * {@inheritdoc}
67   */
68  public static function getPrefix(): string {
69    $reflection = new \ReflectionClass(static::class);
70    $attribute = $reflection->getAttributes(DisplayBuildable::class);
71
72    return $attribute[0]->newInstance()->instance_prefix;
73  }
74
75  /**
76   * {@inheritdoc}
77   */
78  public function label(): string {
79    // Cast the label to a string since it is a TranslatableMarkup object.
80    $definition = $this->pluginDefinition;
81
82    return (string) ($definition instanceof PluginDefinitionInterface ? $definition->id() : ($definition['label'] ?? ''));
83  }
84
85  /**
86   * {@inheritdoc}
87   */
88  public function initInstanceIfMissing(): void {
89    /** @var \Drupal\display_builder\InstanceInterface $instance */
90    $instance = $this->getInstance();
91
92    if (!$instance) {
93      $instance = $this->createDisplayBuilderInstance();
94      $instance->save();
95    }
96  }
97
98  /**
99   * {@inheritdoc}
100   */
101  public function getInstance(): ?InstanceInterface {
102    if (isset($this->instance)) {
103      return $this->instance;
104    }
105
106    if ($this->getInstanceId() === NULL) {
107      return NULL;
108    }
109
110    $storage = $this->entityTypeManager->getStorage('display_builder_instance');
111    /** @var \Drupal\display_builder\InstanceInterface|null $instance */
112    $instance = $storage->load($this->getInstanceId());
113
114    if (!$instance) {
115      return NULL;
116    }
117
118    $this->instance = $instance;
119
120    return $this->instance;
121  }
122
123  /**
124   * {@inheritdoc}
125   */
126  public function getInstanceId(): ?string {
127    // Plugins will override this method.
128    if (!isset($this->instance)) {
129      return NULL;
130    }
131
132    return (string) $this->instance->id();
133  }
134
135  /**
136   * {@inheritdoc}
137   */
138  public function getProfile(): ?ProfileInterface {
139    // Plugins will override this method.
140    return NULL;
141  }
142
143  /**
144   * {@inheritdoc}
145   */
146  public function getBuilderUrl(): Url {
147    // Plugins will override this method.
148    return Url::fromRoute('<front>');
149  }
150
151  /**
152   * {@inheritdoc}
153   */
154  public static function getContextRequirement(): string {
155    // Plugins will override this method.
156    return '';
157  }
158
159  /**
160   * {@inheritdoc}
161   */
162  public function buildInstanceForm(bool $mandatory = TRUE, ?TranslatableMarkup $title = NULL, bool $link = TRUE): array {
163    $profile = $this->getProfile();
164    $allowed = $this->isAllowed();
165
166    if (!$allowed && !$profile) {
167      return [
168        self::PROFILE_PROPERTY => [
169          '#markup' => $this->t('You are not allowed to use Display Builder.'),
170        ],
171      ];
172    }
173
174    if (!$allowed && $profile) {
175      return [
176        self::PROFILE_PROPERTY => $this->buildDisabledSelect($profile),
177      ];
178    }
179
180    $form = [
181      self::PROFILE_PROPERTY => $this->buildSelect($profile, $mandatory, $title),
182    ];
183
184    // Add the builder link to edit.
185    if ($this->getInstanceId() && $profile && $link) {
186      $form['link'] = $this->buildLink();
187    }
188
189    return $form;
190  }
191
192  /**
193   * {@inheritdoc}
194   */
195  public function getAllowedProfiles(?AccountInterface $account = NULL): array {
196    $account ??= $this->currentUser;
197    $options = [];
198    $storage = $this->entityTypeManager->getStorage('display_builder_profile');
199    $entity_ids = $storage->getQuery()->accessCheck(TRUE)->sort('weight', 'ASC')->execute();
200    /** @var \Drupal\display_builder\Entity\ProfileInterface[] $display_builders */
201    $display_builders = $storage->loadMultiple($entity_ids);
202
203    // Entity query doesn't execute access control handlers for config
204    // entities. So we need to do an extra check here.
205    foreach ($display_builders as $entity_id => $entity) {
206      // We don't execute $entity->access() to not catch admin permission
207      // 'administer display builder profile'.
208      // @see ProfileAccessControlHandler.
209      // Administrators can use any profile, but it is better to only propose
210      // them the ones related to their permissions.
211      if ($account->hasPermission($entity->getPermissionName())) {
212        $options[$entity_id] = $entity->label();
213      }
214    }
215
216    return $options;
217  }
218
219  /**
220   * {@inheritdoc}
221   */
222  public function isAllowed(?AccountInterface $account = NULL): bool {
223    $options = $this->getAllowedProfiles($account);
224
225    if (empty($options)) {
226      return FALSE;
227    }
228    $profile = $this->getProfile();
229
230    if (!$profile) {
231      return TRUE;
232    }
233
234    return isset($options[(string) $profile->id()]);
235  }
236
237  /**
238   * Create a display builder instance.
239   *
240   * @return \Drupal\Core\Entity\EntityInterface
241   *   The entity.
242   */
243  protected function createDisplayBuilderInstance(): EntityInterface {
244    $data = $this->getInitialSources();
245
246    $tree = new SourceTree($data);
247    $data = $tree->getTree();
248
249    $present = [
250      'data' => $data,
251      'hash' => Instance::getUniqId($data),
252      'log' => $this->getInitializationMessage(),
253      'time' => \time(),
254      'user' => (int) $this->currentUser->id(),
255    ];
256
257    $data = [
258      'id' => $this->getInstanceId(),
259      'profileId' => $this->getProfile()->id(),
260      'contexts' => $this->getInitialContext(),
261      'present' => $present,
262    ];
263
264    $storage = $this->entityTypeManager->getStorage('display_builder_instance');
265    /** @var \Drupal\display_builder\InstanceInterface $instance */
266    $instance = $storage->create($data);
267
268    // If we get the data directly from config or content, the data is
269    // considered as already saved.
270    // If we convert it from other tools, or import it from other places, the
271    // user needs to save it themselves after retrieval.
272    if ($this->getSources()) {
273      $instance->setSave($this->getSources());
274    }
275
276    return $instance;
277  }
278
279  /**
280   * Get the message to put in the first log step.
281   *
282   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
283   *   The log message.
284   */
285  protected function getInitializationMessage(): TranslatableMarkup {
286    return $this->t('Initialization of the display builder.');
287  }
288
289  /**
290   * Initialize contexts for this implementation.
291   *
292   * @return array<\Drupal\Core\Plugin\Context\ContextInterface>
293   *   The contexts.
294   */
295  protected function getInitialContext(): array {
296    return [];
297  }
298
299  /**
300   * Initialize sources for this implementation.
301   *
302   * @return array
303   *   The data.
304   */
305  protected function getInitialSources(): array {
306    return $this->getSources();
307  }
308
309  /**
310   * Build profile select when user is allowed to select one.
311   *
312   * @param \Drupal\display_builder\Entity\ProfileInterface|null $profile
313   *   Display Builder profile (or not)
314   * @param bool $mandatory
315   *   (Optional) Is it mandatory to use Display Builder? (for example, in
316   *   Page Layouts or in Entity View display Overrides). If not mandatory,
317   *   the Display Builder is activated only if a Display Builder config entity
318   *   is selected.
319   * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $title
320   *   (Optional) The Select title, default to 'Profile'.
321   *
322   * @return array
323   *   A renderable form array.
324   */
325  protected function buildSelect(?ProfileInterface $profile, bool $mandatory = TRUE, ?TranslatableMarkup $title = NULL): array {
326    $select = [
327      '#type' => 'select',
328      '#title' => $title ?? $this->t('Profile'),
329      '#description' => $this->t('The profile defines the features available in the builder.'),
330      '#options' => $this->getAllowedProfiles(),
331    ];
332
333    if ($profile) {
334      $select['#default_value'] = (string) $profile->id();
335    }
336    elseif (isset($select['#options']['default']) && $mandatory) {
337      $select['#default_value'] = 'default';
338    }
339
340    if ($mandatory) {
341      $select['#required'] = TRUE;
342    }
343    else {
344      $select['#empty_option'] = $this->t('- Disabled -');
345    }
346
347    // Add admin information to link the profiles.
348    if ($this->isCurrentUserAllowedToAdministrate()) {
349      $select['#description'] = [
350        [
351          '#markup' => $select['#description'] . '<br>',
352        ],
353        [
354          '#type' => 'link',
355          '#title' => $this->t('Add and configure display builder profiles'),
356          '#url' => Url::fromRoute('entity.display_builder_profile.collection'),
357          '#suffix' => '.',
358        ],
359      ];
360    }
361
362    return $select;
363  }
364
365  /**
366   * Build link to Display Builder.
367   *
368   * @return array
369   *   A renderable array.
370   */
371  protected function buildLink(): array {
372    return [
373      '#type' => 'html_tag',
374      '#tag' => 'p',
375      '#attributes' => [
376        'class' => ['form-item__description'],
377      ],
378      'content' => [
379        '#type' => 'link',
380        '#title' => $this->t('Build the display'),
381        '#url' => $this->getBuilderUrl(),
382        '#attributes' => [
383          'class' => ['button', 'button--small'],
384        ],
385      ],
386    ];
387  }
388
389  /**
390   * Build disabled profile select when user is not allowed to select one.
391   *
392   * @param \Drupal\display_builder\Entity\ProfileInterface $profile
393   *   Display Builder profile (or not)
394   *
395   * @return array
396   *   A renderable form array.
397   */
398  protected function buildDisabledSelect(ProfileInterface $profile): array {
399    return [
400      '#type' => 'select',
401      '#title' => $this->t('Profile'),
402      '#description' => $this->t('You are not allowed to use Display Builder here.'),
403      '#options' => [
404        (string) $profile->id() => $profile->label(),
405      ],
406      '#disabled' => TRUE,
407    ];
408  }
409
410  /**
411   * Is the current user allowed to use administrate display builder profiles?
412   *
413   * @return bool
414   *   Allowed or not.
415   */
416  protected function isCurrentUserAllowedToAdministrate(): bool {
417    return $this->moduleHandler->moduleExists('display_builder_ui') && $this->currentUser->hasPermission('administer display builder profile');
418  }
419
420}