Completed
Push — master ( e9e0b4...cbda97 )
by J.D.
03:49
created

WordPoints_DB_Query   C

Complexity

Total Complexity 76

Size/Duplication

Total Lines 874
Duplicated Lines 3.2 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 28
loc 874
rs 5
c 0
b 0
f 0
wmc 76
lcom 1
cbo 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 14 19 3
A get_arg() 0 19 3
A set_args() 14 23 3
A count() 0 8 1
A get() 0 17 2
A get_sql() 0 15 2
A date_query_valid_columns_filter() 0 11 1
A prepare_query() 0 12 2
B prepare_select() 0 25 4
A validate_value() 0 13 3
A validate_values() 0 13 3
A validate_unsigned_column() 0 8 3
A get_validators_for_column() 0 15 3
A prepare_column_where() 0 11 4
B prepare_column() 0 29 4
A get_comparator_for_column() 0 21 4
B prepare_column__in() 0 33 6
A prepare_where() 0 19 4
C prepare_limit() 0 33 7
B prepare_order_by() 0 45 6
A prepare_date_where() 0 20 4
B prepare_meta_where() 0 37 4

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
		'start'  => 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
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
	 *
217
	 * @see WP_Meta_Query for the proper arguments for 'meta_query', 'meta_key', 'meta_value', 'meta_compare', and 'meta_type'.
218
	 *
219
	 * @param array $args {
220
	 *        The arguments for the query.
221
	 *
222
	 *        @type string|array $fields              Fields to include in the results. Default is all fields.
223
	 *        @type int          $limit               The maximum number of results to return. Default is null (no limit).
224
	 *        @type int          $start               The start for the LIMIT clause. Default: 0.
225
	 *        @type string       $order_by            The field to use to order the results.
226
	 *        @type string       $order               The order for the query: ASC or DESC (default).
227
	 *        @type string       $meta_key            See WP_Meta_Query.
228
	 *        @type mixed        $meta_value          See WP_Meta_Query.
229
	 *        @type string       $meta_compare        See WP_Meta_Query.
230
	 *        @type string       $meta_type           See WP_Meta_Query.
231
	 *        @type array        $meta_query          See WP_Meta_Query.
232
	 * }
233
	 */
234
	public function __construct( $args = array() ) {
235
236 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...
237
			if ( isset( $args[ $arg ] ) ) {
238
239
				_deprecated_argument(
240
					esc_html( "{$data['class']}::__construct" )
241
					, esc_html( $data['version'] )
242
					, esc_html( "{$arg} is deprecated, use {$data['replacement']} instead" )
243
				);
244
245
				$args[ $data['replacement'] ] = $args[ $arg ];
246
247
				unset( $args[ $arg ] );
248
			}
249
		}
250
251
		$this->args = array_merge( $this->defaults, $args );
252
	}
253
254
	/**
255
	 * Get a query arg.
256
	 *
257
	 * @since 2.1.0
258
	 *
259
	 * @param string $arg The query arg whose value to retrieve.
260
	 *
261
	 * @return mixed|null The query arg's value, or null if it isn't set.
262
	 */
263
	public function get_arg( $arg ) {
264
265
		if ( isset( $this->deprecated_args[ $arg ] ) ) {
266
267
			_deprecated_argument(
268
				esc_html( "{$this->deprecated_args[ $arg ]['class']}::get_arg" )
269
				, esc_html( $this->deprecated_args[ $arg ]['version'] )
270
				, esc_html( "{$arg} is deprecated, use {$this->deprecated_args[ $arg ]['replacement']} instead" )
271
			);
272
273
			$arg = $this->deprecated_args[ $arg ]['replacement'];
274
		}
275
276
		if ( isset( $this->args[ $arg ] ) ) {
277
			return $this->args[ $arg ];
278
		} else {
279
			return null;
280
		}
281
	}
282
283
	/**
284
	 * Set arguments for the query.
285
	 *
286
	 * All of the arguments supported by the constructor may be passed in here, and
287
	 * will be merged into the array of existing args.
288
	 *
289
	 * @since 2.1.0
290
	 *
291
	 * @param array $args A list of arguments to set and their values.
292
	 *
293
	 * @return WordPoints_DB_Query To allow for method chaining.
294
	 */
295
	public function set_args( array $args ) {
296
297 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...
298
			if ( isset( $args[ $arg ] ) ) {
299
300
				_deprecated_argument(
301
					esc_html( "{$data['class']}::set_args" )
302
					, esc_html( $data['version'] )
303
					, esc_html( "{$arg} is deprecated, use {$data['replacement']} instead" )
304
				);
305
306
				$args[ $data['replacement'] ] = $args[ $arg ];
307
308
				unset( $args[ $arg ] );
309
			}
310
		}
311
312
		$this->args = array_merge( $this->args, $args );
313
314
		$this->is_query_ready = false;
315
316
		return $this;
317
	}
318
319
	/**
320
	 * Count the number of results.
321
	 *
322
	 * When used with a query that contains a LIMIT clause, this method currently
323
	 * returns the count of the query ignoring the LIMIT, as would be the case with
324
	 * any similar query. However, this behaviour is not hardened and should not be
325
	 * relied upon. Make inquiry before assuming the constancy of this behaviour.
326
	 *
327
	 * @since 2.1.0
328
	 *
329
	 * @return int The number of results.
330
	 */
331
	public function count() {
332
333
		global $wpdb;
334
335
		$count = (int) $wpdb->get_var( $this->get_sql( 'SELECT COUNT' ) ); // WPCS: unprepared SQL, cache OK
336
337
		return $count;
338
	}
339
340
	/**
341
	 * Get the results for the query.
342
	 *
343
	 * @since 2.1.0
344
	 *
345
	 * @param string $method The method to use. Options are 'results', 'row', 'col',
346
	 *                       and 'var'.
347
	 *
348
	 * @return mixed The results of the query, or false on failure.
349
	 */
350
	public function get( $method = 'results' ) {
351
352
		global $wpdb;
353
354
		$methods = array( 'results', 'row', 'col', 'var' );
355
356
		if ( ! in_array( $method, $methods ) ) {
357
358
			_doing_it_wrong( __METHOD__, esc_html( sprintf( 'WordPoints Debug Error: invalid get method %s, possible values are %s', $method, implode( ', ', $methods ) ) ), '1.0.0' );
359
360
			return false;
361
		}
362
363
		$result = $wpdb->{"get_{$method}"}( $this->get_sql() );
364
365
		return $result;
366
	}
367
368
	/**
369
	 * Get the SQL for the query.
370
	 *
371
	 * This function can return the SQL for a SELECT or SELECT COUNT query. To
372
	 * specify which one to return, set the $select_type parameter. Defaults to
373
	 * SELECT.
374
	 *
375
	 * This function is public for debugging purposes.
376
	 *
377
	 * @since 2.1.0
378
	 *
379
	 * @param string $select_type The type of query, SELECT, or SELECT COUNT.
380
	 *
381
	 * @return string The SQL for the query.
382
	 */
383
	public function get_sql( $select_type = 'SELECT' ) {
384
385
		$this->prepare_query();
386
387
		$select = ( 'SELECT COUNT' === $select_type )
388
			? $this->select_count
389
			: $this->select;
390
391
		return $select
392
			. "\nFROM `{$this->table_name}`\n"
393
			. $this->meta_join
394
			. $this->where
395
			. $this->order
396
			. $this->limit;
397
	}
398
399
	//
400
	// Filter Methods.
401
	//
402
403
	/**
404
	 * Filter date query valid columns for WP_Date_Query.
405
	 *
406
	 * @since 2.1.0
407
	 *
408
	 * @WordPress\filter date_query_valid_columns Added and subsequently removed by
409
	 *                                            self::prepare_date_where().
410
	 *
411
	 * @param string[] $valid_columns The names of the valid columns for date queries.
412
	 *
413
	 * @return string[] The valid columns.
414
	 */
415
	public function date_query_valid_columns_filter( $valid_columns ) {
416
417
		$valid_columns = array_merge(
418
			$valid_columns
419
			, array_keys(
420
				wp_list_filter( $this->columns, array( 'is_date' => true ) )
421
			)
422
		);
423
424
		return $valid_columns;
425
	}
426
427
	//
428
	// Protected Methods.
429
	//
430
431
	/**
432
	 * Prepare the query.
433
	 *
434
	 * @since 2.1.0
435
	 */
436
	protected function prepare_query() {
437
438
		if ( ! $this->is_query_ready ) {
439
440
			$this->prepare_select();
441
			$this->prepare_where();
442
			$this->prepare_order_by();
443
			$this->prepare_limit();
444
445
			$this->is_query_ready = true;
446
		}
447
	}
448
449
	/**
450
	 * Prepare the select statement.
451
	 *
452
	 * @since 2.1.0
453
	 */
454
	protected function prepare_select() {
455
456
		$all_fields = array_keys( $this->columns );
457
		$fields = array();
458
459
		if ( ! empty( $this->args['fields'] ) ) {
460
461
			$fields = (array) $this->args['fields'];
462
			$diff   = array_diff( $fields, $all_fields );
463
			$fields = array_intersect( $all_fields, $fields );
464
465
			if ( ! empty( $diff ) ) {
466
				_doing_it_wrong( __METHOD__, esc_html( 'WordPoints Debug Error: invalid field(s) "' . implode( '", "', $diff ) . '" given' ), '1.0.0' );
467
			}
468
		}
469
470
		// Pull all fields by default.
471
		if ( empty( $fields ) ) {
472
			$fields = $all_fields;
473
		}
474
475
		$fields = implode( ', ', array_map( 'wordpoints_escape_mysql_identifier', $fields ) );
476
477
		$this->select = "SELECT {$fields}";
478
	}
479
480
	/**
481
	 * Validates a value against an array of sanitizing functions.
482
	 *
483
	 * @since 2.1.0
484
	 *
485
	 * @param mixed      $value      The value to validate.
486
	 * @param callable[] $validators The validators to validate it against.
487
	 *
488
	 * @return mixed The validated value, or false if invalid.
489
	 */
490
	protected function validate_value( $value, $validators ) {
491
492
		foreach ( $validators as $validator ) {
493
494
			$value = call_user_func_array( $validator, array( &$value ) );
495
496
			if ( false === $value ) {
497
				break;
498
			}
499
		}
500
501
		return $value;
502
	}
503
504
	/**
505
	 * Validates an array of values against an array of sanitizing functions.
506
	 *
507
	 * @since 2.1.0
508
	 *
509
	 * @param array      $values     The values to validate.
510
	 * @param callable[] $validators The validators to validate each value against.
511
	 *
512
	 * @return array The validated values, with any invalid ones removed.
513
	 */
514
	protected function validate_values( $values, $validators ) {
515
516
		foreach ( $values as $index => $value ) {
517
518
			$value = $this->validate_value( $value, $validators );
519
520
			if ( false === $value ) {
521
				unset( $values[ $index ] );
522
			}
523
		}
524
525
		return $values;
526
	}
527
528
	/**
529
	 * Validate an unsigned column.
530
	 *
531
	 * The value must be positive, zero-inclusive. We can't just use
532
	 * wordpoints_posint() because it is zero exclusive.
533
	 *
534
	 * @since 2.1.0
535
	 *
536
	 * @param mixed $value The value to validate.
537
	 *
538
	 * @return int|false The validated value or false.
539
	 */
540
	protected function validate_unsigned_column( $value ) {
541
542
		if ( false !== wordpoints_int( $value ) && $value >= 0 ) {
543
			return $value;
544
		}
545
546
		return false;
547
	}
548
549
	/**
550
	 * Get an array of validating/sanitizing functions for the values of a column.
551
	 *
552
	 * @since 2.1.0
553
	 *
554
	 * @param array $data The data for the column.
555
	 *
556
	 * @return callable[] The validation functions.
557
	 */
558
	protected function get_validators_for_column( $data ) {
559
560
		$validators = array();
561
562
		// Default validators for integer columns.
563
		if ( '%d' === $data['format'] ) {
564
			if ( ! empty( $data['unsigned'] ) ) {
565
				$validators[] = array( $this, 'validate_unsigned_column' );
566
			} else {
567
				$validators[] = 'wordpoints_int';
568
			}
569
		}
570
571
		return $validators;
572
	}
573
574
	/**
575
	 * Prepare the conditions for the WHERE clause for a column.
576
	 *
577
	 * @since 2.1.0
578
	 *
579
	 * @param string $column The column name.
580
	 * @param array  $data   The column data.
581
	 */
582
	protected function prepare_column_where( $column, $data ) {
583
584
		// If a single value has been supplied for the column, it takes precedence.
585
		if ( isset( $this->args[ $column ] ) ) {
586
			$this->prepare_column( $column, $data );
587
		} elseif ( isset( $this->args[ "{$column}__in" ] ) ) {
588
			$this->prepare_column__in( $column, $data );
589
		} elseif ( isset( $this->args[ "{$column}__not_in" ] ) ) {
590
			$this->prepare_column__in( $column, $data, 'NOT IN' );
591
		}
592
	}
593
594
	/**
595
	 * Prepare a single-value condition for the WHERE clause for a column.
596
	 *
597
	 * @since 2.1.0
598
	 *
599
	 * @param string $column The name of the column
600
	 * @param array  $data   The column data.
601
	 */
602
	protected function prepare_column( $column, $data ) {
603
604
		global $wpdb;
605
606
		if (
607
			isset( $data['values'] )
608
			&& ! in_array( $this->args[ $column ], $data['values'], true )
609
		) {
610
			return;
611
		}
612
613
		$value = $this->validate_value(
614
			$this->args[ $column ]
615
			, $this->get_validators_for_column( $data )
616
		);
617
618
		if ( false === $value ) {
619
			return;
620
		}
621
622
		$compare = $this->get_comparator_for_column( $column, $data );
623
624
		$column = wordpoints_escape_mysql_identifier( $column );
625
626
		$this->wheres[] = $wpdb->prepare( // WPCS: unprepared SQL OK.
627
			"{$column} {$compare} {$data['format']}"
628
			, $value
629
		);
630
	}
631
632
	/**
633
	 * Get the comparator for a column.
634
	 *
635
	 * @since 2.1.0
636
	 *
637
	 * @param string $column The column name.
638
	 * @param array  $data   The column data.
639
	 *
640
	 * @return string The comparator for the column.
641
	 */
642
	protected function get_comparator_for_column( $column, $data ) {
643
644
		$comparisons = array( '=', '<', '>', '<>', '!=', '<=', '>=' );
645
646
		// MySQL doesn't support LIKE and NOT LIKE for int columns.
647
		// See https://stackoverflow.com/q/8422455/1924128
648
		if ( '%s' === $data['format'] ) {
649
			$comparisons = array_merge( $comparisons, array( 'LIKE', 'NOT LIKE' ) );
650
		}
651
652
		$comparator = '=';
653
654
		if (
655
			isset( $this->args[ "{$column}__compare" ] )
656
			&& in_array( $this->args[ "{$column}__compare" ], $comparisons, true )
657
		) {
658
			$comparator = $this->args[ "{$column}__compare" ];
659
		}
660
661
		return $comparator;
662
	}
663
664
	/**
665
	 * Prepare the IN or NOT IN conditions for a column.
666
	 *
667
	 * @since 2.1.0
668
	 *
669
	 * @param string $column The name of the column.
670
	 * @param array  $data   The column data.
671
	 * @param string $type   The type of IN clause, IN or NOT IN.
672
	 */
673
	protected function prepare_column__in( $column, $data, $type = 'IN' ) {
674
675
		$key = "{$column}__" . strtolower( str_replace( ' ', '_', $type ) );
676
677
		if ( empty( $this->args[ $key ] ) || ! is_array( $this->args[ $key ] ) ) {
678
			return;
679
		}
680
681
		$values = $this->args[ $key ];
682
683
		if ( isset( $data['values'] ) ) {
684
			$values = array_intersect( $values, $data['values'] );
685
		} else {
686
			$values = $this->validate_values(
687
				$values
688
				, $this->get_validators_for_column( $data )
689
			);
690
		}
691
692
		if ( empty( $values ) ) {
693
			return;
694
		}
695
696
		$in = wordpoints_prepare__in( $values, $data['format'] );
697
698
		if ( false === $in ) {
699
			return;
700
		}
701
702
		$column = wordpoints_escape_mysql_identifier( $column );
703
704
		$this->wheres[] = "{$column} {$type} ({$in})";
705
	}
706
707
	/**
708
	 * Prepare the WHERE clause for the query.
709
	 *
710
	 * @since 2.1.0
711
	 */
712
	protected function prepare_where() {
713
714
		$this->wheres = array();
715
716
		foreach ( $this->columns as $column => $data ) {
717
718
			if ( ! empty( $data['is_date'] ) ) {
719
				$this->prepare_date_where( $column );
720
			} else {
721
				$this->prepare_column_where( $column, $data );
722
			}
723
		}
724
725
		$this->prepare_meta_where();
726
727
		if ( ! empty( $this->wheres ) ) {
728
			$this->where = 'WHERE ' . implode( ' AND ', $this->wheres ) . "\n";
729
		}
730
	}
731
732
	/**
733
	 * Prepare the LIMIT clause for the query.
734
	 *
735
	 * @since 2.1.0
736
	 */
737
	protected function prepare_limit() {
738
739
		// MySQL doesn't allow for the offset without a limit, so if no limit is set
740
		// we can ignore the start arg. See https://stackoverflow.com/a/271650/1924128
741
		if ( ! isset( $this->args['limit'] ) ) {
742
			return;
743
		}
744
745
		foreach ( array( 'limit', 'start' ) as $key ) {
746
747
			// Save a backup of the arg value since wordpoints_int() is by reference.
748
			$arg = $this->args[ $key ];
749
750
			if ( false === wordpoints_int( $this->args[ $key ] ) ) {
751
752
				_doing_it_wrong(
753
					__METHOD__
754
					, sprintf(
755
						"WordPoints Debug Error: '%s' must be a positive integer, %s given"
756
						, esc_html( $key )
757
						, esc_html( strval( $arg ) ? $arg : gettype( $arg ) )
758
					)
759
					, '1.0.0'
760
				);
761
762
				$this->args[ $key ] = 0;
763
			}
764
		}
765
766
		if ( $this->args['limit'] > 0 && $this->args['start'] >= 0 ) {
767
			$this->limit = "LIMIT {$this->args['start']}, {$this->args['limit']}";
768
		}
769
	}
770
771
	/**
772
	 * Prepare the ORDER BY clause for the query.
773
	 *
774
	 * @since 2.1.0
775
	 */
776
	protected function prepare_order_by() {
777
778
		if ( empty( $this->args['order_by'] ) ) {
779
			return;
780
		}
781
782
		$order    = $this->args['order'];
783
		$order_by = $this->args['order_by'];
784
785
		if ( ! in_array( $order, array( 'DESC', 'ASC' ) ) ) {
786
787
			_doing_it_wrong( __METHOD__, esc_html( "WordPoints Debug Error: invalid 'order' \"{$order}\", possible values are DESC and ASC" ), '1.0.0' );
788
			$order = 'DESC';
789
		}
790
791
		if ( 'meta_value' === $order_by ) {
792
793
			global $wpdb;
794
795
			$meta_table_name = wordpoints_escape_mysql_identifier(
796
				$wpdb->{"{$this->meta_type}meta"}
797
			);
798
799
			if ( isset( $this->args['meta_type'] ) ) {
800
801
				$meta_type = $this->meta_query->get_cast_for_type( $this->args['meta_type'] );
802
				$order_by  = "CAST({$meta_table_name}.meta_value AS {$meta_type})";
803
804
			} else {
805
806
				$order_by = "{$meta_table_name}.meta_value";
807
			}
808
809
		} elseif ( isset( $this->columns[ $order_by ] ) ) {
810
811
			$order_by = wordpoints_escape_mysql_identifier( $order_by );
812
813
		} else {
814
815
			_doing_it_wrong( __METHOD__, esc_html( "WordPoints Debug Error: invalid 'order_by' \"{$order_by}\", possible values are " . implode( ', ', array_keys( $this->columns ) ) ), '1.0.0' );
816
			return;
817
		}
818
819
		$this->order = "ORDER BY {$order_by} {$order}\n";
820
	}
821
822
	/**
823
	 * Prepare the date query for a column.
824
	 *
825
	 * @since 2.1.0
826
	 *
827
	 * @param string $column The name of the column.
828
	 */
829
	protected function prepare_date_where( $column ) {
830
831
		if (
832
			empty( $this->args[ "{$column}_query" ] )
833
			|| ! is_array( $this->args[ "{$column}_query" ] )
834
		) {
835
			return;
836
		}
837
838
		add_filter( 'date_query_valid_columns', array( $this, 'date_query_valid_columns_filter' ) );
839
840
		$date_query = new WP_Date_Query( $this->args[ "{$column}_query" ], $column );
841
		$date_query = $date_query->get_sql();
842
843
		if ( ! empty( $date_query ) ) {
844
			$this->wheres[] = ltrim( $date_query, ' AND' );
845
		}
846
847
		remove_filter( 'date_query_valid_columns', array( $this, 'date_query_valid_columns_filter' ) );
848
	}
849
850
	/**
851
	 * Prepare the meta query.
852
	 *
853
	 * @since 2.1.0
854
	 */
855
	protected function prepare_meta_where() {
856
857
		if ( empty( $this->meta_type ) ) {
858
			return;
859
		}
860
861
		$meta_args = array_intersect_key(
862
			$this->args
863
			, array(
864
				'meta_key'     => '',
865
				'meta_value'   => '',
866
				'meta_compare' => '',
867
				'meta_type'    => '',
868
				'meta_query'   => '',
869
			)
870
		);
871
872
		if ( empty( $meta_args ) ) {
873
			return;
874
		}
875
876
		$this->meta_query = new WP_Meta_Query();
877
		$this->meta_query->parse_query_vars( $meta_args );
878
879
		$meta_query = $this->meta_query->get_sql(
880
			$this->meta_type
881
			, $this->table_name
882
			, 'id'
883
			, $this
884
		);
885
886
		if ( ! empty( $meta_query['where'] ) ) {
887
			$this->wheres[] = ltrim( $meta_query['where'], ' AND' );
888
		}
889
890
		$this->meta_join = $meta_query['join'] . "\n";
891
	}
892
}
893
894
// EOF
895