-
Notifications
You must be signed in to change notification settings - Fork 0
feat(slb-531): add Search API index update functionality #518
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * @file | ||
| */ | ||
|
|
||
| use Drupal\Core\Entity\EntityInterface; | ||
|
|
||
| /** | ||
| * Implements hook_ENTITY_TYPE_presave(). | ||
| */ | ||
| function silverback_search_media_presave(EntityInterface $entity) { | ||
| \Drupal::service('search.api.index.update')->searchApiIndexUpdate($entity); | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Implements hook_ENTITY_TYPE_delete(). | ||
| */ | ||
| function silverback_search_media_delete(EntityInterface $entity) { | ||
| \Drupal::service('search.api.index.update')->searchApiIndexUpdate($entity); | ||
| } | ||
|
Comment on lines
+20
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The hooks |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Drupal\silverback_search; | ||
|
|
||
| use Drupal\Core\Entity\EntityInterface; | ||
| use Drupal\Core\Entity\EntityTypeManagerInterface; | ||
| use Drupal\entity_usage\EntityUsageInterface; | ||
|
|
||
| /** | ||
| * @todo Add class description. | ||
| */ | ||
|
Comment on lines
+12
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| final class SearchApiIndexUpdate { | ||
|
|
||
| /** | ||
| * Constructs a SearchApiIndexUpdate object. | ||
| */ | ||
| public function __construct( | ||
| private readonly EntityUsageInterface $entityUsageUsage, | ||
| private readonly EntityTypeManagerInterface $entityTypeManager, | ||
| ) { | ||
| } | ||
|
|
||
| /** | ||
| * Updates the Search API index for all source entities using the given entity. | ||
| * | ||
| * Checks if the Search API module is enabled. If so, retrieves all usages | ||
| * of the provided entity, loads each source entity, and triggers a Search API | ||
| * entity update for each one. Skips any source entities that cannot be loaded. | ||
| * | ||
| * @param \Drupal\Core\Entity\EntityInterface $entity | ||
| * The entity whose usages should trigger Search API index updates. | ||
| */ | ||
| public function searchApiIndexUpdate(EntityInterface $entity): void { | ||
|
|
||
| if (!\Drupal::moduleHandler()->moduleExists('search_api')) { | ||
| return; | ||
| } | ||
|
|
||
| $all_usages = \Drupal::service('entity_usage.usage')->listSources($entity); | ||
| foreach ($all_usages as $source_type => $ids) { | ||
| $type_storage = \Drupal::service('entity_type.manager')->getStorage($source_type); | ||
| foreach ($ids as $source_id => $records) { | ||
| $source_entity = $type_storage->load($source_id); | ||
| if (!$source_entity) { | ||
| // If for some reason this record is broken, just skip it. | ||
| continue; | ||
| } | ||
| search_api_entity_update($source_entity); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,306 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Drupal\Tests\silverback_test\Kernel\Datasource; | ||
|
|
||
| use Drupal\Tests\TestFileCreationTrait; | ||
| use Drupal\Tests\media\Kernel\MediaKernelTestBase; | ||
| use Drupal\Tests\node\Traits\ContentTypeCreationTrait; | ||
| use Drupal\Tests\node\Traits\NodeCreationTrait; | ||
| use Drupal\Tests\search_api\Kernel\PostRequestIndexingTrait; | ||
| use Drupal\field\Entity\FieldConfig; | ||
| use Drupal\field\Entity\FieldStorageConfig; | ||
| use Drupal\file\Entity\File; | ||
| use Drupal\media\Entity\Media; | ||
| use Drupal\node\Entity\Node; | ||
| use Drupal\node\Entity\NodeType; | ||
| use Drupal\search_api\Entity\Index; | ||
| use Drupal\search_api\Entity\Server; | ||
| use Drupal\search_api\Utility\Utility; | ||
| use Drupal\silverback_gutenberg\BlockSerializer; | ||
|
|
||
| /** | ||
| * Tests that changes in related entities are correctly tracked. | ||
| * | ||
| * @group search_api | ||
| */ | ||
| class SearchApiIndexUpdateTest extends MediaKernelTestBase { | ||
|
|
||
| use ContentTypeCreationTrait; | ||
| use NodeCreationTrait; | ||
| use TestFileCreationTrait; | ||
| use PostRequestIndexingTrait; | ||
|
|
||
| /** | ||
| * {@inheritdoc} | ||
| */ | ||
| protected static $modules = [ | ||
| 'node', | ||
| 'search_api', | ||
| 'search_api_test', | ||
| 'media', | ||
| 'path_alias', | ||
| 'filter', | ||
| 'text', | ||
| 'silverback_gutenberg', | ||
| //'language', | ||
| // 'content_translation', | ||
| 'entity_usage', | ||
| 'silverback_external_preview', | ||
| 'silverback_search', | ||
| ]; | ||
|
|
||
| /** | ||
| * The search index used for this test. | ||
| * | ||
| * @var \Drupal\search_api\IndexInterface | ||
| */ | ||
| protected $index; | ||
|
|
||
| /** | ||
| * Entities created for this test, keyed by human-readable string keys. | ||
| * | ||
| * @var \Drupal\Core\Entity\EntityInterface[] | ||
| */ | ||
| protected $entities = []; | ||
|
|
||
| /** | ||
| * {@inheritdoc} | ||
| */ | ||
| public function setUp(): void { | ||
| parent::setUp(); | ||
|
|
||
| $this->installSchema('search_api', ['search_api_item']); | ||
| $this->installSchema('node', ['node_access']); | ||
| $this->installSchema('entity_usage', ['entity_usage']); | ||
|
|
||
| $this->installEntitySchema('node'); | ||
| $this->installEntitySchema('search_api_task'); | ||
| $this->installConfig(['search_api', 'filter']); | ||
|
|
||
| // Do not use a batch for tracking the initial items after creating an | ||
| // index when running the tests via the GUI. Otherwise, it seems Drupal's | ||
| // Batch API gets confused and the test fails. | ||
| if (!Utility::isRunningInCli()) { | ||
| \Drupal::state()->set('search_api_use_tracking_batch', FALSE); | ||
| } | ||
|
|
||
| Server::create([ | ||
| 'id' => 'server', | ||
| 'backend' => 'search_api_test', | ||
| ])->save(); | ||
|
|
||
| $this->createMediaType('image', ['id' => 'image']); | ||
|
|
||
| $pageType = NodeType::create( | ||
| [ | ||
| 'type' => 'page', | ||
| 'name' => 'Page', | ||
| ] | ||
| ); | ||
| $pageType->save(); | ||
|
|
||
| FieldStorageConfig::create([ | ||
| 'field_name' => 'body', | ||
| 'entity_type' => 'node', | ||
| 'type' => 'text_long', | ||
| 'cardinality' => 1, | ||
| ])->save(); | ||
| FieldConfig::create([ | ||
| 'field_name' => 'body', | ||
| 'entity_type' => 'node', | ||
| 'bundle' => 'page', | ||
| 'label' => 'Body', | ||
| ])->save(); | ||
|
|
||
| // Direct entity reference field | ||
| FieldStorageConfig::create([ | ||
| 'field_name' => 'field_media', | ||
| 'entity_type' => 'node', | ||
| 'type' => 'entity_reference', | ||
| 'cardinality' => 1, | ||
| ])->save(); | ||
| FieldConfig::create([ | ||
| 'field_name' => 'field_media', | ||
| 'entity_type' => 'node', | ||
| 'bundle' => 'page', | ||
| 'label' => 'Media reference direct', | ||
| ])->save(); | ||
|
|
||
| $this->index = Index::create([ | ||
| 'id' => 'test_index', | ||
| 'name' => 'Test index', | ||
| 'tracker_settings' => [ | ||
| 'default' => [], | ||
| ], | ||
| 'datasource_settings' => [ | ||
| 'entity:node' => [], | ||
| 'entity:media' => [], | ||
| ], | ||
| 'server' => 'server', | ||
| 'field_settings' => [ | ||
| 'title' => [ | ||
| 'label' => 'Title', | ||
| 'datasource_id' => 'entity:node', | ||
| 'property_path' => 'title', | ||
| 'type' => 'text', | ||
| ], | ||
| 'body' => [ | ||
| 'label' => 'Body', | ||
| 'datasource_id' => 'entity:node', | ||
| 'property_path' => 'body', | ||
| 'type' => 'text', | ||
| ], | ||
| ], | ||
| ]); | ||
|
|
||
| $this->index->save(); | ||
| } | ||
|
|
||
| /** | ||
| * Tests correct tracking of changes in referenced entities inside gutenberg blocks. | ||
| */ | ||
| public function testReferencedEntityChangedGutenbergBlock() { | ||
|
|
||
| $file1 = File::create([ | ||
| 'uri' => $this->getTestFiles('image')[0]->uri, | ||
| ]); | ||
| $file1->save(); | ||
|
|
||
| $file2 = File::create([ | ||
| 'uri' => $this->getTestFiles('image')[1]->uri, | ||
| ]); | ||
| $file2->save(); | ||
|
|
||
| $media = Media::create([ | ||
| 'bundle' => 'image', | ||
| 'name' => 'Screaming hairy armadillo', | ||
| 'field_media_image' => [ | ||
| [ | ||
| 'target_id' => $file1->id(), | ||
| 'alt' => 'Screaming hairy armadillo', | ||
| 'title' => 'Screaming hairy armadillo', | ||
| ], | ||
| ], | ||
| ]); | ||
| $media->save(); | ||
|
|
||
| $serializer = new BlockSerializer(); | ||
| $blocks = [ | ||
| [ | ||
| 'blockName' => 'core/paragraph', | ||
| 'innerContent' => ['<p>A test paragraph</p>'], | ||
| 'attrs' => [], | ||
| 'innerBlocks' => [], | ||
| 'innerHTML' => [], | ||
| ], | ||
| [ | ||
| 'blockName' => 'drupalmedia/drupal-media-entity', | ||
| 'attrs' => [ | ||
| 'caption' => 'This is the caption', | ||
| 'mediaEntityIds' => [$media->id()], | ||
| ], | ||
| 'innerContent' => [], | ||
| 'innerBlocks' => [], | ||
| 'innerHTML' => [], | ||
| ] | ||
| ]; | ||
|
|
||
| $html = $serializer->serialize_blocks($blocks); | ||
|
|
||
| $node = Node::create([ | ||
| 'type' => 'page', | ||
| 'title' => 'Media Gutenberg reference item test', | ||
| 'body' => $html, | ||
| ]); | ||
| $node->save(); | ||
|
|
||
| $this->index->indexItems(); | ||
| $tracker = $this->index->getTrackerInstance(); | ||
|
|
||
| $this->assertEquals($tracker->getIndexedItemsCount(), 2); | ||
|
|
||
| $this->assertEquals( | ||
| [], | ||
| $tracker->getRemainingItems(), | ||
| '⚠️ Initial index matching error (1)', | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| ); | ||
|
|
||
| // Updating the media will also trigger the re-index of the node. | ||
| $media->set('field_media_image', ['target_id' => $file2->id()]); | ||
| $media->save(); | ||
|
|
||
| $expected[] = Utility::createCombinedId('entity:media', $media->id() . ':en'); | ||
| $expected[] = Utility::createCombinedId('entity:node', $node->id() . ':en'); | ||
|
|
||
| $this->assertEquals( | ||
| $expected, | ||
| $tracker->getRemainingItems(), | ||
| '⚠️ After media update index matching error (2)', | ||
| ); | ||
|
|
||
| // Make sure that no unknown items were queued for post-request indexing. | ||
| $this->triggerPostRequestIndexing(); | ||
| } | ||
|
|
||
| /** | ||
| * Tests correct tracking of changes in referenced entities outside gutenberg blocks. | ||
| */ | ||
| public function testReferencedEntityChangedDirect() { | ||
|
|
||
| $file1 = File::create([ | ||
| 'uri' => $this->getTestFiles('image')[0]->uri, | ||
| ]); | ||
| $file1->save(); | ||
|
|
||
| $file2 = File::create([ | ||
| 'uri' => $this->getTestFiles('image')[1]->uri, | ||
| ]); | ||
| $file2->save(); | ||
|
|
||
| $media = Media::create([ | ||
| 'bundle' => 'image', | ||
| 'name' => 'Screaming hairy armadillo', | ||
| 'field_media_image' => [ | ||
| [ | ||
| 'target_id' => $file1->id(), | ||
| 'alt' => 'Screaming hairy armadillo', | ||
| 'title' => 'Screaming hairy armadillo', | ||
| ], | ||
| ], | ||
| ]); | ||
| $media->save(); | ||
|
|
||
| $node = Node::create([ | ||
| 'type' => 'page', | ||
| 'title' => 'Media reference item test', | ||
| 'body' => 'Body value', | ||
| 'field_media' => ['target_id' => $media->id()], | ||
| ]); | ||
| $node->save(); | ||
|
|
||
| $this->index->indexItems(); | ||
| $tracker = $this->index->getTrackerInstance(); | ||
|
|
||
| $this->assertEquals($tracker->getIndexedItemsCount(), 2); | ||
|
|
||
| $this->assertEquals( | ||
| [], | ||
| $tracker->getRemainingItems(), | ||
| '⚠️ Initial index matching error (1)', | ||
| ); | ||
|
|
||
| $media->set('field_media_image', ['target_id' => $file2->id()]); | ||
| $media->save(); | ||
|
|
||
| $expected[] = Utility::createCombinedId('entity:media', $media->id() . ':en'); | ||
| $expected[] = Utility::createCombinedId('entity:node', $node->id() . ':en'); | ||
|
|
||
| $this->assertEquals( | ||
| $expected, | ||
| $tracker->getRemainingItems(), | ||
| '⚠️ After media update index matching error (2)', | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hooks
silverback_search_media_presaveandsilverback_search_media_deleteare specifically implemented for themediaentity type. If the intention is to trigger Search API index updates for usages of other entity types as well (e.g., nodes, taxonomy terms), consider implementing generichook_entity_presaveandhook_entity_deleteand adding logic inside to check the entity type, or using event subscribers for a more modern approach.