Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 149
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
InstanceListBuilder
0.00% covered (danger)
0.00%
0 / 149
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 13
1260
0.00% covered (danger)
0.00%
0 / 1
 __construct
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
 buildHeader
0.00% covered (danger)
0.00%
0 / 28
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
2
 buildRow
0.00% covered (danger)
0.00%
0 / 24
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
56
 createInstance
0.00% covered (danger)
0.00%
0 / 11
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
2
 getFormId
0.00% covered (danger)
0.00%
0 / 1
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
2
 getSessionFilters
0.00% covered (danger)
0.00%
0 / 5
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
12
 load
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
 render
0.00% covered (danger)
0.00%
0 / 19
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
2
 getEntityIds
0.00% covered (danger)
0.00%
0 / 1
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
2
 applyPager
0.00% covered (danger)
0.00%
0 / 8
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
12
 filterEntities
0.00% covered (danger)
0.00%
0 / 13
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
56
 getInstancesFromProviders
0.00% covered (danger)
0.00%
0 / 9
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
12
 sortEntities
0.00% covered (danger)
0.00%
0 / 19
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder_ui;
6
7use Drupal\Core\Datetime\DateFormatterInterface;
8use Drupal\Core\Entity\EntityInterface;
9use Drupal\Core\Entity\EntityListBuilder;
10use Drupal\Core\Entity\EntityStorageInterface;
11use Drupal\Core\Entity\EntityTypeInterface;
12use Drupal\Core\Entity\EntityTypeManagerInterface;
13use Drupal\Core\Form\FormBuilderInterface;
14use Drupal\Core\Pager\PagerManagerInterface;
15use Drupal\Core\Utility\TableSort;
16use Drupal\display_builder\DisplayBuildablePluginManager;
17use Drupal\display_builder\DisplayBuilderHelpers;
18use Drupal\display_builder_ui\Form\InstanceListFilterForm;
19use Symfony\Component\DependencyInjection\ContainerInterface;
20use Symfony\Component\HttpFoundation\RequestStack;
21use Symfony\Component\HttpFoundation\Session\SessionInterface;
22
23/**
24 * Provides a listing of display builders instances.
25 */
26final class InstanceListBuilder extends EntityListBuilder {
27
28  /**
29   * {@inheritdoc}
30   */
31  protected $limit = 20;
32
33  /**
34   * Cached list of display builder providers.
35   */
36  private array $providers = [];
37
38  /**
39   * {@inheritdoc}
40   */
41  public function __construct(
42    protected EntityTypeInterface $entity_type,
43    EntityStorageInterface $storage,
44    private readonly DateFormatterInterface $dateFormatter,
45    private readonly FormBuilderInterface $formBuilder,
46    private readonly PagerManagerInterface $pagerManager,
47    private readonly RequestStack $requestStack,
48    private readonly EntityTypeManagerInterface $entityTypeManager,
49    private DisplayBuildablePluginManager $displayBuildableManager,
50    private EntityStorageInterface $instanceStorage,
51  ) {
52    parent::__construct($entity_type, $storage);
53
54    // Cache providers so we don't call invokeAll multiple times.
55    $this->providers = $this->displayBuildableManager->getDefinitions();
56  }
57
58  /**
59   * {@inheritdoc}
60   */
61  public function buildHeader(): array {
62    $header = [
63      'id' => [
64        'data' => $this->t('ID'),
65        'class' => ['hidden'],
66      ],
67      'context' => [
68        'data' => $this->t('Context'),
69        'class' => ['priority-medium'],
70      ],
71      'name' => [
72        'data' => $this->t('Instance'),
73        'field' => 'name',
74        'sort' => 'asc',
75        'class' => ['priority-medium'],
76      ],
77      'profile' => $this->t('Profile'),
78      'updated' => [
79        'data' => $this->t('Updated'),
80        'field' => 'updated',
81        'sort' => 'desc',
82        'class' => ['priority-medium', 'db-nowrap'],
83      ],
84      'log' => [
85        'data' => $this->t('Last log'),
86        'class' => ['priority-low'],
87      ],
88    ];
89
90    return $header + parent::buildHeader();
91  }
92
93  /**
94   * {@inheritdoc}
95   */
96  public function buildRow(EntityInterface $instance): array {
97    /** @var \Drupal\display_builder\InstanceInterface $instance */
98    $instance_id = (string) $instance->id();
99
100    $row = [];
101
102    $row['id']['data'] = $instance_id;
103    $row['id']['class'] = ['hidden'];
104
105    $type = '-';
106
107    foreach ($this->providers as $provider) {
108      if (\str_starts_with($instance_id, $provider['instance_prefix'])) {
109        $type = $provider['label'];
110
111        break;
112      }
113    }
114
115    // Set a human readable name from id.
116    $row['context']['data'] = $type;
117    $row['context']['class'] = ['priority-medium'];
118
119    $row['name']['data'] = $instance->label();
120    $row['name']['class'] = ['priority-medium'];
121
122    $row['profile']['data'] = $instance->getProfile()?->label() ?? '';
123
124    /** @var \Drupal\display_builder\Plugin\Field\FieldType\HistoryStep $present */
125    $present = $instance->getCurrent() ?? NULL;
126    $row['updated']['data'] = ($present && $present->getTime()) ? DisplayBuilderHelpers::formatTime($this->dateFormatter, (int) $present->getTime()) : '-';
127    $row['updated']['class'] = ['priority-medium', 'db-nowrap'];
128    $row['log']['data'] = ($present && $present->getLog()) ? $present->getLog() : '-';
129    $row['log']['class'] = ['priority-low'];
130
131    $result = [
132      'data' => $row + parent::buildRow($instance),
133      'class' => $instance_id,
134    ];
135
136    return $result;
137  }
138
139  /**
140   * {@inheritdoc}
141   */
142  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type): self {
143    return new self(
144      $entity_type,
145      $container->get('entity_type.manager')->getStorage($entity_type->id()),
146      $container->get('date.formatter'),
147      $container->get('form_builder'),
148      $container->get('pager.manager'),
149      $container->get('request_stack'),
150      $container->get('entity_type.manager'),
151      $container->get('plugin.manager.display_buildable'),
152      $container->get('entity_type.manager')->getStorage('display_builder_instance'),
153    );
154  }
155
156  /**
157   * {@inheritdoc}
158   */
159  public function getFormId(): string {
160    return 'display_builder_instance_list_builder';
161  }
162
163  /**
164   * Retrieve filter values from the current request (GET).
165   *
166   * @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
167   *   Current session.
168   *
169   * @return array
170   *   Associative array of filters.
171   */
172  public static function getSessionFilters(SessionInterface $session): array {
173    $filters = $session->get('db_instances_overview_filter', []);
174
175    return [
176      'context' => isset($filters['context']) ? (string) $filters['context'] : '',
177      'name' => isset($filters['name']) ? (string) $filters['name'] : '',
178    ];
179  }
180
181  /**
182   * {@inheritdoc}
183   */
184  public function load(): array {
185    $entities = $this->getInstancesFromProviders();
186
187    // Apply filters from session and create missing instances if any.
188    $entities = $this->filterEntities($entities);
189
190    // Build headers & request once.
191    $headers = $this->buildHeader();
192    $request = $this->requestStack->getCurrentRequest() ?? \Drupal::request();
193    $order = TableSort::getOrder($headers, $request);
194    $direction = TableSort::getSort($headers, $request);
195    $sortKey = $order['sql'] ?? 'updated';
196
197    // Sort using a dedicated helper.
198    $this->sortEntities($entities, $sortKey, $direction);
199
200    // Apply pager and return the page slice.
201    return $this->applyPager($entities);
202  }
203
204  /**
205   * {@inheritdoc}
206   */
207  public function render(): array {
208    $build = parent::render();
209
210    $build['#attached']['library'][] = 'display_builder_ui/instance_list';
211
212    $info = $this->t('Instances are versions of displays (entity views, page layouts, views...) currently under work.');
213    $info .= '<br>';
214    $info .= $this->t('They are created automatically from the displays and saved in the configuration when published.');
215
216    $build['notice'] = [
217      '#type' => 'html_tag',
218      '#tag' => 'p',
219      '#value' => $info,
220      '#attributes' => ['class' => ['description']],
221      '#weight' => -11,
222    ];
223
224    $build['filters'] = $this->formBuilder->getForm(InstanceListFilterForm::class, $this->providers);
225    $build['filters']['#weight'] = -10;
226
227    $build['pager'] = [
228      '#type' => 'pager',
229      '#weight' => 100,
230    ];
231
232    return $build;
233  }
234
235  /**
236   * {@inheritdoc}
237   */
238  protected function getEntityIds(): array {
239    // To avoid implementing EntityStorageInterface::getQuery().
240    return \array_keys($this->getStorage()->loadMultiple());
241  }
242
243  /**
244   * Apply Drupal pager to an array of entities.
245   *
246   * @param array $entities
247   *   The full list of (already filtered & sorted) entities.
248   *
249   * @return array
250   *   The paged slice of entities for the current page.
251   */
252  private function applyPager(array $entities): array {
253    $total = \count($entities);
254    $limit = (int) $this->limit;
255
256    if ($limit <= 0 || $total <= $limit) {
257      // No paging needed.
258      return $entities;
259    }
260
261    $pager = $this->pagerManager->createPager($total, $limit);
262    $current_page = $pager->getCurrentPage();
263    $offset = $current_page * $limit;
264
265    return \array_slice($entities, $offset, $limit, TRUE);
266  }
267
268  /**
269   * Filter the loaded entities according to GET filters.
270   *
271   * @param array $entities
272   *   Loaded entities.
273   *
274   * @return array
275   *   Filtered entities.
276   */
277  private function filterEntities(array $entities): array {
278    $filters = self::getSessionFilters($this->requestStack->getSession());
279    $context = $filters['context'] ?? '';
280    $name = $filters['name'] ?? '';
281
282    $result = [];
283
284    foreach ($entities as $entity) {
285      if ($context !== '' && $context !== $entity['context']) {
286        continue;
287      }
288
289      if ($name !== '' && !\str_contains($entity['id'] ?? '', $name)) {
290        continue;
291      }
292
293      if (!$entity['instance']) {
294        $entity['instance'] = $this->instanceStorage->create(['id' => $entity['id'], 'label' => $entity['id']]);
295      }
296      $result[] = $entity['instance'];
297    }
298
299    return $result;
300  }
301
302  /**
303   * Get instances from providers definitions.
304   *
305   * @return array
306   *   List of instances indexed by id.
307   */
308  private function getInstancesFromProviders(): array {
309    $instances = [];
310
311    foreach ($this->providers as $provider_id => $provider) {
312      foreach ($provider['class']::collectInstances($this->entityTypeManager) as $instance_id => $instance) {
313        $instances[$instance_id] = [
314          'id' => $instance_id,
315          'instance' => $instance,
316          'context' => $provider_id,
317        ];
318      }
319    }
320
321    return $instances;
322  }
323
324  /**
325   * Sort the entities array in place according to provided sort key/direction.
326   *
327   * @param array $entities
328   *   Entities to sort (passed by reference).
329   * @param string $sortKey
330   *   The SQL sort key from TableSort.
331   * @param string|int $direction
332   *   Sort direction value.
333   */
334  private function sortEntities(array &$entities, string $sortKey, $direction): void {
335    // Factor to invert comparison when descending.
336    $factor = ($direction === TableSort::DESC) ? -1 : 1;
337
338    switch ($sortKey) {
339      case 'updated':
340        \usort($entities, static function ($a, $b) use ($factor) {
341          $aTime = (int) ($a->present->getTime() ?? 0);
342          $bTime = (int) ($b->present->getTime() ?? 0);
343
344          // Default comparator is ascending, multiply by factor to handle desc.
345          return $factor * ($aTime <=> $bTime);
346        });
347
348        break;
349
350      case 'name':
351        \usort($entities, static function ($a, $b) use ($factor) {
352          $aName = $a->label();
353          $bName = $b->label();
354
355          // Use case-insensitive string comparison.
356          return $factor * \strcasecmp($aName, $bName);
357        });
358
359        break;
360
361      default:
362        // Unknown sort: fallback to updated desc behavior for predictability.
363        \usort($entities, static function ($a, $b) {
364          return (int) ($b->present->getTime() ?? 0) <=> (int) ($a->present->getTime() ?? 0);
365        });
366
367        break;
368    }
369  }
370
371}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.