diff --git a/facets.install b/facets.install index 8a5b4e6..794d5c8 100644 --- a/facets.install +++ b/facets.install @@ -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(). @@ -174,17 +175,8 @@ function facets_update_8005() { * Update facet blocks configuration with a block id used for AJAX support. */ function facets_update_8006() { - $query = \Drupal::entityQuery('block') - ->condition('plugin', 'facet_block', 'STARTS_WITH') - ->execute(); - - foreach ($query as $block_id) { - $block = Block::load($block_id); - $configuration = $block->get('settings'); - $configuration['block_id'] = $block_id; - $block->set('settings', $configuration); - $block->save(); - } + // Empty update hook, we do not support this anymore. + // @see https://www.drupal.org/project/facets/issues/3073444 } /** @@ -216,3 +208,37 @@ function facets_update_8009() { $facet->save(); } } + +/** + * 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(); + } + } + if (!empty($processed_views)) { + return t( + 'Facet caching was enabled for the following views: %views.', + ['%views' => implode(', ', $processed_views)] + ); + } + else { + return t('There are no views with search API cache plugins and facets in the same time, so nothing has been updated.'); + } +} diff --git a/facets.module b/facets.module index bc7d27e..e619677 100644 --- a/facets.module +++ b/facets.module @@ -14,6 +14,7 @@ use Drupal\Core\Url; use Drupal\facets\Entity\Facet; use Drupal\facets\Entity\FacetSource; use Drupal\facets\FacetInterface; +use Drupal\search_api\Entity\Index; use Drupal\views\Entity\View; use Drupal\Core\Entity\EntityInterface; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; @@ -66,6 +67,11 @@ function facets_theme($existing, $type, $theme, $path) { 'context' => [], ], ], + 'facets_views_plugin' => [ + 'variables' => [ + 'content' => [], + ], + ], ]; } @@ -365,3 +371,23 @@ function facets_theme_suggestions_facets_result_item(array $variables) { } return $suggestions; } + +/** + * Implements hook_views_data_alter(). + */ +function facets_views_data_alter(array &$data) { + + /** @var \Drupal\search_api\IndexInterface $index */ + foreach (Index::loadMultiple() as $index) { + $data['search_api_index_' . $index->id()]['facets'] = [ + 'title' => t('Facets'), + 'help' => t('Displays facets in a filter or area.'), + 'filter' => [ + 'id' => 'facets_filter', + ], + 'area' => [ + 'id' => 'facets_area', + ], + ]; + } +} diff --git a/facets.routing.yml b/facets.routing.yml index 9f26911..1e897e7 100644 --- a/facets.routing.yml +++ b/facets.routing.yml @@ -48,10 +48,3 @@ entity.facets_facet_source.edit_form: _title: 'Edit facet source configuration' requirements: _entity_create_access: 'facets_facet' - -facets.block.ajax: - path: '/facets-block-ajax' - defaults: - _controller: '\Drupal\facets\Controller\FacetBlockAjaxController::ajaxFacetBlockView' - requirements: - _access: 'TRUE' diff --git a/facets.services.yml b/facets.services.yml index 103f94a..7c31490 100644 --- a/facets.services.yml +++ b/facets.services.yml @@ -24,6 +24,7 @@ services: - '@plugin.manager.facets.facet_source' - '@plugin.manager.facets.processor' - '@entity_type.manager' + - '@current_route_match' facets.utility.date_handler: class: Drupal\facets\Utility\FacetsDateHandler arguments: @@ -38,6 +39,10 @@ services: arguments: ['@plugin.manager.block'] tags: - { name: event_subscriber } + facets.route_alter: + class: \Drupal\facets\EventSubscriber\RouteAlterSubscriber + tags: + - { name: event_subscriber } facets.search_api_subscriber: class: Drupal\facets\EventSubscriber\SearchApiSubscriber arguments: ['@facets.manager'] diff --git a/js/base-widget.js b/js/base-widget.js index 8ca3e6c..3af9e8b 100644 --- a/js/base-widget.js +++ b/js/base-widget.js @@ -24,7 +24,7 @@ * .once('my-custom-widget-on-change') * .on('change', function () { * // In this example $(this).val() will provide needed URL. - * $(this).trigger('facets_filter', [ $(this).val() ]); + * $(this).trigger('facets_filter', [ $(this).val(), false ]); * }); * * The facets module will trigger "facets_filtering" before filter is diff --git a/modules/facets_summary/facets_summary.module b/modules/facets_summary/facets_summary.module index 6720862..d2505eb 100644 --- a/modules/facets_summary/facets_summary.module +++ b/modules/facets_summary/facets_summary.module @@ -5,6 +5,8 @@ * Hook implementations for the facets summary module. */ +use Drupal\search_api\Entity\Index; + /** * Implements hook_theme(). */ @@ -98,3 +100,23 @@ function facets_summary_theme_suggestions_facets_summary_item_list(array $variab function facets_summary_preprocess_facets_summary_item_list(array &$variables) { template_preprocess_item_list($variables); } + +/** + * Implements hook_views_data_alter(). + */ +function facets_summary_views_data_alter(array &$data) { + + /** @var \Drupal\search_api\IndexInterface $index */ + foreach (Index::loadMultiple() as $index) { + $data['search_api_index_' . $index->id()]['facets_summary'] = [ + 'title' => t('Facets summary'), + 'help' => t('Displays facets summary in a filter or area'), + 'filter' => [ + 'id' => 'facets_summary_filter', + ], + 'area' => [ + 'id' => 'facets_summary_area', + ], + ]; + } +} diff --git a/modules/facets_summary/src/Form/FacetsSummarySettingsForm.php b/modules/facets_summary/src/Form/FacetsSummarySettingsForm.php index 2b19def..d61295e 100644 --- a/modules/facets_summary/src/Form/FacetsSummarySettingsForm.php +++ b/modules/facets_summary/src/Form/FacetsSummarySettingsForm.php @@ -245,9 +245,12 @@ class FacetsSummarySettingsForm extends EntityForm { if (isset($facet_source) && $facet_source instanceof SearchApiFacetSourceInterface) { $view = $facet_source->getViewsDisplay(); if ($view !== NULL) { - $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' && strpos($views_cache_type, 'search_api_') === FALSE) { + $view->display_handler->overrideOption('cache', ['type' => 'none']); + $view->save(); + \Drupal::messenger()->addMessage($this->t('Caching of view %view has been disabled.', ['%view' => $view->storage->label()])); + } } } diff --git a/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlock.php b/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlock.php index e805c5e..9990aa1 100644 --- a/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlock.php +++ b/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlock.php @@ -5,7 +5,6 @@ namespace Drupal\facets_summary\Plugin\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Cache\UncacheableDependencyTrait; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Core\Url; use Drupal\facets_summary\Entity\FacetsSummary; use Drupal\facets_summary\FacetsSummaryBlockInterface; use Drupal\facets_summary\FacetsSummaryManager\DefaultFacetsSummaryManager; @@ -102,18 +101,6 @@ class FacetsSummaryBlock extends BlockBase implements FacetsSummaryBlockInterfac ]; } - /** @var \Drupal\views\ViewExecutable $view */ - if ($view = $facets_summary->getFacetSource()->getViewsDisplay()) { - $build['#attached']['drupalSettings']['facets_views_ajax'] = [ - 'facets_summary_ajax' => [ - 'facets_summary_id' => $facets_summary->id(), - 'view_id' => $view->id(), - 'current_display_id' => $view->current_display, - 'ajax_path' => Url::fromRoute('views.ajax')->toString(), - ], - ]; - } - return $build; } diff --git a/modules/facets_summary/src/Plugin/facets_summary/processor/HideWhenNotRenderedProcessor.php b/modules/facets_summary/src/Plugin/facets_summary/processor/HideWhenNotRenderedProcessor.php index 2f41f67..4780b3c 100644 --- a/modules/facets_summary/src/Plugin/facets_summary/processor/HideWhenNotRenderedProcessor.php +++ b/modules/facets_summary/src/Plugin/facets_summary/processor/HideWhenNotRenderedProcessor.php @@ -2,6 +2,8 @@ namespace Drupal\facets_summary\Plugin\facets_summary\processor; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Routing\RouteMatchInterface; use Drupal\facets_summary\FacetsSummaryInterface; use Drupal\facets_summary\Processor\BuildProcessorInterface; use Drupal\facets_summary\Processor\ProcessorPluginBase; @@ -19,7 +19,44 @@ use Drupal\facets_summary\Processor\ProcessorPluginBase; * } * ) */ -class HideWhenNotRenderedProcessor extends ProcessorPluginBase implements BuildProcessorInterface { +class HideWhenNotRenderedProcessor extends ProcessorPluginBase implements BuildProcessorInterface, ContainerFactoryPluginInterface { + + /** + * The current route match. + * + * @var \Drupal\Core\Routing\RouteMatchInterface + */ + protected $routeMatch; + + /** + * Constructs a new object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The current route match. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteMatchInterface $route_match = NULL) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + $this->routeMatch = $route_match; + } + + /** + * {@inheritdoc} + */ + public static function create(\Symfony\Component\DependencyInjection\ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('current_route_match') + ); + } /** * {@inheritdoc} diff --git a/modules/facets_summary/src/Plugin/views/FacetsSummaryViewsPluginTrait.php b/modules/facets_summary/src/Plugin/views/FacetsSummaryViewsPluginTrait.php new file mode 100644 index 0000000..8f58b36 --- /dev/null +++ b/modules/facets_summary/src/Plugin/views/FacetsSummaryViewsPluginTrait.php @@ -0,0 +1,93 @@ +facetSummaryStorage->loadMultiple(); + + $format = 'search_api:views_%s__%s__%s'; + $source = sprintf($format, $this->view->getDisplay()->getPluginId(), $this->view->id(), $this->view->current_display); + foreach ($facets_summaries as $facets_summary) { + if ($facets_summary->getFacetSourceId() === $source) { + $options[$facets_summary->id()] = $facets_summary->label(); + } + } + + $form['facet_summary'] = [ + '#title' => 'Facet summary', + '#options' => $options, + '#type' => 'radios', + '#required' => TRUE, + '#default_value' => isset($this->options['facet_summary']) ? $this->options['facet_summary'] : [], + ]; + } + + /** + * Gets the facets summary to render. + * + * @return array + * A summary of the facets being used. + */ + public function FacetsViewsGetFacetSummary() { + $build = []; + + /** @var \Drupal\facets_summary\Entity\FacetsSummary $summary */ + $summary = $this->facetSummaryStorage->load($this->options['facet_summary']); + if ($summary) { + $facet_summary = $this->facetSummaryManager->build($summary); + if (!empty($facet_summary)) { + $summary_build = [ + '#theme' => 'block', + '#configuration' => [ + 'provider' => 'facets_summary', + 'label' => $summary->label(), + 'label_display' => TRUE, + ], + '#id' => $summary->id(), + '#plugin_id' => 'facet_summary_block:' . $summary->id(), + '#base_plugin_id' => 'facet_block', + '#derivative_plugin_id' => $summary->id(), + '#weight' => 0, + '#cache' => [ + 'contexts' => [], + 'tags' => [], + 'max-age' => 0, + ], + 'content' => $facet_summary, + ]; + } + } + + if (!empty($summary_build)) { + $build = [ + '#theme' => 'facets_views_plugin', + '#content' => $summary_build, + ]; + + if ($this->view->getDisplay()->ajaxEnabled()) { + $build['#attached']['library'][] = 'facets/drupal.facets.views-ajax'; + } + } + + return $build; + } + +} diff --git a/modules/facets_summary/src/Plugin/views/area/FacetsSummaryArea.php b/modules/facets_summary/src/Plugin/views/area/FacetsSummaryArea.php new file mode 100644 index 0000000..0773387 --- /dev/null +++ b/modules/facets_summary/src/Plugin/views/area/FacetsSummaryArea.php @@ -0,0 +1,103 @@ +facetSummaryManager = $facet_summary_manager; + $this->facetSummaryStorage = $facet_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('facets_summary.manager'), + $container->get('entity_type.manager')->getStorage('facets_summary') + ); + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + // Set the default to TRUE so it shows on empty pages by default. + $options['empty']['default'] = TRUE; + $options['facet_summary'] = ['default' => '']; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $this->FacetsSummaryiewsBuildOptionsForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function adminSummary() { + return ''; + } + + /** + * {@inheritdoc} + */ + public function render($empty = FALSE) { + return $this->FacetsViewsGetFacetSummary(); + } + +} diff --git a/modules/facets_summary/src/Plugin/views/filter/FacetsSummaryFilter.php b/modules/facets_summary/src/Plugin/views/filter/FacetsSummaryFilter.php new file mode 100644 index 0000000..a7fbb23 --- /dev/null +++ b/modules/facets_summary/src/Plugin/views/filter/FacetsSummaryFilter.php @@ -0,0 +1,143 @@ +facetSummaryManager = $facet_summary_manager; + $this->facetSummaryStorage = $facet_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('facets_summary.manager'), + $container->get('entity_type.manager')->getStorage('facets_summary') + ); + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $random = new Random(); + $options = parent::defineOptions(); + $options['exposed'] = ['default' => TRUE]; + $options['expose']['contains']['identifier'] = ['default' => 'facet_summary_' . $random->name()]; + $options['facet_summary']['default'] = ''; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $this->FacetsSummaryiewsBuildOptionsForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function adminSummary() { + return ''; + } + + /** + * {@inheritdoc} + */ + public function valueForm(&$form, FormStateInterface $form_state) { + static $is_processing = NULL; + + if ($is_processing) { + $form['value'] = []; + return; + } + + $is_processing = TRUE; + $form['value'] = $this->FacetsViewsGetFacetSummary(); + $is_processing = FALSE; + } + + /** + * {@inheritdoc} + */ + public function acceptExposedInput($input) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function validateExposeForm($form, FormStateInterface $form_state) {} + + /** + * {@inheritdoc} + */ + public function canGroup() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function query() {} + +} diff --git a/src/Controller/FacetsViewsAjaxController.php b/src/Controller/FacetsViewsAjaxController.php new file mode 100644 index 0000000..3f60abe --- /dev/null +++ b/src/Controller/FacetsViewsAjaxController.php @@ -0,0 +1,25 @@ +facetsRemoveQueryParams($request); + $response = parent::ajaxView($request); + return $this->addExposedBlockToResponse($response); + } + +} diff --git a/src/Controller/FacetsViewsAjaxGetController.php b/src/Controller/FacetsViewsAjaxGetController.php new file mode 100644 index 0000000..756ff39 --- /dev/null +++ b/src/Controller/FacetsViewsAjaxGetController.php @@ -0,0 +1,25 @@ +facetsRemoveQueryParams($request); + $response = parent::ajaxView($request); + return $this->addExposedBlockToResponse($response); + } + +} diff --git a/src/Entity/Facet.php b/src/Entity/Facet.php index f615f35..6dd4b2e 100644 --- a/src/Entity/Facet.php +++ b/src/Entity/Facet.php @@ -1063,6 +1063,13 @@ class Facet extends ConfigEntityBase implements FacetInterface { if (!$update) { self::clearBlockCache(); } + // @todo move into condition above when this issue + // https://www.drupal.org/project/search_api/issues/3197050 will be fixed. + // Right now it clears the search api results and views cache by re-saving a + // views view config. + if ($source = $this->getFacetSource()) { + $this->getFacetSource()->registerFacet($this); + } } /** diff --git a/src/EventSubscriber/RouteAlterSubscriber.php b/src/EventSubscriber/RouteAlterSubscriber.php new file mode 100644 index 0000000..54cae76 --- /dev/null +++ b/src/EventSubscriber/RouteAlterSubscriber.php @@ -0,0 +1,39 @@ +getRouteCollection(); + if ($route = $collection->get('views.ajax')) { + $controller = $route->getDefault('_controller'); + // Views AJAX get support. + if ($controller == '\Drupal\views_ajax_get\Controller\ViewsAjaxController::ajaxView') { + $route->setDefault('_controller', '\Drupal\facets\Controller\FacetsViewsAjaxGetController::ajaxView'); + } + else { + $route->setDefault('_controller', '\Drupal\facets\Controller\FacetsViewsAjaxController::ajaxView'); + } + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[RoutingEvents::ALTER][] = ['onRouteAlter', -100]; + return $events; + } + +} diff --git a/src/FacetManager/DefaultFacetManager.php b/src/FacetManager/DefaultFacetManager.php index b3e93df..f2e5fc7 100644 --- a/src/FacetManager/DefaultFacetManager.php +++ b/src/FacetManager/DefaultFacetManager.php @@ -2,7 +2,11 @@ namespace Drupal\facets\FacetManager; +use Drupal\Core\Cache\CacheableDependencyInterface; +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Cache\RefinableCacheableDependencyInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Routing\CurrentRouteMatch; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\facets\Exception\InvalidProcessorException; use Drupal\facets\FacetInterface; @@ -78,23 +82,34 @@ class DefaultFacetManager { */ protected $builtFacets = []; + /** + * @var \Drupal\Core\Routing\CurrentRouteMatch + */ + private $routeMatch; + /** * Constructs a new instance of the DefaultFacetManager. * - * @param \Drupal\facets\QueryType\QueryTypePluginManager $query_type_plugin_manager + * @param \Drupal\facets\QueryType\QueryTypePluginManager $query_type_plugin_manager * The query type plugin manager. - * @param \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_manager + * @param \Drupal\facets\FacetSource\FacetSourcePluginManager $facet_source_manager * The facet source plugin manager. - * @param \Drupal\facets\Processor\ProcessorPluginManager $processor_plugin_manager + * @param \Drupal\facets\Processor\ProcessorPluginManager $processor_plugin_manager * The processor plugin manager. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type plugin manager. + * @param \Drupal\Core\Routing\CurrentRouteMatch $route_match + * The current route match. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ - public function __construct(QueryTypePluginManager $query_type_plugin_manager, FacetSourcePluginManager $facet_source_manager, ProcessorPluginManager $processor_plugin_manager, EntityTypeManagerInterface $entity_type_manager) { + public function __construct(QueryTypePluginManager $query_type_plugin_manager, FacetSourcePluginManager $facet_source_manager, ProcessorPluginManager $processor_plugin_manager, EntityTypeManagerInterface $entity_type_manager, CurrentRouteMatch $route_match) { $this->queryTypePluginManager = $query_type_plugin_manager; $this->facetSourcePluginManager = $facet_source_manager; $this->processorPluginManager = $processor_plugin_manager; $this->facetStorage = $entity_type_manager->getStorage('facets_facet'); + $this->routeMatch = $route_match; } /** @@ -103,17 +118,21 @@ class DefaultFacetManager { * This method is called by the implementing module to initialize the facet * display process. * - * @param mixed $query + * @param mixed $query * The backend's native query object. - * @param string $facetsource_id + * @param string $facetsource_id * The facet source ID to process. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException */ 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 +145,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([]); @@ -156,6 +174,10 @@ class DefaultFacetManager { ] ); $query_type_plugin->execute(); + // Merge cache medata that gathered from facet and its processors. + if ($query_is_cacheable) { + $query->addCacheableDependency($facet); + } } } @@ -172,11 +194,12 @@ class DefaultFacetManager { /** * Returns currently rendered facets filtered by facetsource ID. * - * @param string $facetsource_id + * @param string $facetsource_id * The facetsource ID to filter by. * * @return \Drupal\facets\FacetInterface[] * An array of enabled facets. + * @throws \Drupal\facets\Exception\InvalidProcessorException */ public function getFacetsByFacetSourceId($facetsource_id) { // Immediately initialize the facets. @@ -235,7 +258,11 @@ class DefaultFacetManager { throw new InvalidProcessorException("The processor {$processor->getPluginDefinition()['id']} has a post_query definition but doesn't implement the required PostQueryProcessor interface"); } $post_query_processor->postQuery($facet); + if ($post_query_processor instanceof CacheableDependencyInterface) { + $facet->addCacheableDependency($post_query_processor); + } } + $this->processedFacets[$facetsource_id][$facet->id()] = $facet; } } @@ -263,6 +290,9 @@ class DefaultFacetManager { throw new InvalidProcessorException("The processor {$processor->getPluginDefinition()['id']} has a pre_query definition but doesn't implement the required PreQueryProcessorInterface interface"); } $pre_query_processor->preQuery($facet); + if ($processor instanceof CacheableDependencyInterface) { + $facet->addCacheableDependency($processor); + } } } } @@ -311,12 +341,18 @@ class DefaultFacetManager { throw new InvalidProcessorException("The processor {$processor->getPluginDefinition()['id']} has a build definition but doesn't implement the required BuildProcessorInterface interface"); } $results = $processor->build($facet, $results); + if ($processor instanceof CacheableDependencyInterface) { + $facet->addCacheableDependency($processor); + } } // Trigger sort stage. $active_sort_processors = []; foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_SORT) as $processor) { $active_sort_processors[] = $processor; + if ($processor instanceof CacheableDependencyInterface) { + $facet->addCacheableDependency($processor); + } } // Sort the actual results if we have enabled sort processors. @@ -369,7 +405,7 @@ class DefaultFacetManager { /** @var \Drupal\facets\Widget\WidgetPluginInterface $widget */ $widget = $facet->getWidgetInstance(); $build = $widget->build($facet); - + CacheableMetadata::createFromObject($facet)->applyTo($build); // No results behavior handling. Return a custom text or false depending on // settings. if (empty($facet->getResults())) { @@ -392,10 +428,9 @@ class DefaultFacetManager { ]; } else { - // If the facet has no results, but it is being rendered trough ajax we - // should render a container (that is empty). This is because the - // javascript needs to be able to find a div to replace with the new - // content. + // If the facet has no results, but it is being rendered trough AJAX it + // should render an empty container. This is because the JavaScript + // needs to be able to find a div to replace with the new content. return [ [ 0 => $build, @@ -415,8 +450,10 @@ class DefaultFacetManager { /** * Updates all facets of a given facet source with the raw results. * - * @param string $facetsource_id + * @param string $facetsource_id * The facet source ID of the currently processed facet. + * + * @throws \Drupal\facets\Exception\InvalidProcessorException|\Drupal\Component\Plugin\Exception\PluginException */ public function updateResults($facetsource_id) { $facets = $this->getFacetsByFacetSourceId($facetsource_id); @@ -438,11 +475,12 @@ class DefaultFacetManager { * Keep in mind that if you want to have the facet's build processor executed, * call returnBuiltFacet() instead. * - * @param \Drupal\facets\FacetInterface $facet + * @param \Drupal\facets\FacetInterface $facet * The facet to process. * * @return \Drupal\facets\FacetInterface|null * The updated facet if it exists, NULL otherwise. + * @throws \Drupal\facets\Exception\InvalidProcessorException */ public function returnProcessedFacet(FacetInterface $facet) { $this->processFacets($facet->getFacetSourceId()); @@ -452,11 +490,12 @@ class DefaultFacetManager { /** * Returns one of the built facets. * - * @param \Drupal\facets\FacetInterface $facet + * @param \Drupal\facets\FacetInterface $facet * The facet to process. * * @return \Drupal\facets\FacetInterface * The built facet. + * @throws \Drupal\facets\Exception\InvalidProcessorException */ public function returnBuiltFacet(FacetInterface $facet) { return $this->processBuild($facet); diff --git a/src/FacetSource/FacetSourcePluginBase.php b/src/FacetSource/FacetSourcePluginBase.php index 9a470f8..4141dd9 100644 --- a/src/FacetSource/FacetSourcePluginBase.php +++ b/src/FacetSource/FacetSourcePluginBase.php @@ -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; @@ -24,6 +25,7 @@ use Drupal\facets\QueryType\QueryTypePluginManager; * @see plugin_api */ abstract class FacetSourcePluginBase extends PluginBase implements FacetSourcePluginInterface, ContainerFactoryPluginInterface { + use UncacheableDependencyTrait; /** * The plugin manager. @@ -153,4 +155,10 @@ abstract class FacetSourcePluginBase extends PluginBase implements FacetSourcePl $this->facet->setFieldIdentifier($field_identifier); } + /** + * {@inheritdoc} + */ + public function registerFacet(FacetInterface $facet) { + } + } diff --git a/src/FacetSource/FacetSourcePluginInterface.php b/src/FacetSource/FacetSourcePluginInterface.php index 1b97101..d8eca29 100644 --- a/src/FacetSource/FacetSourcePluginInterface.php +++ b/src/FacetSource/FacetSourcePluginInterface.php @@ -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,15 @@ 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 + * it will be invalidated whenever facet preferences will change. + * + * @param \Drupal\facets\FacetInterface $facet + * Facet that being inserted or updated. + */ + public function registerFacet(FacetInterface $facet); + } diff --git a/src/FacetsViewsAjaxTrait.php b/src/FacetsViewsAjaxTrait.php new file mode 100644 index 0000000..d27f100 --- /dev/null +++ b/src/FacetsViewsAjaxTrait.php @@ -0,0 +1,75 @@ +getMethod() === Request::METHOD_POST) { + foreach (['f', 'page'] as $key) { + if ($request->query->has($key)) { + $request->query->remove($key); + } + } + } + } + + /** + * Adds the exposed form to the response if necessary. + * + * @param mixed $response + * The AJAX response. This is not type hinted because this can either be + * a ViewsAjaxResponse or a CacheableViewsAjaxResponse. + * + * @return mixed + * An empty array, the output from Views, or the response object. + */ + public function addExposedBlockToResponse($response) { + $view = $response->getView(); + $display = $view->getDisplay(); + if ($display->getOption('exposed_block') && $display->usesExposedFormInBlock()) { + + $context = new RenderContext(); + $exposed_block = $this->renderer->executeInRenderContext($context, function () use ($view) { + $output = $view->display_handler->viewExposedFormBlocks(); + if (is_array($output) && !empty($output)) { + return $output; + } + + return []; + }); + + if (!$context->isEmpty() && !empty($exposed_block)) { + $bubbleable_metadata = $context->pop(); + BubbleableMetadata::createFromRenderArray($exposed_block) + ->merge($bubbleable_metadata) + ->applyTo($exposed_block); + } + + // Replace exposed block. + $selector = 'views-exposed-form-' . strtr($view->id(), '_', '-') . '-' . strtr($view->current_display, '_', '-'); + $response->addCommand(new ReplaceCommand("#" . $selector, $exposed_block)); + } + + return $response; + } + +} diff --git a/src/Form/FacetSettingsForm.php b/src/Form/FacetSettingsForm.php index 3000147..4632292 100644 --- a/src/Form/FacetSettingsForm.php +++ b/src/Form/FacetSettingsForm.php @@ -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()])); + } } } diff --git a/src/Plugin/Block/FacetBlock.php b/src/Plugin/Block/FacetBlock.php index ac3b076..12bcabd 100644 --- a/src/Plugin/Block/FacetBlock.php +++ b/src/Plugin/Block/FacetBlock.php @@ -3,6 +3,8 @@ namespace Drupal\facets\Plugin\Block; use Drupal\Core\Block\BlockBase; +use Drupal\Core\Cache\CacheableDependencyInterface; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Form\FormStateInterface; @@ -105,54 +107,85 @@ class FacetBlock extends BlockBase implements ContainerFactoryPluginInterface { else { $build['#attributes']['class'][] = 'facet-inactive'; } - - // Add classes needed for ajax. - if (!empty($build['#use_ajax'])) { - $build['#attributes']['class'][] = 'block-facets-ajax'; - // The configuration block id isn't always set in the configuration. - if (isset($this->configuration['block_id'])) { - $build['#attributes']['class'][] = 'js-facet-block-id-' . $this->configuration['block_id']; - } - else { - $build['#attributes']['class'][] = 'js-facet-block-id-' . $this->pluginId; - } - } } return $build; } + /** + * Get cache metadata from the facet source plugin. + * + * @return \Drupal\Core\Cache\CacheableMetadata + * Facet block cache metadata. + */ + protected function getCacheMetadata() { + $facet = $this->facetStorage->load($this->getDerivativeId()); + $metadata = (new CacheableMetadata()) + ->setCacheTags($facet->getCacheTags()) + ->setCacheContexts($facet->getCacheContexts()); + if ( + ($source = $facet->getFacetSource()) + && $source instanceof CacheableDependencyInterface + ) { + $metadata = $metadata->merge( + (new CacheableMetadata()) + ->setCacheTags($source->getCacheTags()) + ->setCacheContexts($source->getCacheContexts()) + )->setCacheMaxAge($source->getCacheMaxAge()); + } + else { + // 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's + // not considered worth the effort to optimize this just for Drupal core's + // Search. + $metadata->setCacheMaxAge(0); + } + + return $metadata; + } + /** * {@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->getCacheMetadata()->getCacheMaxAge(); + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + return $this->getCacheMetadata()->getCacheTags(); + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return $this->getCacheMetadata()->getCacheContexts(); } /** diff --git a/src/Plugin/facets/facet_source/SearchApiDisplay.php b/src/Plugin/facets/facet_source/SearchApiDisplay.php index 57911d5..4cd3a30 100644 --- a/src/Plugin/facets/facet_source/SearchApiDisplay.php +++ b/src/Plugin/facets/facet_source/SearchApiDisplay.php @@ -3,6 +3,7 @@ namespace Drupal\facets\Plugin\facets\facet_source; use Drupal\Component\Plugin\DependentPluginInterface; +use Drupal\Core\Cache\Cache; use Drupal\Core\Extension\ModuleHandler; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; @@ -146,6 +147,45 @@ class SearchApiDisplay extends FacetSourcePluginBase implements SearchApiFacetSo return $this->getDisplay()->getPath(); } + /** + * Helper function to get arguments for views contextual filters. + * + * @return array + * Values of contextual filters. + */ + private function extractArgumentsForViewDisplay(): array { + $argumentValues = []; + // For AJAX requests we cannot take the value the same way as for non-AJAX + // requests because route is identified as Drupal AJAX and views arguments + // are removed by Views. + if ($this->request->isXmlHttpRequest()) { + $argumentValues = explode('/', $_REQUEST['view_args']); + } + else { + $display = $this->getViewsDisplay()->getDisplay(); + + // Display plugin which have a path, i.e. pages. + // @see \Drupal\views\Plugin\views\display\PathPluginBase + if ($display->hasPath()) { + $viewUrlParameters = $display->getUrl()->getRouteParameters(); + if (!empty($viewUrlParameters)) { + $parameters = []; + foreach ($viewUrlParameters as $viewUrlParameter => $validator) { + $parameters[] = $this->request->attributes->has($viewUrlParameter) ? $this->request->attributes->get($viewUrlParameter) : NULL; + } + + // Add view parameters as arguments only if at least one of them + // resolved to a value, otherwise let views handle the defaults. + if (!empty(array_filter($parameters))) { + $argumentValues = array_merge($argumentValues, $parameters); + } + } + } + // @todo Support other plugin types. + } + return $argumentValues; + } + /** * {@inheritdoc} */ @@ -164,6 +204,8 @@ class SearchApiDisplay extends FacetSourcePluginBase implements SearchApiFacetSo if ($results === NULL && isset($display_definition['view_id'])) { $view = Views::getView($display_definition['view_id']); $view->setDisplay($display_definition['view_display']); + $view->setArguments($this->extractArgumentsForViewDisplay()); + $view->preExecute(); $view->execute(); $results = $this->searchApiQueryHelper->getResults($search_id); } @@ -387,21 +429,6 @@ class SearchApiDisplay extends FacetSourcePluginBase implements SearchApiFacetSo if ($view === NULL) { return $build; } - - // Add JS for Views with Ajax Enabled. - if ($view->display_handler->ajaxEnabled()) { - $js_settings = [ - 'view_id' => $view->id(), - 'current_display_id' => $view->current_display, - 'view_base_path' => ltrim($view->getPath() ?? '', '/'), - 'ajax_path' => Url::fromRoute('views.ajax')->toString(), - ]; - $build['#attached']['library'][] = 'facets/drupal.facets.views-ajax'; - $build['#attached']['drupalSettings']['facets_views_ajax'] = [ - $this->facet->id() => $js_settings, - ]; - $build['#use_ajax'] = TRUE; - } return $build; } @@ -418,4 +445,46 @@ class SearchApiDisplay extends FacetSourcePluginBase implements SearchApiFacetSo } } + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return $this->getViewsDisplay() + ->getDisplay() + ->getCacheMetadata() + ->getCacheContexts(); + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + $view_display = $this->getViewsDisplay()->getDisplay(); + return Cache::mergeTags( + $view_display->getCacheMetadata()->getCacheTags(), + $view_display->getPlugin('cache')->getCacheTags() + ); + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + $view_display = $this->getViewsDisplay()->getDisplay(); + return Cache::mergeMaxAges( + $view_display->getCacheMetadata()->getCacheMaxAge(), + $view_display->getPlugin('cache')->getCacheMaxAge() + ); + } + + /** + * {@inheritdoc} + */ + public function registerFacet(FacetInterface $facet) { + // Alter views view cache metadata. + // @see \Drupal\search_api\Plugin\views\cache\SearchApiCachePluginTrait::generateResultsKey() + // @see \Drupal\views\Plugin\views\cache\CachePluginBase::alterCacheMetadata() + $this->getViewsDisplay()->save(); + } + } diff --git a/src/Plugin/facets/hierarchy/Taxonomy.php b/src/Plugin/facets/hierarchy/Taxonomy.php index 129c0e0..cc04ee2 100644 --- a/src/Plugin/facets/hierarchy/Taxonomy.php +++ b/src/Plugin/facets/hierarchy/Taxonomy.php @@ -2,6 +2,7 @@ namespace Drupal\facets\Plugin\facets\hierarchy; +use Drupal\Core\Cache\Cache; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\facets\Hierarchy\HierarchyPluginBase; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -197,4 +198,11 @@ class Taxonomy extends HierarchyPluginBase { return $this->termParents[$tid] = reset($parents)->id(); } + /** + * {@inheritdoc} + */ + public function getCacheTags() { + return Cache::mergeTags(parent::getCacheTags(), ['taxonomy_term:list']); + } + } diff --git a/src/Plugin/facets/processor/CombineFacetProcessor.php b/src/Plugin/facets/processor/CombineFacetProcessor.php index 40603eb..a51dbbf 100644 --- a/src/Plugin/facets/processor/CombineFacetProcessor.php +++ b/src/Plugin/facets/processor/CombineFacetProcessor.php @@ -148,7 +148,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 +161,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; diff --git a/src/Plugin/facets/processor/TermWeightWidgetOrderProcessor.php b/src/Plugin/facets/processor/TermWeightWidgetOrderProcessor.php index 2993893..5168ea6 100644 --- a/src/Plugin/facets/processor/TermWeightWidgetOrderProcessor.php +++ b/src/Plugin/facets/processor/TermWeightWidgetOrderProcessor.php @@ -2,6 +2,7 @@ namespace Drupal\facets\Plugin\facets\processor; +use Drupal\Core\Cache\Cache; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\TypedData\ComplexDataDefinitionInterface; @@ -126,4 +127,11 @@ class TermWeightWidgetOrderProcessor extends SortProcessorPluginBase implements return FALSE; } + /** + * {@inheritdoc} + */ + public function getCacheTags() { + return Cache::mergeTags(parent::getCacheTags(), ['taxonomy_term:list']); + } + } diff --git a/src/Plugin/facets/processor/TranslateEntityAggregatedFieldProcessor.php b/src/Plugin/facets/processor/TranslateEntityAggregatedFieldProcessor.php index efc4c18..09cbd6c 100644 --- a/src/Plugin/facets/processor/TranslateEntityAggregatedFieldProcessor.php +++ b/src/Plugin/facets/processor/TranslateEntityAggregatedFieldProcessor.php @@ -167,7 +167,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()); } diff --git a/src/Plugin/facets/processor/TranslateEntityProcessor.php b/src/Plugin/facets/processor/TranslateEntityProcessor.php index dff609f..5786703 100644 --- a/src/Plugin/facets/processor/TranslateEntityProcessor.php +++ b/src/Plugin/facets/processor/TranslateEntityProcessor.php @@ -128,7 +128,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()); } diff --git a/src/Plugin/facets/processor/UidToUserNameCallbackProcessor.php b/src/Plugin/facets/processor/UidToUserNameCallbackProcessor.php index 254037d..0eaf4c5 100644 --- a/src/Plugin/facets/processor/UidToUserNameCallbackProcessor.php +++ b/src/Plugin/facets/processor/UidToUserNameCallbackProcessor.php @@ -34,6 +34,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; } } diff --git a/src/Plugin/facets/processor/UrlProcessorHandler.php b/src/Plugin/facets/processor/UrlProcessorHandler.php index 92f8655..54aceda 100644 --- a/src/Plugin/facets/processor/UrlProcessorHandler.php +++ b/src/Plugin/facets/processor/UrlProcessorHandler.php @@ -2,6 +2,7 @@ namespace Drupal\facets\Plugin\facets\processor; +use Drupal\Core\Cache\Cache; use Drupal\facets\Exception\InvalidProcessorException; use Drupal\facets\FacetInterface; use Drupal\facets\Processor\BuildProcessorInterface; @@ -82,4 +83,14 @@ class UrlProcessorHandler extends ProcessorPluginBase implements BuildProcessorI $this->processor->setActiveItems($facet); } + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return Cache::mergeContexts( + parent::getCacheContexts(), + ['url.path', 'url.query_args'] + ); + } + } diff --git a/src/Plugin/views/FacetsViewsPluginTrait.php b/src/Plugin/views/FacetsViewsPluginTrait.php new file mode 100644 index 0000000..4566d1b --- /dev/null +++ b/src/Plugin/views/FacetsViewsPluginTrait.php @@ -0,0 +1,96 @@ +facetStorage->loadMultiple(); + + $format = 'search_api:views_%s__%s__%s'; + $source = sprintf($format, $this->view->getDisplay()->getPluginId(), $this->view->id(), $this->view->current_display); + foreach ($facets as $facet) { + if ($facet->getFacetSourceId() === $source) { + $options[$facet->id()] = $facet->label(); + } + } + + $form['facets'] = [ + '#title' => 'Facets', + '#options' => $options, + '#type' => 'checkboxes', + '#required' => TRUE, + '#default_value' => isset($this->options['facets']) ? $this->options['facets'] : [], + ]; + } + + /** + * Gets the facets to render. + * + * @return array + * The facet blocks to be output, in render array format. + */ + public function FacetsViewsGetFacets() { + $build = []; + + /** @var \Drupal\facets\Entity\Facet[] $facets */ + $items = []; + $facets = $this->facetStorage->loadMultiple(array_filter($this->options['facets'])); + foreach ($facets as $facet) { + $facet_build = $this->facetManager->build($facet); + if (!empty($facet_build)) { + $facet_source = $facet->getFacetSource(); + $facet_build += $facet_source->buildFacet(); + $items[] = [ + '#theme' => 'block', + '#configuration' => [ + 'provider' => 'facets', + 'label' => $facet->label(), + 'label_display' => TRUE, + ], + '#id' => $facet->id(), + '#plugin_id' => 'facet_block:' . $facet->id(), + '#base_plugin_id' => 'facet_block', + '#derivative_plugin_id' => $facet->id(), + '#weight' => $facet->getWeight(), + '#cache' => [ + 'contexts' => [], + 'tags' => [], + 'max-age' => 0, + ], + 'content' => $facet_build, + ]; + } + } + + if (!empty($items)) { + $build = [ + '#theme' => 'facets_views_plugin', + '#content' => $items, + ]; + + if ($this->view->getDisplay()->ajaxEnabled()) { + $build['#attached']['library'][] = 'facets/drupal.facets.views-ajax'; + } + } + + return $build; + } + +} diff --git a/src/Plugin/views/area/FacetsArea.php b/src/Plugin/views/area/FacetsArea.php new file mode 100644 index 0000000..026d181 --- /dev/null +++ b/src/Plugin/views/area/FacetsArea.php @@ -0,0 +1,103 @@ +facetManager = $facet_manager; + $this->facetStorage = $facet_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('facets.manager'), + $container->get('entity_type.manager')->getStorage('facets_facet') + ); + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + // Set the default to TRUE so it shows on empty pages by default. + $options['empty']['default'] = TRUE; + $options['facets'] = ['default' => []]; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $this->FacetsViewsBuildOptionsForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function adminSummary() { + return implode(', ', array_filter($this->options['facets'])); + } + + /** + * {@inheritdoc} + */ + public function render($empty = FALSE) { + return $this->FacetsViewsGetFacets(); + } + +} diff --git a/src/Plugin/views/filter/FacetsFilter.php b/src/Plugin/views/filter/FacetsFilter.php new file mode 100644 index 0000000..4993ec0 --- /dev/null +++ b/src/Plugin/views/filter/FacetsFilter.php @@ -0,0 +1,143 @@ +facetManager = $facet_manager; + $this->facetStorage = $facet_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('facets.manager'), + $container->get('entity_type.manager')->getStorage('facets_facet') + ); + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $random = new Random(); + $options = parent::defineOptions(); + $options['exposed'] = ['default' => TRUE]; + $options['expose']['contains']['identifier'] = ['default' => 'facet_' . $random->name()]; + $options['facets']['default'] = []; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $this->FacetsViewsBuildOptionsForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function adminSummary() { + return implode(', ', array_filter($this->options['facets'])); + } + + /** + * {@inheritdoc} + */ + public function valueForm(&$form, FormStateInterface $form_state) { + static $is_processing = NULL; + + if ($is_processing) { + $form['value'] = []; + return; + } + + $is_processing = TRUE; + $form['value'] = $this->FacetsViewsGetFacets(); + $is_processing = FALSE; + } + + /** + * {@inheritdoc} + */ + public function acceptExposedInput($input) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function validateExposeForm($form, FormStateInterface $form_state) {} + + /** + * {@inheritdoc} + */ + public function canGroup() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function query() {} + +} diff --git a/src/Processor/ProcessorPluginBase.php b/src/Processor/ProcessorPluginBase.php index 5b3f3e5..1d0727e 100644 --- a/src/Processor/ProcessorPluginBase.php +++ b/src/Processor/ProcessorPluginBase.php @@ -2,6 +2,8 @@ namespace Drupal\facets\Processor; +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Entity\DependencyTrait; use Drupal\Core\Plugin\PluginBase; @@ -10,7 +12,7 @@ use Drupal\facets\FacetInterface; /** * A base class for plugins that implements most of the boilerplate. */ -class ProcessorPluginBase extends PluginBase implements ProcessorInterface { +class ProcessorPluginBase extends PluginBase implements ProcessorInterface, CacheableDependencyInterface { use DependencyTrait; @@ -116,4 +118,25 @@ class ProcessorPluginBase extends PluginBase implements ProcessorInterface { return NULL; } + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return []; + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + return []; + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + return Cache::PERMANENT; + } + } diff --git a/src/Widget/WidgetPluginBase.php b/src/Widget/WidgetPluginBase.php index 2cbfb1f..7bbda4b 100644 --- a/src/Widget/WidgetPluginBase.php +++ b/src/Widget/WidgetPluginBase.php @@ -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', - ], - ], ]; } diff --git a/templates/facets-views-plugin.html.twig b/templates/facets-views-plugin.html.twig new file mode 100644 index 0000000..ce4f5d0 --- /dev/null +++ b/templates/facets-views-plugin.html.twig @@ -0,0 +1,3 @@ +