datasource.inc 22 KB

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