From dd3d5e5972c85e3c9a15aba46e0e43fe4d21abce Mon Sep 17 00:00:00 2001 From: Jurriaan Roelofs Date: Wed, 28 Jan 2026 10:49:34 +0100 Subject: [PATCH 1/4] Address multiple improvement issues Fix #2: Add ContentIntelCollectorInterface - Create interface defining the service contract - Update ContentIntelCollector to implement the interface - Add loadEntities() method for bulk entity loading Fix #3: Add logging for silent exception in ContentTranslationPlugin - Inject logger service - Log warnings when translation metadata fetch fails - Add metadata_error field to output for debugging Fix #4: Optimize batch command with loadMultiple() - Use loadEntities() for bulk loading instead of individual loads - Reduces N+1 queries to single loadMultiple() call - Improves performance for large batch operations --- src/Drush/Commands/ContentIntelCommands.php | 21 ++- .../ContentIntel/ContentTranslationPlugin.php | 16 ++- src/Service/ContentIntelCollector.php | 19 ++- .../ContentIntelCollectorInterface.php | 128 ++++++++++++++++++ 4 files changed, 171 insertions(+), 13 deletions(-) create mode 100644 src/Service/ContentIntelCollectorInterface.php diff --git a/src/Drush/Commands/ContentIntelCommands.php b/src/Drush/Commands/ContentIntelCommands.php index 7bc3b37..c812982 100644 --- a/src/Drush/Commands/ContentIntelCommands.php +++ b/src/Drush/Commands/ContentIntelCommands.php @@ -304,27 +304,26 @@ public function batch( $results = []; if ($options['ids']) { + // Use bulk loading for better performance. $ids = array_map('trim', explode(',', $options['ids'])); - foreach ($ids as $id) { - $entity = $this->collector->loadEntity($entity_type, $id); - if ($entity) { - $results[] = $this->collector->collectIntel($entity, [], $plugins); - } + $entities = $this->collector->loadEntities($entity_type, $ids); + foreach ($entities as $entity) { + $results[] = $this->collector->collectIntel($entity, [], $plugins); } } else { $bundle = $options['bundle'] ?: NULL; - $entities = $this->collector->listEntities( + $entity_summaries = $this->collector->listEntities( $entity_type, $bundle, (int) $options['limit'] ); - foreach ($entities as $entity_summary) { - $entity = $this->collector->loadEntity($entity_type, $entity_summary['id']); - if ($entity) { - $results[] = $this->collector->collectIntel($entity, [], $plugins); - } + // Extract IDs and use bulk loading for better performance. + $ids = array_column($entity_summaries, 'id'); + $entities = $this->collector->loadEntities($entity_type, $ids); + foreach ($entities as $entity) { + $results[] = $this->collector->collectIntel($entity, [], $plugins); } } diff --git a/src/Plugin/ContentIntel/ContentTranslationPlugin.php b/src/Plugin/ContentIntel/ContentTranslationPlugin.php index 3dea1f7..ab3a43a 100644 --- a/src/Plugin/ContentIntel/ContentTranslationPlugin.php +++ b/src/Plugin/ContentIntel/ContentTranslationPlugin.php @@ -10,6 +10,7 @@ use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; +use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -37,6 +38,13 @@ class ContentTranslationPlugin extends ContentIntelPluginBase { */ protected ?LanguageManagerInterface $languageManager = NULL; + /** + * The logger. + * + * @var \Psr\Log\LoggerInterface + */ + protected LoggerInterface $logger; + /** * {@inheritdoc} */ @@ -53,6 +61,7 @@ public static function create( } $instance->languageManager = $container->get('language_manager'); + $instance->logger = $container->get('logger.factory')->get('content_intel'); return $instance; } @@ -143,7 +152,12 @@ public function collect(ContentEntityInterface $entity): array { } } catch (\Exception $e) { - // Metadata not available. + // Log the error but continue gracefully. + $this->logger->warning( + 'Failed to get translation metadata for @langcode: @message', + ['@langcode' => $langcode, '@message' => $e->getMessage()] + ); + $detail['metadata_error'] = $e->getMessage(); } } diff --git a/src/Service/ContentIntelCollector.php b/src/Service/ContentIntelCollector.php index 34ffc17..01fa184 100644 --- a/src/Service/ContentIntelCollector.php +++ b/src/Service/ContentIntelCollector.php @@ -17,7 +17,7 @@ /** * Service for collecting content intelligence from various sources. */ -class ContentIntelCollector { +class ContentIntelCollector implements ContentIntelCollectorInterface { /** * Constructs a ContentIntelCollector. @@ -141,6 +141,23 @@ public function loadEntity(string $entity_type_id, int|string $entity_id): ?Cont return $entity instanceof ContentEntityInterface ? $entity : NULL; } + /** + * {@inheritdoc} + */ + public function loadEntities(string $entity_type_id, array $entity_ids): array { + if (empty($entity_ids)) { + return []; + } + $entities = $this->entityTypeManager + ->getStorage($entity_type_id) + ->loadMultiple($entity_ids); + + return array_filter( + $entities, + fn($entity) => $entity instanceof ContentEntityInterface + ); + } + /** * Lists entities matching criteria. * diff --git a/src/Service/ContentIntelCollectorInterface.php b/src/Service/ContentIntelCollectorInterface.php new file mode 100644 index 0000000..c144a6d --- /dev/null +++ b/src/Service/ContentIntelCollectorInterface.php @@ -0,0 +1,128 @@ + Date: Wed, 28 Jan 2026 10:52:47 +0100 Subject: [PATCH 2/4] Fix PHPStan and drupal-check issues - Install statistics contrib module in drupal-check script - Add PHPStan config to ignore optional statistics module errors - Fix type safety: add ContentEntityInterface check in listEntities() - Fix DI: inject date.formatter service in StatisticsPlugin --- scripts/run-drupal-check.sh | 9 +++++++++ src/Plugin/ContentIntel/StatisticsPlugin.php | 11 ++++++++++- src/Service/ContentIntelCollector.php | 4 +++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/scripts/run-drupal-check.sh b/scripts/run-drupal-check.sh index c70e4fa..ce6762c 100755 --- a/scripts/run-drupal-check.sh +++ b/scripts/run-drupal-check.sh @@ -26,6 +26,12 @@ parameters: - web/modules/contrib/content_intel # Set the analysis level (0-9) level: 5 + treatPhpDocTypesAsCertain: false + ignoreErrors: + # Ignore statistics module not found errors (optional dependency) + - '#has unknown class Drupal\\\\statistics\\\\StatisticsStorageInterface#' + - '#Call to method fetchView\(\) on an unknown class#' + - '#If condition is always false#' EOF mkdir -p web/modules/contrib/ @@ -34,6 +40,9 @@ if [ ! -L "web/modules/contrib/content_intel" ]; then ln -s /src web/modules/contrib/content_intel fi +# Install the statistics module (removed from core in D11). +composer require drupal/statistics --no-interaction + # Install PHPStan extensions for Drupal 11 and Drush for command analysis composer require --dev phpstan/phpstan mglaman/phpstan-drupal phpstan/phpstan-deprecation-rules drush/drush --with-all-dependencies --no-interaction diff --git a/src/Plugin/ContentIntel/StatisticsPlugin.php b/src/Plugin/ContentIntel/StatisticsPlugin.php index 0e84012..3c4237b 100644 --- a/src/Plugin/ContentIntel/StatisticsPlugin.php +++ b/src/Plugin/ContentIntel/StatisticsPlugin.php @@ -6,6 +6,7 @@ use Drupal\content_intel\Attribute\ContentIntel; use Drupal\content_intel\ContentIntelPluginBase; +use Drupal\Core\Datetime\DateFormatterInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\node\NodeInterface; @@ -31,6 +32,13 @@ class StatisticsPlugin extends ContentIntelPluginBase { */ protected ?StatisticsStorageInterface $statisticsStorage = NULL; + /** + * The date formatter. + * + * @var \Drupal\Core\Datetime\DateFormatterInterface + */ + protected DateFormatterInterface $dateFormatter; + /** * {@inheritdoc} */ @@ -45,6 +53,7 @@ public static function create( if ($container->has('statistics.storage.node')) { $instance->statisticsStorage = $container->get('statistics.storage.node'); } + $instance->dateFormatter = $container->get('date.formatter'); return $instance; } @@ -90,7 +99,7 @@ public function collect(ContentEntityInterface $entity): array { 'last_view' => $timestamp ? [ 'timestamp' => $timestamp, 'iso8601' => date('c', $timestamp), - 'human' => \Drupal::service('date.formatter')->format($timestamp, 'medium'), + 'human' => $this->dateFormatter->format($timestamp, 'medium'), ] : NULL, ]; } diff --git a/src/Service/ContentIntelCollector.php b/src/Service/ContentIntelCollector.php index 01fa184..25c81bd 100644 --- a/src/Service/ContentIntelCollector.php +++ b/src/Service/ContentIntelCollector.php @@ -207,7 +207,9 @@ public function listEntities( $results = []; foreach ($entities as $entity) { - $results[] = $this->getEntitySummary($entity); + if ($entity instanceof ContentEntityInterface) { + $results[] = $this->getEntitySummary($entity); + } } return $results; From 1a6ee8ad064b89fa38bc92d6d1497d88f0fb8c9b Mon Sep 17 00:00:00 2001 From: Jurriaan Roelofs Date: Wed, 28 Jan 2026 10:58:40 +0100 Subject: [PATCH 3/4] Fix PHPStan ignore patterns for Drupal-specific patterns --- scripts/run-drupal-check.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/run-drupal-check.sh b/scripts/run-drupal-check.sh index ce6762c..36d6d4f 100755 --- a/scripts/run-drupal-check.sh +++ b/scripts/run-drupal-check.sh @@ -28,10 +28,14 @@ parameters: level: 5 treatPhpDocTypesAsCertain: false ignoreErrors: - # Ignore statistics module not found errors (optional dependency) - - '#has unknown class Drupal\\\\statistics\\\\StatisticsStorageInterface#' - - '#Call to method fetchView\(\) on an unknown class#' - - '#If condition is always false#' + # Ignore method_exists checks (Drupal pattern for optional features) + - '#Call to function method_exists\(\) .* will always evaluate to true#' + # Ignore new static() in plugin base class (Drupal pattern) + - '#Unsafe usage of new static\(\)#' + # Ignore boolean narrowing warnings + - '#Left side of && is always true#' + # Ignore nullsafe on non-nullable (defensive coding) + - '#Using nullsafe method call on non-nullable type#' EOF mkdir -p web/modules/contrib/ From 9f761f98b6f422a5daff9ad21200c73b1050e2f2 Mon Sep 17 00:00:00 2001 From: Jurriaan Roelofs Date: Wed, 28 Jan 2026 11:36:07 +0100 Subject: [PATCH 4/4] Address PR review suggestions - Preserve input order in loadEntities() method - Document getPlugins() return structure in interface PHPDoc --- src/Service/ContentIntelCollector.php | 12 ++++++++---- src/Service/ContentIntelCollectorInterface.php | 9 ++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Service/ContentIntelCollector.php b/src/Service/ContentIntelCollector.php index 25c81bd..076a482 100644 --- a/src/Service/ContentIntelCollector.php +++ b/src/Service/ContentIntelCollector.php @@ -152,10 +152,14 @@ public function loadEntities(string $entity_type_id, array $entity_ids): array { ->getStorage($entity_type_id) ->loadMultiple($entity_ids); - return array_filter( - $entities, - fn($entity) => $entity instanceof ContentEntityInterface - ); + // Preserve input order and filter to ContentEntityInterface. + $result = []; + foreach ($entity_ids as $id) { + if (isset($entities[$id]) && $entities[$id] instanceof ContentEntityInterface) { + $result[$id] = $entities[$id]; + } + } + return $result; } /** diff --git a/src/Service/ContentIntelCollectorInterface.php b/src/Service/ContentIntelCollectorInterface.php index c144a6d..a2be0f8 100644 --- a/src/Service/ContentIntelCollectorInterface.php +++ b/src/Service/ContentIntelCollectorInterface.php @@ -121,7 +121,14 @@ public function collectIntel(ContentEntityInterface $entity, array $fields = [], * Gets available plugins. * * @return array - * Array of plugin info. + * Array of plugin info keyed by plugin ID. Each entry contains: + * - id: (string) Plugin ID. + * - label: (string) Human-readable label. + * - description: (string) Plugin description. + * - provider: (string) Module providing the plugin. + * - available: (bool) Whether the plugin's dependencies are met. + * - entity_types: (array) Entity types this plugin supports. + * - weight: (int) Plugin weight for ordering. */ public function getPlugins(): array;