search_api.views.inc 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828
  1. <?php
  2. /**
  3. * @file
  4. * Views hook implementations for the Search API module.
  5. */
  6. use Drupal\Core\Entity\FieldableEntityInterface;
  7. use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
  8. use Drupal\Core\TypedData\DataDefinitionInterface;
  9. use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
  10. use Drupal\search_api\Datasource\DatasourceInterface;
  11. use Drupal\search_api\Entity\Index;
  12. use Drupal\search_api\Item\FieldInterface;
  13. use Drupal\search_api\SearchApiException;
  14. use Drupal\search_api\Utility\Utility;
  15. /**
  16. * Implements hook_views_data().
  17. *
  18. * For each search index, we provide the following tables:
  19. * - One base table, with key "search_api_index_INDEX", which contains field,
  20. * filter, argument and sort handlers for all indexed fields. (Field handlers,
  21. * too, to allow things like click-sorting.)
  22. * - Tables for each datasource, by default with key
  23. * "search_api_datasource_INDEX_DATASOURCE", with field and (where applicable)
  24. * relationship handlers for each property of the datasource. Those will be
  25. * joined to the index base table by default.
  26. *
  27. * Also, for each entity type encountered in any table, a table with
  28. * field/relationship handlers for all of that entity type's properties is
  29. * created. Those tables will use the key "search_api_entity_ENTITY".
  30. */
  31. function search_api_views_data() {
  32. $data = [];
  33. /** @var \Drupal\search_api\IndexInterface $index */
  34. foreach (Index::loadMultiple() as $index) {
  35. try {
  36. // Fill in base data.
  37. $key = 'search_api_index_' . $index->id();
  38. $table = &$data[$key];
  39. $index_label = $index->label();
  40. $table['table']['group'] = t('Index @name', ['@name' => $index_label]);
  41. $table['table']['base'] = [
  42. 'field' => 'search_api_id',
  43. 'index' => $index->id(),
  44. 'title' => t('Index @name', ['@name' => $index_label]),
  45. 'help' => t('Use the @name search index for filtering and retrieving data.', ['@name' => $index_label]),
  46. 'query_id' => 'search_api_query',
  47. ];
  48. // Add suitable handlers for all indexed fields.
  49. foreach ($index->getFields(TRUE) as $field_id => $field) {
  50. $field_alias = _search_api_views_find_field_alias($field_id, $table);
  51. $field_definition = _search_api_views_get_handlers($field);
  52. // The field handler has to be extra, since it is a) determined by the
  53. // field's underlying property and b) needs a different "real field"
  54. // set.
  55. if ($field->getPropertyPath()) {
  56. $field_handler = _search_api_views_get_field_handler_for_property($field->getDataDefinition(), $field->getPropertyPath());
  57. if ($field_handler) {
  58. $field_definition['field'] = $field_handler;
  59. $field_definition['field']['real field'] = $field->getCombinedPropertyPath();
  60. $field_definition['field']['search_api field'] = $field_id;
  61. }
  62. }
  63. if ($field_definition) {
  64. $field_label = $field->getLabel();
  65. $field_definition += [
  66. 'title' => $field_label,
  67. 'help' => $field->getDescription() ?: t('(No description available)'),
  68. ];
  69. if ($datasource = $field->getDatasource()) {
  70. $field_definition['group'] = t('@datasource datasource', ['@datasource' => $datasource->label()]);
  71. }
  72. if ($field_id != $field_alias) {
  73. $field_definition['real field'] = $field_id;
  74. }
  75. if (isset($field_definition['field'])) {
  76. $field_definition['field']['title'] = t('@field (indexed field)', ['@field' => $field_label]);
  77. }
  78. $table[$field_alias] = $field_definition;
  79. }
  80. }
  81. // Add special fields.
  82. _search_api_views_data_special_fields($table);
  83. // Add relationships for field data of all datasources.
  84. $datasource_tables_prefix = 'search_api_datasource_' . $index->id() . '_';
  85. foreach ($index->getDatasources() as $datasource_id => $datasource) {
  86. $table_key = _search_api_views_find_field_alias($datasource_tables_prefix . $datasource_id, $data);
  87. $data[$table_key] = _search_api_views_datasource_table($datasource, $data);
  88. // Automatically join this table for views of this index.
  89. $data[$table_key]['table']['join'][$key] = [
  90. 'join_id' => 'search_api',
  91. ];
  92. }
  93. }
  94. catch (\Exception $e) {
  95. $args = [
  96. '%index' => $index->label(),
  97. ];
  98. watchdog_exception('search_api', $e, '%type while computing Views data for index %index: @message in %function (line %line of %file).', $args);
  99. }
  100. }
  101. return array_filter($data);
  102. }
  103. /**
  104. * Implements hook_views_plugins_cache_alter().
  105. */
  106. function search_api_views_plugins_cache_alter(array &$plugins) {
  107. // Collect all base tables provided by this module.
  108. $bases = [];
  109. /** @var \Drupal\search_api\IndexInterface $index */
  110. foreach (Index::loadMultiple() as $index) {
  111. $bases[] = 'search_api_index_' . $index->id();
  112. }
  113. $plugins['search_api']['base'] = $bases;
  114. }
  115. /**
  116. * Implements hook_views_plugins_row_alter().
  117. */
  118. function search_api_views_plugins_row_alter(array &$plugins) {
  119. // Collect all base tables provided by this module.
  120. $bases = [];
  121. /** @var \Drupal\search_api\IndexInterface $index */
  122. foreach (Index::loadMultiple() as $index) {
  123. $bases[] = 'search_api_index_' . $index->id();
  124. }
  125. $plugins['search_api']['base'] = $bases;
  126. }
  127. /**
  128. * Finds an unused field alias for a field in a Views table definition.
  129. *
  130. * @param string $field_id
  131. * The original ID of the Search API field.
  132. * @param array $table
  133. * The Views table definition.
  134. *
  135. * @return string
  136. * The field alias to use.
  137. */
  138. function _search_api_views_find_field_alias($field_id, array &$table) {
  139. $base = $field_alias = preg_replace('/[^a-zA-Z0-9]+/S', '_', $field_id);
  140. $i = 0;
  141. while (isset($table[$field_alias])) {
  142. $field_alias = $base . '_' . ++$i;
  143. }
  144. return $field_alias;
  145. }
  146. /**
  147. * Returns the Views handlers to use for a given field.
  148. *
  149. * @param \Drupal\search_api\Item\FieldInterface $field
  150. * The field to add to the definition.
  151. *
  152. * @return array
  153. * The Views definition to add for the given field.
  154. */
  155. function _search_api_views_get_handlers(FieldInterface $field) {
  156. $mapping = _search_api_views_handler_mapping();
  157. try {
  158. $types = [];
  159. $definition = $field->getDataDefinition();
  160. if ($definition->getSetting('target_type')) {
  161. $types[] = 'entity:' . $definition->getSetting('target_type');
  162. $types[] = 'entity';
  163. }
  164. if ($definition->getSetting('allowed_values')) {
  165. $types[] = 'options';
  166. }
  167. $types[] = $field->getType();
  168. /** @var \Drupal\search_api\DataType\DataTypeInterface $data_type */
  169. $data_type = \Drupal::service('plugin.manager.search_api.data_type')->createInstance($field->getType());
  170. if (!$data_type->isDefault()) {
  171. $types[] = $data_type->getFallbackType();
  172. }
  173. foreach ($types as $type) {
  174. if (isset($mapping[$type])) {
  175. _search_api_views_handler_adjustments($type, $field, $mapping[$type]);
  176. return $mapping[$type];
  177. }
  178. }
  179. }
  180. catch (SearchApiException $e) {
  181. $vars['%index'] = $field->getIndex()->label();
  182. $vars['%field'] = $field->getPrefixedLabel();
  183. watchdog_exception('search_api', $e, '%type while adding Views handlers for field %field on index %index: @message in %function (line %line of %file).', $vars);
  184. }
  185. return [];
  186. }
  187. /**
  188. * Determines the mapping of Search API data types to their Views handlers.
  189. *
  190. * @return array
  191. * An associative array with data types as the keys and Views field data
  192. * definitions as the values. In addition to all normally defined data types,
  193. * keys can also be "options" for any field with an options list, "entity" for
  194. * general entity-typed fields or "entity:ENTITY_TYPE" (with "ENTITY_TYPE"
  195. * being the machine name of an entity type) for entities of that type.
  196. *
  197. * @see search_api_views_handler_mapping_alter()
  198. */
  199. function _search_api_views_handler_mapping() {
  200. $mapping = &drupal_static(__FUNCTION__);
  201. if (!isset($mapping)) {
  202. $mapping = [
  203. 'boolean' => [
  204. 'argument' => [
  205. 'id' => 'search_api',
  206. ],
  207. 'filter' => [
  208. 'id' => 'search_api_boolean',
  209. ],
  210. 'sort' => [
  211. 'id' => 'search_api',
  212. ],
  213. ],
  214. 'date' => [
  215. 'argument' => [
  216. 'id' => 'search_api_date',
  217. ],
  218. 'filter' => [
  219. 'id' => 'search_api_date',
  220. ],
  221. 'sort' => [
  222. 'id' => 'search_api',
  223. ],
  224. ],
  225. 'decimal' => [
  226. 'argument' => [
  227. 'id' => 'search_api',
  228. 'filter' => 'floatval',
  229. ],
  230. 'filter' => [
  231. 'id' => 'search_api_numeric',
  232. ],
  233. 'sort' => [
  234. 'id' => 'search_api',
  235. ],
  236. ],
  237. 'integer' => [
  238. 'argument' => [
  239. 'id' => 'search_api',
  240. 'filter' => 'intval',
  241. ],
  242. 'filter' => [
  243. 'id' => 'search_api_numeric',
  244. ],
  245. 'sort' => [
  246. 'id' => 'search_api',
  247. ],
  248. ],
  249. 'string' => [
  250. 'argument' => [
  251. 'id' => 'search_api',
  252. ],
  253. 'filter' => [
  254. 'id' => 'search_api_string',
  255. ],
  256. 'sort' => [
  257. 'id' => 'search_api',
  258. ],
  259. ],
  260. 'text' => [
  261. 'argument' => [
  262. 'id' => 'search_api',
  263. ],
  264. 'filter' => [
  265. 'id' => 'search_api_text',
  266. ],
  267. 'sort' => [
  268. 'id' => 'search_api',
  269. ],
  270. ],
  271. 'options' => [
  272. 'argument' => [
  273. 'id' => 'search_api',
  274. ],
  275. 'filter' => [
  276. 'id' => 'search_api_options',
  277. ],
  278. 'sort' => [
  279. 'id' => 'search_api',
  280. ],
  281. ],
  282. 'entity:taxonomy_term' => [
  283. 'argument' => [
  284. 'id' => 'search_api_term',
  285. ],
  286. 'filter' => [
  287. 'id' => 'search_api_term',
  288. ],
  289. 'sort' => [
  290. 'id' => 'search_api',
  291. ],
  292. ],
  293. 'entity:user' => [
  294. 'argument' => [
  295. 'id' => 'search_api',
  296. 'filter' => 'intval',
  297. ],
  298. 'filter' => [
  299. 'id' => 'search_api_user',
  300. ],
  301. 'sort' => [
  302. 'id' => 'search_api',
  303. ],
  304. ],
  305. 'entity:node_type' => [
  306. 'argument' => [
  307. 'id' => 'search_api',
  308. ],
  309. 'filter' => [
  310. 'id' => 'search_api_options',
  311. 'options callback' => 'node_type_get_names',
  312. ],
  313. 'sort' => [
  314. 'id' => 'search_api',
  315. ],
  316. ],
  317. ];
  318. $alter_id = 'search_api_views_handler_mapping';
  319. \Drupal::moduleHandler()->alter($alter_id, $mapping);
  320. }
  321. return $mapping;
  322. }
  323. /**
  324. * Makes necessary, field-specific adjustments to Views handler definitions.
  325. *
  326. * @param string $type
  327. * The type of field, as defined in _search_api_views_handler_mapping().
  328. * @param \Drupal\search_api\Item\FieldInterface $field
  329. * The field whose handler definitions are being created.
  330. * @param array $definitions
  331. * The handler definitions for the field, as a reference.
  332. */
  333. function _search_api_views_handler_adjustments($type, FieldInterface $field, array &$definitions) {
  334. // By default, all fields can be empty (or at least have to be treated that
  335. // way by the Search API).
  336. if (!isset($definitions['filter']['allow empty'])) {
  337. $definitions['filter']['allow empty'] = TRUE;
  338. }
  339. // For taxonomy term references, set the referenced vocabulary.
  340. $data_definition = $field->getDataDefinition();
  341. if ($type == 'entity:taxonomy_term') {
  342. if (isset($data_definition->getSettings()['handler_settings']['target_bundles'])) {
  343. $target_bundles = $data_definition->getSettings()['handler_settings']['target_bundles'];
  344. if (count($target_bundles) == 1) {
  345. $definitions['filter']['vocabulary'] = reset($target_bundles);
  346. }
  347. }
  348. }
  349. elseif ($type == 'options') {
  350. if ($data_definition instanceof FieldItemDataDefinition) {
  351. // If this is a normal Field API field, dynamically retrieve the options
  352. // list at query time.
  353. $field_definition = $data_definition->getFieldDefinition();
  354. $bundle = $field_definition->getTargetBundle();
  355. $field_name = $field_definition->getName();
  356. $entity_type = $field_definition->getTargetEntityTypeId();
  357. $definitions['filter']['options callback'] = '_search_api_views_get_allowed_values';
  358. $definitions['filter']['options arguments'] = [$entity_type, $bundle, $field_name];
  359. }
  360. else {
  361. // Otherwise, include the options list verbatim in the Views data, unless
  362. // it's too big (or doesn't look valid).
  363. $options = $data_definition->getSetting('allowed_values');
  364. if (is_array($options) && count($options) <= 50) {
  365. // Since the Views InOperator filter plugin doesn't allow just including
  366. // the options in the definition, we use this workaround.
  367. $definitions['filter']['options callback'] = 'array_filter';
  368. $definitions['filter']['options arguments'] = [$options];
  369. }
  370. }
  371. }
  372. }
  373. /**
  374. * Adds definitions for our special fields to a Views data table definition.
  375. *
  376. * @param array $table
  377. * The existing Views data table definition.
  378. */
  379. function _search_api_views_data_special_fields(array &$table) {
  380. $id_field = _search_api_views_find_field_alias('search_api_id', $table);
  381. $table[$id_field]['title'] = t('Item ID');
  382. $table[$id_field]['help'] = t("The item's internal (Search API-specific) ID");
  383. $table[$id_field]['field']['id'] = 'standard';
  384. $table[$id_field]['sort']['id'] = 'search_api';
  385. if ($id_field != 'search_api_id') {
  386. $table[$id_field]['real field'] = 'search_api_id';
  387. }
  388. $datasource_field = _search_api_views_find_field_alias('search_api_datasource', $table);
  389. $table[$datasource_field]['title'] = t('Datasource');
  390. $table[$datasource_field]['help'] = t("The data source ID");
  391. $table[$datasource_field]['argument']['id'] = 'search_api';
  392. $table[$datasource_field]['argument']['disable_break_phrase'] = TRUE;
  393. $table[$datasource_field]['field']['id'] = 'standard';
  394. $table[$datasource_field]['filter']['id'] = 'search_api_datasource';
  395. $table[$datasource_field]['sort']['id'] = 'search_api';
  396. if ($datasource_field != 'search_api_datasource') {
  397. $table[$datasource_field]['real field'] = 'search_api_datasource';
  398. }
  399. $language_field = _search_api_views_find_field_alias('search_api_language', $table);
  400. $table[$language_field]['title'] = t('Item language');
  401. $table[$language_field]['help'] = t("The item's language");
  402. $table[$language_field]['field']['id'] = 'language';
  403. $table[$language_field]['filter']['id'] = 'search_api_language';
  404. $table[$language_field]['filter']['allow empty'] = FALSE;
  405. $table[$language_field]['sort']['id'] = 'search_api';
  406. if ($language_field != 'search_api_language') {
  407. $table[$language_field]['real field'] = 'search_api_language';
  408. }
  409. $relevance_field = _search_api_views_find_field_alias('search_api_relevance', $table);
  410. $table[$relevance_field]['group'] = t('Search');
  411. $table[$relevance_field]['title'] = t('Relevance');
  412. $table[$relevance_field]['help'] = t('The relevance of this search result with respect to the query');
  413. $table[$relevance_field]['field']['type'] = 'decimal';
  414. $table[$relevance_field]['field']['id'] = 'numeric';
  415. $table[$relevance_field]['field']['search_api field'] = 'search_api_relevance';
  416. $table[$relevance_field]['sort']['id'] = 'search_api';
  417. if ($relevance_field != 'search_api_relevance') {
  418. $table[$relevance_field]['real field'] = 'search_api_relevance';
  419. }
  420. $excerpt_field = _search_api_views_find_field_alias('search_api_excerpt', $table);
  421. $table[$excerpt_field]['group'] = t('Search');
  422. $table[$excerpt_field]['title'] = t('Excerpt');
  423. $table[$excerpt_field]['help'] = t('The search result excerpted to show found search terms');
  424. $table[$excerpt_field]['field']['id'] = 'search_api';
  425. $table[$excerpt_field]['field']['filter_type'] = 'xss';
  426. if ($excerpt_field != 'search_api_excerpt') {
  427. $table[$excerpt_field]['real field'] = 'search_api_excerpt';
  428. }
  429. $fulltext_field = _search_api_views_find_field_alias('search_api_fulltext', $table);
  430. $table[$fulltext_field]['group'] = t('Search');
  431. $table[$fulltext_field]['title'] = t('Fulltext search');
  432. $table[$fulltext_field]['help'] = t('Search several or all fulltext fields at once.');
  433. $table[$fulltext_field]['filter']['id'] = 'search_api_fulltext';
  434. $table[$fulltext_field]['argument']['id'] = 'search_api_fulltext';
  435. if ($fulltext_field != 'search_api_fulltext') {
  436. $table[$fulltext_field]['real field'] = 'search_api_fulltext';
  437. }
  438. $mlt_field = _search_api_views_find_field_alias('search_api_more_like_this', $table);
  439. $table[$mlt_field]['group'] = t('Search');
  440. $table[$mlt_field]['title'] = t('More like this');
  441. $table[$mlt_field]['help'] = t('Find similar content.');
  442. $table[$mlt_field]['argument']['id'] = 'search_api_more_like_this';
  443. if ($mlt_field != 'search_api_more_like_this') {
  444. $table[$mlt_field]['real field'] = 'search_api_more_like_this';
  445. }
  446. // @todo Add an "All taxonomy terms" contextual filter (if applicable).
  447. }
  448. /**
  449. * Creates a Views table definition for one datasource of an index.
  450. *
  451. * @param \Drupal\search_api\Datasource\DatasourceInterface $datasource
  452. * The datasource for which to create a table definition.
  453. * @param array $data
  454. * The existing Views data definitions. Passed by reference so additionally
  455. * needed tables can be inserted.
  456. *
  457. * @return array
  458. * A Views table definition for the given datasource.
  459. */
  460. function _search_api_views_datasource_table(DatasourceInterface $datasource, array &$data) {
  461. $datasource_id = $datasource->getPluginId();
  462. $table = [
  463. 'table' => [
  464. 'group' => t('@datasource datasource', ['@datasource' => $datasource->label()]),
  465. 'index' => $datasource->getIndex()->id(),
  466. 'datasource' => $datasource_id,
  467. ],
  468. ];
  469. $entity_type_id = $datasource->getEntityTypeId();
  470. if ($entity_type_id) {
  471. $table['table']['entity type'] = $entity_type_id;
  472. $table['table']['entity revision'] = FALSE;
  473. }
  474. _search_api_views_add_handlers_for_properties($datasource->getPropertyDefinitions(), $table, $data);
  475. // Prefix the "real field" of each entry with the datasource ID.
  476. foreach ($table as $key => $definition) {
  477. if ($key == 'table') {
  478. continue;
  479. }
  480. $real_field = isset($definition['real field']) ? $definition['real field'] : $key;
  481. $table[$key]['real field'] = Utility::createCombinedId($datasource_id, $real_field);
  482. // Relationships sometimes have different real fields set, since they might
  483. // also include the nested property that contains the actual reference. So,
  484. // if a "real field" is set for that, we need to adapt it as well.
  485. if (isset($definition['relationship']['real field'])) {
  486. $real_field = $definition['relationship']['real field'];
  487. $table[$key]['relationship']['real field'] = Utility::createCombinedId($datasource_id, $real_field);
  488. }
  489. }
  490. return $table;
  491. }
  492. /**
  493. * Creates a Views table definition for an entity type.
  494. *
  495. * @param string $entity_type_id
  496. * The ID of the entity type.
  497. * @param array $data
  498. * The existing Views data definitions, passed by reference.
  499. *
  500. * @return array
  501. * A Views table definition for the given entity type. Or an empty array if
  502. * the entity type could not be found.
  503. */
  504. function _search_api_views_entity_type_table($entity_type_id, array &$data) {
  505. $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
  506. if (!$entity_type || !$entity_type->entityClassImplements(FieldableEntityInterface::class)) {
  507. return [];
  508. }
  509. $table = [
  510. 'table' => [
  511. 'group' => t('@entity_type relationship', ['@entity_type' => $entity_type->getLabel()]),
  512. 'entity type' => $entity_type_id,
  513. 'entity revision' => FALSE,
  514. ],
  515. ];
  516. $entity_field_manager = \Drupal::getContainer()->get('entity_field.manager');
  517. $bundle_info = \Drupal::getContainer()->get('entity_type.bundle.info');
  518. $properties = $entity_field_manager->getBaseFieldDefinitions($entity_type_id);
  519. foreach (array_keys($bundle_info->getBundleInfo($entity_type_id)) as $bundle_id) {
  520. $additional = $entity_field_manager->getFieldDefinitions($entity_type_id, $bundle_id);
  521. $properties += $additional;
  522. }
  523. _search_api_views_add_handlers_for_properties($properties, $table, $data);
  524. return $table;
  525. }
  526. /**
  527. * Adds field and relationship handlers for the given properties.
  528. *
  529. * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $properties
  530. * The properties for which handlers should be added.
  531. * @param array $table
  532. * The existing Views data table definition, passed by reference.
  533. * @param array $data
  534. * The existing Views data definitions, passed by reference.
  535. */
  536. function _search_api_views_add_handlers_for_properties(array $properties, array &$table, array &$data) {
  537. $entity_reference_types = array_flip([
  538. 'field_item:entity_reference',
  539. 'field_item:image',
  540. 'field_item:file',
  541. ]);
  542. foreach ($properties as $property_path => $property) {
  543. $key = _search_api_views_find_field_alias($property_path, $table);
  544. $original_property = $property;
  545. $property = \Drupal::getContainer()
  546. ->get('search_api.fields_helper')
  547. ->getInnerProperty($property);
  548. // Add a field handler, if applicable.
  549. $definition = _search_api_views_get_field_handler_for_property($property, $property_path);
  550. if ($definition) {
  551. $table[$key]['field'] = $definition;
  552. }
  553. // For entity-typed properties, also add a relationship to the entity type
  554. // table.
  555. if ($property instanceof FieldItemDataDefinition && isset($entity_reference_types[$property->getDataType()])) {
  556. $entity_type_id = $property->getSetting('target_type');
  557. if ($entity_type_id) {
  558. $entity_type_table_key = 'search_api_entity_' . $entity_type_id;
  559. if (!isset($data[$entity_type_table_key])) {
  560. // Initialize the table definition before calling
  561. // _search_api_views_entity_type_table() to avoid an infinite
  562. // recursion.
  563. $data[$entity_type_table_key] = [];
  564. $data[$entity_type_table_key] = _search_api_views_entity_type_table($entity_type_id, $data);
  565. }
  566. // Add the relationship only if we have a non-empty table definition.
  567. if ($data[$entity_type_table_key]) {
  568. // Get the entity type to determine the label for the relationship.
  569. $entity_type = \Drupal::entityTypeManager()
  570. ->getDefinition($entity_type_id);
  571. $entity_type_label = $entity_type ? $entity_type->getLabel() : $entity_type_id;
  572. $args = [
  573. '@label' => $entity_type_label,
  574. '@field_name' => $original_property->getLabel(),
  575. ];
  576. // Look through the child properties to find the data reference
  577. // property that should be the "real field" for the relationship.
  578. // (For Core entity references, this will usually be ":entity".)
  579. $suffix = '';
  580. foreach ($property->getPropertyDefinitions() as $name => $nested_property) {
  581. if ($nested_property instanceof DataReferenceDefinitionInterface) {
  582. $suffix = ":$name";
  583. break;
  584. }
  585. }
  586. $table[$key]['relationship'] = [
  587. 'title' => t('@label referenced from @field_name', $args),
  588. 'label' => t('@field_name: @label', $args),
  589. 'help' => $property->getDescription() ?: t('(No description available)'),
  590. 'id' => 'search_api',
  591. 'base' => $entity_type_table_key,
  592. 'entity type' => $entity_type_id,
  593. 'entity revision' => FALSE,
  594. 'real field' => $property_path . $suffix,
  595. ];
  596. }
  597. }
  598. }
  599. if (!empty($table[$key]) && empty($table[$key]['title'])) {
  600. $table[$key]['title'] = $original_property->getLabel();
  601. $table[$key]['help'] = $original_property->getDescription() ?: t('(No description available)');
  602. if ($key != $property_path) {
  603. $table[$key]['real field'] = $property_path;
  604. }
  605. }
  606. }
  607. }
  608. /**
  609. * Computes a handler definition for the given property.
  610. *
  611. * @param \Drupal\Core\TypedData\DataDefinitionInterface $property
  612. * The property definition.
  613. * @param string|null $property_path
  614. * (optional) The property path of the property. If set, it will be used for
  615. * Field API fields to set the "field_name" property of the definition.
  616. *
  617. * @return array|null
  618. * Either a Views field handler definition for this property, or NULL if the
  619. * property shouldn't have one.
  620. *
  621. * @see hook_search_api_views_field_handler_mapping_alter()
  622. */
  623. function _search_api_views_get_field_handler_for_property(DataDefinitionInterface $property, $property_path = NULL) {
  624. $mappings = _search_api_views_get_field_handler_mapping();
  625. // First, look for an exact match.
  626. $data_type = $property->getDataType();
  627. if (array_key_exists($data_type, $mappings['simple'])) {
  628. $definition = $mappings['simple'][$data_type];
  629. }
  630. else {
  631. // Then check all the patterns defined by regular expressions, defaulting to
  632. // the "default" definition.
  633. $definition = $mappings['default'];
  634. foreach (array_keys($mappings['regex']) as $regex) {
  635. if (preg_match($regex, $data_type)) {
  636. $definition = $mappings['regex'][$regex];
  637. }
  638. }
  639. }
  640. // Field items have a special handler, but need a fallback handler set to be
  641. // able to optionally circumvent entity field rendering. That's why we just
  642. // set the "field_item:…" types to their fallback handlers in
  643. // _search_api_views_get_field_handler_mapping(), along with non-field item
  644. // types, and here manually update entity field properties to have the correct
  645. // definition, with "search_api_field" handler, correct fallback handler and
  646. // "field_name" and "entity_type" correctly set.
  647. if (isset($definition) && $property instanceof FieldItemDataDefinition) {
  648. list(, $field_name) = Utility::splitPropertyPath($property_path, TRUE);
  649. if (!isset($definition['fallback_handler'])) {
  650. $definition['fallback_handler'] = $definition['id'];
  651. $definition['id'] = 'search_api_field';
  652. }
  653. $definition['field_name'] = $field_name;
  654. $definition['entity_type'] = $property
  655. ->getFieldDefinition()
  656. ->getTargetEntityTypeId();
  657. }
  658. return $definition;
  659. }
  660. /**
  661. * Retrieves the field handler mapping used by the Search API Views integration.
  662. *
  663. * @return array
  664. * An associative array with three keys:
  665. * - simple: An associative array mapping property data types to their field
  666. * handler definitions.
  667. * - regex: An array associative array mapping regular expressions for
  668. * property data types to their field handler definitions, ordered by
  669. * descending string length of the regular expression.
  670. * - default: The default definition for data types that match no other field.
  671. */
  672. function _search_api_views_get_field_handler_mapping() {
  673. $mappings = &drupal_static(__FUNCTION__);
  674. if (!isset($mappings)) {
  675. // First create a plain mapping and pass it to the alter hook.
  676. $plain_mapping = [];
  677. $plain_mapping['*'] = [
  678. 'id' => 'search_api',
  679. ];
  680. $text_mapping = [
  681. 'id' => 'search_api',
  682. 'filter_type' => 'xss',
  683. ];
  684. $plain_mapping['field_item:text_long'] = $text_mapping;
  685. $plain_mapping['field_item:text_with_summary'] = $text_mapping;
  686. $plain_mapping['search_api_html'] = $text_mapping;
  687. unset($text_mapping['filter_type']);
  688. $plain_mapping['search_api_text'] = $text_mapping;
  689. $numeric_mapping = [
  690. 'id' => 'search_api_numeric',
  691. ];
  692. $plain_mapping['field_item:integer'] = $numeric_mapping;
  693. $plain_mapping['field_item:list_integer'] = $numeric_mapping;
  694. $plain_mapping['integer'] = $numeric_mapping;
  695. $plain_mapping['timespan'] = $numeric_mapping;
  696. $float_mapping = [
  697. 'id' => 'search_api_numeric',
  698. 'float' => TRUE,
  699. ];
  700. $plain_mapping['field_item:decimal'] = $float_mapping;
  701. $plain_mapping['field_item:float'] = $float_mapping;
  702. $plain_mapping['field_item:list_float'] = $float_mapping;
  703. $plain_mapping['decimal'] = $float_mapping;
  704. $plain_mapping['float'] = $float_mapping;
  705. $date_mapping = [
  706. 'id' => 'search_api_date',
  707. ];
  708. $plain_mapping['field_item:created'] = $date_mapping;
  709. $plain_mapping['field_item:changed'] = $date_mapping;
  710. $plain_mapping['datetime_iso8601'] = $date_mapping;
  711. $plain_mapping['timestamp'] = $date_mapping;
  712. $bool_mapping = [
  713. 'id' => 'search_api_boolean',
  714. ];
  715. $plain_mapping['boolean'] = $bool_mapping;
  716. $plain_mapping['field_item:boolean'] = $bool_mapping;
  717. $ref_mapping = [
  718. 'id' => 'search_api_entity',
  719. ];
  720. $plain_mapping['field_item:entity_reference'] = $ref_mapping;
  721. $plain_mapping['field_item:comment'] = $ref_mapping;
  722. // Finally, set a default handler for unknown field items.
  723. $plain_mapping['field_item:*'] = [
  724. 'id' => 'search_api',
  725. ];
  726. // Let other modules change or expand this mapping.
  727. $alter_id = 'search_api_views_field_handler_mapping';
  728. \Drupal::moduleHandler()->alter($alter_id, $plain_mapping);
  729. // Then create a new, more practical structure, with the mappings grouped by
  730. // mapping type.
  731. $mappings = [
  732. 'simple' => [],
  733. 'regex' => [],
  734. 'default' => NULL,
  735. ];
  736. foreach ($plain_mapping as $type => $definition) {
  737. if ($type == '*') {
  738. $mappings['default'] = $definition;
  739. }
  740. elseif (strpos($type, '*') === FALSE) {
  741. $mappings['simple'][$type] = $definition;
  742. }
  743. else {
  744. // Transform the type into a PCRE regular expression, taking care to
  745. // quote everything except for the wildcards.
  746. $parts = explode('*', $type);
  747. // Passing the second parameter to preg_quote() is a bit tricky with
  748. // array_map(), we need to construct an array of slashes.
  749. $slashes = array_fill(0, count($parts), '/');
  750. $parts = array_map('preg_quote', $parts, $slashes);
  751. // Use the "S" modifier for closer analysis of the pattern, since it
  752. // might be executed a lot.
  753. $regex = '/^' . implode('.*', $parts) . '$/S';
  754. $mappings['regex'][$regex] = $definition;
  755. }
  756. }
  757. // Finally, order the regular expressions descending by their lengths.
  758. $compare = function ($a, $b) {
  759. return strlen($b) - strlen($a);
  760. };
  761. uksort($mappings['regex'], $compare);
  762. }
  763. return $mappings;
  764. }