435 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			435 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
| <?php
 | ||
| 
 | ||
| /**
 | ||
|  * @file
 | ||
|  * Integrates the Search API with the Facet API.
 | ||
|  */
 | ||
| 
 | ||
| /**
 | ||
|  * Implements hook_menu().
 | ||
|  */
 | ||
| function search_api_facetapi_menu() {
 | ||
|   // We need to handle our own menu paths for facets because we need a facet
 | ||
|   // configuration page per index.
 | ||
|   $first = TRUE;
 | ||
|   foreach (facetapi_get_realm_info() as $realm_name => $realm) {
 | ||
|     if ($first) {
 | ||
|       $first = FALSE;
 | ||
|       $items['admin/config/search/search_api/index/%search_api_index/facets'] = array(
 | ||
|         'title'            => 'Facets',
 | ||
|         'page callback'    => 'search_api_facetapi_settings',
 | ||
|         'page arguments'   =>  array($realm_name, 5),
 | ||
|         'weight'           => -1,
 | ||
|         'access arguments' => array('administer search_api'),
 | ||
|         'type'             => MENU_LOCAL_TASK,
 | ||
|         'context'          => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
 | ||
|       );
 | ||
|       $items['admin/config/search/search_api/index/%search_api_index/facets/' . $realm_name] = array(
 | ||
|         'title'            => $realm['label'],
 | ||
|         'type'             => MENU_DEFAULT_LOCAL_TASK,
 | ||
|         'weight'           => $realm['weight'],
 | ||
|       );
 | ||
|     }
 | ||
|     else {
 | ||
|       $items['admin/config/search/search_api/index/%search_api_index/facets/' . $realm_name] = array(
 | ||
|         'title'            => $realm['label'],
 | ||
|         'page callback'    => 'search_api_facetapi_settings',
 | ||
|         'page arguments'   => array($realm_name, 5),
 | ||
|         'access arguments' => array('administer search_api'),
 | ||
|         'type'             => MENU_LOCAL_TASK,
 | ||
|         'context'          => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
 | ||
|         'weight'           => $realm['weight'],
 | ||
|       );
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   return $items;
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * Implements hook_facetapi_searcher_info().
 | ||
|  */
 | ||
| function search_api_facetapi_facetapi_searcher_info() {
 | ||
|   $info = array();
 | ||
|   $indexes = search_api_index_load_multiple(FALSE);
 | ||
|   foreach ($indexes as $index) {
 | ||
|     if ($index->enabled && $index->server()->supportsFeature('search_api_facets')) {
 | ||
|       $searcher_name = 'search_api@' . $index->machine_name;
 | ||
|       $info[$searcher_name] = array(
 | ||
|         'label' => t('Search service: @name', array('@name' => $index->name)),
 | ||
|         'adapter' => 'search_api',
 | ||
|         'instance' => $index->machine_name,
 | ||
|         'types' => array($index->item_type),
 | ||
|         'path' => '',
 | ||
|         'supports facet missing' => TRUE,
 | ||
|         'supports facet mincount' => TRUE,
 | ||
|         'include default facets' => FALSE,
 | ||
|       );
 | ||
|       if (($entity_type = $index->getEntityType()) && $entity_type !== $index->item_type) {
 | ||
|         $info[$searcher_name]['types'][] = $entity_type;
 | ||
|       }
 | ||
|     }
 | ||
|   }
 | ||
|   return $info;
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * Implements hook_facetapi_facet_info().
 | ||
|  */
 | ||
| function search_api_facetapi_facetapi_facet_info(array $searcher_info) {
 | ||
|   $facet_info = array();
 | ||
|   if ('search_api' == $searcher_info['adapter']) {
 | ||
|     $index = search_api_index_load($searcher_info['instance']);
 | ||
|     if (!empty($index->options['fields'])) {
 | ||
|       $wrapper = $index->entityWrapper();
 | ||
|       $bundle_key = NULL;
 | ||
|       if ($index->getEntityType() && ($entity_info = entity_get_info($index->getEntityType())) && !empty($entity_info['bundle keys']['bundle'])) {
 | ||
|         $bundle_key = $entity_info['bundle keys']['bundle'];
 | ||
|       }
 | ||
| 
 | ||
|       // Some type-specific settings. Allowing to set some additional callbacks
 | ||
|       // (and other settings) in the map options allows for easier overriding by
 | ||
|       // other modules.
 | ||
|       $type_settings = array(
 | ||
|         'taxonomy_term' => array(
 | ||
|           'hierarchy callback' => 'search_api_facetapi_get_taxonomy_hierarchy',
 | ||
|         ),
 | ||
|         'date' => array(
 | ||
|           'query type' => 'date',
 | ||
|           'map options' => array(
 | ||
|             'map callback' => 'facetapi_map_date',
 | ||
|           ),
 | ||
|         ),
 | ||
|       );
 | ||
| 
 | ||
|       // Iterate through the indexed fields to set the facetapi settings for
 | ||
|       // each one.
 | ||
|       foreach ($index->getFields() as $key => $field) {
 | ||
|         $field['key'] = $key;
 | ||
|         // Determine which, if any, of the field type-specific options will be
 | ||
|         // used for this field.
 | ||
|         $type = isset($field['entity_type']) ? $field['entity_type'] : $field['type'];
 | ||
|         $type_settings += array($type => array());
 | ||
| 
 | ||
|         $facet_info[$key] = $type_settings[$type] + array(
 | ||
|           'label' => $field['name'],
 | ||
|           'description' => t('Filter by @type.', array('@type' => $field['name'])),
 | ||
|           'allowed operators' => array(
 | ||
|             FACETAPI_OPERATOR_AND => TRUE,
 | ||
|             FACETAPI_OPERATOR_OR => $index->server()->supportsFeature('search_api_facets_operator_or'),
 | ||
|           ),
 | ||
|           'dependency plugins' => array('role'),
 | ||
|           'facet missing allowed' => TRUE,
 | ||
|           'facet mincount allowed' => TRUE,
 | ||
|           'map callback' => 'search_api_facetapi_facet_map_callback',
 | ||
|           'map options' => array(),
 | ||
|           'field type' => $type,
 | ||
|         );
 | ||
|         if ($type === 'date') {
 | ||
|           $facet_info[$key]['description'] .= ' ' . t('(Caution: This may perform very poorly for large result sets.)');
 | ||
|         }
 | ||
|         $facet_info[$key]['map options'] += array(
 | ||
|           'field' => $field,
 | ||
|           'index id' => $index->machine_name,
 | ||
|           'value callback' => '_search_api_facetapi_facet_create_label',
 | ||
|         );
 | ||
|         // Find out whether this property is a Field API field.
 | ||
|         if (strpos($key, ':') === FALSE) {
 | ||
|           if (isset($wrapper->$key)) {
 | ||
|             $property_info = $wrapper->$key->info();
 | ||
|             if (!empty($property_info['field'])) {
 | ||
|               $facet_info[$key]['field api name'] = $key;
 | ||
|             }
 | ||
|           }
 | ||
|         }
 | ||
| 
 | ||
|         // Add bundle information, if applicable.
 | ||
|         if ($bundle_key) {
 | ||
|           if ($key === $bundle_key) {
 | ||
|             // Set entity type this field contains bundle information for.
 | ||
|             $facet_info[$key]['field api bundles'][] = $index->getEntityType();
 | ||
|           }
 | ||
|           else {
 | ||
|             // Add "bundle" as possible dependency plugin.
 | ||
|             $facet_info[$key]['dependency plugins'][] = 'bundle';
 | ||
|           }
 | ||
|         }
 | ||
|       }
 | ||
|     }
 | ||
|   }
 | ||
|   return $facet_info;
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * Implements hook_facetapi_adapters().
 | ||
|  */
 | ||
| function search_api_facetapi_facetapi_adapters() {
 | ||
|   return array(
 | ||
|     'search_api' => array(
 | ||
|       'handler' => array(
 | ||
|         'class' => 'SearchApiFacetapiAdapter',
 | ||
|       ),
 | ||
|     ),
 | ||
|   );
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * Implements hook_facetapi_query_types().
 | ||
|  */
 | ||
| function search_api_facetapi_facetapi_query_types() {
 | ||
|   return array(
 | ||
|     'search_api_term' => array(
 | ||
|       'handler' => array(
 | ||
|         'class' => 'SearchApiFacetapiTerm',
 | ||
|         'adapter' => 'search_api',
 | ||
|       ),
 | ||
|     ),
 | ||
|     'search_api_date' => array(
 | ||
|       'handler' => array(
 | ||
|         'class' => 'SearchApiFacetapiDate',
 | ||
|         'adapter' => 'search_api',
 | ||
|       ),
 | ||
|     ),
 | ||
|   );
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * Implements hook_search_api_query_alter().
 | ||
|  *
 | ||
|  * Adds Facet API support to the query.
 | ||
|  */
 | ||
| function search_api_facetapi_search_api_query_alter($query) {
 | ||
|   $index = $query->getIndex();
 | ||
|   if ($index->server()->supportsFeature('search_api_facets')) {
 | ||
|     // This is the main point of communication between the facet system and the
 | ||
|     // search back-end - it makes the query respond to active facets.
 | ||
|     $searcher = 'search_api@' . $index->machine_name;
 | ||
|     $adapter = facetapi_adapter_load($searcher);
 | ||
|     if ($adapter) {
 | ||
|       $adapter->addActiveFilters($query);
 | ||
|     }
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * Menu callback for the facet settings page.
 | ||
|  */
 | ||
| function search_api_facetapi_settings($realm_name, SearchApiIndex $index) {
 | ||
|   if (!$index->enabled) {
 | ||
|     return array('#markup' => t('Since this index is at the moment disabled, no facets can be activated.'));
 | ||
|   }
 | ||
|   if (!$index->server()->supportsFeature('search_api_facets')) {
 | ||
|     return array('#markup' => t('This index uses a server that does not support facet functionality.'));
 | ||
|   }
 | ||
|   $searcher_name = 'search_api@' . $index->machine_name;
 | ||
|   module_load_include('inc', 'facetapi', 'facetapi.admin');
 | ||
|   return drupal_get_form('facetapi_realm_settings_form', $searcher_name, $realm_name);
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * Gets hierarchy information for taxonomy terms.
 | ||
|  *
 | ||
|  * Used as a hierarchy callback in search_api_facetapi_facetapi_facet_info().
 | ||
|  *
 | ||
|  * Internally just uses facetapi_get_taxonomy_hierarchy(), but makes sure that
 | ||
|  * our special "!" value is not passed.
 | ||
|  *
 | ||
|  * @param array $values
 | ||
|  *   An array containing the term IDs.
 | ||
|  *
 | ||
|  * @return array
 | ||
|  *   An associative array mapping term IDs to parent IDs (where parents could be
 | ||
|  *   found).
 | ||
|  */
 | ||
| function search_api_facetapi_get_taxonomy_hierarchy(array $values) {
 | ||
|   $values = array_filter($values, 'is_numeric');
 | ||
|   return $values ? facetapi_get_taxonomy_hierarchy($values) : array();
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * Map callback for all search_api facet fields.
 | ||
|  *
 | ||
|  * @param array $values
 | ||
|  *   The values to map.
 | ||
|  * @param array $options
 | ||
|  *   An associative array containing:
 | ||
|  *   - field: Field information, as stored in the index, but with an additional
 | ||
|  *     "key" property set to the field's internal name.
 | ||
|  *   - index id: The machine name of the index for this facet.
 | ||
|  *   - map callback: (optional) A callback that will be called at the beginning,
 | ||
|  *     which allows initial mapping of filters. Only values not mapped by that
 | ||
|  *     callback will be processed by this method.
 | ||
|  *   - value callback: A callback used to map single values and the limits of
 | ||
|  *     ranges. The signature is the same as for this function, but all values
 | ||
|  *     will be single values.
 | ||
|  *   - missing label: (optional) The label used for the "missing" facet.
 | ||
|  *
 | ||
|  * @return array
 | ||
|  *   An array mapping raw filter values to their labels.
 | ||
|  */
 | ||
| function search_api_facetapi_facet_map_callback(array $values, array $options = array()) {
 | ||
|   $map = array();
 | ||
|   // See if we have an additional map callback.
 | ||
|   if (isset($options['map callback']) && is_callable($options['map callback'])) {
 | ||
|     $map = call_user_func($options['map callback'], $values, $options);
 | ||
|   }
 | ||
| 
 | ||
|   // Then look at all unmapped values and save information for them.
 | ||
|   $mappable_values = array();
 | ||
|   $ranges = array();
 | ||
|   foreach ($values as $value) {
 | ||
|     $value = (string) $value;
 | ||
|     if (isset($map[$value])) {
 | ||
|       continue;
 | ||
|     }
 | ||
|     if ($value == '!') {
 | ||
|       // The "missing" filter is usually always the same, but we allow an easy
 | ||
|       // override via the "missing label" map option.
 | ||
|       $map['!'] = isset($options['missing label']) ? $options['missing label'] : '(' . t('none') . ')';
 | ||
|       continue;
 | ||
|     }
 | ||
|     $length = strlen($value);
 | ||
|     if ($length > 5 && $value[0] == '[' && $value[$length - 1] == ']' && ($pos = strpos($value, ' TO '))) {
 | ||
|       // This is a range filter.
 | ||
|       $lower = trim(substr($value, 1, $pos));
 | ||
|       $upper = trim(substr($value, $pos + 4, -1));
 | ||
|       if ($lower != '*') {
 | ||
|         $mappable_values[$lower] = TRUE;
 | ||
|       }
 | ||
|       if ($upper != '*') {
 | ||
|         $mappable_values[$upper] = TRUE;
 | ||
|       }
 | ||
|       $ranges[$value] = array(
 | ||
|         'lower' => $lower,
 | ||
|         'upper' => $upper,
 | ||
|       );
 | ||
|     }
 | ||
|     else {
 | ||
|       // A normal, single-value filter.
 | ||
|       $mappable_values[$value] = TRUE;
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   if ($mappable_values) {
 | ||
|     $map += call_user_func($options['value callback'], array_keys($mappable_values), $options);
 | ||
|   }
 | ||
| 
 | ||
|   foreach ($ranges as $value => $range) {
 | ||
|     $lower = isset($map[$range['lower']]) ? $map[$range['lower']] : $range['lower'];
 | ||
|     $upper = isset($map[$range['upper']]) ? $map[$range['upper']] : $range['upper'];
 | ||
|     if ($lower == '*' && $upper == '*') {
 | ||
|       $map[$value] =  t('any');
 | ||
|     }
 | ||
|     elseif ($lower == '*') {
 | ||
|       $map[$value] = "< $upper";
 | ||
|     }
 | ||
|     elseif ($upper == '*') {
 | ||
|       $map[$value] = "> $lower";
 | ||
|     }
 | ||
|     else {
 | ||
|       $map[$value] = "$lower – $upper";
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   return $map;
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * Creates a human-readable label for single facet filter values.
 | ||
|  *
 | ||
|  * @param array $values
 | ||
|  *   The values for which labels should be returned.
 | ||
|  * @param array $options
 | ||
|  *   An associative array containing the following information about the facet:
 | ||
|  *   - field: Field information, as stored in the index, but with an additional
 | ||
|  *     "key" property set to the field's internal name.
 | ||
|  *   - index id: The machine name of the index for this facet.
 | ||
|  *   - map callback: (optional) A callback that will be called at the beginning,
 | ||
|  *     which allows initial mapping of filters. Only values not mapped by that
 | ||
|  *     callback will be processed by this method.
 | ||
|  *   - value callback: A callback used to map single values and the limits of
 | ||
|  *     ranges. The signature is the same as for this function, but all values
 | ||
|  *     will be single values.
 | ||
|  *   - missing label: (optional) The label used for the "missing" facet.
 | ||
|  *
 | ||
|  * @return array
 | ||
|  *   An array mapping raw facet values to their labels.
 | ||
|  */
 | ||
| function _search_api_facetapi_facet_create_label(array $values, array $options) {
 | ||
|   $field = $options['field'];
 | ||
|   $map = array();
 | ||
|   $n = count($values);
 | ||
| 
 | ||
|   // For entities, we can simply use the entity labels.
 | ||
|   if (isset($field['entity_type'])) {
 | ||
|     $type = $field['entity_type'];
 | ||
|     $entities = entity_load($type, $values);
 | ||
|     foreach ($entities as $id => $entity) {
 | ||
|       $label = entity_label($type, $entity);
 | ||
|       if ($label) {
 | ||
|         $map[$id] = $label;
 | ||
|       }
 | ||
|     }
 | ||
|     if (count($map) == $n) {
 | ||
|       return $map;
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   // Then, we check whether there is an options list for the field.
 | ||
|   $index = search_api_index_load($options['index id']);
 | ||
|   $wrapper = $index->entityWrapper();
 | ||
|   $values = drupal_map_assoc($values);
 | ||
|   foreach (explode(':', $field['key']) as $part) {
 | ||
|     if (!isset($wrapper->$part)) {
 | ||
|       $wrapper = NULL;
 | ||
|       break;
 | ||
|     }
 | ||
|     $wrapper = $wrapper->$part;
 | ||
|     while (($info = $wrapper->info()) && search_api_is_list_type($info['type'])) {
 | ||
|       $wrapper = $wrapper[0];
 | ||
|     }
 | ||
|   }
 | ||
|   if ($wrapper && ($options_list = $wrapper->optionsList('view'))) {
 | ||
|     // We have no use for empty strings, as then the facet links would be
 | ||
|     // invisible.
 | ||
|     $map += array_intersect_key(array_filter($options_list, 'strlen'), $values);
 | ||
|     if (count($map) == $n) {
 | ||
|       return $map;
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   // As a "last resort" we try to create a label based on the field type, for
 | ||
|   // all values that haven't got a mapping yet.
 | ||
|   foreach (array_diff_key($values, $map) as $value) {
 | ||
|     switch ($field['type']) {
 | ||
|       case 'boolean':
 | ||
|         $map[$value] = $value ? t('true') : t('false');
 | ||
|         break;
 | ||
|       case 'date':
 | ||
|         $v = is_numeric($value) ? $value : strtotime($value);
 | ||
|         $map[$value] = format_date($v, 'short');
 | ||
|         break;
 | ||
|       case 'duration':
 | ||
|         $map[$value] = format_interval($value);
 | ||
|         break;
 | ||
|     }
 | ||
|   }
 | ||
|   return $map;
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * Implements hook_form_FORM_ID_alter().
 | ||
|  */
 | ||
| function search_api_facetapi_form_search_api_admin_index_fields_alter(&$form, &$form_state) {
 | ||
|   $form['#submit'][] = 'search_api_facetapi_search_api_admin_index_fields_submit';
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * Form submission handler for search_api_admin_index_fields().
 | ||
|  */
 | ||
| function search_api_facetapi_search_api_admin_index_fields_submit($form, &$form_state) {
 | ||
|   // Clears this searcher's cached facet definitions.
 | ||
|   $cid = 'facetapi:facet_info:search_api@' . $form_state['index']->machine_name . ':';
 | ||
|   cache_clear_all($cid, 'cache', TRUE);
 | ||
| }
 | 
