You are here

abstract class SearchApiAbstractDataSourceController in Search API 7

Provides a default base class for datasource controllers.

Contains default implementations for a number of methods which will be similar for most data sources. Concrete data sources can decide to extend this base class to save time, but can also implement the interface directly.

A subclass will still have to provide implementations for the following methods:

  • getIdFieldInfo()
  • loadItems()
  • getMetadataWrapper() or getPropertyInfo()
  • startTracking() or getAllItemIds()

The table used by default for tracking the index status of items is {search_api_item}. This can easily be changed, for example when an item type has non-integer IDs, by changing the $table property.

Hierarchy

Expanded class hierarchy of SearchApiAbstractDataSourceController

File

includes/datasource.inc, line 379
Contains the SearchApiDataSourceControllerInterface as well as a default base class.

View source
abstract class SearchApiAbstractDataSourceController implements SearchApiDataSourceControllerInterface {

  /**
   * The item type for this controller instance.
   */
  protected $type;

  /**
   * The entity type for this controller instance.
   *
   * @var string|null
   *
   * @see getEntityType()
   */
  protected $entityType = NULL;

  /**
   * The info array for the item type, as specified via
   * hook_search_api_item_type_info().
   *
   * @var array
   */
  protected $info;

  /**
   * The table used for tracking items. Set to NULL on subclasses to disable
   * the default tracking for an item type, or change the property to use a
   * different table for tracking.
   *
   * @var string
   */
  protected $table = 'search_api_item';

  /**
   * When using the default tracking mechanism: the name of the column on
   * $this->table containing the item ID.
   *
   * @var string
   */
  protected $itemIdColumn = 'item_id';

  /**
   * When using the default tracking mechanism: the name of the column on
   * $this->table containing the index ID.
   *
   * @var string
   */
  protected $indexIdColumn = 'index_id';

  /**
   * When using the default tracking mechanism: the name of the column on
   * $this->table containing the indexing status.
   *
   * @var string
   */
  protected $changedColumn = 'changed';

  /**
   * {@inheritdoc}
   */
  public function __construct($type) {
    $this->type = $type;
    $this->info = search_api_get_item_type_info($type);
    if (!empty($this->info['entity_type'])) {
      $this->entityType = $this->info['entity_type'];
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getEntityType() {
    return $this->entityType;
  }

  /**
   * {@inheritdoc}
   */
  public function getMetadataWrapper($item = NULL, array $info = array()) {
    $info += $this
      ->getPropertyInfo();
    return entity_metadata_wrapper($this->entityType ? $this->entityType : $this->type, $item, $info);
  }

  /**
   * Retrieves the property info for this item type.
   *
   * This is a helper method for getMetadataWrapper() that can be used by
   * subclasses to specify the property information to use when creating a
   * metadata wrapper.
   *
   * The data structure uses largely the format specified in
   * hook_entity_property_info(). However, the first level of keys (containing
   * the entity types) is omitted, and the "properties" key is called
   * "property info" instead. So, an example return value would look like this:
   *
   * @code
   * return array(
   *   'property info' => array(
   *     'foo' => array(
   *       'label' => t('Foo'),
   *       'type' => 'text',
   *     ),
   *     'bar' => array(
   *       'label' => t('Bar'),
   *       'type' => 'list<integer>',
   *     ),
   *   ),
   * );
   * @endcode
   *
   * SearchApiExternalDataSourceController::getPropertyInfo() contains a working
   * example of this method.
   *
   * If the item type is an entity type, no additional property information is
   * required, the method will thus just return an empty array. You can still
   * use this to append additional properties to the entities, or the like,
   * though.
   *
   * @return array
   *   Property information as specified by entity_metadata_wrapper().
   *
   * @throws SearchApiDataSourceException
   *   If any error state was encountered.
   *
   * @see getMetadataWrapper()
   * @see hook_entity_property_info()
   */
  protected function getPropertyInfo() {

    // If this is an entity type, no additional property info is needed.
    if ($this->entityType) {
      return array();
    }
    throw new SearchApiDataSourceException(t('No known property information for type @type.', array(
      '@type' => $this->type,
    )));
  }

  /**
   * {@inheritdoc}
   */
  public function getItemId($item) {
    $id_info = $this
      ->getIdFieldInfo();
    $field = $id_info['key'];
    $wrapper = $this
      ->getMetadataWrapper($item);
    if (!isset($wrapper->{$field})) {
      return NULL;
    }
    $id = $wrapper->{$field}
      ->value();
    return $id ? $id : NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getItemLabel($item) {
    $label = $this
      ->getMetadataWrapper($item)
      ->label();
    return $label ? $label : NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getItemUrl($item) {
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function startTracking(array $indexes) {
    if (!$this->table) {
      return;
    }

    // We first clear the tracking table for all indexes, so we can just insert
    // all items again without any key conflicts.
    $this
      ->stopTracking($indexes);

    // Insert all items as new.
    $this
      ->trackItemInsert($this
      ->getAllItemIds(), $indexes);
  }

  /**
   * Returns the IDs of all items that are known for this controller's type.
   *
   * Helper method that can be used by subclasses instead of implementing
   * startTracking().
   *
   * @return array
   *   An array containing all item IDs for this type.
   *
   * @throws SearchApiDataSourceException
   *   If any error state was encountered.
   */
  protected function getAllItemIds() {
    throw new SearchApiDataSourceException(t('Items not known for type @type.', array(
      '@type' => $this->type,
    )));
  }

  /**
   * {@inheritdoc}
   */
  public function stopTracking(array $indexes) {
    if (!$this->table) {
      return;
    }

    // We could also use a single query with "IN" operator, but this method
    // will mostly be called with only one index.
    foreach ($indexes as $index) {
      $this
        ->checkIndex($index);
      db_delete($this->table)
        ->condition($this->indexIdColumn, $index->id)
        ->execute();
    }
  }

  /**
   * {@inheritdoc}
   */
  public function trackItemInsert(array $item_ids, array $indexes) {
    if (!$this->table || $item_ids === array()) {
      return;
    }
    foreach ($indexes as $index) {
      $this
        ->checkIndex($index);
    }

    // Since large amounts of items can overstrain the database, only add items
    // in chunks.
    foreach (array_chunk($item_ids, 1000) as $chunk) {
      $insert = db_insert($this->table)
        ->fields(array(
        $this->itemIdColumn,
        $this->indexIdColumn,
        $this->changedColumn,
      ));
      foreach ($indexes as $index) {

        // We have to make sure we don't try to insert duplicate items.
        $select = db_select($this->table, 't');
        $select
          ->addField('t', $this->itemIdColumn);
        $select
          ->condition($this->indexIdColumn, $index->id);
        $select
          ->condition($this->itemIdColumn, $chunk, 'IN');
        $existing = $select
          ->execute()
          ->fetchCol();
        $existing = array_flip($existing);
        foreach ($chunk as $item_id) {
          if (isset($existing[$item_id])) {
            continue;
          }
          $insert
            ->values(array(
            $this->itemIdColumn => $item_id,
            $this->indexIdColumn => $index->id,
            $this->changedColumn => 1,
          ));
        }
      }
      try {
        $insert
          ->execute();
      } catch (Exception $e) {
        $tmp = array_slice($item_ids, 0, 10);
        $item_ids_string = '"' . implode('", "', $tmp) . '"';
        $index_names = array();
        foreach ($indexes as $index) {
          $index_names[] = $index->name;
        }
        $vars = array(
          '%indexes' => implode(', ', $index_names),
          '@item_ids' => $item_ids_string,
        );
        watchdog_exception('search_api', $e, '%type while tracking item inserts (IDs: @item_ids) on index(es) %indexes: !message in %function (line %line of %file).', $vars);
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE) {
    if (!$this->table || $item_ids === array()) {
      return NULL;
    }
    $indexes_by_id = array();
    foreach ($indexes as $index) {
      $this
        ->checkIndex($index);
      $update = db_update($this->table)
        ->fields(array(
        $this->changedColumn => REQUEST_TIME,
      ))
        ->condition($this->indexIdColumn, $index->id)
        ->condition($this->changedColumn, 0, $dequeue ? '<=' : '=');
      if ($item_ids !== FALSE) {
        $update
          ->condition($this->itemIdColumn, $item_ids, 'IN');
      }
      try {
        $update
          ->execute();
        $indexes_by_id[$index->id] = $index;
      } catch (Exception $e) {
        if ($item_ids === FALSE) {
          $item_ids_string = t('all');
        }
        else {
          $tmp = array_slice($item_ids, 0, 10);
          $item_ids_string = '"' . implode('", "', $tmp) . '"';
        }
        $vars = array(
          '%index' => $index->name,
          '@item_ids' => $item_ids_string,
        );
        watchdog_exception('search_api', $e, '%type while tracking item updates (IDs: @item_ids) on index %index: !message in %function (line %line of %file).', $vars);
      }
    }

    // Determine and return the indexes with any changed items. If $item_ids is
    // FALSE, all items are marked as changed and, thus, all indexes will be
    // affected (unless they don't have any items, but no real point in treating
    // that special case).
    if ($item_ids !== FALSE) {
      $indexes_with_items = db_select($this->table, 't')
        ->fields('t', array(
        $this->indexIdColumn,
      ))
        ->distinct()
        ->condition($this->indexIdColumn, array_keys($indexes_by_id), 'IN')
        ->condition($this->itemIdColumn, $item_ids, 'IN')
        ->execute()
        ->fetchCol();
      return array_intersect_key($indexes_by_id, array_flip($indexes_with_items));
    }
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function trackItemQueued($item_ids, SearchApiIndex $index) {
    $this
      ->checkIndex($index);
    if (!$this->table || $item_ids === array()) {
      return;
    }
    $update = db_update($this->table)
      ->fields(array(
      $this->changedColumn => -1,
    ))
      ->condition($this->indexIdColumn, $index->id);
    if ($item_ids !== FALSE) {
      $update
        ->condition($this->itemIdColumn, $item_ids, 'IN');
    }
    $update
      ->execute();
  }

  /**
   * {@inheritdoc}
   */
  public function trackItemIndexed(array $item_ids, SearchApiIndex $index) {
    if (!$this->table || $item_ids === array()) {
      return;
    }
    $this
      ->checkIndex($index);
    try {
      db_update($this->table)
        ->fields(array(
        $this->changedColumn => 0,
      ))
        ->condition($this->itemIdColumn, $item_ids, 'IN')
        ->condition($this->indexIdColumn, $index->id)
        ->execute();
    } catch (Exception $e) {
      $tmp = array_slice($item_ids, 0, 10);
      $item_ids_string = '"' . implode('", "', $tmp) . '"';
      $vars = array(
        '%index' => $index->name,
        '@item_ids' => $item_ids_string,
      );
      watchdog_exception('search_api', $e, '%type while tracking indexed items (IDs: @item_ids) on index %index: !message in %function (line %line of %file).', $vars);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function trackItemDelete(array $item_ids, array $indexes) {
    if (!$this->table || $item_ids === array()) {
      return NULL;
    }
    $ret = array();
    foreach ($indexes as $index) {
      $this
        ->checkIndex($index);
      $delete = db_delete($this->table)
        ->condition($this->indexIdColumn, $index->id)
        ->condition($this->itemIdColumn, $item_ids, 'IN');
      try {
        if ($delete
          ->execute()) {
          $ret[] = $index;
        }
      } catch (Exception $e) {
        $tmp = array_slice($item_ids, 0, 10);
        $item_ids_string = '"' . implode('", "', $tmp) . '"';
        $vars = array(
          '%index' => $index->name,
          '@item_ids' => $item_ids_string,
        );
        watchdog_exception('search_api', $e, '%type while tracking deleted items (IDs: @item_ids) on index %index: !message in %function (line %line of %file).', $vars);
      }
    }
    return $ret;
  }

  /**
   * {@inheritdoc}
   */
  public function getChangedItems(SearchApiIndex $index, $limit = -1) {
    if ($limit == 0) {
      return array();
    }
    $this
      ->checkIndex($index);
    $select = db_select($this->table, 't');
    $select
      ->addField('t', $this->itemIdColumn);
    $select
      ->condition($this->indexIdColumn, $index->id);
    $select
      ->condition($this->changedColumn, 0, '>');
    $select
      ->orderBy($this->changedColumn, 'ASC');
    if ($limit > 0) {
      $select
        ->range(0, $limit);
    }
    return $select
      ->execute()
      ->fetchCol();
  }

  /**
   * {@inheritdoc}
   */
  public function getIndexStatus(SearchApiIndex $index) {
    if (!$this->table) {
      return array(
        'indexed' => 0,
        'total' => 0,
      );
    }
    $this
      ->checkIndex($index);
    $indexed = db_select($this->table, 'i')
      ->condition($this->indexIdColumn, $index->id)
      ->condition($this->changedColumn, 0)
      ->countQuery()
      ->execute()
      ->fetchField();
    $total = db_select($this->table, 'i')
      ->condition($this->indexIdColumn, $index->id)
      ->countQuery()
      ->execute()
      ->fetchField();
    return array(
      'indexed' => $indexed,
      'total' => $total,
    );
  }

  /**
   * {@inheritdoc}
   */
  public function configurationForm(array $form, array &$form_state) {
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function configurationFormValidate(array $form, array &$values, array &$form_state) {
  }

  /**
   * {@inheritdoc}
   */
  public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
  }

  /**
   * {@inheritdoc}
   */
  public function getConfigurationSummary(SearchApiIndex $index) {
    return NULL;
  }

  /**
   * Checks whether the given index is valid for this datasource controller.
   *
   * Helper method used by various methods in this class. By default only checks
   * whether the types match.
   *
   * @param SearchApiIndex $index
   *   The index to check.
   *
   * @throws SearchApiDataSourceException
   *   If the index doesn't fit to this datasource controller.
   */
  protected function checkIndex(SearchApiIndex $index) {
    if ($index->item_type != $this->type) {
      $index_type = search_api_get_item_type_info($index->item_type);
      $index_type = empty($index_type['name']) ? $index->item_type : $index_type['name'];
      $msg = t('Invalid index @index of type @index_type passed to data source controller for type @this_type.', array(
        '@index' => $index->name,
        '@index_type' => $index_type,
        '@this_type' => $this->info['name'],
      ));
      throw new SearchApiDataSourceException($msg);
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
SearchApiAbstractDataSourceController::$changedColumn protected property When using the default tracking mechanism: the name of the column on $this->table containing the indexing status.
SearchApiAbstractDataSourceController::$entityType protected property The entity type for this controller instance.
SearchApiAbstractDataSourceController::$indexIdColumn protected property When using the default tracking mechanism: the name of the column on $this->table containing the index ID.
SearchApiAbstractDataSourceController::$info protected property The info array for the item type, as specified via hook_search_api_item_type_info().
SearchApiAbstractDataSourceController::$itemIdColumn protected property When using the default tracking mechanism: the name of the column on $this->table containing the item ID.
SearchApiAbstractDataSourceController::$table protected property The table used for tracking items. Set to NULL on subclasses to disable the default tracking for an item type, or change the property to use a different table for tracking. 1
SearchApiAbstractDataSourceController::$type protected property The item type for this controller instance.
SearchApiAbstractDataSourceController::checkIndex protected function Checks whether the given index is valid for this datasource controller.
SearchApiAbstractDataSourceController::configurationForm public function Form constructor for configuring the datasource for a given index. Overrides SearchApiDataSourceControllerInterface::configurationForm 2
SearchApiAbstractDataSourceController::configurationFormSubmit public function Submit callback for the form returned by configurationForm(). Overrides SearchApiDataSourceControllerInterface::configurationFormSubmit 2
SearchApiAbstractDataSourceController::configurationFormValidate public function Validation callback for the form returned by configurationForm(). Overrides SearchApiDataSourceControllerInterface::configurationFormValidate
SearchApiAbstractDataSourceController::getAllItemIds protected function Returns the IDs of all items that are known for this controller's type.
SearchApiAbstractDataSourceController::getChangedItems public function Retrieves a list of items that need to be indexed. Overrides SearchApiDataSourceControllerInterface::getChangedItems 1
SearchApiAbstractDataSourceController::getConfigurationSummary public function Returns a summary of an index's current datasource configuration. Overrides SearchApiDataSourceControllerInterface::getConfigurationSummary 2
SearchApiAbstractDataSourceController::getEntityType public function Retrieves the entity type of items from this datasource. Overrides SearchApiDataSourceControllerInterface::getEntityType
SearchApiAbstractDataSourceController::getIndexStatus public function Retrieves information on how many items have been indexed for a certain index. Overrides SearchApiDataSourceControllerInterface::getIndexStatus 1
SearchApiAbstractDataSourceController::getItemId public function Retrieves the unique ID of an item. Overrides SearchApiDataSourceControllerInterface::getItemId 3
SearchApiAbstractDataSourceController::getItemLabel public function Retrieves a human-readable label for an item. Overrides SearchApiDataSourceControllerInterface::getItemLabel 3
SearchApiAbstractDataSourceController::getItemUrl public function Retrieves a URL at which the item can be viewed on the web. Overrides SearchApiDataSourceControllerInterface::getItemUrl 3
SearchApiAbstractDataSourceController::getMetadataWrapper public function Creates a metadata wrapper for this datasource controller's type. Overrides SearchApiDataSourceControllerInterface::getMetadataWrapper 1
SearchApiAbstractDataSourceController::getPropertyInfo protected function Retrieves the property info for this item type. 2
SearchApiAbstractDataSourceController::startTracking public function Initializes tracking of the index status of items for the given indexes. Overrides SearchApiDataSourceControllerInterface::startTracking 3
SearchApiAbstractDataSourceController::stopTracking public function Stops tracking of the index status of items for the given indexes. Overrides SearchApiDataSourceControllerInterface::stopTracking 1
SearchApiAbstractDataSourceController::trackItemChange public function Sets the tracking status of the given items to "changed"/"dirty". Overrides SearchApiDataSourceControllerInterface::trackItemChange 1
SearchApiAbstractDataSourceController::trackItemDelete public function Stops tracking the index status for the given items on the given indexes. Overrides SearchApiDataSourceControllerInterface::trackItemDelete 1
SearchApiAbstractDataSourceController::trackItemIndexed public function Sets the tracking status of the given items to "indexed". Overrides SearchApiDataSourceControllerInterface::trackItemIndexed 1
SearchApiAbstractDataSourceController::trackItemInsert public function Starts tracking the index status for the given items on the given indexes. Overrides SearchApiDataSourceControllerInterface::trackItemInsert 3
SearchApiAbstractDataSourceController::trackItemQueued public function Sets the tracking status of the given items to "queued". Overrides SearchApiDataSourceControllerInterface::trackItemQueued
SearchApiAbstractDataSourceController::__construct public function Constructs an SearchApiDataSourceControllerInterface object. Overrides SearchApiDataSourceControllerInterface::__construct 1
SearchApiDataSourceControllerInterface::getIdFieldInfo public function Returns information on the ID field for this controller's type. 3
SearchApiDataSourceControllerInterface::loadItems public function Loads items of the type of this data source controller. 3