WordPoints_DB_Query   F
last analyzed

Complexity

Total Complexity 77

Size/Duplication

Total Lines 886
Duplicated Lines 3.16 %

Importance

Changes 0
Metric Value
dl 28
loc 886
rs 1.7391
c 0
b 0
f 0
wmc 77

22 Methods

Rating   Name   Duplication   Size   Complexity  
A get_sql() 0 18 2
A get() 0 16 2
A validate_unsigned_column() 0 7 3
A prepare_column_where() 0 9 4
A count() 0 7 1
A validate_values() 0 12 3
A date_query_valid_columns_filter() 0 10 1
B __construct() 14 26 4
A prepare_date_where() 0 19 4
B prepare_order_by() 0 45 6
B prepare_select() 0 24 4
A prepare_where() 0 17 4
A get_arg() 0 17 3
A get_validators_for_column() 0 14 3
C prepare_limit() 0 31 7
A prepare_query() 0 10 2
A get_comparator_for_column() 0 20 4
B prepare_column__in() 0 32 6
B prepare_column() 0 27 4
A validate_value() 0 12 3
B prepare_meta_where() 0 36 4
A set_args() 14 22 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WordPoints_DB_Query often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WordPoints_DB_Query, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Database query class.
5
 *
6
 * @package WordPoints
7
 * @since 2.1.0
8
 */
9
10
/**
11
 * Database query bootstrap.
12
 *
13
 * This class provides a bootstrap that can be extended to provide a simple, common
14
 * interface for querying a database. The child class defines the table schema, and
15
 * this bootstrap takes care of the rest.
16
 *
17
 * @since 2.1.0
18
 */
19
class WordPoints_DB_Query {
20
21
	/**
22
	 * The name of the table this query class is for.
23
	 *
24
	 * This should be the full name of the table, including the prefix. You will
25
	 * therefore likely need to define it from inside your constructor.
26
	 *
27
	 * @since 2.1.0
28
	 *
29
	 * @var string
30
	 */
31
	protected $table_name;
32
33
	/**
34
	 * The columns in the table being queried.
35
	 *
36
	 * The keys are the names of the columns. The values are arrays that support the
37
	 * following keys:
38
	 *
39
	 * - format (required) The format (%s, %d, or %f) to use when passing the values
40
	 *   for this format to $wpdb->prepare().
41
	 * - values (optional) An array of values that this column can have. Any values
42
	 *   that aren't in this list will be discarded from a query.
43
	 * - unsigned (optional) Whether the value is unsigned. If this is true, values
44
	 *   for this column will be rejected if they are not positive.
45
	 * - is_date (optional) Whether this is a DATETIME field. If so date queries will
46
	 *   be supported.
47
	 *
48
	 * For each column in this array, the following query args are supported:
49
	 *
50
	 * - "{$column}"          A single value that this column should have.
51
	 * - "{$column}__compare" How to compare the above value to the value in the DB.
52
	 *                        The default is '='.
53
	 * - "{$column}__in"      An array of values that this column may have.
54
	 * - "{$column}__not_in"  An array of values that this column may not have.
55
	 *
56
	 * Where {$column} is the name of the column.
57
	 *
58
	 * The "{$column}" query arg takes precedence over the "{$column}__in" and
59
	 * "{$column}__not_in" query args.
60
	 *
61
	 * However, if the column specifies that is_date is true, then the above are not
62
	 * supported, and the following are offered instead:
63
	 *
64
	 * - "{$column}_query" Arguments to pass to a WP_Date_Query.
65
	 *
66
	 * @since 2.1.0
67
	 *
68
	 * @var array[]
69
	 */
70
	protected $columns = array();
71
72
	/**
73
	 * The slug of the meta type.
74
	 *
75
	 * If this is defined, the 'meta_query', 'meta_key', 'meta_value',
76
	 * 'meta_compare', and 'meta_type' args are supported, and will be passed to
77
	 * WP_Meta_Query.
78
	 *
79
	 * @since 2.1.0
80
	 *
81
	 * @var string
82
	 */
83
	protected $meta_type;
84
85
	/**
86
	 * The default values for the query args.
87
	 *
88
	 * You can override this entirely if needed, or just modify it in your
89
	 * constructor before calling parent::__construct().
90
	 *
91
	 * @since 2.1.0
92
	 *
93
	 * @var array
94
	 */
95
	protected $defaults = array(
96
		'offset' => 0,
97
		'order'  => 'DESC',
98
	);
99
100
	/**
101
	 * A list of args that are deprecated and information about their replacements.
102
	 *
103
	 * Each element of the array should contain the following key-value pairs:
104
	 *
105
	 * - 'replacement' - The replacement arg.
106
	 * - 'version'     - The version in which this arg was deprecated.
107
	 * - 'class'       - The class this arg is from. Usually you will just want to
108
	 *                   use `__CLASS__` here.
109
	 *
110
	 * @since 2.3.0
111
	 *
112
	 * @var string[][]
113
	 */
114
	protected $deprecated_args = array();
115
116
	/**
117
	 * The query arguments.
118
	 *
119
	 * @since 2.1.0
120
	 *
121
	 * @type array $args
122
	 */
123
	protected $args = array();
124
125
	/**
126
	 * Whether the query is ready for execution, or still needs to be prepared.
127
	 *
128
	 * @since 2.1.0
129
	 *
130
	 * @type bool $is_query_ready
131
	 */
132
	protected $is_query_ready = false;
133
134
	/**
135
	 * The SELECT statement for the query.
136
	 *
137
	 * @since 2.1.0
138
	 *
139
	 * @type string $select
140
	 */
141
	protected $select;
142
143
	/**
144
	 * The SELECT COUNT statement for a count query.
145
	 *
146
	 * @since 2.1.0
147
	 *
148
	 * @type string $select_count
149
	 */
150
	protected $select_count = 'SELECT COUNT(*)';
151
152
	/**
153
	 * The JOIN query with the meta table.
154
	 *
155
	 * @since 2.1.0
156
	 *
157
	 * @type string $meta_join
158
	 */
159
	protected $meta_join;
160
161
	/**
162
	 * The WHERE clause for the query.
163
	 *
164
	 * @since 2.1.0
165
	 *
166
	 * @type string $where
167
	 */
168
	protected $where;
169
170
	/**
171
	 * The array of conditions for the WHERE clause.
172
	 *
173
	 * @since 2.1.0
174
	 *
175
	 * @type array $wheres
176
	 */
177
	protected $wheres = array();
178
179
	/**
180
	 * The LIMIT clause for the query.
181
	 *
182
	 * @since 2.1.0
183
	 *
184
	 * @type string $limit
185
	 */
186
	protected $limit;
187
188
	/**
189
	 * The ORDER clause for the query.
190
	 *
191
	 * @since 2.1.0
192
	 *
193
	 * @type string $order
194
	 */
195
	protected $order;
196
197
	/**
198
	 * Holds the meta query object when a meta query is being performed.
199
	 *
200
	 * @since 2.1.0
201
	 *
202
	 * @type WP_Meta_Query $meta_query
0 ignored issues
show
Bug introduced by
The type WP_Meta_Query was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
203
	 */
204
	protected $meta_query;
205
206
	//
207
	// Public Methods.
208
	//
209
210
	/**
211
	 * Construct the class.
212
	 *
213
	 * All of the arguments are expected *not* to be SQL escaped.
214
	 *
215
	 * @since 2.1.0
216
	 * @since 2.4.0 The 'start' query arg was deprecated in favor of 'offset'.
217
	 *
218
	 * @see WP_Meta_Query for the proper arguments for 'meta_query', 'meta_key', 'meta_value', 'meta_compare', and 'meta_type'.
219
	 *
220
	 * @param array $args {
221
	 *        The arguments for the query.
222
	 *
223
	 *        @type string|array $fields              Fields to include in the results. Default is all fields.
224
	 *        @type int          $limit               The maximum number of results to return. Default is null (no limit).
225
	 *        @type int          $offset              The offset for the LIMIT clause. Default: 0.
226
	 *        @type string       $order_by            The field to use to order the results.
227
	 *        @type string       $order               The order for the query: ASC or DESC (default).
228
	 *        @type string       $meta_key            See WP_Meta_Query.
229
	 *        @type mixed        $meta_value          See WP_Meta_Query.
230
	 *        @type string       $meta_compare        See WP_Meta_Query.
231
	 *        @type string       $meta_type           See WP_Meta_Query.
232
	 *        @type array        $meta_query          See WP_Meta_Query.
233
	 * }
234
	 */
235
	public function __construct( $args = array() ) {
236
237
		if ( ! isset( $this->deprecated_args['start'] ) ) {
238
			$this->deprecated_args['start'] = array(
239
				'class'       => __CLASS__,
240
				'version'     => '2.4.0',
241
				'replacement' => 'offset',
242
			);
243
		}
244
245 View Code Duplication
		foreach ( $this->deprecated_args as $arg => $data ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
246
			if ( isset( $args[ $arg ] ) ) {
247
248
				_deprecated_argument(
0 ignored issues
show
Bug introduced by
The function _deprecated_argument was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

248
				/** @scrutinizer ignore-call */ 
249
    _deprecated_argument(
Loading history...
249
					esc_html( "{$data['class']}::__construct" )
0 ignored issues
show
Bug introduced by
The function esc_html was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

249
					/** @scrutinizer ignore-call */ 
250
     esc_html( "{$data['class']}::__construct" )
Loading history...
250
					, esc_html( $data['version'] )
251
					, esc_html( "{$arg} is deprecated, use {$data['replacement']} instead" )
252
				);
253
254
				$args[ $data['replacement'] ] = $args[ $arg ];
255
256
				unset( $args[ $arg ] );
257
			}
258
		}
259
260
		$this->args = array_merge( $this->defaults, $args );
261
	}
262
263
	/**
264
	 * Get a query arg.
265
	 *
266
	 * @since 2.1.0
267
	 *
268
	 * @param string $arg The query arg whose value to retrieve.
269
	 *
270
	 * @return mixed|null The query arg's value, or null if it isn't set.
271
	 */
272
	public function get_arg( $arg ) {
273
274
		if ( isset( $this->deprecated_args[ $arg ] ) ) {
275
276
			_deprecated_argument(
0 ignored issues
show
Bug introduced by
The function _deprecated_argument was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

276
			/** @scrutinizer ignore-call */ 
277
   _deprecated_argument(
Loading history...
277
				esc_html( "{$this->deprecated_args[ $arg ]['class']}::get_arg" )
0 ignored issues
show
Bug introduced by
The function esc_html was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

277
				/** @scrutinizer ignore-call */ 
278
    esc_html( "{$this->deprecated_args[ $arg ]['class']}::get_arg" )
Loading history...
278
				, esc_html( $this->deprecated_args[ $arg ]['version'] )
279
				, esc_html( "{$arg} is deprecated, use {$this->deprecated_args[ $arg ]['replacement']} instead" )
280
			);
281
282
			$arg = $this->deprecated_args[ $arg ]['replacement'];
283
		}
284
285
		if ( isset( $this->args[ $arg ] ) ) {
286
			return $this->args[ $arg ];
287
		} else {
288
			return null;
289
		}
290
	}
291
292
	/**
293
	 * Set arguments for the query.
294
	 *
295
	 * All of the arguments supported by the constructor may be passed in here, and
296
	 * will be merged into the array of existing args.
297
	 *
298
	 * @since 2.1.0
299
	 *
300
	 * @param array $args A list of arguments to set and their values.
301
	 *
302
	 * @return WordPoints_DB_Query To allow for method chaining.
303
	 */
304
	public function set_args( array $args ) {
305
306 View Code Duplication
		foreach ( $this->deprecated_args as $arg => $data ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
307
			if ( isset( $args[ $arg ] ) ) {
308
309
				_deprecated_argument(
0 ignored issues
show
Bug introduced by
The function _deprecated_argument was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

309
				/** @scrutinizer ignore-call */ 
310
    _deprecated_argument(
Loading history...
310
					esc_html( "{$data['class']}::set_args" )
0 ignored issues
show
Bug introduced by
The function esc_html was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

310
					/** @scrutinizer ignore-call */ 
311
     esc_html( "{$data['class']}::set_args" )
Loading history...
311
					, esc_html( $data['version'] )
312
					, esc_html( "{$arg} is deprecated, use {$data['replacement']} instead" )
313
				);
314
315
				$args[ $data['replacement'] ] = $args[ $arg ];
316
317
				unset( $args[ $arg ] );
318
			}
319
		}
320
321
		$this->args = array_merge( $this->args, $args );
322
323
		$this->is_query_ready = false;
324
325
		return $this;
326
	}
327
328
	/**
329
	 * Count the number of results.
330
	 *
331
	 * When used with a query that contains a LIMIT clause, this method currently
332
	 * returns the count of the query ignoring the LIMIT, as would be the case with
333
	 * any similar query. However, this behaviour is not hardened and should not be
334
	 * relied upon. Make inquiry before assuming the constancy of this behaviour.
335
	 *
336
	 * @since 2.1.0
337
	 *
338
	 * @return int The number of results.
339
	 */
340
	public function count() {
341
342
		global $wpdb;
343
344
		$count = (int) $wpdb->get_var( $this->get_sql( 'SELECT COUNT' ) ); // WPCS: unprepared SQL, cache OK
345
346
		return $count;
347
	}
348
349
	/**
350
	 * Get the results for the query.
351
	 *
352
	 * @since 2.1.0
353
	 *
354
	 * @param string $method The method to use. Options are 'results', 'row', 'col',
355
	 *                       and 'var'.
356
	 *
357
	 * @return mixed The results of the query, or false on failure.
358
	 */
359
	public function get( $method = 'results' ) {
360
361
		global $wpdb;
362
363
		$methods = array( 'results', 'row', 'col', 'var' );
364
365
		if ( ! in_array( $method, $methods, true ) ) {
366
367
			_doing_it_wrong( __METHOD__, esc_html( sprintf( 'WordPoints Debug Error: invalid get method %s, possible values are %s', $method, implode( ', ', $methods ) ) ), '1.0.0' );
0 ignored issues
show
Bug introduced by
The function esc_html was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

367
			_doing_it_wrong( __METHOD__, /** @scrutinizer ignore-call */ esc_html( sprintf( 'WordPoints Debug Error: invalid get method %s, possible values are %s', $method, implode( ', ', $methods ) ) ), '1.0.0' );
Loading history...
Bug introduced by
The function _doing_it_wrong was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

367
			/** @scrutinizer ignore-call */ 
368
   _doing_it_wrong( __METHOD__, esc_html( sprintf( 'WordPoints Debug Error: invalid get method %s, possible values are %s', $method, implode( ', ', $methods ) ) ), '1.0.0' );
Loading history...
368
369
			return false;
370
		}
371
372
		$result = $wpdb->{"get_{$method}"}( $this->get_sql() );
373
374
		return $result;
375
	}
376
377
	/**
378
	 * Get the SQL for the query.
379
	 *
380
	 * This function can return the SQL for a SELECT or SELECT COUNT query. To
381
	 * specify which one to return, set the $select_type parameter. Defaults to
382
	 * SELECT.
383
	 *
384
	 * This function is public for debugging purposes.
385
	 *
386
	 * @since 2.1.0
387
	 *
388
	 * @param string $select_type The type of query, SELECT, or SELECT COUNT.
389
	 *
390
	 * @return string The SQL for the query.
391
	 */
392
	public function get_sql( $select_type = 'SELECT' ) {
393
394
		$this->prepare_query();
395
396
		if ( 'SELECT COUNT' === $select_type ) {
397
			$select = $this->select_count;
398
			$order  = '';
399
		} else {
400
			$select = $this->select;
401
			$order  = $this->order;
402
		}
403
404
		return $select
405
			. "\nFROM `{$this->table_name}`\n"
406
			. $this->meta_join
407
			. $this->where
408
			. $order
409
			. $this->limit;
410
	}
411
412
	//
413
	// Filter Methods.
414
	//
415
416
	/**
417
	 * Filter date query valid columns for WP_Date_Query.
418
	 *
419
	 * @since 2.1.0
420
	 *
421
	 * @WordPress\filter date_query_valid_columns Added and subsequently removed by
422
	 *                                            self::prepare_date_where().
423
	 *
424
	 * @param string[] $valid_columns The names of the valid columns for date queries.
425
	 *
426
	 * @return string[] The valid columns.
427
	 */
428
	public function date_query_valid_columns_filter( $valid_columns ) {
429
430
		$valid_columns = array_merge(
431
			$valid_columns
432
			, array_keys(
433
				wp_list_filter( $this->columns, array( 'is_date' => true ) )
0 ignored issues
show
Bug introduced by
The function wp_list_filter was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

433
				/** @scrutinizer ignore-call */ 
434
    wp_list_filter( $this->columns, array( 'is_date' => true ) )
Loading history...
434
			)
435
		);
436
437
		return $valid_columns;
438
	}
439
440
	//
441
	// Protected Methods.
442
	//
443
444
	/**
445
	 * Prepare the query.
446
	 *
447
	 * @since 2.1.0
448
	 */
449
	protected function prepare_query() {
450
451
		if ( ! $this->is_query_ready ) {
452
453
			$this->prepare_select();
454
			$this->prepare_where();
455
			$this->prepare_order_by();
456
			$this->prepare_limit();
457
458
			$this->is_query_ready = true;
459
		}
460
	}
461
462
	/**
463
	 * Prepare the select statement.
464
	 *
465
	 * @since 2.1.0
466
	 */
467
	protected function prepare_select() {
468
469
		$all_fields = array_keys( $this->columns );
470
		$fields     = array();
471
472
		if ( ! empty( $this->args['fields'] ) ) {
473
474
			$fields = (array) $this->args['fields'];
475
			$diff   = array_diff( $fields, $all_fields );
476
			$fields = array_intersect( $all_fields, $fields );
477
478
			if ( ! empty( $diff ) ) {
479
				_doing_it_wrong( __METHOD__, esc_html( 'WordPoints Debug Error: invalid field(s) "' . implode( '", "', $diff ) . '" given' ), '1.0.0' );
0 ignored issues
show
Bug introduced by
The function esc_html was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

479
				_doing_it_wrong( __METHOD__, /** @scrutinizer ignore-call */ esc_html( 'WordPoints Debug Error: invalid field(s) "' . implode( '", "', $diff ) . '" given' ), '1.0.0' );
Loading history...
Bug introduced by
The function _doing_it_wrong was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

479
				/** @scrutinizer ignore-call */ 
480
    _doing_it_wrong( __METHOD__, esc_html( 'WordPoints Debug Error: invalid field(s) "' . implode( '", "', $diff ) . '" given' ), '1.0.0' );
Loading history...
480
			}
481
		}
482
483
		// Pull all fields by default.
484
		if ( empty( $fields ) ) {
485
			$fields = $all_fields;
486
		}
487
488
		$fields = implode( ', ', array_map( 'wordpoints_escape_mysql_identifier', $fields ) );
489
490
		$this->select = "SELECT {$fields}";
491
	}
492
493
	/**
494
	 * Validates a value against an array of sanitizing functions.
495
	 *
496
	 * @since 2.1.0
497
	 *
498
	 * @param mixed      $value      The value to validate.
499
	 * @param callable[] $validators The validators to validate it against.
500
	 *
501
	 * @return mixed The validated value, or false if invalid.
502
	 */
503
	protected function validate_value( $value, $validators ) {
504
505
		foreach ( $validators as $validator ) {
506
507
			$value = call_user_func_array( $validator, array( &$value ) );
508
509
			if ( false === $value ) {
510
				break;
511
			}
512
		}
513
514
		return $value;
515
	}
516
517
	/**
518
	 * Validates an array of values against an array of sanitizing functions.
519
	 *
520
	 * @since 2.1.0
521
	 *
522
	 * @param array      $values     The values to validate.
523
	 * @param callable[] $validators The validators to validate each value against.
524
	 *
525
	 * @return array The validated values, with any invalid ones removed.
526
	 */
527
	protected function validate_values( $values, $validators ) {
528
529
		foreach ( $values as $index => $value ) {
530
531
			$value = $this->validate_value( $value, $validators );
532
533
			if ( false === $value ) {
534
				unset( $values[ $index ] );
535
			}
536
		}
537
538
		return $values;
539
	}
540
541
	/**
542
	 * Validate an unsigned column.
543
	 *
544
	 * The value must be positive, zero-inclusive. We can't just use
545
	 * wordpoints_posint() because it is zero exclusive.
546
	 *
547
	 * @since 2.1.0
548
	 *
549
	 * @param mixed $value The value to validate.
550
	 *
551
	 * @return int|false The validated value or false.
552
	 */
553
	protected function validate_unsigned_column( $value ) {
554
555
		if ( false !== wordpoints_int( $value ) && $value >= 0 ) {
556
			return $value;
557
		}
558
559
		return false;
560
	}
561
562
	/**
563
	 * Get an array of validating/sanitizing functions for the values of a column.
564
	 *
565
	 * @since 2.1.0
566
	 *
567
	 * @param array $data The data for the column.
568
	 *
569
	 * @return callable[] The validation functions.
570
	 */
571
	protected function get_validators_for_column( $data ) {
572
573
		$validators = array();
574
575
		// Default validators for integer columns.
576
		if ( '%d' === $data['format'] ) {
577
			if ( ! empty( $data['unsigned'] ) ) {
578
				$validators[] = array( $this, 'validate_unsigned_column' );
579
			} else {
580
				$validators[] = 'wordpoints_int';
581
			}
582
		}
583
584
		return $validators;
585
	}
586
587
	/**
588
	 * Prepare the conditions for the WHERE clause for a column.
589
	 *
590
	 * @since 2.1.0
591
	 *
592
	 * @param string $column The column name.
593
	 * @param array  $data   The column data.
594
	 */
595
	protected function prepare_column_where( $column, $data ) {
596
597
		// If a single value has been supplied for the column, it takes precedence.
598
		if ( isset( $this->args[ $column ] ) ) {
599
			$this->prepare_column( $column, $data );
600
		} elseif ( isset( $this->args[ "{$column}__in" ] ) ) {
601
			$this->prepare_column__in( $column, $data );
602
		} elseif ( isset( $this->args[ "{$column}__not_in" ] ) ) {
603
			$this->prepare_column__in( $column, $data, 'NOT IN' );
604
		}
605
	}
606
607
	/**
608
	 * Prepare a single-value condition for the WHERE clause for a column.
609
	 *
610
	 * @since 2.1.0
611
	 *
612
	 * @param string $column The name of the column
613
	 * @param array  $data   The column data.
614
	 */
615
	protected function prepare_column( $column, $data ) {
616
617
		global $wpdb;
618
619
		if (
620
			isset( $data['values'] )
621
			&& ! in_array( $this->args[ $column ], $data['values'], true )
622
		) {
623
			return;
624
		}
625
626
		$value = $this->validate_value(
627
			$this->args[ $column ]
628
			, $this->get_validators_for_column( $data )
629
		);
630
631
		if ( false === $value ) {
632
			return;
633
		}
634
635
		$compare = $this->get_comparator_for_column( $column, $data );
636
637
		$column = wordpoints_escape_mysql_identifier( $column );
638
639
		$this->wheres[] = $wpdb->prepare( // WPCS: unprepared SQL, PreparedSQLPlaceholders replacement count OK.
640
			"{$column} {$compare} {$data['format']}"
641
			, $value
642
		);
643
	}
644
645
	/**
646
	 * Get the comparator for a column.
647
	 *
648
	 * @since 2.1.0
649
	 *
650
	 * @param string $column The column name.
651
	 * @param array  $data   The column data.
652
	 *
653
	 * @return string The comparator for the column.
654
	 */
655
	protected function get_comparator_for_column( $column, $data ) {
656
657
		$comparisons = array( '=', '<', '>', '<>', '!=', '<=', '>=' );
658
659
		// MySQL doesn't support LIKE and NOT LIKE for int columns.
660
		// See https://stackoverflow.com/q/8422455/1924128
661
		if ( '%s' === $data['format'] ) {
662
			$comparisons = array_merge( $comparisons, array( 'LIKE', 'NOT LIKE' ) );
663
		}
664
665
		$comparator = '=';
666
667
		if (
668
			isset( $this->args[ "{$column}__compare" ] )
669
			&& in_array( $this->args[ "{$column}__compare" ], $comparisons, true )
670
		) {
671
			$comparator = $this->args[ "{$column}__compare" ];
672
		}
673
674
		return $comparator;
675
	}
676
677
	/**
678
	 * Prepare the IN or NOT IN conditions for a column.
679
	 *
680
	 * @since 2.1.0
681
	 *
682
	 * @param string $column The name of the column.
683
	 * @param array  $data   The column data.
684
	 * @param string $type   The type of IN clause, IN or NOT IN.
685
	 */
686
	protected function prepare_column__in( $column, $data, $type = 'IN' ) {
687
688
		$key = "{$column}__" . strtolower( str_replace( ' ', '_', $type ) );
689
690
		if ( empty( $this->args[ $key ] ) || ! is_array( $this->args[ $key ] ) ) {
691
			return;
692
		}
693
694
		$values = $this->args[ $key ];
695
696
		if ( isset( $data['values'] ) ) {
697
			$values = array_intersect( $values, $data['values'] );
698
		} else {
699
			$values = $this->validate_values(
700
				$values
701
				, $this->get_validators_for_column( $data )
702
			);
703
		}
704
705
		if ( empty( $values ) ) {
706
			return;
707
		}
708
709
		$in = wordpoints_prepare__in( $values, $data['format'] );
710
711
		if ( false === $in ) {
712
			return;
713
		}
714
715
		$column = wordpoints_escape_mysql_identifier( $column );
716
717
		$this->wheres[] = "{$column} {$type} ({$in})";
718
	}
719
720
	/**
721
	 * Prepare the WHERE clause for the query.
722
	 *
723
	 * @since 2.1.0
724
	 */
725
	protected function prepare_where() {
726
727
		$this->wheres = array();
728
729
		foreach ( $this->columns as $column => $data ) {
730
731
			if ( ! empty( $data['is_date'] ) ) {
732
				$this->prepare_date_where( $column );
733
			} else {
734
				$this->prepare_column_where( $column, $data );
735
			}
736
		}
737
738
		$this->prepare_meta_where();
739
740
		if ( ! empty( $this->wheres ) ) {
741
			$this->where = 'WHERE ' . implode( ' AND ', $this->wheres ) . "\n";
742
		}
743
	}
744
745
	/**
746
	 * Prepare the LIMIT clause for the query.
747
	 *
748
	 * @since 2.1.0
749
	 */
750
	protected function prepare_limit() {
751
752
		// MySQL doesn't allow for the offset without a limit, so if no limit is set
753
		// we can ignore the offset arg. See https://stackoverflow.com/a/271650/1924128
754
		if ( ! isset( $this->args['limit'] ) ) {
755
			return;
756
		}
757
758
		foreach ( array( 'limit', 'offset' ) as $key ) {
759
760
			// Save a backup of the arg value since wordpoints_int() is by reference.
761
			$arg = $this->args[ $key ];
762
763
			if ( false === wordpoints_int( $this->args[ $key ] ) ) {
764
765
				_doing_it_wrong(
0 ignored issues
show
Bug introduced by
The function _doing_it_wrong was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

765
				/** @scrutinizer ignore-call */ 
766
    _doing_it_wrong(
Loading history...
766
					__METHOD__
767
					, sprintf(
768
						"WordPoints Debug Error: '%s' must be a positive integer, %s given"
769
						, esc_html( $key )
0 ignored issues
show
Bug introduced by
The function esc_html was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

769
						, /** @scrutinizer ignore-call */ esc_html( $key )
Loading history...
770
						, esc_html( strval( $arg ) ? $arg : gettype( $arg ) )
771
					)
772
					, '1.0.0'
773
				);
774
775
				$this->args[ $key ] = 0;
776
			}
777
		}
778
779
		if ( $this->args['limit'] > 0 && $this->args['offset'] >= 0 ) {
780
			$this->limit = "LIMIT {$this->args['offset']}, {$this->args['limit']}";
781
		}
782
	}
783
784
	/**
785
	 * Prepare the ORDER BY clause for the query.
786
	 *
787
	 * @since 2.1.0
788
	 */
789
	protected function prepare_order_by() {
790
791
		if ( empty( $this->args['order_by'] ) ) {
792
			$this->order = '';
793
			return;
794
		}
795
796
		$order    = $this->args['order'];
797
		$order_by = $this->args['order_by'];
798
799
		if ( ! in_array( $order, array( 'DESC', 'ASC' ), true ) ) {
800
801
			_doing_it_wrong( __METHOD__, esc_html( "WordPoints Debug Error: invalid 'order' \"{$order}\", possible values are DESC and ASC" ), '2.1.0' );
0 ignored issues
show
Bug introduced by
The function esc_html was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

801
			_doing_it_wrong( __METHOD__, /** @scrutinizer ignore-call */ esc_html( "WordPoints Debug Error: invalid 'order' \"{$order}\", possible values are DESC and ASC" ), '2.1.0' );
Loading history...
Bug introduced by
The function _doing_it_wrong was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

801
			/** @scrutinizer ignore-call */ 
802
   _doing_it_wrong( __METHOD__, esc_html( "WordPoints Debug Error: invalid 'order' \"{$order}\", possible values are DESC and ASC" ), '2.1.0' );
Loading history...
802
			$order = 'DESC';
803
		}
804
805
		if ( 'meta_value' === $order_by ) {
806
807
			global $wpdb;
808
809
			$meta_table_name = wordpoints_escape_mysql_identifier(
810
				$wpdb->{"{$this->meta_type}meta"}
811
			);
812
813
			if ( isset( $this->args['meta_type'] ) ) {
814
815
				$meta_type = $this->meta_query->get_cast_for_type( $this->args['meta_type'] );
816
				$order_by  = "CAST({$meta_table_name}.meta_value AS {$meta_type})";
817
818
			} else {
819
820
				$order_by = "{$meta_table_name}.meta_value";
821
			}
822
823
		} elseif ( isset( $this->columns[ $order_by ] ) ) {
824
825
			$order_by = wordpoints_escape_mysql_identifier( $order_by );
826
827
		} else {
828
829
			_doing_it_wrong( __METHOD__, esc_html( "WordPoints Debug Error: invalid 'order_by' \"{$order_by}\", possible values are " . implode( ', ', array_keys( $this->columns ) ) ), '2.1.0' );
830
			return;
831
		}
832
833
		$this->order = "ORDER BY {$order_by} {$order}\n";
834
	}
835
836
	/**
837
	 * Prepare the date query for a column.
838
	 *
839
	 * @since 2.1.0
840
	 *
841
	 * @param string $column The name of the column.
842
	 */
843
	protected function prepare_date_where( $column ) {
844
845
		if (
846
			empty( $this->args[ "{$column}_query" ] )
847
			|| ! is_array( $this->args[ "{$column}_query" ] )
848
		) {
849
			return;
850
		}
851
852
		add_filter( 'date_query_valid_columns', array( $this, 'date_query_valid_columns_filter' ) );
0 ignored issues
show
Bug introduced by
The function add_filter was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

852
		/** @scrutinizer ignore-call */ 
853
  add_filter( 'date_query_valid_columns', array( $this, 'date_query_valid_columns_filter' ) );
Loading history...
853
854
		$date_query = new WP_Date_Query( $this->args[ "{$column}_query" ], $column );
0 ignored issues
show
Bug introduced by
The type WP_Date_Query was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
855
		$date_query = $date_query->get_sql();
856
857
		if ( ! empty( $date_query ) ) {
858
			$this->wheres[] = ltrim( $date_query, ' AND' );
859
		}
860
861
		remove_filter( 'date_query_valid_columns', array( $this, 'date_query_valid_columns_filter' ) );
0 ignored issues
show
Bug introduced by
The function remove_filter was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

861
		/** @scrutinizer ignore-call */ 
862
  remove_filter( 'date_query_valid_columns', array( $this, 'date_query_valid_columns_filter' ) );
Loading history...
862
	}
863
864
	/**
865
	 * Prepare the meta query.
866
	 *
867
	 * @since 2.1.0
868
	 */
869
	protected function prepare_meta_where() {
870
871
		if ( empty( $this->meta_type ) ) {
872
			return;
873
		}
874
875
		$meta_args = array_intersect_key(
876
			$this->args
877
			, array(
878
				'meta_key'     => '',
879
				'meta_value'   => '',
880
				'meta_compare' => '',
881
				'meta_type'    => '',
882
				'meta_query'   => '',
883
			)
884
		);
885
886
		if ( empty( $meta_args ) ) {
887
			return;
888
		}
889
890
		$this->meta_query = new WP_Meta_Query();
891
		$this->meta_query->parse_query_vars( $meta_args );
892
893
		$meta_query = $this->meta_query->get_sql(
894
			$this->meta_type
895
			, $this->table_name
896
			, 'id'
897
			, $this
898
		);
899
900
		if ( ! empty( $meta_query['where'] ) ) {
901
			$this->wheres[] = ltrim( $meta_query['where'], ' AND' );
902
		}
903
904
		$this->meta_join = $meta_query['join'] . "\n";
905
	}
906
}
907
908
// EOF
909