The Facets module allows you, after a search, to create and manage faceted interfaces. Developers are also able to create their own widgets. Facets work with the Drupal Search API, which means that the code and configuration can be reused as is with the most popular search solutions available for Drupal.
The Facets module offers default methods for organizing sorting, transforming IDs into labels, for example. The method here takes the form of a code class that alters the facet elements in a given way (altering the options).
Grouping taxonomy terms from different vocabularies for a unified sort
We wanted to address an initial case on a Drupal 8 site: grouping taxonomy terms in different vocabularies that have the same label. A vocabulary "A" contained the label "Economie", vocabulary "B" contained "économie", and vocabulary "C" also contained "économie". In our example, each label, even if identical, has a different term ID.
To solve this case, it was necessary to create a custom method to group the options of a facet that have the same Label.
In the Drupal backend (see image below), we activated the method in the facet configuration. The groupings are created automatically. The label can be edited if you wish.

Grouping dates for a unified sort
While developing another project on Drupal 9, we needed to make it possible to sort by dates (by years) and group several dates (years) into a single sort order. We used the same method. The example speaks for itself:

Grouping multiple content types under a single label
We used the same method to enable grouping of several content types under a single label. We had to bring together the content types "Formation X," "Formation Y," and "Formation Z" under one label. We used the same method to group them into a single facet option: Formation.

Our solution
This solution is largely inspired by the logic presented by Kenneth Bolívar Castro (keboca) - https://www.keboca.com/drupal-8-combine-two-facets
Create a file in a custom module in src/plugin/facets/processor/ - for example, MergeTaxonomyTermsByLabel.php. This example will group taxonomy terms with the same name from different vocabularies (with different tids) under the same label in the facet.
label();
}, $this->entityTypeManager->getStorage('taxonomy_term')->loadMultiple());
asort($terms);
return $terms;
}
/**
* Pre define Groups, create groups of taxonomy terms grouping.
*
* @return array $groups
*
*/
protected function preDefineGroups(){
$terms = array_filter($this->getTaxonomyTerms()); //remove null, 0 and "" values.
if (empty($terms)) { //no taxonomy terms to show
return;
}
$groupings = [];
//Array with values and their number of iterations.
$similar_terms = self::arrayCountValues($terms);
//remove items with number of iterations < = 1.
$filtered_similar_terms = array_filter($similar_terms, function ($value){
return $value > 1;
});
//Compare and group similar labels.
foreach ($filtered_similar_terms as $value=>$number_iterations){
foreach($terms as $tid=>$tname){
if(strcmp(self::transliterateString($value), self::transliterateString($tname)) == 0){
$groupings[$value][] = $tid;
}
}
}
//Build the mapping array.
$groups = [];
foreach($groupings as $grouping_name=>$array_tids){
$taxonomy_terms = [];
$taxonomy_terms_values = [];
foreach($array_tids as $tid){
$taxonomy_terms[$tid] = $terms[$tid];
$taxonomy_terms_values[$tid] = $tid;
}
$groups[] = [
'facet_name' => $grouping_name,
'taxonomy_terms' => $taxonomy_terms,
'taxonomy_terms_values'=> $taxonomy_terms_values,
];
}
return $groups;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
/** @var array $config */
$config = $this->getConfiguration()['facet_terms_groups'];
// Gather the number of groups in the form already.
$groups = $form_state->get('groups');
/** Create groups automatically if there are taxnonomy terms with the same name **/
$config = $this->preDefineGroups();
$groups = count($config);
$form_state->set('groups', $groups);
// Prepare form widget.
$build['#tree'] = TRUE;
$build['container_open']['#markup'] = '';
// Iterate same times as groups available.
for ($i = 0; $i < $groups; $i++) {
// Build details wrapper on each group.
$build['facet_terms_groups'][$i] = [
'#type' => 'details',
//'#title' => $this->t('Facet group'),
'#title' => $config[$i]['facet_name'],
'#open' => FALSE,
];
// Include field to overwrite facet name.
$build['facet_terms_groups'][$i]['facet_name'] = [
'#type' => 'textfield',
'#title' => $this->t('New Facet name'),
'#default_value' => $config[$i]['facet_name'] ?? NULL,
];
// Expose all possible content types available.
$build['facet_terms_groups'][$i]['taxonomy_terms'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Taxonomy terms to be grouped.'),
'#options' => $config[$i]['taxonomy_terms'],
'#default_value' => $config[$i]['taxonomy_terms_values'] ?? [],
];
}
// Close container element.
$build['container_close']['#markup'] = '';
return $build;
}
/*
* Function to count elements in an array and return an Array with every value and its number of iterations.
* Transliteration insensitive and case insensitive.
*/
public static function arrayCountValues($array){
foreach ($array as $k=>$v){
$array[$k] = self::transliterateString($v);
}
return array_count_values($array);
}
/* Function to transfom a string containing special characters and transliteration */
public static function transliterateString($txt) {
$transliterationTable = array('á' => 'a', 'Á' => 'A', 'à' => 'a', 'À' => 'A', 'ă' => 'a', 'Ă' => 'A', 'â' => 'a', 'Â' => 'A', 'å' => 'a', 'Å' => 'A', 'ã' => 'a', 'Ã' => 'A', 'ą' => 'a', 'Ą' => 'A', 'ā' => 'a', 'Ā' => 'A', 'ä' => 'ae', 'Ä' => 'AE', 'æ' => 'ae', 'Æ' => 'AE', 'ḃ' => 'b', 'Ḃ' => 'B', 'ć' => 'c', 'Ć' => 'C', 'ĉ' => 'c', 'Ĉ' => 'C', 'č' => 'c', 'Č' => 'C', 'ċ' => 'c', 'Ċ' => 'C', 'ç' => 'c', 'Ç' => 'C', 'ď' => 'd', 'Ď' => 'D', 'ḋ' => 'd', 'Ḋ' => 'D', 'đ' => 'd', 'Đ' => 'D', 'ð' => 'dh', 'Ð' => 'Dh', 'é' => 'e', 'É' => 'E', 'è' => 'e', 'È' => 'E', 'ĕ' => 'e', 'Ĕ' => 'E', 'ê' => 'e', 'Ê' => 'E', 'ě' => 'e', 'Ě' => 'E', 'ë' => 'e', 'Ë' => 'E', 'ė' => 'e', 'Ė' => 'E', 'ę' => 'e', 'Ę' => 'E', 'ē' => 'e', 'Ē' => 'E', 'ḟ' => 'f', 'Ḟ' => 'F', 'ƒ' => 'f', 'Ƒ' => 'F', 'ğ' => 'g', 'Ğ' => 'G', 'ĝ' => 'g', 'Ĝ' => 'G', 'ġ' => 'g', 'Ġ' => 'G', 'ģ' => 'g', 'Ģ' => 'G', 'ĥ' => 'h', 'Ĥ' => 'H', 'ħ' => 'h', 'Ħ' => 'H', 'í' => 'i', 'Í' => 'I', 'ì' => 'i', 'Ì' => 'I', 'î' => 'i', 'Î' => 'I', 'ï' => 'i', 'Ï' => 'I', 'ĩ' => 'i', 'Ĩ' => 'I', 'į' => 'i', 'Į' => 'I', 'ī' => 'i', 'Ī' => 'I', 'ĵ' => 'j', 'Ĵ' => 'J', 'ķ' => 'k', 'Ķ' => 'K', 'ĺ' => 'l', 'Ĺ' => 'L', 'ľ' => 'l', 'Ľ' => 'L', 'ļ' => 'l', 'Ļ' => 'L', 'ł' => 'l', 'Ł' => 'L', 'ṁ' => 'm', 'Ṁ' => 'M', 'ń' => 'n', 'Ń' => 'N', 'ň' => 'n', 'Ň' => 'N', 'ñ' => 'n', 'Ñ' => 'N', 'ņ' => 'n', 'Ņ' => 'N', 'ó' => 'o', 'Ó' => 'O', 'ò' => 'o', 'Ò' => 'O', 'ô' => 'o', 'Ô' => 'O', 'ő' => 'o', 'Ő' => 'O', 'õ' => 'o', 'Õ' => 'O', 'ø' => 'oe', 'Ø' => 'OE', 'ō' => 'o', 'Ō' => 'O', 'ơ' => 'o', 'Ơ' => 'O', 'ö' => 'oe', 'Ö' => 'OE', 'ṗ' => 'p', 'Ṗ' => 'P', 'ŕ' => 'r', 'Ŕ' => 'R', 'ř' => 'r', 'Ř' => 'R', 'ŗ' => 'r', 'Ŗ' => 'R', 'ś' => 's', 'Ś' => 'S', 'ŝ' => 's', 'Ŝ' => 'S', 'š' => 's', 'Š' => 'S', 'ṡ' => 's', 'Ṡ' => 'S', 'ş' => 's', 'Ş' => 'S', 'ș' => 's', 'Ș' => 'S', 'ß' => 'SS', 'ť' => 't', 'Ť' => 'T', 'ṫ' => 't', 'Ṫ' => 'T', 'ţ' => 't', 'Ţ' => 'T', 'ț' => 't', 'Ț' => 'T', 'ŧ' => 't', 'Ŧ' => 'T', 'ú' => 'u', 'Ú' => 'U', 'ù' => 'u', 'Ù' => 'U', 'ŭ' => 'u', 'Ŭ' => 'U', 'û' => 'u', 'Û' => 'U', 'ů' => 'u', 'Ů' => 'U', 'ű' => 'u', 'Ű' => 'U', 'ũ' => 'u', 'Ũ' => 'U', 'ų' => 'u', 'Ų' => 'U', 'ū' => 'u', 'Ū' => 'U', 'ư' => 'u', 'Ư' => 'U', 'ü' => 'ue', 'Ü' => 'UE', 'ẃ' => 'w', 'Ẃ' => 'W', 'ẁ' => 'w', 'Ẁ' => 'W', 'ŵ' => 'w', 'Ŵ' => 'W', 'ẅ' => 'w', 'Ẅ' => 'W', 'ý' => 'y', 'Ý' => 'Y', 'ỳ' => 'y', 'Ỳ' => 'Y', 'ŷ' => 'y', 'Ŷ' => 'Y', 'ÿ' => 'y', 'Ÿ' => 'Y', 'ź' => 'z', 'Ź' => 'Z', 'ž' => 'z', 'Ž' => 'Z', 'ż' => 'z', 'Ż' => 'Z', 'þ' => 'th', 'Þ' => 'Th', 'µ' => 'u', 'а' => 'a', 'А' => 'a', 'б' => 'b', 'Б' => 'b', 'в' => 'v', 'В' => 'v', 'г' => 'g', 'Г' => 'g', 'д' => 'd', 'Д' => 'd', 'е' => 'e', 'Е' => 'e', 'ё' => 'e', 'Ё' => 'e', 'ж' => 'zh', 'Ж' => 'zh', 'з' => 'z', 'З' => 'z', 'и' => 'i', 'И' => 'i', 'й' => 'j', 'Й' => 'j', 'к' => 'k', 'К' => 'k', 'л' => 'l', 'Л' => 'l', 'м' => 'm', 'М' => 'm', 'н' => 'n', 'Н' => 'n', 'о' => 'o', 'О' => 'o', 'п' => 'p', 'П' => 'p', 'р' => 'r', 'Р' => 'r', 'с' => 's', 'С' => 's', 'т' => 't', 'Т' => 't', 'у' => 'u', 'У' => 'u', 'ф' => 'f', 'Ф' => 'f', 'х' => 'h', 'Х' => 'h', 'ц' => 'c', 'Ц' => 'c', 'ч' => 'ch', 'Ч' => 'ch', 'ш' => 'sh', 'Ш' => 'sh', 'щ' => 'sch', 'Щ' => 'sch', 'ъ' => '', 'Ъ' => '', 'ы' => 'y', 'Ы' => 'y', 'ь' => '', 'Ь' => '', 'э' => 'e', 'Э' => 'e', 'ю' => 'ju', 'Ю' => 'ju', 'я' => 'ja', 'Я' => 'ja');
$txt = str_replace(array_keys($transliterationTable), array_values($transliterationTable), $txt);
return trim(strtolower($txt));
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet) {
$form_state->unsetValue('actions');
parent::submitConfigurationForm($form, $form_state, $facet);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'facet_terms_groups' => [
[
'facet_name' => '',
'taxonomy_terms' => [],
'taxonomy_terms_values' => [],
],
],
];
}
/**
* {@inheritdoc}
*/
public function build(FacetInterface $facet, array $results) {
/** @var array $facet_groups */
$facet_groups = $this->getConfiguration()['facet_terms_groups'];
/** @var \Drupal\facets\Result\Result[] $facets */
$facets = array_reduce($results, function ($carry, $item) {
/** @var \Drupal\facets\Result\Result $item */
$carry[$item->getRawValue()] = $item;
return $carry;
}, []);
array_walk($facet_groups, function ($config) use ($results, &$facets) {
/** @var array $terms */
$terms = array_filter($config['taxonomy_terms']); //remove null, 0 and "" values.
if (empty($terms)) {
return;
}
//keep only taxonomy terms enabled chosen in facet.
/** @var array $filtered */
$filtered = array_filter($terms, function ($term) use ($facets) {
return array_key_exists($term, $facets);
});
if (empty($filtered)) {
return;
}
/** @var string $key */
$key = array_shift($filtered); //get first element's key (term_id)
/** @var \Drupal\facets\Result\Result $first */
$first = &$facets[$key];
// Overwrite label if new facet name was defined.
if (!empty($config['facet_name'])) {
$first->setDisplayValue($config['facet_name']);
}
// Init flag variables.
$updated = FALSE;
/** @var \Drupal\Core\Url $url */
$url = $first->getUrl();
/** @var array $query */
$query = $url->getOption('query');
// Walk-through all remain filtered types.
foreach ($filtered as $item) {
// Setup dynamic filter.
$filter = "thematiques:{$item}"; //thematique should be change, check point de vigilance.
// Look-up for query string.
//"f" parameter should be dynamiic, check point de vigilance.
if (!in_array($filter, $query['f'])) {
// Inject filter to current query.
$updated = TRUE;
$query['f'][] = $filter;
}
// Verify when current facet is active.
elseif ($first->isActive()) {
// Remove duplication filter values.
$updated = TRUE;
$query['f'] = array_filter($query['f'], function ($param) use ($filter) {
return $param != $filter;
});
// Remove whole query string when there are not filters.
if (empty($query['f'])) {
unset($query['f']);
}
}
// Overwrite URL options then define it back to facet.
if ($updated) {
$url->setOption('query', $query);
$first->setUrl($url);
}
// Update facet count value when lab facet was found.
$first->setCount($first->getCount() + $facets[$item]->getCount());
// Remove facet instance.
unset($facets[$item]);
}
});
return array_values($facets);
}
/**
* Setting entity type manager property.
*
* @param \Drupal\Core\Entity\EntityTypeManager $entityTypeManager
*/
public function setEntityTypeManager(EntityTypeManager $entityTypeManager): void {
$this->entityTypeManager = $entityTypeManager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
/** @var static $plugin */
$plugin = new static(
$configuration,
$plugin_id,
$plugin_definition
);
// Inject dependency into current plugin's instance.
$plugin->setEntityTypeManager($container->get('entity_type.manager'));
return $plugin;
}
}Points to watch out for
The "f" query string parameter was hard-coded in the patch, but the query string parameter could be anything. Instead, it should read from the getFilterKey function of the URL processor to get the query string parameter name. The "thématique" machine name was hard-coded, but that could be any value. Instead, it should read from the function $facet->getUrlAlias() to retrieve this machine name.
Add this code and test it:
$url_processor = $this->urlProcessorManager->createInstance($facet->getFacetSourceConfig()
->getUrlProcessorName(), ['facet' => $facet]);
$filter_key = $url_processor->getFilterKey();
$field_name = $facet->getUrlAlias();Hope you find this useful...
Elie C.
Elie has been a Drupal developer since September 2016, on the team at the bluedrop.fr agency.
?>