Contributed modules

This commit is contained in:
Robert 2022-07-18 17:40:18 +02:00
parent 6286c6b165
commit e822299d2f
222 changed files with 4660 additions and 821 deletions

View File

@ -16,7 +16,7 @@
},
"license": "GPL-2.0+",
"require-dev": {
"drupal/search_api": "~1.21",
"drupal/search_api": "^1.24||1.x-dev",
"drupal/jquery_ui_slider": "~1.1",
"drupal/jquery_ui_touch_punch": "~1.0"
},

View File

@ -14,6 +14,12 @@ facets.facet.*:
min_count:
type: integer
label: 'Minimum count'
missing:
type: boolean
label: 'Missing'
missing_label:
type: label
label: 'Missing label'
url_alias:
type: label
label: 'Name of facet as used in the URL'

View File

@ -1,14 +1,14 @@
name: 'Facets'
type: module
description: 'Faceted search interfaces that can be used on Search API searchers.'
core_version_requirement: ^9.2 || ^10.0
core_version_requirement: ^9.3 || ^10.0
package: Search
configure: entity.facets_facet.collection
test_dependencies:
- search_api:search_api
- drupal:views
# Information added by Drupal.org packaging script on 2022-04-04
version: '2.0.2'
# Information added by Drupal.org packaging script on 2022-07-09
version: '2.0.4'
project: 'facets'
datestamp: 1649070272
datestamp: 1657367472

View File

@ -8,6 +8,7 @@
use Drupal\facets\Entity\Facet;
use Drupal\facets\Entity\FacetSource;
use Drupal\block\Entity\Block;
use Drupal\facets\Plugin\facets\facet_source\SearchApiDisplay;
/**
* Implements hook_update_dependencies().
@ -211,6 +212,41 @@ function facets_update_8008() {
* Resave facets for consistent configuration export.
*/
function facets_update_8009() {
// Moved to facets_update_8011().
}
/**
* Enable facet block caching for the views with "Search API tag or time" cache.
*/
function facets_update_8010() {
$facet_storage = \Drupal::entityTypeManager()->getStorage('facets_facet');
$processed_views = [];
/** @var \Drupal\facets\FacetInterface $facet */
foreach ($facet_storage->loadMultiple() as $facet) {
if (
($source = $facet->getFacetSource())
&& $source instanceof SearchApiDisplay
&& ($view_executable = $source->getViewsDisplay())
&& !in_array($view_executable->id(), $processed_views)
&& ($cache_plugin = $view_executable->getDisplay()->getPlugin('cache'))
&& in_array(
$cache_plugin->getPluginId(),
['search_api_tag', 'search_api_time']
)
) {
$view_executable->save();
$processed_views[] = $view_executable->id();
}
}
return !empty($processed_views)
? sprintf('Facet caching was enabled for the following views: %s.', implode(', ', $processed_views))
: 'There are no views with search API cache plugins and facets in the same time, so nothing has been updated.';
}
/**
* Resave facets for consistent configuration export.
*/
function facets_update_8011() {
$facets = Facet::loadMultiple();
foreach ($facets as $facet) {
$facet->save();

View File

@ -10,10 +10,12 @@ use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Drupal\facets\Entity\Facet;
use Drupal\facets\Entity\FacetSource;
use Drupal\facets\FacetInterface;
use Drupal\facets\FacetSource\SearchApiFacetSourceInterface;
use Drupal\views\Entity\View;
use Drupal\Core\Entity\EntityInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
@ -202,8 +204,18 @@ function facets_entity_predelete(EntityInterface $entity) {
* @see https://www.drupal.org/node/1842756
*/
function facets_preprocess_facets_item_list(array &$variables) {
if ($variables['facet'] !== NULL && $variables['facet']->get('show_title') === TRUE) {
$variables['title'] = $variables['facet']->label();
/** @var FacetInterface $facet */
$facet = $variables['facet'];
if ($facet !== NULL) {
if ($facet->get('show_title') === TRUE) {
$variables['title'] = $facet->label();
}
if (Settings::get('facets_debug_cacheable_metadata', FALSE) && $facet->getFacetSource() instanceof SearchApiFacetSourceInterface) {
$variables['cache_hash'] = $hash = substr(base_convert(hash('sha256', uniqid(time())), 16, 36), 0, 6);
$variables['cache_contexts'] = implode(', ', $facet->getCacheContexts());
$variables['cache_tags'] = implode(', ', $facet->getCacheTags());
$variables['cache_max_age'] = $facet->getCacheMaxAge();
}
}
template_preprocess_item_list($variables);
}

View File

@ -12,7 +12,7 @@ test_dependencies:
- facets:facets
- drupal:views
# Information added by Drupal.org packaging script on 2022-04-04
version: '2.0.2'
# Information added by Drupal.org packaging script on 2022-07-09
version: '2.0.4'
project: 'facets'
datestamp: 1649070272
datestamp: 1657367472

View File

@ -12,7 +12,7 @@ test_dependencies:
- drupal:views
- drupal:rest
# Information added by Drupal.org packaging script on 2022-04-04
version: '2.0.2'
# Information added by Drupal.org packaging script on 2022-07-09
version: '2.0.4'
project: 'facets'
datestamp: 1649070272
datestamp: 1657367472

View File

@ -12,7 +12,7 @@ dependencies:
- search_api:search_api_test_db
- facets:facets_rest
# Information added by Drupal.org packaging script on 2022-04-04
version: '2.0.2'
# Information added by Drupal.org packaging script on 2022-07-09
version: '2.0.4'
project: 'facets'
datestamp: 1649070272
datestamp: 1657367472

View File

@ -10,7 +10,7 @@ test_dependencies:
- facets:facets
- drupal:views
# Information added by Drupal.org packaging script on 2022-04-04
version: '2.0.2'
# Information added by Drupal.org packaging script on 2022-07-09
version: '2.0.4'
project: 'facets'
datestamp: 1649070272
datestamp: 1657367472

View File

@ -10,7 +10,7 @@ test_dependencies:
- search_api:search_api
- drupal:views
# Information added by Drupal.org packaging script on 2022-04-04
version: '2.0.2'
# Information added by Drupal.org packaging script on 2022-07-09
version: '2.0.4'
project: 'facets'
datestamp: 1649070272
datestamp: 1657367472

View File

@ -292,9 +292,7 @@ class FacetsSummaryForm extends EntityForm {
foreach ($processors_by_stage as $stage => $processors) {
/** @var \Drupal\facets\Processor\ProcessorInterface $processor */
foreach ($processors as $processor_id => $processor) {
$weight = isset($processor_settings[$processor_id]['weights'][$stage])
? $processor_settings[$processor_id]['weights'][$stage]
: $processor->getDefaultWeight($stage);
$weight = $processor_settings[$processor_id]['weights'][$stage] ?? $processor->getDefaultWeight($stage);
if ($processor->isHidden()) {
$form['processors'][$processor_id]['weights'][$stage] = [
'#type' => 'value',

View File

@ -234,7 +234,7 @@ class FacetsSummarySettingsForm extends EntityForm {
// Clear Drupal cache for blocks to reflect recent changes.
$this->blockManager->clearCachedDefinitions();
$facet_source_id = $form_state->getValue('facet_source_id');
list($type,) = explode(':', $facet_source_id);
[$type] = explode(':', $facet_source_id);
if ($type !== 'search_api') {
return $facets_summary;
}

View File

@ -44,7 +44,7 @@ class FacetsSummaryBlockDeriver implements ContainerDeriverInterface {
*/
public function getDerivativeDefinition($derivative_id, $base_plugin_definition) {
$derivatives = $this->getDerivativeDefinitions($base_plugin_definition);
return isset($derivatives[$derivative_id]) ? $derivatives[$derivative_id] : NULL;
return $derivatives[$derivative_id] ?? NULL;
}
/**

View File

@ -51,9 +51,7 @@ class ResetFacetsProcessor extends ProcessorPluginBase implements BuildProcessor
}
$request_stack = \Drupal::requestStack();
// Support 9.3+.
// @todo remove switch after 9.3 or greater is required.
$request = version_compare(\Drupal::VERSION, '9.3', '>=') ? $request_stack->getMainRequest() : $request_stack->getMasterRequest();
$request = $request_stack->getMainRequest();
$query_params = $request->query->all();
// Bypass all active facets and remove them from the query parameters array.

View File

@ -66,7 +66,7 @@ class ProcessorPluginBase extends PluginBase implements ProcessorInterface {
*/
public function getDescription() {
$plugin_definition = $this->getPluginDefinition();
return isset($plugin_definition['description']) ? $plugin_definition['description'] : '';
return $plugin_definition['description'] ?? '';
}
/**

View File

@ -249,6 +249,7 @@ class IntegrationTest extends FacetsTestBase {
'id' => $id,
'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1',
];
$this->drupalGet('admin/config/search/facets/add-facet-summary');
$this->submitForm($values, 'Save');
$this->assertSession()->pageTextContains('Caching of view Search API Test Fulltext search view has been disabled.');

View File

@ -127,15 +127,8 @@ class FacetBlockAjaxController extends ControllerBase {
$facets_blocks = array_unique($facets_blocks);
$new_request = Request::create($path);
// Support 9.3+.
// @todo remove after 9.3 or greater is required.
if (class_exists(DrupalRequestStack::class)) {
$request_stack = new DrupalRequestStack();
}
// Legacy request stack.
else {
$request_stack = new SymfonyRequestStack();
}
$request_stack = new DrupalRequestStack();
$processed = $this->pathProcessor->processInbound($path, $new_request);
$processed_request = Request::create($processed);

View File

@ -4,6 +4,9 @@ namespace Drupal\facets\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\facets\Event\GetFacetCacheContexts;
use Drupal\facets\Event\GetFacetCacheMaxAge;
use Drupal\facets\Event\GetFacetCacheTags;
use Drupal\facets\Exception\Exception;
use Drupal\facets\Exception\InvalidProcessorException;
use Drupal\facets\Exception\InvalidQueryTypeException;
@ -41,6 +44,8 @@ use Drupal\facets\FacetInterface;
* "url_alias",
* "weight",
* "min_count",
* "missing",
* "missing_label",
* "show_only_one_result",
* "field_identifier",
* "facet_source_id",
@ -56,7 +61,7 @@ use Drupal\facets\FacetInterface;
* "only_visible_when_facet_source_is_visible",
* "processor_configs",
* "empty_behavior",
* "show_title"
* "show_title",
* },
* links = {
* "collection" = "/admin/config/search/facets",
@ -317,6 +322,24 @@ class Facet extends ConfigEntityBase implements FacetInterface {
*/
protected $min_count = 1;
protected $cache_dependencies_calculated = FALSE;
/**
* The missing parameter.
*
* @var bool
* The missing parameter.
*/
protected $missing = FALSE;
/**
* The missing parameter label.
*
* @var string
* The missing parameter label.
*/
protected $missing_label = 'others';
/**
* Returns the widget plugin manager.
*
@ -423,7 +446,7 @@ class Facet extends ConfigEntityBase implements FacetInterface {
* The loaded processors, keyed by processor ID.
*/
protected function loadProcessors() {
if (is_array($this->processors)) {
if (isset($this->processors) && is_array($this->processors)) {
return $this->processors;
}
@ -721,7 +744,7 @@ class Facet extends ConfigEntityBase implements FacetInterface {
* {@inheritdoc}
*/
public function getFacetSource() {
if (is_null($this->facet_source_instance) && $this->facet_source_id) {
if (!isset($this->facet_source_instance) && $this->facet_source_id) {
/** @var \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_plugin_manager */
$facet_source_plugin_manager = \Drupal::service('plugin.manager.facets.facet_source');
if (!$facet_source_plugin_manager->hasDefinition($this->facet_source_id)) {
@ -758,13 +781,13 @@ class Facet extends ConfigEntityBase implements FacetInterface {
}
$storage = \Drupal::entityTypeManager()->getStorage('facets_facet_source');
$source_id = str_replace(':', '__', $this->facet_source_id);
// Load and return the facet source config object from the storage.
$facet_source = $storage->load($source_id);
if ($facet_source instanceof FacetSource) {
$this->facetSourceConfig = $facet_source;
return $this->facetSourceConfig;
if ($source_id = str_replace(':', '__', $this->facet_source_id ?? '')) {
// Load and return the facet source config object from the storage.
$facet_source = $storage->load($source_id);
if ($facet_source instanceof FacetSource) {
$this->facetSourceConfig = $facet_source;
return $this->facetSourceConfig;
}
}
// We didn't have a facet source config entity yet for this facet source
@ -821,6 +844,14 @@ class Facet extends ConfigEntityBase implements FacetInterface {
if (in_array($result->getRawValue(), $this->active_values)) {
$result->setActiveState(TRUE);
}
elseif ($result->isMissing()) {
foreach ($this->active_values as $active_value) {
if (str_starts_with($active_value, '!(')) {
$result->setActiveState(TRUE);
break;
}
}
}
}
}
}
@ -849,7 +880,7 @@ class Facet extends ConfigEntityBase implements FacetInterface {
foreach ($facet_source_plugin_manager->getDefinitions() as $name => $facet_source_definition) {
if (class_exists($facet_source_definition['class']) && empty($this->facetSourcePlugins[$name])) {
// Create our settings for this facet source..
$config = isset($this->facetSourcePlugins[$name]) ? $this->facetSourcePlugins[$name] : [];
$config = $this->facetSourcePlugins[$name] ?? [];
/** @var \Drupal\facets\FacetSource\FacetSourcePluginInterface $facet_source */
$facet_source = $facet_source_plugin_manager->createInstance($name, $config);
@ -881,7 +912,7 @@ class Facet extends ConfigEntityBase implements FacetInterface {
// Filter processors by status if required. Enabled processors are those
// which have settings in the processor_configs.
if ($only_enabled) {
if ($processors && $only_enabled) {
$processors_settings = $this->getProcessorConfigs();
$processors = array_intersect_key($processors, $processors_settings);
}
@ -973,8 +1004,10 @@ class Facet extends ConfigEntityBase implements FacetInterface {
'weights' => $processor['weights'],
'settings' => $processor['settings'],
];
// Sort the processors so we won't have unnecessary changes.
// Sort the processors, so we won't have unnecessary changes.
ksort($this->processor_configs);
$this->cache_dependencies_calculated = FALSE;
}
/**
@ -983,6 +1016,8 @@ class Facet extends ConfigEntityBase implements FacetInterface {
public function removeProcessor($processor_id) {
unset($this->processor_configs[$processor_id]);
unset($this->processors[$processor_id]);
$this->cache_dependencies_calculated = FALSE;
}
/**
@ -1027,6 +1062,34 @@ class Facet extends ConfigEntityBase implements FacetInterface {
return $this->min_count;
}
/**
* {@inheritdoc}
*/
public function setMissing(bool $missing) {
$this->missing = $missing;
}
/**
* {@inheritdoc}
*/
public function isMissing(): bool {
return $this->missing;
}
/**
* {@inheritdoc}
*/
public function setMissingLabel(string $label) {
$this->missing_label = $label;
}
/**
* {@inheritdoc}
*/
public function getMissingLabel(): string {
return $this->missing_label;
}
/**
* {@inheritdoc}
*/
@ -1062,6 +1125,10 @@ class Facet extends ConfigEntityBase implements FacetInterface {
parent::postSave($storage, $update);
if (!$update) {
self::clearBlockCache();
// Register newly created facet within its source, for the caching.
if (($source = $this->getFacetSource()) && $source->getCacheMaxAge() !== 0) {
$source->registerFacet($this);
}
}
}
@ -1091,12 +1158,69 @@ class Facet extends ConfigEntityBase implements FacetInterface {
$container->get('plugin.manager.block')->clearCachedDefinitions();
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
$this->calculateCacheDependencies();
$eventDispatcher = \Drupal::service('event_dispatcher');
$event = new GetFacetCacheTags(parent::getCacheTags(), $this);
$eventDispatcher->dispatch($event);
$this->cacheTags = $event->getCacheTags() ?? $this->cacheTags;
return array_values($this->cacheTags);
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
$this->calculateCacheDependencies();
$eventDispatcher = \Drupal::service('event_dispatcher');
$event = new GetFacetCacheContexts(parent::getCacheContexts(), $this);
$eventDispatcher->dispatch($event);
$this->cacheContexts = $event->getCacheContexts() ?? $this->cacheContexts;
return array_values($this->cacheContexts);
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
$this->calculateCacheDependencies();
$eventDispatcher = \Drupal::service('event_dispatcher');
$event = new GetFacetCacheMaxAge(parent::getCacheMaxAge(), $this);
$eventDispatcher->dispatch($event);
$this->cacheMaxAge = $event->getCacheMaxAge() ?? $this->cacheMaxAge;
return $this->cacheMaxAge;
}
protected function calculateCacheDependencies(): void {
if (!$this->cache_dependencies_calculated) {
if ($facet_source = $this->getFacetSource()) {
$this->addCacheableDependency($facet_source);
}
foreach ($this->getProcessors() ?? [] as $processor) {
$this->addCacheableDependency($processor);
}
$this->cache_dependencies_calculated = TRUE;
}
}
/**
* Remove the facet lazy built data when the facet is serialized.
*/
public function __sleep() {
unset($this->facet_source_instance);
unset($this->processors);
return parent::__sleep();
}

View File

@ -25,4 +25,49 @@ final class FacetsEvents {
*/
public const ACTIVE_FILTERS_PARSED = ActiveFiltersParsed::class;
/**
* This event allows modules to change the facet links' URL if needed.
*
* @Event
*
* @see \Drupal\facets\Event\UrlCreated
*/
public const URL_CREATED = UrlCreated::class;
/**
* This event allows modules to modify a facet after it is built.
*
* @Event
*
* @see \Drupal\facets\Event\PostBuildFacet
*/
public const POST_BUILD_FACET = PostBuildFacet::class;
/**
* This event allows modules to change the cache contexts of a facet.
*
* @Event
*
* @see \Drupal\facets\Event\GetFacetCacheContexts
*/
public const GET_FACET_CACHE_CONTEXTS = GetFacetCacheContexts::class;
/**
* This event allows modules to change the cache max age of a facet.
*
* @Event
*
* @see \Drupal\facets\Event\GetFacetCacheMaxAge
*/
public const GET_FACET_CACHE_MAX_AGE = GetFacetCacheMaxAge::class;
/**
* This event allows modules to change the cache tags of a facet.
*
* @Event
*
* @see \Drupal\facets\Event\GetFacetCacheTags
*/
public const GET_FACET_CACHE_TAGS = GetFacetCacheTags::class;
}

View File

@ -0,0 +1,72 @@
<?php
namespace Drupal\facets\Event;
use Drupal\facets\FacetInterface;
use Drupal\Component\EventDispatcher\Event;
/**
* Implements the get cache contexts event.
*
* This event allows modules to change the cache contexts of a facet if needed.
*/
final class GetFacetCacheContexts extends Event {
/**
* The cache contexts.
*
* @var string[]
*/
private $cacheContexts;
/**
* The facet.
*
* @var \Drupal\facets\FacetInterface
*/
private $facet;
/**
* GetCacheContexts constructor.
*
* @param string[] $cacheContexts
* The cache contexts.
* @param \Drupal\facets\FacetInterface $facet
* The facet.
*/
public function __construct($cacheContexts, FacetInterface $facet) {
$this->cacheContexts = $cacheContexts;
$this->facet = $facet;
}
/**
* Get the cache contexts.
*
* @return string[]
* The cache contexts.
*/
public function getCacheContexts(): array {
return $this->cacheContexts ?? [];
}
/**
* Get the cache contexts.
*
* @param string[] $cacheContexts
* The cache contexts.
*/
public function setCacheContexts($cacheContexts): void {
$this->cacheContexts = $cacheContexts;
}
/**
* Get the facet.
*
* @return \Drupal\facets\FacetInterface
* The facet.
*/
public function getFacet() {
return $this->facet;
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace Drupal\facets\Event;
use Drupal\facets\FacetInterface;
use Drupal\Component\EventDispatcher\Event;
/**
* Implements the get cache max age event.
*
* This event allows modules to change the cache max age of a facet if needed.
*/
final class GetFacetCacheMaxAge extends Event {
/**
* The cache max age.
*
* @var int
*/
private $cacheMaxAge;
/**
* The facet.
*
* @var \Drupal\facets\FacetInterface
*/
private $facet;
/**
* GetCacheMaxAge constructor.
*
* @param int $cacheMaxAge
* The cache max age.
* @param \Drupal\facets\FacetInterface $facet
* The facet.
*/
public function __construct($cacheMaxAge, FacetInterface $facet) {
$this->cacheMaxAge = $cacheMaxAge;
$this->facet = $facet;
}
/**
* Get the cache max age.
*
* @return int
* The cache max age.
*/
public function getCacheMaxAge(): int {
return $this->cacheMaxAge ?? 0;
}
/**
* Get the cache max age.
*
* @param int $cacheMaxAge
* The cache max age.
*/
public function setCacheMaxAge($cacheMaxAge): void {
$this->cacheMaxAge = $cacheMaxAge;
}
/**
* Get the facet.
*
* @return \Drupal\facets\FacetInterface
* The facet.
*/
public function getFacet() {
return $this->facet;
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace Drupal\facets\Event;
use Drupal\facets\FacetInterface;
use Drupal\Component\EventDispatcher\Event;
/**
* Implements the get cache tags event.
*
* This event allows modules to change the cache tags of a facet if needed.
*/
final class GetFacetCacheTags extends Event {
/**
* The cache tags.
*
* @var string[]
*/
private $cacheTags;
/**
* The facet.
*
* @var \Drupal\facets\FacetInterface
*/
private $facet;
/**
* GetCacheTags constructor.
*
* @param string[] $cacheTags
* The cache tags.
* @param \Drupal\facets\FacetInterface $facet
* The facet.
*/
public function __construct($cacheTags, FacetInterface $facet) {
$this->cacheTags = $cacheTags;
$this->facet = $facet;
}
/**
* Get the cache tags.
*
* @return string[]
* The cache tags.
*/
public function getCacheTags(): array {
return $this->cacheTags ?? [];
}
/**
* Get the cache tags.
*
* @param string[] $cacheTags
* The cache tags.
*/
public function setCacheTags($cacheTags): void {
$this->cacheTags = $cacheTags;
}
/**
* Get the facet.
*
* @return \Drupal\facets\FacetInterface
* The facet.
*/
public function getFacet() {
return $this->facet;
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace Drupal\facets\Event;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\facets\FacetInterface;
use Drupal\Component\EventDispatcher\Event;
/**
* Implements the PostBuildFacet event.
*
* This event allows modules to modify a facet after it is built and before it
* will be cached and rendered.
*/
final class PostBuildFacet extends Event {
/**
* The facet.
*
* @var \Drupal\facets\FacetInterface
*/
private $facet;
/**
* PreAddFacetSourceCacheableDependencies constructor.
*
* @param \Drupal\facets\FacetInterface $facet
* The facet.
*/
public function __construct(FacetInterface $facet) {
$this->facet = $facet;
}
/**
* Get the facet.
*
* @return \Drupal\facets\FacetInterface
* The facet.
*/
public function getFacet(): FacetInterface {
return $this->facet;
}
/**
* Set the facet.
*
* @param \Drupal\facets\FacetInterface $facet
* The facet.
*
* @return void
*/
public function setFacet(FacetInterface $facet): void {
$this->facet = $facet;
}
}

View File

@ -0,0 +1,100 @@
<?php
namespace Drupal\facets\Event;
use Drupal\Core\Url;
use Drupal\facets\FacetInterface;
use Drupal\facets\Result\ResultInterface;
use Drupal\Component\EventDispatcher\Event;
/**
* Implements the url created event.
*
* This event allows modules to change the facet link's URL if needed.
*/
final class UrlCreated extends Event {
/**
* The get parameters.
*
* @var \Drupal\Core\Url
*/
private $url;
/**
* The facet result.
*
* @var \Drupal\facets\Result\ResultInterface
*/
private $facetResult;
/**
* The facet.
*
* @var \Drupal\facets\FacetInterface
*/
private $facet;
/**
* UrlCreated constructor.
*
* @param \Drupal\Core\Url $url
* The facet link URL.
* @param \Drupal\facets\Result\ResultInterface $facetResult
* The facet result.
* @param \Drupal\facets\FacetInterface $facet
* The facet.
*/
public function __construct(Url $url, ResultInterface $facetResult, FacetInterface $facet) {
$this->url = $url;
$this->facetResult = $facetResult;
$this->facet = $facet;
}
/**
* Get the URL.
*
* @return \Drupal\Core\Url
* The URL.
*/
public function getUrl(): Url {
return $this->url;
}
/**
* Set the URL.
*
* @param \Drupal\Core\Url $url
* The URL to set.
*/
public function setUrl(Url $url): void {
$this->url = $url;
}
/**
* Get the facet result.
*
* Only to be used as context, because changing this will not result in any
* changes to the final url.
*
* @return \Drupal\facets\Result\ResultInterface
* The facet result.
*/
public function getFacetResult() {
return $this->facetResult;
}
/**
* Get the facet.
*
* Only to be used as context, because changing this will not result in any
* changes to the final url.
*
* @return \Drupal\facets\FacetInterface
* The facet.
*/
public function getFacet() {
return $this->facet;
}
}

View File

@ -504,4 +504,36 @@ interface FacetInterface extends ConfigEntityInterface {
*/
public function getMinCount();
/**
* Sets the missing parameter.
*
* @param bool $missing
* Whether to show a missing item or not.
*/
public function setMissing(bool $missing);
/**
* Returns the missing parameter.
*
* @return bool
* Minimum count.
*/
public function isMissing(): bool;
/**
* Sets the missing parameter label.
*
* @param string $label
* The label.
*/
public function setMissingLabel(string $label);
/**
* Returns the missing parameter label.
*
* @return string
* The label.
*/
public function getMissingLabel(): string;
}

View File

@ -191,6 +191,7 @@ class FacetListBuilder extends DraggableListBuilder {
'#type' => 'markup',
'#markup' => 'Facet source',
],
'machine_name' => ['#markup' => $facet_source['id']],
'title' => [
'#theme_wrappers' => [
'container' => [
@ -198,9 +199,9 @@ class FacetListBuilder extends DraggableListBuilder {
],
],
'#type' => 'markup',
'#markup' => $facet_source['id'],
'#markup' => $facet_source['label'],
'#wrapper_attributes' => [
'colspan' => 3,
'colspan' => 2,
],
],
'operations' => [

View File

@ -2,8 +2,10 @@
namespace Drupal\facets\FacetManager;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\facets\Event\PostBuildFacet;
use Drupal\facets\Exception\InvalidProcessorException;
use Drupal\facets\FacetInterface;
use Drupal\facets\FacetSource\FacetSourcePluginManager;
@ -109,11 +111,13 @@ class DefaultFacetManager {
* The facet source ID to process.
*/
public function alterQuery(&$query, $facetsource_id) {
$query_is_cacheable = $query instanceof RefinableCacheableDependencyInterface;
/** @var \Drupal\facets\FacetInterface[] $facets */
$facets = $this->getFacetsByFacetSourceId($facetsource_id);
foreach ($facets as $facet) {
foreach ($facets as $facet) {
$processors = $facet->getProcessors();
if (isset($processors['dependent_processor'])) {
$conditions = $processors['dependent_processor']->getConfiguration();
@ -126,7 +130,6 @@ class DefaultFacetManager {
}
foreach ($enabled_conditions as $facet_id => $condition_settings) {
if (!isset($facets[$facet_id]) || !$processors['dependent_processor']->isConditionMet($condition_settings, $facets[$facet_id])) {
// The conditions are not met anymore, remove the active items.
$facet->setActiveItems([]);
@ -139,7 +142,12 @@ class DefaultFacetManager {
unset($active_filters[$facet->id()]);
$urlProcessor->setActiveFilters($active_filters);
}
// Add "dependend facet" cacheabillity to make sure that whenever
// its preferences will change, for instance to "negate", query
// results cache will take it to consideration.
if ($query_is_cacheable) {
$query->addCacheableDependency($facet);
}
// Don't convert this facet's active items into query conditions.
// Continue with the next facet.
continue(2);
@ -156,6 +164,10 @@ class DefaultFacetManager {
]
);
$query_type_plugin->execute();
// Merge cache medata that gathered from facet and its processors.
if ($query_is_cacheable) {
$query->addCacheableDependency($facet);
}
}
}
@ -236,6 +248,7 @@ class DefaultFacetManager {
}
$post_query_processor->postQuery($facet);
}
$this->processedFacets[$facetsource_id][$facet->id()] = $facet;
}
}
@ -326,7 +339,11 @@ class DefaultFacetManager {
$facet->setResults($results);
$this->builtFacets[$facet->getFacetSourceId()][$facet->id()] = $facet;
$eventDispatcher = \Drupal::service('event_dispatcher');
$event = new PostBuildFacet($facet);
$eventDispatcher->dispatch($event);
$this->builtFacets[$facet->getFacetSourceId()][$facet->id()] = $event->getFacet();
}
return $this->builtFacets[$facet->getFacetSourceId()][$facet->id()];
@ -338,7 +355,8 @@ class DefaultFacetManager {
* This method delegates to the relevant plugins to render a facet, it calls
* out to a widget plugin to do the actual rendering when results are found.
* When no results are found it calls out to the correct empty result plugin
* to build a render array.
* to build a render array. Renderable array will include all facet plugins
* cache metadata that were used to build this facet.
*
* Before doing any rendering, the processors that implement the
* BuildProcessorInterface enabled on this facet will run.
@ -369,7 +387,6 @@ class DefaultFacetManager {
/** @var \Drupal\facets\Widget\WidgetPluginInterface $widget */
$widget = $facet->getWidgetInstance();
$build = $widget->build($facet);
// No results behavior handling. Return a custom text or false depending on
// settings.
if (empty($facet->getResults())) {
@ -439,7 +456,7 @@ class DefaultFacetManager {
* call returnBuiltFacet() instead.
*
* @param \Drupal\facets\FacetInterface $facet
* The facet to process.
* The facet to process with a collected plugins cache metadata.
*
* @return \Drupal\facets\FacetInterface|null
* The updated facet if it exists, NULL otherwise.
@ -456,7 +473,7 @@ class DefaultFacetManager {
* The facet to process.
*
* @return \Drupal\facets\FacetInterface
* The built facet.
* The built Facet object with a collected plugins cache metadata.
*/
public function returnBuiltFacet(FacetInterface $facet) {
return $this->processBuild($facet);

View File

@ -87,7 +87,7 @@ abstract class FacetSourceDeriverBase implements ContainerDeriverInterface {
*/
public function getDerivativeDefinition($derivative_id, $base_plugin_definition) {
$derivatives = $this->getDerivativeDefinitions($base_plugin_definition);
return isset($derivatives[$derivative_id]) ? $derivatives[$derivative_id] : NULL;
return $derivatives[$derivative_id] ?? NULL;
}
/**

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\FacetSource;
use Drupal\Core\Cache\UncacheableDependencyTrait;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@ -12,6 +13,11 @@ use Drupal\facets\QueryType\QueryTypePluginManager;
/**
* Defines a base class from which other facet sources may extend.
*
* By default all plugins that will extend this class will disable facets
* caching mechanism. It is strongly recommended to turn it on by implementing
* own methods for the CacheableDependencyInterface interface and
* ::registerFacet() method.
*
* Plugins extending this class need to define a plugin definition array through
* annotation. The definition includes the following keys:
* - id: The unique, system-wide identifier of the facet source.
@ -24,6 +30,7 @@ use Drupal\facets\QueryType\QueryTypePluginManager;
* @see plugin_api
*/
abstract class FacetSourcePluginBase extends PluginBase implements FacetSourcePluginInterface, ContainerFactoryPluginInterface {
use UncacheableDependencyTrait;
/**
* The plugin manager.
@ -153,4 +160,10 @@ abstract class FacetSourcePluginBase extends PluginBase implements FacetSourcePl
$this->facet->setFieldIdentifier($field_identifier);
}
/**
* {@inheritdoc}
*/
public function registerFacet(FacetInterface $facet) {
}
}

View File

@ -3,6 +3,7 @@
namespace Drupal\facets\FacetSource;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\facets\FacetInterface;
@ -15,7 +16,7 @@ use Drupal\facets\FacetInterface;
*
* @see plugin_api
*/
interface FacetSourcePluginInterface extends PluginFormInterface, DependentPluginInterface {
interface FacetSourcePluginInterface extends PluginFormInterface, DependentPluginInterface, CacheableDependencyInterface {
/**
* Fills the facet entities with results from the facet source.
@ -115,4 +116,16 @@ interface FacetSourcePluginInterface extends PluginFormInterface, DependentPlugi
*/
public function buildFacet();
/**
* Register newly added facet within its source.
*
* Add facet cache tags and contexts into the facet source, to make sure that
* search results will change whenever facets will be updated. Usually can be
* achieved by adding facet entity as a cache dependency to a search results.
*
* @param \Drupal\facets\FacetInterface $facet
* Facet entity that being inserted.
*/
public function registerFacet(FacetInterface $facet);
}

View File

@ -426,7 +426,7 @@ class FacetForm extends EntityForm {
$form['facet_settings']['empty_behavior_container']['empty_behavior_text'] = [
'#type' => 'text_format',
'#title' => $this->t('Empty text'),
'#format' => isset($empty_behavior_config['text_format']) ? $empty_behavior_config['text_format'] : 'plain_text',
'#format' => $empty_behavior_config['text_format'] ?? 'plain_text',
'#editor' => TRUE,
'#default_value' => isset($empty_behavior_config['text_format']) ? $empty_behavior_config['text'] : '',
];
@ -465,35 +465,34 @@ class FacetForm extends EntityForm {
'#title' => $this->t('Use hierarchy'),
'#default_value' => $facet->getUseHierarchy(),
];
if (!$facet->getFacetSource() instanceof SearchApiDisplay) {
$form['facet_settings']['use_hierarchy']['#disabled'] = TRUE;
$form['facet_settings']['use_hierarchy']['#description'] = $this->t('This setting only works with Search API based facets.');
}
else {
if ($facet->getFacetSource() instanceof SearchApiDisplay) {
$processor_url = Url::fromRoute('entity.search_api_index.processors', [
'search_api_index' => $facet->getFacetSource()->getIndex()->id(),
]);
$description = $this->t('Renders the items using hierarchy. Make sure to enable the hierarchy processor on the <a href=":processor-url">Search api index processor configuration</a> for this to work as expected. If disabled all items will be flattened.', [
$description = $this->t('Renders the items using hierarchy. Depending on the selected plugin below, make sure to enable the hierarchy processor on the <a href=":processor-url">Search api index processor configuration</a> for this to work as expected. If disabled all items might be flattened.', [
':processor-url' => $processor_url->toString(),
]);
$form['facet_settings']['use_hierarchy']['#description'] = $description;
$hierarchy = $facet->getHierarchy();
$options = array_map(function (HierarchyPluginBase $plugin) {
return $plugin->getPluginDefinition()['label'];
}, $facet->getHierarchies());
$form['facet_settings']['hierarchy'] = [
'#type' => 'select',
'#title' => $this->t('Hierarchy type'),
'#options' => $options,
'#default_value' => $hierarchy ? $hierarchy['type'] : '',
'#states' => [
'visible' => [
':input[name="facet_settings[use_hierarchy]"]' => ['checked' => TRUE],
],
],
];
}
else {
$description = $this->t('Renders the items using hierarchy. Note that some of the selectable plugins below will not supports all search backends. The taxonomy plugin will only work with Search API.');
}
$form['facet_settings']['use_hierarchy']['#description'] = $description;
$hierarchy = $facet->getHierarchy();
$options = array_map(function (HierarchyPluginBase $plugin) {
return $plugin->getPluginDefinition()['label'];
}, $facet->getHierarchies());
$form['facet_settings']['hierarchy'] = [
'#type' => 'select',
'#title' => $this->t('Hierarchy type'),
'#options' => $options,
'#default_value' => $hierarchy ? $hierarchy['type'] : '',
'#states' => [
'visible' => [
':input[name="facet_settings[use_hierarchy]"]' => ['checked' => TRUE],
],
],
];
$form['facet_settings']['keep_hierarchy_parents_active'] = [
'#type' => 'checkbox',
@ -535,8 +534,9 @@ class FacetForm extends EntityForm {
'#type' => 'number',
'#title' => $this->t('Minimum count'),
'#default_value' => $facet->getMinCount(),
'#description' => $this->t('Only display the results if there is this minimum amount of results.'),
'#description' => $this->t('Only display the results if there is this minimum amount of results. The default is "1". A setting "0" might result in a list of all possible facet items, regardless of the actual search query. But the result of a minimum count of "0" is not reliable and may very on the type of the field, the Search API backend and even between different releases or runtime configurations of the backend (for example Solr). Therefore it is highly recommended to avoid any feature that depends on a minimum count of "0".'),
'#maxlength' => 4,
'#min' => 0,
'#required' => TRUE,
];
if (!$facet->getFacetSource() instanceof SearchApiDisplay) {
@ -545,6 +545,30 @@ class FacetForm extends EntityForm {
$form['facet_settings']['min_count']['#description'] .= $this->t('This setting only works with Search API based facets.');
}
$form['facet_settings']['missing'] = [
'#type' => 'checkbox',
'#title' => $this->t('Show missing'),
'#default_value' => $facet->isMissing(),
'#description' => $this->t('Add a facet item that counts and selects all search results which match the current query but do not belong to any of the facet items.'),
];
if (!$facet->getFacetSource() instanceof SearchApiDisplay) {
$form['facet_settings']['missing']['#disabled'] = TRUE;
$form['facet_settings']['missing']['#description'] .= '<br />';
$form['facet_settings']['missing']['#description'] .= $this->t('This setting only works with Search API based facets.');
}
$form['facet_settings']['missing_label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label of missing items'),
'#description' => $this->t('Label of the facet item for which do not belong to any of the regular items.'),
'#default_value' => $facet->getMissingLabel(),
'#states' => [
'visible' => [
':input[name="facet_settings[missing]"]' => ['checked' => TRUE],
],
],
];
$form['facet_settings']['weight'] = [
'#type' => 'number',
'#title' => $this->t('Weight'),
@ -597,9 +621,7 @@ class FacetForm extends EntityForm {
foreach ($processors_by_stage as $stage => $processors) {
/** @var \Drupal\facets\Processor\ProcessorInterface $processor */
foreach ($processors as $processor_id => $processor) {
$weight = isset($processor_settings[$processor_id]['weights'][$stage])
? $processor_settings[$processor_id]['weights'][$stage]
: $processor->getDefaultWeight($stage);
$weight = $processor_settings[$processor_id]['weights'][$stage] ?? $processor->getDefaultWeight($stage);
if ($processor->isHidden()) {
$form['processors'][$processor_id]['weights'][$stage] = [
'#type' => 'value',
@ -682,10 +704,10 @@ class FacetForm extends EntityForm {
// Validate url alias.
$url_alias = $form_state->getValue(['facet_settings', 'url_alias']);
if ($url_alias == 'page') {
if ($url_alias === 'page') {
$form_state->setErrorByName('url_alias', $this->t('This URL alias is not allowed.'));
}
elseif (preg_match('/[^a-zA-Z0-9_~\.\-]/', $url_alias)) {
elseif (preg_match('/[^a-zA-Z0-9_~.\-]/', $url_alias)) {
$form_state->setErrorByName('url_alias', $this->t('The URL alias contains characters that are not allowed.'));
}
}
@ -733,6 +755,18 @@ class FacetForm extends EntityForm {
'min_count',
]
));
$facet->setMissing((bool) $form_state->getValue(
[
'facet_settings',
'missing',
]
));
$facet->setMissingLabel($form_state->getValue(
[
'facet_settings',
'missing_label',
]
));
$facet->setOnlyVisibleWhenFacetSourceIsVisible($form_state->getValue(
[
'facet_settings',

View File

@ -292,7 +292,7 @@ class FacetSettingsForm extends EntityForm {
$this->messenger()->addMessage($this->t('Facet %name has been updated.', ['%name' => $facet->getName()]));
}
list($type,) = explode(':', $facet_source_id);
[$type] = explode(':', $facet_source_id);
if ($type !== 'search_api') {
return $facet;
}
@ -305,9 +305,10 @@ class FacetSettingsForm extends EntityForm {
if ($view->display_handler instanceof Block) {
$facet->setOnlyVisibleWhenFacetSourceIsVisible(FALSE);
}
$view->display_handler->overrideOption('cache', ['type' => 'none']);
$view->save();
$this->messenger()->addMessage($this->t('Caching of view %view has been disabled.', ['%view' => $view->storage->label()]));
$views_cache_type = $view->display_handler->getOption('cache')['type'];
if ($views_cache_type !== 'none') {
$this->messenger()->addMessage($this->t('You may experience issues, because %view use cache. In case you will try to turn set cache plugin to none.', ['%view' => $view->storage->label()]));
}
}
}

View File

@ -102,12 +102,12 @@ class FacetSourceEditForm extends EntityForm {
$form['breadcrumb']['active'] = [
'#type' => 'checkbox',
'#title' => $this->t('Append active facets to breadcrumb'),
'#default_value' => isset($breadcrumb_settings['active']) ? $breadcrumb_settings['active'] : FALSE,
'#default_value' => $breadcrumb_settings['active'] ?? FALSE,
];
$form['breadcrumb']['before'] = [
'#type' => 'checkbox',
'#title' => $this->t('Show facet label before active facet'),
'#default_value' => isset($breadcrumb_settings['before']) ? $breadcrumb_settings['before'] : TRUE,
'#default_value' => $breadcrumb_settings['before'] ?? TRUE,
'#states' => [
'visible' => [
':input[name="breadcrumb[active]"]' => ['checked' => TRUE],
@ -117,7 +117,7 @@ class FacetSourceEditForm extends EntityForm {
$form['breadcrumb']['group'] = [
'#type' => 'checkbox',
'#title' => $this->t('Group active items under same crumb (not implemented yet - now always grouping)'),
'#default_value' => isset($breadcrumb_settings['group']) ? $breadcrumb_settings['group'] : FALSE,
'#default_value' => $breadcrumb_settings['group'] ?? FALSE,
'#states' => [
'visible' => [
':input[name="breadcrumb[active]"]' => ['checked' => TRUE],

View File

@ -2,6 +2,8 @@
namespace Drupal\facets\Hierarchy;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\UncacheableDependencyTrait;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\ProcessorPluginBase;
@ -9,17 +11,21 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* A base class for plugins that implements most of the boilerplate.
*
* By default all plugins that will extend this class will disable facets
* caching mechanism. It is strongly recommended to turn it on by implementing
* own methods for the CacheableDependencyInterface interface.
*/
abstract class HierarchyPluginBase extends ProcessorPluginBase implements HierarchyInterface, ContainerFactoryPluginInterface {
abstract class HierarchyPluginBase extends ProcessorPluginBase implements HierarchyInterface, ContainerFactoryPluginInterface, CacheableDependencyInterface {
use UncacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$request_stack = $container->get('request_stack');
// Support 9.3+.
// @todo remove switch after 9.3 or greater is required.
$request = version_compare(\Drupal::VERSION, '9.3', '>=') ? $request_stack->getMainRequest() : $request_stack->getMasterRequest();
$request = $request_stack->getMainRequest();
return new static($configuration, $plugin_id, $plugin_definition, $request);
}

View File

@ -2,10 +2,14 @@
namespace Drupal\facets\Plugin\Block;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\facets\FacetInterface;
use Drupal\facets\FacetManager\DefaultFacetManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
@ -33,6 +37,11 @@ class FacetBlock extends BlockBase implements ContainerFactoryPluginInterface {
*/
protected $facetStorage;
/**
* @var \Drupal\facets\FacetInterface
*/
protected $facet;
/**
* Construct a FacetBlock instance.
*
@ -70,24 +79,19 @@ class FacetBlock extends BlockBase implements ContainerFactoryPluginInterface {
* {@inheritdoc}
*/
public function build() {
/** @var \Drupal\facets\FacetInterface $facet */
$facet = $this->facetStorage->load($this->getDerivativeId());
// No need to build the facet if it does not need to be visible.
if ($facet->getOnlyVisibleWhenFacetSourceIsVisible() &&
(!$facet->getFacetSource() || !$facet->getFacetSource()->isRenderedInCurrentRequest())) {
return [];
}
// Do not build the facet if the block is being previewed.
if ($this->getContextValue('in_preview')) {
return [];
}
$facet = $this->getFacet();
// Let the facet_manager build the facets.
$build = $this->facetManager->build($facet);
if (!empty($build)) {
CacheableMetadata::createFromObject($facet)->applyTo($build);
// Add extra elements from facet source, for example, ajax scripts.
// @see Drupal\facets\Plugin\facets\facet_source\SearchApiDisplay
/** @var \Drupal\facets\FacetSource\FacetSourcePluginInterface $facet_source */
@ -122,47 +126,45 @@ class FacetBlock extends BlockBase implements ContainerFactoryPluginInterface {
return $build;
}
/**
* Get facet block entity.
*
* @return \Drupal\facets\FacetInterface
* The facet entity.
*/
protected function getFacet(): FacetInterface {
if (!$this->facet) {
$this->facet = $this->facetStorage->load($this->getDerivativeId());
}
return $this->facet;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return $this->getFacet()->getCacheTags();
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return $this->getFacet()->getCacheContexts();
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
// A facet block cannot be cached, because it must always match the current
// search results, and Search API gets those search results from a data
// source that can be 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. If the search results cannot be cached, then neither can the facets,
// because they must always match.
// Fortunately, facet blocks are rendered using a lazy builder (like all
// blocks in Drupal), which means their rendering can be deferred (unlike
// the search results, which are the main content of the page, and deferring
// their rendering would mean sending an empty page to the user). This means
// that facet blocks can be rendered and sent *after* the initial page was
// loaded, by installing the BigPipe (big_pipe) module.
//
// When BigPipe is enabled, the search results will appear first, and then
// each facet block will appear one-by-one, in DOM order.
// See https://www.drupal.org/project/big_pipe.
//
// In a future version of Facet API, this could be refined, but due to the
// reliance on external data sources, it will be very difficult if not
// impossible to improve this significantly.
//
// Note: when using Drupal core's Search module instead of the contributed
// Search API module, the above limitations do not apply, but for now it is
// not considered worth the effort to optimize this just for Drupal core's
// Search.
return 0;
return $this->getFacet()->getCacheMaxAge();
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
/** @var \Drupal\facets\FacetInterface $facet */
$facet = $this->facetStorage->load($this->getDerivativeId());
return ['config' => [$facet->getConfigDependencyName()]];
return ['config' => [$this->getFacet()->getConfigDependencyName()]];
}
/**
@ -185,4 +187,19 @@ class FacetBlock extends BlockBase implements ContainerFactoryPluginInterface {
return $this->t('Placeholder for the "@facet" facet', ['@facet' => $this->getDerivativeId()]);
}
/**
* {@inheritDoc}
*
* Allow to render facet block if one of the following conditions are met:
* - facet is allowed to be displayed regardless of the source visibility
* - facet source is rendered in the same request as facet.
*/
public function blockAccess(AccountInterface $account) {
$facet = $this->getFacet();
return AccessResult::allowedIf(
!$facet->getOnlyVisibleWhenFacetSourceIsVisible()
|| ($facet->getFacetSource() && $facet->getFacetSource()->isRenderedInCurrentRequest())
)->addCacheableDependency($facet);
}
}

View File

@ -44,7 +44,7 @@ class FacetBlockDeriver implements ContainerDeriverInterface {
*/
public function getDerivativeDefinition($derivative_id, $base_plugin_definition) {
$derivatives = $this->getDerivativeDefinitions($base_plugin_definition);
return isset($derivatives[$derivative_id]) ? $derivatives[$derivative_id] : NULL;
return $derivatives[$derivative_id] ?? NULL;
}
/**

View File

@ -3,6 +3,8 @@
namespace Drupal\facets\Plugin\facets\facet_source;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Extension\ModuleHandler;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
@ -24,6 +26,13 @@ use Symfony\Component\HttpFoundation\Request;
/**
* Provides a facet source based on a Search API display.
*
* @todo The support for non views displays might be removed from facets 3.x and
* moved into a sub or contributed module. So this class needs to become
* something like "SearchApiViewsDisplay" and a "SearchApiCustomDisplay"
* plugin needs to be provided by the sub or contributed module. At the
* moment we have switches within this class for example to get the cache
* metadata. Those need to be removed.
*
* @FacetsFacetSource(
* id = "search_api",
* deriver = "Drupal\facets\Plugin\facets\facet_source\SearchApiDisplayDeriver"
@ -31,6 +40,14 @@ use Symfony\Component\HttpFoundation\Request;
*/
class SearchApiDisplay extends FacetSourcePluginBase implements SearchApiFacetSourceInterface {
/**
* List of Search API cache plugins that works with Facets cache system.
*/
const CACHEABLE_PLUGINS = [
'search_api_tag',
'search_api_time',
];
/**
* The search index the query should is executed on.
*
@ -115,9 +132,7 @@ class SearchApiDisplay extends FacetSourcePluginBase implements SearchApiFacetSo
$container->get('plugin.manager.facets.query_type'),
$container->get('search_api.query_helper'),
$container->get('plugin.manager.search_api.display'),
// Support 9.3+.
// @todo remove switch after 9.3 or greater is required.
version_compare(\Drupal::VERSION, '9.3', '>=') ? $request_stack->getMainRequest() : $request_stack->getMasterRequest(),
$request_stack->getMainRequest(),
$container->get('module_handler')
);
}
@ -159,11 +174,12 @@ class SearchApiDisplay extends FacetSourcePluginBase implements SearchApiFacetSo
// If there are no results, we can check the Search API Display plugin has
// configuration for views. If that configuration exists, we can execute
// that view and try to use it's results.
// that view and try to use its results.
$display_definition = $this->getDisplay()->getPluginDefinition();
if ($results === NULL && isset($display_definition['view_id'])) {
$view = Views::getView($display_definition['view_id']);
$view->setDisplay($display_definition['view_display']);
$view->preExecute();
$view->execute();
$results = $this->searchApiQueryHelper->getResults($search_id);
}
@ -187,7 +203,7 @@ class SearchApiDisplay extends FacetSourcePluginBase implements SearchApiFacetSo
$configuration = [
'query' => $results->getQuery(),
'facet' => $facet,
'results' => isset($facet_results[$facet->getFieldIdentifier()]) ? $facet_results[$facet->getFieldIdentifier()] : [],
'results' => $facet_results[$facet->getFieldIdentifier()] ?? [],
];
// Get the Facet Specific Query Type so we can process the results
@ -418,4 +434,94 @@ class SearchApiDisplay extends FacetSourcePluginBase implements SearchApiFacetSo
}
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
if ($views_display = $this->getViewsDisplay()) {
return $views_display
->getDisplay()
->getCacheMetadata()
->getCacheContexts();
}
// Custom display implementations should provide their own cache metadata.
$display = $this->getDisplay();
if ($display instanceof CacheableDependencyInterface) {
return $display->getCacheContexts();
}
return [];
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
if ($views_display = $this->getViewsDisplay()) {
return Cache::mergeTags(
$views_display->getDisplay()->getCacheMetadata()->getCacheTags(),
$views_display->getCacheTags()
);
}
// Custom display implementations should provide their own cache metadata.
$display = $this->getDisplay();
if ($display instanceof CacheableDependencyInterface) {
return $display->getCacheTags();
}
return [];
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
if ($views_display = $this->getViewsDisplay()) {
$cache_plugin = $views_display->getDisplay()->getPlugin('cache');
return Cache::mergeMaxAges(
$views_display->getDisplay()->getCacheMetadata()->getCacheMaxAge(),
$cache_plugin ? $cache_plugin->getCacheMaxAge() : 0
);
}
// Custom display implementations should provide their own cache metadata.
$display = $this->getDisplay();
if ($display instanceof CacheableDependencyInterface) {
return $display->getCacheMaxAge();
}
// Caching is not supported.
return 0;
}
/**
* {@inheritDoc}
*
* Alter views view cache metadata:
* - When view being re-saved it will collect all cache metadata from its
* plugins, including cache plugin.
* - Search API cache plugin will pre-execute the query and collect cacheable
* metadata from all facets and will pass it to the view.
*
* View will use collected cache tags to invalidate search results. And cache
* context provided by the facet to vary results.
*
* @see \Drupal\views\Plugin\views\display\DisplayPluginBase::calculateCacheMetadata()
* @see \Drupal\search_api\Plugin\views\cache\SearchApiCachePluginTrait::alterCacheMetadata()
* @see \Drupal\facets\FacetManager\DefaultFacetManager::alterQuery()
*/
public function registerFacet(FacetInterface $facet) {
if (
// On the config-sync or site install view will already have all required
// cache tags, so don't react if it's already there.
!in_array('config:' . $facet->getConfigDependencyName(), $this->getCacheTags())
// Re-save it only if we know that views cache plugin works with facets.
&& in_array($this->getViewsDisplay()->getDisplay()->getOption('cache')['type'], static::CACHEABLE_PLUGINS)
) {
$this->getViewsDisplay()->save();
}
}
}

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\hierarchy;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\facets\Hierarchy\HierarchyPluginBase;
/**
@ -15,6 +16,8 @@ use Drupal\facets\Hierarchy\HierarchyPluginBase;
*/
class DateItems extends HierarchyPluginBase {
use UnchangingCacheableDependencyTrait;
/**
* Static cache for the parents.
*

View File

@ -2,8 +2,10 @@
namespace Drupal\facets\Plugin\facets\hierarchy;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\facets\Hierarchy\HierarchyPluginBase;
use Drupal\taxonomy\TermInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@ -96,7 +98,7 @@ class Taxonomy extends HierarchyPluginBase {
$current_tid = $parent;
$parents[$id][] = $parent;
}
return isset($parents[$id]) ? $parents[$id] : [];
return $parents[$id] ?? [];
}
/**
@ -152,6 +154,17 @@ class Taxonomy extends HierarchyPluginBase {
if (!$topLevelTerms) {
/** @var \Drupal\taxonomy\Entity\Term $term */
$term = $this->getTermStorage()->load($id);
// Issue #3260603:
// Due to a bug in core
// https://www.drupal.org/project/drupal/issues/2723323
// it may happen that a taxonomy term is still referenced in a field,
// even though the term has been deleted.
// Not checking the term is empty produces a fatal error.
if (!$term instanceof TermInterface) {
continue;
}
$topLevelTerms = array_map(function ($term) {
return $term->tid;
}, $this->getTermStorage()->loadTree($term->bundle(), 0, 1));
@ -197,4 +210,11 @@ class Taxonomy extends HierarchyPluginBase {
return $this->termParents[$tid] = reset($parents)->id();
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return Cache::mergeTags(parent::getCacheTags(), ['taxonomy_term:list']);
}
}

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\facets\Processor\SortProcessorPluginBase;
use Drupal\facets\Processor\SortProcessorInterface;
use Drupal\facets\Result\Result;
@ -21,6 +22,8 @@ use Drupal\facets\Result\Result;
*/
class ActiveWidgetOrderProcessor extends SortProcessorPluginBase implements SortProcessorInterface {
use UnchangingCacheableDependencyTrait;
/**
* {@inheritdoc}
*/

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
use Drupal\facets\FacetInterface;
use Drupal\Core\Form\FormStateInterface;
@ -23,6 +24,8 @@ use Drupal\facets\Processor\ProcessorPluginBase;
*/
class BooleanItemProcessor extends ProcessorPluginBase implements BuildProcessorInterface {
use UnchangingCacheableDependencyTrait;
/**
* {@inheritdoc}
*/

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\Core\Form\FormStateInterface;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
@ -25,6 +26,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*/
class CombineFacetProcessor extends ProcessorPluginBase implements BuildProcessorInterface, ContainerFactoryPluginInterface {
use UnchangingCacheableDependencyTrait;
/**
* The language manager.
*
@ -148,7 +151,6 @@ class CombineFacetProcessor extends ProcessorPluginBase implements BuildProcesso
/** @var \Drupal\facets\Entity\Facet $current_facet */
$current_facet = $this->facetStorage->load($facet_id);
$current_facet = $this->facetsManager->returnBuiltFacet($current_facet);
switch ($settings['mode']) {
case 'union':
$results = $keyed_results + $current_facet->getResultsKeyedByRawValue();
@ -162,6 +164,8 @@ class CombineFacetProcessor extends ProcessorPluginBase implements BuildProcesso
$results = array_intersect_key($keyed_results, $current_facet->getResultsKeyedByRawValue());
break;
}
// Pass build processor information into current facet.
$facet->addCacheableDependency($current_facet);
}
return $results;

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\Core\Form\FormStateInterface;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
@ -21,6 +22,8 @@ use Drupal\facets\Processor\ProcessorPluginBase;
*/
class CountLimitProcessor extends ProcessorPluginBase implements BuildProcessorInterface {
use UnchangingCacheableDependencyTrait;
/**
* {@inheritdoc}
*/

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\facets\Processor\SortProcessorPluginBase;
use Drupal\facets\Processor\SortProcessorInterface;
use Drupal\facets\Result\Result;
@ -21,6 +22,8 @@ use Drupal\facets\Result\Result;
*/
class CountWidgetOrderProcessor extends SortProcessorPluginBase implements SortProcessorInterface {
use UnchangingCacheableDependencyTrait;
/**
* {@inheritdoc}
*/

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\facets\FacetInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
@ -22,6 +23,8 @@ use Drupal\facets\Plugin\facets\query_type\SearchApiDate;
*/
class DateItemProcessor extends ProcessorPluginBase implements BuildProcessorInterface {
use UnchangingCacheableDependencyTrait;
/**
* {@inheritdoc}
*/

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\Core\Form\FormStateInterface;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
@ -25,6 +26,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*/
class DependentFacetProcessor extends ProcessorPluginBase implements BuildProcessorInterface, ContainerFactoryPluginInterface {
use UnchangingCacheableDependencyTrait;
/**
* The language manager.
*
@ -125,6 +128,7 @@ class DependentFacetProcessor extends ProcessorPluginBase implements BuildProces
'#title' => $this->t('Values'),
'#type' => 'textfield',
'#default_value' => empty($config[$facet->id()]['values']) ? '' : $config[$facet->id()]['values'],
'#description' => $this->t('Enter a comma-separated list of values. Example: value1, value2, value3'),
'#states' => [
'visible' => [
':input[name="facet_settings[' . $this->getPluginId() . '][settings][' . $facet->id() . '][enable]"]' => ['checked' => TRUE],
@ -168,10 +172,10 @@ class DependentFacetProcessor extends ProcessorPluginBase implements BuildProces
}
foreach ($enabled_conditions as $facet_id => $condition_settings) {
/** @var \Drupal\facets\Entity\Facet $current_facet */
$current_facet = $this->facetStorage->load($facet_id);
$current_facet = $this->facetsManager->returnBuiltFacet($current_facet);
$facet->addCacheableDependency($current_facet);
if (!$this->isConditionMet($condition_settings, $current_facet)) {
return [];
@ -202,7 +206,7 @@ class DependentFacetProcessor extends ProcessorPluginBase implements BuildProces
if ($condition_settings['condition'] === 'values') {
$return = FALSE;
$values = explode(',', $condition_settings['values']);
$values = array_map('trim', explode(',', $condition_settings['values']));
foreach ($facet->getActiveItems() as $value) {
if (in_array($value, $values)) {
$return = TRUE;

View File

@ -3,6 +3,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Component\Transliteration\TransliterationInterface;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\facets\Processor\SortProcessorInterface;
use Drupal\facets\Processor\SortProcessorPluginBase;
@ -24,6 +25,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*/
class DisplayValueWidgetOrderProcessor extends SortProcessorPluginBase implements SortProcessorInterface, ContainerFactoryPluginInterface {
use UnchangingCacheableDependencyTrait;
/**
* The transliteration service.
*

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\Core\Form\FormStateInterface;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
@ -21,6 +22,8 @@ use Drupal\facets\Processor\ProcessorPluginBase;
*/
class ExcludeSpecifiedItemsProcessor extends ProcessorPluginBase implements BuildProcessorInterface {
use UnchangingCacheableDependencyTrait;
/**
* {@inheritdoc}
*/

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\Core\Form\FormStateInterface;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
@ -21,6 +22,8 @@ use Drupal\facets\Processor\ProcessorPluginBase;
*/
class GranularItemProcessor extends ProcessorPluginBase implements BuildProcessorInterface {
use UnchangingCacheableDependencyTrait;
/**
* {@inheritdoc}
*/

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
use Drupal\facets\Processor\ProcessorPluginBase;
@ -20,6 +21,8 @@ use Drupal\facets\Processor\ProcessorPluginBase;
*/
class HideActiveItemsProcessor extends ProcessorPluginBase implements BuildProcessorInterface {
use UnchangingCacheableDependencyTrait;
/**
* {@inheritdoc}
*/

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
use Drupal\facets\Processor\ProcessorPluginBase;
@ -20,6 +21,8 @@ use Drupal\facets\Processor\ProcessorPluginBase;
*/
class HideInactiveSiblingsProcessor extends ProcessorPluginBase implements BuildProcessorInterface {
use UnchangingCacheableDependencyTrait;
/**
* {@inheritdoc}
*/
@ -29,6 +32,7 @@ class HideInactiveSiblingsProcessor extends ProcessorPluginBase implements Build
if ($facet->getUseHierarchy()) {
$hierarchy = $facet->getHierarchyInstance();
$facet->addCacheableDependency($hierarchy);
if (!$facet->getKeepHierarchyParentsActive()) {
$parents_of_active_items = [];

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
use Drupal\facets\Processor\ProcessorPluginBase;
@ -20,6 +21,8 @@ use Drupal\facets\Processor\ProcessorPluginBase;
*/
class HideNonNarrowingResultProcessor extends ProcessorPluginBase implements BuildProcessorInterface {
use UnchangingCacheableDependencyTrait;
/**
* {@inheritdoc}
*/

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
use Drupal\facets\Processor\ProcessorPluginBase;
@ -20,6 +21,8 @@ use Drupal\facets\Processor\ProcessorPluginBase;
*/
class HideOnlyOneItemProcessor extends ProcessorPluginBase implements BuildProcessorInterface {
use UnchangingCacheableDependencyTrait;
/**
* {@inheritdoc}
*/

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
use Drupal\facets\Processor\ProcessorPluginBase;
@ -21,6 +22,8 @@ use Drupal\facets\Processor\ProcessorPluginBase;
*/
class HierarchyProcessor extends ProcessorPluginBase implements BuildProcessorInterface {
use UnchangingCacheableDependencyTrait;
/**
* An array of all entity ids in the active resultset which are a child.
*
@ -38,8 +41,10 @@ class HierarchyProcessor extends ProcessorPluginBase implements BuildProcessorIn
foreach ($results as $result) {
$keyed_results[$result->getRawValue()] = $result;
}
$hierarchy = $facet->getHierarchyInstance();
$facet->addCacheableDependency($hierarchy);
$parent_groups = $facet->getHierarchyInstance()->getChildIds(array_keys($keyed_results));
$parent_groups = $hierarchy->getChildIds(array_keys($keyed_results));
$keyed_results = $this->buildHierarchicalTree($keyed_results, $parent_groups);
// Remove children from primary level.

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
@ -30,6 +31,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*/
class ListItemProcessor extends ProcessorPluginBase implements BuildProcessorInterface, ContainerFactoryPluginInterface {
use UnchangingCacheableDependencyTrait;
/**
* The config manager.
*
@ -133,12 +136,13 @@ class ListItemProcessor extends ProcessorPluginBase implements BuildProcessorInt
$fieldDefinition->getTargetEntityTypeId(),
$fieldDefinition->getName()
);
if ($fieldDefinition instanceof BaseFieldDefinition) {
if (isset($base_fields[$field->getPropertyPath()])) {
$field = $base_fields[$field->getPropertyPath()];
}
}
// Diggs down to get the referenced field the entity reference is based
// on.
elseif ($this->configManager->loadConfigEntityByName($referenced_entity_name) !== NULL) {
@ -148,6 +152,7 @@ class ListItemProcessor extends ProcessorPluginBase implements BuildProcessorInt
}
if ($field instanceof FieldStorageDefinitionInterface) {
if ($field->getName() !== 'type') {
$facet->addCacheableDependency($field);
$allowed_values = options_allowed_values($field);
if (!empty($allowed_values)) {
return $this->overWriteDisplayValues($results, $allowed_values);
@ -158,6 +163,7 @@ class ListItemProcessor extends ProcessorPluginBase implements BuildProcessorInt
// bundle field.
$list_bundles = $this->entityTypeBundleInfo->getBundleInfo($entity);
if (!empty($list_bundles)) {
$facet->addCacheTags(['entity_bundles']);
foreach ($list_bundles as $key => $bundle) {
$allowed_values[$key] = $bundle['label'];
}

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\facets\Processor\SortProcessorPluginBase;
use Drupal\facets\Processor\SortProcessorInterface;
use Drupal\facets\Result\Result;
@ -20,6 +21,8 @@ use Drupal\facets\Result\Result;
*/
class RawValueWidgetOrderProcessor extends SortProcessorPluginBase implements SortProcessorInterface {
use UnchangingCacheableDependencyTrait;
/**
* {@inheritdoc}
*/

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
use Drupal\facets\Processor\ProcessorPluginBase;
@ -20,6 +21,8 @@ use Drupal\facets\Processor\ProcessorPluginBase;
*/
class ShowOnlyDeepestLevelItemsProcessor extends ProcessorPluginBase implements BuildProcessorInterface {
use UnchangingCacheableDependencyTrait;
/**
* {@inheritdoc}
*/

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\Core\Form\FormStateInterface;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
@ -22,6 +23,8 @@ use Drupal\facets\Result\Result;
*/
class ShowSiblingsProcessor extends ProcessorPluginBase implements BuildProcessorInterface {
use UnchangingCacheableDependencyTrait;
/**
* {@inheritdoc}
*/
@ -31,7 +34,9 @@ class ShowSiblingsProcessor extends ProcessorPluginBase implements BuildProcesso
$rawValues = array_map(function ($result) {
return $result->getRawValue();
}, $results);
foreach ($facet->getHierarchyInstance()->getSiblingIds($rawValues, $facet->getActiveItems(), $this->getConfiguration()['show_parent_siblings']) as $siblingId) {
$hierarchy = $facet->getHierarchyInstance();
$facet->addCacheableDependency($hierarchy);
foreach ($hierarchy->getSiblingIds($rawValues, $facet->getActiveItems(), $this->getConfiguration()['show_parent_siblings']) as $siblingId) {
$results[] = new Result($facet, $siblingId, $siblingId, 0);
}
}

View File

@ -2,6 +2,8 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
@ -25,6 +27,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*/
class TermWeightWidgetOrderProcessor extends SortProcessorPluginBase implements ContainerFactoryPluginInterface {
use UnchangingCacheableDependencyTrait;
/**
* The entity type manager.
*
@ -126,4 +130,11 @@ class TermWeightWidgetOrderProcessor extends SortProcessorPluginBase implements
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return Cache::mergeTags(parent::getCacheTags(), ['taxonomy_term_list']);
}
}

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Config\ConfigManagerInterface;
@ -167,7 +168,7 @@ class TranslateEntityAggregatedFieldProcessor extends ProcessorPluginBase implem
if ($entity instanceof TranslatableInterface && $entity->hasTranslation($language_interface->getId())) {
$entity = $entity->getTranslation($language_interface->getId());
}
$facet->addCacheableDependency($entity);
// Overwrite the result's display value.
$results[$i]->setDisplayValue($entity->label());
}
@ -179,6 +180,7 @@ class TranslateEntityAggregatedFieldProcessor extends ProcessorPluginBase implem
// bundle field.
foreach ($entity_type_ids as $entity) {
$list_bundles = $this->entityTypeBundleInfo->getBundleInfo($entity);
$facet->addCacheTags(['entity_bundles']);
if (!empty($list_bundles)) {
foreach ($list_bundles as $key => $bundle) {
$allowed_values[$key] = $bundle['label'];
@ -231,4 +233,21 @@ class TranslateEntityAggregatedFieldProcessor extends ProcessorPluginBase implem
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return Cache::mergeContexts(parent::getCacheContexts(), ['languages:language_interface']);
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
// This will work unless the Search API Query uses "wrong" caching. Ideally
// we would set a cache tag to invalidate the cache whenever a translatable
// entity is added or changed. But there's no tag in drupal yet.
return Cache::PERMANENT;
}
}

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
@ -116,7 +117,12 @@ class TranslateEntityProcessor extends ProcessorPluginBase implements BuildProce
// Loop over all results.
foreach ($results as $i => $result) {
if (!isset($entities[$ids[$i]])) {
unset($results[$i]);
if ($result->isMissing()) {
$results[$i]->setDisplayValue($facet->getMissingLabel());
}
else {
unset($results[$i]);
}
continue;
}
@ -128,7 +134,7 @@ class TranslateEntityProcessor extends ProcessorPluginBase implements BuildProce
if ($entity instanceof TranslatableInterface && $entity->hasTranslation($language_interface->getId())) {
$entity = $entity->getTranslation($language_interface->getId());
}
$facet->addCacheableDependency($entity);
// Overwrite the result's display value.
$results[$i]->setDisplayValue($entity->label());
}
@ -158,4 +164,21 @@ class TranslateEntityProcessor extends ProcessorPluginBase implements BuildProce
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return Cache::mergeContexts(parent::getCacheContexts(), ['languages:language_interface']);
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
// This will work unless the Search API Query uses "wrong" caching. Ideally
// we would set a cache tag to invalidate the cache whenever a translatable
// entity is added or changed. But there's no tag in drupal yet.
return Cache::PERMANENT;
}
}

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\Cache;
use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
use Drupal\facets\FacetInterface;
@ -34,6 +35,7 @@ class UidToUserNameCallbackProcessor extends ProcessorPluginBase implements Buil
/** @var \Drupal\user\Entity\User $user */
if (($user = User::load($result->getRawValue())) !== NULL) {
$result->setDisplayValue($user->getDisplayName());
$facet->addCacheableDependency($user);
$usernames[] = $result;
}
}
@ -68,4 +70,18 @@ class UidToUserNameCallbackProcessor extends ProcessorPluginBase implements Buil
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return Cache::mergeTags(parent::getCacheTags(), ['user_list']);
}
}

View File

@ -2,6 +2,7 @@
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\facets\Exception\InvalidProcessorException;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
@ -12,7 +13,7 @@ use Drupal\facets\Processor\ProcessorPluginBase;
* The URL processor handler triggers the actual url processor.
*
* The URL processor handler allows managing the weight of the actual URL
* processor per Facet. This handler will trigger the actual.
* processor per Facet. This handler will trigger the actual.
*
* @FacetsUrlProcessor, which can be configured on the Facet source.
*
@ -82,4 +83,25 @@ class UrlProcessorHandler extends ProcessorPluginBase implements BuildProcessorI
$this->processor->setActiveItems($facet);
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return CacheableMetadata::createFromObject($this->processor)->getCacheTags();
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return CacheableMetadata::createFromObject($this->processor)->getCacheContexts();
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return CacheableMetadata::createFromObject($this->processor)->getCacheMaxAge();
}
}

View File

@ -48,7 +48,7 @@ class SearchApiGranular extends QueryTypeRangeBase {
$query_operator = $this->facet->getQueryOperator();
$facet_results = [];
foreach ($this->results as $result) {
if ($result['count'] || $query_operator == 'or') {
if ($result['count'] || $query_operator === 'or') {
$result_filter = trim($result['filter'], '"');
$facet_results[] = new Result($this->facet, $result_filter, $result_filter, $result['count']);
}

View File

@ -58,12 +58,17 @@ class SearchApiRange extends QueryTypePluginBase {
if (!empty($this->results)) {
$facet_results = [];
foreach ($this->results as $result) {
if ($result['count'] || $query_operator == 'or') {
if ($result['count'] || $query_operator === 'or') {
$count = $result['count'];
while (is_array($result['filter'])) {
$result['filter'] = current($result['filter']);
}
$result_filter = trim($result['filter'], '"');
if ($result_filter === 'NULL' || $result_filter === '') {
// "Missing" facet items could not be handled in ranges.
continue;
}
$result = new Result($this->facet, $result_filter, $result_filter, $count);
$facet_results[] = $result;
}

View File

@ -47,7 +47,17 @@ class SearchApiString extends QueryTypePluginBase {
if (count($active_items)) {
$filter = $query->createConditionGroup($operator, ['facet:' . $field_identifier]);
foreach ($active_items as $value) {
$filter->addCondition($this->facet->getFieldIdentifier(), $value, $exclude ? '<>' : '=');
if (str_starts_with($value, '!(')) {
/** @var \Drupal\facets\UrlProcessor\UrlProcessorInterface $urlProcessor */
$urlProcessor = $this->facet->getProcessors()['url_processor_handler']->getProcessor();
foreach (explode($urlProcessor->getDelimiter(), substr($value, 2, -1)) as $missing_value) {
// Note that $exclude needs to be inverted for "missing".
$filter->addCondition($this->facet->getFieldIdentifier(), $missing_value, !$exclude ? '<>' : '=');
}
}
else {
$filter->addCondition($this->facet->getFieldIdentifier(), $value, $exclude ? '<>' : '=');
}
}
$query->addConditionGroup($filter);
}
@ -63,8 +73,8 @@ class SearchApiString extends QueryTypePluginBase {
if (!empty($this->results)) {
$facet_results = [];
foreach ($this->results as $result) {
if ($result['count'] || $query_operator == 'or') {
$result_filter = $result['filter'];
if ($result['count'] || $query_operator === 'or') {
$result_filter = $result['filter'] ?? '';
if ($result_filter[0] === '"') {
$result_filter = substr($result_filter, 1);
}
@ -73,10 +83,16 @@ class SearchApiString extends QueryTypePluginBase {
}
$count = $result['count'];
$result = new Result($this->facet, $result_filter, $result_filter, $count);
$facet_results[] = $result;
$result->setMissing($this->facet->isMissing() && $result_filter === '!');
$facet_results[$result_filter] = $result;
}
}
$this->facet->setResults($facet_results);
if (isset($facet_results['!']) && $facet_results['!']->isMissing()) {
$facet_results['!']->setMissingFilters(array_keys($facet_results));
}
$this->facet->setResults(array_values($facet_results));
}
return $this->facet;

View File

@ -2,17 +2,18 @@
namespace Drupal\facets\Plugin\facets\url_processor;
use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Url;
use Drupal\facets\Event\ActiveFiltersParsed;
use Drupal\facets\Event\QueryStringCreated;
use Drupal\facets\Event\UrlCreated;
use Drupal\facets\FacetInterface;
use Drupal\facets\UrlProcessor\UrlProcessorPluginBase;
use Drupal\facets\Utility\FacetsUrlGenerator;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
/**
* Query string URL processor.
@ -25,6 +26,8 @@ use Symfony\Component\Routing\Exception\ResourceNotFoundException;
*/
class QueryString extends UrlProcessorPluginBase {
use UnchangingCacheableDependencyTrait;
/**
* A string of how to represent the facet in the url.
*
@ -39,12 +42,20 @@ class QueryString extends UrlProcessorPluginBase {
*/
protected $eventDispatcher;
/**
* The URL generator.
*
* @var \Drupal\facets\Utility\FacetsUrlGenerator
*/
protected $urlGenerator;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, Request $request, EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $eventDispatcher) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, Request $request, EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $eventDispatcher, FacetsUrlGenerator $urlGenerator) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $request, $entity_type_manager);
$this->eventDispatcher = $eventDispatcher;
$this->urlGenerator = $urlGenerator;
$this->initializeActiveFilters();
}
@ -58,7 +69,8 @@ class QueryString extends UrlProcessorPluginBase {
$plugin_definition,
$container->get('request_stack')->getCurrentRequest(),
$container->get('entity_type.manager'),
$container->get('event_dispatcher')
$container->get('event_dispatcher'),
$container->get('facets.utility.url_generator')
);
}
@ -84,6 +96,8 @@ class QueryString extends UrlProcessorPluginBase {
// Set the url alias from the facet object.
$this->urlAlias = $facet->getUrlAlias();
// In case of a view page display, the facet source has a path, If the
// source is a block, the path is null.
$facet_source_path = $facet->getFacetSource()->getPath();
$request = $this->getRequestByFacetSourcePath($facet_source_path);
$requestUrl = $this->getUrlForRequest($facet_source_path, $request);
@ -108,9 +122,14 @@ class QueryString extends UrlProcessorPluginBase {
$this->buildUrls($facet, $children);
}
$filter_missing = '';
if ($result->getRawValue() === NULL) {
$filter_string = NULL;
}
elseif ($result->isMissing()) {
$filter_missing = $this->urlAlias . $this->getSeparator() . '!(';
$filter_string = $filter_missing . implode($this->getDelimiter(), $result->getMissingFilters()) . ')';
}
else {
$filter_string = $this->urlAlias . $this->getSeparator() . $result->getRawValue();
}
@ -121,11 +140,11 @@ class QueryString extends UrlProcessorPluginBase {
// If the value is active, remove the filter string from the parameters.
if ($result->isActive()) {
foreach ($filter_params as $key => $filter_param) {
if ($filter_param == $filter_string) {
if ($filter_param === $filter_string || ($filter_missing && str_starts_with($filter_param, $filter_missing))) {
unset($filter_params[$key]);
}
}
if ($facet->getUseHierarchy()) {
if ($facet->getUseHierarchy() && !$result->isMissing()) {
$id = $result->getRawValue();
// Disable child filters.
@ -197,7 +216,7 @@ class QueryString extends UrlProcessorPluginBase {
}
}
// Allow other modules to alter the result url built.
// Allow other modules to alter the result url query string built.
$event = new QueryStringCreated($result_get_params, $filter_params, $result, $this->activeFilters, $facet);
$this->eventDispatcher->dispatch($event);
$filter_params = $event->getFilterParameters();
@ -216,14 +235,18 @@ class QueryString extends UrlProcessorPluginBase {
// See https://www.drupal.org/node/2898189.
unset($new_url_params['page']);
// Remove core wrapper format (e.g. render-as-ajax-response) paremeters.
// Remove core wrapper format (e.g. render-as-ajax-response) parameters.
unset($new_url_params[MainContentViewSubscriber::WRAPPER_FORMAT]);
// Set the new url parameters.
$url->setOption('query', $new_url_params);
}
$result->setUrl($url);
// Allow other modules to alter the result url built.
$event = new UrlCreated($url, $result, $facet);
$this->eventDispatcher->dispatch($event);
$result->setUrl($event->getUrl());
}
// Restore page parameter again. See https://www.drupal.org/node/2726455.
@ -266,9 +289,8 @@ class QueryString extends UrlProcessorPluginBase {
/**
* Gets the URL object for a request.
*
* This method statically caches the URL object for a request based on the
* facet source path. This reduces subsequent calls to the processor from
* having to regenerate the URL object.
* This method delegates to the URL generator service. But we keep it for
* backward compatibility for custom implementations that extend this class.
*
* @param string $facet_source_path
* The facet source path.
@ -279,37 +301,7 @@ class QueryString extends UrlProcessorPluginBase {
* The URL.
*/
protected function getUrlForRequest($facet_source_path, Request $request) {
/** @var \Drupal\Core\Url[] $requestUrlsByPath */
$requestUrlsByPath = &drupal_static(__CLASS__ . __FUNCTION__, []);
if (array_key_exists($facet_source_path, $requestUrlsByPath)) {
return $requestUrlsByPath[$facet_source_path];
}
// Try to grab any route params from the original request.
// In case of request path not having a matching route, Url generator will
// fail with.
try {
$requestUrl = Url::createFromRequest($request);
}
catch (ResourceNotFoundException $e) {
// Bypass exception if no path available.
// Should be unreachable in default FacetSource implementations,
// but you never know.
if (!$facet_source_path) {
throw $e;
}
$requestUrl = Url::fromUserInput($facet_source_path, [
'query' => [
'_format' => $this->request->get('_format'),
],
]);
}
$requestUrl->setOption('attributes', ['rel' => 'nofollow']);
$requestUrlsByPath[$facet_source_path] = $requestUrl;
return $requestUrl;
return $this->urlGenerator->getUrlForRequest($request, $facet_source_path);
}
/**
@ -417,4 +409,11 @@ class QueryString extends UrlProcessorPluginBase {
return $mapping[$facet_source_id][$facet_id];
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['url.query_args'];
}
}

View File

@ -67,14 +67,14 @@ class LinksWidget extends WidgetPluginBase {
unset($active_filters[$facet->id()]);
// Only if there are still active filters, use url generator.
$urlGenerator = \Drupal::service('facets.utility.url_generator');
if ($active_filters) {
$url = \Drupal::service('facets.utility.url_generator')
->getUrl($active_filters, FALSE);
$url = $urlGenerator->getUrl($active_filters, FALSE);
}
else {
$request = \Drupal::request();
$url = Url::createFromRequest($request);
$facet_source = $facet->getFacetSource();
$url = $urlGenerator->getUrlForRequest($request, $facet_source ? $facet_source->getPath() : NULL);
$params = $request->query->all();
unset($params[$url_processor->getFilterKey()]);
if (\array_key_exists('page', $params)) {

View File

@ -2,6 +2,8 @@
namespace Drupal\facets\Processor;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\UncacheableDependencyTrait;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\DependencyTrait;
use Drupal\Core\Plugin\PluginBase;
@ -9,9 +11,14 @@ use Drupal\facets\FacetInterface;
/**
* A base class for plugins that implements most of the boilerplate.
*
* By default all plugins that will extend this class will disable facets
* caching mechanism. It is strongly recommended to turn it on by implementing
* own methods for the CacheableDependencyInterface interface.
*/
class ProcessorPluginBase extends PluginBase implements ProcessorInterface {
class ProcessorPluginBase extends PluginBase implements ProcessorInterface, CacheableDependencyInterface {
use UncacheableDependencyTrait;
use DependencyTrait;
/**
@ -69,7 +76,7 @@ class ProcessorPluginBase extends PluginBase implements ProcessorInterface {
*/
public function getDescription() {
$plugin_definition = $this->getPluginDefinition();
return isset($plugin_definition['description']) ? $plugin_definition['description'] : '';
return $plugin_definition['description'] ?? '';
}
/**

View File

@ -2,20 +2,21 @@
namespace Drupal\facets\Processor;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\facets\FacetInterface;
/**
* A base class for plugins that implements some boilerplate for a widget order.
*/
abstract class SortProcessorPluginBase extends ProcessorPluginBase implements SortProcessorInterface {
abstract class SortProcessorPluginBase extends ProcessorPluginBase implements SortProcessorInterface, CacheableDependencyInterface {
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
$processors = $facet->getProcessors();
$config = isset($processors[$this->getPluginId()]) ? $processors[$this->getPluginId()] : NULL;
$config = $processors[$this->getPluginId()] ?? NULL;
$build['sort'] = [
'#type' => 'radios',

View File

@ -93,7 +93,7 @@ abstract class QueryTypePluginBase extends PluginBase implements QueryTypeInterf
'limit' => $this->facet->getHardLimit(),
'operator' => $this->facet->getQueryOperator(),
'min_count' => $this->facet->getMinCount(),
'missing' => FALSE,
'missing' => $this->facet->isMissing(),
'query_type' => $this->getPluginId(),
];
}

View File

@ -71,9 +71,14 @@ abstract class QueryTypeRangeBase extends QueryTypePluginBase {
foreach ($this->results as $result) {
// Go through the results and add facet results grouped by filters
// defined by self::calculateResultFilter().
if ($result['count'] || $query_operator == 'or') {
if ($result['count'] || $query_operator === 'or') {
$count = $result['count'];
if ($result_filter = $this->calculateResultFilter(trim($result['filter'], '"'))) {
if ($result_filter === 'NULL' || $result_filter === '') {
// "Missing" facet items could not be handled in ranges.
continue;
}
if (isset($facet_results[$result_filter['raw']])) {
$facet_results[$result_filter['raw']]->setCount(
$facet_results[$result_filter['raw']]->getCount() + $count

View File

@ -38,6 +38,20 @@ class Result implements ResultInterface {
*/
protected $count = 0;
/**
* Indicates if this is the additional result item for "missing".
*
* @var bool
*/
protected $missing = FALSE;
/**
* Other filters that might become active if result item isn't "missing".
*
* @var array
*/
protected $missingFilters = [];
/**
* The Url object.
*
@ -106,6 +120,36 @@ class Result implements ResultInterface {
$this->count = (int) $count;
}
/**
* {@inheritdoc}
*/
public function isMissing(): bool {
return $this->missing;
}
/**
* {@inheritdoc}
*/
public function setMissing(bool $missing) {
$this->missing = $missing;
}
/**
* {@inheritdoc}
*/
public function getMissingFilters(): array {
return $this->missingFilters;
}
/**
* {@inheritdoc}
*/
public function setMissingFilters(array $filters) {
$this->missingFilters = array_filter($filters, static function ($filter) {
return $filter !== '!';
});
}
/**
* {@inheritdoc}
*/

View File

@ -49,6 +49,38 @@ interface ResultInterface {
*/
public function setCount($count);
/**
* Set if this result represents the "missing" facet item.
*
* @return bool
* True if this result represents the missing facet item.
*/
public function isMissing(): bool;
/**
* Returns true if this result represents the "missing" facet item.
*
* @param bool $missing
* True if this result represents the missing facet item.
*/
public function setMissing(bool $missing);
/**
* Get the filter values of the non-missing values to be inverted.
*
* @return array
* The filter values of the non-missing values to be inverted.
*/
public function getMissingFilters(): array;
/**
* Set the filter values of the non-missing values to be inverted.
*
* @param array $filters
* The filter values of the non-missing values to be inverted.
*/
public function setMissingFilters(array $filters);
/**
* Returns the url.
*

View File

@ -59,6 +59,14 @@ interface UrlProcessorInterface {
*/
public function getSeparator();
/**
* Returns the multi-value delimiter.
*
* @return string
* A string containing the multi-value delimiter.
*/
public function getDelimiter(): string;
/**
* Returns the active filters.
*

View File

@ -2,6 +2,8 @@
namespace Drupal\facets\UrlProcessor;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\UncacheableDependencyTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\facets\Exception\InvalidProcessorException;
@ -12,8 +14,14 @@ use Symfony\Component\HttpFoundation\Request;
/**
* A base class for plugins that implements most of the boilerplate.
*
* By default all plugins that will extend this class will disable facets
* caching mechanism. It is strongly recommended to turn it on by implementing
* own methods for the CacheableDependencyInterface interface.
*/
abstract class UrlProcessorPluginBase extends ProcessorPluginBase implements UrlProcessorInterface, ContainerFactoryPluginInterface {
abstract class UrlProcessorPluginBase extends ProcessorPluginBase implements UrlProcessorInterface, ContainerFactoryPluginInterface, CacheableDependencyInterface {
use UncacheableDependencyTrait;
/**
* The query string variable.
@ -27,10 +35,18 @@ abstract class UrlProcessorPluginBase extends ProcessorPluginBase implements Url
* The url separator variable.
*
* @var string
* The sepatator to use between field and value.
* The separator to use between field and value.
*/
protected $separator;
/**
* The delimiter for multiple values.
*
* @var string
* The delimiter to use between multiple values.
*/
protected $delimiter = '|';
/**
* The clone of the current request object.
*
@ -68,6 +84,13 @@ abstract class UrlProcessorPluginBase extends ProcessorPluginBase implements Url
return $this->separator;
}
/**
* {@inheritdoc}
*/
public function getDelimiter(): string {
return $this->delimiter;
}
/**
* Constructs a new instance of the class.
*
@ -119,9 +142,7 @@ abstract class UrlProcessorPluginBase extends ProcessorPluginBase implements Url
$configuration,
$plugin_id,
$plugin_definition,
// Support 9.3+.
// @todo remove switch after 9.3 or greater is required.
version_compare(\Drupal::VERSION, '9.3', '>=') ? $request_stack->getMainRequest() : $request_stack->getMasterRequest(),
$request_stack->getMainRequest(),
$container->get('entity_type.manager')
);
}

View File

@ -150,8 +150,8 @@ class FacetsDateHandler {
// Gets gap numbers for both the gap and minimum gap, checks if the next gap
// is within the limit set by the $min_gap parameter.
$gap_num = isset($gap_numbers[$gap]) ? $gap_numbers[$gap] : 6;
$min_num = isset($gap_numbers[$min_gap]) ? $gap_numbers[$min_gap] : 1;
$gap_num = $gap_numbers[$gap] ?? 6;
$min_num = $gap_numbers[$min_gap] ?? 1;
return ($gap_num > $min_num) ? array_search($gap_num - 1, $gap_numbers) : $min_gap;
}
@ -383,8 +383,8 @@ class FacetsDateHandler {
static::FACETS_DATE_SECOND => 1,
];
$gap1_num = isset($gap_numbers[$gap1]) ? $gap_numbers[$gap1] : 6;
$gap2_num = isset($gap_numbers[$gap2]) ? $gap_numbers[$gap2] : 6;
$gap1_num = $gap_numbers[$gap1] ?? 6;
$gap2_num = $gap_numbers[$gap2] ?? 6;
if ($gap1_num == $gap2_num) {
return 0;

View File

@ -3,8 +3,11 @@
namespace Drupal\facets\Utility;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Url;
use Drupal\facets\Result\Result;
use Drupal\facets\UrlProcessor\UrlProcessorPluginManager;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
/**
* Facets Url Generator service.
@ -109,4 +112,69 @@ class FacetsUrlGenerator {
return NULL;
}
/**
* Gets the URL object for a request.
*
* This method statically caches the URL object for a request based on the
* facet source path. This reduces subsequent calls to the processor from
* having to regenerate the URL object.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
* @param string $facet_source_path
* The facet source path.
*
* @return \Drupal\Core\Url
* The URL.
*/
public function getUrlForRequest(Request $request, $facet_source_path = NULL): Url {
/** @var \Drupal\Core\Url[] $requestUrlsByPath */
$requestUrlsByPath = &drupal_static(__CLASS__ . __FUNCTION__, []);
$request_uri = $request->getRequestUri();
if (array_key_exists($request_uri, $requestUrlsByPath)) {
return $requestUrlsByPath[$request_uri];
}
// Try to grab any route params from the original request.
// In case of request path not having a matching route, Url generator will
// fail with.
try {
$requestUrl = Url::createFromRequest($request);
}
catch (ResourceNotFoundException $e) {
// Bypass exception if no path available.
// Should be unreachable in default FacetSource implementations,
// but you never know.
if ($facet_source_path) {
$requestUrl = Url::fromUserInput($facet_source_path, [
'query' => [
'_format' => \Drupal::request()->get('_format'),
],
]);
}
else {
if ('system.404' === $request->attributes->get('_route')) {
// It seems that a facet that is configured to be rendered without its
// facet source is currently rendered on a dedicated "page not found"
// page. If the facet source has a valid path we would not land here
// but in the condition above. So the facet source must be view block
// display or something similar. In this case we could assume that
// such a facet takes care about its link target itself and doesn't
// depend on the current path or the facet source path. Let's provide
// the front page as valid fallback to let the facet do its job.
$requestUrl = Url::fromRoute('<front>');
}
else {
throw $e;
}
}
}
$requestUrl->setOption('attributes', ['rel' => 'nofollow']);
$requestUrlsByPath[$request_uri] = $requestUrl;
return $requestUrl;
}
}

View File

@ -81,12 +81,6 @@ abstract class WidgetPluginBase extends PluginBase implements WidgetPluginInterf
'class' => [$facet->getActiveItems() ? 'facet-active' : 'facet-inactive'],
],
'#context' => !empty($widget['type']) ? ['list_style' => $widget['type']] : [],
'#cache' => [
'contexts' => [
'url.path',
'url.query_args',
],
],
];
}

View File

@ -24,6 +24,20 @@
* @ingroup themeable
*/
#}
{% if cache_hash %}
<!-- facets cacheable metadata
hash: {{ cache_hash }}
{% if cache_contexts %}
contexts: {{ cache_contexts }}
{%- endif %}
{% if cache_tags %}
tags: {{ cache_tags }}
{%- endif %}
{% if cache_max_age %}
max age: {{ cache_max_age }}
{%- endif %}
-->
{%- endif %}
<div class="facets-widget- {{- facet.widget.type -}} ">
{% if facet.widget.type %}
{%- set attributes = attributes.addClass('item-list__' ~ facet.widget.type) %}

View File

@ -5,7 +5,7 @@ package: 'Testing'
hidden: true
core_version_requirement: ^9.2 || ^10.0
# Information added by Drupal.org packaging script on 2022-04-04
version: '2.0.2'
# Information added by Drupal.org packaging script on 2022-07-09
version: '2.0.4'
project: 'facets'
datestamp: 1649070272
datestamp: 1657367472

View File

@ -5,7 +5,7 @@ package: 'Testing'
hidden: true
core_version_requirement: ^9.2 || ^10.0
# Information added by Drupal.org packaging script on 2022-04-04
version: '2.0.2'
# Information added by Drupal.org packaging script on 2022-07-09
version: '2.0.4'
project: 'facets'
datestamp: 1649070272
datestamp: 1657367472

View File

@ -0,0 +1,13 @@
name: 'Facets processors collection'
type: module
description: 'Contains collection of test facet processors'
package: 'Testing'
hidden: true
core_version_requirement: ^9.2 || ^10.0
dependencies:
- facets:facets
# Information added by Drupal.org packaging script on 2022-07-09
version: '2.0.4'
project: 'facets'
datestamp: 1657367472

View File

@ -0,0 +1,19 @@
<?php
/**
* @file
* The facets processors collection module.
*/
/**
* Implements hook_facets_search_api_query_type_mapping_alter().
*/
function facets_processors_collection_facets_search_api_query_type_mapping_alter($backend_plugin_id, array &$query_types) {
if (
!empty($query_types['string'])
&& $query_types['string'] === 'search_api_string'
&& \Drupal::state()->get('facets_processors_collection_alter_string_query_handler', FALSE)
) {
$query_types['string'] = 'search_api_string_cached';
}
}

View File

@ -0,0 +1,28 @@
services:
cache_context.fpc_build:
class: Drupal\facets_processors_collection\Cache\FpcCacheContext
argumets:
type: build
tags:
- { name: cache.context }
cache_context.fpc_sort:
class: Drupal\facets_processors_collection\Cache\FpcCacheContext
argumets:
type: sort
tags:
- { name: cache.context }
cache_context.fpc_post_query:
class: Drupal\facets_processors_collection\Cache\FpcCacheContext
argumets:
type: post_query
tags:
- { name: cache.context }
cache_context.fpc_query_type_plugin:
class: Drupal\facets_processors_collection\Cache\FpcCacheContext
argumets:
type: query_type_plugin
tags:
- { name: cache.context }

View File

@ -0,0 +1,86 @@
<?php
namespace Drupal\facets_processors_collection\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\Context\CacheContextInterface;
use Drupal\facets\Processor\ProcessorInterface;
/**
* Dummy cache context for the fpc_build_processor facet processor.
*
* Cache context IDs: fpc_build, fpc_sort, fpc_post_query.
*/
class FpcCacheContext implements CacheContextInterface {
/**
* Context type: build, sort or post_query.
*
* @var string
*/
protected $type;
/**
* List of facet processing stages.
*
* @var array
*/
protected static $processorStages = [
ProcessorInterface::STAGE_POST_QUERY,
ProcessorInterface::STAGE_BUILD,
ProcessorInterface::STAGE_SORT,
];
/**
* Cache context type used by query type plugin.
*/
protected const QUERY_PLUGIN = 'query_type_plugin';
/**
* FpcCacheContext constructor.
*
* @param string $type
* Context type sort or build.
*/
public function __construct(string $type) {
if (!in_array($type, static::getAllowedTypes())) {
throw new \InvalidArgumentException('Valid types are: ' . implode(', ', static::getAllowedTypes()));
}
$this->type = $type;
}
/**
* Get all allowed context types.
*
* @return array
* Array of context types: all processor stages + query_type plugin.
*/
protected static function getAllowedTypes() {
return array_merge(static::$processorStages, [static::QUERY_PLUGIN]);
}
/**
* {@inheritdoc}
*/
public static function getLabel() {
return t(
'FPC: cache context, cab be one of the following: %stages.',
['%stages' => implode(', ', static::getAllowedTypes())]
);
}
/**
* {@inheritdoc}
*/
public function getContext() {
return 'fpc_' . $this->type;
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata() {
return new CacheableMetadata();
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace Drupal\facets_processors_collection\Plugin\facets\processor;
use Drupal\Core\Cache\Cache;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
use Drupal\facets\Processor\ProcessorPluginBase;
/**
* Dummy build processor plugin to test plugin.manager cacheability.
*
* @FacetsProcessor(
* id = "fpc_build_processor",
* label = @Translation("FPC: Build test processor"),
* description = @Translation("Adds 'test' prefix to each facet item display."),
* stages = {
* "build" = 50
* }
* )
*/
class FpcBuildProcessor extends ProcessorPluginBase implements BuildProcessorInterface {
/**
* {@inheritdoc}
*/
public function build(FacetInterface $facet, array $results) {
/** @var \Drupal\facets\Result\ResultInterface $result */
foreach ($results as $result) {
$result->setDisplayValue('Test ' . $result->getDisplayValue());
}
// An example cache tag that can be added from the ::build().
$facet->addCacheTags(['fpc:added_within_build_method']);
return $results;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return Cache::mergeTags(parent::getCacheTags(), ['fpc:build_processor']);
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return Cache::mergeContexts(parent::getCacheContexts(), ['fpc_build']);
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Drupal\facets_processors_collection\Plugin\facets\processor;
use Drupal\Core\Cache\Cache;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\PostQueryProcessorInterface;
use Drupal\facets\Processor\ProcessorPluginBase;
/**
* Dummy post query processor plugin to test plugin.manager cacheability.
*
* @FacetsProcessor(
* id = "fpc_post_query_processor",
* label = @Translation("FPC: Post query plugin"),
* description = @Translation("Does nothing."),
* stages = {
* "post_query" = 50
* }
* )
*/
class FpcPostQueryProcessor extends ProcessorPluginBase implements PostQueryProcessorInterface {
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return Cache::mergeTags(parent::getCacheTags(), ['fpc:post_query_processor']);
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return Cache::mergeContexts(parent::getCacheContexts(), ['fpc_post_query']);
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*/
public function postQuery(FacetInterface $facet) {
$facet->addCacheTags(['fpc:added_within_postQuery_method']);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Drupal\facets_processors_collection\Plugin\facets\processor;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Form\FormStateInterface;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\SortProcessorPluginBase;
use Drupal\facets\Result\Result;
/**
* A processor that emulates sort plugin, but does nothing with ordering.
*
* @FacetsProcessor(
* id = "fpc_sort_processor",
* label = @Translation("FPC: Sort test processor"),
* description = @Translation("Does nothing."),
* stages = {
* "sort" = 50
* }
* )
*/
class FpcSortProcessor extends SortProcessorPluginBase {
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
return [];
}
/**
* {@inheritdoc}
*/
public function sortResults(Result $a, Result $b) {
return 0;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return ['fpc:sort_processor'];
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['fpc_sort'];
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace Drupal\facets_processors_collection\Plugin\facets\processor;
use Drupal\facets\Result\Result;
/**
* A processor that does "random sort" plugin.
*
* @FacetsProcessor(
* id = "fpc_sort_random_processor",
* label = @Translation("FPC: random sorting"),
* description = @Translation("Randomly sorts result, <em>disables cache</em>"),
* stages = {
* "sort" = 50
* }
* )
*/
class FpcSortRandomProcessor extends FpcSortProcessor {
/**
* {@inheritdoc}
*/
public function sortResults(Result $a, Result $b) {
return random_int(-1, 1);
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
// As sorting should be random, we can't cache results.
return 0;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Drupal\facets_processors_collection\Plugin\facets\query_type;
use Drupal\facets\Plugin\facets\query_type\SearchApiString;
/**
* Extends base search_api_string plugin with custom cacheability.
*
* @FacetsQueryType(
* id = "search_api_string_cached",
* label = @Translation("FPC: cached string"),
* )
*/
class CacheableQueryTypePlugin extends SearchApiString {
/**
* {@inheritdoc}
*/
public function execute() {
parent::execute();
$this->query->addCacheTags(['fpc:query_plugin_type_plugin']);
$this->query->addCacheContexts(['fpc_query_type_plugin']);
}
}

View File

@ -5,7 +5,7 @@ package: 'Testing'
hidden: true
core_version_requirement: ^9.2 || ^10.0
# Information added by Drupal.org packaging script on 2022-04-04
version: '2.0.2'
# Information added by Drupal.org packaging script on 2022-07-09
version: '2.0.4'
project: 'facets'
datestamp: 1649070272
datestamp: 1657367472

View File

@ -0,0 +1,14 @@
services:
cache_context.dummy_query_build:
class: Drupal\facets_query_processor\Cache\DummyQuery
argumets:
type: build
tags:
- { name: cache.context }
cache_context.dummy_query_pre_query:
class: Drupal\facets_query_processor\Cache\DummyQuery
argumets:
type: pre_query
tags:
- { name: cache.context }

View File

@ -2,8 +2,11 @@
namespace Drupal\facets_query_processor\Plugin\facets\url_processor;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\facets\FacetInterface;
use Drupal\facets\Plugin\facets\url_processor\QueryString;
use Drupal\facets\Utility\FacetsUrlGenerator;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
@ -21,10 +24,33 @@ class DummyQuery extends QueryString {
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, Request $request, EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $eventDispatcher) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, Request $request, EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $eventDispatcher, FacetsUrlGenerator $urlGenerator) {
// Override the default separator.
$configuration['separator'] = '||';
parent::__construct($configuration, $plugin_id, $plugin_definition, $request, $entity_type_manager, $eventDispatcher);
parent::__construct($configuration, $plugin_id, $plugin_definition, $request, $entity_type_manager, $eventDispatcher, $urlGenerator);
}
/**
* {@inheritdoc}
*/
public function buildUrls(FacetInterface $facet, array $results) {
$facet->addCacheTags(['dummy_query_build_urls_tag']);
$facet->addCacheContexts(['dummy_query_build']);
return parent::buildUrls($facet, $results);
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return Cache::mergeTags(parent::getCacheTags(), ['dummy_query_pre_query_tag']);
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return Cache::mergeContexts(parent::getCacheContexts(), ['dummy_query_pre_query']);
}
}

View File

@ -1,76 +1,34 @@
base_field: search_api_id
base_table: search_api_index_database_search_index
core: 8.x
description: ''
langcode: en
status: true
dependencies:
config:
- search_api.index.database_search_index
module:
- search_api
id: search_api_test_view
label: 'Search API Test Fulltext search view'
module: views
description: ''
tag: ''
base_table: search_api_index_database_search_index
base_field: search_api_id
display:
default:
display_plugin: default
id: default
display_title: Master
display_plugin: default
position: 0
display_options:
access:
type: none
options: { }
cache:
type: none
options: { }
query:
type: search_api_query
options:
skip_access: true
exposed_form:
type: basic
options:
submit_button: Search
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: full
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 20, 40, 60'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ' previous'
next: 'next '
first: '« first'
last: 'last »'
quantity: 9
style:
type: default
row:
type: search_api
options:
view_modes:
bundle:
'article': default
'page': default
datasource:
'entity:entity_test': default
title: 'Fulltext test index'
fields:
search_api_id:
id: search_api_id
table: search_api_index_database_search_index
field: search_api_id
id: search_api_id
plugin_id: numeric
relationship: none
group_type: group
admin_label: ''
plugin_id: numeric
label: 'Entity ID'
exclude: false
alter:
@ -117,9 +75,81 @@ display:
decimal: .
separator: ','
format_plural: false
format_plural_string: "1\x03@count"
format_plural_string: !!binary MQNAY291bnQ=
prefix: ''
suffix: ''
pager:
type: full
options:
offset: 0
items_per_page: 10
total_pages: null
id: 0
tags:
next: 'next '
previous: ' previous'
first: '« first'
last: 'last »'
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 20, 40, 60'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
quantity: 9
exposed_form:
type: basic
options:
submit_button: Search
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
access:
type: none
options: { }
cache:
type: none
options: { }
empty: { }
sorts:
search_api_id:
id: search_api_id
table: search_api_index_database_search_index
field: search_api_id
relationship: none
group_type: group
admin_label: ''
plugin_id: search_api
order: ASC
expose:
label: ''
field_identifier: search_api_id
exposed: false
arguments:
search_api_datasource:
id: search_api_datasource
table: search_api_index_database_search_index
field: search_api_datasource
plugin_id: search_api
break_phrase: true
type:
id: type
table: search_api_index_database_search_index
field: type
plugin_id: search_api
break_phrase: false
not: true
keywords:
id: keywords
table: search_api_index_database_search_index
field: keywords
plugin_id: search_api
break_phrase: true
filters:
search_api_fulltext:
id: search_api_fulltext
@ -128,6 +158,7 @@ display:
relationship: none
group_type: group
admin_label: ''
plugin_id: search_api_fulltext
operator: and
value: ''
group: 1
@ -138,6 +169,8 @@ display:
description: ''
use_operator: true
operator: search_api_fulltext_op
operator_limit_selection: false
operator_list: { }
identifier: search_api_fulltext
required: false
remember: false
@ -160,14 +193,13 @@ display:
group_items: { }
min_length: 3
fields: { }
plugin_id: search_api_fulltext
id:
plugin_id: search_api_numeric
id: id
table: search_api_index_database_search_index
field: id
relationship: none
admin_label: ''
plugin_id: search_api_numeric
operator: '='
group: 1
exposed: true
@ -177,6 +209,8 @@ display:
description: ''
use_operator: true
operator: id_op
operator_limit_selection: false
operator_list: { }
identifier: id
required: false
remember: false
@ -187,12 +221,12 @@ display:
administrator: '0'
is_grouped: false
created:
plugin_id: search_api_date
id: created
table: search_api_index_database_search_index
field: created
relationship: none
admin_label: ''
plugin_id: search_api_date
operator: '='
group: 1
exposed: true
@ -202,6 +236,8 @@ display:
description: ''
use_operator: true
operator: created_op
operator_limit_selection: false
operator_list: { }
identifier: created
required: false
remember: false
@ -212,12 +248,12 @@ display:
administrator: '0'
is_grouped: false
keywords:
plugin_id: search_api_string
id: keywords
table: search_api_index_database_search_index
field: keywords
relationship: none
admin_label: ''
plugin_id: search_api_string
operator: '='
group: 1
exposed: true
@ -227,6 +263,8 @@ display:
description: ''
use_operator: true
operator: keywords_op
operator_limit_selection: false
operator_list: { }
identifier: keywords
required: false
remember: false
@ -237,13 +275,13 @@ display:
administrator: '0'
is_grouped: false
search_api_language:
plugin_id: search_api_language
id: search_api_language
table: search_api_index_database_search_index
field: search_api_language
relationship: none
admin_label: ''
operator: 'in'
plugin_id: search_api_language
operator: in
group: 1
exposed: true
expose:
@ -252,6 +290,8 @@ display:
description: ''
use_operator: true
operator: language_op
operator_limit_selection: false
operator_list: { }
identifier: language
required: false
remember: false
@ -261,20 +301,22 @@ display:
anonymous: '0'
administrator: '0'
is_grouped: false
sorts:
search_api_id:
id: search_api_id
table: search_api_index_database_search_index
field: search_api_id
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
plugin_id: search_api
title: 'Fulltext test index'
style:
type: default
row:
type: search_api
options:
view_modes:
bundle:
article: default
page: default
datasource:
'entity:entity_test': default
query:
type: search_api_query
options:
skip_access: true
relationships: { }
header:
result:
id: result
@ -283,54 +325,117 @@ display:
relationship: none
group_type: group
admin_label: ''
content: 'Displaying @total search results'
plugin_id: result
content: 'Displaying @total search results'
footer: { }
empty: { }
relationships: { }
arguments:
search_api_datasource:
plugin_id: search_api
id: search_api_datasource
table: search_api_index_database_search_index
field: search_api_datasource
break_phrase: true
type:
plugin_id: search_api
id: type
table: search_api_index_database_search_index
field: type
break_phrase: false
not: true
keywords:
plugin_id: search_api
id: keywords
table: search_api_index_database_search_index
field: keywords
break_phrase: true
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
path: search-api-test-fulltext
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- url
- url.query_args
tags:
- 'config:search_api.index.database_search_index'
block_1:
display_plugin: block
id: block_1
display_title: Block
position: 2
display_plugin: block
position: 3
display_options:
display_extenders: { }
defaults:
use_ajax: false
use_ajax: true
label: 'Search API Test Fulltext search view'
module: views
id: search_api_test_view
tag: ''
langcode: en
dependencies:
module:
- search_api
- facets_search_api_dependency
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- url
- url.query_args
tags:
- 'config:search_api.index.database_search_index'
block_1_sapi_tag:
id: block_1_sapi_tag
display_title: 'Block Search API cache tag'
display_plugin: block
position: 4
display_options:
cache:
type: search_api_tag
options: { }
defaults:
cache: false
use_ajax: false
use_ajax: true
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- url
- url.query_args
tags:
- 'config:search_api.index.database_search_index'
page_1:
id: page_1
display_title: Page
display_plugin: page
position: 1
display_options:
display_extenders: { }
path: search-api-test-fulltext
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- url
- url.query_args
tags:
- 'config:search_api.index.database_search_index'
page_2_sapi_tag:
id: page_2_sapi_tag
display_title: 'Page Search API cache tag'
display_plugin: page
position: 2
display_options:
cache:
type: search_api_tag
options: { }
defaults:
cache: false
display_extenders: { }
path: search-api-test-fulltext-cache-tag
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- url
- url.query_args
tags:
- 'config:search_api.index.database_search_index'
page_2_sapi_time:
id: page_2_sapi_time
display_title: 'Page Search API cache time'
display_plugin: page
position: 2
display_options:
cache:
type: search_api_time
options:
results_lifespan: 21600
results_lifespan_custom: 0
output_lifespan: 518400
output_lifespan_custom: 0
defaults:
cache: false
display_extenders: { }
path: search-api-test-fulltext-cache-time
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- url
- url.query_args
tags:
- 'config:search_api.index.database_search_index'

View File

@ -9,7 +9,7 @@ dependencies:
- drupal:views
core_version_requirement: ^9.2 || ^10.0
# Information added by Drupal.org packaging script on 2022-04-04
version: '2.0.2'
# Information added by Drupal.org packaging script on 2022-07-09
version: '2.0.4'
project: 'facets'
datestamp: 1649070272
datestamp: 1657367472

View File

@ -85,9 +85,7 @@ trait BlockTestTrait {
* The id of the block.
*/
protected function deleteBlock($id) {
// Delete a facet block through the UI, the text for the success message has
// changed in Drupal::VERSION 9.3.
$orig_success_message = 'The block ' . $this->blocks[$id]->label() . ' has been removed' . (\Drupal::VERSION >= 9.3 ? ' from the Footer region' : '') . '.';
$orig_success_message = 'The block ' . $this->blocks[$id]->label() . ' has been removed from the Footer region.';
$this->drupalGet('admin/structure/block/manage/' . $this->blocks[$id]->id(), ['query' => ['destination' => 'admin']]);
$this->clickLink('Remove block');

View File

@ -121,7 +121,7 @@ class BreadcrumbIntegrationTest extends FacetsTestBase {
*/
protected function editFacetConfig(array $config = []) {
$this->drupalGet('admin/config/search/facets');
$this->clickLink('Configure', 1);
$this->clickLink('Configure', 2);
$default_config = [
'filter_key' => 'f',
'url_processor' => 'query_string',

View File

@ -383,7 +383,7 @@ class HierarchicalFacetIntegrationTest extends FacetsTestBase {
*/
public function testHierarchyBreadcrumb() {
$this->drupalGet('admin/config/search/facets');
$this->clickLink('Configure', 1);
$this->clickLink('Configure', 2);
$default_config = [
'filter_key' => 'f',
'url_processor' => 'query_string',

View File

@ -0,0 +1,741 @@
<?php
namespace Drupal\Tests\facets\Functional;
use Drupal\Core\Url;
use Drupal\facets\FacetInterface;
use Drupal\facets\Plugin\facets\query_type\SearchApiDate;
/**
* Tests facets functionality that have search_api view with search_api_cache.
*
* @group facets
*/
class IntegrationCacheTest extends FacetsTestBase {
/**
* Views view url with search_api_tag cache plugin.
*/
protected const VIEW_URL = 'search-api-test-fulltext-cache-tag';
/**
* Views view display id with search_api_tag cache plugin.
*/
protected const VIEW_DISPLAY = 'page_2_sapi_tag';
/**
* {@inheritdoc}
*/
protected static $modules = [
'views',
'node',
'search_api',
'facets',
'block',
'facets_search_api_dependency',
'taxonomy',
'page_cache',
];
/**
* Facets entity storage.
*
* @var \Drupal\Core\Config\Entity\ConfigEntityStorage
*/
protected $facetStorage;
/**
* The entity_test_mulrev_changed entity storage.
*
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
*/
protected $entityTestStorage;
/**
* {@inheritdoc}
*/
public function setUp(): void {
parent::setUp();
$this->enableWebsiteCache();
$this->setUpExampleStructure();
$this->insertExampleContent();
$this->assertEquals(5, $this->indexItems($this->indexId), '5 items were indexed.');
$this->facetStorage = $this->container->get('entity_type.manager')
->getStorage('facets_facet');
$this->entityTestStorage = \Drupal::entityTypeManager()
->getStorage('entity_test_mulrev_changed');
}
/**
* Tests various operations via the Facets' admin UI.
*
* Cached implementation of testBlockView integration test.
*
* @see \Drupal\Tests\facets\Functional\IntegrationTest::testBlockView()
*/
public function testFramework() {
$facet_id = 'test_facet_name';
$this->drupalGet(static::VIEW_URL);
// By default, the view should show all entities.
$this->assertSession()->pageTextContains('Displaying 5 search results');
$this->createFacet('Test Facet name', $facet_id, 'type', static::VIEW_DISPLAY);
// Verify that the facet results are correct.
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->pageTextContains('item');
$this->assertSession()->pageTextContains('article');
// Verify that facet blocks appear as expected.
$this->assertFacetBlocksAppear();
// Verify that the facet only shows when the facet source is visible, it
// should not show up on the user page.
$this->drupalGet('<front>');
$this->assertNoFacetBlocksAppear();
// Do not show the block on empty behaviors.
$this->clearIndex();
$this->drupalGet(static::VIEW_URL);
// Verify that no facet blocks appear. Empty behavior "None" is selected by
// default.
$this->assertNoFacetBlocksAppear();
// Verify that the "empty_text" appears as expected.
$settings = [
'behavior' => 'text',
'text' => 'No results found for this block!',
'text_format' => 'plain_text',
];
$facet = $this->getFacetById($facet_id);
$facet->setEmptyBehavior($settings);
$this->facetStorage->save($facet);
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->responseContains('block-test-facet-name');
$this->assertSession()->responseContains('No results found for this block!');
}
/**
* Tests that a block view also works.
*
* Cached implementation of testBlockView integration test.
*
* @see \Drupal\Tests\facets\Functional\IntegrationTest::testBlockView()
*/
public function testBlockView() {
$webAssert = $this->assertSession();
$this->createFacet(
'Block view facet',
'block_view_facet',
'type',
'block_1_sapi_tag',
'views_block__search_api_test_view'
);
// Place the views block in the footer of all pages.
$block_settings = [
'region' => 'sidebar_first',
'id' => 'view_block',
];
$this->drupalPlaceBlock('views_block:search_api_test_view-block_1_sapi_tag', $block_settings);
// By default, the view should show all entities.
$this->drupalGet('<front>');
$webAssert->pageTextContains('Fulltext test index');
$webAssert->pageTextContains('Displaying 5 search results');
$webAssert->pageTextContains('item');
$webAssert->pageTextContains('article');
// Click the item link, and test that filtering of results actually works.
$this->clickLink('item');
$webAssert->pageTextContains('Displaying 3 search results');
}
/**
* Tests that an url alias works correctly.
*
* Cached implementation of testUrlAlias integration test.
*
* @see \Drupal\Tests\facets\Functional\IntegrationTest::testUrlAlias()
*/
public function testUrlAlias() {
$facet_id = 'ab_facet';
$this->createFacet('ab Facet', $facet_id, 'type', static::VIEW_DISPLAY);
$this->drupalGet(static::VIEW_URL);
$this->assertFacetLabel('item');
$this->assertFacetLabel('article');
$this->clickLink('item');
$url = Url::fromUserInput('/' . static::VIEW_URL, ['query' => ['f' => ['ab_facet:item']]]);
$this->assertSession()->addressEquals($url);
$this->updateFacet($facet_id, ['url_alias' => 'llama']);
$this->drupalGet(static::VIEW_URL);
$this->assertFacetLabel('item');
$this->assertFacetLabel('article');
$this->clickLink('item');
$url = Url::fromUserInput('/' . static::VIEW_URL, ['query' => ['f' => ['llama:item']]]);
$this->assertSession()->addressEquals($url);
}
/**
* Tests facet dependencies.
*
* Cached implementation of testFacetDependencies integration test.
*
* @see \Drupal\Tests\facets\Functional\IntegrationTest::testFacetDependencies()
*/
public function testFacetDependencies() {
$facet_name = "DependableFacet";
$facet_id = 'dependablefacet';
$depending_facet_name = "DependingFacet";
$depending_facet_id = "dependingfacet";
$this->createFacet($facet_name, $facet_id, 'type', static::VIEW_DISPLAY);
$this->createFacet($depending_facet_name, $depending_facet_id, 'keywords', static::VIEW_DISPLAY);
// Go to the view and test that both facets are shown. Item and article
// come from the DependableFacet, orange and grape come from DependingFacet.
$this->drupalGet(static::VIEW_URL);
$this->assertFacetLabel('grape');
$this->assertFacetLabel('orange');
$this->assertFacetLabel('item');
$this->assertFacetLabel('article');
$this->assertFacetBlocksAppear();
// Change the visiblity settings of the DependingFacet.
$facet = $this->getFacetById($depending_facet_id);
$processor = [
'processor_id' => 'dependent_processor',
'weights' => ['build' => 5],
'settings' => [
$facet_id => [
'enable' => TRUE,
'condition' => 'values',
'values' => 'item',
'negate' => FALSE,
],
],
];
$facet->addProcessor($processor);
$this->facetStorage->save($facet);
// Go to the view and test that only the types are shown.
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->linkNotExists('grape');
$this->assertSession()->linkNotExists('orange');
$this->assertFacetLabel('item');
$this->assertFacetLabel('article');
// Click on the item, and test that this shows the keywords.
$this->clickLink('item');
$this->assertFacetLabel('grape');
$this->assertFacetLabel('orange');
// Go back to the view, click on article and test that the keywords are
// hidden.
$this->drupalGet(static::VIEW_URL);
$this->clickLink('article');
$this->assertSession()->linkNotExists('grape');
$this->assertSession()->linkNotExists('orange');
// Change the visibility settings to negate the previous settings.
$processor['settings'][$facet_id]['negate'] = TRUE;
$facet->addProcessor($processor);
$this->facetStorage->save($facet);
// Go to the view and test only the type facet is shown.
$this->drupalGet(static::VIEW_URL);
$this->assertFacetLabel('item');
$this->assertFacetLabel('article');
$this->assertFacetLabel('grape');
$this->assertFacetLabel('orange');
// Click on the article, and test that this shows the keywords.
$this->clickLink('article');
$this->assertFacetLabel('grape');
$this->assertFacetLabel('orange');
// Go back to the view, click on item and test that the keywords are
// hidden.
$this->drupalGet(static::VIEW_URL);
$this->clickLink('item');
$this->assertSession()->linkNotExists('grape');
$this->assertSession()->linkNotExists('orange');
// Disable negation again.
$processor['settings'][$facet_id]['negate'] = FALSE;
$facet->addProcessor($processor);
$this->facetStorage->save($facet);
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->pageTextContains('Displaying 5 search results');
$this->assertSession()->linkNotExists('grape');
$this->clickLink('item');
$this->assertSession()->pageTextContains('Displaying 3 search results');
$this->assertSession()->linkExists('grape');
$this->clickLink('grape');
$this->assertSession()->pageTextContains('Displaying 1 search results');
// Disable item again, and the grape should not be reflected in the search
// result anymore.
$this->clickLink('item');
$this->assertSession()->pageTextContains('Displaying 5 search results');
}
/**
* Tests the facet's and/or functionality.
*
* Cached implementation of testAndOrFacet integration test.
*
* @see \Drupal\Tests\facets\Functional\IntegrationTest::testAndOrFacet()
*/
public function testAndOrFacet() {
$facet_id = 'test_facet';
$this->createFacet('test & facet', $facet_id, 'type', static::VIEW_DISPLAY);
$this->updateFacet($facet_id, ['query_operator' => 'and']);
$this->drupalGet(static::VIEW_URL);
$this->assertFacetLabel('item');
$this->assertFacetLabel('article');
$this->clickLink('item');
$this->checkFacetIsActive('item');
$this->assertSession()->linkNotExists('article');
$this->updateFacet($facet_id, ['query_operator' => 'or']);
$this->drupalGet(static::VIEW_URL);
$this->assertFacetLabel('item');
$this->assertFacetLabel('article');
$this->clickLink('item (3)');
$this->checkFacetIsActive('item');
$this->assertFacetLabel('article (2)');
}
/**
* Tests the facet's exclude functionality.
*
* Cached implementation of testExcludeFacet integration test.
*
* @see \Drupal\Tests\facets\Functional\IntegrationTest::testExcludeFacet()
*/
public function testExcludeFacet() {
$facet_id = 'test_facet';
$this->createFacet('test & facet', $facet_id, 'type', static::VIEW_DISPLAY);
$this->updateFacet($facet_id, ['exclude' => TRUE]);
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->pageTextContains('foo bar baz');
$this->assertSession()->pageTextContains('foo baz');
$this->assertFacetLabel('item');
$this->clickLink('item');
$this->checkFacetIsActive('item');
$this->assertSession()->pageTextContains('foo baz');
$this->assertSession()->pageTextContains('bar baz');
$this->assertSession()->pageTextNotContains('foo bar baz');
$this->updateFacet($facet_id, ['exclude' => FALSE]);
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->pageTextContains('foo bar baz');
$this->assertSession()->pageTextContains('foo baz');
$this->assertFacetLabel('item');
$this->clickLink('item');
$this->checkFacetIsActive('item');
$this->assertSession()->pageTextContains('foo bar baz');
$this->assertSession()->pageTextContains('foo test');
$this->assertSession()->pageTextContains('bar');
$this->assertSession()->pageTextNotContains('foo baz');
}
/**
* Tests the facet's exclude functionality for a date field.
*
* Cached implementation of testExcludeFacetDate integration test.
*
* @see \Drupal\Tests\facets\Functional\IntegrationTest::testExcludeFacetDate()
*/
public function testExcludeFacetDate() {
$facet_id = $field_name = 'created';
$this->entityTestStorage->create([
'name' => 'foo new',
'body' => 'test test',
'type' => 'item',
'keywords' => ['orange'],
'category' => 'item_category',
$field_name => 1490000000,
])->save();
$this->entityTestStorage->create([
'name' => 'foo old',
'body' => 'test test',
'type' => 'item',
'keywords' => ['orange'],
'category' => 'item_category',
$field_name => 1460000000,
])->save();
$this->assertEquals(2, $this->indexItems($this->indexId), '2 items were indexed.');
$this->createFacet('Created', $facet_id, $field_name, static::VIEW_DISPLAY);
$facet = $this->getFacetById($facet_id);
$facet->addProcessor([
'processor_id' => 'date_item',
'weights' => ['build' => 35],
'settings' => [
'date_display' => 'actual_date',
'granularity' => SearchApiDate::FACETAPI_DATE_MONTH,
'hierarchy' => FALSE,
'date_format' => '',
],
]);
$this->facetStorage->save($facet);
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->pageTextContains('foo old');
$this->assertSession()->pageTextContains('foo new');
$this->clickLink('March 2017');
$this->checkFacetIsActive('March 2017');
$this->assertSession()->pageTextContains('foo new');
$this->assertSession()->pageTextNotContains('foo old');
$this->updateFacet($facet->id(), ['exclude' => TRUE]);
$this->drupalGet(static::VIEW_URL);
$this->clickLink('March 2017');
$this->checkFacetIsActive('March 2017');
$this->assertSession()->pageTextContains('foo old');
$this->assertSession()->pageTextNotContains('foo new');
}
/**
* Tests allow only one active item.
*
* Cached implementation of testAllowOneActiveItem integration test.
*
* @see \Drupal\Tests\facets\Functional\IntegrationTest::testAllowOneActiveItem()
*/
public function testAllowOneActiveItem() {
$this->createFacet('Spotted wood owl', 'spotted_wood_owl', 'keywords', static::VIEW_DISPLAY);
$facet = $this->getFacetById('spotted_wood_owl');
$facet->setShowOnlyOneResult(TRUE);
$this->facetStorage->save($facet);
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->pageTextContains('Displaying 5 search results');
$this->assertFacetLabel('grape');
$this->assertFacetLabel('orange');
$this->clickLink('grape');
$this->assertSession()->pageTextContains('Displaying 3 search results');
$this->checkFacetIsActive('grape');
$this->assertFacetLabel('orange');
$this->clickLink('orange');
$this->assertSession()->pageTextContains('Displaying 3 search results');
$this->assertFacetLabel('grape');
$this->checkFacetIsActive('orange');
}
/**
* Tests calculations of facet count.
*
* Cached implementation of testFacetCountCalculations integration test.
*
* @see \Drupal\Tests\facets\Functional\IntegrationTest::testFacetCountCalculations()
*/
public function testFacetCountCalculations() {
$this->createFacet('Type', 'type', 'type', static::VIEW_DISPLAY);
$this->createFacet('Keywords', 'keywords', 'keywords', static::VIEW_DISPLAY);
foreach (['type', 'keywords'] as $facet_id) {
$this->updateFacet($facet_id, ['query_operator' => 'and']);
}
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->pageTextContains('Displaying 5 search results');
$this->assertFacetLabel('article (2)');
$this->assertFacetLabel('grape (3)');
// Make sure that after clicking on article, which has only 2 entities,
// there are only 2 items left in the results for other facets as well.
// In this case, that means we can't have 3 entities tagged with grape. Both
// remaining entities are tagged with grape and strawberry.
$this->clickPartialLink('article');
$this->assertSession()->pageTextNotContains('(3)');
$this->assertFacetLabel('grape (2)');
$this->assertFacetLabel('strawberry (2)');
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->pageTextContains('Displaying 5 search results');
$this->assertFacetLabel('article (2)');
$this->assertFacetLabel('grape (3)');
// Make sure that after clicking on grape, which has only 3 entities, there
// are only 3 items left in the results for other facets as well. In this
// case, that means 2 entities of type article and 1 item.
$this->clickPartialLink('grape');
$this->assertSession()->pageTextContains('Displaying 3 search results');
$this->assertFacetLabel('article (2)');
$this->assertFacetLabel('item (1)');
}
/**
* Tests the hard limit setting.
*
* Cached implementation of testHardLimit integration test.
*
* @see \Drupal\Tests\facets\Functional\IntegrationTest::testHardLimit()
*/
public function testHardLimit() {
$this->createFacet('Owl', 'owl', 'keywords', static::VIEW_DISPLAY);
$facet = $this->getFacetById('owl');
$facet->addProcessor([
'processor_id' => 'active_widget_order',
'weights' => ['sort' => 20],
'settings' => [],
]);
$facet->addProcessor([
'processor_id' => 'display_value_widget_order',
'weights' => ['build' => 40],
'settings' => [],
]);
$this->facetStorage->save($facet);
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->pageTextContains('Displaying 5 search results');
$this->assertFacetLabel('grape (3)');
$this->assertFacetLabel('orange (3)');
$this->assertFacetLabel('apple (2)');
$this->assertFacetLabel('banana (1)');
$this->assertFacetLabel('strawberry (2)');
$this->updateFacet($facet->id(), ['hard_limit' => 3]);
$this->drupalGet(static::VIEW_URL);
// We're still testing for 5 search results here, the hard limit only limits
// the facets, not the search results.
$this->assertSession()->pageTextContains('Displaying 5 search results');
$this->assertFacetLabel('grape (3)');
$this->assertFacetLabel('orange (3)');
$this->assertFacetLabel('apple (2)');
$this->assertSession()->pageTextNotContains('banana (0)');
$this->assertSession()->pageTextNotContains('strawberry (0)');
}
/**
* Test minimum amount of items.
*
* Cached implementation of testMinimumAmount integration test.
*
* @see \Drupal\Tests\facets\Functional\IntegrationTest::testMinimumAmount()
*/
public function testMinimumAmount() {
$this->createFacet('Elf owl', 'elf_owl', 'type', static::VIEW_DISPLAY);
$this->updateFacet('elf_owl', ['min_count' => 1]);
// See that both article and item are showing.
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->pageTextContains('Displaying 5 search results');
$this->assertFacetLabel('article (2)');
$this->assertFacetLabel('item (3)');
$this->updateFacet('elf_owl', ['min_count' => 3]);
// See that article is now hidden, item should still be showing.
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->pageTextContains('Displaying 5 search results');
$this->assertSession()->pageTextNotContains('article');
$this->assertFacetLabel('item (3)');
}
/**
* Tests the visibility of facet source.
*
* Cached implementation of testFacetSourceVisibility integration test.
*
* @see \Drupal\Tests\facets\Functional\IntegrationTest::testFacetSourceVisibility()
*/
public function testFacetSourceVisibility() {
$this->createFacet('Vicuña', 'vicuna', 'type', static::VIEW_DISPLAY);
// Facet appears only on the search page for which it was created.
$this->drupalGet(static::VIEW_URL);
$this->assertFacetBlocksAppear();
$this->drupalGet('');
$this->assertNoFacetBlocksAppear();
$facet = $this->getFacetById('vicuna');
$facet->setOnlyVisibleWhenFacetSourceIsVisible(FALSE);
$this->facetStorage->save($facet);
// Test that the facet source is visible on the search page and user/2 page.
$this->drupalGet(static::VIEW_URL);
$this->assertFacetBlocksAppear();
$this->drupalGet('');
$this->assertFacetBlocksAppear();
}
/**
* Tests behavior with multiple enabled facets and their interaction.
*
* Cached implementation of testMultipleFacets integration test.
*
* @see \Drupal\Tests\facets\Functional\IntegrationTest::testMultipleFacets()
*/
public function testMultipleFacets() {
// Create 2 facets.
$this->createFacet('Snow Owl', 'snow_owl', 'type', static::VIEW_DISPLAY);
$this->createFacet('Forest Owl', 'forest_owl', 'category', static::VIEW_DISPLAY);
foreach (['snow_owl', 'forest_owl'] as $facet_id) {
$this->updateFacet($facet_id, ['min_count' => 0]);
}
// Go to the view and check the default behavior.
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->pageTextContains('Displaying 5 search results');
$this->assertFacetLabel('item (3)');
$this->assertFacetLabel('article (2)');
$this->assertFacetLabel('item_category (2)');
$this->assertFacetLabel('article_category (2)');
// Start filtering.
$this->clickPartialLink('item_category');
$this->assertSession()->pageTextContains('Displaying 2 search results');
$this->checkFacetIsActive('item_category');
$this->assertFacetLabel('item (2)');
// Go back to the overview and start another filter, from the second facet
// block this time.
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->pageTextContains('Displaying 5 search results');
$this->clickPartialLink('article (2)');
$this->assertSession()->pageTextContains('Displaying 2 search results');
$this->checkFacetIsActive('article');
$this->assertFacetLabel('article_category (2)');
$this->assertFacetLabel('item_category (0)');
}
/**
* Tests that the configuration for showing a title works.
*
* Cached implementation of testShowTitle integration test.
*
* @see \Drupal\Tests\facets\Functional\IntegrationTest::testShowTitle()
*/
public function testShowTitle() {
$this->createFacet('Llama', 'llama', 'type', static::VIEW_DISPLAY);
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->pageTextNotContains('Llama');
$this->updateFacet('llama', ['show_title' => TRUE]);
$this->drupalGet(static::VIEW_URL);
$this->assertSession()->responseContains('<h3>Llama</h3>');
$this->assertSession()->pageTextContains('Llama');
}
/**
* Test facet blocks cache invalidation.
*
* Test covers search page with a facets and standalone facet block on FP.
*/
public function testFacetBlockCacheNewContentIndexing() {
$this->createFacet('Test Facet name', 'test_facet_name', 'type', static::VIEW_DISPLAY);
$facet = $this->getFacetById('test_facet_name');
$facet->setOnlyVisibleWhenFacetSourceIsVisible(FALSE);
$this->facetStorage->save($facet);
foreach (['', static::VIEW_URL] as $url) {
$this->drupalGet($url);
$this->assertFacetLabel('article (2)');
$this->assertFacetLabel('item (3)');
}
$this->entityTestStorage->create([
'name' => 'foo jiz baz',
'body' => 'test test and a bit more test',
'type' => 'item',
'keywords' => ['orange', 'black'],
'category' => 'item_category',
])->save();
// Entity was added but not indexed yet, so facet state should remain the
// same.
foreach (['', static::VIEW_URL] as $url) {
$this->drupalGet($url);
$this->assertFacetLabel('article (2)');
$this->assertFacetLabel('item (3)');
}
// Index 1 remaining item and check that count has been updated.
$this->assertEquals(1, $this->indexItems($this->indexId), '1 item was indexed.');
foreach (['', static::VIEW_URL] as $url) {
$this->drupalGet($url);
$this->assertFacetLabel('article (2)');
$this->assertFacetLabel('item (4)');
}
}
/**
* Enable website page caching, set 1 day max age.
*/
protected function enableWebsiteCache() {
$max_age = 86400;
$this->config('system.performance')
->set('cache.page.max_age', $max_age)
->save();
$this->drupalGet(static::VIEW_URL);
$this->assertSession()
->responseHeaderContains('Cache-Control', 'max-age=' . $max_age);
}
/**
* Get facet entity by ids.
*
* @param string $id
* Facet id.
*
* @return \Drupal\facets\FacetInterface
* Loaded facet object.
*/
protected function getFacetById(string $id): FacetInterface {
return $this->facetStorage->load($id);
}
/**
* Update facet tith with given values.
*
* @param string $id
* The facet entity ID.
* @param array $settings
* Array with values keyed by property names.
*
* @return \Drupal\facets\FacetInterface
* An updated facet entity.
*/
protected function updateFacet(string $id, array $settings): FacetInterface {
$facet = $this->getFacetById($id);
foreach ($settings as $name => $value) {
$facet->set($name, $value);
}
$this->facetStorage->save($facet);
return $facet;
}
}

Some files were not shown because too many files have changed in this diff Show More