datasource.inc 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686
  1. <?php
  2. /**
  3. * @file
  4. * Contains the SearchApiDataSourceControllerInterface as well as a default base class.
  5. */
  6. /**
  7. * Interface for all data source controllers for Search API indexes.
  8. *
  9. * Data source controllers encapsulate all operations specific to an item type.
  10. * They are used for loading items, extracting item data, keeping track of the
  11. * item status, etc.
  12. *
  13. * Modules providing implementations of this interface that use a different way
  14. * (either different table or different method altogether) of keeping track of
  15. * indexed/dirty items than SearchApiAbstractDataSourceController should be
  16. * aware that indexes' numerical IDs can change due to feature reverts. It is
  17. * therefore recommended to use search_api_index_update_datasource(), or similar
  18. * code, in a hook_search_api_index_update() implementation.
  19. */
  20. interface SearchApiDataSourceControllerInterface {
  21. /**
  22. * Constructs a new data source controller.
  23. *
  24. * @param string $type
  25. * The item type for which this controller is created.
  26. */
  27. public function __construct($type);
  28. /**
  29. * Returns information on the ID field for this controller's type.
  30. *
  31. * @return array
  32. * An associative array containing the following keys:
  33. * - key: The property key for the ID field, as used in the item wrapper.
  34. * - type: The type of the ID field. Has to be one of the types from
  35. * search_api_field_types(). List types ("list<*>") are not allowed.
  36. *
  37. * @throws SearchApiDataSourceException
  38. * If any error state was encountered.
  39. */
  40. public function getIdFieldInfo();
  41. /**
  42. * Loads items of the type of this data source controller.
  43. *
  44. * @param array $ids
  45. * The IDs of the items to laod.
  46. *
  47. * @return array
  48. * The loaded items, keyed by ID.
  49. *
  50. * @throws SearchApiDataSourceException
  51. * If any error state was encountered.
  52. */
  53. public function loadItems(array $ids);
  54. /**
  55. * Creates a metadata wrapper for this datasource controller's type.
  56. *
  57. * @param mixed $item
  58. * Unless NULL, an item of the item type for this controller to be wrapped.
  59. * @param array $info
  60. * Optionally, additional information that should be used for creating the
  61. * wrapper. Uses the same format as entity_metadata_wrapper().
  62. *
  63. * @return EntityMetadataWrapper
  64. * A wrapper for the item type of this data source controller, according to
  65. * the info array, and optionally loaded with the given data.
  66. *
  67. * @throws SearchApiDataSourceException
  68. * If any error state was encountered.
  69. *
  70. * @see entity_metadata_wrapper()
  71. */
  72. public function getMetadataWrapper($item = NULL, array $info = array());
  73. /**
  74. * Retrieves the unique ID of an item.
  75. *
  76. * @param mixed $item
  77. * An item of this controller's type.
  78. *
  79. * @return mixed
  80. * Either the unique ID of the item, or NULL if none is available.
  81. *
  82. * @throws SearchApiDataSourceException
  83. * If any error state was encountered.
  84. */
  85. public function getItemId($item);
  86. /**
  87. * Retrieves a human-readable label for an item.
  88. *
  89. * @param mixed $item
  90. * An item of this controller's type.
  91. *
  92. * @return string|null
  93. * Either a human-readable label for the item, or NULL if none is available.
  94. *
  95. * @throws SearchApiDataSourceException
  96. * If any error state was encountered.
  97. */
  98. public function getItemLabel($item);
  99. /**
  100. * Retrieves a URL at which the item can be viewed on the web.
  101. *
  102. * @param mixed $item
  103. * An item of this controller's type.
  104. *
  105. * @return array|null
  106. * Either an array containing the 'path' and 'options' keys used to build
  107. * the URL of the item, and matching the signature of url(), or NULL if the
  108. * item has no URL of its own.
  109. *
  110. * @throws SearchApiDataSourceException
  111. * If any error state was encountered.
  112. */
  113. public function getItemUrl($item);
  114. /**
  115. * Initializes tracking of the index status of items for the given indexes.
  116. *
  117. * All currently known items of this data source's type should be inserted
  118. * into the tracking table for the given indexes, with status "changed". If
  119. * items were already present, these should also be set to "changed" and not
  120. * be inserted again.
  121. *
  122. * @param SearchApiIndex[] $indexes
  123. * The SearchApiIndex objects for which item tracking should be initialized.
  124. *
  125. * @throws SearchApiDataSourceException
  126. * If any error state was encountered.
  127. */
  128. public function startTracking(array $indexes);
  129. /**
  130. * Stops tracking of the index status of items for the given indexes.
  131. *
  132. * The tracking tables of the given indexes should be completely cleared.
  133. *
  134. * @param SearchApiIndex[] $indexes
  135. * The SearchApiIndex objects for which item tracking should be stopped.
  136. *
  137. * @throws SearchApiDataSourceException
  138. * If any error state was encountered.
  139. */
  140. public function stopTracking(array $indexes);
  141. /**
  142. * Starts tracking the index status for the given items on the given indexes.
  143. *
  144. * @param array $item_ids
  145. * The IDs of new items to track.
  146. * @param SearchApiIndex[] $indexes
  147. * The indexes for which items should be tracked.
  148. *
  149. * @throws SearchApiDataSourceException
  150. * If any error state was encountered.
  151. */
  152. public function trackItemInsert(array $item_ids, array $indexes);
  153. /**
  154. * Sets the tracking status of the given items to "changed"/"dirty".
  155. *
  156. * Unless $dequeue is set to TRUE, this operation is ignored for items whose
  157. * status is not "indexed".
  158. *
  159. * @param array|false $item_ids
  160. * Either an array with the IDs of the changed items. Or FALSE to mark all
  161. * items as changed for the given indexes.
  162. * @param SearchApiIndex[] $indexes
  163. * The indexes for which the change should be tracked.
  164. * @param bool $dequeue
  165. * (deprecated) If set to TRUE, also change the status of queued items.
  166. * The concept of queued items will be removed in the Drupal 8 version of
  167. * this module.
  168. *
  169. * @throws SearchApiDataSourceException
  170. * If any error state was encountered.
  171. */
  172. public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE);
  173. /**
  174. * Sets the tracking status of the given items to "queued".
  175. *
  176. * Queued items are not marked as "dirty" even when they are changed, and they
  177. * are not returned by the getChangedItems() method.
  178. *
  179. * @param array|false $item_ids
  180. * Either an array with the IDs of the queued items. Or FALSE to mark all
  181. * items as queued for the given indexes.
  182. * @param SearchApiIndex $index
  183. * The index for which the items were queued.
  184. *
  185. * @throws SearchApiDataSourceException
  186. * If any error state was encountered.
  187. *
  188. * @deprecated
  189. * As of Search API 1.10, the cron queue is not used for indexing anymore,
  190. * therefore this method has become useless. It will be removed in the
  191. * Drupal 8 version of this module.
  192. */
  193. public function trackItemQueued($item_ids, SearchApiIndex $index);
  194. /**
  195. * Sets the tracking status of the given items to "indexed".
  196. *
  197. * @param array $item_ids
  198. * The IDs of the indexed items.
  199. * @param SearchApiIndex $index
  200. * The index on which the items were indexed.
  201. *
  202. * @throws SearchApiDataSourceException
  203. * If any error state was encountered.
  204. */
  205. public function trackItemIndexed(array $item_ids, SearchApiIndex $index);
  206. /**
  207. * Stops tracking the index status for the given items on the given indexes.
  208. *
  209. * @param array $item_ids
  210. * The IDs of the removed items.
  211. * @param SearchApiIndex[] $indexes
  212. * The indexes for which the deletions should be tracked.
  213. *
  214. * @throws SearchApiDataSourceException
  215. * If any error state was encountered.
  216. */
  217. public function trackItemDelete(array $item_ids, array $indexes);
  218. /**
  219. * Retrieves a list of items that need to be indexed.
  220. *
  221. * If possible, completely unindexed items should be returned before items
  222. * that were indexed but later changed. Also, items that were changed longer
  223. * ago should be favored.
  224. *
  225. * @param SearchApiIndex $index
  226. * The index for which changed items should be returned.
  227. * @param int $limit
  228. * The maximum number of items to return. Negative values mean "unlimited".
  229. *
  230. * @return array
  231. * The IDs of items that need to be indexed for the given index.
  232. *
  233. * @throws SearchApiDataSourceException
  234. * If any error state was encountered.
  235. */
  236. public function getChangedItems(SearchApiIndex $index, $limit = -1);
  237. /**
  238. * Retrieves information on how many items have been indexed for a certain index.
  239. *
  240. * @param SearchApiIndex $index
  241. * The index whose index status should be returned.
  242. *
  243. * @return array
  244. * An associative array containing two keys (in this order):
  245. * - indexed: The number of items already indexed in their latest version.
  246. * - total: The total number of items that have to be indexed for this
  247. * index.
  248. *
  249. * @throws SearchApiDataSourceException
  250. * If any error state was encountered.
  251. */
  252. public function getIndexStatus(SearchApiIndex $index);
  253. /**
  254. * Retrieves the entity type of items from this datasource.
  255. *
  256. * @return string|null
  257. * An entity type string if the items provided by this datasource are
  258. * entities; NULL otherwise.
  259. *
  260. * @throws SearchApiDataSourceException
  261. * If any error state was encountered.
  262. */
  263. public function getEntityType();
  264. }
  265. /**
  266. * Provides a default base class for datasource controllers.
  267. *
  268. * Contains default implementations for a number of methods which will be
  269. * similar for most data sources. Concrete data sources can decide to extend
  270. * this base class to save time, but can also implement the interface directly.
  271. *
  272. * A subclass will still have to provide implementations for the following
  273. * methods:
  274. * - getIdFieldInfo()
  275. * - loadItems()
  276. * - getMetadataWrapper() or getPropertyInfo()
  277. * - startTracking() or getAllItemIds()
  278. *
  279. * The table used by default for tracking the index status of items is
  280. * {search_api_item}. This can easily be changed, for example when an item type
  281. * has non-integer IDs, by changing the $table property.
  282. */
  283. abstract class SearchApiAbstractDataSourceController implements SearchApiDataSourceControllerInterface {
  284. /**
  285. * The item type for this controller instance.
  286. */
  287. protected $type;
  288. /**
  289. * The entity type for this controller instance.
  290. *
  291. * @var string|null
  292. *
  293. * @see getEntityType()
  294. */
  295. protected $entityType = NULL;
  296. /**
  297. * The info array for the item type, as specified via
  298. * hook_search_api_item_type_info().
  299. *
  300. * @var array
  301. */
  302. protected $info;
  303. /**
  304. * The table used for tracking items. Set to NULL on subclasses to disable
  305. * the default tracking for an item type, or change the property to use a
  306. * different table for tracking.
  307. *
  308. * @var string
  309. */
  310. protected $table = 'search_api_item';
  311. /**
  312. * When using the default tracking mechanism: the name of the column on
  313. * $this->table containing the item ID.
  314. *
  315. * @var string
  316. */
  317. protected $itemIdColumn = 'item_id';
  318. /**
  319. * When using the default tracking mechanism: the name of the column on
  320. * $this->table containing the index ID.
  321. *
  322. * @var string
  323. */
  324. protected $indexIdColumn = 'index_id';
  325. /**
  326. * When using the default tracking mechanism: the name of the column on
  327. * $this->table containing the indexing status.
  328. *
  329. * @var string
  330. */
  331. protected $changedColumn = 'changed';
  332. /**
  333. * {@inheritdoc}
  334. */
  335. public function __construct($type) {
  336. $this->type = $type;
  337. $this->info = search_api_get_item_type_info($type);
  338. if (!empty($this->info['entity_type'])) {
  339. $this->entityType = $this->info['entity_type'];
  340. }
  341. }
  342. /**
  343. * {@inheritdoc}
  344. */
  345. public function getEntityType() {
  346. return $this->entityType;
  347. }
  348. /**
  349. * {@inheritdoc}
  350. */
  351. public function getMetadataWrapper($item = NULL, array $info = array()) {
  352. $info += $this->getPropertyInfo();
  353. return entity_metadata_wrapper($this->entityType ? $this->entityType : $this->type, $item, $info);
  354. }
  355. /**
  356. * Retrieves the property info for this item type.
  357. *
  358. * This is a helper method for getMetadataWrapper() that can be used by
  359. * subclasses to specify the property information to use when creating a
  360. * metadata wrapper.
  361. *
  362. * The data structure uses largely the format specified in
  363. * hook_entity_property_info(). However, the first level of keys (containing
  364. * the entity types) is omitted, and the "properties" key is called
  365. * "property info" instead. So, an example return value would look like this:
  366. *
  367. * @code
  368. * return array(
  369. * 'property info' => array(
  370. * 'foo' => array(
  371. * 'label' => t('Foo'),
  372. * 'type' => 'text',
  373. * ),
  374. * 'bar' => array(
  375. * 'label' => t('Bar'),
  376. * 'type' => 'list<integer>',
  377. * ),
  378. * ),
  379. * );
  380. * @endcode
  381. *
  382. * SearchApiExternalDataSourceController::getPropertyInfo() contains a working
  383. * example of this method.
  384. *
  385. * If the item type is an entity type, no additional property information is
  386. * required, the method will thus just return an empty array. You can still
  387. * use this to append additional properties to the entities, or the like,
  388. * though.
  389. *
  390. * @return array
  391. * Property information as specified by entity_metadata_wrapper().
  392. *
  393. * @throws SearchApiDataSourceException
  394. * If any error state was encountered.
  395. *
  396. * @see getMetadataWrapper()
  397. * @see hook_entity_property_info()
  398. */
  399. protected function getPropertyInfo() {
  400. // If this is an entity type, no additional property info is needed.
  401. if ($this->entityType) {
  402. return array();
  403. }
  404. throw new SearchApiDataSourceException(t('No known property information for type @type.', array('@type' => $this->type)));
  405. }
  406. /**
  407. * {@inheritdoc}
  408. */
  409. public function getItemId($item) {
  410. $id_info = $this->getIdFieldInfo();
  411. $field = $id_info['key'];
  412. $wrapper = $this->getMetadataWrapper($item);
  413. if (!isset($wrapper->$field)) {
  414. return NULL;
  415. }
  416. $id = $wrapper->$field->value();
  417. return $id ? $id : NULL;
  418. }
  419. /**
  420. * {@inheritdoc}
  421. */
  422. public function getItemLabel($item) {
  423. $label = $this->getMetadataWrapper($item)->label();
  424. return $label ? $label : NULL;
  425. }
  426. /**
  427. * {@inheritdoc}
  428. */
  429. public function getItemUrl($item) {
  430. return NULL;
  431. }
  432. /**
  433. * {@inheritdoc}
  434. */
  435. public function startTracking(array $indexes) {
  436. if (!$this->table) {
  437. return;
  438. }
  439. // We first clear the tracking table for all indexes, so we can just insert
  440. // all items again without any key conflicts.
  441. $this->stopTracking($indexes);
  442. // Insert all items as new.
  443. $this->trackItemInsert($this->getAllItemIds(), $indexes);
  444. }
  445. /**
  446. * Returns the IDs of all items that are known for this controller's type.
  447. *
  448. * Helper method that can be used by subclasses instead of implementing
  449. * startTracking().
  450. *
  451. * @return array
  452. * An array containing all item IDs for this type.
  453. *
  454. * @throws SearchApiDataSourceException
  455. * If any error state was encountered.
  456. */
  457. protected function getAllItemIds() {
  458. throw new SearchApiDataSourceException(t('Items not known for type @type.', array('@type' => $this->type)));
  459. }
  460. /**
  461. * {@inheritdoc}
  462. */
  463. public function stopTracking(array $indexes) {
  464. if (!$this->table) {
  465. return;
  466. }
  467. // We could also use a single query with "IN" operator, but this method
  468. // will mostly be called with only one index.
  469. foreach ($indexes as $index) {
  470. $this->checkIndex($index);
  471. db_delete($this->table)
  472. ->condition($this->indexIdColumn, $index->id)
  473. ->execute();
  474. }
  475. }
  476. /**
  477. * {@inheritdoc}
  478. */
  479. public function trackItemInsert(array $item_ids, array $indexes) {
  480. if (!$this->table) {
  481. return;
  482. }
  483. // Since large amounts of items can overstrain the database, only add items
  484. // in chunks.
  485. foreach (array_chunk($item_ids, 1000) as $chunk) {
  486. $insert = db_insert($this->table)
  487. ->fields(array($this->itemIdColumn, $this->indexIdColumn, $this->changedColumn));
  488. foreach ($chunk as $item_id) {
  489. foreach ($indexes as $index) {
  490. $this->checkIndex($index);
  491. $insert->values(array(
  492. $this->itemIdColumn => $item_id,
  493. $this->indexIdColumn => $index->id,
  494. $this->changedColumn => 1,
  495. ));
  496. }
  497. }
  498. $insert->execute();
  499. }
  500. }
  501. /**
  502. * {@inheritdoc}
  503. */
  504. public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE) {
  505. if (!$this->table) {
  506. return;
  507. }
  508. $index_ids = array();
  509. foreach ($indexes as $index) {
  510. $this->checkIndex($index);
  511. $index_ids[] = $index->id;
  512. }
  513. $update = db_update($this->table)
  514. ->fields(array(
  515. $this->changedColumn => REQUEST_TIME,
  516. ))
  517. ->condition($this->indexIdColumn, $index_ids, 'IN')
  518. ->condition($this->changedColumn, 0, $dequeue ? '<=' : '=');
  519. if ($item_ids !== FALSE) {
  520. $update->condition($this->itemIdColumn, $item_ids, 'IN');
  521. }
  522. $update->execute();
  523. }
  524. /**
  525. * {@inheritdoc}
  526. */
  527. public function trackItemQueued($item_ids, SearchApiIndex $index) {
  528. $this->checkIndex($index);
  529. if (!$this->table) {
  530. return;
  531. }
  532. $update = db_update($this->table)
  533. ->fields(array(
  534. $this->changedColumn => -1,
  535. ))
  536. ->condition($this->indexIdColumn, $index->id);
  537. if ($item_ids !== FALSE) {
  538. $update->condition($this->itemIdColumn, $item_ids, 'IN');
  539. }
  540. $update->execute();
  541. }
  542. /**
  543. * {@inheritdoc}
  544. */
  545. public function trackItemIndexed(array $item_ids, SearchApiIndex $index) {
  546. if (!$this->table) {
  547. return;
  548. }
  549. $this->checkIndex($index);
  550. db_update($this->table)
  551. ->fields(array(
  552. $this->changedColumn => 0,
  553. ))
  554. ->condition($this->itemIdColumn, $item_ids, 'IN')
  555. ->condition($this->indexIdColumn, $index->id)
  556. ->execute();
  557. }
  558. /**
  559. * {@inheritdoc}
  560. */
  561. public function trackItemDelete(array $item_ids, array $indexes) {
  562. if (!$this->table) {
  563. return;
  564. }
  565. $index_ids = array();
  566. foreach ($indexes as $index) {
  567. $this->checkIndex($index);
  568. $index_ids[] = $index->id;
  569. }
  570. db_delete($this->table)
  571. ->condition($this->itemIdColumn, $item_ids, 'IN')
  572. ->condition($this->indexIdColumn, $index_ids, 'IN')
  573. ->execute();
  574. }
  575. /**
  576. * {@inheritdoc}
  577. */
  578. public function getChangedItems(SearchApiIndex $index, $limit = -1) {
  579. if ($limit == 0) {
  580. return array();
  581. }
  582. $this->checkIndex($index);
  583. $select = db_select($this->table, 't');
  584. $select->addField('t', 'item_id');
  585. $select->condition($this->indexIdColumn, $index->id);
  586. $select->condition($this->changedColumn, 0, '>');
  587. $select->orderBy($this->changedColumn, 'ASC');
  588. if ($limit > 0) {
  589. $select->range(0, $limit);
  590. }
  591. return $select->execute()->fetchCol();
  592. }
  593. /**
  594. * {@inheritdoc}
  595. */
  596. public function getIndexStatus(SearchApiIndex $index) {
  597. if (!$this->table) {
  598. return array('indexed' => 0, 'total' => 0);
  599. }
  600. $this->checkIndex($index);
  601. $indexed = db_select($this->table, 'i')
  602. ->condition($this->indexIdColumn, $index->id)
  603. ->condition($this->changedColumn, 0)
  604. ->countQuery()
  605. ->execute()
  606. ->fetchField();
  607. $total = db_select($this->table, 'i')
  608. ->condition($this->indexIdColumn, $index->id)
  609. ->countQuery()
  610. ->execute()
  611. ->fetchField();
  612. return array('indexed' => $indexed, 'total' => $total);
  613. }
  614. /**
  615. * Checks whether the given index is valid for this datasource controller.
  616. *
  617. * Helper method used by various methods in this class. By default only checks
  618. * whether the types match.
  619. *
  620. * @param SearchApiIndex $index
  621. * The index to check.
  622. *
  623. * @throws SearchApiDataSourceException
  624. * If the index doesn't fit to this datasource controller.
  625. */
  626. protected function checkIndex(SearchApiIndex $index) {
  627. if ($index->item_type != $this->type) {
  628. $index_type = search_api_get_item_type_info($index->item_type);
  629. $index_type = empty($index_type['name']) ? $index->item_type : $index_type['name'];
  630. $msg = t(
  631. 'Invalid index @index of type @index_type passed to data source controller for type @this_type.',
  632. array('@index' => $index->name, '@index_type' => $index_type, '@this_type' => $this->info['name'])
  633. );
  634. throw new SearchApiDataSourceException($msg);
  635. }
  636. }
  637. }