diff --git a/content_intel.api.php b/content_intel.api.php new file mode 100644 index 0000000..b1268ed --- /dev/null +++ b/content_intel.api.php @@ -0,0 +1,228 @@ + 123, + * 'custom_status' => 'active', + * ]; + * } + * + * } + * @endcode + * + * Plugin with dependency injection example: + * @code + * namespace Drupal\my_module\Plugin\ContentIntel; + * + * use Drupal\content_intel\Attribute\ContentIntel; + * use Drupal\content_intel\ContentIntelPluginBase; + * use Drupal\Core\Database\Connection; + * use Drupal\Core\Entity\ContentEntityInterface; + * use Drupal\Core\StringTranslation\TranslatableMarkup; + * use Drupal\node\NodeInterface; + * use Symfony\Component\DependencyInjection\ContainerInterface; + * + * #[ContentIntel( + * id: 'my_database_intel', + * label: new TranslatableMarkup('Database Intel'), + * description: new TranslatableMarkup('Collects data from custom database tables.'), + * entity_types: ['node'], + * weight: 60, + * )] + * class MyDatabaseIntelPlugin extends ContentIntelPluginBase { + * + * protected Connection $database; + * + * public static function create( + * ContainerInterface $container, + * array $configuration, + * $plugin_id, + * $plugin_definition, + * ): static { + * $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); + * $instance->database = $container->get('database'); + * return $instance; + * } + * + * public function isAvailable(): bool { + * // Check if required table exists. + * return $this->database->schema()->tableExists('my_custom_table'); + * } + * + * public function applies(ContentEntityInterface $entity): bool { + * // Only apply to published nodes. + * return $entity instanceof NodeInterface && $entity->isPublished(); + * } + * + * public function collect(ContentEntityInterface $entity): array { + * $result = $this->database->select('my_custom_table', 't') + * ->fields('t', ['score', 'category']) + * ->condition('entity_id', $entity->id()) + * ->execute() + * ->fetchAssoc(); + * + * return $result ?: []; + * } + * + * } + * @endcode + * + * Available plugin attribute parameters: + * - id: (required) Unique plugin identifier. + * - label: (required) Human-readable plugin name (TranslatableMarkup). + * - description: (optional) Plugin description (TranslatableMarkup). + * - entity_types: (optional) Array of entity type IDs this plugin applies to. + * If empty, the plugin applies to all entity types. + * - weight: (optional) Integer weight for ordering plugins. Lower weights + * execute first. Default is 0. + * - deriver: (optional) Deriver class for dynamic plugin generation. + * + * Plugin methods: + * - isAvailable(): Returns TRUE if the plugin's dependencies are met. + * - applies(ContentEntityInterface $entity): Returns TRUE if the plugin + * should collect data for the given entity. + * - collect(ContentEntityInterface $entity): Returns an associative array + * of intelligence data for the entity. + * - label(): Returns the plugin label. + * - description(): Returns the plugin description. + * - getWeight(): Returns the plugin weight. + * + * @see \Drupal\content_intel\Attribute\ContentIntel + * @see \Drupal\content_intel\ContentIntelPluginInterface + * @see \Drupal\content_intel\ContentIntelPluginBase + * @see \Drupal\content_intel\ContentIntelPluginManager + * @see content_intel_example + * @} + */ + +/** + * @addtogroup hooks + * @{ + */ + +/** + * Alter Content Intelligence plugin definitions. + * + * This hook is invoked during plugin discovery to allow modules to modify + * plugin definitions before they are cached. Use this hook to change plugin + * properties such as labels, descriptions, entity type restrictions, or + * weights. + * + * @param array $definitions + * An array of plugin definitions, keyed by plugin ID. Each definition + * contains the following keys: + * - id: The plugin ID. + * - label: The plugin label (TranslatableMarkup). + * - description: The plugin description (TranslatableMarkup or NULL). + * - entity_types: Array of entity type IDs the plugin applies to. + * - weight: Integer weight for ordering. + * - class: The fully qualified plugin class name. + * - provider: The module providing the plugin. + * + * @see \Drupal\content_intel\ContentIntelPluginManager + */ +function hook_content_intel_info_alter(array &$definitions) { + // Change the label of an existing plugin. + if (isset($definitions['statistics'])) { + $definitions['statistics']['label'] = t('Page View Statistics'); + } + + // Restrict a plugin to specific entity types. + if (isset($definitions['my_plugin'])) { + $definitions['my_plugin']['entity_types'] = ['node', 'media']; + } + + // Adjust plugin weight to change execution order. + if (isset($definitions['content_translation'])) { + $definitions['content_translation']['weight'] = 100; + } + + // Remove a plugin entirely. + unset($definitions['unwanted_plugin']); +} + +/** + * Alter collected intelligence data for an entity. + * + * This hook is invoked after all plugins have collected their data, allowing + * modules to modify or extend the final intelligence result. Use this hook to: + * - Add computed or derived metrics based on combined plugin data + * - Modify or remove data from specific plugins + * - Add custom data that doesn't warrant a full plugin. + * + * @param array $data + * The collected intelligence data array with the following structure: + * - entity: Entity summary (entity_type, id, uuid, label, bundle, langcode). + * - fields: Extracted field data keyed by field name. + * - intel: Plugin data keyed by plugin ID, each containing: + * - plugin: The plugin label. + * - data: The collected data array. + * - error: (optional) Error message if collection failed. + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity that was analyzed. + * + * @see \Drupal\content_intel\Service\ContentIntelCollector::collectIntel() + */ +function hook_content_intel_collect_alter(array &$data, \Drupal\Core\Entity\ContentEntityInterface $entity) { + // Add a computed score based on statistics data. + if (isset($data['intel']['statistics']['data']['total_views'])) { + $views = $data['intel']['statistics']['data']['total_views']; + $data['intel']['computed']['data']['popularity'] = match (TRUE) { + $views > 1000 => 'viral', + $views > 100 => 'popular', + $views > 10 => 'moderate', + default => 'low', + }; + } + + // Add custom metadata. + $data['intel']['custom']['data']['analyzed_at'] = date('c'); + + // Remove sensitive data from a specific plugin. + if (isset($data['intel']['my_plugin']['data']['internal_id'])) { + unset($data['intel']['my_plugin']['data']['internal_id']); + } +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/content_intel.services.yml b/content_intel.services.yml index 3ced029..e7c238c 100644 --- a/content_intel.services.yml +++ b/content_intel.services.yml @@ -13,3 +13,4 @@ services: - '@renderer' - '@entity_type.bundle.info' - '@file_url_generator' + - '@module_handler' diff --git a/modules/content_intel_example/README.md b/modules/content_intel_example/README.md new file mode 100644 index 0000000..6c83061 --- /dev/null +++ b/modules/content_intel_example/README.md @@ -0,0 +1,71 @@ +# Content Intelligence Example + +Provides example plugins and hook implementations for the Content Intelligence +module. This module is intended for developer reference only. + +## Contents + +This module demonstrates: + +1. **Basic Plugin** (`WordCountPlugin`): A simple plugin that counts words in + text fields without requiring external services. + +2. **Plugin with Dependency Injection** (`EntityAgePlugin`): A plugin that + uses injected services to calculate entity age. + +3. **Alter Hook Implementation**: Demonstrates the collect alter hook to add + computed metrics based on collected plugin data. + +## Example Plugins + +### Word Count Plugin + +Location: `src/Plugin/ContentIntel/WordCountPlugin.php` + +A basic plugin demonstrating: +- Simple `collect()` implementation +- Working with entity field values +- Returning structured data + +### Entity Age Plugin + +Location: `src/Plugin/ContentIntel/EntityAgePlugin.php` + +An advanced plugin demonstrating: +- Dependency injection via `create()` method +- Using Drupal services (`datetime.time`, `date.formatter`) +- Conditional availability based on entity properties + +## Hook Implementation + +The module file demonstrates `hook_content_intel_collect_alter()` which: +- Adds a `content_score` metric combining word count data +- Shows how to access and modify collected plugin data + +## Usage + +Enable the module: + +```bash +drush en content_intel_example +``` + +Test with Drush: + +```bash +# List available plugins (should include word_count and entity_age) +drush ci:plugins + +# Get intel for a node (will include example plugin data) +drush ci:entity node 1 --format=json +``` + +## Requirements + +- Content Intelligence module +- Drupal 10.3+ or 11.x + +## Note + +This module is for development reference only. It should not be installed on +production sites. Use it as a template for creating your own plugins. diff --git a/modules/content_intel_example/content_intel_example.info.yml b/modules/content_intel_example/content_intel_example.info.yml new file mode 100644 index 0000000..8bd256d --- /dev/null +++ b/modules/content_intel_example/content_intel_example.info.yml @@ -0,0 +1,7 @@ +name: 'Content Intelligence Example' +type: module +description: 'Provides example plugins and hook implementations for the Content Intelligence module.' +package: Example +core_version_requirement: ^10.3 || ^11 +dependencies: + - content_intel:content_intel diff --git a/modules/content_intel_example/content_intel_example.module b/modules/content_intel_example/content_intel_example.module new file mode 100644 index 0000000..927d866 --- /dev/null +++ b/modules/content_intel_example/content_intel_example.module @@ -0,0 +1,48 @@ += 1000 => 'comprehensive', + $total_words >= 500 => 'detailed', + $total_words >= 200 => 'moderate', + $total_words >= 50 => 'brief', + default => 'minimal', + }; + + // Add computed data under a custom key. + $data['intel']['content_score'] = [ + 'plugin' => 'Content Score (computed)', + 'data' => [ + 'score' => $score, + 'word_count_used' => $total_words, + 'computed_by' => 'content_intel_example', + ], + ]; +} diff --git a/modules/content_intel_example/src/Plugin/ContentIntel/EntityAgePlugin.php b/modules/content_intel_example/src/Plugin/ContentIntel/EntityAgePlugin.php new file mode 100644 index 0000000..99d3a25 --- /dev/null +++ b/modules/content_intel_example/src/Plugin/ContentIntel/EntityAgePlugin.php @@ -0,0 +1,129 @@ +time = $container->get('datetime.time'); + $instance->dateFormatter = $container->get('date.formatter'); + return $instance; + } + + /** + * {@inheritdoc} + */ + public function applies(ContentEntityInterface $entity): bool { + // Only apply to entities that track creation time. + return method_exists($entity, 'getCreatedTime'); + } + + /** + * {@inheritdoc} + */ + public function collect(ContentEntityInterface $entity): array { + $now = $this->time->getRequestTime(); + $data = []; + + // Get creation time if available. + if (method_exists($entity, 'getCreatedTime')) { + $created = $entity->getCreatedTime(); + $age_seconds = $now - $created; + + $data['created'] = [ + 'timestamp' => $created, + 'iso8601' => date('c', $created), + 'human' => $this->dateFormatter->format($created, 'medium'), + ]; + + $data['age'] = [ + 'seconds' => $age_seconds, + 'days' => (int) floor($age_seconds / 86400), + 'human' => $this->dateFormatter->formatInterval($age_seconds, 2), + ]; + + // Calculate freshness category. + $days_old = $data['age']['days']; + $data['freshness'] = match (TRUE) { + $days_old <= 1 => 'new', + $days_old <= 7 => 'recent', + $days_old <= 30 => 'current', + $days_old <= 90 => 'aging', + $days_old <= 365 => 'old', + default => 'archival', + }; + } + + // Get last modified time if entity supports it. + if ($entity instanceof EntityChangedInterface) { + $changed = $entity->getChangedTime(); + $since_changed = $now - $changed; + + $data['last_modified'] = [ + 'timestamp' => $changed, + 'iso8601' => date('c', $changed), + 'human' => $this->dateFormatter->format($changed, 'medium'), + ]; + + $data['time_since_update'] = [ + 'seconds' => $since_changed, + 'days' => (int) floor($since_changed / 86400), + 'human' => $this->dateFormatter->formatInterval($since_changed, 2), + ]; + + // Check if entity was modified after creation. + if (isset($data['created']['timestamp'])) { + $data['was_edited'] = $changed > $data['created']['timestamp']; + } + } + + return $data; + } + +} diff --git a/modules/content_intel_example/src/Plugin/ContentIntel/WordCountPlugin.php b/modules/content_intel_example/src/Plugin/ContentIntel/WordCountPlugin.php new file mode 100644 index 0000000..1db4e3b --- /dev/null +++ b/modules/content_intel_example/src/Plugin/ContentIntel/WordCountPlugin.php @@ -0,0 +1,98 @@ +getFields() as $field_name => $field) { + $field_type = $field->getFieldDefinition()->getType(); + + // Only process text fields. + if (!in_array($field_type, self::TEXT_FIELD_TYPES, TRUE)) { + continue; + } + + // Skip empty fields. + if ($field->isEmpty()) { + continue; + } + + $field_text = ''; + foreach ($field as $item) { + // Get the text value, stripping HTML if present. + $value = $item->value ?? ''; + if (!empty($value)) { + $field_text .= ' ' . strip_tags((string) $value); + } + } + + $field_text = trim($field_text); + if (empty($field_text)) { + continue; + } + + // Count words and characters. + $words = str_word_count($field_text); + $characters = mb_strlen($field_text); + + $field_counts[$field_name] = [ + 'words' => $words, + 'characters' => $characters, + ]; + + $total_words += $words; + $total_characters += $characters; + } + + return [ + 'total_words' => $total_words, + 'total_characters' => $total_characters, + 'fields_analyzed' => count($field_counts), + 'field_breakdown' => $field_counts, + ]; + } + +} diff --git a/src/Service/ContentIntelCollector.php b/src/Service/ContentIntelCollector.php index 076a482..eff3099 100644 --- a/src/Service/ContentIntelCollector.php +++ b/src/Service/ContentIntelCollector.php @@ -10,6 +10,7 @@ use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\File\FileUrlGeneratorInterface; use Drupal\Core\Render\RendererInterface; @@ -36,6 +37,8 @@ class ContentIntelCollector implements ContentIntelCollectorInterface { * The entity type bundle info service. * @param \Drupal\Core\File\FileUrlGeneratorInterface $fileUrlGenerator * The file URL generator. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler + * The module handler. */ public function __construct( protected ContentIntelPluginManager $pluginManager, @@ -45,6 +48,7 @@ public function __construct( protected RendererInterface $renderer, protected EntityTypeBundleInfoInterface $bundleInfo, protected FileUrlGeneratorInterface $fileUrlGenerator, + protected ModuleHandlerInterface $moduleHandler, ) {} /** @@ -309,6 +313,9 @@ public function collectIntel( } } + // Allow other modules to alter the collected intelligence data. + $this->moduleHandler->alter('content_intel_collect', $data, $entity); + return $data; }