callback_add_aggregation.inc 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. <?php
  2. /**
  3. * @file
  4. * Contains SearchApiAlterAddAggregation.
  5. */
  6. /**
  7. * Search API data alteration callback that adds an URL field for all items.
  8. */
  9. class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
  10. /**
  11. * The type of aggregation currently performed.
  12. *
  13. * Used to temporarily store the current aggregation type for use of
  14. * SearchApiAlterAddAggregation::reduce() with array_reduce().
  15. *
  16. * @var string
  17. */
  18. protected $reductionType;
  19. /**
  20. * A separator to use when the aggregation type is 'fulltext'.
  21. *
  22. * Used to temporarily store a string separator when the aggregation type is
  23. * "fulltext", for use in SearchApiAlterAddAggregation::reduce() with
  24. * array_reduce().
  25. *
  26. * @var string
  27. */
  28. protected $fulltextReductionSeparator;
  29. public function configurationForm() {
  30. $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';
  31. $fields = $this->index->getFields(FALSE);
  32. $field_options = array();
  33. $field_properties = array();
  34. foreach ($fields as $name => $field) {
  35. $field_options[$name] = check_plain($field['name']);
  36. $field_properties[$name] = array(
  37. '#attributes' => array('title' => $name),
  38. '#description' => check_plain($field['description']),
  39. );
  40. }
  41. $additional = empty($this->options['fields']) ? array() : $this->options['fields'];
  42. $types = $this->getTypes();
  43. $type_descriptions = $this->getTypes('description');
  44. $tmp = array();
  45. foreach ($types as $type => $name) {
  46. $tmp[$type] = array(
  47. '#type' => 'item',
  48. '#description' => $type_descriptions[$type],
  49. );
  50. }
  51. $type_descriptions = $tmp;
  52. $form['#id'] = 'edit-callbacks-search-api-alter-add-aggregation-settings';
  53. $form['description'] = array(
  54. '#markup' => t('<p>This data alteration lets you define additional fields that will be added to this index. ' .
  55. 'Each of these new fields will be an aggregation of one or more existing fields.</p>' .
  56. '<p>To add a new aggregated field, click the "Add new field" button and then fill out the form.</p>' .
  57. '<p>To remove a previously defined field, click the "Remove field" button.</p>' .
  58. '<p>You can also change the names or contained fields of existing aggregated fields.</p>'),
  59. );
  60. $form['fields']['#prefix'] = '<div id="search-api-alter-add-aggregation-field-settings">';
  61. $form['fields']['#suffix'] = '</div>';
  62. if (isset($this->changes)) {
  63. $form['fields']['#prefix'] .= '<div class="messages warning">All changes in the form will not be saved until the <em>Save configuration</em> button at the form bottom is clicked.</div>';
  64. }
  65. foreach ($additional as $name => $field) {
  66. $form['fields'][$name] = array(
  67. '#type' => 'fieldset',
  68. '#title' => $field['name'] ? $field['name'] : t('New field'),
  69. '#collapsible' => TRUE,
  70. '#collapsed' => (boolean) $field['name'],
  71. );
  72. $form['fields'][$name]['name'] = array(
  73. '#type' => 'textfield',
  74. '#title' => t('New field name'),
  75. '#default_value' => $field['name'],
  76. '#required' => TRUE,
  77. );
  78. $form['fields'][$name]['type'] = array(
  79. '#type' => 'select',
  80. '#title' => t('Aggregation type'),
  81. '#options' => $types,
  82. '#default_value' => $field['type'],
  83. '#required' => TRUE,
  84. );
  85. $form['fields'][$name]['type_descriptions'] = $type_descriptions;
  86. $type_selector = ':input[name="callbacks[search_api_alter_add_aggregation][settings][fields][' . $name . '][type]"]';
  87. foreach (array_keys($types) as $type) {
  88. $form['fields'][$name]['type_descriptions'][$type]['#states']['visible'][$type_selector]['value'] = $type;
  89. }
  90. $form['fields'][$name]['separator'] = array(
  91. '#type' => 'textfield',
  92. '#title' => t('Fulltext separator'),
  93. '#description' => t('For aggregation type "Fulltext", set the text that should be used to separate the aggregated field values. Use "\t" for tabs and "\n" for newline characters.'),
  94. '#default_value' => addcslashes(isset($field['separator']) ? $field['separator'] : "\n\n", "\0..\37\\"),
  95. '#states' => array(
  96. 'visible' => array(
  97. $type_selector => array(
  98. 'value' => 'fulltext',
  99. ),
  100. ),
  101. ),
  102. );
  103. $form['fields'][$name]['fields'] = array_merge($field_properties, array(
  104. '#type' => 'checkboxes',
  105. '#title' => t('Contained fields'),
  106. '#options' => $field_options,
  107. '#default_value' => drupal_map_assoc($field['fields']),
  108. '#attributes' => array('class' => array('search-api-alter-add-aggregation-fields')),
  109. '#required' => TRUE,
  110. ));
  111. $form['fields'][$name]['actions'] = array(
  112. '#type' => 'actions',
  113. 'remove' => array(
  114. '#type' => 'submit',
  115. '#value' => t('Remove field'),
  116. '#submit' => array('_search_api_add_aggregation_field_submit'),
  117. '#limit_validation_errors' => array(),
  118. '#name' => 'search_api_add_aggregation_remove_' . $name,
  119. '#ajax' => array(
  120. 'callback' => '_search_api_add_aggregation_field_ajax',
  121. 'wrapper' => 'search-api-alter-add-aggregation-field-settings',
  122. ),
  123. ),
  124. );
  125. }
  126. $form['actions']['#type'] = 'actions';
  127. $form['actions']['add_field'] = array(
  128. '#type' => 'submit',
  129. '#value' => t('Add new field'),
  130. '#submit' => array('_search_api_add_aggregation_field_submit'),
  131. '#limit_validation_errors' => array(),
  132. '#ajax' => array(
  133. 'callback' => '_search_api_add_aggregation_field_ajax',
  134. 'wrapper' => 'search-api-alter-add-aggregation-field-settings',
  135. ),
  136. );
  137. return $form;
  138. }
  139. public function configurationFormValidate(array $form, array &$values, array &$form_state) {
  140. unset($values['actions']);
  141. if (empty($values['fields'])) {
  142. return;
  143. }
  144. foreach ($values['fields'] as $name => $field) {
  145. unset($values['fields'][$name]['actions']);
  146. $fields = $values['fields'][$name]['fields'] = array_values(array_filter($field['fields']));
  147. if ($field['name'] && !$fields) {
  148. form_error($form['fields'][$name]['fields'], t('You have to select at least one field to aggregate. If you want to remove an aggregated field, please delete its name.'));
  149. }
  150. $values['fields'][$name]['separator'] = stripcslashes($field['separator']);
  151. }
  152. }
  153. public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
  154. if (empty($values['fields'])) {
  155. return array();
  156. }
  157. $index_fields = $this->index->getFields(FALSE);
  158. foreach ($values['fields'] as $name => $field) {
  159. if (!$field['name']) {
  160. unset($values['fields'][$name]);
  161. }
  162. else {
  163. $values['fields'][$name]['description'] = $this->fieldDescription($field, $index_fields);
  164. }
  165. }
  166. $this->options = $values;
  167. return $values;
  168. }
  169. public function alterItems(array &$items) {
  170. if (!$items) {
  171. return;
  172. }
  173. if (isset($this->options['fields'])) {
  174. $types = $this->getTypes('type');
  175. foreach ($items as $item) {
  176. $wrapper = $this->index->entityWrapper($item);
  177. foreach ($this->options['fields'] as $name => $field) {
  178. if ($field['name']) {
  179. $required_fields = array();
  180. foreach ($field['fields'] as $f) {
  181. if (!isset($required_fields[$f])) {
  182. $required_fields[$f]['type'] = $types[$field['type']];
  183. }
  184. }
  185. $fields = search_api_extract_fields($wrapper, $required_fields);
  186. $values = array();
  187. foreach ($fields as $f) {
  188. if (isset($f['value'])) {
  189. $values[] = $f['value'];
  190. }
  191. }
  192. $values = $this->flattenArray($values);
  193. $this->reductionType = $field['type'];
  194. $this->fulltextReductionSeparator = isset($field['separator']) ? $field['separator'] : "\n\n";
  195. $item->$name = array_reduce($values, array($this, 'reduce'), NULL);
  196. if ($field['type'] == 'count' && !$item->$name) {
  197. $item->$name = 0;
  198. }
  199. }
  200. }
  201. }
  202. }
  203. }
  204. /**
  205. * Helper method for reducing an array to a single value.
  206. */
  207. public function reduce($a, $b) {
  208. switch ($this->reductionType) {
  209. case 'fulltext':
  210. return isset($a) ? $a . $this->fulltextReductionSeparator . $b : $b;
  211. case 'sum':
  212. return $a + $b;
  213. case 'count':
  214. return $a + 1;
  215. case 'max':
  216. return isset($a) ? max($a, $b) : $b;
  217. case 'min':
  218. return isset($a) ? min($a, $b) : $b;
  219. case 'first':
  220. return isset($a) ? $a : $b;
  221. case 'first_char':
  222. $b = "$b";
  223. if (isset($a) || $b === '') {
  224. return $a;
  225. }
  226. return drupal_substr($b, 0, 1);
  227. case 'last':
  228. return isset($b) ? $b : $a;
  229. case 'list':
  230. if (!isset($a)) {
  231. $a = array();
  232. }
  233. $a[] = $b;
  234. return $a;
  235. }
  236. return NULL;
  237. }
  238. /**
  239. * Helper method for flattening a multi-dimensional array.
  240. */
  241. protected function flattenArray(array $data) {
  242. $ret = array();
  243. foreach ($data as $item) {
  244. if (!isset($item)) {
  245. continue;
  246. }
  247. if (is_scalar($item)) {
  248. $ret[] = $item;
  249. }
  250. else {
  251. $ret = array_merge($ret, $this->flattenArray($item));
  252. }
  253. }
  254. return $ret;
  255. }
  256. public function propertyInfo() {
  257. $types = $this->getTypes('type');
  258. $ret = array();
  259. if (isset($this->options['fields'])) {
  260. foreach ($this->options['fields'] as $name => $field) {
  261. $ret[$name] = array(
  262. 'label' => $field['name'],
  263. 'description' => empty($field['description']) ? '' : $field['description'],
  264. 'type' => $types[$field['type']],
  265. );
  266. }
  267. }
  268. return $ret;
  269. }
  270. /**
  271. * Helper method for creating a field description.
  272. */
  273. protected function fieldDescription(array $field, array $index_fields) {
  274. $fields = array();
  275. foreach ($field['fields'] as $f) {
  276. $fields[] = isset($index_fields[$f]) ? $index_fields[$f]['name'] : $f;
  277. }
  278. $type = $this->getTypes();
  279. $type = $type[$field['type']];
  280. return t('A @type aggregation of the following fields: @fields.', array('@type' => $type, '@fields' => implode(', ', $fields)));
  281. }
  282. /**
  283. * Helper method for getting all available aggregation types.
  284. *
  285. * @param string $info
  286. * (optional) One of "name", "type" or "description", to indicate what
  287. * information should be returned for the types.
  288. *
  289. * @return string[]
  290. * An associative array of aggregation type identifiers mapped to their
  291. * names, data types or descriptions, as requested.
  292. */
  293. protected function getTypes($info = 'name') {
  294. switch ($info) {
  295. case 'name':
  296. return array(
  297. 'fulltext' => t('Fulltext'),
  298. 'sum' => t('Sum'),
  299. 'count' => t('Count'),
  300. 'max' => t('Maximum'),
  301. 'min' => t('Minimum'),
  302. 'first' => t('First'),
  303. 'first_char' => t('First letter'),
  304. 'last' => t('Last'),
  305. 'list' => t('List'),
  306. );
  307. case 'type':
  308. return array(
  309. 'fulltext' => 'text',
  310. 'sum' => 'integer',
  311. 'count' => 'integer',
  312. 'max' => 'integer',
  313. 'min' => 'integer',
  314. 'first' => 'token',
  315. 'first_char' => 'token',
  316. 'last' => 'token',
  317. 'list' => 'list<token>',
  318. );
  319. case 'description':
  320. return array(
  321. 'fulltext' => t('The Fulltext aggregation concatenates the text data of all contained fields.'),
  322. 'sum' => t('The Sum aggregation adds the values of all contained fields numerically.'),
  323. 'count' => t('The Count aggregation takes the total number of contained field values as the aggregated field value.'),
  324. 'max' => t('The Maximum aggregation computes the numerically largest contained field value.'),
  325. 'min' => t('The Minimum aggregation computes the numerically smallest contained field value.'),
  326. 'first' => t('The First aggregation will simply keep the first encountered field value. This is helpful foremost when you know that a list field will only have a single value.'),
  327. 'first_char' => t('The "First letter" aggregation uses just the first letter of the first encountered field value as the aggregated value. This can, for example, be used to build a Glossary view.'),
  328. 'last' => t('The Last aggregation will simply keep the last encountered field value.'),
  329. 'list' => t('The List aggregation collects all field values into a multi-valued field containing all values.'),
  330. );
  331. }
  332. return array();
  333. }
  334. /**
  335. * Submit helper callback for buttons in the callback's configuration form.
  336. */
  337. public function formButtonSubmit(array $form, array &$form_state) {
  338. $button_name = $form_state['triggering_element']['#name'];
  339. if ($button_name == 'op') {
  340. // Increment $i until the corresponding field is not set, then create the
  341. // field with that number as suffix.
  342. for ($i = 1; isset($this->options['fields']['search_api_aggregation_' . $i]); ++$i) {
  343. }
  344. $this->options['fields']['search_api_aggregation_' . $i] = array(
  345. 'name' => '',
  346. 'type' => 'fulltext',
  347. 'fields' => array(),
  348. );
  349. }
  350. else {
  351. $field = substr($button_name, 34);
  352. unset($this->options['fields'][$field]);
  353. }
  354. $form_state['rebuild'] = TRUE;
  355. $this->changes = TRUE;
  356. }
  357. }
  358. /**
  359. * Submit function for buttons in the callback's configuration form.
  360. */
  361. function _search_api_add_aggregation_field_submit(array $form, array &$form_state) {
  362. $form_state['callbacks']['search_api_alter_add_aggregation']->formButtonSubmit($form, $form_state);
  363. }
  364. /**
  365. * AJAX submit function for buttons in the callback's configuration form.
  366. */
  367. function _search_api_add_aggregation_field_ajax(array $form, array &$form_state) {
  368. return $form['callbacks']['settings']['search_api_alter_add_aggregation']['fields'];
  369. }