Marrying facets and exposed filters
Facet API facets have the option to show the links they provide normally with checkboxes, which makes them look even more like selectable filters. If you use Better Exposed Filters, or BEF in short, you can make the exposed filters have checkboxes instead of the select list which Views provides by default. Then these two look just alike and basically do the same thing, filter results in a list.![]()
So why cannot they walk hand in hand I ask? The problem I was having was with facets working as links. They can be displayed on a non-search page as a block, but as soon as user clicks on a facet, it takes the user to the search page. So they cannot be really combined with any other exposed filter or facet to offer an advanced search to start with where user can choose multiple filters and maybe a keyword before clicking Search. Views exposed filters are doing just this, but they don't know anything about facets, so when you arrive to the search results page, your chosen filters are active but the facets are not.
This happens because they use a different syntax when it comes to URL parameters they use for storing the active filters and facets.
- Views exposed filters:
- Facet API facets:
Can you spot the difference? They are like two cousins, same same but different. So I decided to try out if I can make them communicate simply by altering the way how Facet API deals with the URL parameters and how it builds them. I was too scared to dig into Views and try to alter how it does the same thing. It took me a while to get the hang of the module's inner workings, but in the end it wasn't that complicated. What I had to do was to introduce my custom URL processor which Facet API offers as a class, so it's possible to extend that in a custom module without hacking any contrib modules. And it works!
Before diving too much into the code, you can check this issue to see if this will be a part of Facet API or another extra module for it.
So here we go. First you need to create a custom module with the .info and .module files. I have attached the whole working module at the end of the post if you don't want to learn the code and hate copy-pasting.
// exposed_facets.info name = Exposed facets description = "Rewrites facetapi facet URLs to work with Views exposed filters." package = FacetAPI core = "7.x" files[] = exposed_facets.module files[] = plugins/facetapi/url_processor_exposed_facets.incNothing fancy going on here, but you can already see that there is a inc file which has to be introduced here.
// exposed_facets.module
/**
* Allows for alterations to the searcher definitions.
*
* @param array &$searcher_info
* The return values of hook_facetapi_searcher_info() implementations.
*
* Implements hook_facetapi_searcher_info().
*/
function exposed_facets_facetapi_searcher_info_alter(array &$searcher_info) {
foreach ($searcher_info as &$info) {
// Activate custom URL processor.
$id = 'exposed_facets_searcher_' . $info['name'];
$info['url processor'] = 'exposed_facets';
}
}
/**
* Implements hook_facetapi_url_processors().
*/
function exposed_facets_facetapi_url_processors() {
return array(
'exposed_facets' => array(
'handler' => array(
'label' => t('Custom URL processor'),
'class' => 'FacetapiUrlProcessorExposedFacets',
),
),
);
}Nothing logically challenging here either. The first function alters the search defitions to use a custom URL processor and the second function tells which class implements the processor. This is why we have to include the file in the module's .info file, otherwise the class doesn't exist.
Then we get to the real beef of the post. The altered processor. It extends the FacetapiUrlProcessorStandard from Facet API by altering only two functions. getQueryString() is constructing the link with the URL parameters, so this had to be modified to create the path in the same syntax as Views does for the exposed facets. The comments in the code might help you to follow the principle. fetchParams() is used to get the URL parameters from the path if there are active facets. So this had to be modified in a similar fashion to read the altered parameters.
There are some quite nasty string replacements and array flipping etc. which makes it difficult to follow the logic behind them, but I have tried to explain stuff in the comments of the code.
/**
* @file
* A custom URL processor which works with Views exposed filters.
*/
/**
* Extension of FacetapiUrlProcessor.
*/
class FacetapiUrlProcessorExposedFacets extends FacetapiUrlProcessorStandard {
/**
* Implements FacetapiUrlProcessor::fetchParams().
*
* Use $_GET as the source for facet data.
*/
public function fetchParams() {
$params = array();
$filter_key = $this->filterKey;
$enabled_facets = $this->adapter->getEnabledFacets();
// Rewriting facet's format with only underscores which matches Views
// exposed filters formatting
foreach ($enabled_facets as $facetapi_alias => $array_values) {
$enabled_facets[str_replace(':', '_', $facetapi_alias)]['field type'] = $enabled_facets[$facetapi_alias]['field type'];
// Saving original facetapi alias to use when returning parameters for facetapi
$enabled_facets[str_replace(':', '_', $facetapi_alias)]['facetapi alias'] = $facetapi_alias;
}
foreach ($_GET as $filter_alias => $filter_values) {
if (is_array($filter_values)) {
foreach ($filter_values as $pos => $value) {
// If the field type for the facet is taxonomy term, then handle it differently
if (isset($enabled_facets[$filter_alias]) && $enabled_facets[$filter_alias]['field type'] == 'taxonomy_term' && !strpos($value, "!")) {
$params[$filter_key][$pos] = rawurlencode($enabled_facets[$filter_alias]['facetapi alias']) . ':' . $value;
} else {
// Saving parameters as they are when no need to make them match
// with Views exposed filters
$params[$filter_key][$pos] = $value;
}
}
} else {
$params[$filter_alias] = $filter_values;
}
}
return $params;
}
/**
* Implements FacetapiUrlProcessor::getQueryString().
*/
public function getQueryString(array $facet, array $values, $active) {
$qstring = $this->params;
$active_items = $this->adapter->getActiveItems($facet);
// Appends to qstring if inactive, removes if active.
foreach ($values as $value) {
if ($active && isset($active_items[$value])) {
unset($qstring[$this->filterKey][$active_items[$value]['pos']]);
}
elseif (!$active) {
$field_alias = rawurlencode($facet['field alias']);
$qstring[$this->filterKey][] = $field_alias . ':' . $value;
}
}
// Removes duplicates, resets array keys and returns query string.
// @see http://drupal.org/node/1340528
$qstring[$this->filterKey] = array_values(array_unique($qstring[$this->filterKey]));
// We need to rewrite the query in the format which Views exposed filters use.
$enabled_facets = $this->adapter->getEnabledFacets();
$query = array();
foreach ($qstring[$this->filterKey] as $pos => $filter_value) {
// Inverted explode, field alias can have multiple colons,
// but the last one is always value.
$parts = array_map('strrev', explode(':', strrev($filter_value), 2));
// Checking if the facet's field type is taxonomy term and making sure it's not a facet
// for missing values (with a value of "!") and rewriting the output in views exposed
// filters structure.
if (isset($enabled_facets[rawurldecode($parts[1])]) && $enabled_facets[rawurldecode($parts[1])]['field type'] == 'taxonomy_term' && $parts[0] != '!') {
// Removing the query part with filterKey
unset($qstring[$this->filterKey][$pos]);
// Rewriting facet URL using only underscores like Views exposed filters do.
$parts[1] = rawurldecode($parts[1]);
$parts[1] = str_replace(':', '_', $parts[1]);
$qstring[$parts[1]][$pos] = $parts[0];
} else {
$qstring[$this->filterKey][$pos] = $filter_value;
}
}
return $qstring;
}
}So, there we go. This module should work just like activate and forget, then the facet items will enable the exposed filters and vice versa. If something breaks, then just disable the module. I have used it to provide checkboxes on the front page with exposed filters + keyword search and then hidden the filters on the results page where facets are activated. Check it live at http://www.srv.fi/en. If you find this useful, comment something on the issue so maybe this will come part of the main Facet API module.
