• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wp-graphql / wp-graphql / 17562196906

08 Sep 2025 07:44PM UTC coverage: 84.575% (+0.4%) from 84.17%
17562196906

push

github

web-flow
Merge pull request #3389 from wp-graphql/develop

release: next version 📦

238 of 308 new or added lines in 13 files covered. (77.27%)

6 existing lines in 6 files now uncovered.

15884 of 18781 relevant lines covered (84.57%)

261.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.0
/src/Data/Cursor/PostObjectCursor.php
1
<?php
2

3
namespace WPGraphQL\Data\Cursor;
4

5
/**
6
 * Post Cursor
7
 *
8
 * This class generates the SQL AND operators for cursor based pagination for posts
9
 *
10
 * @package WPGraphQL\Data\Cursor
11
 */
12
class PostObjectCursor extends AbstractCursor {
13
        /**
14
         * @var ?\WP_Post
15
         */
16
        public $cursor_node;
17

18
        /**
19
         * Counter for meta value joins
20
         *
21
         * @var int
22
         */
23
        public $meta_join_alias = 0;
24

25
        /**
26
         * {@inheritDoc}
27
         */
28
        public function __construct( $query_vars, $cursor = 'after' ) {
193✔
29
                // @todo remove in 3.0.0
30
                if ( $query_vars instanceof \WP_Query ) {
193✔
NEW
31
                        _doing_it_wrong(
×
NEW
32
                                __METHOD__,
×
NEW
33
                                esc_html__( 'The first argument should be an array of $query_vars, not the WP_Query object. This will throw an error in the next major release', 'wp-graphql' ),
×
NEW
34
                                '1.9.0'
×
NEW
35
                        );
×
UNCOV
36
                        $query_vars = $query_vars->query_vars;
×
37
                }
38

39
                // Initialize the class properties.
40
                parent::__construct( $query_vars, $cursor );
193✔
41

42
                // Set ID key.
43
                $this->id_key = "{$this->wpdb->posts}.ID";
193✔
44
        }
45

46
        /**
47
         * {@inheritDoc}
48
         *
49
         * @return ?\WP_Post
50
         */
51
        public function get_cursor_node() {
193✔
52
                // Bail if no offset.
53
                if ( ! $this->cursor_offset ) {
193✔
54
                        return null;
189✔
55
                }
56

57
                /**
58
                 * If pre-hooked, return filtered node.
59
                 *
60
                 * @param \WP_Post|null                           $pre_post The pre-filtered post node.
61
                 * @param int                                     $offset   The cursor offset.
62
                 * @param \WPGraphQL\Data\Cursor\PostObjectCursor $node     The cursor instance.
63
                 *
64
                 * @return \WP_Post|null
65
                 */
66
                $pre_post = apply_filters( 'graphql_pre_post_cursor_node', null, $this->cursor_offset, $this );
38✔
67
                if ( null !== $pre_post ) {
38✔
68
                        return $pre_post;
×
69
                }
70

71
                // Get cursor node.
72
                $post = \WP_Post::get_instance( $this->cursor_offset );
38✔
73

74
                return false !== $post ? $post : null;
38✔
75
        }
76

77
        /**
78
         * {@inheritDoc}
79
         */
80
        public function to_sql() {
38✔
81
                $orderby = isset( $this->query_vars['orderby'] ) ? $this->query_vars['orderby'] : null;
38✔
82

83
                $orderby_should_not_convert_to_sql = isset( $orderby ) && in_array(
38✔
84
                        $orderby,
38✔
85
                        [
38✔
86
                                'post__in',
38✔
87
                                'post_name__in',
38✔
88
                                'post_parent__in',
38✔
89
                        ],
38✔
90
                        true
38✔
91
                );
38✔
92

93
                if ( true === $orderby_should_not_convert_to_sql ) {
38✔
94
                        return '';
4✔
95
                }
96

97
                $sql = $this->builder->to_sql();
34✔
98

99
                if ( empty( $sql ) ) {
34✔
100
                        return '';
×
101
                }
102

103
                return ' AND ' . $sql;
34✔
104
        }
105

106
        /**
107
         * {@inheritDoc}
108
         */
109
        public function get_where() {
38✔
110
                // If we have a bad cursor, just skip.
111
                if ( ! $this->is_valid_offset_and_node() ) {
38✔
112
                        return '';
×
113
                }
114

115
                $orderby = $this->get_query_var( 'orderby' );
38✔
116
                $order   = $this->get_query_var( 'order' );
38✔
117

118
                if ( 'menu_order' === $orderby ) {
38✔
119
                        if ( '>' === $this->compare ) {
3✔
120
                                $order         = 'DESC';
2✔
121
                                $this->compare = '<';
2✔
122
                        } elseif ( '<' === $this->compare ) {
2✔
123
                                $this->compare = '>';
2✔
124
                                $order         = 'ASC';
2✔
125
                        }
126
                }
127

128
                if ( ! empty( $orderby ) && is_array( $orderby ) ) {
38✔
129

130
                        /**
131
                         * Loop through all order keys if it is an array
132
                         */
133
                        foreach ( $orderby as $by => $order ) {
13✔
134
                                $this->compare_with( $by, $order );
13✔
135
                        }
136
                } elseif ( ! empty( $orderby ) && is_string( $orderby ) ) {
25✔
137

138
                        /**
139
                         * If $orderby is just a string just compare with it directly as DESC
140
                         */
141
                        $this->compare_with( $orderby, $order );
17✔
142
                }
143

144
                /**
145
                 * If there's no orderby specified yet, compare with the following fields.
146
                 */
147
                if ( ! $this->builder->has_fields() ) {
38✔
148
                        $this->compare_with_cursor_fields(
13✔
149
                                [
13✔
150
                                        [
13✔
151
                                                'key'   => "{$this->wpdb->posts}.post_date",
13✔
152
                                                'value' => $this->cursor_node ? $this->cursor_node->post_date : null,
13✔
153
                                                'type'  => 'DATETIME',
13✔
154
                                        ],
13✔
155
                                ]
13✔
156
                        );
13✔
157
                }
158

159
                $this->compare_with_id_field();
38✔
160

161
                return $this->to_sql();
38✔
162
        }
163

164
        /**
165
         * Get AND operator for given order by key
166
         *
167
         * @param string $by    The order by key
168
         * @param string $order The order direction ASC or DESC
169
         */
170
        private function compare_with( $by, $order ): void {
30✔
171
                // Bail early, if "key" and "value" provided in query_vars.
172
                $key   = $this->get_query_var( "graphql_cursor_compare_by_{$by}_key" );
30✔
173
                $value = $this->get_query_var( "graphql_cursor_compare_by_{$by}_value" );
30✔
174
                if ( ! empty( $key ) && ! empty( $value ) ) {
30✔
175
                        $this->builder->add_field( $key, $value, null, $order );
×
176
                        return;
×
177
                }
178

179
                /**
180
                 * Find out whether this is a post field
181
                 */
182
                $orderby_post_fields = [
30✔
183
                        'post_author',
30✔
184
                        'post_title',
30✔
185
                        'post_type',
30✔
186
                        'post_name',
30✔
187
                        'post_modified',
30✔
188
                        'post_date',
30✔
189
                        'post_parent',
30✔
190
                        'menu_order',
30✔
191
                ];
30✔
192
                if ( in_array( $by, $orderby_post_fields, true ) ) {
30✔
193
                        $key   = "{$this->wpdb->posts}.{$by}";
15✔
194
                        $value = $this->cursor_node->{$by} ?? null;
15✔
195
                }
196

197
                /**
198
                 * If key or value are null, check whether this is a meta key based ordering before bailing.
199
                 */
200
                if ( null === $key || null === $value ) {
30✔
201
                        $meta_key = $this->get_meta_key( $by );
16✔
202
                        if ( $meta_key ) {
16✔
203
                                $this->compare_with_meta_field( $meta_key, $order );
11✔
204
                        }
205
                        return;
16✔
206
                }
207

208
                // Add field to build.
209
                $this->builder->add_field( $key, $value, null, $order );
15✔
210
        }
211

212
        /**
213
         * Compare with meta key field
214
         *
215
         * @param string $meta_key post meta key
216
         * @param string $order    The comparison string
217
         */
218
        private function compare_with_meta_field( string $meta_key, string $order ): void {
11✔
219
                $meta_type  = $this->get_query_var( 'meta_type' );
11✔
220
                $meta_value = get_post_meta( $this->cursor_offset, $meta_key, true );
11✔
221

222
                $key = "{$this->wpdb->postmeta}.meta_value";
11✔
223

224
                /**
225
                 * WP uses mt1, mt2 etc. style aliases for additional meta value joins.
226
                 */
227
                $meta_query = $this->get_query_var( 'meta_query' );
11✔
228
                if ( ! empty( $meta_query ) && is_array( $meta_query ) ) {
11✔
229
                        if ( ! empty( $meta_query['relation'] ) ) {
4✔
230
                                unset( $meta_query['relation'] );
1✔
231
                        }
232

233
                        $meta_keys = array_column( $meta_query, 'key' );
4✔
234
                        $index     = array_search( $meta_key, $meta_keys, true );
4✔
235

236
                        if ( $index && 1 < count( $meta_query ) ) {
4✔
237
                                $key = "mt{$index}.meta_value";
1✔
238
                        }
239
                }
240

241
                /**
242
                 * Allow filtering the meta key used for cursor based pagination
243
                 *
244
                 * @param string $key       The meta key to use for cursor based pagination
245
                 * @param string $meta_key  The original meta key
246
                 * @param string $meta_type The meta type
247
                 * @param string $order     The order direction
248
                 * @param object $cursor    The PostObjectCursor instance
249
                 */
250
                $key = apply_filters( 'graphql_post_object_cursor_meta_key', $key, $meta_key, $meta_type, $order, $this );
11✔
251

252
                $this->builder->add_field( $key, $meta_value, $meta_type, $order, $this );
11✔
253
        }
254

255
        /**
256
         * Get the actual meta key if any
257
         *
258
         * @param string $by The order by key
259
         *
260
         * @return string|null
261
         */
262
        private function get_meta_key( $by ) {
16✔
263
                if ( 'meta_value' === $by || 'meta_value_num' === $by ) {
16✔
264
                        return $this->get_query_var( 'meta_key' );
8✔
265
                }
266

267
                /**
268
                 * Check for the WP 4.2+ style meta clauses
269
                 * https://make.wordpress.org/core/2015/03/30/query-improvements-in-wp-4-2-orderby-and-meta_query/
270
                 */
271
                if ( ! isset( $this->query_vars['meta_query'][ $by ] ) ) {
8✔
272
                        return null;
5✔
273
                }
274

275
                $clause = $this->query_vars['meta_query'][ $by ];
3✔
276

277
                return empty( $clause['key'] ) ? null : $clause['key'];
3✔
278
        }
279

280
        /**
281
         * @todo Remove in 3.0.0
282
         * @deprecated 1.9.0
283
         * @codeCoverageIgnore
284
         *
285
         * @return ?\WP_Post
286
         */
287
        public function get_cursor_post() {
288
                _doing_it_wrong(
289
                        __METHOD__,
290
                        sprintf(
291
                                // translators: %s is the method name
292
                                esc_html__( 'This method will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
293
                                self::class . '::get_cursor_node()'
294
                        ),
295
                        '1.9.0'
296
                );
297

298
                return $this->cursor_node;
299
        }
300
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc