Is possible to set up a default value for the facet results? Even with custom code?

I'm on Drupal 9 and I have an ajax view with a page display and an exposed filter block and a single filter exposed, "Search: Fulltext search".
The search api is configured to search inside 3 different content type: products, articles and authors.
I've also configured a facet to filter on the content type, using a widget type "links", that let filter one content type at time
So, initially, the search page shows the results of all 3 content type. After a user filters a content type - e.g. articles - he can switch only to another and never return to the "no content type chosen" situation.

What I need to do is that the results are listed by default with one specific content type - e.g. product.
If that content type yield no results, the following content type should be used as filter. If there are no result, just "no result" should be shown.
In other words, the "no content type chosen" situation should be never available.

I've already tried to see if I can alter the query\options inside a hook_views_pre_view or SearchApiEvents::QUERY_PRE_EXECUTE, but with no success.

Comments

Giuseppe87 created an issue. See original summary.

giuseppe87’s picture

Initially I tried some different approach:

* extending SearchApiString
* an event subscriber of KernelEvents::REQUEST altering the incoming url

because by default facets\search api rely on the query_params of the url and the "values" of the relative facets various objects of the modules.
Both of those those approaches kinda failed, because the first didn't update the query params of the request, while the second caused some bugs that I couldn't manage to solve.

The working approach was to extending the default url_processor QueryString - which both initialises the active facets filters and build the url after a search.

Injecting the logic when the class is created allowed to do a minor modification without breaking all the standard functionality.

<?php
    
    namespace Drupal\my_module\Plugin\facets\url_processor;
    
    use Drupal\Core\Entity\EntityTypeManagerInterface;
    use Drupal\facets\Plugin\facets\url_processor\QueryString;
    use Symfony\Component\EventDispatcher\EventDispatcherInterface;
    use Symfony\Component\HttpFoundation\Request;
    
    /**
     * Query string URL processor.
     *
     * @FacetsUrlProcessor(
     *   id = "my_query_string",
     *   label = @Translation("My Query string"),
     *   description = @Translation("Add description")
     * )
     */
    class MyQueryString extends QueryString {
    
      /**
       * A string of how to represent the facet in the url.
       *
       * @var string
       */
      protected $urlAlias;
    
      /**
       * The event dispatcher.
       *
       * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
       */
      protected $eventDispatcher;
    
      /**
       * {@inheritdoc}
       */
      public function __construct(array $configuration, $plugin_id, $plugin_definition, Request $request, EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $eventDispatcher) {
        //this is where I'm adding a default value
        $this->alterRequest($request);
        parent::__construct($configuration, $plugin_id, $plugin_definition, $request, $entity_type_manager, $eventDispatcher);
      }
    
      /**
       * @param \Symfony\Component\HttpFoundation\Request $request
       */
      private function alterRequest(Request $request): void {
        $query = $request->query;
        $facet_new_parameters = [];
    
        if ($facet_parameters = $query->get('f')) {
          $content_type_filter_exists = FALSE;
    
          foreach ($facet_parameters as $parameter) {
            $sepator = ':';
            $explosion = explode($sepator, $parameter);
            $facet_type = array_shift($explosion);
            if ($facet_type === 'content_type') {
              $content_type_filter_exists = TRUE;
            }
          }
    
          if (!$content_type_filter_exists) {
            $facet_new_parameters = array_merge($facet_parameters, ['content_type:prodotto']);
          }
        }
        else {
          $facet_new_parameters = ['content_type:prodotto'];
        }
    
        if (!empty($facet_new_parameters)) {
          $query->set('f', $facet_new_parameters);
        }
      }
    }


mpotter’s picture

@Giuseppe87 how did you override the default querystring plugin for facets to use the above code? Is there something in a services.yml file somewhere? I didn't see anything in the Facet config to select the new processor.

mpotter’s picture

Oh, nevermind, found the answer. For others: Go to the Configure link for your Facet Source and there will be a radio selector for the url processor.

giuseppe87’s picture

@mpotter: Sorry I've read now your question. Yes it is done as you've explained, via the config of the Facet Source

nicolas bouteille’s picture

Thanks a lot for sharing this Giuseppe87!
Creating a custom URL Processor in order to pre-fill / force a value on a facet served for us as a workaround to the facet + contextual filter problem.
Indeed, on each node of type School, we are displaying the list of all trainings related to that specific School.
At first, we obviously used Views' Contextual filter for that.
However, we also had some facet filters with search API for the Level of the training for example.
Unfortunately, facets are not aware of the data transmitted to the view through contextual filters. So they were not properly restricting the available options regarding to the currently viewed School page.
In the end, we removed the contextual filter and replaced it with an additional facet "School" which value is pre-filled / forced with the current School (retrieved form canonical url) in a custom URL Processor, following code at #2.

brolad’s picture

#2 Thanks for your solution but it works when we have only one search page. In another case we'll apply the default value for all pages as alterRequest() will be called each time where we're rendering facet even when the facet related to default value is not expected to be displayed.
I got the same result with the next code:

<?php

namespace Drupal\my_module\Plugin\facets\url_processor;

use Drupal\facets\Plugin\facets\url_processor\QueryString;
use Drupal\facets\FacetInterface;

/**
 * Query string URL processor.
 *
 * @FacetsUrlProcessor(
 *   id = "my_query_string",
 *   label = @Translation("My Query string"),
 *   description = @Translation("Add description")
 * )
 */
class MyQueryString extends QueryString {

  /**
   * {@inheritdoc}
   */
  public function buildUrls(FacetInterface $facet, array $results) {
    if ($facet->getOriginalId() == 'content_type') {
      if (!$facet->getActiveItems()) {
        $facet->setActiveItem('prodotto');
      }
    }
    return parent::buildUrls($facet, $results);
  }

}

mb it'll be helpful for someone

mfrosch’s picture

#7 did not work for me. Despite the facet had a default value, the results were not filtered.
#2 worked for me. The facet had a default value as well the results were filtered.

anybody’s picture

Version: 8.x-1.6 » 2.0.x-dev

Should this perhaps be changed to a feature request to allow defining default values in facets? Might be a relevant feature?

We should ensure that not only a single value, but always a list of values is allowed to be defined per facet.

alorenc’s picture

Approach #7 allows me to set default filter value but please dont forget to set processor for the facet source configuration.

shiraz dindar’s picture

For me,

#7 sets the default facet value in the facet field, but the results are not actually filtered accordingly.

#2 works for me. However, since then a new argument ($urlGenerator) was added to the url_processor constructor (the one we are extending), so I had to add it here as well, as follows:

<?php

namespace Drupal\my_module\Plugin\facets\url_processor;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\facets\Plugin\facets\url_processor\QueryString;
use Drupal\facets\Utility\FacetsUrlGenerator;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * Query string URL processor.
 *
 * @FacetsUrlProcessor(
 *   id = "my_query_string",
 *   label = @Translation("My Query string"),
 *   description = @Translation("Add description")
 * )
 */
class MyQueryString extends QueryString {

  /**
   * A string of how to represent the facet in the url.
   *
   * @var string
   */
  protected $urlAlias;

  /**
   * The event dispatcher.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected $eventDispatcher;

  /**
   * {@inheritdoc}
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, Request $request, EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $eventDispatcher, FacetsUrlGenerator $urlGenerator) {
    $this->alterRequest($request);
    parent::__construct($configuration, $plugin_id, $plugin_definition, $request, $entity_type_manager, $eventDispatcher, $urlGenerator);
  }

  /**
   * @param \Symfony\Component\HttpFoundation\Request $request
   */
  private function alterRequest(Request $request): void {
    $query = $request->query;
    $facet_new_parameters = [];

    if ($facet_parameters = $query->get('f')) {
      $article_type_filter_exists = FALSE;

      foreach ($facet_parameters as $parameter) {
        $sepator = ':';
        $explosion = explode($sepator, $parameter);
        $facet_type = array_shift($explosion);
        if ($facet_type === 'article_type') {
          $article_type_filter_exists = TRUE;
        }
      }

      if (!$article_type_filter_exists) {
        $facet_new_parameters = array_merge($facet_parameters, ['article_type:10372']);
      }
    }
    else {
      $facet_new_parameters = ['article_type:10372'];
    }

    if (!empty($facet_new_parameters)) {
      $query->set('f', $facet_new_parameters);
    }
  }
}

That said, I agree with #9 -- this should be something that we can set in the facet config.

Thanks all!

reszli’s picture

$query->get('f') will not work anymore since it's not a scalar value but an array
I used $query->all()['f'] ?? [] instead

problem with this approach is that you can't even intentionally reset the filter to "any" since then the value is not in the URL and it will force the default

vensires’s picture

Based on the previous comments of this thread, I was able to build the following URL processor.

In my case, I wanted that in some views only the values from user fields became the default values for facets when entering the page. In some other views though, the default query_string plugin is what I needed. Read the inline comments if you intend to use it. Might require some refactoring for your case.


namespace Drupal\mymodule\Plugin\facets\url_processor;

use Drupal\Core\Cache\UnchangingCacheableDependencyTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\facets\Plugin\facets\url_processor\QueryString;
use Drupal\facets\Utility\FacetsUrlGenerator;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * Query string URL processor.
 *
 * @FacetsUrlProcessor(
 *   id = "mymodule_query_string",
 *   label = @Translation("My module query string"),
 *   description = @Translation("Extends the original query string with predefined values")
 * )
 */
class MymoduleQueryString extends QueryString {

  use UnchangingCacheableDependencyTrait;

  /**
   * {@inheritdoc}
   *
   * Since we are operating on the construct function and this gets called for
   * every facet on every page, we have to tamper the query object and then
   * restore it; otherwise facets without default values don't seem to work.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, Request $request, EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $eventDispatcher, FacetsUrlGenerator $urlGenerator) {
    // Keep a clone of the original query to restore it later.
    $query = clone $request->query;
    /** @var \Drupal\facets\Entity\Facet $facet */
    $facet = $configuration['facet'];
    if ($facet->getFacetSourceConfig()->getUrlProcessorName() === $plugin_id) {
      $this->alterRequest($request);
    }
    parent::__construct($configuration, $plugin_id, $plugin_definition, $request, $entity_type_manager, $eventDispatcher, $urlGenerator);
    if ($facet->getFacetSourceConfig()->getUrlProcessorName() === $plugin_id) {
      // Restore the original query.
      $request->query = $query;
    }
  }

  /**
   * Alter the request object used by facets.
   *
   * When this function gets called, the default constructor has not yet been
   * called; thus we have to take all the variables we need as arguments instead
   * of relying on $this.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object.
   */
  protected function alterRequest(Request $request) {
    // For this static variable to have any performance improvements, you must
    // have big_pipe module turned off. Otherwise, you would require another way
    // of caching due to different requests; maybe using a computed field on the
    // user entity is a possible solution.
    static $facet_parameters = [];
    /** @var \Drupal\user\UserInterface $account */
    $account = \Drupal::entityTypeManager()->getStorage('user')->load(\Drupal::currentUser()->id());
    $query = &$request->query;
    // Allow "?disable_defaults=1" for a "Clear defaults" button to be able to
    // work at this point.
    if ($query->getInt('disable_defaults', 0)) {
      return;
    }

    if (empty($facet_parameters)) {
      $original_query_parameters = $query->all()['f'] ?? [];
      if (empty($original_query_parameters)) {
        $facet_parameters = [];

        // Get user fields...
        if ($account->hasField('field_name_1') && !$account->get('field_name_1')->isEmpty()) {
          foreach ($account->get('field_name_1')->getValue() as $values) {
            $facet_parameters[] = 'facet_alias1:' . $values['target_id'];
          }
        }
        if ($account->hasField('field_name_2') && !$account->get('field_name_2')->isEmpty()) {
          foreach ($account->get('field_name_2')->getValue() as $values) {
            $facet_parameters[] = 'facet_alias2:' . $values['target_id'];
          }
        }
      }
    }

    if (!empty($facet_parameters)) {
      $query->set('f', $facet_parameters);
    }
  }

}
ludo.r’s picture

It seems, my custom URL processor is being called event when the facet source is set to use the default one.

Anyone encountered this issue?

EDIT: nevermind, it seems another facet source is being triggered on the same page, I just have to find out why.