• 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

50.0
/src/Data/Cursor/AbstractCursor.php
1
<?php
2

3
namespace WPGraphQL\Data\Cursor;
4

5
use GraphQL\Error\InvariantViolation;
6

7
/**
8
 * Abstract Cursor
9
 *
10
 * @package WPGraphQL\Data\Loader
11
 * @since 1.9.0
12
 */
13
abstract class AbstractCursor {
14

15
        /**
16
         * The global WordPress Database instance
17
         *
18
         * @var \wpdb $wpdb
19
         */
20
        public $wpdb;
21

22
        /**
23
         * @var \WPGraphQL\Data\Cursor\CursorBuilder
24
         */
25
        public $builder;
26

27
        /**
28
         * @var string
29
         */
30
        public $compare;
31

32
        /**
33
         * Our current cursor offset.
34
         * For example, the term, post, user, or comment ID.
35
         *
36
         * @var int
37
         */
38
        public $cursor_offset;
39

40
        /**
41
         * @var string|null
42
         */
43
        public $cursor;
44

45
        /**
46
         * The WP object instance for the cursor.
47
         *
48
         * @var mixed
49
         */
50
        public $cursor_node;
51

52
        /**
53
         * Copy of query vars so we can modify them safely
54
         *
55
         * @var array<string,mixed>
56
         */
57
        public $query_vars = [];
58

59
        /**
60
         * Stores SQL statement alias for the ID column applied to the cutoff
61
         *
62
         * @var string
63
         */
64
        protected $id_key = '';
65

66
        /**
67
         * The constructor
68
         *
69
         * @param array<string,mixed> $query_vars         Query variable for the query to be executed.
70
         * @param string|null         $cursor             Cursor type. Either 'after' or 'before'.
71
         */
72
        public function __construct( $query_vars, $cursor = 'after' ) {
297✔
73
                global $wpdb;
297✔
74

75
                $this->wpdb       = $wpdb;
297✔
76
                $this->query_vars = $query_vars;
297✔
77
                $this->cursor     = $cursor;
297✔
78

79
                /**
80
                 * Get the cursor offset if any
81
                 */
82
                $offset = $this->get_query_var( 'graphql_' . $cursor . '_cursor' );
297✔
83

84
                // Handle deprecated use of `graphql_cursor_offset`. @todo remove in 3.0.0
85
                if ( empty( $offset ) ) {
297✔
86
                        $offset = $this->get_query_var( 'graphql_cursor_offset' );
290✔
87

88
                        if ( ! empty( $offset ) ) {
290✔
NEW
89
                                _doing_it_wrong(
×
NEW
90
                                        self::class . "::get_query_var('graphql_cursor_offset')",
×
NEW
91
                                        esc_html__( "Use 'graphql_before_cursor' or 'graphql_after_cursor' instead. This will be removed in the next major release", 'wp-graphql' ),
×
NEW
92
                                        '1.9.0'
×
NEW
93
                                );
×
94
                        }
95
                }
96

97
                $this->cursor_offset = ! empty( $offset ) ? absint( $offset ) : 0;
297✔
98

99
                // Get the WP Object for the cursor.
100
                $this->cursor_node = $this->get_cursor_node();
297✔
101

102
                // Get the direction for the builder query.
103
                $this->compare = $this->get_cursor_compare();
297✔
104

105
                $this->builder = new CursorBuilder( $this->compare );
297✔
106
        }
107

108
        /**
109
         * Get the query variable for the provided name.
110
         *
111
         * @param string $name .
112
         *
113
         * @return mixed|null
114
         */
115
        public function get_query_var( string $name ) {
297✔
116
                if ( isset( $this->query_vars[ $name ] ) && '' !== $this->query_vars[ $name ] ) {
297✔
117
                        return $this->query_vars[ $name ];
80✔
118
                }
119
                return null;
297✔
120
        }
121

122
        /**
123
         * Get the direction pagination is going in.
124
         *
125
         * @return string
126
         */
127
        public function get_cursor_compare() {
241✔
128
                if ( 'before' === $this->cursor ) {
241✔
129
                        return '>';
21✔
130
                }
131

132
                return '<';
240✔
133
        }
134

135
        /**
136
         * Ensure the cursor_offset is a positive integer and we have a valid object for our cursor node.
137
         *
138
         * @return bool
139
         */
140
        protected function is_valid_offset_and_node() {
80✔
141
                if (
142
                        ! is_int( $this->cursor_offset ) ||
80✔
143
                        0 >= $this->cursor_offset ||
80✔
144
                        ! $this->cursor_node
80✔
145
                ) {
146
                        return false;
×
147
                }
148

149
                return true;
80✔
150
        }
151

152
        /**
153
         * Validates cursor compare field configuration. Validation failure results in a fatal
154
         * error because query execution is guaranteed to fail.
155
         *
156
         * @param array<string,mixed> $field Threshold configuration.
157
         *
158
         * @throws \GraphQL\Error\InvariantViolation Invalid configuration format.
159
         */
160
        protected function validate_cursor_compare_field( $field ): void {
34✔
161
                // Throw if an array not provided.
162
                if ( ! is_array( $field ) ) {
34✔
163
                        throw new InvariantViolation(
×
164
                                esc_html(
×
165
                                        sprintf(
×
166
                                                /* translators: %1$s: Cursor class name. %2$s: value type. */
167
                                                __( 'Invalid value provided for %1$s cursor compare field. Expected Array, %2$s given.', 'wp-graphql' ),
×
168
                                                static::class,
×
169
                                                gettype( $field )
×
170
                                        )
×
171
                                )
×
172
                        );
×
173
                }
174

175
                // Guard against missing or invalid "table column".
176
                if ( empty( $field['key'] ) || ! is_string( $field['key'] ) ) {
34✔
177
                        throw new InvariantViolation(
×
178
                                esc_html(
×
179
                                        sprintf(
×
180
                                                /* translators: %s: Cursor class name. */
181
                                                __( 'Expected "key" value to be provided for %s cursor compare field. A string value must be given.', 'wp-graphql' ),
×
182
                                                static::class
×
183
                                        )
×
184
                                )
×
185
                        );
×
186
                }
187

188
                // Guard against missing or invalid "by".
189
                if ( ! isset( $field['value'] ) ) {
34✔
190
                        throw new InvariantViolation(
×
191
                                esc_html(
×
192
                                        sprintf(
×
193
                                                /* translators: %s: Cursor class name. */
194
                                                __( 'Expected "value" value to be provided for %s cursor compare field. A scalar value must be given.', 'wp-graphql' ),
×
195
                                                static::class
×
196
                                        )
×
197
                                )
×
198
                        );
×
199
                }
200

201
                // Guard against invalid "type".
202
                if ( ! empty( $field['type'] ) && ! is_string( $field['type'] ) ) {
34✔
203
                        throw new InvariantViolation(
×
204
                                esc_html(
×
205
                                        sprintf(
×
206
                                        /* translators: %s: Cursor class name. */
207
                                                __( 'Invalid value provided for "type" value to be provided for type of %s cursor compare field. A string value must be given.', 'wp-graphql' ),
×
208
                                                static::class
×
209
                                        )
×
210
                                )
×
211
                        );
×
212
                }
213

214
                // Guard against invalid "order".
215
                if ( ! empty( $field['order'] ) && ! in_array( strtoupper( $field['order'] ), [ 'ASC', 'DESC' ], true ) ) {
34✔
216
                        throw new InvariantViolation(
×
217
                                esc_html(
×
218
                                        sprintf(
×
219
                                        /* translators: %s: Cursor class name. */
220
                                                __( 'Invalid value provided for "order" value to be provided for type of %s cursor compare field. Either "ASC" or "DESC" must be given.', 'wp-graphql' ),
×
221
                                                static::class
×
222
                                        )
×
223
                                )
×
224
                        );
×
225
                }
226
        }
227

228
        /**
229
         * Returns the ID key.
230
         *
231
         * @return string
232
         */
233
        public function get_cursor_id_key() {
297✔
234
                $key = $this->get_query_var( 'graphql_cursor_id_key' );
297✔
235
                if ( null === $key ) {
297✔
236
                        $key = $this->id_key;
297✔
237
                }
238

239
                return $key;
297✔
240
        }
241

242
        /**
243
         * Applies cursor compare fields to the cursor cutoff.
244
         *
245
         * @param array<string,mixed>[] $fallback Fallback cursor compare fields.
246
         *
247
         * @throws \GraphQL\Error\InvariantViolation Invalid configuration format.
248
         */
249
        protected function compare_with_cursor_fields( $fallback = [] ): void {
34✔
250
                /**
251
                 * Get cursor compare fields from query vars.
252
                 *
253
                 * @var array<string,mixed>[]|null $cursor_compare_fields
254
                 */
255
                $cursor_compare_fields = $this->get_query_var( 'graphql_cursor_compare_fields' );
34✔
256
                if ( null === $cursor_compare_fields ) {
34✔
257
                        $cursor_compare_fields = $fallback;
34✔
258
                }
259
                // Bail early if no cursor compare fields.
260
                if ( empty( $cursor_compare_fields ) ) {
34✔
261
                        return;
×
262
                }
263

264
                if ( ! is_array( $cursor_compare_fields ) ) {
34✔
265
                        throw new InvariantViolation(
×
266
                                esc_html(
×
267
                                        sprintf(
×
268
                                                /* translators: %s: value type. */
269
                                                __( 'Invalid value provided for graphql_cursor_compare_fields. Expected Array, %s given.', 'wp-graphql' ),
×
270
                                                gettype( $cursor_compare_fields )
×
271
                                        )
×
272
                                )
×
273
                        );
×
274
                }
275

276
                // Check if only one cursor compare field provided, wrap it in an array.
277
                if ( ! isset( $cursor_compare_fields[0] ) ) {
34✔
278
                        $cursor_compare_fields = [ $cursor_compare_fields ];
×
279
                }
280

281
                foreach ( $cursor_compare_fields as $field ) {
34✔
282
                        $this->validate_cursor_compare_field( $field );
34✔
283

284
                        $key   = $field['key'];
34✔
285
                        $value = $field['value'];
34✔
286
                        $type  = ! empty( $field['type'] ) ? $field['type'] : null;
34✔
287
                        $order = ! empty( $field['order'] ) ? $field['order'] : null;
34✔
288

289
                        $this->builder->add_field( $key, $value, $type, $order );
34✔
290
                }
291
        }
292

293
        /**
294
         * Applies ID field to the cursor builder.
295
         */
296
        protected function compare_with_id_field(): void {
80✔
297
                // Get ID value.
298
                $value = $this->get_query_var( 'graphql_cursor_id_value' );
80✔
299
                if ( null === $value ) {
80✔
300
                        $value = (string) $this->cursor_offset;
80✔
301
                }
302

303
                // Get ID SQL Query alias.
304
                $key = $this->get_cursor_id_key();
80✔
305

306
                $this->builder->add_field( $key, $value, 'ID' );
80✔
307
        }
308

309
        /**
310
         * Get the WP Object instance for the cursor.
311
         *
312
         * This is cached internally so it should not generate additionl queries.
313
         *
314
         * @return mixed|null
315
         */
316
        abstract public function get_cursor_node();
317

318
        /**
319
         * Return the additional AND operators for the where statement
320
         *
321
         * @return string
322
         */
323
        abstract public function get_where();
324

325
        /**
326
         * Generate the final SQL string to be appended to WHERE clause
327
         *
328
         * @return string
329
         */
330
        abstract public function to_sql();
331
}
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