diff --git a/src/Plugin/views/argument/SearchApiArgument.php b/src/Plugin/views/argument/SearchApiArgument.php index f982a56..9f288fe 100644 --- a/src/Plugin/views/argument/SearchApiArgument.php +++ b/src/Plugin/views/argument/SearchApiArgument.php @@ -9,6 +9,7 @@ namespace Drupal\search_api\Plugin\views\argument; use Drupal\Component\Utility\Unicode; use Drupal\Core\Form\FormStateInterface; +use Drupal\search_api\UncacheableDependencyTrait; use Drupal\views\Plugin\views\argument\ArgumentPluginBase; /** @@ -20,6 +21,8 @@ use Drupal\views\Plugin\views\argument\ArgumentPluginBase; */ class SearchApiArgument extends ArgumentPluginBase { + use UncacheableDependencyTrait; + /** * The Views query object used by this contextual filter. * diff --git a/src/Plugin/views/argument/SearchApiDate.php b/src/Plugin/views/argument/SearchApiDate.php index 9978eec..c94a225 100644 --- a/src/Plugin/views/argument/SearchApiDate.php +++ b/src/Plugin/views/argument/SearchApiDate.php @@ -9,6 +9,7 @@ namespace Drupal\search_api\Plugin\views\argument; use Drupal\Component\Render\FormattableMarkup; use Drupal\Component\Utility\Html; +use Drupal\search_api\UncacheableDependencyTrait; /** * Defines a contextual filter for conditions on date fields. @@ -19,6 +20,8 @@ use Drupal\Component\Utility\Html; */ class SearchApiDate extends SearchApiArgument { + use UncacheableDependencyTrait; + /** * {@inheritdoc} */ diff --git a/src/Plugin/views/argument/SearchApiFulltext.php b/src/Plugin/views/argument/SearchApiFulltext.php index 44b939c..55fc156 100644 --- a/src/Plugin/views/argument/SearchApiFulltext.php +++ b/src/Plugin/views/argument/SearchApiFulltext.php @@ -10,6 +10,7 @@ namespace Drupal\search_api\Plugin\views\argument; use Drupal\Component\Utility\Html; use Drupal\Core\Form\FormStateInterface; use Drupal\search_api\Plugin\views\query\SearchApiQuery; +use Drupal\search_api\UncacheableDependencyTrait; /** * Defines a contextual filter for doing fulltext searches. @@ -20,6 +21,8 @@ use Drupal\search_api\Plugin\views\query\SearchApiQuery; */ class SearchApiFulltext extends SearchApiArgument { + use UncacheableDependencyTrait; + /** * {@inheritdoc} */ diff --git a/src/Plugin/views/argument/SearchApiMoreLikeThis.php b/src/Plugin/views/argument/SearchApiMoreLikeThis.php index f39c8b0..3829337 100644 --- a/src/Plugin/views/argument/SearchApiMoreLikeThis.php +++ b/src/Plugin/views/argument/SearchApiMoreLikeThis.php @@ -10,6 +10,7 @@ namespace Drupal\search_api\Plugin\views\argument; use Drupal\Core\Form\FormStateInterface; use Drupal\search_api\Entity\Index; use Drupal\search_api\SearchApiException; +use Drupal\search_api\UncacheableDependencyTrait; /** * Defines a contextual filter for displaying a "More Like This" list. @@ -20,6 +21,8 @@ use Drupal\search_api\SearchApiException; */ class SearchApiMoreLikeThis extends SearchApiArgument { + use UncacheableDependencyTrait; + /** * {@inheritdoc} */ diff --git a/src/Plugin/views/argument/SearchApiTaxonomyTerm.php b/src/Plugin/views/argument/SearchApiTaxonomyTerm.php index e840c29..1ffcc15 100644 --- a/src/Plugin/views/argument/SearchApiTaxonomyTerm.php +++ b/src/Plugin/views/argument/SearchApiTaxonomyTerm.php @@ -8,6 +8,7 @@ namespace Drupal\search_api\Plugin\views\argument; use Drupal\Component\Utility\Html; +use Drupal\search_api\UncacheableDependencyTrait; use Drupal\taxonomy\Entity\Term; /** @@ -20,6 +21,8 @@ use Drupal\taxonomy\Entity\Term; // @todo This seems to be only partially ported to D8. class SearchApiTaxonomyTerm extends SearchApiArgument { + use UncacheableDependencyTrait; + /** * {@inheritdoc} */ diff --git a/src/Plugin/views/cache/SearchApiCache.php b/src/Plugin/views/cache/SearchApiCache.php index a9af6fd..60051cf 100644 --- a/src/Plugin/views/cache/SearchApiCache.php +++ b/src/Plugin/views/cache/SearchApiCache.php @@ -22,7 +22,6 @@ use Drupal\views\Plugin\views\cache\Time; * help = @Translation("Cache Search API views. (Other methods probably won't work with search views.)") * ) */ -// @todo Limit to Search API base tables. class SearchApiCache extends Time { /** @@ -97,19 +96,15 @@ class SearchApiCache extends Time { if (!isset($this->resultsKey)) { $query = $this->getQuery()->getSearchApiQuery(); $query->preExecute(); - $user = \Drupal::currentUser(); - $key_data = array( - 'query' => $query, - 'roles' => $user->getRoles(), - 'super-user' => $user->id() == 1, // special caching for super user. - 'langcode' => \Drupal::languageManager()->getCurrentLanguage()->getId(), - 'base_url' => $GLOBALS['base_url'], - ); - foreach (array('exposed_info', 'page', 'sort', 'order', 'items_per_page', 'offset') as $key) { - if ($this->view->getRequest()->query->has($key)) { - $key_data[$key] = $this->view->getRequest()->query->get($key); - } - } + + $build_info = $this->view->build_info; + + $key_data = ['build_info' => $build_info]; + $key_data += \Drupal::service('cache_contexts_manager') + ->convertTokensToKeys($this->displayHandler + ->getCacheMetadata() + ->getCacheContexts()) + ->getKeys(); $this->resultsKey = $this->view->storage->id() . ':' . $this->displayHandler->display['id'] . ':results:' . hash('sha256', serialize($key_data)); } diff --git a/src/Plugin/views/field/SearchApiExcerpt.php b/src/Plugin/views/field/SearchApiExcerpt.php index 83528dc..8a78746 100644 --- a/src/Plugin/views/field/SearchApiExcerpt.php +++ b/src/Plugin/views/field/SearchApiExcerpt.php @@ -7,6 +7,7 @@ namespace Drupal\search_api\Plugin\views\field; +use Drupal\search_api\UncacheableDependencyTrait; use Drupal\views\Plugin\views\field\FieldPluginBase; use Drupal\views\ResultRow; @@ -19,6 +20,8 @@ use Drupal\views\ResultRow; */ class SearchApiExcerpt extends FieldPluginBase { + use UncacheableDependencyTrait; + /** * {@inheritdoc} */ diff --git a/src/Plugin/views/filter/SearchApiFilterBoolean.php b/src/Plugin/views/filter/SearchApiFilterBoolean.php index a0f1c45..b849a26 100644 --- a/src/Plugin/views/filter/SearchApiFilterBoolean.php +++ b/src/Plugin/views/filter/SearchApiFilterBoolean.php @@ -7,6 +7,7 @@ namespace Drupal\search_api\Plugin\views\filter; +use Drupal\search_api\UncacheableDependencyTrait; use Drupal\views\Plugin\views\filter\BooleanOperator; /** @@ -18,6 +19,7 @@ use Drupal\views\Plugin\views\filter\BooleanOperator; */ class SearchApiFilterBoolean extends BooleanOperator { + use UncacheableDependencyTrait; use SearchApiFilterTrait; } diff --git a/src/Plugin/views/filter/SearchApiFilterDatasource.php b/src/Plugin/views/filter/SearchApiFilterDatasource.php index 9ed5382..56bbbd5 100644 --- a/src/Plugin/views/filter/SearchApiFilterDatasource.php +++ b/src/Plugin/views/filter/SearchApiFilterDatasource.php @@ -7,6 +7,8 @@ namespace Drupal\search_api\Plugin\views\filter; +use Drupal\search_api\UncacheableDependencyTrait; + /** * Provides filtering on the datasource. * @@ -16,6 +18,8 @@ namespace Drupal\search_api\Plugin\views\filter; */ class SearchApiFilterDatasource extends SearchApiFilterOptions { + use UncacheableDependencyTrait; + /** * {@inheritdoc} */ diff --git a/src/Plugin/views/filter/SearchApiFilterDate.php b/src/Plugin/views/filter/SearchApiFilterDate.php index d684c77..349f4e5 100644 --- a/src/Plugin/views/filter/SearchApiFilterDate.php +++ b/src/Plugin/views/filter/SearchApiFilterDate.php @@ -7,6 +7,7 @@ namespace Drupal\search_api\Plugin\views\filter; +use Drupal\search_api\UncacheableDependencyTrait; use Drupal\views\Plugin\views\filter\Date; /** @@ -18,6 +19,7 @@ use Drupal\views\Plugin\views\filter\Date; */ class SearchApiFilterDate extends Date { + use UncacheableDependencyTrait; use SearchApiFilterTrait; /** diff --git a/src/Plugin/views/filter/SearchApiFilterEntityBase.php b/src/Plugin/views/filter/SearchApiFilterEntityBase.php index 928041e..c4574a2 100644 --- a/src/Plugin/views/filter/SearchApiFilterEntityBase.php +++ b/src/Plugin/views/filter/SearchApiFilterEntityBase.php @@ -9,12 +9,15 @@ namespace Drupal\search_api\Plugin\views\filter; use Drupal\Component\Utility\Tags; use Drupal\Core\Form\FormStateInterface; +use Drupal\search_api\UncacheableDependencyTrait; /** * Provides a base class for filters on entity-typed fields. */ abstract class SearchApiFilterEntityBase extends SearchApiFilterString { + use UncacheableDependencyTrait; + /** * Where the $query object will reside: * diff --git a/src/Plugin/views/filter/SearchApiFilterNumeric.php b/src/Plugin/views/filter/SearchApiFilterNumeric.php index c6b5f6f..ff2e5fa 100644 --- a/src/Plugin/views/filter/SearchApiFilterNumeric.php +++ b/src/Plugin/views/filter/SearchApiFilterNumeric.php @@ -7,6 +7,7 @@ namespace Drupal\search_api\Plugin\views\filter; +use Drupal\search_api\UncacheableDependencyTrait; use Drupal\views\Plugin\views\filter\NumericFilter; /** @@ -18,6 +19,7 @@ use Drupal\views\Plugin\views\filter\NumericFilter; */ class SearchApiFilterNumeric extends NumericFilter { + use UncacheableDependencyTrait; use SearchApiFilterTrait; /** diff --git a/src/Plugin/views/filter/SearchApiFilterOptions.php b/src/Plugin/views/filter/SearchApiFilterOptions.php index 6235505..d095dd2 100644 --- a/src/Plugin/views/filter/SearchApiFilterOptions.php +++ b/src/Plugin/views/filter/SearchApiFilterOptions.php @@ -8,6 +8,7 @@ namespace Drupal\search_api\Plugin\views\filter; use Drupal\Core\Form\FormStateInterface; +use Drupal\search_api\UncacheableDependencyTrait; use Drupal\views\Plugin\views\filter\ManyToOne; /** @@ -19,6 +20,7 @@ use Drupal\views\Plugin\views\filter\ManyToOne; */ class SearchApiFilterOptions extends ManyToOne { + use UncacheableDependencyTrait; use SearchApiFilterTrait; /** diff --git a/src/Plugin/views/filter/SearchApiFilterString.php b/src/Plugin/views/filter/SearchApiFilterString.php index 992db5e..f6cc5f7 100644 --- a/src/Plugin/views/filter/SearchApiFilterString.php +++ b/src/Plugin/views/filter/SearchApiFilterString.php @@ -7,6 +7,8 @@ namespace Drupal\search_api\Plugin\views\filter; +use Drupal\search_api\UncacheableDependencyTrait; + /** * Defines a filter for adding conditions on string fields to the query. * @@ -21,4 +23,6 @@ namespace Drupal\search_api\Plugin\views\filter; */ class SearchApiFilterString extends SearchApiFilterNumeric { + use UncacheableDependencyTrait; + } diff --git a/src/Plugin/views/filter/SearchApiFilterText.php b/src/Plugin/views/filter/SearchApiFilterText.php index 959a9d0..21aee15 100644 --- a/src/Plugin/views/filter/SearchApiFilterText.php +++ b/src/Plugin/views/filter/SearchApiFilterText.php @@ -6,6 +6,7 @@ */ namespace Drupal\search_api\Plugin\views\filter; +use Drupal\search_api\UncacheableDependencyTrait; /** * Defines a filter for filtering on fulltext fields. @@ -16,6 +17,8 @@ namespace Drupal\search_api\Plugin\views\filter; */ class SearchApiFilterText extends SearchApiFilterString { + use UncacheableDependencyTrait; + /** * {@inheritdoc} */ diff --git a/src/Plugin/views/filter/SearchApiFulltext.php b/src/Plugin/views/filter/SearchApiFulltext.php index a777b3b..26fb7ad 100644 --- a/src/Plugin/views/filter/SearchApiFulltext.php +++ b/src/Plugin/views/filter/SearchApiFulltext.php @@ -11,6 +11,7 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; use Drupal\search_api\Entity\Index; +use Drupal\search_api\UncacheableDependencyTrait; use Drupal\views\Plugin\views\filter\FilterPluginBase; /** @@ -22,6 +23,7 @@ use Drupal\views\Plugin\views\filter\FilterPluginBase; */ class SearchApiFulltext extends FilterPluginBase { + use UncacheableDependencyTrait; use SearchApiFilterTrait; /** diff --git a/src/Plugin/views/filter/SearchApiLanguage.php b/src/Plugin/views/filter/SearchApiLanguage.php index b97031c..c4e5161 100644 --- a/src/Plugin/views/filter/SearchApiLanguage.php +++ b/src/Plugin/views/filter/SearchApiLanguage.php @@ -9,6 +9,7 @@ namespace Drupal\search_api\Plugin\views\filter; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\search_api\UncacheableDependencyTrait; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -20,6 +21,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface; */ class SearchApiLanguage extends SearchApiFilterOptions { + use UncacheableDependencyTrait; + /** * The language manager. * diff --git a/src/Plugin/views/filter/SearchApiTerm.php b/src/Plugin/views/filter/SearchApiTerm.php index b30a14c..bcc337e 100644 --- a/src/Plugin/views/filter/SearchApiTerm.php +++ b/src/Plugin/views/filter/SearchApiTerm.php @@ -7,6 +7,7 @@ namespace Drupal\search_api\Plugin\views\filter; +use Drupal\search_api\UncacheableDependencyTrait; use Drupal\taxonomy\Plugin\views\filter\TaxonomyIndexTid; /** @@ -20,6 +21,7 @@ use Drupal\taxonomy\Plugin\views\filter\TaxonomyIndexTid; */ class SearchApiTerm extends TaxonomyIndexTid { + use UncacheableDependencyTrait; use SearchApiFilterTrait; } diff --git a/src/Plugin/views/filter/SearchApiUser.php b/src/Plugin/views/filter/SearchApiUser.php index 8c77ed0..89fc575 100644 --- a/src/Plugin/views/filter/SearchApiUser.php +++ b/src/Plugin/views/filter/SearchApiUser.php @@ -7,6 +7,7 @@ namespace Drupal\search_api\Plugin\views\filter; +use Drupal\search_api\UncacheableDependencyTrait; use Drupal\user\Plugin\views\filter\Name; /** @@ -20,6 +21,7 @@ use Drupal\user\Plugin\views\filter\Name; */ class SearchApiUser extends Name { + use UncacheableDependencyTrait; use SearchApiFilterTrait; /** diff --git a/src/Plugin/views/query/SearchApiQuery.php b/src/Plugin/views/query/SearchApiQuery.php index da6e85f..2880540 100644 --- a/src/Plugin/views/query/SearchApiQuery.php +++ b/src/Plugin/views/query/SearchApiQuery.php @@ -9,6 +9,7 @@ namespace Drupal\search_api\Plugin\views\query; use Drupal\Component\Render\FormattableMarkup; use Drupal\Component\Utility\Html; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Database\Query\ConditionInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; @@ -591,6 +592,55 @@ class SearchApiQuery extends QueryPluginBase { return $dependencies; } + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + // Search API gets its search results from a data source that is external to + // Drupal. Therefore it is impossible to guarantee that the search results + // are in sync with the data managed by Drupal. Consequently, it is not + // possible to cache the search results at all. + // Unless the search backend plugin supports the 'cacheable_dependency' + // feature, which means it not only implements BackendInterface, but also + // CacheableDependencyInterface, which is capable of conveying how long its + // search results can be cached. + // @todo What's in the if-statement below is just an outline, it's *one* + // possible implementation. The name of the feature is not final, nor + // is the requirement that the backend implement + // CacheableDependencyInterface. To be discussed & investigated. + $server = $this->index->getServer(); + if ($server->supportsFeature('cacheable_dependency')) { + $backend = $server->getBackend(); + assert('$backend instanceof \Drupal\Core\Cache\CacheableDependencyInterface'); + return $backend->getCacheMaxAge(); + } + else { + // When the search back-end doesn't provide the necessary metadata, we + // must assume its search results are not cacheable at all. + return 0; + } + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return CacheableMetadata::createFromObject($this->index) + ->addCacheableDependency($this->index->getServer()) + ->addCacheContexts(parent::getCacheContexts()) + ->getCacheContexts(); + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + return CacheableMetadata::createFromObject($this->index) + ->addCacheableDependency($this->index->getServer()) + ->addCacheTags(parent::getCacheTags()) + ->getCacheTags(); + } + // // Query interface methods (proxy to $this->query) // diff --git a/src/Plugin/views/row/SearchApiRow.php b/src/Plugin/views/row/SearchApiRow.php index b0ba304..0b3ae45 100644 --- a/src/Plugin/views/row/SearchApiRow.php +++ b/src/Plugin/views/row/SearchApiRow.php @@ -14,6 +14,7 @@ use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\Core\TypedData\ComplexDataInterface; use Drupal\search_api\Plugin\views\query\SearchApiQuery; use Drupal\search_api\SearchApiException; +use Drupal\search_api\UncacheableDependencyTrait; use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\Plugin\views\row\RowPluginBase; use Drupal\views\ViewExecutable; @@ -30,6 +31,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface; */ class SearchApiRow extends RowPluginBase { + use UncacheableDependencyTrait; + /** * The search index. * diff --git a/src/Plugin/views/sort/SearchApiSort.php b/src/Plugin/views/sort/SearchApiSort.php index 3214517..ba520e4 100644 --- a/src/Plugin/views/sort/SearchApiSort.php +++ b/src/Plugin/views/sort/SearchApiSort.php @@ -7,6 +7,7 @@ namespace Drupal\search_api\Plugin\views\sort; +use Drupal\search_api\UncacheableDependencyTrait; use Drupal\views\Plugin\views\sort\SortPluginBase; /** @@ -16,6 +17,8 @@ use Drupal\views\Plugin\views\sort\SortPluginBase; */ class SearchApiSort extends SortPluginBase { + use UncacheableDependencyTrait; + /** * The associated views query object. * diff --git a/src/Tests/CacheabilityTest.php b/src/Tests/CacheabilityTest.php new file mode 100644 index 0000000..dd336b7 --- /dev/null +++ b/src/Tests/CacheabilityTest.php @@ -0,0 +1,95 @@ +getTestServer(); + $this->getTestIndex(); + + // Setup example structure and content and populate the test index with that + // content. + $this->setUpExampleStructure(); + $this->insertExampleContent(); + \Drupal::getContainer() + ->get('search_api.index_task_manager') + ->addItemsAll(Index::load($this->indexId)); + $this->indexItems($this->indexId); + } + + /** + * Tests the cacheability settings of Search API. + */ + public function testFramework() { + $this->drupalLogin($this->adminUser); + + $index_cachetag = 'config:search_api.index.database_search_index'; + $server_cachetag = 'config:search_api.server.database_search_server'; + $view_cachetag = 'config:views.view.search_api_test_view'; + $expected_tags = $index_cachetag . ' ' . $server_cachetag . ' ' . $view_cachetag . ' rendered'; + + // Test the cacheability headers of a backend that doesn't support cacheing. + $this->drupalGet('search-api-test-fulltext'); + $this->assertResponse(200); + $this->assertHeader('x-drupal-dynamic-cache', 'UNCACHEABLE'); + $this->assertHeader('x-drupal-cache-tags', $expected_tags); + $this->assertHeader('cache-control', 'must-revalidate, no-cache, post-check=0, pre-check=0, private'); + + // Test the cacheability headers of a backend that supports cacheing. + \Drupal::state()->set('search_api_test_backend.feature.cacheable_dependency', 100); + + $this->drupalGet('search-api-test-fulltext'); + $this->assertResponse(200); + $this->assertHeader('x-drupal-dynamic-cache', 'UNCACHEABLE'); + $this->assertHeader('x-drupal-cache-tags', $expected_tags); + $this->assertHeader('cache-control', 'must-revalidate, no-cache, post-check=0, pre-check=0, private'); + + $this->drupalGet('search-api-test-fulltext'); + $this->assertResponse(200); + $this->assertHeader('x-drupal-dynamic-cache', 'UNCACHEABLE'); + $this->assertHeader('x-drupal-cache-tags', $expected_tags); + $this->assertHeader('cache-control', 'must-revalidate, no-cache, post-check=0, pre-check=0, private'); + } + +} diff --git a/src/UncacheableDependencyTrait.php b/src/UncacheableDependencyTrait.php new file mode 100644 index 0000000..c11e933 --- /dev/null +++ b/src/UncacheableDependencyTrait.php @@ -0,0 +1,58 @@ +get('search_api_test_backend.feature.cacheable_dependency', FALSE) !== FALSE) { + return TRUE; + } + if ($feature === 'search_api_mlt') { + return TRUE; + } + + // By default, we should return false. + return FALSE; } /** @@ -192,6 +200,10 @@ class TestBackend extends BackendPluginBase { return $remove; } + public function getCacheMaxAge() { + return \Drupal::state()->get('search_api_test_backend.feature.cacheable_dependency', 0); + } + /** * Throws an exception if set in the Drupal state for the given method. *