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 e9a5057..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,24 +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); + $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; } /** @@ -1368,21 +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]; + } + } + } + } + } + + $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]); + } } } + // 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; + } + } + } + + 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 f3e083a..1f7c0a0 100644 --- a/src/Entity/Server.php +++ b/src/Entity/Server.php @@ -441,11 +441,37 @@ class Server extends ConfigEntityBase implements ServerInterface { parent::calculateDependencies(); // Add the backend's dependencies. - if ($this->hasValidBackend() && ($backend = $this->getBackend())) { - $this->addDependencies($backend->calculateDependencies()); + if ($this->hasValidBackend()) { + $this->calculatePluginDependencies($this->getBackend()); + } + + 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 $this->dependencies; + return $changed; } /** diff --git a/src/Plugin/ConfigurablePluginBase.php b/src/Plugin/ConfigurablePluginBase.php index d3edd25..9c8f78d 100644 --- a/src/Plugin/ConfigurablePluginBase.php +++ b/src/Plugin/ConfigurablePluginBase.php @@ -100,8 +100,16 @@ abstract class ConfigurablePluginBase extends PluginBase implements Configurable * {@inheritdoc} */ public function calculateDependencies() { - $this->addDependency('module', $this->getPluginDefinition()['provider']); - return $this->dependencies; + return array(); + } + + /** + * {@inheritdoc} + */ + 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 e937f42..67227bd 100644 --- a/src/Plugin/ConfigurablePluginInterface.php +++ b/src/Plugin/ConfigurablePluginInterface.php @@ -35,4 +35,23 @@ interface ConfigurablePluginInterface extends PluginInspectionInterface, Derivat */ public function getDescription(); + /** + * Informs the plugin that some of its dependencies are being removed. + * + * 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 + * 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(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/HooksTest.php b/src/Tests/HooksTest.php index b9aa86c..348549c 100644 --- a/src/Tests/HooksTest.php +++ b/src/Tests/HooksTest.php @@ -20,13 +20,6 @@ class HooksTest extends WebTestBase { public static $modules = array('node', 'search_api', 'search_api_test_backend', 'search_api_test_views', 'search_api_test_hooks'); /** - * The id of the index. - * - * @var string - */ - protected $indexId; - - /** * {@inheritdoc} */ public function setUp() { @@ -45,8 +38,7 @@ class HooksTest extends WebTestBase { // Create an index and server to work with. $this->getTestServer(); - $index = $this->getTestIndex(); - $this->indexId = $index->id(); + $this->getTestIndex(); // Log in, so we can test all the things. $this->drupalLogin($this->adminUser); @@ -103,21 +95,4 @@ class HooksTest extends WebTestBase { $this->assertText('Llama'); } - /** - * Returns the system path for the test index. - * - * @param string|null $tab - * (optional) If set, the path suffix for a specific index tab. - * - * @return string - * A system path. - */ - protected function getIndexPath($tab = NULL) { - $path = 'admin/config/search/search-api/index/' . $this->indexId; - if ($tab) { - $path .= "/$tab"; - } - return $path; - } - } diff --git a/src/Tests/IntegrationTest.php b/src/Tests/IntegrationTest.php index 570f5e6..cb3dd22 100644 --- a/src/Tests/IntegrationTest.php +++ b/src/Tests/IntegrationTest.php @@ -28,13 +28,6 @@ class IntegrationTest extends WebTestBase { protected $serverId; /** - * The ID of the search index used for this test. - * - * @var string - */ - protected $indexId; - - /** * A storage instance for indexes. * * @var \Drupal\Core\Entity\EntityStorageInterface @@ -852,23 +845,6 @@ class IntegrationTest extends WebTestBase { } /** - * Returns the system path for the test index. - * - * @param string|null $tab - * (optional) If set, the path suffix for a specific index tab. - * - * @return string - * A system path. - */ - protected function getIndexPath($tab = NULL) { - $path = 'admin/config/search/search-api/index/' . $this->indexId; - if ($tab) { - $path .= "/$tab"; - } - return $path; - } - - /** * Retrieves test index. * * @return \Drupal\search_api\IndexInterface diff --git a/src/Tests/Processor/ProcessorIntegrationTest.php b/src/Tests/Processor/ProcessorIntegrationTest.php index 912b715..ad617c8 100644 --- a/src/Tests/Processor/ProcessorIntegrationTest.php +++ b/src/Tests/Processor/ProcessorIntegrationTest.php @@ -21,13 +21,6 @@ use Drupal\search_api\Tests\WebTestBase; class ProcessorIntegrationTest extends WebTestBase { /** - * The ID of the search index used by this test. - * - * @var string - */ - protected $indexId; - - /** * {@inheritdoc} */ public function setUp() { diff --git a/src/Tests/WebTestBase.php b/src/Tests/WebTestBase.php index f19efd0..a2d7734 100644 --- a/src/Tests/WebTestBase.php +++ b/src/Tests/WebTestBase.php @@ -32,6 +32,15 @@ abstract class WebTestBase extends SimpletestWebTestBase { * @var \Drupal\Core\Session\AccountInterface */ protected $adminUser; + /** + * The permissions of the admin user. + * + * @var string[] + */ + protected $adminUserPermissions = array( + 'administer search_api', + 'access administration pages' + ); /** * A user without Search API admin permission. @@ -55,13 +64,20 @@ abstract class WebTestBase extends SimpletestWebTestBase { protected $urlGenerator; /** + * The ID of the search index used for this test. + * + * @var string + */ + protected $indexId; + + /** * {@inheritdoc} */ public function setUp() { parent::setUp(); // Create the users used for the tests. - $this->adminUser = $this->drupalCreateUser(array('administer search_api', 'access administration pages')); + $this->adminUser = $this->drupalCreateUser($this->adminUserPermissions); $this->unauthorizedUser = $this->drupalCreateUser(array('access administration pages')); $this->anonymousUser = $this->drupalCreateUser(); @@ -101,6 +117,7 @@ abstract class WebTestBase extends SimpletestWebTestBase { */ public function getTestServer($name = 'WebTest server', $id = 'webtest_server', $backend_id = 'search_api_test_backend', $backend_config = array(), $reset = FALSE) { if ($reset) { + /** @var \Drupal\search_api\ServerInterface $server */ $server = Server::load($id); if ($server) { $server->delete(); @@ -140,6 +157,7 @@ abstract class WebTestBase extends SimpletestWebTestBase { */ public function getTestIndex($name = 'WebTest Index', $id = 'webtest_index', $server_id = 'webtest_server', $datasource_id = 'entity:node', $reset = FALSE) { if ($reset) { + /** @var \Drupal\search_api\IndexInterface $index */ $index = Index::load($id); if ($index) { $index->delete(); @@ -154,9 +172,27 @@ abstract class WebTestBase extends SimpletestWebTestBase { 'datasources' => array($datasource_id), )); $index->save(); + $this->indexId = $index->id(); } return $index; } + /** + * Returns the system path for the test index. + * + * @param string|null $tab + * (optional) If set, the path suffix for a specific index tab. + * + * @return string + * A system path. + */ + protected function getIndexPath($tab = NULL) { + $path = 'admin/config/search/search-api/index/' . $this->indexId; + if ($tab) { + $path .= "/$tab"; + } + return $path; + } + } 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_dependencies/search_api_test_dependencies.info.yml b/tests/search_api_test_dependencies/search_api_test_dependencies.info.yml new file mode 100644 index 0000000..db94294 --- /dev/null +++ b/tests/search_api_test_dependencies/search_api_test_dependencies.info.yml @@ -0,0 +1,8 @@ +name: 'Search API Test Dependencies' +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_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/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()); + } + +}