Source: includes/classes/Indexable/Comment/QueryIntegration.php

  1. <?php
  2. /**
  3. * Integrate with WP_Comment_Query
  4. *
  5. * @since 3.6.0
  6. * @package elasticpress
  7. */
  8. namespace ElasticPress\Indexable\Comment;
  9. use WP_Comment_Query;
  10. use ElasticPress\Indexables;
  11. use ElasticPress\Utils;
  12. if ( ! defined( 'ABSPATH' ) ) {
  13. exit; // Exit if accessed directly.
  14. }
  15. /**
  16. * Query integration class
  17. */
  18. class QueryIntegration {
  19. /**
  20. * Comment indexable
  21. *
  22. * @var Comment
  23. */
  24. public $indexable = '';
  25. /**
  26. * Index name
  27. *
  28. * @var string
  29. */
  30. public $index = '';
  31. /**
  32. * Sets up the appropriate actions and filters.
  33. *
  34. * @param string $indexable_slug Indexable slug. Optional.
  35. *
  36. * @since 3.6.0
  37. */
  38. public function __construct( $indexable_slug = 'comment' ) {
  39. /**
  40. * Filter whether to enable query integration during indexing
  41. *
  42. * @since 4.5.2
  43. * @hook ep_enable_query_integration_during_indexing
  44. *
  45. * @param {bool} $enable To allow query integration during indexing
  46. * @param {string} $indexable_slug Indexable slug
  47. * @return {bool} New value
  48. */
  49. $allow_query_integration_during_indexing = apply_filters( 'ep_enable_query_integration_during_indexing', false, $indexable_slug );
  50. // Check if we are currently indexing
  51. if ( Utils\is_indexing() && ! $allow_query_integration_during_indexing ) {
  52. return;
  53. }
  54. // Add header
  55. add_action( 'pre_get_comments', array( $this, 'action_pre_get_comments' ), 5 );
  56. // Filter comment query
  57. add_filter( 'comments_pre_query', [ $this, 'maybe_filter_query' ], 10, 2 );
  58. }
  59. /**
  60. * Add EP header
  61. *
  62. * @param WP_Comment_Query $query Query object
  63. * @since 3.6.0
  64. * @return void
  65. */
  66. public function action_pre_get_comments( WP_Comment_Query $query ) {
  67. /**
  68. * Filter to skip WP_Comment_Query integration
  69. *
  70. * @hook ep_skip_comment_query_integration
  71. * @since 3.6.0
  72. * @param {bool} $skip True to skip
  73. * @param {WP_Comment_Query} $query WP_Comment_Query to evaluate
  74. * @return {bool} New skip value
  75. */
  76. if ( ! Indexables::factory()->get( 'comment' )->elasticpress_enabled( $query ) || apply_filters( 'ep_skip_comment_query_integration', false, $query ) ) {
  77. return;
  78. }
  79. if ( ! headers_sent() ) {
  80. /**
  81. * Manually setting a header as $wp_query isn't yet initialized
  82. * when we call: add_filter('wp_headers', 'filter_wp_headers');
  83. */
  84. header( 'X-ElasticPress-Search: true' );
  85. }
  86. }
  87. /**
  88. * If WP_Comment_Query meets certain conditions, query results from ES
  89. *
  90. * @param array $results Query results.
  91. * @param WP_Comment_Query $query Current query.
  92. * @since 3.6.0
  93. * @return array
  94. */
  95. public function maybe_filter_query( $results, WP_Comment_Query $query ) {
  96. $this->indexable = Indexables::factory()->get( 'comment' );
  97. if ( ! $this->indexable->elasticpress_enabled( $query ) || apply_filters( 'ep_skip_comment_query_integration', false, $query ) ) {
  98. return $results;
  99. }
  100. /**
  101. * Filter cached comments pre-post query
  102. *
  103. * @hook ep_wp_query_cached_comments
  104. * @since 3.6.0
  105. * @param {mixed} $comments Comments or null
  106. * @param {WP_Comment_Query} $query WP_Comment_Query object
  107. * @return {array} New cached comments
  108. */
  109. $new_comments = apply_filters( 'ep_wp_query_cached_comments', null, $query );
  110. if ( null !== $new_comments ) {
  111. return $new_comments;
  112. }
  113. $new_comments = [];
  114. $formatted_args = $this->indexable->format_args( $query->query_vars );
  115. $scope = 'current';
  116. $site__in = [];
  117. $site__not_in = [];
  118. if ( ! empty( $query->query_vars['sites'] ) ) {
  119. _deprecated_argument( __FUNCTION__, '4.4.0', esc_html__( 'sites is deprecated. Use site__in instead.', 'elasticpress' ) );
  120. }
  121. if ( ! empty( $query->query_vars['site__in'] ) || ! empty( $query->query_vars['sites'] ) ) {
  122. $site__in = ! empty( $query->query_vars['site__in'] ) ? (array) $query->query_vars['site__in'] : (array) $query->query_vars['sites'];
  123. if ( in_array( 'all', $site__in, true ) ) {
  124. $scope = 'all';
  125. } elseif ( in_array( 'current', $site__in, true ) ) {
  126. $site__in = (array) get_current_blog_id();
  127. }
  128. }
  129. if ( ! empty( $query->query_vars['site__not_in'] ) ) {
  130. $site__not_in = (array) $query->query_vars['site__not_in'];
  131. }
  132. /**
  133. * Filter search scope
  134. *
  135. * @since 3.6.0
  136. *
  137. * @param mixed $scope The search scope. Accepts `all` (string), a single
  138. * site id (int or string), or an array of site ids (array).
  139. */
  140. $scope = apply_filters( 'ep_comment_search_scope', $scope );
  141. if ( ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) {
  142. $scope = 'current';
  143. }
  144. $this->index = null;
  145. if ( 'all' === $scope ) {
  146. $this->index = $this->indexable->get_network_alias();
  147. } elseif ( ! empty( $site__in ) ) {
  148. $this->index = [];
  149. foreach ( $site__in as $site_id ) {
  150. $this->index[] = $this->indexable->get_index_name( $site_id );
  151. }
  152. $this->index = implode( ',', $this->index );
  153. } elseif ( ! empty( $site__not_in ) ) {
  154. $sites = \get_sites(
  155. array(
  156. 'fields' => 'ids',
  157. 'site__not_in' => $site__not_in,
  158. )
  159. );
  160. foreach ( $sites as $site_id ) {
  161. if ( ! Utils\is_site_indexable( $site_id ) ) {
  162. continue;
  163. }
  164. $index[] = Indexables::factory()->get( 'comment' )->get_index_name( $site_id );
  165. }
  166. $this->index = implode( ',', $index );
  167. }
  168. $ep_query = $this->indexable->query_es( $formatted_args, $query->query_vars, $this->index, $query );
  169. if ( false === $ep_query ) {
  170. $query->elasticsearch_success = false;
  171. return $results;
  172. }
  173. if ( ! empty( $query->query_vars['count'] ) ) {
  174. return count( $ep_query['documents'] );
  175. }
  176. $query->found_comments = $ep_query['found_documents']['value'];
  177. $query->max_num_pages = $query->query_vars['number'] <= 0 ? 1 : max( 1, ceil( $query->found_comments / absint( $query->query_vars['number'] ) ) );
  178. $query->elasticsearch_success = true;
  179. // Determine how we should format the results from ES based on the fields parameter.
  180. $fields = $query->query_vars['fields'];
  181. switch ( $fields ) {
  182. case 'count':
  183. $new_comments = count( $ep_query['documents'] );
  184. break;
  185. case 'ids':
  186. $new_comments = $this->format_hits_as_ids( $ep_query['documents'], $new_comments );
  187. break;
  188. default:
  189. $new_comments = $this->format_hits_as_comments( $ep_query['documents'], $new_comments, $query->query_vars );
  190. break;
  191. }
  192. return $new_comments;
  193. }
  194. /**
  195. * Format the ES hits/results as comments objects.
  196. *
  197. * @param array $comments The comments that should be formatted.
  198. * @param array $new_comments Array of comments from cache.
  199. * @param array $query_vars Query variables.
  200. * @since 3.6.0
  201. * @return array
  202. */
  203. protected function format_hits_as_comments( $comments, $new_comments, $query_vars ) {
  204. $hierarchical = isset( $query_vars['hierarchical'] ) ? $query_vars['hierarchical'] : false;
  205. foreach ( $comments as $comment_array ) {
  206. $comment = new \WP_Comment( (object) $comment_array );
  207. if ( ! empty( $comment_array['site_id'] ) ) {
  208. $comment->site_id = $comment_array['site_id'];
  209. } else {
  210. $comment->site_id = get_current_blog_id();
  211. }
  212. $comment->elasticsearch = true; // Super useful for debugging
  213. if ( $comment ) {
  214. $new_comments[] = $comment;
  215. }
  216. }
  217. if ( $hierarchical ) {
  218. $new_comments = $this->fill_descendants( $new_comments, $query_vars );
  219. }
  220. return $new_comments;
  221. }
  222. /**
  223. * Format the ES hits/results as an array of ids.
  224. *
  225. * @param array $comments The comments that should be formatted.
  226. * @param array $new_comments Array of comments from cache.
  227. * @since 3.6.0
  228. * @return array
  229. */
  230. protected function format_hits_as_ids( $comments, $new_comments ) {
  231. foreach ( $comments as $comment_array ) {
  232. $new_comments[] = $comment_array['comment_ID'];
  233. }
  234. return $new_comments;
  235. }
  236. /**
  237. * Fetch descendants for located comments.
  238. *
  239. * @param array $comments Array of top-level comments whose descendants should be filled in.
  240. * @param array $query_vars Current query vars.
  241. * @since 3.6.0
  242. * @return array
  243. */
  244. protected function fill_descendants( $comments, $query_vars ) {
  245. $levels = [
  246. 0 => $comments,
  247. ];
  248. // Fetch an entire level of the descendant tree at a time.
  249. $level = 0;
  250. $exclude_keys = [ 'parent', 'parent__in', 'parent__not_in' ];
  251. do {
  252. $child_comments = [];
  253. $_parent_ids = wp_list_pluck( $levels[ $level ], 'comment_ID' );
  254. if ( $_parent_ids ) {
  255. $parent_query_args = $query_vars;
  256. foreach ( $exclude_keys as $exclude_key ) {
  257. $parent_query_args[ $exclude_key ] = '';
  258. }
  259. $parent_query_args['parent__in'] = $_parent_ids;
  260. $parent_query_args['hierarchical'] = false;
  261. $parent_query_args['offset'] = 0;
  262. $parent_query_args['number'] = 0;
  263. $formatted_args = $this->indexable->format_args( $parent_query_args );
  264. $ep_query = $this->indexable->query_es( $formatted_args, $query_vars, $this->index );
  265. if ( false === $ep_query ) {
  266. $level_comments = [];
  267. } else {
  268. $level_comments = $this->format_hits_as_comments( $ep_query['documents'], [], [] );
  269. }
  270. foreach ( $level_comments as $level_comment ) {
  271. $child_comments[] = $level_comment;
  272. }
  273. }
  274. ++$level;
  275. $levels[ $level ] = $child_comments;
  276. } while ( $child_comments );
  277. // Pull out just the descendant comments
  278. $descendants = [];
  279. for ( $i = 1, $c = count( $levels ); $i < $c; $i++ ) {
  280. $descendants = array_merge( $descendants, $levels[ $i ] );
  281. }
  282. // Assemble a flat array of all comments + descendants.
  283. $all_comments = $comments;
  284. foreach ( $descendants as $descendant ) {
  285. $all_comments[] = $descendant;
  286. }
  287. // If a threaded representation was requested, build the tree.
  288. if ( 'threaded' === $query_vars['hierarchical'] ) {
  289. $threaded_comments = [];
  290. $ref = [];
  291. foreach ( $all_comments as $c ) {
  292. // If the comment isn't in the reference array, it goes in the top level of the thread.
  293. if ( ! isset( $ref[ $c->comment_parent ] ) ) {
  294. $threaded_comments[ $c->comment_ID ] = $c;
  295. $ref[ $c->comment_ID ] = $threaded_comments[ $c->comment_ID ];
  296. // Otherwise, set it as a child of its parent.
  297. } else {
  298. $ref[ $c->comment_parent ]->add_child( $c );
  299. $ref[ $c->comment_ID ] = $c;
  300. }
  301. }
  302. $comments = $threaded_comments;
  303. } else {
  304. $comments = $all_comments;
  305. }
  306. return $comments;
  307. }
  308. }