Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
0.00% |
0 / 179 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 13 |
CRAP | |
0.00% |
0 / 1 |
||
| InstanceListBuilder | |
0.00% |
0 / 179 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 13 |
1190 | |
0.00% |
0 / 1 |
||
| __construct | |
0.00% |
0 / 2 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
2 | |||||
| buildHeader | |
0.00% |
0 / 28 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
2 | |||||
| buildRow | |
0.00% |
0 / 21 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
12 | |||||
| createInstance | |
0.00% |
0 / 10 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
2 | |||||
| getSessionFilters | |
0.00% |
0 / 6 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
12 | |||||
| getSessionSort | |
0.00% |
0 / 2 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
2 | |||||
| load | |
0.00% |
0 / 17 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
12 | |||||
| render | |
0.00% |
0 / 28 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
12 | |||||
| getOperations | |
0.00% |
0 / 18 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
2 | |||||
| applyPager | |
0.00% |
0 / 8 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
12 | |||||
| filterEntities | |
0.00% |
0 / 11 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
42 | |||||
| getInstancesFromProviders | |
0.00% |
0 / 9 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
12 | |||||
| sortEntities | |
0.00% |
0 / 19 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
30 | |||||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Drupal\display_builder_ui; |
| 6 | |
| 7 | use Drupal\Core\Cache\CacheableMetadata; |
| 8 | use Drupal\Core\Datetime\DateFormatterInterface; |
| 9 | use Drupal\Core\Entity\EntityInterface; |
| 10 | use Drupal\Core\Entity\EntityListBuilder; |
| 11 | use Drupal\Core\Entity\EntityStorageInterface; |
| 12 | use Drupal\Core\Entity\EntityTypeInterface; |
| 13 | use Drupal\Core\Entity\EntityTypeManagerInterface; |
| 14 | use Drupal\Core\Form\FormBuilderInterface; |
| 15 | use Drupal\Core\Pager\PagerManagerInterface; |
| 16 | use Drupal\Core\StringTranslation\TranslatableMarkup; |
| 17 | use Drupal\Core\Utility\TableSort; |
| 18 | use Drupal\display_builder\DisplayBuildablePluginManager; |
| 19 | use Drupal\display_builder\DisplayBuilderHelpers; |
| 20 | use Drupal\display_builder_ui\Form\InstanceListFilterForm; |
| 21 | use Symfony\Component\DependencyInjection\ContainerInterface; |
| 22 | use Symfony\Component\HttpFoundation\RequestStack; |
| 23 | use Symfony\Component\HttpFoundation\Session\SessionInterface; |
| 24 | |
| 25 | /** |
| 26 | * Provides a listing of display builders instances. |
| 27 | */ |
| 28 | final 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 | } |
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.