") are not allowed. * * @throws SearchApiDataSourceException * If any error state was encountered. */ public function getIdFieldInfo(); /** * Loads items of the type of this data source controller. * * @param array $ids * The IDs of the items to load. * * @return array * The loaded items, keyed by ID. * * @throws SearchApiDataSourceException * If any error state was encountered. */ public function loadItems(array $ids); /** * Creates a metadata wrapper for this datasource controller's type. * * @param mixed $item * Unless NULL, an item of the item type for this controller to be wrapped. * @param array $info * Optionally, additional information that should be used for creating the * wrapper. Uses the same format as entity_metadata_wrapper(). * * @return EntityMetadataWrapper * A wrapper for the item type of this data source controller, according to * the info array, and optionally loaded with the given data. * * @throws SearchApiDataSourceException * If any error state was encountered. * * @see entity_metadata_wrapper() */ public function getMetadataWrapper($item = NULL, array $info = array()); /** * Retrieves the unique ID of an item. * * @param mixed $item * An item of this controller's type. * * @return mixed * Either the unique ID of the item, or NULL if none is available. * * @throws SearchApiDataSourceException * If any error state was encountered. */ public function getItemId($item); /** * Retrieves a human-readable label for an item. * * @param mixed $item * An item of this controller's type. * * @return string|null * Either a human-readable label for the item, or NULL if none is available. * * @throws SearchApiDataSourceException * If any error state was encountered. */ public function getItemLabel($item); /** * Retrieves a URL at which the item can be viewed on the web. * * @param mixed $item * An item of this controller's type. * * @return array|null * Either an array containing the 'path' and 'options' keys used to build * the URL of the item, and matching the signature of url(), or NULL if the * item has no URL of its own. * * @throws SearchApiDataSourceException * If any error state was encountered. */ public function getItemUrl($item); /** * Initializes tracking of the index status of items for the given indexes. * * All currently known items of this data source's type should be inserted * into the tracking table for the given indexes, with status "changed". If * items were already present, these should also be set to "changed" and not * be inserted again. * * @param SearchApiIndex[] $indexes * The SearchApiIndex objects for which item tracking should be initialized. * * @throws SearchApiDataSourceException * If any error state was encountered. */ public function startTracking(array $indexes); /** * Stops tracking of the index status of items for the given indexes. * * The tracking tables of the given indexes should be completely cleared. * * @param SearchApiIndex[] $indexes * The SearchApiIndex objects for which item tracking should be stopped. * * @throws SearchApiDataSourceException * If any error state was encountered. */ public function stopTracking(array $indexes); /** * Starts tracking the index status for the given items on the given indexes. * * @param array $item_ids * The IDs of new items to track. * @param SearchApiIndex[] $indexes * The indexes for which items should be tracked. * * @return SearchApiIndex[]|null * All indexes for which any items were added; or NULL if items were added * for all of them. * * @throws SearchApiDataSourceException * If any error state was encountered. */ public function trackItemInsert(array $item_ids, array $indexes); /** * Sets the tracking status of the given items to "changed"/"dirty". * * Unless $dequeue is set to TRUE, this operation is ignored for items whose * status is not "indexed". * * @param array|false $item_ids * Either an array with the IDs of the changed items. Or FALSE to mark all * items as changed for the given indexes. * @param SearchApiIndex[] $indexes * The indexes for which the change should be tracked. * @param bool $dequeue * (deprecated) If set to TRUE, also change the status of queued items. * The concept of queued items will be removed in the Drupal 8 version of * this module. * * @return SearchApiIndex[]|null * All indexes for which any items were updated; or NULL if items were * updated for all of them. * * @throws SearchApiDataSourceException * If any error state was encountered. */ public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE); /** * Sets the tracking status of the given items to "queued". * * Queued items are not marked as "dirty" even when they are changed, and they * are not returned by the getChangedItems() method. * * @param array|false $item_ids * Either an array with the IDs of the queued items. Or FALSE to mark all * items as queued for the given indexes. * @param SearchApiIndex $index * The index for which the items were queued. * * @throws SearchApiDataSourceException * If any error state was encountered. * * @deprecated * As of Search API 1.10, the cron queue is not used for indexing anymore, * therefore this method has become useless. It will be removed in the * Drupal 8 version of this module. */ public function trackItemQueued($item_ids, SearchApiIndex $index); /** * Sets the tracking status of the given items to "indexed". * * @param array $item_ids * The IDs of the indexed items. * @param SearchApiIndex $index * The index on which the items were indexed. * * @throws SearchApiDataSourceException * If any error state was encountered. */ public function trackItemIndexed(array $item_ids, SearchApiIndex $index); /** * Stops tracking the index status for the given items on the given indexes. * * @param array $item_ids * The IDs of the removed items. * @param SearchApiIndex[] $indexes * The indexes for which the deletions should be tracked. * * @return SearchApiIndex[]|null * All indexes for which any items were deleted; or NULL if items were * deleted for all of them. * * @throws SearchApiDataSourceException * If any error state was encountered. */ public function trackItemDelete(array $item_ids, array $indexes); /** * Retrieves a list of items that need to be indexed. * * If possible, completely unindexed items should be returned before items * that were indexed but later changed. Also, items that were changed longer * ago should be favored. * * @param SearchApiIndex $index * The index for which changed items should be returned. * @param int $limit * The maximum number of items to return. Negative values mean "unlimited". * * @return array * The IDs of items that need to be indexed for the given index. * * @throws SearchApiDataSourceException * If any error state was encountered. */ public function getChangedItems(SearchApiIndex $index, $limit = -1); /** * Retrieves information on how many items have been indexed for a certain index. * * @param SearchApiIndex $index * The index whose index status should be returned. * * @return array * An associative array containing two keys (in this order): * - indexed: The number of items already indexed in their latest version. * - total: The total number of items that have to be indexed for this * index. * * @throws SearchApiDataSourceException * If any error state was encountered. */ public function getIndexStatus(SearchApiIndex $index); /** * Retrieves the entity type of items from this datasource. * * @return string|null * An entity type string if the items provided by this datasource are * entities; NULL otherwise. * * @throws SearchApiDataSourceException * If any error state was encountered. */ public function getEntityType(); /** * Form constructor for configuring the datasource for a given index. * * @param array $form * The form returned by configurationForm(). * @param array $form_state * The form state. $form_state['index'] will contain the edited index. If * this key is empty, then a new index is being created. In case of an edit, * $form_state['index']->options['datasource'] contains the previous * settings for the datasource. * * @return array|false * A form array for configuring this callback, or FALSE if no configuration * is possible. */ public function configurationForm(array $form, array &$form_state); /** * Validation callback for the form returned by configurationForm(). * * This method will only be called if that form was non-empty. * * @param array $form * The form returned by configurationForm(). * @param array $values * The part of the $form_state['values'] array corresponding to this form. * @param array $form_state * The complete form state. */ public function configurationFormValidate(array $form, array &$values, array &$form_state); /** * Submit callback for the form returned by configurationForm(). * * This method will only be called if that form was non-empty. * * Any necessary changes to the submitted values should be made, afterwards * they will automatically be stored as the index's "datasource" options. The * method can also be used by the datasource controller to react to the * possible change in its settings. * * @param array $form * The form returned by configurationForm(). * @param array $values * The part of the $form_state['values'] array corresponding to this form. * @param array $form_state * The complete form state. */ public function configurationFormSubmit(array $form, array &$values, array &$form_state); /** * Returns a summary of an index's current datasource configuration. * * @param SearchApiIndex $index * The index whose datasource configuration should be summarized. * * @return string|null * A translated string describing the index's current datasource * configuration. Or NULL, if there is no configuration (or no description * is available). */ public function getConfigurationSummary(SearchApiIndex $index); } /** * 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. */ 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', * ), * ), * ); * @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 ($chunk as $item_id) { foreach ($indexes as $index) { $insert->values(array( $this->itemIdColumn => $item_id, $this->indexIdColumn => $index->id, $this->changedColumn => 1, )); } } $insert->execute(); } } /** * {@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'); } $update->execute(); $indexes_by_id[$index->id] = $index; } // 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); db_update($this->table) ->fields(array( $this->changedColumn => 0, )) ->condition($this->itemIdColumn, $item_ids, 'IN') ->condition($this->indexIdColumn, $index->id) ->execute(); } /** * {@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'); if ($delete->execute()) { $ret[] = $index; } } 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); } } }