diff --git a/core_search_facets/src/Plugin/facets/facet_source/CoreNodeSearchFacetSource.php b/core_search_facets/src/Plugin/facets/facet_source/CoreNodeSearchFacetSource.php index 0ad5523..bdb3717 100644 --- a/core_search_facets/src/Plugin/facets/facet_source/CoreNodeSearchFacetSource.php +++ b/core_search_facets/src/Plugin/facets/facet_source/CoreNodeSearchFacetSource.php @@ -322,4 +322,20 @@ class CoreNodeSearchFacetSource extends FacetSourcePluginBase implements CoreSea return FALSE; } + + /** + * {@inheritdoc} + */ + public function supportsFeature($feature_name) { + + switch ($feature_name) { + case 'facets_or': + return FALSE; + break; + } + + // Make sure a new feature is automatically not supported. + return FALSE; + } + } diff --git a/src/Entity/Facet.php b/src/Entity/Facet.php index 6e3de73..acfe6fe 100644 --- a/src/Entity/Facet.php +++ b/src/Entity/Facet.php @@ -162,6 +162,13 @@ class Facet extends ConfigEntityBase implements FacetInterface { */ protected $results = []; + /** + * The results. + * + * @var \Drupal\facets\Result\ResultInterface[] + */ + protected $unfiltered_results = []; + protected $active_values = []; /** @@ -255,6 +262,14 @@ class Facet extends ConfigEntityBase implements FacetInterface { /** * {@inheritdoc} */ + protected function urlRouteParameters($rel) { + $parameters = parent::urlRouteParameters($rel); + return $parameters; + } + + /** + * {@inheritdoc} + */ public function getDescription() { return $this->description; } @@ -270,11 +285,51 @@ class Facet extends ConfigEntityBase implements FacetInterface { /** * {@inheritdoc} */ + public function getQueryTypes() { + return $this->query_type_name; + } + + /** + * {@inheritdoc} + */ public function getWidget() { return $this->widget; } /** + * Retrieves all processors supported by this facet. + * + * @return \Drupal\facets\Processor\ProcessorInterface[] + * The loaded processors, keyed by processor ID. + */ + protected function loadProcessors() { + if (!isset($this->processors)) { + /* @var $processor_plugin_manager \Drupal\facets\Processor\ProcessorPluginManager */ + $processor_plugin_manager = \Drupal::service('plugin.manager.facets.processor'); + $processor_settings = $this->getOption('processors', []); + + foreach ($processor_plugin_manager->getDefinitions() as $name => $processor_definition) { + if (class_exists($processor_definition['class']) && empty($this->processors[$name])) { + // Create our settings for this processor. + $settings = empty($processor_settings[$name]['settings']) ? [] : $processor_settings[$name]['settings']; + $settings['facet'] = $this; + + /* @var $processor \Drupal\facets\Processor\ProcessorInterface */ + $processor = $processor_plugin_manager->createInstance($name, $settings); + $this->processors[$name] = $processor; + } + elseif (!class_exists($processor_definition['class'])) { + \Drupal::logger('facets') + ->warning('Processor @id specifies a non-existing @class.', array( + '@id' => $name, + '@class' => $processor_definition['class'] + )); + } + } + } + + return $this->processors; + } /** * {@inheritdoc} */ public function getQueryType() { @@ -292,6 +347,13 @@ class Facet extends ConfigEntityBase implements FacetInterface { /** * {@inheritdoc} */ + public function getQueryOperator() { + return $this->getOption('query_operator', 'OR'); + } + + /** + * {@inheritdoc} + */ public function getFieldAlias() { // For now, create the field alias based on the field identifier. $field_alias = preg_replace('/[:\/]+/', '_', $this->field_identifier); @@ -359,12 +421,7 @@ class Facet extends ConfigEntityBase implements FacetInterface { return $this; } - /** - * {@inheritdoc} - */ - public function getQueryTypes() { - return $this->query_type_name; - } + /** * {@inheritdoc} @@ -459,44 +516,6 @@ class Facet extends ConfigEntityBase implements FacetInterface { } /** - * Retrieves all processors supported by this facet. - * - * @return \Drupal\facets\Processor\ProcessorInterface[] - * The loaded processors, keyed by processor ID. - */ - protected function loadProcessors() { - if (!isset($this->processors)) { - /* @var $processor_plugin_manager \Drupal\facets\Processor\ProcessorPluginManager */ - $processor_plugin_manager = \Drupal::service('plugin.manager.facets.processor'); - - foreach ($processor_plugin_manager->getDefinitions() as $processor_id => $processor_definition) { - if (class_exists($processor_definition['class']) && empty($this->processors[$processor_id])) { - $settings = empty($this->processor_configs[$processor_id]['settings']) ? [] : $this->processor_configs[$processor_id]['settings']; - $settings['enabled'] = empty($this->processor_configs[$processor_id]) ? FALSE : TRUE; - $settings['facet'] = $this; - - /* @var $processor \Drupal\facets\Processor\ProcessorInterface */ - $processor = $processor_plugin_manager->createInstance($processor_id, $settings); - $this->processors[$processor_id] = $processor; - } - elseif (!class_exists($processor_definition['class'])) { - \Drupal::logger('facets')->warning('Processor @id specifies a non-existing @class.', array('@id' => $processor_id, '@class' => $processor_definition['class'])); - } - } - } - - return $this->processors; - } - - /** - * {@inheritdoc} - */ - protected function urlRouteParameters($rel) { - $parameters = parent::urlRouteParameters($rel); - return $parameters; - } - - /** * {@inheritdoc} */ public function getResults() { @@ -522,6 +541,20 @@ class Facet extends ConfigEntityBase implements FacetInterface { /** * {@inheritdoc} */ + public function setUnfilteredResults(array $all_results = []) { + $this->unfiltered_results = $all_results; + } + + /** + * {@inheritdoc} + */ + public function getUnfilteredResults() { + return $this->unfiltered_results; + } + + /** + * {@inheritdoc} + */ public function isActiveValue($value) { $is_active = FALSE; if (in_array($value, $this->active_values)) { @@ -550,7 +583,11 @@ class Facet extends ConfigEntityBase implements FacetInterface { $this->facetSourcePlugins[$name] = $facet_source; } elseif (!class_exists($facet_source_definition['class'])) { - \Drupal::logger('facets')->warning('Facet Source @id specifies a non-existing @class.', ['@id' => $name, '@class' => $facet_source_definition['class']]); + \Drupal::logger('facets') + ->warning('Facet Source @id specifies a non-existing @class.', [ + '@id' => $name, + '@class' => $facet_source_definition['class'] + ]); } } } diff --git a/src/FacetInterface.php b/src/FacetInterface.php index 0f13cbe..1d86289 100644 --- a/src/FacetInterface.php +++ b/src/FacetInterface.php @@ -126,6 +126,24 @@ interface FacetInterface extends ConfigEntityInterface { */ public function setResults(array $results); + /** + * Sets an array of unfiltered results. + * + * These unfiltered results are used to set the correct count of the actual + * facet results when using the OR query operator. They are not results value + * objects like those in ::$results. + * + * @param array + * Unfiltered results. + */ + public function setUnfilteredResults(array $all_results = []); + + /** + * Gets an array of unfiltered results. + * + * @return array + */ + public function getUnfilteredResults(); /** * Get the query type instance. @@ -136,6 +154,14 @@ interface FacetInterface extends ConfigEntityInterface { public function getQueryType(); /** + * Get the query operator. + * + * @return string + * The query operator being used. + */ + public function getQueryOperator(); + + /** * Get the plugin name for the url processor. * * @return string diff --git a/src/FacetManager/DefaultFacetManager.php b/src/FacetManager/DefaultFacetManager.php index 688f9ad..c3345a6 100644 --- a/src/FacetManager/DefaultFacetManager.php +++ b/src/FacetManager/DefaultFacetManager.php @@ -159,8 +159,14 @@ class DefaultFacetManager { // Make sure we don't alter queries for facets with a different source. if ($facet->getFacetSourceId() == $this->facetSourceId) { /** @var \Drupal\facets\QueryType\QueryTypeInterface $query_type_plugin */ - $query_type_plugin = $this->queryTypePluginManager->createInstance($facet->getQueryType(), ['query' => $query, 'facet' => $facet]); - $query_type_plugin->execute(); + $query_type_plugin = $this->queryTypePluginManager + ->createInstance($facet->getQueryType(), ['query' => $query, 'facet' => $facet]); + $unfiltered_results = $query_type_plugin->execute(); + + // Save unfiltered results in facet. + if (!is_null($unfiltered_results)) { + $facet->setUnfilteredResults($unfiltered_results); + } } } } diff --git a/src/FacetSource/FacetSourcePluginInterface.php b/src/FacetSource/FacetSourcePluginInterface.php index 976f3dd..2e26d8b 100644 --- a/src/FacetSource/FacetSourcePluginInterface.php +++ b/src/FacetSource/FacetSourcePluginInterface.php @@ -115,4 +115,15 @@ interface FacetSourcePluginInterface { */ public function getSearchKeys(); + /** + * Checks the support for a feature. + * + * @param string $feature_name + * Feature we should check support for. + * + * @return bool + * Checks the support for a feature. + */ + public function supportsFeature($feature_name); + } diff --git a/src/Form/FacetDisplayForm.php b/src/Form/FacetDisplayForm.php index 56b5cda..6315e77 100644 --- a/src/Form/FacetDisplayForm.php +++ b/src/Form/FacetDisplayForm.php @@ -352,6 +352,15 @@ class FacetDisplayForm extends EntityForm { '#default_value' => isset($empty_behavior_config['text_format']) ? $empty_behavior_config['text'] : '', ]; + // Query operator. + $form['facet_settings']['query_operator'] = [ + '#type' => 'radios', + '#title' => $this->t('Operator'), + '#options' => ['OR' => $this->t('OR'), 'AND' => $this->t('AND')], + '#description' => $this->t('AND filters are exclusive and narrow the result set. OR filters are inclusive and widen the result set.'), + '#default_value' => $facet->getQueryOperator(), + ]; + $form['weights'] = array( '#type' => 'details', '#title' => t('Advanced settings'), @@ -527,6 +536,8 @@ class FacetDisplayForm extends EntityForm { } $facet->setEmptyBehavior($empty_behavior_config); + $facet->setOption('query_operator', $form_state->getValue(['facet_settings', 'query_operator'])); + $facet->save(); drupal_set_message(t('Facet %name has been updated.', ['%name' => $facet->getName()])); } diff --git a/src/Plugin/facets/facet_source/SearchApiBaseFacetSource.php b/src/Plugin/facets/facet_source/SearchApiBaseFacetSource.php index 74e98c8..1ace654 100644 --- a/src/Plugin/facets/facet_source/SearchApiBaseFacetSource.php +++ b/src/Plugin/facets/facet_source/SearchApiBaseFacetSource.php @@ -6,6 +6,7 @@ namespace Drupal\facets\Plugin\facets\facet_source; +use Drupal\Component\Plugin\Exception\PluginException; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\facets\Exception\InvalidQueryTypeException; @@ -163,4 +164,18 @@ abstract class SearchApiBaseFacetSource extends FacetSourcePluginBase { return $query_types; } + /** + * {@inheritdoc} + */ + public function supportsFeature($feature_name) { + if (is_null($this->index)) { + throw new PluginException('No index found for facet source.'); + } + if (is_null($this->index->getServer())) { + throw new PluginException('No server found for facet source.'); + } + + return $this->index->getServer()->supportsFeature($feature_name); + } + } diff --git a/src/Plugin/facets/query_type/SearchApiString.php b/src/Plugin/facets/query_type/SearchApiString.php index c44d0bb..eb2a484 100644 --- a/src/Plugin/facets/query_type/SearchApiString.php +++ b/src/Plugin/facets/query_type/SearchApiString.php @@ -41,11 +41,33 @@ class SearchApiString extends QueryTypePluginBase { public function execute() { $query = $this->query; - // Alter the query here. - if (!empty($query)) { - $options = &$query->getOptions(); + $unfiltered_results = []; + // Only alter the query when there's an actual query object to alter. + if (!empty($query)) { + $operator = $this->facet->getQueryOperator(); $field_identifier = $this->facet->getFieldIdentifier(); + + // Copy the query object so we can do an unfiltered query. We need to have + // this unfiltered results to make sure that the count of a facet is + // correct. The unfiltered results get returned to the facet manager, the + // facet manager will save it on facet::unfiltered_results. + $unfiltered_query = $query; + $unfiltered_options = &$unfiltered_query->getOptions(); + $unfiltered_options['search_api_facets'][$field_identifier] = array( + 'field' => $field_identifier, + 'limit' => 50, + 'operator' => 'and', + 'min_count' => 0, + 'missing' => FALSE, + ); + $unfiltered_results = $unfiltered_query + ->execute() + ->getExtraData('search_api_facets'); + + + // Set the options for the actual query. + $options = &$query->getOptions(); $options['search_api_facets'][$field_identifier] = array( 'field' => $field_identifier, 'limit' => 50, @@ -56,25 +78,36 @@ class SearchApiString extends QueryTypePluginBase { // Add the filter to the query if there are active values. $active_items = $this->facet->getActiveItems(); + if (count($active_items)) { + $filter = $query->createConditionGroup($operator); foreach ($active_items as $value) { - $filter = $query->createConditionGroup(); $filter->addCondition($this->facet->getFieldIdentifier(), $value); - $query->addConditionGroup($filter); } + $query->addConditionGroup($filter); } } + + return $unfiltered_results; } /** * {@inheritdoc} */ public function build() { + $query_operator = $this->facet->getQueryOperator(); + if (!empty($this->results)) { $facet_results = array(); - foreach ($this->results as $result) { - if ($result['count']) { - $facet_results[] = new Result(trim($result['filter'], '"'), trim($result['filter'], '"'), $result['count']); + foreach ($this->results as $key => $result) { + if ($result['count'] || $query_operator == 'OR') { + $count = $result['count']; + if ($query_operator === 'OR') { + $count = $this->facet->getUnfilteredResults()[$this->facet->getFieldIdentifier()][$key]['count']; + } + + $result = new Result(trim($result['filter'], '"'), trim($result['filter'], '"'), $count); + $facet_results[] = $result; } } $this->facet->setResults($facet_results); diff --git a/src/Plugin/facets/widget/LinksWidget.php b/src/Plugin/facets/widget/LinksWidget.php index 759ada9..0591f2e 100644 --- a/src/Plugin/facets/widget/LinksWidget.php +++ b/src/Plugin/facets/widget/LinksWidget.php @@ -45,22 +45,20 @@ class LinksWidget implements WidgetInterface { $show_numbers = (bool) $configuration['show_numbers']; foreach ($results as $result) { - if ($result->getCount()) { - // Get the link. - $text = $result->getDisplayValue(); - if ($show_numbers) { - $text .= ' (' . $result->getCount() . ')'; - } - if ($result->isActive()) { - $text = '(-) ' . $text; - } - - if (is_null($result->getUrl())) { - $items[] = $text; - } - else { - $items[] = new Link($text, $result->getUrl()); - } + // Get the link. + $text = $result->getDisplayValue(); + if ($show_numbers) { + $text .= ' (' . $result->getCount() . ')'; + } + if ($result->isActive()) { + $text = '(-) ' . $text; + } + + if (is_null($result->getUrl())) { + $items[] = $text; + } + else { + $items[] = new Link($text, $result->getUrl()); } } diff --git a/src/QueryType/QueryTypeInterface.php b/src/QueryType/QueryTypeInterface.php index e4a9ed3..d43e3ac 100644 --- a/src/QueryType/QueryTypeInterface.php +++ b/src/QueryType/QueryTypeInterface.php @@ -13,6 +13,9 @@ interface QueryTypeInterface { /** * Add facet info to the query using the backend native query object. + * + * @return array + * Returns an array of unfiltered results */ public function execute(); diff --git a/src/Tests/IntegrationTest.php b/src/Tests/IntegrationTest.php index 2bf9636..198e15d 100644 --- a/src/Tests/IntegrationTest.php +++ b/src/Tests/IntegrationTest.php @@ -201,6 +201,43 @@ class IntegrationTest extends FacetWebTestBase { } /** + * Tests the facet's and/or functionality. + */ + public function testAndOrFacet() { + $facet_name = 'test & facet'; + $facet_id = 'test_facet'; + $facet_edit_page = 'admin/config/search/facets/' . $facet_id . '/display'; + + $this->drupalLogin($this->adminUser); + $this->addFacet($facet_name); + $this->createFacetBlock('test_facet'); + + $this->drupalGet($facet_edit_page); + $this->drupalPostForm(NULL, ['facet_settings[query_operator]' => 'AND'], $this->t('Save')); + + $this->insertExampleContent(); + $this->assertEqual($this->indexItems($this->indexId), 5, '5 items were indexed.'); + + $this->drupalGet('search-api-test-fulltext'); + $this->assertLink('item'); + $this->assertLink('article'); + + $this->clickLink('item'); + $this->assertLink('(-) item'); + $this->assertNoLink('article'); + + $this->drupalGet($facet_edit_page); + $this->drupalPostForm(NULL, ['facet_settings[query_operator]' => 'OR'], $this->t('Save')); + $this->drupalGet('search-api-test-fulltext'); + $this->assertLink('item'); + $this->assertLink('article'); + + $this->clickLink('item'); + $this->assertLink('(-) item'); + $this->assertLink('article'); + } + + /** * Deletes a facet block by id. * * @param string $id diff --git a/src/Tests/UrlIntegrationTest.php b/src/Tests/UrlIntegrationTest.php index f451b83..9af92df 100644 --- a/src/Tests/UrlIntegrationTest.php +++ b/src/Tests/UrlIntegrationTest.php @@ -93,7 +93,7 @@ class UrlIntegrationTest extends FacetWebTestBase { $this->assertResponse(200); $this->assertLink('(-) item'); - $this->assertNoLink('article'); + $this->assertLink('article'); $this->assertUrl('search-api-test-fulltext', ['query' => ['y[0]' => 'facet||item']]); } diff --git a/src/Tests/WidgetIntegrationTest.php b/src/Tests/WidgetIntegrationTest.php index 5f91902..0a4576f 100644 --- a/src/Tests/WidgetIntegrationTest.php +++ b/src/Tests/WidgetIntegrationTest.php @@ -80,4 +80,98 @@ class WidgetIntegrationTest extends FacetWebTestBase { $this->assertFieldChecked('edit-type-item'); } + /** + * Test links widget's basic functionality. + */ + public function testLinksWidget() { + $id = 'links_widget'; + $name = '>.Facet &* Links'; + $facet_add_page = 'admin/config/search/facets/add-facet'; + + $this->drupalGet($facet_add_page); + + $form_values = [ + 'id' => $id, + 'status' => 1, + 'url_alias' => $id, + 'name' => $name, + 'facet_source_id' => 'search_api_views:search_api_test_view:page_1', + 'facet_source_configs[search_api_views:search_api_test_view:page_1][field_identifier]' => 'type', + ]; + $this->drupalPostForm(NULL, ['facet_source_id' => 'search_api_views:search_api_test_view:page_1'], $this->t('Configure facet source')); + $this->drupalPostForm(NULL, $form_values, $this->t('Save')); + $this->drupalPostForm(NULL, ['widget' => 'links'], $this->t('Save')); + + $block_values = [ + 'plugin_id' => 'facet_block:' . $id, + 'settings' => [ + 'region' => 'footer', + 'id' => str_replace('_', '-', $id), + ] + ]; + $this->drupalPlaceBlock($block_values['plugin_id'], $block_values['settings']); + + $this->drupalGet('search-api-test-fulltext'); + $this->assertLink('item'); + $this->assertLink('article'); + + $this->clickLink('item'); + $this->assertLink('(-) item'); + } + + /** + * Tests the functionality of a widget to hide/show the item-count. + */ + public function testLinksShowHideCount() { + $id = 'links_widget'; + $name = '>.Facet &* Links'; + $facet_add_page = 'admin/config/search/facets/add-facet'; + $facet_edit_page = 'admin/config/search/facets/' . $id . '/display'; + + $this->drupalGet($facet_add_page); + + $form_values = [ + 'id' => $id, + 'status' => 1, + 'url_alias' => $id, + 'name' => $name, + 'facet_source_id' => 'search_api_views:search_api_test_view:page_1', + 'facet_source_configs[search_api_views:search_api_test_view:page_1][field_identifier]' => 'type', + ]; + $this->drupalPostForm(NULL, ['facet_source_id' => 'search_api_views:search_api_test_view:page_1'], $this->t('Configure facet source')); + $this->drupalPostForm(NULL, $form_values, $this->t('Save')); + $this->drupalPostForm(NULL, ['widget' => 'links'], $this->t('Save')); + + $block_values = [ + 'plugin_id' => 'facet_block:' . $id, + 'settings' => [ + 'region' => 'footer', + 'id' => str_replace('_', '-', $id), + ] + ]; + $this->drupalPlaceBlock($block_values['plugin_id'], $block_values['settings']); + + // Go to the view and check that the facet links are shown with their + // default settings. + $this->drupalGet('search-api-test-fulltext'); + $this->assertLink('item'); + $this->assertLink('article'); + + $this->drupalGet($facet_edit_page); + $this->drupalPostForm(NULL, ['widget' => 'links', 'widget_configs[show_numbers]' => TRUE], $this->t('Save')); + + // Go back to the same view and check that links now display the count + $this->drupalGet('search-api-test-fulltext'); + $this->assertLink('item (3)'); + $this->assertLink('article (2)'); + + $this->drupalGet($facet_edit_page); + $this->drupalPostForm(NULL, ['widget' => 'links', 'widget_configs[show_numbers]' => FALSE], $this->t('Save')); + + // The count should be hidden again. + $this->drupalGet('search-api-test-fulltext'); + $this->assertLink('item'); + $this->assertLink('article'); + } + } diff --git a/tests/src/Unit/Plugin/query_type/SearchApiStringTest.php b/tests/src/Unit/Plugin/query_type/SearchApiStringTest.php index cecd79f..9f15b78 100644 --- a/tests/src/Unit/Plugin/query_type/SearchApiStringTest.php +++ b/tests/src/Unit/Plugin/query_type/SearchApiStringTest.php @@ -24,7 +24,10 @@ class SearchApiStringTest extends UnitTestCase { */ public function testQueryType() { $query = new SearchApiQuery([], 'search_api_query', []); - $facet = new Facet([], 'facets_facet'); + $facet = new Facet( + ['options' => ['query_operator' => 'AND']], + 'facets_facet' + ); $original_results = [ ['count' => 3, 'filter' => 'badger'], diff --git a/tests/src/Unit/Plugin/widget/LinksWidgetTest.php b/tests/src/Unit/Plugin/widget/LinksWidgetTest.php index 7e4424c..49b99fc 100644 --- a/tests/src/Unit/Plugin/widget/LinksWidgetTest.php +++ b/tests/src/Unit/Plugin/widget/LinksWidgetTest.php @@ -79,29 +79,6 @@ class LinksWidgetTest extends UnitTestCase { /** * Test widget. */ - public function testHideEmptyCount() { - $original_results = $this->originalResults; - $original_results[1] = new Result('badger', 'Badger', 0); - - $facet = new Facet([], 'facet'); - $facet->setResults($original_results); - $facet->setWidgetConfigs(['show_numbers' => 1]); - - $output = $this->widget->build($facet); - - $this->assertInternalType('array', $output); - $this->assertCount(3, $output['#items']); - - $expected_links = ['Llama (10)', 'Duck (15)', 'Alpaca (9)']; - foreach ($expected_links as $index => $value) { - $this->assertInstanceOf('\Drupal\Core\Link', $output['#items'][$index]); - $this->assertEquals($value, $output['#items'][$index]->getText()); - } - } - - /** - * Test widget. - */ public function testActiveItems() { $original_results = $this->originalResults; $original_results[0]->setActiveState(TRUE);