Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
0.00% |
0 / 149 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 13 |
CRAP | |
0.00% |
0 / 1 |
||
| InstanceListBuilder | |
0.00% |
0 / 149 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 13 |
1260 | |
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 / 24 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
56 | |||||
| createInstance | |
0.00% |
0 / 11 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
2 | |||||
| getFormId | |
0.00% |
0 / 1 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
2 | |||||
| getSessionFilters | |
0.00% |
0 / 5 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
12 | |||||
| load | |
0.00% |
0 / 9 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
2 | |||||
| render | |
0.00% |
0 / 19 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
2 | |||||
| getEntityIds | |
0.00% |
0 / 1 |
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 / 13 |
n/a |
0 / 0 |
n/a |
0 / 0 |
|
0.00% |
0 / 1 |
56 | |||||
| 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\Datetime\DateFormatterInterface; |
| 8 | use Drupal\Core\Entity\EntityInterface; |
| 9 | use Drupal\Core\Entity\EntityListBuilder; |
| 10 | use Drupal\Core\Entity\EntityStorageInterface; |
| 11 | use Drupal\Core\Entity\EntityTypeInterface; |
| 12 | use Drupal\Core\Entity\EntityTypeManagerInterface; |
| 13 | use Drupal\Core\Form\FormBuilderInterface; |
| 14 | use Drupal\Core\Pager\PagerManagerInterface; |
| 15 | use Drupal\Core\Utility\TableSort; |
| 16 | use Drupal\display_builder\DisplayBuildablePluginManager; |
| 17 | use Drupal\display_builder\DisplayBuilderHelpers; |
| 18 | use Drupal\display_builder_ui\Form\InstanceListFilterForm; |
| 19 | use Symfony\Component\DependencyInjection\ContainerInterface; |
| 20 | use Symfony\Component\HttpFoundation\RequestStack; |
| 21 | use Symfony\Component\HttpFoundation\Session\SessionInterface; |
| 22 | |
| 23 | /** |
| 24 | * Provides a listing of display builders instances. |
| 25 | */ |
| 26 | final 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 | } |
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.