diff --git a/README.md b/README.md index e0e122f..ad5ca2a 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ it's easy to forget that they [don't work with Term Merge](https://www.drupal.or # Usage 1. install -2. Go to `/admin/zombies` to see the list of problems. -3. Fix the broken links manually. +2. Go to `/admin/reports` and select `Zombie Reference Hunter`. +3. Click on the Source ID to navigate to the edit page. # Who to Blame Rosie Le Faive (rlefaive@upei.ca) and the human behind / the AI Bot that made the original. diff --git a/src/Controller/ZombieReferenceHunterController.php b/src/Controller/ZombieReferenceHunterController.php index e02b17b..02fa37d 100644 --- a/src/Controller/ZombieReferenceHunterController.php +++ b/src/Controller/ZombieReferenceHunterController.php @@ -8,6 +8,10 @@ use Drupal\Core\Messenger\MessengerTrait; use Drupal\Core\Session\AccountInterface; use Drupal\zombie_reference_hunter\Service\ZombieReferenceHunterQuery; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Link; +use Drupal\Core\Url; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RedirectResponse; /** * Creating a controller. @@ -42,30 +46,108 @@ class ZombieReferenceHunterController extends ControllerBase * Returns the Report page. * */ - public function view(): array + public function view(Request $request): array|RedirectResponse { - $issues = $this->query->scan(); + // Force a new scan when requested. + if ($request->query->getBoolean('rescan')) { + $this->query->rebuildCache(); + + // Redirect back to the clean report URL so refreshing the browser + // doesn't perform another scan. + return new RedirectResponse( + Url::fromRoute('zombie_reference_hunter.prepare')->toString() + ); + } + + // This now returns cached results unless no cache exists yet. + $issues = $this->query->getIssues(); $count = count($issues); + $build = [ '#cache' => ['max-age' => 0], ]; $build['summary'] = [ - '#markup' => $this->t('Found @count broken reference(s).', ['@count' => $count]), + '#type' => 'container', + ]; + + $build['summary']['count'] = [ + '#markup' => $this->formatPlural( + $count, + 'Found 1 broken reference.', + 'Found @count broken references.', + ), + ]; + + // Rescan button. + $build['summary']['rescan'] = [ + '#type' => 'link', + '#title' => $this->t('Rescan'), + '#url' => Url::fromRoute( + 'zombie_reference_hunter.prepare', + [], + [ + 'query' => [ + 'rescan' => 1, + ], + ] + ), + '#attributes' => [ + 'class' => [ + 'button', + 'button--primary', + ], + 'style' => 'margin-left: 1em;', + ], ]; if ($count === 0) { $build['empty'] = [ '#markup' => $this->t('No broken references found.'), ]; + return $build; } + // Pager. + $limit = 50; + + $pager = \Drupal::service('pager.manager') + ->createPager($count, $limit); + + $current_page = $pager->getCurrentPage(); + + // Only build rows for the current page. + $issues = array_slice( + $issues, + $current_page * $limit, + $limit + ); + $rows = []; + foreach ($issues as $issue) { + $entity_type_id = $issue['source_entity_type']; + $entity_type = $this->entityTypeManager() + ->getDefinition($entity_type_id); + + $source_id = $issue['source_id']; + + if ($entity_type->hasLinkTemplate('edit-form')) { + $source_id = Link::fromTextAndUrl( + (string) $issue['source_id'], + Url::fromRoute( + 'entity.' . $entity_type_id . '.edit_form', + [ + $entity_type_id => $issue['source_id'], + ] + ) + ); + } + $rows[] = [ - $issue['source_entity_type'], - $issue['source_id'], + $entity_type_id, + $source_id, $issue['field_name'], $issue['field_type'], $issue['target_type'], @@ -87,6 +169,10 @@ class ZombieReferenceHunterController extends ControllerBase '#empty' => $this->t('No broken references found.'), ]; + $build['pager'] = [ + '#type' => 'pager', + ]; + return $build; } diff --git a/src/Service/ZombieReferenceHunterQuery.php b/src/Service/ZombieReferenceHunterQuery.php index 63e9615..506456a 100644 --- a/src/Service/ZombieReferenceHunterQuery.php +++ b/src/Service/ZombieReferenceHunterQuery.php @@ -7,13 +7,26 @@ use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Logger\LoggerChannelInterface; +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheBackendInterface; + +class ZombieReferenceHunterQuery { -class ZombieReferenceHunterQuery -{ /** * Default number of entities loaded per batch. */ const BATCH_SIZE = 200; + + /** + * Cache ID for scan results. + */ + const CACHE_ID = 'zombie_reference_hunter.issues'; + + /** + * Cached scan results. + */ + protected CacheBackendInterface $cache; + /** * The entity type manager. * @@ -48,11 +61,13 @@ class ZombieReferenceHunterQuery public function __construct( EntityTypeManagerInterface $entityTypeManager, EntityFieldManagerInterface $entityFieldManager, - LoggerChannelInterface $logger + LoggerChannelInterface $logger, + CacheBackendInterface $cache ) { $this->entityTypeManager = $entityTypeManager; $this->entityFieldManager = $entityFieldManager; $this->logger = $logger; + $this->cache = $cache; } /** @@ -63,8 +78,8 @@ class ZombieReferenceHunterQuery */ protected function getFieldMaps(): array { return [ -// 'entity_reference' => $this->entityFieldManager->getFieldMapByFieldType('entity_reference'), -// 'entity_reference_revisions' => $this->entityFieldManager->getFieldMapByFieldType('entity_reference_revisions'), + // 'entity_reference' => $this->entityFieldManager->getFieldMapByFieldType('entity_reference'), + // 'entity_reference_revisions' => $this->entityFieldManager->getFieldMapByFieldType('entity_reference_revisions'), 'typed_relation' => $this->entityFieldManager->getFieldMapByFieldType('typed_relation'), ]; } @@ -90,7 +105,7 @@ class ZombieReferenceHunterQuery $source_storage = $this->entityTypeManager->getStorage($entity_type_id); $storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id); } - catch (PluginNotFoundException | InvalidPluginDefinitionException $exception) { + catch (PluginNotFoundException|InvalidPluginDefinitionException $exception) { $this->logger->warning('Skipping entity type %type due to storage error: %message', [ '%type' => $entity_type_id, '%message' => $exception->getMessage(), @@ -104,9 +119,9 @@ class ZombieReferenceHunterQuery } $field_storage = $storage_definitions[$field_name]; -// $field_storage->isComputed() || !$field_storage->isQueryable()) { -// continue; -// } + // $field_storage->isComputed() || !$field_storage->isQueryable()) { + // continue; + // } $target_type = $field_storage->getSetting('target_type'); if (empty($target_type)) { @@ -116,7 +131,7 @@ class ZombieReferenceHunterQuery try { $target_storage = $this->entityTypeManager->getStorage($target_type); } - catch (PluginNotFoundException | InvalidPluginDefinitionException $exception) { + catch (PluginNotFoundException|InvalidPluginDefinitionException $exception) { $this->logger->warning('Skipping target type %type due to storage error: %message', [ '%type' => $target_type, '%message' => $exception->getMessage(), @@ -187,4 +202,47 @@ class ZombieReferenceHunterQuery return $issues; } + + /** + * Gets the current report results. + * + * Uses cached results when available. If there is no cached scan, + * a new scan is performed automatically. + * + * @return array + * Broken reference records. + */ + public function getIssues(): array { + if ($cache = $this->cache->get(self::CACHE_ID)) { + return $cache->data; + } + + return $this->rebuildCache(); + } + + /** + * Performs a fresh scan and caches the results. + * + * @return array + * Broken reference records. + */ + public function rebuildCache(): array { + $issues = $this->scan(); + + $this->cache->set( + self::CACHE_ID, + $issues, + Cache::PERMANENT + ); + + return $issues; + } + + /** + * Clears the cached scan. + */ + public function clearCache(): void { + $this->cache->delete(self::CACHE_ID); + } + } diff --git a/zombie_reference_hunter.links.menu.yml b/zombie_reference_hunter.links.menu.yml new file mode 100644 index 0000000..250fb32 --- /dev/null +++ b/zombie_reference_hunter.links.menu.yml @@ -0,0 +1,6 @@ +zombie_reference_hunter.prepare: + title: 'Zombie Reference Hunter' + description: 'Find and repair zombie entity references.' + route_name: zombie_reference_hunter.prepare + parent: system.admin_reports + weight: 10 diff --git a/zombie_reference_hunter.routing.yml b/zombie_reference_hunter.routing.yml index 018bea0..6e47fa4 100644 --- a/zombie_reference_hunter.routing.yml +++ b/zombie_reference_hunter.routing.yml @@ -1,6 +1,7 @@ zombie_reference_hunter.prepare: - path: '/admin/zombies' + path: '/admin/reports/zombies' defaults: _controller: '\Drupal\zombie_reference_hunter\Controller\ZombieReferenceHunterController::view' + _title: 'Zombie Reference Hunter' requirements: _custom_access: '\Drupal\zombie_reference_hunter\Controller\ZombieReferenceHunterController::access' diff --git a/zombie_reference_hunter.services.yml b/zombie_reference_hunter.services.yml index 636e974..844ca85 100644 --- a/zombie_reference_hunter.services.yml +++ b/zombie_reference_hunter.services.yml @@ -6,6 +6,7 @@ services: - '@entity_type.manager' - '@entity_field.manager' - '@logger.channel.zombie_reference_hunter' + - '@cache.data' logger.channel.zombie_reference_hunter: parent: logger.channel_base