callback_add_hierarchy.inc 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. <?php
  2. /**
  3. * Search API data alteration callback that adds an URL field for all items.
  4. */
  5. class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
  6. /**
  7. * Cached value for the hierarchical field options.
  8. *
  9. * @var array
  10. *
  11. * @see getHierarchicalFields()
  12. */
  13. protected $field_options;
  14. /**
  15. * Enable this data alteration only if any hierarchical fields are available.
  16. *
  17. * @param SearchApiIndex $index
  18. * The index to check for.
  19. *
  20. * @return boolean
  21. * TRUE if the callback can run on the given index; FALSE otherwise.
  22. */
  23. public function supportsIndex(SearchApiIndex $index) {
  24. return (bool) $this->getHierarchicalFields();
  25. }
  26. /**
  27. * Display a form for configuring this callback.
  28. *
  29. * @return array
  30. * A form array for configuring this callback, or FALSE if no configuration
  31. * is possible.
  32. */
  33. public function configurationForm() {
  34. $options = $this->getHierarchicalFields();
  35. $this->options += array('fields' => array());
  36. $form['fields'] = array(
  37. '#title' => t('Hierarchical fields'),
  38. '#description' => t('Select the fields which should be supplemented with their ancestors. ' .
  39. 'Each field is listed along with its children of the same type. ' .
  40. 'When selecting several child properties of a field, all those properties will be recursively added to that field. ' .
  41. 'Please note that you should de-select all fields before disabling this data alteration.'),
  42. '#type' => 'select',
  43. '#multiple' => TRUE,
  44. '#size' => min(6, count($options, COUNT_RECURSIVE)),
  45. '#options' => $options,
  46. '#default_value' => $this->options['fields'],
  47. );
  48. return $form;
  49. }
  50. /**
  51. * Submit callback for the form returned by configurationForm().
  52. *
  53. * This method should both return the new options and set them internally.
  54. *
  55. * @param array $form
  56. * The form returned by configurationForm().
  57. * @param array $values
  58. * The part of the $form_state['values'] array corresponding to this form.
  59. * @param array $form_state
  60. * The complete form state.
  61. *
  62. * @return array
  63. * The new options array for this callback.
  64. */
  65. public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
  66. // Change the saved type of fields in the index, if necessary.
  67. if (!empty($this->index->options['fields'])) {
  68. $fields = &$this->index->options['fields'];
  69. $previous = drupal_map_assoc($this->options['fields']);
  70. foreach ($values['fields'] as $field) {
  71. list($key, $prop) = explode(':', $field);
  72. if (empty($previous[$field]) && isset($fields[$key]['type'])) {
  73. $fields[$key]['type'] = 'list<' . search_api_extract_inner_type($fields[$key]['type']) . '>';
  74. $change = TRUE;
  75. }
  76. }
  77. $new = drupal_map_assoc($values['fields']);
  78. foreach ($previous as $field) {
  79. list($key, $prop) = explode(':', $field);
  80. if (empty($new[$field]) && isset($fields[$key]['type'])) {
  81. $w = $this->index->entityWrapper(NULL, FALSE);
  82. if (isset($w->$key)) {
  83. $type = $w->$key->type();
  84. $inner = search_api_extract_inner_type($fields[$key]['type']);
  85. $fields[$key]['type'] = search_api_nest_type($inner, $type);
  86. $change = TRUE;
  87. }
  88. }
  89. }
  90. if (isset($change)) {
  91. $this->index->save();
  92. }
  93. }
  94. return parent::configurationFormSubmit($form, $values, $form_state);
  95. }
  96. /**
  97. * Alter items before indexing.
  98. *
  99. * Items which are removed from the array won't be indexed, but will be marked
  100. * as clean for future indexing. This could for instance be used to implement
  101. * some sort of access filter for security purposes (e.g., don't index
  102. * unpublished nodes or comments).
  103. *
  104. * @param array $items
  105. * An array of items to be altered, keyed by item IDs.
  106. */
  107. public function alterItems(array &$items) {
  108. if (empty($this->options['fields'])) {
  109. return array();
  110. }
  111. foreach ($items as $item) {
  112. $wrapper = $this->index->entityWrapper($item, FALSE);
  113. $values = array();
  114. foreach ($this->options['fields'] as $field) {
  115. list($key, $prop) = explode(':', $field);
  116. if (!isset($wrapper->$key)) {
  117. continue;
  118. }
  119. $child = $wrapper->$key;
  120. $values += array($key => array());
  121. $this->extractHierarchy($child, $prop, $values[$key]);
  122. }
  123. foreach ($values as $key => $value) {
  124. $item->$key = $value;
  125. }
  126. }
  127. }
  128. /**
  129. * Declare the properties that are (or can be) added to items with this
  130. * callback. If a property with this name already exists for an entity it
  131. * will be overridden, so keep a clear namespace by prefixing the properties
  132. * with the module name if this is not desired.
  133. *
  134. * @see hook_entity_property_info()
  135. *
  136. * @return array
  137. * Information about all additional properties, as specified by
  138. * hook_entity_property_info() (only the inner "properties" array).
  139. */
  140. public function propertyInfo() {
  141. if (empty($this->options['fields'])) {
  142. return array();
  143. }
  144. $ret = array();
  145. $wrapper = $this->index->entityWrapper(NULL, FALSE);
  146. foreach ($this->options['fields'] as $field) {
  147. list($key, $prop) = explode(':', $field);
  148. if (!isset($wrapper->$key)) {
  149. continue;
  150. }
  151. $child = $wrapper->$key;
  152. while (search_api_is_list_type($child->type())) {
  153. $child = $child[0];
  154. }
  155. if (!isset($child->$prop)) {
  156. continue;
  157. }
  158. if (!isset($ret[$key])) {
  159. $ret[$key] = $child->info();
  160. $type = search_api_extract_inner_type($ret[$key]['type']);
  161. $ret[$key]['type'] = "list<$type>";
  162. $ret[$key]['getter callback'] = 'entity_property_verbatim_get';
  163. // The return value of info() has some additional internal values set,
  164. // which we have to unset for the use here.
  165. unset($ret[$key]['name'], $ret[$key]['parent'], $ret[$key]['langcode'], $ret[$key]['clear'],
  166. $ret[$key]['property info alter'], $ret[$key]['property defaults']);
  167. }
  168. if (isset($ret[$key]['bundle'])) {
  169. $info = $child->$prop->info();
  170. if (empty($info['bundle']) || $ret[$key]['bundle'] != $info['bundle']) {
  171. unset($ret[$key]['bundle']);
  172. }
  173. }
  174. }
  175. return $ret;
  176. }
  177. /**
  178. * Helper method for finding all hierarchical fields of an index's type.
  179. *
  180. * @return array
  181. * An array containing all hierarchical fields of the index, structured as
  182. * an options array grouped by primary field.
  183. */
  184. protected function getHierarchicalFields() {
  185. if (!isset($this->field_options)) {
  186. $this->field_options = array();
  187. $wrapper = $this->index->entityWrapper(NULL, FALSE);
  188. // Only entities can be indexed in hierarchies, as other properties don't
  189. // have IDs that we can extract and store.
  190. $entity_info = entity_get_info();
  191. foreach ($wrapper as $key1 => $child) {
  192. while (search_api_is_list_type($child->type())) {
  193. $child = $child[0];
  194. }
  195. $info = $child->info();
  196. $type = $child->type();
  197. if (empty($entity_info[$type])) {
  198. continue;
  199. }
  200. foreach ($child as $key2 => $prop) {
  201. if (search_api_extract_inner_type($prop->type()) == $type) {
  202. $prop_info = $prop->info();
  203. $this->field_options[$info['label']]["$key1:$key2"] = $prop_info['label'];
  204. }
  205. }
  206. }
  207. }
  208. return $this->field_options;
  209. }
  210. /**
  211. * Extracts a hierarchy from a metadata wrapper by modifying $values.
  212. */
  213. public function extractHierarchy(EntityMetadataWrapper $wrapper, $property, array &$values) {
  214. if (search_api_is_list_type($wrapper->type())) {
  215. foreach ($wrapper as $w) {
  216. $this->extractHierarchy($w, $property, $values);
  217. }
  218. return;
  219. }
  220. $v = $wrapper->value(array('identifier' => TRUE));
  221. if ($v && !isset($values[$v])) {
  222. $values[$v] = $v;
  223. if (isset($wrapper->$property) && $wrapper->$property->value()) {
  224. $this->extractHierarchy($wrapper->$property, $property, $values);
  225. }
  226. }
  227. }
  228. }