PluginProbe
WPGraphQL / trunk
WPGraphQL vtrunk
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 / Cursor / AbstractCursor.php

AbstractCursor.php in WPGraphQL trunk, at src/Data/Cursor/AbstractCursor.php

332 lines 7.9 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\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' ) {
73 global $wpdb;
74
75 $this->wpdb = $wpdb;
76 $this->query_vars = $query_vars;
77 $this->cursor = $cursor;
78
79 /**
80 * Get the cursor offset if any
81 */
82 $offset = $this->get_query_var( 'graphql_' . $cursor . '_cursor' );
83
84 // Handle deprecated use of `graphql_cursor_offset`. @todo remove in 3.0.0
85 if ( empty( $offset ) ) {
86 $offset = $this->get_query_var( 'graphql_cursor_offset' );
87
88 if ( ! empty( $offset ) ) {
89 _doing_it_wrong(
90 self::class . "::get_query_var('graphql_cursor_offset')",
91 esc_html__( "Use 'graphql_before_cursor' or 'graphql_after_cursor' instead. This will be removed in the next major release", 'wp-graphql' ),
92 '1.9.0'
93 );
94 }
95 }
96
97 $this->cursor_offset = ! empty( $offset ) ? absint( $offset ) : 0;
98
99 // Get the WP Object for the cursor.
100 $this->cursor_node = $this->get_cursor_node();
101
102 // Get the direction for the builder query.
103 $this->compare = $this->get_cursor_compare();
104
105 $this->builder = new CursorBuilder( $this->compare );
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 ) {
116 if ( isset( $this->query_vars[ $name ] ) && '' !== $this->query_vars[ $name ] ) {
117 return $this->query_vars[ $name ];
118 }
119 return null;
120 }
121
122 /**
123 * Get the direction pagination is going in.
124 *
125 * @return string
126 */
127 public function get_cursor_compare() {
128 if ( 'before' === $this->cursor ) {
129 return '>';
130 }
131
132 return '<';
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() {
141 if (
142 ! is_int( $this->cursor_offset ) ||
143 0 >= $this->cursor_offset ||
144 ! $this->cursor_node
145 ) {
146 return false;
147 }
148
149 return true;
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 {
161 // Throw if an array not provided.
162 if ( ! is_array( $field ) ) {
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'] ) ) {
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'] ) ) {
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'] ) ) {
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 ) ) {
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() {
234 $key = $this->get_query_var( 'graphql_cursor_id_key' );
235 if ( null === $key ) {
236 $key = $this->id_key;
237 }
238
239 return $key;
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 {
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' );
256 if ( null === $cursor_compare_fields ) {
257 $cursor_compare_fields = $fallback;
258 }
259 // Bail early if no cursor compare fields.
260 if ( empty( $cursor_compare_fields ) ) {
261 return;
262 }
263
264 if ( ! is_array( $cursor_compare_fields ) ) {
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] ) ) {
278 $cursor_compare_fields = [ $cursor_compare_fields ];
279 }
280
281 foreach ( $cursor_compare_fields as $field ) {
282 $this->validate_cursor_compare_field( $field );
283
284 $key = $field['key'];
285 $value = $field['value'];
286 $type = ! empty( $field['type'] ) ? $field['type'] : null;
287 $order = ! empty( $field['order'] ) ? $field['order'] : null;
288
289 $this->builder->add_field( $key, $value, $type, $order );
290 }
291 }
292
293 /**
294 * Applies ID field to the cursor builder.
295 */
296 protected function compare_with_id_field(): void {
297 // Get ID value.
298 $value = $this->get_query_var( 'graphql_cursor_id_value' );
299 if ( null === $value ) {
300 $value = (string) $this->cursor_offset;
301 }
302
303 // Get ID SQL Query alias.
304 $key = $this->get_cursor_id_key();
305
306 $this->builder->add_field( $key, $value, 'ID' );
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 }
332