Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<?php

namespace Drupal\tide_search\Plugin\search_api\datasource;

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\search_api\Datasource\DatasourcePluginBase;

/**
* Indexes Media entities based on a configurable boolean field and bundles.
*
* @SearchApiDatasource(
* id = "filtered_media",
* label = @Translation("Filtered Media"),
* description = @Translation("Indexes media entities based on a configurable filter field and selected bundles."),
* entity_type_id = "media"
* )
*/
class FilteredMediaDatasource extends DatasourcePluginBase implements PluginFormInterface {

/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'indexing_field' => '',
'bundles' => [],
];
}

/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$config = $this->getConfiguration();

// Fetch all available Media Types (bundles).
$media_types = \Drupal::entityTypeManager()->getStorage('media_type')->loadMultiple();
$options = [];
foreach ($media_types as $type) {
$options[$type->id()] = $type->label();
}

$form['bundles'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Media Bundles'),
'#description' => $this->t('Select the media bundles that should be eligible for indexing.'),
'#options' => $options,
'#default_value' => $config['bundles'] ?: [],
'#required' => TRUE,
];

// Dynamically find all Boolean fields on Media.
$field_manager = \Drupal::service('entity_field.manager');
$storage_definitions = $field_manager->getFieldStorageDefinitions('media');

$field_options = [];
foreach ($storage_definitions as $field_name => $storage_definition) {
// Only boolean fields.
if ($storage_definition->getType() === 'boolean') {
$field_options[$field_name] = $this->t('@label (@name)', [
'@label' => $storage_definition->getLabel() ?: $field_name,
'@name' => $field_name,
]);
}
}

asort($field_options);

$form['indexing_field'] = [
'#type' => 'select',
'#title' => $this->t('Filtering Field'),
'#description' => $this->t('Select the boolean field that controls whether an item is indexed.'),
'#options' => $field_options,
'#default_value' => $config['indexing_field'],
'#empty_option' => $this->t('- Select Field -'),
'#required' => TRUE,
];

return $form;
}

/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
// Basic validation is handled by #required => TRUE.
}

/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
// Filter the checkboxes array to remove unselected items (value 0).
$values = $form_state->getValues();
$values['bundles'] = array_values(array_filter($values['bundles']));

$this->setConfiguration($values);
}

/**
* {@inheritdoc}
*/
public function getItemIds($page = NULL) {
$config = $this->getConfiguration();

if (empty($config['bundles']) || empty($config['indexing_field'])) {
return NULL;
}

$limit = 50;
$database = \Drupal::database();

// Construct the table name based on the dynamic field name.
$table_name = 'media__' . $config['indexing_field'];
$column_name = $config['indexing_field'] . '_value';

if (!$database->schema()->tableExists($table_name)) {
\Drupal::logger('tide_search')->error(
'Search API Indexing failed: The table %table does not exist. Please check the "Filtering Field" setting in your datasource configuration.',
['%table' => $table_name]
);
return NULL;
}

try {
$query = $database->select($table_name, 't');
$query->fields('t', ['entity_id']);

// Apply filters based on configuration.
$query->condition('t.bundle', $config['bundles'], 'IN');
$query->condition('t.' . $column_name, 1);
$query->orderBy('t.entity_id', 'ASC');

if ($page !== NULL) {
$query->range($page * $limit, $limit);
}

$ids = $query->execute()->fetchCol();

if (empty($ids) && $page === 0) {
\Drupal::logger('tide_search')->notice('Filtered Media datasource returned 0 items for bundles: %bundles using field: %field.',
[
'%bundles' => implode(', ', $config['bundles']),
'%field' => $config['indexing_field'],
]);
}

return $ids ? array_values(array_map('strval', $ids)) : NULL;
}
catch (\Exception $e) {
\Drupal::logger('tide_search')->error(
'Database error during Filtered Media indexing: @message',
['@message' => $e->getMessage()]
);
return NULL;
}
}

/**
* {@inheritdoc}
*/
public function loadMultiple(array $ids) {
try {
$entities = \Drupal::entityTypeManager()
->getStorage('media')
->loadMultiple($ids);

$items = [];
foreach ($entities as $id => $entity) {
$items[$id] = $entity->getTypedData();
}
// To save memory during large re-indexes.
// Clear the static entity cache.
if (count($ids) > 20) {
\Drupal::entityTypeManager()->getStorage('media')->resetCache($ids);
}
return $items;
}
catch (\Exception $e) {
\Drupal::logger('tide_search')->error(
'Failed to load media entities for indexing: @message',
['@message' => $e->getMessage()]
);
return [];
}
}

/**
* {@inheritdoc}
*/
public function getItemId($item) {
return $item->getValue()->id();
}

/**
* {@inheritdoc}
*/
public function getPropertyDefinitions() {
$config = $this->getConfiguration();
// Use the first selected bundle to determine property definitions.
// Or return all media fields.
$bundle = !empty($config['bundles']) ? reset($config['bundles']) : 'document';
return \Drupal::service('entity_field.manager')->getFieldDefinitions('media', $bundle);
}

}
72 changes: 72 additions & 0 deletions modules/tide_search/tide_search.module
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\media\MediaInterface;
use Drupal\search_api\Entity\Index;
use Drupal\search_api\IndexInterface;

/**
Expand Down Expand Up @@ -509,3 +511,73 @@ function tide_search_theme_suggestions_node_add_list(array $variables) {

return $suggestions;
}

/**
* Implements hook_entity_insert().
*/
function tide_search_media_insert(MediaInterface $entity) {
_tide_search_search_api_sync($entity, 'insert');
}

/**
* Implements hook_entity_update().
*/
function tide_search_media_update(MediaInterface $entity) {
_tide_search_search_api_sync($entity, 'update');
}

/**
* Implements hook_entity_delete().
*/
function tide_search_media_delete(MediaInterface $entity) {
_tide_search_search_api_sync($entity, 'delete');
}

/**
* Helper function to notify Search API indexes about Media changes.
*/
function _tide_search_search_api_sync(MediaInterface $entity, $action) {
// Load all search indexes.
$indexes = Index::loadMultiple();

foreach ($indexes as $index) {
if (!$index->isValidDatasource('filtered_media')) {
continue;
}

// Get the datasource instance and its specific config.
$datasource = $index->getDatasource('filtered_media');
$config = $datasource->getConfiguration();

$target_bundles = $config['bundles'] ?? [];
$indexing_field = $config['indexing_field'] ?? '';

// Only proceed if this Media entity belongs.
if (!in_array($entity->bundle(), $target_bundles)) {
continue;
}

$item_id = $entity->id();

// Determine if the item meets the indexing criteria.
$is_published = $entity->isPublished();
$field_exists = !empty($indexing_field) && $entity->hasField($indexing_field);
$is_field_checked = $field_exists && (bool) $entity->get($indexing_field)->value;

// Logic for tracking changes.
// If deleted OR it no longer meets criteria.
// Remove from index tracker.
if ($action === 'delete' || !$is_published || !$is_field_checked) {
$index->trackItemsDeleted('filtered_media', [$item_id]);
}
// Otherwise, mark as inserted or updated.
else {
if ($action === 'insert') {
$index->trackItemsInserted('filtered_media', [$item_id]);
}
elseif ($action === 'update') {
$index->trackItemsUpdated('filtered_media', [$item_id]);
}
}
}
}