Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions scripts/run-drupal-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ parameters:
- web/modules/contrib/content_intel
# Set the analysis level (0-9)
level: 5
treatPhpDocTypesAsCertain: false
ignoreErrors:
# 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/
Expand All @@ -34,6 +44,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

Expand Down
21 changes: 10 additions & 11 deletions src/Drush/Commands/ContentIntelCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
16 changes: 15 additions & 1 deletion src/Plugin/ContentIntel/ContentTranslationPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -37,6 +38,13 @@ class ContentTranslationPlugin extends ContentIntelPluginBase {
*/
protected ?LanguageManagerInterface $languageManager = NULL;

/**
* The logger.
*
* @var \Psr\Log\LoggerInterface
*/
protected LoggerInterface $logger;

/**
* {@inheritdoc}
*/
Expand All @@ -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;
}
Expand Down Expand Up @@ -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();
}
}

Expand Down
11 changes: 10 additions & 1 deletion src/Plugin/ContentIntel/StatisticsPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,6 +32,13 @@ class StatisticsPlugin extends ContentIntelPluginBase {
*/
protected ?StatisticsStorageInterface $statisticsStorage = NULL;

/**
* The date formatter.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected DateFormatterInterface $dateFormatter;

/**
* {@inheritdoc}
*/
Expand All @@ -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;
}
Expand Down Expand Up @@ -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,
];
}
Expand Down
27 changes: 25 additions & 2 deletions src/Service/ContentIntelCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
/**
* Service for collecting content intelligence from various sources.
*/
class ContentIntelCollector {
class ContentIntelCollector implements ContentIntelCollectorInterface {

/**
* Constructs a ContentIntelCollector.
Expand Down Expand Up @@ -141,6 +141,27 @@ 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);

// 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;
}

/**
* Lists entities matching criteria.
*
Expand Down Expand Up @@ -190,7 +211,9 @@ public function listEntities(

$results = [];
foreach ($entities as $entity) {
$results[] = $this->getEntitySummary($entity);
if ($entity instanceof ContentEntityInterface) {
$results[] = $this->getEntitySummary($entity);
}
}

return $results;
Expand Down
135 changes: 135 additions & 0 deletions src/Service/ContentIntelCollectorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

declare(strict_types=1);

namespace Drupal\content_intel\Service;

use Drupal\Core\Entity\ContentEntityInterface;

/**
* Interface for content intelligence collector service.
*
* This service provides methods to analyze content entities,
* retrieve performance data, and generate insights.
*/
interface ContentIntelCollectorInterface {

/**
* Gets all available entity types that can be analyzed.
*
* @return array
* Array of entity type info with id, label, and bundles.
*/
public function getEntityTypes(): array;

/**
* Gets bundles for an entity type.
*
* @param string $entity_type_id
* The entity type ID.
*
* @return array
* Array of bundle info with id and label.
*/
public function getBundles(string $entity_type_id): array;

/**
* Gets field definitions for an entity type/bundle.
*
* @param string $entity_type_id
* The entity type ID.
* @param string|null $bundle
* The bundle, or NULL for base fields only.
*
* @return array
* Array of field info.
*/
public function getFields(string $entity_type_id, ?string $bundle = NULL): array;

/**
* Loads a single entity.
*
* @param string $entity_type_id
* The entity type ID.
* @param int|string $entity_id
* The entity ID.
*
* @return \Drupal\Core\Entity\ContentEntityInterface|null
* The loaded entity or NULL if not found.
*/
public function loadEntity(string $entity_type_id, int|string $entity_id): ?ContentEntityInterface;

/**
* Loads multiple entities at once.
*
* @param string $entity_type_id
* The entity type ID.
* @param array $entity_ids
* Array of entity IDs to load.
*
* @return \Drupal\Core\Entity\ContentEntityInterface[]
* Array of loaded entities, keyed by entity ID.
*/
public function loadEntities(string $entity_type_id, array $entity_ids): array;

/**
* Lists entities with optional filtering.
*
* @param string $entity_type_id
* The entity type ID.
* @param string|null $bundle
* The bundle to filter by, or NULL for all.
* @param int $limit
* Maximum number of entities to return.
* @param int $offset
* Number of entities to skip.
* @param array $conditions
* Additional query conditions.
*
* @return array
* Array of entity summaries.
*/
public function listEntities(string $entity_type_id, ?string $bundle = NULL, int $limit = 50, int $offset = 0, array $conditions = []): array;

/**
* Gets a summary of an entity (basic info only).
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity to summarize.
*
* @return array
* Entity summary data.
*/
public function getEntitySummary(ContentEntityInterface $entity): array;

/**
* Collects full intelligence for an entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity to analyze.
* @param array $fields
* Specific fields to include, or empty for all.
* @param array $plugins
* Specific plugins to use, or empty for all.
*
* @return array
* Complete entity intelligence data.
*/
public function collectIntel(ContentEntityInterface $entity, array $fields = [], array $plugins = []): array;

/**
* Gets available plugins.
*
* @return array
* 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;

}