diff --git a/config/schema/search_api.server.schema.yml b/config/schema/search_api.server.schema.yml index 86c223a..62dd39c 100644 --- a/config/schema/search_api.server.schema.yml +++ b/config/schema/search_api.server.schema.yml @@ -28,3 +28,6 @@ search_api.server.*: dependencies: type: config_dependencies label: 'Dependencies' + +plugin.plugin_configuration.search_api_backend.*: + type: mapping diff --git a/search_api.install b/search_api.install index 06fa607..3bef1f1 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 34c3448..bd5c2a9 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 1ef2424..75d7259 100644 --- a/src/Backend/BackendInterface.php +++ b/src/Backend/BackendInterface.php @@ -51,6 +51,13 @@ interface BackendInterface extends ConfigurablePluginInterface, BackendSpecificI * * 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 actually + * go into effect. If this might influence the code in this method, you have + * to manually check for overrides to ensure no incorrect action is taken. + * + * @see \Drupal\search_api\Utility\Utility::getConfigOverrides() */ public function preUpdate(); diff --git a/src/Backend/BackendPluginBase.php b/src/Backend/BackendPluginBase.php index 6c49fdb..02aaca7 100644 --- a/src/Backend/BackendPluginBase.php +++ b/src/Backend/BackendPluginBase.php @@ -112,6 +112,7 @@ abstract class BackendPluginBase extends ConfigurablePluginBase implements Backe */ public function setServer(ServerInterface $server) { $this->server = $server; + return $this; } /** diff --git a/src/Entity/Index.php b/src/Entity/Index.php index bbe53e7..cd61b6e 100644 --- a/src/Entity/Index.php +++ b/src/Entity/Index.php @@ -37,7 +37,7 @@ use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; * 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 @@ class Index extends ConfigEntityBase implements IndexInterface { /** * {@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 @@ class Index extends ConfigEntityBase implements IndexInterface { } } + // 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 @@ class Index extends ConfigEntityBase implements IndexInterface { /** * {@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 @@ class Index extends ConfigEntityBase implements IndexInterface { } // 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 @@ class Index extends ConfigEntityBase implements IndexInterface { * {@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 @@ class Index extends ConfigEntityBase implements IndexInterface { 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 @@ class Index extends ConfigEntityBase implements IndexInterface { * {@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 @@ class Index extends ConfigEntityBase implements IndexInterface { 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 !== NULL ? Server::load($server_id) : NULL; + if (!$server || !$server->status()) { + $this->disable(); + } + } } // Merge in default options. @@ -1190,7 +1234,8 @@ class Index extends ConfigEntityBase implements IndexInterface { } // 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 @@ class Index extends ConfigEntityBase implements IndexInterface { 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 @@ class Index extends ConfigEntityBase implements IndexInterface { 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 0000000..b250aff --- /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 08a21c6..47812fd 100644 --- a/src/Entity/Server.php +++ b/src/Entity/Server.php @@ -9,6 +9,7 @@ use Drupal\search_api\LoggerTrait; 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 @@ use Drupal\search_api\ServerInterface; * 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", @@ -464,11 +465,38 @@ class Server extends ConfigEntityBase implements ServerInterface { if (!isset($this->original)) { return; } + // Retrieve active config overrides for this server. + $overrides = Utility::getConfigOverrides($this); + + // If there are overrides for the backend or its configuration, attempt to + // apply them for the preUpdate() call. + if (isset($overrides['backend']) || isset($overrides['backend_config'])) { + $backend_config = $this->getBackendConfig(); + if (isset($overrides['backend_config'])) { + $backend_config = $overrides['backend_config']; + } + $backend_id = $this->getBackendId(); + if (isset($overrides['backend'])) { + $backend_id = $overrides['backend']; + } + $backend_plugin_manager = \Drupal::service('plugin.manager.search_api.backend'); + $backend_config['#server'] = $this; + if (!($backend = $backend_plugin_manager->createInstance($backend_id, $backend_config))) { + $label = $this->label(); + throw new SearchApiException("The backend with ID '$backend_id' could not be retrieved for server '$label'."); + } + } + else { + $backend = $this->getBackend(); + } - $this->getBackend()->preUpdate(); + $backend->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 c8f8813..4b59ade 100644 --- a/src/IndexInterface.php +++ b/src/IndexInterface.php @@ -311,12 +311,15 @@ interface IndexInterface extends ConfigEntityInterface { * @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 @@ interface IndexInterface extends ConfigEntityInterface { 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/ParamConverter/SearchApiConverter.php b/src/ParamConverter/SearchApiConverter.php index 616fad0..a68da13 100644 --- a/src/ParamConverter/SearchApiConverter.php +++ b/src/ParamConverter/SearchApiConverter.php @@ -2,6 +2,7 @@ namespace Drupal\search_api\ParamConverter; +use Drupal\Core\Config\Entity\ConfigEntityStorageInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\ParamConverter\EntityConverter; use Drupal\Core\ParamConverter\ParamConverterInterface; @@ -56,7 +57,11 @@ class SearchApiConverter extends EntityConverter implements ParamConverterInterf */ public function convert($value, $definition, $name, array $defaults) { /** @var \Drupal\search_api\IndexInterface $entity */ - if (!($entity = parent::convert($value, $definition, $name, $defaults))) { + $storage = $this->entityManager->getStorage('search_api_index'); + if (!($storage instanceof ConfigEntityStorageInterface)) { + return NULL; + } + if (!($entity = $storage->loadOverrideFree($value))) { return NULL; } diff --git a/src/Plugin/search_api/processor/RenderedItem.php b/src/Plugin/search_api/processor/RenderedItem.php index 375d9aa..e60b24d 100644 --- a/src/Plugin/search_api/processor/RenderedItem.php +++ b/src/Plugin/search_api/processor/RenderedItem.php @@ -27,7 +27,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * description = @Translation("Adds an additional field containing the rendered item as it would look when viewed."), * stages = { * "add_properties" = 0, - * "pre_index_save" = -10, * }, * locked = true, * hidden = true, diff --git a/src/UnsavedIndexConfiguration.php b/src/UnsavedIndexConfiguration.php index 7e7ec7b..e949251 100644 --- a/src/UnsavedIndexConfiguration.php +++ b/src/UnsavedIndexConfiguration.php @@ -368,8 +368,8 @@ class UnsavedIndexConfiguration implements IndexInterface, UnsavedConfigurationI /** * {@inheritdoc} */ - public function getProcessorsByStage($stage) { - return $this->entity->getProcessorsByStage($stage); + public function getProcessorsByStage($stage, $overrides = []) { + return $this->entity->getProcessorsByStage($stage, $overrides); } /** @@ -508,6 +508,14 @@ class UnsavedIndexConfiguration implements IndexInterface, UnsavedConfigurationI /** * {@inheritdoc} */ + public function discardFieldChanges() { + $this->entity->discardFieldChanges(); + 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 9ed45bb..5cd47b8 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\Datasource\DatasourceInterface; 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 @@ class Utility { 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/search_api_test/src/MethodOverrides.php b/tests/search_api_test/src/MethodOverrides.php new file mode 100644 index 0000000..c2eac6e --- /dev/null +++ b/tests/search_api_test/src/MethodOverrides.php @@ -0,0 +1,50 @@ +getConfiguration() !== ['test' => 'foobar']) { + trigger_error('Critical server method called with incorrect backend configuration.', E_USER_ERROR); + } + return TRUE; + } + + /** + * Provides an override for the test backend's indexItems() method. + * + * @param \Drupal\search_api\Backend\BackendInterface $backend + * The backend plugin on which the method was called. + * @param \Drupal\search_api\IndexInterface $index + * The search index for which items should be indexed. + * @param \Drupal\search_api\Item\ItemInterface[] $items + * An array of items to be indexed, keyed by their item IDs. + * + * @return string[] + * The array keys of $items. + */ + public static function overrideTestBackendIndexItems(BackendInterface $backend, IndexInterface $index, array $items) { + if ($backend->getConfiguration() !== ['test' => 'foobar']) { + trigger_error('Server method indexItems() called with incorrect backend configuration.', E_USER_ERROR); + } + return array_keys($items); + } + +} diff --git a/tests/search_api_test/src/Plugin/search_api/backend/TestBackend.php b/tests/search_api_test/src/Plugin/search_api/backend/TestBackend.php index e0db91a..b0ec81f 100644 --- a/tests/search_api_test/src/Plugin/search_api/backend/TestBackend.php +++ b/tests/search_api_test/src/Plugin/search_api/backend/TestBackend.php @@ -30,7 +30,22 @@ class TestBackend extends BackendPluginBase implements PluginFormInterface { /** * {@inheritdoc} */ + public function postInsert() { + if ($override = $this->getMethodOverride(__FUNCTION__)) { + call_user_func($override, $this); + return; + } + $this->checkError(__FUNCTION__); + } + + /** + * {@inheritdoc} + */ public function preUpdate() { + if ($override = $this->getMethodOverride(__FUNCTION__)) { + call_user_func($override, $this); + return; + } $this->checkError(__FUNCTION__); } @@ -38,6 +53,9 @@ class TestBackend extends BackendPluginBase implements PluginFormInterface { * {@inheritdoc} */ public function postUpdate() { + if ($override = $this->getMethodOverride(__FUNCTION__)) { + return call_user_func($override, $this); + } $this->checkError(__FUNCTION__); return $this->getReturnValue(__FUNCTION__, FALSE); } @@ -96,6 +114,9 @@ class TestBackend extends BackendPluginBase implements PluginFormInterface { * {@inheritdoc} */ public function indexItems(IndexInterface $index, array $items) { + if ($override = $this->getMethodOverride(__FUNCTION__)) { + return call_user_func($override, $this, $index, $items); + } $this->checkError(__FUNCTION__); $state = \Drupal::state(); @@ -117,6 +138,10 @@ class TestBackend extends BackendPluginBase implements PluginFormInterface { * {@inheritdoc} */ public function addIndex(IndexInterface $index) { + if ($override = $this->getMethodOverride(__FUNCTION__)) { + call_user_func($override, $this, $index); + return; + } $this->checkError(__FUNCTION__); } @@ -124,6 +149,10 @@ class TestBackend extends BackendPluginBase implements PluginFormInterface { * {@inheritdoc} */ public function updateIndex(IndexInterface $index) { + if ($override = $this->getMethodOverride(__FUNCTION__)) { + call_user_func($override, $this, $index); + return; + } $this->checkError(__FUNCTION__); $index->reindex(); } @@ -132,6 +161,10 @@ class TestBackend extends BackendPluginBase implements PluginFormInterface { * {@inheritdoc} */ public function removeIndex($index) { + if ($override = $this->getMethodOverride(__FUNCTION__)) { + call_user_func($override, $this, $index); + return; + } $this->checkError(__FUNCTION__); } @@ -139,6 +172,10 @@ class TestBackend extends BackendPluginBase implements PluginFormInterface { * {@inheritdoc} */ public function deleteItems(IndexInterface $index, array $item_ids) { + if ($override = $this->getMethodOverride(__FUNCTION__)) { + call_user_func($override, $this, $index, $item_ids); + return; + } $this->checkError(__FUNCTION__); $state = \Drupal::state(); @@ -155,6 +192,10 @@ class TestBackend extends BackendPluginBase implements PluginFormInterface { * {@inheritdoc} */ public function deleteAllIndexItems(IndexInterface $index, $datasource_id = NULL) { + if ($override = $this->getMethodOverride(__FUNCTION__)) { + call_user_func($override, $this, $index, $datasource_id); + return; + } $this->checkError(__FUNCTION__); $key = 'search_api_test.backend.indexed.' . $index->id(); @@ -178,6 +219,10 @@ class TestBackend extends BackendPluginBase implements PluginFormInterface { * {@inheritdoc} */ public function search(QueryInterface $query) { + if ($override = $this->getMethodOverride(__FUNCTION__)) { + call_user_func($override, $this, $query); + return; + } $this->checkError(__FUNCTION__); $results = $query->getResults(); @@ -217,6 +262,9 @@ class TestBackend extends BackendPluginBase implements PluginFormInterface { * {@inheritdoc} */ public function isAvailable() { + if ($override = $this->getMethodOverride(__FUNCTION__)) { + return call_user_func($override, $this); + } return $this->getReturnValue(__FUNCTION__, TRUE); } @@ -224,6 +272,9 @@ class TestBackend extends BackendPluginBase implements PluginFormInterface { * {@inheritdoc} */ public function getDiscouragedProcessors() { + if ($override = $this->getMethodOverride(__FUNCTION__)) { + return (array) call_user_func($override, $this); + } return $this->getReturnValue(__FUNCTION__, array()); } diff --git a/tests/search_api_test/src/PluginTestTrait.php b/tests/search_api_test/src/PluginTestTrait.php index 4f1e11d..d9f5907 100644 --- a/tests/search_api_test/src/PluginTestTrait.php +++ b/tests/search_api_test/src/PluginTestTrait.php @@ -41,6 +41,21 @@ trait PluginTestTrait { } /** + * Overrides a method for a certain plugin. + * + * @param string $plugin_type + * The "short" plugin type. + * @param string $method + * The name of the method to override. + * @param callable|null $override + * The new code of the method, or NULL to use the default. + */ + protected function setMethodOverride($plugin_type, $method, callable $override = NULL) { + $key = "search_api_test.$plugin_type.method.$method"; + \Drupal::state()->set($key, $override); + } + + /** * Retrieves the methods called on a given plugin. * * @param string $plugin_type diff --git a/tests/search_api_test/src/TestPluginTrait.php b/tests/search_api_test/src/TestPluginTrait.php index 19d8386..e68a190 100644 --- a/tests/search_api_test/src/TestPluginTrait.php +++ b/tests/search_api_test/src/TestPluginTrait.php @@ -74,6 +74,21 @@ trait TestPluginTrait { } /** + * Retrieves a possible override set for the given method. + * + * @param string $method + * The name of the method. + * + * @return callable|null + * The method override to use, or NULL if none was set. + */ + protected function getMethodOverride($method) { + $type = $this->getPluginType(); + $key = "search_api_test.$type.method.$method"; + return \Drupal::state()->get($key); + } + + /** * Returns the plugin type of this object. * * Equivalent to the last component of the namespace. diff --git a/tests/src/Functional/ConfigOverrideIntegrationTest.php b/tests/src/Functional/ConfigOverrideIntegrationTest.php new file mode 100644 index 0000000..1f9a1e1 --- /dev/null +++ b/tests/src/Functional/ConfigOverrideIntegrationTest.php @@ -0,0 +1,203 @@ + 'Overridden server', + 'required' => TRUE, + ); + $settings['config']['search_api.server.test_server']['status'] = (object) array( + 'value' => TRUE, + 'required' => TRUE, + ); + $settings['config']['search_api.server.test_server']['backend_config']['test'] = (object) array( + 'value' => 'foobar', + 'required' => TRUE, + ); + $settings['config']['search_api.server.test_index']['name'] = (object) array( + 'value' => 'Overridden index', + 'required' => TRUE, + ); + $this->writeSettings($settings); + + $permissions = [ + 'administer search_api', + 'access administration pages', + 'administer nodes', + 'bypass node access', + 'administer content types', + ]; + $this->adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + } + + /** + * Tests that the UI works correctly with config entity overrides present. + */ + public function testConfigOverrideIntegration() { + $base_path = 'admin/config/search/search-api'; + $new_user = $this->drupalCreateUser(); + + // Set up a trap through method overrides to ensure that critical methods + // are only ever called with the correct, overridden backend configuration. + $override = [MethodOverrides::class, 'overrideTestBackendMethod']; + $methods = [ + 'postInsert', + 'preUpdate', + 'postUpdate', + 'addIndex', + 'updateIndex', + 'removeIndex', + 'deleteItems', + 'deleteAllIndexItems', + 'search', + 'isAvailable', + 'getDiscouragedProcessors', + ]; + foreach ($methods as $method) { + $this->setMethodOverride('backend', $method, $override); + } + // indexItems() needs a special override since it needs to return item IDs. + $override = [MethodOverrides::class, 'overrideTestBackendIndexItems']; + $this->setMethodOverride('backend', 'indexItems', $override); + + // Add the server. + $this->drupalGet("$base_path/add-server"); + $edit = [ + 'id' => 'test_server', + 'name' => 'Test server', + 'backend' => 'search_api_test', + ]; + $this->submitForm($edit, 'Save'); + $this->assertSession() + ->pageTextContains('The server was successfully saved.'); + $this->assertSession()->addressEquals($base_path . '/server/test_server'); + + // Add the index. + $this->drupalGet("$base_path/add-index"); + $edit = [ + 'id' => 'test_index', + 'name' => 'Test index', + 'server' => 'test_server', + 'datasources[entity:user]' => TRUE, + 'options[index_directly]' => FALSE, + ]; + $this->submitForm($edit, 'Save'); + $this->assertSession() + ->pageTextContains('Please configure the used datasources.'); + $this->submitForm([], 'Save'); + $this->checkForMetaRefresh(); + $this->assertSession() + ->pageTextContains('The index was successfully saved.'); + $this->assertSession()->addressEquals($base_path . '/index/test_index'); + $server = Index::load('test_index')->getServerInstance(); + $this->assertNotEmpty($server); + $this->assertEquals('test_server', $server->id()); + + // Normal server page displays overridden values. + $this->drupalGet($base_path . '/server/test_server'); + $this->assertSession()->pageTextContains('Overridden server'); + $this->assertSession()->pageTextNotContains('Test server'); + $this->assertSession()->pageTextNotContains('Test index'); + $this->assertSession()->pageTextContains('Overridden index'); + $this->assertSession()->pageTextContains('enabled'); + $this->assertSession()->pageTextNotContains('disabled'); + + // Disabling the server isn't possible. + $this->clickLink('disable'); + $this->assertSession()->pageTextContains('enabled'); + $this->assertSession()->pageTextNotContains('disabled'); + + // The "Edit" form shows the override-free server. + $this->drupalGet($base_path . '/server/test_server'); + $this->assertSession()->pageTextContains('Test server'); + $this->assertSession()->pageTextNotContains('Overridden server'); + $this->assertSession()->pageTextNotContains('foobar'); + $edit = [ + 'name' => 'New server name', + 'backend_config[test]' => 'nonsense', + ]; + $this->submitForm($edit, 'Save'); + + // "View" tab still shows overrides, "Edit" tab still doesn't. + $this->assertSession()->addressEquals($base_path . '/server/test_server'); + $this->assertSession()->pageTextContains('Overridden server'); + $this->assertSession()->pageTextNotContains('New server name'); + $this->drupalGet($base_path . '/server/test_server'); + $this->assertSession()->pageTextContains('New server name'); + $this->assertSession()->pageTextNotContains('Overridden server'); + $this->assertSession()->pageTextContains('nonsense'); + $this->assertSession()->pageTextNotContains('foobar'); + + // Server "clear" form and command use overridden server. + $this->drupalGet($base_path . '/server/test_server/clear'); + $this->assertSession()->pageTextContains('Overridden server'); + $this->assertSession()->pageTextNotContains('New server name'); + $this->submitForm([], 'Confirm'); + + // Index "View" tab uses overridden index and server. + $this->drupalGet($base_path . '/index/test_index'); + $this->assertSession()->pageTextContains('Overridden index'); + $this->assertSession()->pageTextNotContains('Test index'); + $this->assertSession()->pageTextContains('Overridden server'); + $this->assertSession()->pageTextNotContains('New server name'); + $this->assertSession()->pageTextContains('enabled'); + $this->assertSession()->pageTextNotContains('disabled'); + + // Index items, see if that triggers an error. + $this->submitForm([], 'Index now'); + $this->checkForMetaRefresh(); + + // Same for deleting an item. + $new_user->delete(); + + // The "Edit", "Fields" and "Processors" tabs should use the override-free + // index version. + foreach (['edit', 'fields', 'fields/add', 'processors'] as $tab) { + $this->drupalGet("$base_path/index/test_index/$tab"); + $this->assertSession()->pageTextContains('Test index'); + $this->assertSession()->pageTextNotContains('Overridden index'); + } + + // The "Reindex" and "Clear" forms and commands both use the overridden + // index. + foreach (['reindex', 'clear'] as $tab) { + $this->drupalGet("$base_path/index/test_index/$tab"); + $this->assertSession()->pageTextContains('Overridden index'); + $this->assertSession()->pageTextNotContains('Test index'); + } + // Clear the index, see if that triggers an error. + $this->submitForm([], 'Confirm'); + + // The server "Delete" form also uses overrides. + $this->drupalGet($base_path . '/server/test_server/delete'); + $this->assertSession()->pageTextContains('Overridden server'); + $this->assertSession()->pageTextNotContains('New server name'); + $this->assertSession()->pageTextContains('Overridden index'); + $this->assertSession()->pageTextNotContains('Test index'); + + // Delete the server, see if that triggers any errors. + $this->submitForm([], 'Delete'); + } + +} diff --git a/tests/src/Kernel/BackendTestBase.php b/tests/src/Kernel/BackendTestBase.php index 73f3175..5157dc7 100644 --- a/tests/src/Kernel/BackendTestBase.php +++ b/tests/src/Kernel/BackendTestBase.php @@ -1018,6 +1018,8 @@ abstract class BackendTestBase extends KernelTestBase { $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/ConfigOverrideKernelTest.php b/tests/src/Kernel/ConfigOverrideKernelTest.php new file mode 100644 index 0000000..a111e26 --- /dev/null +++ b/tests/src/Kernel/ConfigOverrideKernelTest.php @@ -0,0 +1,220 @@ +installSchema('search_api', ['search_api_item']); + $this->installSchema('system', ['router']); + $this->installSchema('user', ['users_data']); + $this->installEntitySchema('user'); + $this->installEntitySchema('search_api_task'); + + // 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 (php_sapi_name() != 'cli') { + \Drupal::state()->set('search_api_use_tracking_batch', FALSE); + } + + // Set tracking page size so tracking will work properly. + \Drupal::configFactory() + ->getEditable('search_api.settings') + ->set('tracking_page_size', 100) + ->save(); + + // Set up overrides. + $GLOBALS['config']['search_api.server.test_server'] = [ + 'name' => 'Overridden server', + 'backend' => 'search_api_test', + 'backend_config' => [ + 'test' => 'foobar', + ], + ]; + $GLOBALS['config']['search_api.index.test_index'] = [ + 'name' => 'Overridden index', + 'server' => 'test_server', + 'processor_settings' => [ + 'search_api_test' => [], + ], + ]; + + // Create a test server and index. + $this->server = Server::create([ + 'id' => 'test_server', + 'name' => 'Test server', + 'backend' => 'does not exist', + ]); + $this->index = Index::create([ + 'id' => 'test_index', + 'name' => 'Test index', + 'server' => 'unknown_server', + 'datasource_settings' => [ + 'entity:user' => [], + ], + 'tracker_settings' => [ + 'default' => [], + ], + ]); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() { + parent::tearDown(); + + unset($GLOBALS['config']['search_api.server.test_server']); + unset($GLOBALS['config']['search_api.index.test_index']); + } + + /** + * Checks whether saving an index with overrides works correctly. + */ + public function testIndexSave() { + $this->server->save(); + + // Even though no processors are set on the index, saving it should trigger + // the test processor's preIndexSave() method (since we added that processor + // in the override). + $this->assertEmpty($this->index->getProcessorsByStage(ProcessorInterface::STAGE_PRE_INDEX_SAVE)); + $this->index->save(); + $this->assertEquals(['preIndexSave'], $this->getCalledMethods('processor')); + $this->assertEmpty($this->index->getProcessorsByStage(ProcessorInterface::STAGE_PRE_INDEX_SAVE)); + + // Verify the override is correctly present when loading the index. + /** @var \Drupal\search_api\IndexInterface $index */ + $index = Index::load($this->index->id()); + $this->assertEquals('Overridden index', $index->label()); + $server = $index->getServerInstance(); + $this->assertNotEmpty($server); + $this->assertEquals('Overridden server', $server->label()); + $this->assertTrue($index->status()); + + // Verify that overrides are not present when loading the index + // override-free. + /** @var \Drupal\search_api\Entity\SearchApiConfigEntityStorage $index_storage */ + $index_storage = \Drupal::entityTypeManager() + ->getStorage('search_api_index'); + $index = $index_storage->loadOverrideFree($index->id()); + $this->assertEquals('Test index', $index->label()); + + // Try to change the index's name (starting from the override-free index) + // and verify a copy with overrides is used for post-save operations. + $args = []; + $this->setMethodOverride('backend', 'updateIndex', function () use (&$args) { + $args = func_get_args(); + }); + $index->set('name', 'New index name')->save(); + $this->assertCount(2, $args); + $index = $args[1]; + $this->assertEquals('Overridden index', $index->label()); + + // Verify the override is correctly present when loading the index. + $index = Index::load($this->index->id()); + $this->assertEquals('Overridden index', $index->label()); + $this->assertEquals('Overridden server', $index->getServerInstance() + ->label()); + + // Verify the new name is included when loading the index override-free. + $index = $index_storage->loadOverrideFree($index->id()); + $this->assertEquals('New index name', $index->label()); + } + + /** + * Checks whether saving a server with overrides works correctly. + */ + public function testServerSave() { + // Verify that in postInsert() the backend overrides are already applied. + $passed_config = []; + $passed_name = NULL; + $override = function (BackendInterface $backend) use (&$passed_config, &$passed_name) { + $passed_config = $backend->getConfiguration(); + $passed_name = $backend->getServer()->label(); + }; + $this->setMethodOverride('backend', 'postInsert', $override); + $this->server->save(); + $this->assertEquals(['test' => 'foobar'], $passed_config); + $this->assertEquals('Overridden server', $passed_name); + $this->assertEquals('Test server', $this->server->label()); + $this->assertTrue($this->server->status()); + + // Save the index. + $this->index->save(); + + // Verify that on load, the overrides are correctly applied. + $server = Server::load($this->server->id()); + $this->assertEquals('Overridden server', $server->label()); + $this->assertTrue($server->status()); + $this->assertEquals('does not exist', $this->server->getBackendId()); + + // Verify that in preUpdate() the backend overrides are already applied. + $this->setMethodOverride('backend', 'preUpdate', $override); + $this->server->save(); + $this->assertEquals(['test' => 'foobar'], $passed_config); + + // Verify that overriding "status" prevents the server's indexes from being + // disabled when attempting to disable the server. + $GLOBALS['config']['search_api.server.test_server']['status'] = TRUE; + $this->server->disable()->save(); + \Drupal::configFactory()->clearStaticCache(); + $index = Index::load($this->index->id()); + $this->assertTrue($index->status()); + $server = Server::load($this->server->id()); + $this->assertTrue($server->status()); + + // Verify that overrides are not present when loading the server + // override-free. + /** @var \Drupal\search_api\Entity\SearchApiConfigEntityStorage $server_storage */ + $server_storage = \Drupal::entityTypeManager() + ->getStorage('search_api_server'); + $server = $server_storage->loadOverrideFree($server->id()); + $this->assertEquals('Test server', $server->label()); + } + +} diff --git a/tests/src/Kernel/IndexChangesTest.php b/tests/src/Kernel/IndexChangesTest.php index 1aa64b9..44a0385 100644 --- a/tests/src/Kernel/IndexChangesTest.php +++ b/tests/src/Kernel/IndexChangesTest.php @@ -266,11 +266,7 @@ class IndexChangesTest extends KernelTestBase { $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', ); diff --git a/tests/src/Unit/Plugin/Processor/AggregatedFieldsTest.php b/tests/src/Unit/Plugin/Processor/AggregatedFieldsTest.php index e8570ad..0f383bd 100644 --- a/tests/src/Unit/Plugin/Processor/AggregatedFieldsTest.php +++ b/tests/src/Unit/Plugin/Processor/AggregatedFieldsTest.php @@ -365,6 +365,7 @@ class AggregatedFieldsTest extends UnitTestCase { ->willReturnMap(array( array( ProcessorInterface::STAGE_ADD_PROPERTIES, + array(), array( 'aggregated_field' => $this->processor, 'processor1' => $processor_mock, diff --git a/tests/src/Unit/Plugin/Processor/HighlightTest.php b/tests/src/Unit/Plugin/Processor/HighlightTest.php index b017e74..fb4b97f 100644 --- a/tests/src/Unit/Plugin/Processor/HighlightTest.php +++ b/tests/src/Unit/Plugin/Processor/HighlightTest.php @@ -862,6 +862,7 @@ class HighlightTest extends UnitTestCase { ->willReturnMap(array( array( ProcessorInterface::STAGE_ADD_PROPERTIES, + array(), array( 'aggregated_field' => $this->processor, 'processor1' => $processor_mock, diff --git a/tests/src/Unit/Plugin/Processor/TestFieldsProcessorPlugin.php b/tests/src/Unit/Plugin/Processor/TestFieldsProcessorPlugin.php index e9dbd0b..91f3a93 100644 --- a/tests/src/Unit/Plugin/Processor/TestFieldsProcessorPlugin.php +++ b/tests/src/Unit/Plugin/Processor/TestFieldsProcessorPlugin.php @@ -59,7 +59,7 @@ class TestFieldsProcessorPlugin extends FieldsProcessorPluginBase { * @param callable|null $override * The new code of the method, or NULL to use the default. */ - public function setMethodOverride($method, $override = NULL) { + public function setMethodOverride($method, callable $override = NULL) { $this->methodOverrides[$method] = $override; }