diff --git a/src/Datasource/DatasourcePluginBase.php b/src/Datasource/DatasourcePluginBase.php index 2f1fa8b..3a91803 100644 --- a/src/Datasource/DatasourcePluginBase.php +++ b/src/Datasource/DatasourcePluginBase.php @@ -41,13 +41,49 @@ abstract class DatasourcePluginBase extends IndexPluginBase implements Datasourc /** * {@inheritdoc} */ - public function getBundles() { + public function getPropertyDefinitions() { + return array(); + } + + /** + * {@inheritdoc} + */ + public function load($id) { + $items = $this->loadMultiple(array($id)); + return $items ? reset($items) : NULL; + } + + /** + * {@inheritdoc} + */ + public function loadMultiple(array $ids) { return array(); } /** * {@inheritdoc} */ + public function getItemLabel(ComplexDataInterface $item) { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function getItemBundle(ComplexDataInterface $item) { + return $this->getPluginId(); + } + + /** + * {@inheritdoc} + */ + public function getItemUrl(ComplexDataInterface $item) { + return NULL; + } + + /** + * {@inheritdoc} + */ public function getViewModes($bundle = NULL) { return array(); } @@ -55,6 +91,13 @@ abstract class DatasourcePluginBase extends IndexPluginBase implements Datasourc /** * {@inheritdoc} */ + public function getBundles() { + return array(); + } + + /** + * {@inheritdoc} + */ public function viewItem(ComplexDataInterface $item, $view_mode, $langcode = NULL) { return array(); } @@ -70,4 +113,18 @@ abstract class DatasourcePluginBase extends IndexPluginBase implements Datasourc return $build; } + /** + * {@inheritdoc} + */ + public function getEntityTypeId() { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function getItemIds($page = NULL) { + return array(); + } + } diff --git a/src/Entity/Index.php b/src/Entity/Index.php index 5069f9d..14191af 100644 --- a/src/Entity/Index.php +++ b/src/Entity/Index.php @@ -10,7 +10,6 @@ namespace Drupal\search_api\Entity; use Drupal\Component\Render\FormattableMarkup; use Drupal\Core\Cache\Cache; use Drupal\Core\Config\Entity\ConfigEntityBase; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\search_api\IndexInterface; use Drupal\search_api\Item\FieldInterface; @@ -1342,26 +1341,111 @@ class Index extends ConfigEntityBase implements IndexInterface { * {@inheritdoc} */ public function calculateDependencies() { - parent::calculateDependencies(); + $dependencies = $this->getDependencyData(); + $this->dependencies = array(); - // Add a dependency on the server, if there is one set. - if ($this->hasValidServer()) { - $this->addDependency('config', $this->getServer()->getConfigDependencyName()); + foreach ($dependencies as $type => $list) { + $this->dependencies[$type] = array_keys($list); } - // Add dependencies for all of the index's plugins. - if ($this->hasValidTracker()) { - $this->calculatePluginDependencies($this->getTracker()); + + return $this; + } + + /** + * Retrieves data about this index's dependencies. + * + * The return value is structured as follows: + * + * @code + * array( + * 'config' => array( + * 'CONFIG_DEPENDENCY_KEY' => array( + * 'always' => array( + * 'processors' => array( + * 'PROCESSOR_ID' => $processor, + * ), + * 'datasources' => array( + * 'DATASOURCE_ID_1' => $datasource_1, + * 'DATASOURCE_ID_2' => $datasource_2, + * ), + * ), + * 'optional' => array( + * 'index' => array( + * 'INDEX_ID' => $index, + * ), + * 'tracker' => array( + * 'TRACKER_ID' => $tracker, + * ), + * ), + * ), + * ) + * ) + * @endcode + * + * @return object[][][][][] + * An associative array containing the index's dependencies. The array is + * first keyed by the config dependency type ("module", "config", etc.) and + * then by the names of the config dependencies of that type which the index + * has. The values are associative arrays with up to two keys, "always" and + * "optional", specifying whether the dependency is a hard one by the plugin + * (or index) in question or potentially depending on the configuration. The + * values on this level are arrays with keys "index", "tracker", + * "datasources" and/or "processors" and values arrays of IDs mapped to + * their entities/plugins. + */ + protected function getDependencyData() { + $dependency_data = array(); + + // Since calculateDependencies() will work directly on the $dependencies + // property, we first save its original state and then restore it + // afterwards. + $original_dependencies = $this->dependencies; + parent::calculateDependencies(); + foreach ($this->dependencies as $dependency_type => $list) { + foreach ($list as $name) { + $dependency_data[$dependency_type][$name]['always']['index'][$this->id] = $this; + } } - foreach ($this->getProcessors() as $processor) { - $this->calculatePluginDependencies($processor); - $processor_dependencies = $processor->calculateDependencies(); - $this->dependencies += $processor_dependencies; + $this->dependencies = $original_dependencies; + + // The server needs special treatment, since it is a dependency of the index + // itself, and not one of its plugins. + if ($this->hasValidServer()) { + $name = $this->getServer()->getConfigDependencyName(); + $dependency_data['config'][$name]['optional']['index'][$this->id] = $this; } - foreach ($this->getDatasources() as $datasource) { - $this->calculatePluginDependencies($datasource); + + // All other plugins can be treated uniformly. + $plugins = $this->getAllPlugins(); + + foreach ($plugins as $plugin_type => $type_plugins) { + foreach ($type_plugins as $plugin_id => $plugin) { + // Largely copied from + // \Drupal\Core\Plugin\PluginDependencyTrait::calculatePluginDependencies(). + $definition = $plugin->getPluginDefinition(); + + // First, always depend on the module providing the plugin. + $dependency_data['module'][$definition['provider']]['always'][$plugin_type][$plugin_id] = $plugin; + + // Plugins can declare additional dependencies in their definition. + if (isset($definition['config_dependencies'])) { + foreach ($definition['config_dependencies'] as $dependency_type => $list) { + foreach ($list as $name) { + $dependency_data[$dependency_type][$name]['always'][$plugin_type][$plugin_id] = $plugin; + } + } + } + + // Finally, add the dynamically-calculated dependencies of the plugin. + foreach ($plugin->calculateDependencies() as $dependency_type => $list) { + foreach ($list as $name) { + $dependency_data[$dependency_type][$name]['optional'][$plugin_type][$plugin_id] = $plugin; + } + } + } } - return $this->dependencies; + return $dependency_data; } /** @@ -1370,44 +1454,130 @@ class Index extends ConfigEntityBase implements IndexInterface { public function onDependencyRemoval(array $dependencies) { $changed = parent::onDependencyRemoval($dependencies); - // @todo Also react sensibly when removing the dependency of a plugin or an - // indexed field. See #2574633 and #2541206. - foreach ($dependencies['config'] as $entity) { - if ($entity instanceof EntityInterface && $entity->getEntityTypeId() == 'search_api_server') { - // Remove this index from the deleted server (thus disabling it). - $this->setServer(NULL); - $this->setStatus(FALSE); - $changed = TRUE; + $all_plugins = $this->getAllPlugins(); + $dependency_data = $this->getDependencyData(); + // Make sure our dependency data has the exact same keys as $dependencies, + // to simplify the subsequent code. + $dependencies = array_filter($dependencies); + $dependency_data = array_intersect_key($dependency_data, $dependencies); + $dependency_data += array_fill_keys(array_keys($dependencies), array()); + $call_on_removal = array(); + + foreach ($dependencies as $dependency_type => $dependency_objects) { + $dependency_data[$dependency_type] = array_intersect_key($dependency_data[$dependency_type], $dependency_objects); + foreach ($dependency_data[$dependency_type] as $name => $dependency_sources) { + // We first remove all the "hard" dependencies. + if (!empty($dependency_sources['always'])) { + foreach ($dependency_sources['always'] as $plugin_type => $plugins) { + // We can hardly remove the index itself. + if ($plugin_type == 'index') { + continue; + } + + $all_plugins[$plugin_type] = array_diff_key($all_plugins[$plugin_type], $plugins); + $changed = TRUE; + } + } + + // Then, collect all the optional ones. + if (!empty($dependency_sources['optional'])) { + // However this plays out, it will lead to a change. + $changed = TRUE; + + foreach ($dependency_sources['optional'] as $plugin_type => $plugins) { + // Deal with the index right away, since that dependency can only be + // the server. + if ($plugin_type == 'index') { + $this->setServer(NULL); + continue; + } + + // Only include those plugins that have not already been removed. + $plugins = array_intersect_key($plugins, $all_plugins[$plugin_type]); + + foreach ($plugins as $plugin_id => $plugin) { + $call_on_removal[$plugin_type][$plugin_id][$dependency_type][$name] = $dependency_objects[$name]; + } + } + } } } - $processors = $this->getOption('processors', array()); - foreach ($dependencies['module'] as $module) { - foreach ($this->getProcessors() as $key => $processor) { - if ($processor->getPluginDefinition()['provider'] == $module) { - unset($processors[$key]); - $changed = TRUE; + $updated_config = array(); + foreach ($call_on_removal as $plugin_type => $plugins) { + foreach ($plugins as $plugin_id => $plugin_dependencies) { + $removal_successful = $all_plugins[$plugin_type][$plugin_id]->onDependencyRemoval($plugin_dependencies); + if ($removal_successful) { + $updated_config[$plugin_type][$plugin_id] = $all_plugins[$plugin_type][$plugin_id]->getConfiguration(); + } + else { + unset($all_plugins[$plugin_type][$plugin_id]); } } } - $this->setOption('processors', $processors); - // Make sure that all enabled processors are notified about the dependencies - // that will suddenly be removed and give it time to react to that. - $processors = $this->getOption('processors', array()); - foreach ($this->getProcessors() as $name => $processor) { - $is_processor_changed = $processor->onDependencyRemoval($this, $dependencies); - if ($is_processor_changed) { - unset($processors[$name]); - $changed = TRUE; + // The handling of how we translate plugin changes back to the index varies + // according to plugin type, unfortunately. + // First, remove plugins that need to be removed. + $this->processors = array_intersect_key($this->processors, $all_plugins['processors']); + $this->datasources = array_keys($all_plugins['datasources']); + $this->datasource_configs = array_intersect_key($this->datasource_configs, $all_plugins['datasources']); + // There always needs to be a tracker. + if (empty($all_plugins['tracker'])) { + $this->tracker = \Drupal::config('search_api.settings')->get('default_tracker'); + $this->tracker_config = array(); + } + // There also always needs to be a datasource, but here we have no easy way + // out – if we had to remove all datasources, the operation fails. Return + // FALSE to indicate this (and hope this means the right thing). + if (!$this->datasources) { + return FALSE; + } + + // Then, update configuration as necessary. + foreach ($updated_config as $plugin_type => $plugin_configs) { + foreach ($plugin_configs as $plugin_id => $plugin_config) { + switch ($plugin_type) { + case 'processors': + $this->processors[$plugin_id]['settings'] = $plugin_config; + break; + case 'datasources': + $this->datasource_configs[$plugin_id] = $plugin_config; + break; + case 'tracker': + $this->tracker_config = $plugin_config; + break; + } } } - $this->setOption('processors', $processors); + + if ($changed) { + $this->resetCaches(); + } return $changed; } /** + * Retrieves all the plugins contained in this index. + * + * @return \Drupal\search_api\Plugin\IndexPluginInterface[][] + * All plugins contained in this index, keyed by their property on the index + * and their plugin ID. + */ + protected function getAllPlugins() { + $plugins = array(); + + if ($this->hasValidTracker()) { + $plugins['tracker'][$this->getTrackerId()] = $this->getTracker(); + } + $plugins['processors'] = $this->getProcessors(); + $plugins['datasources'] = $this->getDatasources(); + + return $plugins; + } + + /** * Implements the magic __clone() method. * * Prevents the cached plugins and fields from being cloned, too (since they diff --git a/src/Entity/Server.php b/src/Entity/Server.php index 8bf966c..1f7c0a0 100644 --- a/src/Entity/Server.php +++ b/src/Entity/Server.php @@ -445,7 +445,33 @@ class Server extends ConfigEntityBase implements ServerInterface { $this->calculatePluginDependencies($this->getBackend()); } - return $this->dependencies; + return $this; + } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $changed = parent::onDependencyRemoval($dependencies); + + if ($this->hasValidBackend()) { + $removed_backend_dependencies = array(); + $backend = $this->getBackend(); + foreach ($backend->calculateDependencies() as $dependency_type => $list) { + if (isset($dependencies[$dependency_type])) { + $removed_backend_dependencies[$dependency_type] = array_intersect_key($dependencies[$dependency_type], array_flip($list)); + } + } + $removed_backend_dependencies = array_filter($removed_backend_dependencies); + if ($removed_backend_dependencies) { + if ($backend->onDependencyRemoval($removed_backend_dependencies)) { + $this->backend_config = $backend->getConfiguration(); + $changed = TRUE; + } + } + } + + return $changed; } /** diff --git a/src/Plugin/ConfigurablePluginBase.php b/src/Plugin/ConfigurablePluginBase.php index 40a3623..9c8f78d 100644 --- a/src/Plugin/ConfigurablePluginBase.php +++ b/src/Plugin/ConfigurablePluginBase.php @@ -10,7 +10,6 @@ namespace Drupal\search_api\Plugin; use Drupal\Core\Entity\DependencyTrait; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\PluginBase; -use Drupal\search_api\IndexInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -107,7 +106,7 @@ abstract class ConfigurablePluginBase extends PluginBase implements Configurable /** * {@inheritdoc} */ - public function onDependencyRemoval(IndexInterface $index, array $dependencies) { + public function onDependencyRemoval(array $dependencies) { // By default, we're not reacting to anything and so we should leave // everything as it was. return FALSE; diff --git a/src/Plugin/ConfigurablePluginInterface.php b/src/Plugin/ConfigurablePluginInterface.php index d0d5345..67227bd 100644 --- a/src/Plugin/ConfigurablePluginInterface.php +++ b/src/Plugin/ConfigurablePluginInterface.php @@ -12,7 +12,6 @@ use Drupal\Component\Plugin\DerivativeInspectionInterface; use Drupal\Component\Plugin\PluginInspectionInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginFormInterface; -use Drupal\search_api\IndexInterface; /** * Describes a configurable Search API plugin. @@ -37,16 +36,22 @@ interface ConfigurablePluginInterface extends PluginInspectionInterface, Derivat public function getDescription(); /** - * Informs the plugin that entities it depends on will be deleted. + * Informs the plugin that some of its dependencies are being removed. * - * @param \Drupal\search_api\IndexInterface $index - * The index we're being removed from. - * @param array $dependencies - * An array of dependencies + * The plugin should attempt to change its configuration in a way to remove + * its dependency on those items. However, to avoid problems, it should (as + * far as possible) not add any new dependencies in the process, since there + * is no guarantee that those are not currently being removed, too. + * + * @param object[][] $dependencies + * An array of dependencies, keyed by dependency type ("module", "config", + * etc.) and dependency name. * * @return bool - * Has something changed? + * Whether the dependency was successfully removed from the plugin – i.e., + * after the configuration changes that were made, none of the removed + * items are dependencies of this plugin anymore. */ - public function onDependencyRemoval(IndexInterface $index, array $dependencies); + public function onDependencyRemoval(array $dependencies); } diff --git a/src/Plugin/search_api/datasource/ContentEntity.php b/src/Plugin/search_api/datasource/ContentEntity.php index 3c53b91..8bb2378 100644 --- a/src/Plugin/search_api/datasource/ContentEntity.php +++ b/src/Plugin/search_api/datasource/ContentEntity.php @@ -350,14 +350,6 @@ class ContentEntity extends DatasourcePluginBase { /** * {@inheritdoc} */ - public function load($id) { - $items = $this->loadMultiple(array($id)); - return $items ? reset($items) : NULL; - } - - /** - * {@inheritdoc} - */ public function loadMultiple(array $ids) { $allowed_languages = $all_languages = $this->getLanguageManager()->getLanguages(); diff --git a/src/Plugin/search_api/processor/RenderedItem.php b/src/Plugin/search_api/processor/RenderedItem.php index 68069d4..b817e34 100644 --- a/src/Plugin/search_api/processor/RenderedItem.php +++ b/src/Plugin/search_api/processor/RenderedItem.php @@ -315,4 +315,36 @@ class RenderedItem extends ProcessorPluginBase { return $this->dependencies; } + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + // Make arrays of dependencies associative to simplify the code. + foreach (array_keys($dependencies) as $dependency_type) { + $dependencies[$dependency_type] = array_combine($dependencies[$dependency_type], $dependencies[$dependency_type]); + } + + $view_modes = $this->configuration['view_mode']; + foreach ($this->index->getDatasources() as $datasource_id => $datasource) { + if ($entity_type_id = $datasource->getEntityTypeId() && !empty($view_modes[$datasource_id])) { + foreach ($view_modes[$datasource_id] as $bundle => $view_mode_id) { + if ($view_mode_id) { + /** @var \Drupal\Core\Entity\EntityViewModeInterface $view_mode */ + $view_mode = EntityViewMode::load($entity_type_id . '.' . $view_mode_id); + if ($view_mode) { + $dependency_key = $view_mode->getConfigDependencyKey(); + $dependency_name = $view_mode->getConfigDependencyName(); + if (!empty($dependencies[$dependency_key][$dependency_name])) { + unset($this->configuration['view_mode'][$datasource_id][$bundle]); + unset($dependencies[$dependency_key][$dependency_name]); + } + } + } + } + } + } + + return empty($dependencies); + } + } diff --git a/src/Tests/DependencyRemovalTest.php b/src/Tests/DependencyRemovalTest.php deleted file mode 100644 index 370bacb..0000000 --- a/src/Tests/DependencyRemovalTest.php +++ /dev/null @@ -1,175 +0,0 @@ -drupalLogin($this->adminUser); - - // Create a server and index. - $this->getTestServer(); - $this->getTestIndex(); - } - - /** - * Tests whether removing dependencies of an index works correctly. - * - * Single test method to avoid the overhead of Drupal installation for each of - * the test methods. - */ - public function testDependencyRemoval() { - $this->checkProcessorRemoval(); - } - - /** - * Tests what happens when a processors dependencies are removed. - * - * This is in a seperate test, because we need the search_api_test_processor - * enabled. - */ - public function testProcessorDependencyRemoval() { - EntityViewMode::create(array('id' => 'node.teaser_test', 'targetEntityType' => 'node'))->save(); - - $this->drupalGet($this->getIndexPath('processors')); - $this->assertText($this->t('Dependency test processor')); - $this->drupalPostForm(NULL, array('status[dependency_test_processor]' => 1), $this->t('Save')); - $this->assertResponse(200); - $this->assertText($this->t('The indexing workflow was successfully edited.')); - - $evm = EntityViewMode::load('node.teaser_test'); - $evm->delete(); - - // The index was removed, because the entity view mode was removed. - $this->drupalGet($this->getIndexPath()); - $this->assertResponse(404); - } - - /** - * Tests what happens when a processors dependencies are removed. - * - * This is in a seperate test, because we need the search_api_test_processor - * enabled. - */ - public function testProcessorDependencyRemoval2() { - EntityViewMode::create(array('id' => 'node.teaser_test', 'targetEntityType' => 'node'))->save(); - \Drupal::state()->set('search_api_test_processor.remove', TRUE); - - $this->drupalGet($this->getIndexPath('processors')); - $this->assertText($this->t('Dependency test processor')); - $this->drupalPostForm(NULL, array('status[dependency_test_processor]' => 1), $this->t('Save')); - $this->assertResponse(200); - $this->assertText($this->t('The indexing workflow was successfully edited.')); - $this->assertFieldChecked('edit-status-dependency-test-processor'); - - $evm = EntityViewMode::load('node.teaser_test'); - $evm->delete(); - - $this->drupalGet($this->getIndexPath()); - $this->assertResponse(200); - - $this->drupalGet($this->getIndexPath('processors')); - $this->assertNoFieldChecked('edit-status-dependency-test-processor'); - } - - /** - * Tests the reaction of an index when a processor's module is removed. - * - * When a module that provides a processor is uninstalled, it should not - * delete the indexes that processor is enabled on. This would cause a chain - * removal of facets and views as well. Instead, the processor should just be - * disabled and no longer be found on the processors tab. - */ - protected function checkProcessorRemoval() { - // Enable the dummy processor on the index. - $this->drupalGet($this->getIndexPath('processors')); - $this->assertText($this->t('Dummy processor')); - $this->drupalPostForm(NULL, array('status[dummy_processor]' => 1), $this->t('Save')); - $this->assertResponse(200); - $this->assertText($this->t('The indexing workflow was successfully edited.')); - - // Uninstall the module. - $this->drupalGet('admin/modules/uninstall'); - $this->drupalPostForm(NULL, array('uninstall[search_api_test_processor]' => 1), $this->t('Uninstall')); - // Make sure the index is not deleted when uninstalling the module. - $this->assertNoText($this->t('The listed configuration will be deleted.')); - $this->assertText($this->t('The listed configuration will be updated.')); - - $this->drupalPostForm(NULL, array(), $this->t('Uninstall')); - $this->assertText($this->t('The selected modules have been uninstalled.')); - - // Make sure the index is still actually present. - $this->drupalGet('admin/config/search/search-api'); - $this->assertText('WebTest Index'); - - // Make sure the processor is no longer found on the processors tab. - $this->drupalGet($this->getIndexPath('processors')); - $this->assertResponse(200); - $this->assertNoText('Dummy processor'); - } - - public function testTrackerRemoval() { - /** @var \Drupal\search_api\IndexInterface $index */ - $index = Index::load('webtest_index'); - if ($index) { - $index->delete(); - } - - $edit = array( - 'name' => 'Web test index', - 'id' => $this->indexId, - 'status' => 1, - 'description' => '&* (description)', - 'server' => 'webtest_server', - 'datasources[]' => array('entity:node'), - 'tracker' => 'test_tracker', - ); - $this->drupalGet('admin/config/search/search-api/add-index'); - $this->drupalPostForm(NULL, $edit, $this->t('Save')); - $this->assertResponse(200); - } - -} diff --git a/tests/search_api_test_backend/config/schema/search_api_test_backend.schema.yml b/tests/search_api_test_backend/config/schema/search_api_test_backend.schema.yml index 12c982a..561b9fe 100644 --- a/tests/search_api_test_backend/config/schema/search_api_test_backend.schema.yml +++ b/tests/search_api_test_backend/config/schema/search_api_test_backend.schema.yml @@ -5,3 +5,6 @@ search_api.backend.plugin.search_api_test_backend: test: type: string label: Test configuration + dependencies: + type: config_dependencies + label: Dependencies diff --git a/tests/search_api_test_backend/src/Plugin/search_api/backend/TestBackend.php b/tests/search_api_test_backend/src/Plugin/search_api/backend/TestBackend.php index 93d60c1..5032989 100644 --- a/tests/search_api_test_backend/src/Plugin/search_api/backend/TestBackend.php +++ b/tests/search_api_test_backend/src/Plugin/search_api/backend/TestBackend.php @@ -174,6 +174,25 @@ class TestBackend extends BackendPluginBase { } /** + * {@inheritdoc} + */ + public function calculateDependencies() { + return !empty($this->configuration['dependencies']) ? $this->configuration['dependencies'] : array(); + } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $key = 'search_api_test_backend.dependencies.remove'; + $remove = \Drupal::state()->get($key, FALSE); + if ($remove) { + unset($this->configuration['dependencies']); + } + return $remove; + } + + /** * Throws an exception if set in the Drupal state for the given method. * * Also records (successful) calls to these methods. @@ -194,4 +213,5 @@ class TestBackend extends BackendPluginBase { $methods_called[] = $method; $state->set($key, $methods_called); } + } diff --git a/tests/search_api_test_dependencies/config/schema/search_api_test_dependencies.schema.yml b/tests/search_api_test_dependencies/config/schema/search_api_test_dependencies.schema.yml new file mode 100644 index 0000000..a96bca8 --- /dev/null +++ b/tests/search_api_test_dependencies/config/schema/search_api_test_dependencies.schema.yml @@ -0,0 +1,9 @@ +search_api.datasource.plugin.search_api_test_dependencies: + type: config_dependencies + +plugin.plugin_configuration.search_api_processor.search_api_test_dependencies: + type: config_dependencies + +plugin.plugin_configuration.search_api_tracker.search_api_test_dependencies: + type: config_dependencies + diff --git a/tests/search_api_test_tracker/search_api_test_tracker.info.yml b/tests/search_api_test_dependencies/search_api_test_dependencies.info.yml similarity index 79% rename from tests/search_api_test_tracker/search_api_test_tracker.info.yml rename to tests/search_api_test_dependencies/search_api_test_dependencies.info.yml index fb364ce..db94294 100644 --- a/tests/search_api_test_tracker/search_api_test_tracker.info.yml +++ b/tests/search_api_test_dependencies/search_api_test_dependencies.info.yml @@ -1,4 +1,4 @@ -name: 'Search API Custom tracker' +name: 'Search API Test Dependencies' type: module description: 'Support module for Search API tests' package: Search diff --git a/tests/search_api_test_dependencies/src/Plugin/search_api/datasource/TestDatasource.php b/tests/search_api_test_dependencies/src/Plugin/search_api/datasource/TestDatasource.php new file mode 100644 index 0000000..800f18f --- /dev/null +++ b/tests/search_api_test_dependencies/src/Plugin/search_api/datasource/TestDatasource.php @@ -0,0 +1,47 @@ +configuration; + } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $remove = \Drupal::state() + ->get('search_api_test_dependencies.datasource.remove', FALSE); + if ($remove) { + $this->configuration = array(); + } + return $remove; + } + +} diff --git a/tests/search_api_test_dependencies/src/Plugin/search_api/processor/TestProcessor.php b/tests/search_api_test_dependencies/src/Plugin/search_api/processor/TestProcessor.php new file mode 100644 index 0000000..5ce230c --- /dev/null +++ b/tests/search_api_test_dependencies/src/Plugin/search_api/processor/TestProcessor.php @@ -0,0 +1,39 @@ +configuration; + } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $remove = \Drupal::state() + ->get('search_api_test_dependencies.processor.remove', FALSE); + if ($remove) { + $this->configuration = array(); + } + return $remove; + } + +} diff --git a/tests/search_api_test_dependencies/src/Plugin/search_api/tracker/TestTracker.php b/tests/search_api_test_dependencies/src/Plugin/search_api/tracker/TestTracker.php new file mode 100644 index 0000000..6ac77d4 --- /dev/null +++ b/tests/search_api_test_dependencies/src/Plugin/search_api/tracker/TestTracker.php @@ -0,0 +1,39 @@ +configuration; + } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $remove = \Drupal::state() + ->get('search_api_test_dependencies.tracker.remove', FALSE); + if ($remove) { + $this->configuration = array(); + } + return $remove; + } + +} diff --git a/tests/search_api_test_processor/config/schema/search_api_test_processor.schema.yml b/tests/search_api_test_processor/config/schema/search_api_test_processor.schema.yml deleted file mode 100644 index 15f5d25..0000000 --- a/tests/search_api_test_processor/config/schema/search_api_test_processor.schema.yml +++ /dev/null @@ -1,21 +0,0 @@ -plugin.plugin_configuration.search_api_processor.dummy_processor: - type: mapping - label: 'Dummy processor configuration' - mapping: - fields: - type: sequence - label: 'The selected fields' - sequence: - type: string - label: 'Selected field' - -plugin.plugin_configuration.search_api_processor.dependency_test_processor: - type: mapping - label: 'Dummy test processor configuration' - mapping: - fields: - type: sequence - label: 'The selected fields' - sequence: - type: string - label: 'Selected field' diff --git a/tests/search_api_test_processor/search_api_test_processor.info.yml b/tests/search_api_test_processor/search_api_test_processor.info.yml deleted file mode 100644 index de85f76..0000000 --- a/tests/search_api_test_processor/search_api_test_processor.info.yml +++ /dev/null @@ -1,8 +0,0 @@ -name: 'Search API Custom processor Test' -type: module -description: 'Support module for Search API tests' -package: Search -dependencies: - - search_api:search_api -core: 8.x -hidden: true diff --git a/tests/search_api_test_processor/src/Plugin/search_api/processor/DependencyTestProcessor.php b/tests/search_api_test_processor/src/Plugin/search_api/processor/DependencyTestProcessor.php deleted file mode 100644 index ebaffc7..0000000 --- a/tests/search_api_test_processor/src/Plugin/search_api/processor/DependencyTestProcessor.php +++ /dev/null @@ -1,65 +0,0 @@ -addDependency('config', $evm->getEntityType()->getConfigPrefix() . '.' . $evm->id()); - } - - return $form; - } - - /** - * {@inheritdoc} - */ - public function calculateDependencies() { - return $this->dependencies; - } - - /** - * {@inheritdoc} - */ - public function onDependencyRemoval(IndexInterface $index, array $dependencies) { - return \Drupal::state()->get('search_api_test_processor.remove'); - } - -} diff --git a/tests/search_api_test_processor/src/Plugin/search_api/processor/DummyProcessor.php b/tests/search_api_test_processor/src/Plugin/search_api/processor/DummyProcessor.php deleted file mode 100644 index 537e6c7..0000000 --- a/tests/search_api_test_processor/src/Plugin/search_api/processor/DummyProcessor.php +++ /dev/null @@ -1,35 +0,0 @@ -addDependency('config', $evm->getEntityType()->getConfigPrefix() . '.' . $evm->id()); - } - - return $form; - } - - /** - * {@inheritdoc} - */ - public function calculateDependencies() { - return $this->dependencies; - } - - /** - * {@inheritdoc} - */ - public function onDependencyRemoval(IndexInterface $index, array $dependencies) { - return \Drupal::state()->get('search_api_test_tracker.remove', FALSE); - } -} diff --git a/tests/src/Kernel/DependencyRemovalTest.php b/tests/src/Kernel/DependencyRemovalTest.php new file mode 100644 index 0000000..7aa0cc3 --- /dev/null +++ b/tests/src/Kernel/DependencyRemovalTest.php @@ -0,0 +1,409 @@ +getMock('Drupal\search_api\Task\ServerTaskManagerInterface'); + $this->container->set('search_api.server_task_manager', $mock); + + // Create the index object, but don't save it yet since we want to change + // its settings anyways in every test. + $this->index = Index::create(array( + 'id' => 'test_index', + 'name' => 'Test index', + 'tracker' => 'default', + 'datasources' => array( + 'entity:user', + ), + )); + + // Use a search server as the dependency, since we have that available + // anyways. The entity type should not matter at all, though. + $this->dependency = Server::create(array( + 'id' => 'dependency', + 'name' => 'Test dependency', + 'backend' => 'search_api_test_backend', + )); + $this->dependency->save(); + } + + /** + * Tests a backend with a dependency that gets removed. + * + * If the dependency does not get removed, proper cascading to the index is + * also verified. + * + * @param bool $remove_dependency + * Whether to remove the dependency from the backend when the object + * depended on is deleted. + * + * @dataProvider dependencyTestDataProvider + */ + public function testBackendDependency($remove_dependency) { + $dependency_key = $this->dependency->getConfigDependencyKey(); + $dependency_name = $this->dependency->getConfigDependencyName(); + + // Create a server using the test backend, and set the dependency in the + // configuration. + /** @var \Drupal\search_api\ServerInterface $server */ + $server = Server::create(array( + 'id' => 'test_server', + 'name' => 'Test server', + 'backend' => 'search_api_test_backend', + 'backend_config' => array( + 'dependencies' => array( + $dependency_key => array( + $dependency_name, + ), + ), + ), + )); + $server->save(); + $server_dependency_key = $server->getConfigDependencyKey(); + $server_dependency_name = $server->getConfigDependencyName(); + + // Set the server on the index and save that, too. However, we don't want + // the index enabled, since that would lead to all kinds of overhead which + // is completely irrelevant for this test. + $this->index->set('server', $server->id()); + $this->index->disable(); + $this->index->save(); + + // Check the dependencies were calculated correctly. + $server_dependencies = $server->getDependencies(); + $this->assertContains($dependency_name, $server_dependencies[$dependency_key], 'Backend dependency correctly inserted'); + $index_dependencies = $this->index->getDependencies(); + $this->assertContains($server_dependency_name, $index_dependencies[$server_dependency_key], 'Server dependency correctly inserted'); + + // Set our magic state key to let the test plugin know whether the + // dependency should be removed or not. See + // \Drupal\search_api_test_backend\Plugin\search_api\backend\TestBackend::onDependencyRemoval(). + $key = 'search_api_test_backend.dependencies.remove'; + \Drupal::state()->set($key, $remove_dependency); + + // Delete the backend's dependency. + $this->dependency->delete(); + + // Reload the index and check it's still there. + $this->reloadIndex(); + $this->assertNotNull($this->index, 'Index not removed'); + + // Reload the server. + $storage = \Drupal::entityTypeManager()->getStorage('search_api_server'); + $storage->resetCache(); + $server = $storage->load($server->id()); + + if ($remove_dependency) { + $this->assertNotNull($server, 'Server was not removed'); + $this->assertArrayNotHasKey('dependencies', $server->get('backend_config'), 'Backend config was adapted'); + // @todo Logically, this should not be changed: if the server does not get + // removed, there is no need to adapt the index's configuration. + // However, the way this config dependency cascading is actually + // implemented in + // \Drupal\Core\Config\ConfigManager::getConfigEntitiesToChangeOnDependencyRemoval() + // does not seem to follow that logic, but just computes the complete + // tree of dependencies once and operates generally on the assumption + // that all of them will be deleted. See #2642374. +// $this->assertEquals($server->id(), $this->index->getServerId(), "Index's server was not changed"); + } + else { + $this->assertNull($server, 'Server was removed'); + $this->assertEquals(NULL, $this->index->getServerId(), 'Index server was changed'); + } + } + + /** + * Tests a datasource with a dependency that gets removed. + * + * @param bool $remove_dependency + * Whether to remove the dependency from the datasource when the object + * depended on is deleted. + * + * @dataProvider dependencyTestDataProvider + */ + public function testDatasourceDependency($remove_dependency) { + // Add the datasource to the index and save it. The datasource configuration + // contains the dependencies it will return – in our case, we use the test + // server. + $dependency_key = $this->dependency->getConfigDependencyKey(); + $dependency_name = $this->dependency->getConfigDependencyName(); + $this->index->set('datasources', array( + 'entity:user', + 'search_api_test_dependencies', + )); + $this->index->set('datasource_configs', array( + 'search_api_test_dependencies' => array( + $dependency_key => array( + $dependency_name, + ), + ), + )); + $this->index->save(); + + // Check the dependencies were calculated correctly. + $dependencies = $this->index->getDependencies(); + $this->assertContains($dependency_name, $dependencies[$dependency_key], 'Datasource dependency correctly inserted'); + + // Set our magic state key to let the test plugin know whether the + // dependency should be removed or not. See + // \Drupal\search_api_test_dependencies\Plugin\search_api\datasource\TestDatasource::onDependencyRemoval(). + $key = 'search_api_test_dependencies.datasource.remove'; + \Drupal::state()->set($key, $remove_dependency); + + // Delete the datasource's dependency. + $this->dependency->delete(); + + // Reload the index and check it's still there. + $this->reloadIndex(); + $this->assertNotNull($this->index, 'Index not removed'); + + // Make sure the dependency has been removed, one way or the other. + $dependencies = $this->index->getDependencies(); + $dependencies += array($dependency_key => array()); + $this->assertNotContains($dependency_name, $dependencies[$dependency_key], 'Datasource dependency removed from index'); + + // Depending on whether the plugin should have removed the dependency or + // not, make sure the right action was taken. + $datasources = $this->index->get('datasources'); + $datasource_configs = $this->index->get('datasource_configs'); + if ($remove_dependency) { + $this->assertContains('search_api_test_dependencies', $datasources, 'Datasource not removed'); + $this->assertEmpty($datasource_configs['search_api_test_dependencies'], 'Datasource settings adapted'); + } + else { + $this->assertNotContains('search_api_test_dependencies', $datasources, 'Datasource removed'); + $this->assertArrayNotHasKey('search_api_test_dependencies', $datasource_configs, 'Datasource config removed'); + } + } + + /** + * Tests removing the (hard) dependency of the index's single datasource. + */ + public function testSingleDatasourceDependency() { + // Add the datasource to the index and save it. The datasource configuration + // contains the dependencies it will return – in our case, we use the test + // server. + $dependency_key = $this->dependency->getConfigDependencyKey(); + $dependency_name = $this->dependency->getConfigDependencyName(); + $this->index->set('datasources', array( + 'search_api_test_dependencies', + )); + $this->index->set('datasource_configs', array( + 'search_api_test_dependencies' => array( + $dependency_key => array( + $dependency_name, + ), + ), + )); + $this->index->save(); + + // Since in this test the index will be removed, we need a mock + $mock = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface'); + $mock_factory = $this->getMock('Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface'); + $mock_factory->method('get')->willReturn($mock); + $this->container->set('keyvalue.expirable', $mock_factory); + + // Delete the datasource's dependency. + $this->dependency->delete(); + + // Reload the index to ensure it was deleted. + $this->reloadIndex(); + $this->assertNull($this->index, 'Index was removed'); + } + + /** + * Tests a processor with a dependency that gets removed. + * + * @param bool $remove_dependency + * Whether to remove the dependency from the processor when the object + * depended on is deleted. + * + * @dataProvider dependencyTestDataProvider + */ + public function testProcessorDependency($remove_dependency) { + // Add the processor to the index and save it. The processor configuration + // contains the dependencies it will return – in our case, we use the test + // server. + $dependency_key = $this->dependency->getConfigDependencyKey(); + $dependency_name = $this->dependency->getConfigDependencyName(); + $this->index->set('processors', array( + 'search_api_test_dependencies' => array( + 'processor_id' => 'search_api_test_dependencies', + 'settings' => array( + $dependency_key => array( + $dependency_name, + ), + ), + ), + )); + $this->index->save(); + + // Check the dependencies were calculated correctly. + $dependencies = $this->index->getDependencies(); + $this->assertContains($dependency_name, $dependencies[$dependency_key], 'Processor dependency correctly inserted'); + + // Set our magic state key to let the test plugin know whether the + // dependency should be removed or not. See + // \Drupal\search_api_test_dependencies\Plugin\search_api\processor\TestProcessor::onDependencyRemoval(). + $key = 'search_api_test_dependencies.processor.remove'; + \Drupal::state()->set($key, $remove_dependency); + + // Delete the processor's dependency. + $this->dependency->delete(); + + // Reload the index and check it's still there. + $this->reloadIndex(); + $this->assertNotNull($this->index, 'Index not removed'); + + // Make sure the dependency has been removed, one way or the other. + $dependencies = $this->index->getDependencies(); + $dependencies += array($dependency_key => array()); + $this->assertNotContains($dependency_name, $dependencies[$dependency_key], 'Processor dependency removed from index'); + + // Depending on whether the plugin should have removed the dependency or + // not, make sure the right action was taken. + $processors = $this->index->get('processors'); + if ($remove_dependency) { + $this->assertArrayHasKey('search_api_test_dependencies', $processors, 'Processor not removed'); + $this->assertEmpty($processors['search_api_test_dependencies']['settings'], 'Processor settings adapted'); + } + else { + $this->assertArrayNotHasKey('search_api_test_dependencies', $processors, 'Processor removed'); + } + } + + /** + * Tests a tracker with a dependency that gets removed. + * + * @param bool $remove_dependency + * Whether to remove the dependency from the tracker when the object + * depended on is deleted. + * + * @dataProvider dependencyTestDataProvider + */ + public function testTrackerDependency($remove_dependency) { + // Set the tracker for the index and save it. The tracker configuration + // contains the dependencies it will return – in our case, we use the test + // server. + $dependency_key = $this->dependency->getConfigDependencyKey(); + $dependency_name = $this->dependency->getConfigDependencyName(); + $this->index->set('tracker', 'search_api_test_dependencies'); + $this->index->set('tracker_config', array( + $dependency_key => array( + $dependency_name, + ), + )); + $this->index->save(); + + // Check the dependencies were calculated correctly. + $dependencies = $this->index->getDependencies(); + $this->assertContains($dependency_name, $dependencies[$dependency_key], 'Tracker dependency correctly inserted'); + + // Set our magic state key to let the test plugin know whether the + // dependency should be removed or not. See + // \Drupal\search_api_test_dependencies\Plugin\search_api\tracker\TestTracker::onDependencyRemoval(). + $key = 'search_api_test_dependencies.tracker.remove'; + \Drupal::state()->set($key, $remove_dependency); + // If the index resets the tracker, it needs to have the config setting to + // work correctly. + if (!$remove_dependency) { + \Drupal::configFactory()->getEditable('search_api.settings') + ->set('default_tracker', 'default') + ->save(); + } + + // Delete the tracker's dependency. + $this->dependency->delete(); + + // Reload the index and check it's still there. + $this->reloadIndex(); + $this->assertNotNull($this->index, 'Index not removed'); + + // Make sure the dependency has been removed, one way or the other. + $dependencies = $this->index->getDependencies(); + $dependencies += array($dependency_key => array()); + $this->assertNotContains($dependency_name, $dependencies[$dependency_key], 'Tracker dependency removed from index'); + + // Depending on whether the plugin should have removed the dependency or + // not, make sure the right action was taken. + $tracker = $this->index->get('tracker'); + $tracker_config = $this->index->get('tracker_config'); + if ($remove_dependency) { + $this->assertEquals('search_api_test_dependencies', $tracker, 'Tracker not reset'); + $this->assertEmpty($tracker_config, 'Tracker settings adapted'); + } + else { + $this->assertEquals('default', $tracker, 'Tracker was reset'); + $this->assertEmpty($tracker_config, 'Tracker settings were cleared'); + } + } + + /** + * Data provider for this class's test methods. + * + * @return array + * An array of argument arrays for this class's test methods. + */ + public function dependencyTestDataProvider() { + return array( + array(TRUE), + array(FALSE), + ); + } + + /** + * Reloads the index with the latest copy from storage. + */ + protected function reloadIndex() { + $storage = \Drupal::entityTypeManager()->getStorage('search_api_index'); + $storage->resetCache(); + $this->index = $storage->load($this->index->id()); + } + +}