PluginProbe
WPGraphQL / 2.22.1
WPGraphQL v2.22.1
2.22.3 2.22.2 2.22.1 2.22.0 2.21.1 2.21.0 2.20.0 2.19.0 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.1 2.14.0 2.13.0 2.2.0 2.3.0 2.3.3 2.3.6 2.3.8 2.5.0 2.5.1 2.5.2 2.5.3 All 177 releases
wp-graphql / src / Data / Config.php

Config.php in WPGraphQL 2.22.1, at src/Data/Config.php

564 lines 18.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPGraphQL\Data;
4
5 use WPGraphQL\Data\Cursor\CommentObjectCursor;
6 use WPGraphQL\Data\Cursor\PostObjectCursor;
7 use WPGraphQL\Data\Cursor\TermObjectCursor;
8 use WPGraphQL\Data\Cursor\UserCursor;
9 use WP_Comment_Query;
10 use WP_Query;
11
12 /**
13 * Class Config
14 *
15 * This class contains configurations for various data-related things, such as query filters for
16 * cursor pagination.
17 *
18 * @package WPGraphQL\Data
19 */
20 class Config {
21
22 /**
23 * Config constructor.
24 */
25 public function __construct() {
26
27 /**
28 * Filter the term_clauses in the WP_Term_Query to allow for cursor pagination support where a Term ID
29 * can be used as a point of comparison when slicing the results to return.
30 */
31 add_filter(
32 'comments_clauses',
33 [
34 $this,
35 'graphql_wp_comments_query_cursor_pagination_support',
36 ],
37 10,
38 2
39 );
40
41 /**
42 * Filter the WP_Query to support cursor based pagination where a post ID can be used
43 * as a point of comparison when slicing the results to return.
44 */
45 add_filter( 'posts_where', [ $this, 'graphql_wp_query_cursor_pagination_support' ], 10, 2 );
46
47 /**
48 * Filter the search relevance ORDER BY expression so backward (last) pagination
49 * reads the tail of the result set, mirroring how the date ordering is inverted.
50 */
51 add_filter( 'posts_search_orderby', [ $this, 'graphql_wp_query_cursor_pagination_search_orderby' ], 10, 2 );
52
53 /**
54 * Filter the term_clauses in the WP_Term_Query to allow for cursor pagination support where a Term ID
55 * can be used as a point of comparison when slicing the results to return.
56 */
57 add_filter(
58 'terms_clauses',
59 [
60 $this,
61 'graphql_wp_term_query_cursor_pagination_support',
62 ],
63 10,
64 3
65 );
66
67 /**
68 * Filter WP_Query order by add some stability to meta query ordering
69 */
70 add_filter(
71 'posts_orderby',
72 [
73 $this,
74 'graphql_wp_query_cursor_pagination_stability',
75 ],
76 10,
77 2
78 );
79
80 if ( ! defined( 'ABSPATH' ) ) {
81 exit;
82 }
83
84 /**
85 * Copied from https://github.com/wp-graphql/wp-graphql/issues/274#issuecomment-510150571
86 * Shoutouts to epeli!
87 *
88 * Add missing filters to WP_User_Query class.
89 */
90 add_filter(
91 'pre_user_query',
92 static function ( $query ) {
93 if ( ! $query->get( 'suppress_filters' ) ) {
94 $query->set( 'suppress_filters', 0 );
95 }
96
97 if ( ! $query->get( 'suppress_filters' ) ) {
98
99 /**
100 * Filters the WHERE clause of the query.
101 *
102 * Specifically for manipulating paging queries.
103 **
104 *
105 * @param string $where The WHERE clause of the query.
106 * @param \WPGraphQL\Data\WP_User_Query $query The WP_User_Query instance (passed by reference).
107 */
108 $query->query_where = apply_filters_ref_array(
109 'graphql_users_where',
110 [
111 $query->query_where,
112 &$query,
113 ]
114 );
115
116 /**
117 * Filters the ORDER BY clause of the query.
118 *
119 * @param string $orderby The ORDER BY clause of the query.
120 * @param \WPGraphQL\Data\WP_User_Query $query The WP_User_Query instance (passed by reference).
121 */
122 $query->query_orderby = apply_filters_ref_array(
123 'graphql_users_orderby',
124 [
125 $query->query_orderby,
126 &$query,
127 ]
128 );
129 }
130
131 return $query;
132 }
133 );
134
135 /**
136 * Filter the WP_User_Query to support cursor based pagination where a user ID can be used
137 * as a point of comparison when slicing the results to return.
138 */
139 add_filter(
140 'graphql_users_where',
141 [
142 $this,
143 'graphql_wp_user_query_cursor_pagination_support',
144 ],
145 10,
146 2
147 );
148
149 /**
150 * Filter WP_User_Query order by add some stability to meta query ordering
151 */
152 add_filter(
153 'graphql_users_orderby',
154 [
155 $this,
156 'graphql_wp_user_query_cursor_pagination_stability',
157 ],
158 10,
159 2
160 );
161 }
162
163 /**
164 * When posts are ordered by fields that have duplicate values, we need to consider
165 * another field to "stabilize" the query order. We use IDs as they're always unique.
166 *
167 * This allows for posts with the same title or same date or same meta value to exist
168 * and for their cursors to properly go forward/backward to the proper place in the database.
169 *
170 * @param string $orderby The ORDER BY clause of the query.
171 * @param \WP_Query $query The WP_Query instance executing.
172 *
173 * @return string
174 */
175 public function graphql_wp_query_cursor_pagination_stability( string $orderby, WP_Query $query ) {
176 // Bail early if it's not a GraphQL Request.
177 if ( true !== is_graphql_request() ) {
178 return $orderby;
179 }
180
181 /**
182 * If pre-filter hooked, return $pre_orderby.
183 *
184 * @param string|null $pre_orderby The pre-filtered ORDER BY clause of the query.
185 * @param string $orderby The ORDER BY clause of the query.
186 * @param \WP_Query $query The WP_Query instance (passed by reference).
187 *
188 * @return string|null
189 * @hookGroup connections
190 * @since 0.0.5
191 */
192 $pre_orderby = apply_filters( 'graphql_pre_wp_query_cursor_pagination_stability', null, $orderby, $query );
193 if ( null !== $pre_orderby ) {
194 return $pre_orderby;
195 }
196
197 // Bail early if disabled by connection.
198 if ( isset( $query->query_vars['graphql_apply_cursor_pagination_orderby'] )
199 && false === $query->query_vars['graphql_apply_cursor_pagination_orderby'] ) {
200 return $orderby;
201 }
202
203 // Bail early if the cursor "graphql_cursor_compare" arg is not in the query,
204 if ( ! isset( $query->query_vars['graphql_cursor_compare'] ) ) {
205 return $orderby;
206 }
207
208 // Check the cursor compare order
209 $order = '>' === $query->query_vars['graphql_cursor_compare'] ? 'ASC' : 'DESC';
210
211 // Get Cursor ID key.
212 $cursor = new PostObjectCursor( $query->query_vars );
213 $key = $cursor->get_cursor_id_key();
214
215 // If there is a cursor compare in the arguments, use it as the stablizer for cursors.
216 return ( $orderby ? "{$orderby}, " : '' ) . "{$key} {$order}";
217 }
218
219 /**
220 * When a searched query is paginated backward (`last`), the rest of the ordering is
221 * inverted (e.g. post_date ASC instead of DESC), but WP_Query never inverts the search
222 * relevance expression it prepends via parse_search_order(). Invert it here so the SQL
223 * window reads the tail of the relevance-ordered result set.
224 *
225 * The multi-term expression is a CASE that sorts ascending by default; the single-term
226 * expression is a boolean LIKE sorted DESC. Inverting means appending DESC to the former
227 * and swapping DESC for ASC on the latter.
228 *
229 * @since 2.17.0
230 *
231 * @param string $search_orderby The ORDER BY clause for search relevance.
232 * @param \WP_Query $query The WP_Query instance (passed by reference).
233 *
234 * @return string
235 */
236 public function graphql_wp_query_cursor_pagination_search_orderby( string $search_orderby, WP_Query $query ) {
237 // Bail early if it's not a GraphQL Request.
238 if ( true !== is_graphql_request() ) {
239 return $search_orderby;
240 }
241
242 // Bail early if there is nothing to invert.
243 if ( empty( $search_orderby ) ) {
244 return $search_orderby;
245 }
246
247 // Bail early if disabled by connection.
248 if ( isset( $query->query_vars['graphql_apply_cursor_pagination_orderby'] )
249 && false === $query->query_vars['graphql_apply_cursor_pagination_orderby'] ) {
250 return $search_orderby;
251 }
252
253 // Only invert for backward (`last`) pagination.
254 if ( ! isset( $query->query_vars['graphql_cursor_compare'] ) || '>' !== $query->query_vars['graphql_cursor_compare'] ) {
255 return $search_orderby;
256 }
257
258 if ( ' DESC' === substr( $search_orderby, -5 ) ) {
259 return substr( $search_orderby, 0, -5 ) . ' ASC';
260 }
261
262 return $search_orderby . ' DESC';
263 }
264
265 /**
266 * This filters the WPQuery 'where' $args, enforcing the query to return results before or
267 * after the referenced cursor
268 *
269 * @param string $where The WHERE clause of the query.
270 * @param \WP_Query $query The WP_Query instance (passed by reference).
271 *
272 * @return string
273 */
274 public function graphql_wp_query_cursor_pagination_support( string $where, WP_Query $query ) {
275 // Bail early if it's not a GraphQL Request.
276 if ( true !== is_graphql_request() ) {
277 return $where;
278 }
279
280 /**
281 * If pre-filter hooked, return $pre_where.
282 *
283 * @param string|null $pre_where The pre-filtered WHERE clause of the query.
284 * @param string $where The WHERE clause of the query.
285 * @param \WP_Query $query The WP_Query instance (passed by reference).
286 *
287 * @return string|null
288 * @hookGroup connections
289 * @since 0.0.5
290 */
291 $pre_where = apply_filters( 'graphql_pre_wp_query_cursor_pagination_support', null, $where, $query );
292 if ( null !== $pre_where ) {
293 return $pre_where;
294 }
295
296 // Bail early if disabled by connection.
297 if ( isset( $query->query_vars['graphql_apply_cursor_pagination_where'] )
298 && false === $query->query_vars['graphql_apply_cursor_pagination_where'] ) {
299 return $where;
300 }
301
302 // Apply the after cursor, moving forward through results
303 if ( ! empty( $query->query_vars['graphql_after_cursor'] ) ) {
304 $after_cursor = new PostObjectCursor( $query->query_vars, 'after' );
305 $where .= $after_cursor->get_where();
306 }
307
308 // Apply the after cursor, moving backward through results.
309 if ( ! empty( $query->query_vars['graphql_before_cursor'] ) ) {
310 $before_cursor = new PostObjectCursor( $query->query_vars, 'before' );
311 $where .= $before_cursor->get_where();
312 }
313
314 return $where;
315 }
316
317 /**
318 * When users are ordered by a meta query the order might be random when
319 * the meta values have same values multiple times. This filter adds a
320 * secondary ordering by the post ID which forces stable order in such cases.
321 *
322 * @param string $orderby The ORDER BY clause of the query.
323 * @param \WP_User_Query $query The WP_User_Query instance (passed by reference).
324 *
325 * @return string
326 */
327 public function graphql_wp_user_query_cursor_pagination_stability( $orderby, \WP_User_Query $query ) {
328
329 // Bail early if it's not a GraphQL Request.
330 if ( true !== is_graphql_request() ) {
331 return $orderby;
332 }
333
334 /**
335 * If pre-filter hooked, return $pre_orderby.
336 *
337 * @param string|null $pre_orderby The pre-filtered ORDER BY clause of the query.
338 * @param string $orderby The ORDER BY clause of the query.
339 * @param \WP_User_Query $query The WP_User_Query instance (passed by reference).
340 *
341 * @return string|null
342 * @hookGroup connections
343 * @since 0.0.5
344 */
345 $pre_orderby = apply_filters( 'graphql_pre_wp_user_query_cursor_pagination_stability', null, $orderby, $query );
346 if ( null !== $pre_orderby ) {
347 return $pre_orderby;
348 }
349
350 // Bail early if disabled by connection.
351 if ( isset( $query->query_vars['graphql_apply_cursor_pagination_orderby'] )
352 && false === $query->query_vars['graphql_apply_cursor_pagination_orderby'] ) {
353 return $orderby;
354 }
355
356 // Bail early if the cursor "graphql_cursor_compare" arg is not in the query,
357 if ( ! isset( $query->query_vars['graphql_cursor_compare'] ) ) {
358 return $orderby;
359 }
360
361 // Check the cursor compare order
362 $order = '>' === $query->query_vars['graphql_cursor_compare'] ? 'ASC' : 'DESC';
363
364 // Get Cursor ID key.
365 $cursor = new UserCursor( $query->query_vars );
366 $key = $cursor->get_cursor_id_key();
367
368 return ( $orderby ? "{$orderby}, " : '' ) . "{$key} {$order}";
369 }
370
371 /**
372 * This filters the WP_User_Query 'where' $args, enforcing the query to return results before or
373 * after the referenced cursor
374 *
375 * @param string $where The WHERE clause of the query.
376 * @param \WP_User_Query $query The WP_User_Query instance (passed by reference).
377 *
378 * @return string
379 */
380 public function graphql_wp_user_query_cursor_pagination_support( $where, \WP_User_Query $query ) {
381
382 // Bail early if it's not a GraphQL Request.
383 if ( true !== is_graphql_request() ) {
384 return $where;
385 }
386
387 /**
388 * If pre-filter hooked, return $pre_where.
389 *
390 * @param string|null $pre_where The pre-filtered WHERE clause of the query.
391 * @param string $where The WHERE clause of the query.
392 * @param \WP_User_Query $query The WP_Query instance (passed by reference).
393 *
394 * @return string|null
395 * @hookGroup connections
396 * @since 0.0.5
397 */
398 $pre_where = apply_filters( 'graphql_pre_wp_user_query_cursor_pagination_support', null, $where, $query );
399 if ( null !== $pre_where ) {
400 return $pre_where;
401 }
402
403 // Bail early if disabled by connection.
404 if ( isset( $query->query_vars['graphql_apply_cursor_pagination_where'] )
405 && false === $query->query_vars['graphql_apply_cursor_pagination_where'] ) {
406 return $where;
407 }
408
409 // Apply the after cursor.
410 if ( ! empty( $query->query_vars['graphql_after_cursor'] ) ) {
411 $after_cursor = new UserCursor( $query->query_vars, 'after' );
412 $where = $where . $after_cursor->get_where();
413 }
414
415 // Apply the after cursor.
416 if ( ! empty( $query->query_vars['graphql_before_cursor'] ) ) {
417 $before_cursor = new UserCursor( $query->query_vars, 'before' );
418 $where = $where . $before_cursor->get_where();
419 }
420
421 return $where;
422 }
423
424 /**
425 * This filters the term_clauses in the WP_Term_Query to support cursor based pagination, where
426 * we can move forward or backward from a particular record, instead of typical offset
427 * pagination which can be much more expensive and less accurate.
428 *
429 * @param array<string,mixed> $pieces Terms query SQL clauses.
430 * @param string[] $taxonomies An array of taxonomies.
431 * @param array<string,mixed> $args An array of terms query arguments.
432 *
433 * @return array<string,mixed> $pieces
434 */
435 public function graphql_wp_term_query_cursor_pagination_support( array $pieces, array $taxonomies, array $args ) {
436
437 // Bail early if it's not a GraphQL Request.
438 if ( true !== is_graphql_request() ) {
439 return $pieces;
440 }
441
442 /**
443 * If pre-filter hooked, return $pre_pieces.
444 *
445 * @param ?array<string,mixed> $pre_pieces The pre-filtered term query SQL clauses.
446 * @param array<string,mixed> $pieces Terms query SQL clauses.
447 * @param string[] $taxonomies An array of taxonomies.
448 * @param array<string,mixed> $args An array of terms query arguments.
449 *
450 * @hookGroup connections
451 * @since 0.0.5
452 */
453 $pre_pieces = apply_filters( 'graphql_pre_wp_term_query_cursor_pagination_support', null, $pieces, $taxonomies, $args );
454 if ( null !== $pre_pieces ) {
455 return $pre_pieces;
456 }
457
458 // Bail early if disabled by connection.
459 if ( isset( $args['graphql_apply_cursor_pagination_where'] )
460 && false === $args['graphql_apply_cursor_pagination_where'] ) {
461 return $pieces;
462 }
463
464 // Bail early if the cursor "graphql_cursor_compare" arg is not in the query,
465 if ( ! isset( $args['graphql_cursor_compare'] ) ) {
466 return $pieces;
467 }
468
469 // Determine the limit for the query.
470 //
471 // WP_Term_Query intentionally omits the SQL LIMIT when it has to descend the
472 // term hierarchy (e.g. a `child_of` query): it queries the full set, filters
473 // the descendants in PHP, then applies `number`/`offset` in PHP. Forcing a
474 // SQL LIMIT here would truncate the result set before that descendant
475 // filtering runs, dropping children that fall outside the LIMIT window. In
476 // that case we defer to WP's PHP-side pagination. See #2739.
477 if ( empty( $args['child_of'] ) && isset( $args['number'] ) && absint( $args['number'] ) ) {
478 $pieces['limits'] = sprintf( ' LIMIT 0, %d', absint( $args['number'] ) );
479 }
480
481 // Apply the after cursor.
482 if ( ! empty( $args['graphql_after_cursor'] ) ) {
483 $after_cursor = new TermObjectCursor( $args, 'after' );
484 $pieces['where'] = $pieces['where'] . $after_cursor->get_where();
485 }
486
487 // Apply the before cursor.
488 if ( ! empty( $args['graphql_before_cursor'] ) ) {
489 $before_cursor = new TermObjectCursor( $args, 'before' );
490 $pieces['where'] = $pieces['where'] . $before_cursor->get_where();
491 }
492
493 // Check the cursor compare order.
494 $order = '>' === $args['graphql_cursor_compare'] ? 'ASC' : 'DESC';
495
496 // Get Cursor ID key.
497 $cursor = new TermObjectCursor( $args );
498 $key = $cursor->get_cursor_id_key();
499
500 // If there is a cursor compare in the arguments, use it as the stabilizer for cursors.
501 if ( ! empty( $pieces['orderby'] ) ) {
502 $pieces['orderby'] = "{$pieces['orderby']} {$pieces['order']}, {$key} {$order}";
503 } else {
504 $pieces['orderby'] = "ORDER BY {$key} {$order}";
505 }
506
507 $pieces['order'] = '';
508
509 return $pieces;
510 }
511
512 /**
513 * This returns a modified version of the $pieces of the comment query clauses if the request
514 * is a GraphQL Request and before or after cursors are passed to the query
515 *
516 * @param array<string,mixed> $pieces A compacted array of comment query clauses.
517 * @param \WP_Comment_Query $query Current instance of WP_Comment_Query, passed by reference.
518 *
519 * @return array<string,mixed> $pieces
520 */
521 public function graphql_wp_comments_query_cursor_pagination_support( array $pieces, WP_Comment_Query $query ) {
522
523 // Bail early if it's not a GraphQL Request.
524 if ( true !== is_graphql_request() ) {
525 return $pieces;
526 }
527
528 /**
529 * If pre-filter hooked, return $pre_pieces.
530 *
531 * @param ?array<string,mixed> $pre_pieces The pre-filtered comment query clauses.
532 * @param array<string,mixed> $pieces A compacted array of comment query clauses.
533 * @param \WP_Comment_Query $query Current instance of WP_Comment_Query, passed by reference.
534 *
535 * @hookGroup connections
536 * @since 0.0.5
537 */
538 $pre_pieces = apply_filters( 'graphql_pre_wp_comments_query_cursor_pagination_support', null, $pieces, $query );
539 if ( null !== $pre_pieces ) {
540 return $pre_pieces;
541 }
542
543 // Bail early if disabled by connection.
544 if ( isset( $query->query_vars['graphql_apply_cursor_pagination_where'] )
545 && false === $query->query_vars['graphql_apply_cursor_pagination_where'] ) {
546 return $pieces;
547 }
548
549 // Apply the after cursor, moving forward through results.
550 if ( ! empty( $query->query_vars['graphql_after_cursor'] ) ) {
551 $after_cursor = new CommentObjectCursor( $query->query_vars, 'after' );
552 $pieces['where'] .= $after_cursor->get_where();
553 }
554
555 // Apply the after cursor, moving backward through results.
556 if ( ! empty( $query->query_vars['graphql_before_cursor'] ) ) {
557 $before_cursor = new CommentObjectCursor( $query->query_vars, 'before' );
558 $pieces['where'] .= $before_cursor->get_where();
559 }
560
561 return $pieces;
562 }
563 }
564