diff --git a/search_api.install b/search_api.install index 06fa6071..3bef1f10 100644 --- a/search_api.install +++ b/search_api.install @@ -57,6 +57,16 @@ function search_api_schema() { } /** + * Implements hook_uninstall(). + */ +function search_api_uninstall() { + \Drupal::state()->delete('search_api_use_tracking_batch'); + foreach (\Drupal::configFactory()->listAll('search_api.index.') as $index_id) { + \Drupal::state()->delete("search_api.index.$index_id.has_reindexed"); + } +} + +/** * Implements hook_requirements(). */ function search_api_requirements($phase) { diff --git a/search_api.routing.yml b/search_api.routing.yml index 34c34480..bd5c2a93 100644 --- a/search_api.routing.yml +++ b/search_api.routing.yml @@ -30,6 +30,10 @@ entity.search_api_server.canonical: _title: "View" requirements: _entity_access: 'search_api_server.view' + options: + parameters: + search_api_server: + with_config_overrides: TRUE entity.search_api_server.edit_form: path: '/admin/config/search/search-api/server/{search_api_server}/edit' @@ -44,6 +48,10 @@ entity.search_api_server.delete_form: _entity_form: 'search_api_server.delete' requirements: _entity_access: 'search_api_server.delete' + options: + parameters: + search_api_server: + with_config_overrides: TRUE entity.search_api_server.enable: path: '/admin/config/search/search-api/server/{search_api_server}/enable' @@ -52,6 +60,10 @@ entity.search_api_server.enable: requirements: _entity_access: 'search_api_server.enable' _csrf_token: 'TRUE' + options: + parameters: + search_api_server: + with_config_overrides: TRUE entity.search_api_server.disable: path: '/admin/config/search/search-api/server/{search_api_server}/disable' @@ -59,6 +71,10 @@ entity.search_api_server.disable: _entity_form: 'search_api_server.disable' requirements: _entity_access: 'search_api_server.disable' + options: + parameters: + search_api_server: + with_config_overrides: TRUE entity.search_api_server.clear: path: '/admin/config/search/search-api/server/{search_api_server}/clear' @@ -66,6 +82,10 @@ entity.search_api_server.clear: _entity_form: 'search_api_server.clear' requirements: _entity_access: 'search_api_server.clear' + options: + parameters: + search_api_server: + with_config_overrides: TRUE entity.search_api_index.add_form: path: '/admin/config/search/search-api/add-index' @@ -81,6 +101,10 @@ entity.search_api_index.canonical: _title_callback: '\Drupal\search_api\Controller\IndexController::pageTitle' requirements: _entity_access: 'search_api_index.view' + options: + parameters: + search_api_index: + with_config_overrides: TRUE entity.search_api_index.edit_form: path: '/admin/config/search/search-api/index/{search_api_index}/edit' @@ -95,6 +119,10 @@ entity.search_api_index.delete_form: _entity_form: 'search_api_index.delete' requirements: _entity_access: 'search_api_index.delete' + options: + parameters: + search_api_index: + with_config_overrides: TRUE entity.search_api_index.enable: path: '/admin/config/search/search-api/index/{search_api_index}/enable' @@ -103,6 +131,10 @@ entity.search_api_index.enable: requirements: _entity_access: 'search_api_index.enable' _csrf_token: 'TRUE' + options: + parameters: + search_api_index: + with_config_overrides: TRUE entity.search_api_index.disable: path: '/admin/config/search/search-api/index/{search_api_index}/disable' @@ -110,6 +142,10 @@ entity.search_api_index.disable: _entity_form: 'search_api_index.disable' requirements: _entity_access: 'search_api_index.disable' + options: + parameters: + search_api_index: + with_config_overrides: TRUE entity.search_api_index.fields: path: '/admin/config/search/search-api/index/{search_api_index}/fields' @@ -182,6 +218,10 @@ entity.search_api_index.reindex: _entity_form: 'search_api_index.reindex' requirements: _entity_access: 'search_api_index.reindex' + options: + parameters: + search_api_index: + with_config_overrides: TRUE entity.search_api_index.clear: path: '/admin/config/search/search-api/index/{search_api_index}/clear' @@ -189,3 +229,7 @@ entity.search_api_index.clear: _entity_form: 'search_api_index.clear' requirements: _entity_access: 'search_api_index.clear' + options: + parameters: + search_api_index: + with_config_overrides: TRUE diff --git a/src/Backend/BackendInterface.php b/src/Backend/BackendInterface.php index 1ef2424f..ad802db5 100644 --- a/src/Backend/BackendInterface.php +++ b/src/Backend/BackendInterface.php @@ -51,6 +51,10 @@ public function postInsert(); * * The server's $original property can be used to inspect the old * configuration values. + * + * Take care, though, that the server at this point might be override-free and + * thus contain property values (and apparent changes) which will not really + * go into effect. The same goes for the If this might be the */ public function preUpdate(); diff --git a/src/Entity/Index.php b/src/Entity/Index.php index bbe53e78..83bb126e 100644 --- a/src/Entity/Index.php +++ b/src/Entity/Index.php @@ -37,7 +37,7 @@ * plural = "@count search indexes", * ), * handlers = { - * "storage" = "Drupal\Core\Config\Entity\ConfigEntityStorage", + * "storage" = "Drupal\search_api\Entity\SearchApiConfigEntityStorage", * "list_builder" = "Drupal\search_api\IndexListBuilder", * "form" = { * "default" = "Drupal\search_api\Form\IndexForm", @@ -259,13 +259,6 @@ class Index extends ConfigEntityBase implements IndexInterface { protected $processorInstances; /** - * Whether reindexing has been triggered for this index in this page request. - * - * @var bool - */ - protected $hasReindexed = FALSE; - - /** * The number of currently active "batch tracking" modes. * * @var int @@ -568,9 +561,9 @@ public function getProcessors() { /** * {@inheritdoc} */ - public function getProcessorsByStage($stage) { - // Get a list of all processors meeting the criteria (stage and, optionally, - // enabled) along with their effective weights (user-set or default). + public function getProcessorsByStage($stage, $overrides = []) { + // Get a list of all processors which support this stage, along with their + // weights. $processors = $this->getProcessors(); $processor_weights = array(); foreach ($processors as $name => $processor) { @@ -579,6 +572,16 @@ public function getProcessorsByStage($stage) { } } + // Apply any overrides that were passed by the caller. + foreach ($overrides as $name => $settings) { + /** @var \Drupal\search_api\Processor\ProcessorInterface $processor */ + $processor = $this->createPlugin('processor', $name, $settings); + if ($processor->supportsStage($stage)) { + $processors[$name] = $processor; + $processor_weights[$name] = $processor->getWeight($stage); + } + } + // Sort requested processors by weight. asort($processor_weights); @@ -818,6 +821,14 @@ public function getFieldRenames() { /** * {@inheritdoc} */ + public function discardFieldChanges() { + $this->fieldInstances = NULL; + return $this; + } + + /** + * {@inheritdoc} + */ public function getPropertyDefinitions($datasource_id) { if (isset($datasource_id)) { $datasource = $this->getDatasource($datasource_id); @@ -978,7 +989,7 @@ public function indexSpecificItems(array $search_objects) { } // Since we've indexed items now, triggering reindexing would have some // effect again. Therefore, we reset the flag. - $this->hasReindexed = FALSE; + $this->setHasReindexed(FALSE); \Drupal::moduleHandler()->invokeAll('search_api_items_indexed', array($this, $processed_ids)); } @@ -1084,8 +1095,8 @@ public function trackItemsDeleted($datasource_id, array $ids) { * {@inheritdoc} */ public function reindex() { - if ($this->status() && !$this->hasReindexed) { - $this->hasReindexed = TRUE; + if ($this->status() && !$this->isReindexing()) { + $this->setHasReindexed(); $this->getTrackerInstance()->trackAllItemsUpdated(); \Drupal::moduleHandler()->invokeAll('search_api_index_reindex', array($this, FALSE)); } @@ -1098,9 +1109,9 @@ public function clear() { if ($this->status()) { // Only invoke the hook if we actually did something. $invoke_hook = FALSE; - if (!$this->hasReindexed) { + if (!$this->isReindexing()) { $invoke_hook = TRUE; - $this->hasReindexed = TRUE; + $this->setHasReindexed(); $this->getTrackerInstance()->trackAllItemsUpdated(); } if (!$this->isReadOnly()) { @@ -1117,7 +1128,23 @@ public function clear() { * {@inheritdoc} */ public function isReindexing() { - return $this->hasReindexed; + $id = $this->id(); + return \Drupal::state()->get("search_api.index.$id.has_reindexed", FALSE); + } + + /** + * Sets whether this index has all items marked for re-indexing. + * + * @param bool $has_reindexed + * (optional) TRUE if the index has all items marked for re-indexing, FALSE + * otherwise. + * + * @return $this + */ + protected function setHasReindexed($has_reindexed = TRUE) { + $id = $this->id(); + \Drupal::state()->set("search_api.index.$id.has_reindexed", $has_reindexed); + return $this; } /** @@ -1155,9 +1182,26 @@ public function preSave(EntityStorageInterface $storage) { return; } - // Prevent enabling of indexes when the server is disabled. - if ($this->status() && !$this->isServerEnabled()) { - $this->disable(); + // Retrieve active config overrides for this index. + $overrides = Utility::getConfigOverrides($this); + + // Prevent enabling of indexes when the server is disabled. Take into + // account that both the index's "status" and "server" properties might be + // overridden. + if ($this->status() && !isset($overrides['status'])) { + // NULL would be a valid override, so we can't use isset() here. + if (!array_key_exists('server', $overrides)) { + if ($this->isServerEnabled()) { + $this->disable(); + } + } + else { + $server_id = $overrides['server']; + $server = $server_id ? Server::load($server_id) : NULL; + if (!$server || !$server->status()) { + $this->disable(); + } + } } // Merge in default options. @@ -1190,7 +1234,8 @@ public function preSave(EntityStorageInterface $storage) { } // Call the preIndexSave() method of all applicable processors. - foreach ($this->getProcessorsByStage(ProcessorInterface::STAGE_PRE_INDEX_SAVE) as $processor) { + $processor_overrides = !empty($overrides['processor_settings']) ? $overrides['processor_settings'] : []; + foreach ($this->getProcessorsByStage(ProcessorInterface::STAGE_PRE_INDEX_SAVE, $processor_overrides) as $processor) { $processor->preIndexSave(); } @@ -1252,6 +1297,11 @@ protected function writeChangesToSettings() { public function postSave(EntityStorageInterface $storage, $update = TRUE) { parent::postSave($storage, $update); + // New indexes don't have any items indexed. + if (!$update) { + $this->setHasReindexed(); + } + try { // Fake an original for inserts to make code cleaner. /** @var \Drupal\search_api\IndexInterface $original */ @@ -1306,9 +1356,6 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { catch (SearchApiException $e) { $this->logException($e); } - - // Reset the field instances so saved renames won't be reported anymore. - $this->fieldInstances = NULL; } /** diff --git a/src/Entity/SearchApiConfigEntityStorage.php b/src/Entity/SearchApiConfigEntityStorage.php new file mode 100644 index 00000000..b250affc --- /dev/null +++ b/src/Entity/SearchApiConfigEntityStorage.php @@ -0,0 +1,77 @@ +resetCache(array($entity->id())); + + // The entity is no longer new. + $entity->enforceIsNew(FALSE); + + $overridden_entity = $this->load($entity->id()); + if (isset($entity->original)) { + $overridden_entity->original = $entity->original; + + // In the case of indexes, we also need to clone the fields to allow the + // correct detection of renamed field. Conversely, we need to set the new, + // rename-free fields on the passed index ($entity) so a subsequent save + // won't false detect field renames. + if ($entity instanceof IndexInterface) { + /** @var \Drupal\search_api\IndexInterface $overridden_entity */ + $old_fields = $entity->original->getFields(); + $new_fields = $entity->getFields(); + $saved_fields = $overridden_entity->getFields(); +// $cloned_fields = []; +// foreach ($saved_fields as $field_id => $field) { +// $field = clone $field; +// $field->setIndex($entity); +// $cloned_fields[$field_id] = $field; +// } +// $entity->setFields($cloned_fields); + foreach ($entity->getFieldRenames() as $old_id => $new_id) { + if (!empty($old_fields[$old_id]) && !empty($saved_fields[$new_id])) { + $field = clone $new_fields[$new_id]; + $field->setIndex($overridden_entity); + $saved_fields[$new_id] = $field; + } + } + $overridden_entity->setFields($saved_fields); + } + } + + // Allow code to run after saving. + $overridden_entity->postSave($this, $update); + $this->invokeHook($update ? 'update' : 'insert', $overridden_entity); + + if ($entity instanceof IndexInterface) { + // Reset the field instances so saved renames won't be reported anymore. + $entity->discardFieldChanges(); + $overridden_entity->discardFieldChanges(); + } + + // After saving, this is now the "original entity", and subsequent saves + // will be updates instead of inserts, and updates must always be able to + // correctly identify the original entity. + $entity->setOriginalId($entity->id()); + $overridden_entity->setOriginalId($entity->id()); + + unset($entity->original); + unset($overridden_entity->original); + } + +} diff --git a/src/Entity/Server.php b/src/Entity/Server.php index 08a21c6a..a69335df 100644 --- a/src/Entity/Server.php +++ b/src/Entity/Server.php @@ -9,6 +9,7 @@ use Drupal\search_api\Query\QueryInterface; use Drupal\search_api\SearchApiException; use Drupal\search_api\ServerInterface; +use Drupal\search_api\Utility\Utility; /** * Defines the search server configuration entity. @@ -24,7 +25,7 @@ * plural = "@count search servers", * ), * handlers = { - * "storage" = "Drupal\Core\Config\Entity\ConfigEntityStorage", + * "storage" = "Drupal\search_api\Entity\SearchApiConfigEntityStorage", * "form" = { * "default" = "Drupal\search_api\Form\ServerForm", * "edit" = "Drupal\search_api\Form\ServerForm", @@ -465,10 +466,25 @@ public function preSave(EntityStorageInterface $storage) { return; } + // Retrieve active config overrides for this server. + $overrides = Utility::getConfigOverrides($this); + + if (isset($overrides['backend'])) { + $this->backend = $overrides['backend']; + $this->backendPlugin = NULL; + } + if (isset($overrides['backend_config'])) { + $this->backend_config = $overrides['backend_config']; + $this->backendPlugin = NULL; + } + $this->getBackend()->preUpdate(); // If the server is being disabled, also disable all its indexes. - if (!$this->isSyncing() && !$this->status() && $this->original->status()) { + if (!$this->isSyncing() + && !isset($overrides['status']) + && !$this->status() + && $this->original->status()) { foreach ($this->getIndexes(array('status' => TRUE)) as $index) { /** @var \Drupal\search_api\IndexInterface $index */ $index->setStatus(FALSE)->save(); diff --git a/src/IndexInterface.php b/src/IndexInterface.php index c8f8813b..4b59adeb 100644 --- a/src/IndexInterface.php +++ b/src/IndexInterface.php @@ -311,12 +311,15 @@ public function getProcessors(); * @param string $stage * The stage for which to return the processors. One of the * \Drupal\search_api\Processor\ProcessorInterface::STAGE_* constants. + * @param array[] $overrides + * (optional) Overrides to apply to the index's processors, keyed by + * processor IDs with their respective overridden settings as values. * * @return \Drupal\search_api\Processor\ProcessorInterface[] * An array of all enabled processors that support the given stage, ordered * by the weight for that stage. */ - public function getProcessorsByStage($stage); + public function getProcessorsByStage($stage, $overrides = []); /** * Determines whether the given processor ID is valid for this index. @@ -545,6 +548,13 @@ public function getFulltextFields(); public function getFieldRenames(); /** + * Resets the index's fields to the saved state. + * + * @return $this + */ + public function discardFieldChanges(); + + /** * Retrieves the properties of one of this index's datasources. * * @param string|null $datasource_id diff --git a/src/UnsavedIndexConfiguration.php b/src/UnsavedIndexConfiguration.php index 7e7ec7bb..dd7c6b12 100644 --- a/src/UnsavedIndexConfiguration.php +++ b/src/UnsavedIndexConfiguration.php @@ -508,6 +508,14 @@ public function getFieldRenames() { /** * {@inheritdoc} */ + public function discardFieldAndPluginChanges() { + $this->discardFieldAndPluginChanges(); + return $this; + } + + /** + * {@inheritdoc} + */ public function getPropertyDefinitions($datasource_id) { return $this->entity->getPropertyDefinitions($datasource_id); } diff --git a/src/Utility/Utility.php b/src/Utility/Utility.php index 9ed45bb1..5cd47b81 100644 --- a/src/Utility/Utility.php +++ b/src/Utility/Utility.php @@ -2,6 +2,10 @@ namespace Drupal\search_api\Utility; +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Config\ConfigFactoryOverrideInterface; +use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\TypedData\ComplexDataDefinitionInterface; use Drupal\Core\TypedData\ComplexDataInterface; use Drupal\Core\TypedData\DataDefinitionInterface; @@ -10,6 +14,7 @@ use Drupal\search_api\IndexInterface; use Drupal\search_api\Item\FieldInterface; use Drupal\search_api\Plugin\search_api\data_type\value\TextToken; +use Symfony\Component\DependencyInjection\TaggedContainerInterface; /** * Contains utility methods for the Search API. @@ -492,4 +497,50 @@ public static function splitPropertyPath($property_path, $separate_last = TRUE, return array($property_path, NULL); } + /** + * Retrieves all overridden property values for the given config entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The config entity to check for overrides. + * + * @return array + * An associative array mapping property names to their overridden values. + */ + public static function getConfigOverrides(EntityInterface $entity) { + $entity_type = $entity->getEntityType(); + if (!($entity_type instanceof ConfigEntityTypeInterface)) { + return []; + } + + $config_key = $entity_type->getConfigPrefix() . '.' . $entity->id(); + $overrides = []; + + // Overrides from tagged services. + $container = \Drupal::getContainer(); + if ($container instanceof TaggedContainerInterface) { + $tags = $container->findTaggedServiceIds('config.factory.override'); + foreach (array_keys($tags) as $service_id) { + $override = $container->get($service_id); + if ($override instanceof ConfigFactoryOverrideInterface) { + $service_overrides = $override->loadOverrides([$config_key]); + if (!empty($service_overrides[$config_key])) { + // Existing overrides take precedence since these will have been + // added by events with a higher priority. + $arrays = [$service_overrides[$config_key], $overrides]; + $overrides = NestedArray::mergeDeepArray($arrays, TRUE); + } + } + } + } + + // Overrides from settings.php. (This takes precedence over overrides from + // services.) + if (isset($GLOBALS['config'][$config_key])) { + $arrays = [$overrides, $GLOBALS['config'][$config_key]]; + $overrides = NestedArray::mergeDeepArray($arrays, TRUE); + } + + return $overrides; + } + } diff --git a/tests/src/Kernel/BackendTestBase.php b/tests/src/Kernel/BackendTestBase.php index 73f3175f..5157dc72 100644 --- a/tests/src/Kernel/BackendTestBase.php +++ b/tests/src/Kernel/BackendTestBase.php @@ -1018,6 +1018,8 @@ protected function regressionTest2471509() { $index->getField('body')->setType('text'); $index->save(); + $count = $this->indexItems($this->indexId); + $this->assertEquals(count($this->entities), $count, 'All items needed to be re-indexed after switching type from string to text.'); } /** diff --git a/tests/src/Kernel/IndexChangesTest.php b/tests/src/Kernel/IndexChangesTest.php index 1aa64b9b..44a0385a 100644 --- a/tests/src/Kernel/IndexChangesTest.php +++ b/tests/src/Kernel/IndexChangesTest.php @@ -266,11 +266,7 @@ public function testTrackerChange() { $this->taskManager->executeAllTasks(); $methods = $this->getCalledMethods('tracker'); - // Note: The initial "trackAllItemsUpdated" call comes from the test - // backend, which marks the index for re-indexing every time it gets - // updated. $expected = array( - 'trackAllItemsUpdated', 'trackItemsInserted', 'trackItemsInserted', );