datasource_entity.inc 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. <?php
  2. /**
  3. * @file
  4. * Contains the SearchApiEntityDataSourceController class.
  5. */
  6. /**
  7. * Represents a datasource for all entities known to the Entity API.
  8. */
  9. class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceController {
  10. /**
  11. * Entity type info for this type.
  12. *
  13. * @var array
  14. */
  15. protected $entityInfo;
  16. /**
  17. * The ID key of this entity type, if any.
  18. *
  19. * @var string|null
  20. */
  21. protected $idKey;
  22. /**
  23. * The bundle key of this entity type, if any.
  24. *
  25. * @var string|null
  26. */
  27. protected $bundleKey;
  28. /**
  29. * Cached return values for getBundles(), keyed by index machine name.
  30. *
  31. * @var array
  32. */
  33. protected $bundles = array();
  34. /**
  35. * {@inheritdoc}
  36. */
  37. public function __construct($type) {
  38. parent::__construct($type);
  39. $this->entityInfo = entity_get_info($this->entityType);
  40. if (!empty($this->entityInfo['entity keys']['id'])) {
  41. $this->idKey = $this->entityInfo['entity keys']['id'];
  42. }
  43. if (!empty($this->entityInfo['entity keys']['bundle'])) {
  44. $this->bundleKey = $this->entityInfo['entity keys']['bundle'];
  45. }
  46. }
  47. /**
  48. * {@inheritdoc}
  49. */
  50. public function getIdFieldInfo() {
  51. $properties = entity_get_property_info($this->entityType);
  52. if (!$this->idKey) {
  53. throw new SearchApiDataSourceException(t("Entity type @type doesn't specify an ID key.", array('@type' => $this->entityInfo['label'])));
  54. }
  55. if (empty($properties['properties'][$this->idKey]['type'])) {
  56. throw new SearchApiDataSourceException(t("Entity type @type doesn't specify a type for the @prop property.", array('@type' => $this->entityInfo['label'], '@prop' => $this->idKey)));
  57. }
  58. $type = $properties['properties'][$this->idKey]['type'];
  59. if (search_api_is_list_type($type)) {
  60. throw new SearchApiDataSourceException(t("Entity type @type uses list field @prop as its ID.", array('@type' => $this->entityInfo['label'], '@prop' => $this->idKey)));
  61. }
  62. if ($type == 'token') {
  63. $type = 'string';
  64. }
  65. return array(
  66. 'key' => $this->idKey,
  67. 'type' => $type,
  68. );
  69. }
  70. /**
  71. * {@inheritdoc}
  72. */
  73. public function loadItems(array $ids) {
  74. $items = entity_load($this->entityType, $ids);
  75. // If some items couldn't be loaded, remove them from tracking.
  76. if (count($items) != count($ids)) {
  77. $ids = array_flip($ids);
  78. $unknown = array_keys(array_diff_key($ids, $items));
  79. if ($unknown) {
  80. search_api_track_item_delete($this->type, $unknown);
  81. }
  82. }
  83. return $items;
  84. }
  85. /**
  86. * {@inheritdoc}
  87. */
  88. public function getMetadataWrapper($item = NULL, array $info = array()) {
  89. return entity_metadata_wrapper($this->entityType, $item, $info);
  90. }
  91. /**
  92. * {@inheritdoc}
  93. */
  94. public function getItemId($item) {
  95. $id = entity_id($this->entityType, $item);
  96. return $id ? $id : NULL;
  97. }
  98. /**
  99. * {@inheritdoc}
  100. */
  101. public function getItemLabel($item) {
  102. $label = entity_label($this->entityType, $item);
  103. return $label ? $label : NULL;
  104. }
  105. /**
  106. * {@inheritdoc}
  107. */
  108. public function getItemUrl($item) {
  109. if ($this->entityType == 'file') {
  110. return array(
  111. 'path' => file_create_url($item->uri),
  112. 'options' => array(
  113. 'entity_type' => 'file',
  114. 'entity' => $item,
  115. ),
  116. );
  117. }
  118. $url = entity_uri($this->entityType, $item);
  119. return $url ? $url : NULL;
  120. }
  121. /**
  122. * {@inheritdoc}
  123. */
  124. public function startTracking(array $indexes) {
  125. if (!$this->table) {
  126. return;
  127. }
  128. // We first clear the tracking table for all indexes, so we can just insert
  129. // all items again without any key conflicts.
  130. $this->stopTracking($indexes);
  131. if (!empty($this->entityInfo['base table']) && $this->idKey) {
  132. // Use a subselect, which will probably be much faster than entity_load().
  133. // Assumes that all entities use the "base table" property and the
  134. // "entity keys[id]" in the same way as the default controller.
  135. $table = $this->entityInfo['base table'];
  136. // We could also use a single insert (with a UNION in the nested query),
  137. // but this method will be mostly called with a single index, anyways.
  138. foreach ($indexes as $index) {
  139. // Select all entity ids.
  140. $query = db_select($table, 't');
  141. $query->addField('t', $this->idKey, 'item_id');
  142. $query->addExpression(':index_id', 'index_id', array(':index_id' => $index->id));
  143. $query->addExpression('1', 'changed');
  144. if ($bundles = $this->getIndexBundles($index)) {
  145. $bundle_column = $this->bundleKey;
  146. if (!db_field_exists($table, $bundle_column)) {
  147. if ($this->entityType == 'taxonomy_term') {
  148. $bundle_column = 'vid';
  149. $bundles = db_query('SELECT vid FROM {taxonomy_vocabulary} WHERE machine_name IN (:bundles)', array(':bundles' => $bundles))->fetchCol();
  150. }
  151. elseif ($this->entityType == 'flagging') {
  152. $bundle_column = 'fid';
  153. $bundles = db_query('SELECT fid FROM {flag} WHERE name IN (:bundles)', array(':bundles' => $bundles))->fetchCol();
  154. }
  155. elseif ($this->entityType == 'comment') {
  156. // Comments are significantly more complicated, since they don't
  157. // store their bundle explicitly in their database table. Instead,
  158. // we need to get all the nodes from the enabled types and filter
  159. // by those.
  160. $bundle_column = 'nid';
  161. $node_types = array();
  162. foreach ($bundles as $bundle) {
  163. if (substr($bundle, 0, 13) === 'comment_node_') {
  164. $node_types[] = substr($bundle, 13);
  165. }
  166. }
  167. if ($node_types) {
  168. $bundles = db_query('SELECT nid FROM {node} WHERE type IN (:bundles)', array(':bundles' => $node_types))->fetchCol();
  169. }
  170. else {
  171. continue;
  172. }
  173. }
  174. else {
  175. $this->startTrackingFallback(array($index->machine_name => $index));
  176. continue;
  177. }
  178. }
  179. if ($bundles) {
  180. $query->condition($bundle_column, $bundles);
  181. }
  182. }
  183. // INSERT ... SELECT ...
  184. db_insert($this->table)
  185. ->from($query)
  186. ->execute();
  187. }
  188. }
  189. else {
  190. $this->startTrackingFallback($indexes);
  191. }
  192. }
  193. /**
  194. * Initializes tracking of the index status of items for the given indexes.
  195. *
  196. * Fallback for when the items cannot directly be loaded into
  197. * {search_api_item} via "INSERT INTO … SELECT …".
  198. *
  199. * @param SearchApiIndex[] $indexes
  200. * The indexes for which item tracking should be initialized.
  201. *
  202. * @throws SearchApiDataSourceException
  203. * Thrown if any error state was encountered.
  204. *
  205. * @see SearchApiEntityDataSourceController::startTracking()
  206. */
  207. protected function startTrackingFallback(array $indexes) {
  208. // In the absence of a 'base table', use the slower way of retrieving the
  209. // items and inserting them "manually". For each index we get the item IDs
  210. // (since selected bundles might differ) and insert all of them as new.
  211. foreach ($indexes as $index) {
  212. $query = new EntityFieldQuery();
  213. $query->entityCondition('entity_type', $this->entityType);
  214. if ($bundles = $this->getIndexBundles($index)) {
  215. $query->entityCondition('bundle', $bundles);
  216. }
  217. $result = $query->execute();
  218. $ids = !empty($result[$this->entityType]) ? array_keys($result[$this->entityType]) : array();
  219. if ($ids) {
  220. $this->trackItemInsert($ids, array($index));
  221. }
  222. }
  223. }
  224. /**
  225. * {@inheritdoc}
  226. */
  227. public function trackItemInsert(array $item_ids, array $indexes) {
  228. $ret = array();
  229. foreach ($indexes as $index_id => $index) {
  230. $ids = $item_ids;
  231. if ($bundles = $this->getIndexBundles($index)) {
  232. $ids = drupal_map_assoc($ids);
  233. foreach (entity_load($this->entityType, $ids) as $id => $entity) {
  234. if (empty($bundles[$entity->{$this->bundleKey}])) {
  235. unset($ids[$id]);
  236. }
  237. }
  238. }
  239. if ($ids) {
  240. parent::trackItemInsert($ids, array($index));
  241. $ret[$index_id] = $index;
  242. }
  243. }
  244. return $ret;
  245. }
  246. /**
  247. * {@inheritdoc}
  248. */
  249. public function configurationForm(array $form, array &$form_state) {
  250. $options = $this->getAvailableBundles();
  251. if (!$options) {
  252. return FALSE;
  253. }
  254. $form['bundles'] = array(
  255. '#type' => 'checkboxes',
  256. '#title' => t('Bundles'),
  257. '#description' => t('Restrict the entity bundles that will be included in this index. Leave blank to include all bundles. This setting cannot be changed for enabled indexes.'),
  258. '#options' => array_map('check_plain', $options),
  259. '#attributes' => array('class' => array('search-api-checkboxes-list')),
  260. '#disabled' => !empty($form_state['index']) && $form_state['index']->enabled,
  261. );
  262. if (!empty($form_state['index']->options['datasource'])) {
  263. $form['bundles']['#default_value'] = drupal_map_assoc($form_state['index']->options['datasource']['bundles']);
  264. }
  265. return $form;
  266. }
  267. /**
  268. * {@inheritdoc}
  269. */
  270. public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
  271. if (!empty($values['bundles'])) {
  272. $values['bundles'] = array_keys(array_filter($values['bundles']));
  273. }
  274. }
  275. /**
  276. * {@inheritdoc}
  277. */
  278. public function getConfigurationSummary(SearchApiIndex $index) {
  279. if ($bundles = $this->getIndexBundles($index)) {
  280. $args['!bundles'] = implode(', ', array_intersect_key($this->getAvailableBundles(), $bundles));
  281. return format_plural(count($bundles), 'Indexed bundle: !bundles.', 'Indexed bundles: !bundles.', $args);
  282. }
  283. return NULL;
  284. }
  285. /**
  286. * Retrieves the available bundles for this entity type.
  287. *
  288. * @return array
  289. * An array (which might be empty) mapping this entity type's bundle keys to
  290. * their labels.
  291. */
  292. protected function getAvailableBundles() {
  293. if (!$this->bundleKey || empty($this->entityInfo['bundles'])) {
  294. return array();
  295. }
  296. $bundles = array();
  297. foreach ($this->entityInfo['bundles'] as $bundle => $bundle_info) {
  298. $bundles[$bundle] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle;
  299. }
  300. return $bundles;
  301. }
  302. /**
  303. * Computes the bundles that should be indexed for an index.
  304. *
  305. * @param SearchApiIndex $index
  306. * The index for which to check.
  307. *
  308. * @return array
  309. * An array containing all bundles that should be included in this index, as
  310. * both the keys and values. An empty array means all current bundles should
  311. * be included.
  312. *
  313. * @throws SearchApiException
  314. * If the index doesn't belong to this datasource controller.
  315. */
  316. protected function getIndexBundles(SearchApiIndex $index) {
  317. $this->checkIndex($index);
  318. if (!isset($this->bundles[$index->machine_name])) {
  319. $this->bundles[$index->machine_name] = array();
  320. if (!empty($index->options['datasource']['bundles'])) {
  321. // We retrieve the available bundles here to check whether all of them
  322. // are included by the index's setting. In this case, we return an empty
  323. // array, too, to save on complexity.
  324. // On the other hand, we still want to return deleted bundles since we
  325. // do not want to suddenly include all bundles when all selected bundles
  326. // were deleted.
  327. $available = $this->getAvailableBundles();
  328. foreach ($index->options['datasource']['bundles'] as $bundle) {
  329. $this->bundles[$index->machine_name][$bundle] = $bundle;
  330. unset($available[$bundle]);
  331. }
  332. if (!$available) {
  333. $this->bundles[$index->machine_name] = array();
  334. }
  335. }
  336. }
  337. return $this->bundles[$index->machine_name];
  338. }
  339. }