You are here

adapter.inc in Facet API 6.3

Same filename and directory in other branches
  1. 7.2 plugins/facetapi/adapter.inc
  2. 7 plugins/facetapi/adapter.inc

Adapter plugin and adapter related calsses.

File

plugins/facetapi/adapter.inc
View source
<?php

/**
 * @file
 * Adapter plugin and adapter related calsses.
 */

/**
 * Abstract class extended by search backends that retrieves facet information
 * from the database.
 */
abstract class FacetapiAdapter {

  /**
   * Searcher information retrieved by the hook.
   *
   * @var array
   */
  protected $info = array();

  /**
   * The search keys passed by the user.
   *
   * @var string
   */
  protected $keys;

  /**
   * An array of FacetapiFacet objects.
   *
   * @var array
   */
  protected $facets = array();

  /**
   * An array of FacetapiFacetProcessor objects.
   *
   * @var array
   */
  protected $processors = array();

  /**
   * An array of executed query type plugins keyed by field name.
   *
   * @var array
   */
  protected $queryTypes = array();

  /**
   * The URL processor associated with this adapter.
   *
   * @var FacetapiUrlProcessor
   */
  protected $urlProcessor;

  /**
   * An array of active filters.
   *
   * @var array
   */
  protected $activeItems;

  /**
   * A boolean flagging whether the facets have been processed.
   *
   * @var boolean
   */
  protected $processed = FALSE;

  /**
   * Stores the search path so we only need to calculate it once.
   *
   * @var string
   */
  protected $searchPath;

  /**
   * Stores whether each facet passed dependencies.
   *
   * @var array
   */
  protected $dependenciesPassed = array();

  /**
   * Stores settings with defaults.
   *
   * @var array
   */
  protected $settings = array();

  /**
   * Constructor, sets searcher and type of content being indexed.
   *
   * @param array $searcher_info
   *   The searcher definition.
   */
  public function __construct(array $searcher_info) {
    $this->info = $searcher_info;

    // Registers the query type plugins classes associated with this adapter.
    $registered_types = array();
    foreach (ctools_get_plugins('facetapi', 'query_types') as $plugin) {
      if (isset($searcher_info['adapter']) && isset($plugin['handler']['adapter']) && $searcher_info['adapter'] == $plugin['handler']['adapter']) {
        $class = ctools_plugin_get_class($plugin, 'handler');
        $type = call_user_func(array(
          $class,
          'getType',
        ));
        $registered_types[$type] = $class;
      }
    }

    // Iterates over facets and registers query type plugins.
    foreach ($this
      ->getEnabledFacets() as $facet) {

      // Gets widget type from setting if there are more than one available.
      if (1 == count($facet['query types'])) {
        $query_type = $facet['query types'][0];
      }
      else {
        $settings = $this
          ->getFacetSettingsGlobal($facet)->settings;
        $query_type = !empty($settings['query_type']) ? $settings['query_type'] : FALSE;
      }

      // If we found a query type, register the query type plugin.
      if ($query_type && isset($registered_types[$query_type])) {
        $plugin = new $registered_types[$query_type]($this, $facet);
        $this->queryTypes[$facet['name']] = $plugin;
      }
      else {
        $this->queryTypes[$facet['name']] = FALSE;
      }
    }

    // Instantiates URL processor plugin.
    $id = $searcher_info['url processor'];
    $class = ctools_plugin_load_class('facetapi', 'url_processors', $id, 'handler');
    if (!$class) {
      $class = ctools_plugin_load_class('facetapi', 'url_processors', 'standard', 'handler');
    }
    $this->urlProcessor = new $class($this);

    // Fetches, normalizes, and sets filter params.
    $filter_key = $this->urlProcessor
      ->getFilterKey();
    $params = $this->urlProcessor
      ->fetchParams();
    $this
      ->setParams($params, $filter_key);
  }

  /**
   * Returns a boolean flagging whether $this->info['searcher'] executed a search.
   *
   * @return
   *   A boolean flagging whether $this->info['searcher'] executed a search.
   *
   * @todo Generic search API should provide consistent functionality.
   */
  public abstract function searchExecuted();

  /**
   * Returns a boolean flagging whether facets in a realm shoud be displayed.
   *
   * Useful, for example, for suppressing sidebar blocks in some cases.
   *
   * @return
   *   A boolean flagging whether to display a given realm.
   *
   * @todo Generic search API should provide consistent functionality.
   */
  public abstract function suppressOutput($realm_name);

  /**
   * Processes a raw array of active filters.
   *
   * @param array $params
   *   An array of keyed params, such as $_GET.
   * @param string $filter_key
   *   The array key in $params corresponding to filters.
   *
   * @return FacetapiAdapter
   *   An instance of this class.
   */
  public function setParams(array $params = array(), $filter_key = 'f') {
    $this->facets = array();
    $normalized = $this->urlProcessor
      ->normalizeParams($params, $filter_key);
    $this->urlProcessor
      ->setParams($normalized, $filter_key);
    $this
      ->processActiveItems();
    return $this;
  }

  /**
   * Processes active items.
   *
   * @see FacetapiAdapter::setParams()
   */
  public function processActiveItems() {
    $this->activeItems = array(
      'facet' => array(),
      'filter' => array(),
    );

    // Groups enabled facets by facet alias.
    $enabled_aliases = array();
    foreach ($this
      ->getEnabledFacets() as $facet) {
      $enabled_aliases[$facet['field alias']][] = $facet['name'];
      $this->activeItems['facet'][$facet['name']] = array();
    }

    // Extracts valid filters from query string.
    $filter_key = $this->urlProcessor
      ->getFilterKey();
    $params = $this->urlProcessor
      ->getParams();
    foreach ($params[$filter_key] as $pos => $filter) {

      // Bails if an object or array.
      if (!is_scalar($filter)) {
        continue;
      }

      // Performs basic parsing of the filter.
      $parts = explode(':', $filter, 2);
      $field_alias = rawurldecode($parts[0]);
      if (isset($parts[1]) && isset($enabled_aliases[$field_alias])) {

        // Stores the base item.
        $item = array(
          'field alias' => $field_alias,
          'value' => $parts[1],
          'pos' => $pos,
        );

        // Stores active items in the global active item array.
        $this->activeItems['filter'][$filter] = $item;
        $this->activeItems['filter'][$filter]['facets'] = array();

        // Stores active items per facet.
        foreach ($enabled_aliases[$field_alias] as $facet_name) {
          $item += $this->queryTypes[$facet_name]
            ->extract($item);
          $this->activeItems['filter'][$filter]['facets'][] = $facet_name;
          $this->activeItems['facet'][$facet_name][$parts[1]] = $item;
        }
      }
    }
  }

  /**
   * Returns the URL Processor.
   *
   * @return FacetapiUrlProcessor
   *   The URL Processor plugin.
   */
  public function getUrlProcessor() {
    return $this->urlProcessor;
  }

  /**
   * Returns all active filters.
   *
   * @return array
   *   An array of active filters.
   */
  public function getAllActiveItems() {
    return $this->activeItems['filter'];
  }

  /**
   * Returns a facet's active items.
   *
   * @param array|string $facet
   *   The facet definition or facet name,
   *
   * @return array
   *   The facet's active items.
   */
  public function getActiveItems(array $facet) {
    return $this->activeItems['facet'][$facet['name']];
  }

  /**
   * Tests whether a facet item is active by passing it's value.
   *
   * @param string $facet_name
   *   The facet name.
   * @param string $value
   *   The facet item's value.
   *
   * @return
   *   Returns 1 if the item is active, 0 if it is inactive.
   */
  public function itemActive($facet_name, $value) {
    return (int) isset($this->activeItems['facet'][$facet_name][$value]);
  }

  /**
   * Returns the id of the adapter plugin.
   *
   * @return string
   *   The machine readable if of the adapter plugin.
   */
  public function getId() {
    return $this->info['adapter'];
  }

  /**
   * Returns the machine readable name of the searcher.
   *
   * @return string
   *   The machine readable name of the searcher.
   */
  public function getSearcher() {
    return $this->info['name'];
  }

  /**
   * Returns the type of content indexed by $this->info['searcher'].
   *
   * @return
   *   The type of content indexed by $this->info['searcher'].
   */
  public function getTypes() {
    return $this->info['types'];
  }

  /**
   * Returns the path to the admin settings for a given realm.
   *
   * @param $realm_name
   *   The name of the realm.
   *
   * @return
   *   The path to the admin settings.
   */
  public function getPath($realm_name) {
    return $this->info['path'] . '/facets/' . $realm_name;
  }

  /**
   * Returns the search path.
   *
   * @return string
   *   A string containing the search path.
   *
   * @todo D8 should provide an API function for this.
   */
  public function getSearchPath() {
    if (NULL === $this->searchPath) {

      // Backwards compatibility with apachesolr <= beta8.
      // @see http://drupal.org/node/1305748#comment-5102352
      foreach (array(
        $this->info['module'],
        $this->info['module'] . '_search',
      ) as $module) {
        if ($path = module_invoke($module, 'search_info')) {
          $this->searchPath = 'search/' . $path['path'];
          if (!isset($_GET['keys']) && ($keys = $this
            ->getSearchKeys())) {
            $this->searchPath .= '/' . $keys;
          }
          break;
        }
      }
    }
    return $this->searchPath;
  }

  /**
   * Sets the search keys.
   *
   * @param string $keys
   *   The search keys entered by the user.
   *
   * @return FacetapiAdapter
   *   An instance of this class.
   */
  public function setSearchKeys($keys) {
    $this->keys = $keys;
    return $this;
  }

  /**
   * Gets the search keys.
   *
   * @return string
   *   The search keys entered by the user.
   */
  public function getSearchKeys() {
    return $this->keys;
  }

  /**
   * Returns the number of results returned by the search query.
   *
   * @return int
   *   An integer containing the number of results.
   */
  public function getResultCount() {
    global $pager_total;
    return isset($pager_total[0]) ? $pager_total[0] : 0;
  }

  /**
   * Returns the number of results per page.
   *
   * @return int
   *   The number of results per page, or the limit.
   */
  public function getPageLimit() {
    global $pager_limits;
    return isset($pager_limits[0]) ? $pager_limits[0] : 10;
  }

  /**
   * Returns the page number of the search result set.
   *
   * @return int
   *   The current page of the result set.
   */
  public function getPageNumber() {
    return (isset($_GET['page']) ? $_GET['page'] : 0) + 1;
  }

  /**
   * Returns the total number of pages in the result set.
   *
   * @return int
   *   The total number of pages.
   */
  public function getPageTotal() {
    global $pager_total;
    return isset($pager_total[0]) ? $pager_total[0] : 0;
  }

  /**
   * Allows for backend specific overrides to the settings form.
   */
  public function settingsForm(&$form_state) {

    // Nothing to do...
  }

  /**
   * Provides default values for the backend specific settings.
   *
   * @return array
   *   The defaults keyed by setting name to value.
   */
  public function getDefaultSettings() {
    return array();
  }

  /**
   * Returns TRUE if the back-end supports "missing" facets.
   *
   * @return bool
   *   TRUE or FALSE.
   */
  public function supportsFacetMissing() {
    return $this->info['supports facet missing'];
  }

  /**
   * Returns TRUE if the back-end supports "minimum facet counts".
   *
   * @return bool
   *   TRUE or FALSE.
   */
  public function supportsFacetMincount() {
    return $this->info['supports facet mincount'];
  }

  /**
   * Adds facet query type plugins to the queue and invokes the execute() hook
   * to allow for the backend to add filters to its native query object.
   *
   * @param mixed $query
   *   The backend's native object.
   */
  function addActiveFilters($query) {
    module_load_include('inc', 'facetapi', 'facetapi.callbacks');
    facetapi_add_active_searcher($this->info['name']);

    // Runs initActiveFilters hook, finds active facets.
    $this
      ->initActiveFilters($query);
    foreach ($this
      ->getEnabledFacets() as $facet) {
      $settings = $this
        ->getFacet($facet)
        ->getSettings();

      // Invoke the dependency plugins.
      $display = TRUE;
      foreach ($facet['dependency plugins'] as $id) {
        $class = ctools_plugin_load_class('facetapi', 'dependencies', $id, 'handler');
        $plugin = new $class($id, $this, $facet, $settings, $this->activeItems['facet']);
        if (NULL !== ($return = $plugin
          ->execute())) {
          $display = $return;
        }
      }

      // Stores whether this facet passed its dependencies.
      $this->dependenciesPassed[$facet['name']] = $display;

      // Add query type plugin if dependencies were met, otherwise remove the
      // facet's active items so they don't display in the current search block
      // or appear as active in the breadcrumb trail.
      if ($display && $this->queryTypes[$facet['name']]) {
        $this->queryTypes[$facet['name']]
          ->execute($query);
      }
      else {
        foreach ($this->activeItems['facet'][$facet['name']] as $item) {
          $this->urlProcessor
            ->removeParam($item['pos']);
          $filter = $item['field alias'] . ':' . $item['value'];
          unset($this->activeItems['filter'][$filter]);
        }
        $this->activeItems['facet'][$facet['name']] = array();
      }
    }
  }

  /**
   * Allows the backend to initialize its query object before adding the facet
   * filters.
   *
   * @param mixed $query
   *   The backend's native object.
   */
  public function initActiveFilters($query) {

    // Nothing to do ...
  }

  /**
   * Initializes a new settings object.
   *
   * @param $name
   *   A string containing the unique name of the configuration.
   * @param array $facet_name
   *   A string containing the machine readable name of the facet.
   * @param $realm_name
   *   A string containing the machine readable name of the realm, NULL if we
   *   are initializing global settings.
   *
   * @return stdClass
   *   An object containing the initialized settings.
   */
  public function initSettingsObject($name, $facet_name, $realm_name = NULL) {
    $cached_settings = facetapi_get_searcher_settings($this->info['name']);
    if (!isset($cached_settings[$name])) {
      $settings = ctools_export_crud_new('facetapi');
      $settings->name = $name;
      $settings->searcher = $this->info['name'];
      $settings->realm = (string) $realm_name;
      $settings->facet = $facet_name;
      $settings->enabled = 0;
      $settings->settings = array();
    }
    else {
      $settings = $cached_settings[$name];
    }
    return $settings;
  }

  /**
   * Returns realm specific settings for a facet.
   *
   * @param array $facet
   *   An array containing the facet definition.
   * @param array $realm
   *   An array containing the realm definition.
   *
   * @return stdClass
   *   An object containing the settings.
   *
   * @see ctools_export_crud_load()
   */
  public function getFacetSettings(array $facet, array $realm) {

    // Builds the unique name of the configuration settings and loads.
    $name = $this->info['name'] . ':' . $realm['name'] . ':' . $facet['name'];
    if (!isset($this->settings[$name])) {
      $this->settings[$name] = $this
        ->initSettingsObject($name, $facet['name'], $realm['name']);
      $is_new = empty($this->settings[$name]->settings);

      // Use realm's default widget if facet doesn't define one.
      if (!empty($facet['default widget'])) {
        $widget = $facet['default widget'];
      }
      else {
        $widget = $realm['default widget'];
      }

      // Apply default settings.
      $this->settings[$name]->settings += array(
        'weight' => 0,
        'widget' => $widget,
        'filters' => array(),
        'active_sorts' => array(),
        'sort_weight' => array(),
        'sort_order' => array(),
        'empty_behavior' => 'none',
      );

      // Apply default sort info if necessary.
      if ($is_new) {
        $weight = -50;
        foreach ($facet['default sorts'] as $sort => $default) {
          $this->settings[$name]->settings['active_sorts'][$default[0]] = $default[0];
          $this->settings[$name]->settings['sort_weight'][$default[0]] = $weight++;
          $this->settings[$name]->settings['sort_order'][$default[0]] = $default[1];
        }
      }

      // Apply the widget plugin's default settings.
      $id = $this->settings[$name]->settings['widget'];
      $class = ctools_plugin_load_class('facetapi', 'widgets', $id, 'handler');

      // If we have an invalid widget, fall back to the realm's default.
      if (!$class) {
        $id = $this->settings[$name]->settings['widget'] = $realm['default widget'];
        $class = ctools_plugin_load_class('facetapi', 'widgets', $id, 'handler');
      }
      $plugin = new $class($id, $realm, $this
        ->getFacet($facet), $this->settings[$name]);
      $this->settings[$name]->settings += $plugin
        ->getDefaultSettings();

      // @todo Save for performance?
    }
    return $this->settings[$name];
  }

  /**
   * Returns realm specific settings for a facet.
   *
   * @param array $facet
   *   An array containing the facet definition.
   *
   * @return
   *   An object containing the settings.
   *
   * @see ctools_export_crud_load()
   */
  public function getFacetSettingsGlobal(array $facet) {
    $name = $this->info['name'] . '::' . $facet['name'];
    if (!isset($this->settings[$name])) {
      $this->settings[$name] = $this
        ->initSettingsObject($name, $facet['name']);
      $is_new = empty($this->settings[$name]->settings);

      // Ensure the default operator and query type are valid.
      // @see http://drupal.org/node/1443340
      $default_query_type = reset($facet['query types']);
      $allowed_operators = array_filter($facet['allowed operators']);
      $default_operator = key($allowed_operators);

      // Apply defaults common across all configs.
      $this->settings[$name]->settings += array(
        'operator' => $default_operator,
        'hard_limit' => 50,
        'dependencies' => array(),
        'facet_mincount' => 1,
        'facet_missing' => 0,
        'flatten' => 0,
        'query_type' => $default_query_type,
      );

      // Apply the adapter's default settings.
      $this->settings[$name]->settings += $this
        ->getDefaultSettings();

      // Applies each dependency plugin's default settings.
      foreach ($facet['dependency plugins'] as $id) {
        if ($is_new) {
          $this->settings[$name]->settings['dependencies'] = array();
        }
        $class = ctools_plugin_load_class('facetapi', 'dependencies', $id, 'handler');
        $plugin = new $class($id, $this, $facet, $this->settings[$name], array());
        $this->settings[$name]->settings['dependencies'] += $plugin
          ->getDefaultSettings();
      }

      // @todo Save for performance?
    }
    return $this->settings[$name];
  }

  /**
   * Returns the enabled facets associated with the instance of the adapter.
   *
   * @param string $realm_name
   *   The machine readable name of the realm, pass NULL to get the enabled
   *   facets in all realms.
   *
   * @return array
   *   An array of enabled facets.
   */
  public function getEnabledFacets($realm_name = NULL) {
    return facetapi_get_enabled_facets($this->info['name'], $realm_name);
  }

  /**
   * Returns a FacetapiFacet instance for the facet being rendered.
   *
   * @param array $facet
   *   The facet definition.
   *
   * @return FacetapiFacet
   *   The facet rendering object object.
   */
  public function getFacet(array $facet) {
    if (!isset($this->facets[$facet['name']])) {
      $this->facets[$facet['name']] = new FacetapiFacet($this, $facet);
    }
    return $this->facets[$facet['name']];
  }

  /**
   * Returns a registered facet query
   *
   * @param array|string $facet
   *   The facet definition or facet name.
   *
   * @return FacetapiQueryTypeInterface
   *   The instantiated query type plugin.
   */
  public function getFacetQuery($facet) {
    $facet_name = is_array($facet) ? $facet['name'] : $facet;
    if (isset($this->queryTypes[$facet_name])) {
      return $this->queryTypes[$facet_name];
    }
  }

  /**
   * Returns the human readable value associated with a facet's raw value.
   *
   * @param string $facet_name
   *   The machine readable name of the facet.
   * @param string $value
   *   The raw value passed through the query string.
   *
   * @return string
   *   The mapped value.
   */
  public function getMappedValue($facet_name, $value) {
    if (isset($this->processors[$facet_name])) {
      return $this->processors[$facet_name]
        ->getMappedValue($value);
    }
    else {
      return array(
        '#value' => $value,
      );
    }
  }

  /**
   * Returns the processor associates with the facet.
   *
   * @param string $facet_name
   *   The machine readable name of the facet.
   *
   * @return FacetapiFacetProcessor
   */
  public function getProcessor($facet_name) {
    if (isset($this->processors[$facet_name])) {
      return $this->processors[$facet_name];
    }
    else {
      return FALSE;
    }
  }

  /**
   * Helper function that returns the query string variables for a facet item.
   *
   * @param array $facet
   *   The facet definition.
   * @param array $values
   *   An array containing the item's values being added to or removed from the
   *   query string dependent on whether or not the item is active.
   * @param int $active
   *   An integer flagging whether the item is active or not.
   *
   * @return array
   *   The query string vriables.
   */
  public function getQueryString(array $facet, array $values, $active) {
    return $this->urlProcessor
      ->getQueryString($facet, $values, $active);
  }

  /**
   * Helper function that returns the facet path for a facet item.
   *
   * @param array $facet
   *   The facet definition.
   * @param array $values
   *   An array containing the item's values being added to or removed from the
   *   query string dependent on whether or not the item is active.
   * @param int $active
   *   An integer flagging whether the item is active or not.
   *
   * @return string
   *   The facet path.
   */
  public function getFacetPath(array $facet, array $values, $active) {
    return $this->urlProcessor
      ->getFacetPath($facet, $values, $active);
  }

  /**
   * Initializes facet builds, adds breadcrumb trail.
   */
  public function processFacets() {
    if (!$this->processed) {
      $this->processed = TRUE;

      // Initializes each facet's render array.
      foreach ($this
        ->getEnabledFacets() as $facet) {
        $processor = new FacetapiFacetProcessor($this
          ->getFacet($facet));
        $this->processors[$facet['name']] = $processor;
        $this->processors[$facet['name']]
          ->process();
      }

      // Sets the breadcrumb trail if a search was executed.
      if ($this
        ->searchExecuted()) {
        $this->urlProcessor
          ->setBreadcrumb();
      }
    }
  }

  /**
   * Builds the render array for facets in a realm.
   *
   * @param string $realm_name
   *   The machine readable name of the realm.
   *
   * @return array
   *   The render array.
   */
  public function buildRealm($realm_name) {

    // Bails if realm isn't valid.
    // @todo Call watchdog()?
    if (!($realm = facetapi_realm_load($realm_name))) {
      return array();
    }

    // Makes sure facet builds are initialized.
    $this
      ->processFacets();

    // Adds JavaScript, initializes render array.
    drupal_add_js(drupal_get_path('module', 'facetapi') . '/facetapi.js');
    $build = array(
      '#adapter' => $this,
      '#realm' => $realm,
    );

    // Builds each facet in the realm, merges into realm's render array.
    foreach ($this
      ->getEnabledFacets($realm['name']) as $facet) {

      // Continue to the next facet if this one failed its dependencies.
      if (!$this->dependenciesPassed[$facet['name']]) {
        continue;
      }

      // Gets the initialized build.
      $field_alias = $facet['field alias'];
      $processor = $this->processors[$facet['name']];
      $facet_build = $this
        ->getFacet($facet)
        ->build($realm, $processor);

      // Tries to be smart when merging the render arrays. Crazy things happen
      // when merging facets with the same field alias such as taxonomy terms in
      // the fieldset realm. We want to merge only the values.
      foreach (element_children($facet_build) as $child) {

        // Bails if there is nothing to render.
        if (!element_children($facet_build[$child])) {
          continue;
        }

        // Attempts to merge gracefully.
        if (!isset($build[$child])) {
          $build = array_merge_recursive($build, $facet_build);
        }
        else {
          if (isset($build[$child][$field_alias]) && isset($facet_build[$child][$field_alias])) {
            $build[$child][$field_alias] = array_merge_recursive($build[$child][$field_alias], $facet_build[$child][$field_alias]);
          }
          elseif (isset($build[$child]['#options']) && isset($facet_build[$child]['#options'])) {
            $build[$child]['#options'] = array_merge_recursive($build[$child]['#options'], $facet_build[$child]['#options']);
          }
          else {
            $build = array_merge_recursive($build, $facet_build);
          }
        }
      }
    }
    return $build;
  }

}

/**
 * Stores facet data, provides methods that build the facet's render array.
 */
class FacetapiFacet implements ArrayAccess {

  /**
   * The FacetapiAdapter object.
   *
   * @var FacetapiAdapter
   */
  protected $adapter;

  /**
   * The facet definition.
   *
   * @var array
   */
  protected $facet;

  /**
   * The build array for the facet items.
   *
   * @var array
   */
  protected $build = array();

  /**
   * Constructor, sets adapter and facet definition.
   *
   * @param $adapter
   *   A FacetapiAdapter object.
   * @param $facet
   *   An array containing the facet definition.
   */
  public function __construct(FacetapiAdapter $adapter, array $facet) {
    $this->adapter = $adapter;
    $this->facet = $facet;
  }

  /**
   * Whether a offset exists
   *
   * @param mixed offset
   *   An offset to check for.
   *
   * @return boolean
   */
  public function offsetExists($offset) {
    return isset($this->facet[$offset]);
  }

  /**
   * Returns the value at specified offset.
   *
   * @param mixed offset
   *   The offset to retrieve.
   *
   * @return mixed
   */
  public function offsetGet($offset) {
    return isset($this->facet[$offset]) ? $this->facet[$offset] : NULL;
  }

  /**
   * Assigns a value to the specified offset.
   *
   * @param mixed offset
   *   The offset to assign the value to.
   * @param mixed value
   *   The value to set.
   */
  public function offsetSet($offset, $value) {
    if (NULL === $offset) {
      $this->facet[] = $value;
    }
    else {
      $this->facet[$offset] = $value;
    }
  }

  /**
   * Unsets an offset.
   *
   * @param mixed offset
   *   The offset to unset.
   */
  public function offsetUnset($offset) {
    unset($this->facet[$offset]);
  }

  /**
   * Returns the adapter object.
   *
   * @return FacetapiAdapter
   *   The adapter object.
   */
  public function getAdapter() {
    return $this->adapter;
  }

  /**
   * Returns the facet definition.
   *
   * @return array
   *   An array containing the facet definition.
   */
  public function getFacet() {
    return $this->facet;
  }

  /**
   * Returns the facet definition.
   *
   * @return array
   *   An array containing the facet definition.
   */
  public function getBuild() {
    return $this->build;
  }

  /**
   * Gets facet setting for the passed realm.
   *
   * @param string|array $realm
   *   The machine readable name of the realm or realm definition. Pass null to
   *   get global settings.
   *
   * @return
   *   An object containing the settings.
   */
  public function getSettings($realm = NULL) {
    if ($realm && !is_array($realm)) {
      $realm = facetapi_realm_load($realm);
    }
    $method = $realm ? 'getFacetSettings' : 'getFacetSettingsGlobal';
    return $this->adapter
      ->{$method}($this->facet, $realm);
  }

  /**
   * Returns the facet's render array.
   *
   * @param array $realm
   *   An array containing the realm definition.
   * @param FacetapiFacetProcessor $processor
   *   The processor object.
   *
   * @return
   *   The facet's build array.
   */
  public function build(array $realm, FacetapiFacetProcessor $processor) {
    $settings = $this
      ->getSettings($realm);

    // Gets the base render array from the facet processor.
    $this->build = $processor
      ->getBuild();

    // Executes filter plugins.
    // @todo Defensive coding here for filters?
    $enabled_filters = array_filter($settings->settings['filters'], 'facetapi_filter_disabled_filters');
    uasort($enabled_filters, 'facetapi_sort_weight');
    foreach ($enabled_filters as $filter_id => $filter_settings) {
      if ($class = ctools_plugin_load_class('facetapi', 'filters', $filter_id, 'handler')) {
        $filter_plugin = new $class($filter_id, $this->adapter, $settings);
        $this->build = $filter_plugin
          ->execute($this->build);
      }
      else {
        watchdog('facetapi', 'Filter %name not valid.', array(
          '%name' => $filter_id,
        ), WATCHDOG_ERROR);
      }
    }

    // Instantiates the widget plugin and initializes.
    // @todo Defensive coding here for widgets?
    $widget_name = $settings->settings['widget'];
    if (!($class = ctools_plugin_load_class('facetapi', 'widgets', $widget_name, 'handler'))) {
      watchdog('facetapi', 'Widget %name not valid.', array(
        '%name' => $widget_name,
      ), WATCHDOG_ERROR);
      return array();
    }
    $widget_plugin = new $class($widget_name, $realm, $this, $settings);
    $widget_plugin
      ->init();
    if ($this->build) {

      // Executes widget plugin.
      $widget_plugin
        ->execute();
      $build = $widget_plugin
        ->getBuild();
    }
    else {

      // Instantiates empty behavior plugin.
      $id = $settings->settings['empty_behavior'];
      $class = ctools_plugin_load_class('facetapi', 'empty_behaviors', $id, 'handler');
      $empty_plugin = new $class($settings);

      // Executes empty behavior plugin.
      $build = $widget_plugin
        ->getBuild();
      $build[$this['field alias']] = $empty_plugin
        ->execute();
    }

    // If the element is empty, unset it.
    if (!$build[$this['field alias']]) {
      unset($build[$this['field alias']]);
    }

    // Adds JavaScript settings in a way that merges with others already set.
    $merge_settings['facetapi']['facets'][] = $widget_plugin
      ->getJavaScriptSettings();
    drupal_add_js($merge_settings, 'setting');

    // Returns array keyed by the FacetapiWidget::$key property.
    return array(
      $widget_plugin
        ->getKey() => $build,
    );
  }

}

/**
 * Processes facets, initializes the build.
 */
class FacetapiFacetProcessor {

  /**
   * An array of mapped values keyed by their raw value.
   *
   * @var $map
   */
  protected $map = array();

  /**
   * The facet being processed.
   *
   * @var FacetapiFacet
   */
  protected $facet;

  /**
   * The facet's initialized render array.
   *
   * @var array
   */
  protected $build = array();

  /**
   * Arrays of children keyed by their active parent's value.
   *
   * @var array
   */
  protected $activeChildren = array();

  /**
   * Constructor, initializes render array.
   *
   * @param FacetapiFacet $facet
   *   The facet being processed.
   *
   */
  public function __construct(FacetapiFacet $facet) {
    $this->facet = $facet;
  }

  /**
   * Processes the facet items.
   */
  public function process() {
    $this->build = array();

    // Only initializes facet if a query type plugin is registered for it.
    // NOTE: We don't use the chaining pattern so the methods can be tested.
    if ($this->facet
      ->getAdapter()
      ->getFacetQuery($this->facet
      ->getFacet())) {
      $this->build = $this
        ->initializeBuild($this->build);
      $this->build = $this
        ->mapValues($this->build);
      if ($this->build) {
        $settings = $this->facet
          ->getSettings();
        if (!$settings->settings['flatten']) {
          $this->build = $this
            ->processHierarchy($this->build);
        }
        $this
          ->processQueryStrings($this->build);
      }
    }
  }

  /**
   * Helper function to get the facet's active items.
   *
   * @return array
   *   The facet's active items.
   */
  public function getActiveItems() {
    return $this->facet
      ->getAdapter()
      ->getActiveItems($this->facet
      ->getFacet());
  }

  /**
   * Gets an active item's children.
   *
   * @param string $value
   *   The value of the active item.
   *
   * @return array
   *   The active item's childen.
   */
  public function getActiveChildren($value) {
    return isset($this->activeChildren[$value]) ? $this->activeChildren[$value] : array();
  }

  /**
   * Gets the initialized render array.
   */
  public function getBuild() {
    return $this->build;
  }

  /**
   * Returns the human readable value associated with a raw value.
   *
   * @param string $value
   *   The raw value passed through the query string.
   *
   * @return string
   *   The mapped value.
   */
  public function getMappedValue($value) {
    return isset($this->map[$value]) ? $this->map[$value] : array(
      '#value' => $value,
    );
  }

  /**
   * Initializes the facet's render array.
   *
   * @return array
   *   The initialized render array.
   */
  protected function initializeBuild() {
    $build = array();

    // Build array defaults.
    $defaults = array(
      '#value' => '',
      '#path' => $this->facet
        ->getAdapter()
        ->getSearchPath(),
      '#html' => FALSE,
      '#indexed_value' => '',
      '#count' => 0,
      '#active' => 0,
      '#item_parents' => array(),
      '#item_children' => array(),
    );

    // Builds render arrays for each item.
    $adapter = $this->facet
      ->getAdapter();
    $build = $adapter
      ->getFacetQuery($this->facet
      ->getFacet())
      ->build();

    // Invoke the alter callbacks for the facet.
    foreach ($this->facet['alter callbacks'] as $callback) {
      $callback($build, $adapter, $this->facet
        ->getFacet());
    }

    // Iterates over the render array and merges in defaults.
    foreach (element_children($build) as $value) {
      $item_defaults = array(
        '#value' => $value,
        '#indexed_value' => $value,
        '#active' => $adapter
          ->itemActive($this->facet['name'], $value),
      );
      $build[$value] = array_merge($defaults, $item_defaults, $build[$value]);
    }
    return $build;
  }

  /**
   * Maps the IDs to human readable values via the mapping callback.
   *
   * @param array $build
   *   The initialized render array.
   *
   * @return array
   *   The initialized render array with mapped values.
   */
  protected function mapValues(array $build) {
    if ($this->facet['map callback']) {

      // Gets available items and active items, runs through map callback only
      // when there are values to map.
      // NOTE: array_merge() doesn't work here when the values are numeric.
      if ($values = array_unique(array_keys($build + $this
        ->getActiveItems()))) {
        $this->map = call_user_func($this->facet['map callback'], $values, $this->facet['map options']);

        // Normalize all mapped values to a two element array.
        foreach ($this->map as $key => $value) {
          if (!is_array($value)) {
            $this->map[$key] = array();
            $this->map[$key]['#value'] = $value;
            $this->map[$key]['#html'] = FALSE;
          }
          if (isset($build[$key])) {
            $build[$key]['#value'] = $this->map[$key]['#value'];
            $build[$key]['#html'] = !empty($this->map[$key]['#html']);
          }
        }
      }
    }
    return $build;
  }

  /**
   * Processes hierarchical relationships between the facet items.
   *
   * @param array $build
   *   The initialized render array.
   *
   * @return array
   *   The initialized render array with processed hierarchical relationships.
   */
  protected function processHierarchy(array $build) {

    // Builds the hierarchy information if the hierarchy callback is defined.
    if ($this->facet['hierarchy callback']) {
      $parents = $this->facet['hierarchy callback'](array_keys($build));
      foreach ($parents as $value => $parents) {
        foreach ($parents as $parent) {
          if (isset($build[$parent]) && isset($build[$value])) {

            // Use a reference so we see the updated data.
            $build[$parent]['#item_children'][$value] =& $build[$value];
            $build[$value]['#item_parents'][$parent] = $parent;
          }
        }
      }
    }

    // Tests whether parents have an active child.
    // @todo: Can we make this more efficient?
    do {
      $active = 0;
      foreach ($build as $value => $item) {
        if ($item['#active'] && !empty($item['#item_parents'])) {

          // @todo Can we build facets with multiple parents? Core taxonomy
          // form cannot, so we will need a check here.
          foreach ($item['#item_parents'] as $parent) {
            if (!$build[$parent]['#active']) {
              $active = $build[$parent]['#active'] = 1;
            }
          }
        }
      }
    } while ($active);

    // Since the children are copied to their parent's "#item_parents" property
    // during processing, we have to filter the original child items from the
    // top level of the hierarchy.
    return array_filter($build, 'facetapi_filter_top_level_children');
  }

  /**
   * Initializes the render array's query string variables.
   *
   * @param array &$build
   *   The initialized render array.
   */
  protected function processQueryStrings(array &$build) {
    foreach ($build as $value => &$item) {
      $values = array(
        $value,
      );

      // Calculate paths for the children.
      if (!empty($item['#item_children'])) {
        $this
          ->processQueryStrings($item['#item_children']);

        // Merges the childrens' values if the item is active so the children
        // are deactivated along with the parent.
        if ($item['#active']) {
          $values = array_merge(facetapi_get_child_values($item['#item_children']), $values);
        }
      }

      // Stores this item's active children so we can deactivate them in the
      // current search block as well.
      $this->activeChildren[$value] = $values;

      // Formats path and query string for facet item, sets theme function.
      $item['#path'] = $this
        ->getFacetPath($values, $item['#active']);
      $item['#query'] = $this
        ->getQueryString($values, $item['#active']);
    }
  }

  /**
   * Helper function that returns the path for a facet item.
   *
   * @param array $values
   *   An array containing the item's values being added to or removed from the
   *   query string dependent on whether or not the item is active.
   * @param int $active
   *   An integer flagging whether the item is active or not.
   *
   * @return
   *   The facet path.
   */
  public function getFacetPath(array $values, $active) {
    return $this->facet
      ->getAdapter()
      ->getFacetPath($this->facet
      ->getFacet(), $values, $active);
  }

  /**
   * Helper function that returns the query string variables for a facet item.
   *
   * @param array $values
   *   An array containing the item's values being added to or removed from the
   *   query string dependent on whether or not the item is active.
   * @param int $active
   *   An integer flagging whether the item is active or not.
   *
   * @return
   *   An array containing the query string variables.
   */
  public function getQueryString(array $values, $active) {
    return $this->facet
      ->getAdapter()
      ->getQueryString($this->facet
      ->getFacet(), $values, $active);
  }

}

/**
 * Recursive function that returns an array of values for all descendants of a
 * facet item.
 *
 * @param $build
 *   A render array containing the facet item's children.
 *
 * @return
 *   An array containing the values of all descendants.
 */
function facetapi_get_child_values(array $build) {
  $values = array_keys($build);
  foreach ($build as $item) {
    if (!empty($item['#item_children'])) {
      $values = array_merge(facetapi_get_child_values($item['#item_children']), $values);
    }
  }
  return $values;
}

/**
 * Callback for array_filter() that strips child items at the top level.
 *
 * When hierarchies are processed, all children are copied to their parent's
 * "#item_children" property to establish the relationship. This callback
 * filters the original child items from the top level of the hierarchy so the
 * aren't also displayed along-side their parents.
 *
 * @param $build
 *   The facet item's render array.
 *
 * @return
 *   A boolean flagging whether the value should remain in the array.
 */
function facetapi_filter_top_level_children(array $build) {
  return empty($build['#item_parents']);
}

/**
 * Callback for array_filter() that strips out disabled filters.
 *
 * @param array $settings
 *   The individual filter settings.
 *
 * @return
 *   A boolean flagging whether the value should remain in the array.
 */
function facetapi_filter_disabled_filters($settings) {
  return !empty($settings['status']);
}

Functions

Namesort descending Description
facetapi_filter_disabled_filters Callback for array_filter() that strips out disabled filters.
facetapi_filter_top_level_children Callback for array_filter() that strips child items at the top level.
facetapi_get_child_values Recursive function that returns an array of values for all descendants of a facet item.

Classes

Namesort descending Description
FacetapiAdapter Abstract class extended by search backends that retrieves facet information from the database.
FacetapiFacet Stores facet data, provides methods that build the facet's render array.
FacetapiFacetProcessor Processes facets, initializes the build.