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

Paths

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