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