Completed
Push — master ( 6e1f17...7db120 )
by Brian
40s queued 38s
created

GetPaid_Subscriptions_Query   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 478
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 60
eloc 148
c 1
b 0
f 0
dl 0
loc 478
rs 3.6

13 Methods

Rating   Name   Duplication   Size   Complexity  
A set() 0 2 1
B prepare_query_fields() 0 19 7
A parse_order() 0 9 4
B query() 0 27 9
C prepare_query_where() 0 45 12
A get_results() 0 2 1
A fill_query_vars() 0 19 1
A get_total() 0 2 1
B prepare_query_order() 0 41 7
A parse_orderby() 0 14 5
B prepare_query() 0 39 8
A get() 0 6 2
A __construct() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like GetPaid_Subscriptions_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 GetPaid_Subscriptions_Query, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * GetPaid_Subscriptions_Query class
4
 *
5
 * Contains core class used to query for subscriptions.
6
 *
7
 * @since 1.0.19
8
 */
9
10
/**
11
 * Main class used for querying subscriptions.
12
 *
13
 * @since 1.0.19
14
 *
15
 * @see GetPaid_Subscriptions_Query::prepare_query() for information on accepted arguments.
16
 */
17
class GetPaid_Subscriptions_Query {
18
19
	/**
20
	 * Query vars, after parsing
21
	 *
22
	 * @since 1.0.19
23
	 * @var array
24
	 */
25
	public $query_vars = array();
26
27
	/**
28
	 * List of found subscriptions.
29
	 *
30
	 * @since 1.0.19
31
	 * @var array
32
	 */
33
	private $results;
34
35
	/**
36
	 * Total number of found subscriptions for the current query
37
	 *
38
	 * @since 1.0.19
39
	 * @var int
40
	 */
41
	private $total_subscriptions = 0;
42
43
	/**
44
	 * The SQL query used to fetch matching subscriptions.
45
	 *
46
	 * @since 1.0.19
47
	 * @var string
48
	 */
49
	public $request;
50
51
	// SQL clauses
52
53
	/**
54
	 * Contains the 'FIELDS' sql clause
55
	 *
56
	 * @since 1.0.19
57
	 * @var string
58
	 */
59
	public $query_fields;
60
61
	/**
62
	 * Contains the 'FROM' sql clause
63
	 *
64
	 * @since 1.0.19
65
	 * @var string
66
	 */
67
	public $query_from;
68
69
	/**
70
	 * Contains the 'WHERE' sql clause
71
	 *
72
	 * @since 1.0.19
73
	 * @var string
74
	 */
75
	public $query_where;
76
77
	/**
78
	 * Contains the 'ORDER BY' sql clause
79
	 *
80
	 * @since 1.0.19
81
	 * @var string
82
	 */
83
	public $query_orderby;
84
85
	/**
86
	 * Contains the 'LIMIT' sql clause
87
	 *
88
	 * @since 1.0.19
89
	 * @var string
90
	 */
91
	public $query_limit;
92
93
	/**
94
	 * Class constructor.
95
	 *
96
	 * @since 1.0.19
97
	 *
98
	 * @param null|string|array $query Optional. The query variables.
99
	 */
100
	public function __construct( $query = null ) {
101
		if ( ! is_null( $query ) ) {
102
			$this->prepare_query( $query );
103
			$this->query();
104
		}
105
	}
106
107
	/**
108
	 * Fills in missing query variables with default values.
109
	 *
110
	 * @since 1.0.19
111
	 *
112
	 * @param array $args Query vars, as passed to `GetPaid_Subscriptions_Query`.
113
	 * @return array Complete query variables with undefined ones filled in with defaults.
114
	 */
115
	public static function fill_query_vars( $args ) {
116
		$defaults = array(
117
			'status'            => 'all',
118
			'customer_in'       => array(),
119
			'customer_not_in'   => array(),
120
			'product_in'        => array(),
121
			'product_not_in'    => array(),
122
			'include'           => array(),
123
			'exclude'           => array(),
124
			'orderby'           => 'id',
125
			'order'             => 'DESC',
126
			'offset'            => '',
127
			'number'            => 10,
128
			'paged'             => 1,
129
			'count_total'       => true,
130
			'fields'            => 'all',
131
		);
132
133
		return wp_parse_args( $args, $defaults );
134
	}
135
136
	/**
137
	 * Prepare the query variables.
138
	 *
139
	 * @since 1.0.19
140
	 *
141
	 * @global wpdb $wpdb WordPress database abstraction object.
142
	 *
143
	 * @param string|array $query {
144
	 *     Optional. Array or string of Query parameters.
145
	 *
146
	 *     @type string|array $status              The subscription status to filter by. Can either be a single status or an array of statuses.
147
	 *                                             Default is all.
148
	 *     @type int[]        $customer_in         An array of customer ids to filter by.
149
	 *     @type int[]        $customer_not_in     An array of customer ids whose subscriptions should be excluded.
150
	 *     @type int[]        $product_in          An array of product ids to filter by.
151
	 *     @type int[]        $product_not_in      An array of product ids whose subscriptions should be excluded.
152
	 *     @type array        $date_created_query  A WP_Date_Query compatible array use to filter subscriptions by their date of creation.
153
	 *     @type array        $date_expires_query  A WP_Date_Query compatible array use to filter subscriptions by their expiration date.
154
	 *     @type array        $include             An array of subscription IDs to include. Default empty array.
155
	 *     @type array        $exclude             An array of subscription IDs to exclude. Default empty array.
156
	 *     @type string|array $orderby             Field(s) to sort the retrieved subscription by. May be a single value,
157
	 *                                             an array of values, or a multi-dimensional array with fields as
158
	 *                                             keys and orders ('ASC' or 'DESC') as values. Accepted values are
159
	 *                                             'id', 'customer_id', 'frequency', 'period', 'initial_amount,
160
	 *                                             'recurring_amount', 'bill_times', 'parent_payment_id', 'created', 'expiration'
161
	 *                                             'transaction_id', 'product_id', 'trial_period', 'include', 'status', 'profile_id'. Default array( 'id' ).
162
	 *     @type string       $order               Designates ascending or descending order of subscriptions. Order values
163
	 *                                             passed as part of an `$orderby` array take precedence over this
164
	 *                                             parameter. Accepts 'ASC', 'DESC'. Default 'DESC'.
165
	 *     @type int          $offset              Number of subscriptions to offset in retrieved results. Can be used in
166
	 *                                             conjunction with pagination. Default 0.
167
	 *     @type int          $number              Number of subscriptions to limit the query for. Can be used in
168
	 *                                             conjunction with pagination. Value -1 (all) is supported, but
169
	 *                                             should be used with caution on larger sites.
170
	 *                                             Default 10.
171
	 *     @type int          $paged               When used with number, defines the page of results to return.
172
	 *                                             Default 1.
173
	 *     @type bool         $count_total         Whether to count the total number of subscriptions found. If pagination
174
	 *                                             is not needed, setting this to false can improve performance.
175
	 *                                             Default true.
176
	 *     @type string|array $fields              Which fields to return. Single or all fields (string), or array
177
	 *                                             of fields. Accepts 'id', 'customer_id', 'frequency', 'period', 'initial_amount,
178
	 *                                             'recurring_amount', 'bill_times', 'parent_payment_id', 'created', 'expiration'
179
	 *                                             'transaction_id', 'product_id', 'trial_period', 'status', 'profile_id'.
180
	 *                                             Use 'all' for all fields. Default 'all'.
181
	 * }
182
	 */
183
	public function prepare_query( $query = array() ) {
184
		global $wpdb;
185
186
		if ( empty( $this->query_vars ) || ! empty( $query ) ) {
187
			$this->query_limit = null;
188
			$this->query_vars  = $this->fill_query_vars( $query );
0 ignored issues
show
Bug introduced by
It seems like $query can also be of type string; however, parameter $args of GetPaid_Subscriptions_Query::fill_query_vars() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

188
			$this->query_vars  = $this->fill_query_vars( /** @scrutinizer ignore-type */ $query );
Loading history...
189
		}
190
191
		if ( ! empty( $this->query_vars['fields'] ) && 'all' !== $this->query_vars['fields'] ) {
192
			$this->query_vars['fields'] = wpinv_parse_list( $this->query_vars['fields'] );
193
		}
194
195
		do_action( 'getpaid_pre_get_subscriptions', array( &$this ) );
196
197
		// Ensure that query vars are filled after 'getpaid_pre_get_subscriptions'.
198
		$qv                =& $this->query_vars;
199
		$qv                = $this->fill_query_vars( $qv );
200
		$table             = $wpdb->prefix . 'wpinv_subscriptions';
201
		$this->query_from  = "FROM $table";
202
203
		// Prepare query fields.
204
		$this->prepare_query_fields( $qv, $table );
205
206
		// Prepare query where.
207
		$this->prepare_query_where( $qv, $table );
208
209
		// Prepare query order.
210
		$this->prepare_query_order( $qv, $table );
211
212
		// limit
213
		if ( isset( $qv['number'] ) && $qv['number'] > 0 ) {
214
			if ( $qv['offset'] ) {
215
				$this->query_limit = $wpdb->prepare( 'LIMIT %d, %d', $qv['offset'], $qv['number'] );
216
			} else {
217
				$this->query_limit = $wpdb->prepare( 'LIMIT %d, %d', $qv['number'] * ( $qv['paged'] - 1 ), $qv['number'] );
218
			}
219
		}
220
221
		do_action_ref_array( 'getpaid_after_subscriptions_query', array( &$this ) );
222
	}
223
224
	/**
225
	 * Prepares the query fields.
226
	 *
227
	 * @since 1.0.19
228
	 *
229
	 * @param array $qv Query vars.
230
	 * @param string $table Table name.
231
	 */
232
	protected function prepare_query_fields( &$qv, $table ) {
233
234
		if ( is_array( $qv['fields'] ) ) {
235
			$qv['fields'] = array_unique( $qv['fields'] );
236
237
			$this->query_fields = array();
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type string of property $query_fields.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
238
			foreach ( $qv['fields'] as $field ) {
239
				$field                = 'id' === strtolower( $field ) ? 'id' : sanitize_key( $field );
240
				$this->query_fields[] = "$table.`$field`";
241
			}
242
			$this->query_fields = implode( ',', $this->query_fields );
243
		} elseif ( 'all' === $qv['fields'] ) {
244
			$this->query_fields = "$table.*";
245
		} else {
246
			$this->query_fields = "$table.id";
247
		}
248
249
		if ( isset( $qv['count_total'] ) && $qv['count_total'] ) {
250
			$this->query_fields = 'SQL_CALC_FOUND_ROWS ' . $this->query_fields;
251
		}
252
253
	}
254
255
	/**
256
	 * Prepares the query where.
257
	 *
258
	 * @since 1.0.19
259
	 *
260
	 * @param array $qv Query vars.
261
	 * @param string $table Table name.
262
	 */
263
	protected function prepare_query_where( &$qv, $table ) {
264
		global $wpdb;
265
		$this->query_where = 'WHERE 1=1';
266
267
		// Status.
268
		if ( 'all' !== $qv['status'] ) {
269
			$statuses           = wpinv_clean( wpinv_parse_list( $qv['status'] ) );
270
			$prepared_statuses  = join( ',', array_fill( 0, count( $statuses ), '%s' ) );
271
			$this->query_where .= $wpdb->prepare( " AND $table.`status` IN ( $prepared_statuses )", $statuses );
272
		}
273
274
		if ( ! empty( $qv['customer_in'] ) ) {
275
			$customer_in        = implode( ',', wp_parse_id_list( $qv['customer_in'] ) );
276
			$this->query_where .= " AND $table.`customer_id` IN ($customer_in)";
277
		} elseif ( ! empty( $qv['customer_not_in'] ) ) {
278
			$customer_not_in    = implode( ',', wp_parse_id_list( $qv['customer_not_in'] ) );
279
			$this->query_where .= " AND $table.`customer_id` NOT IN ($customer_not_in)";
280
		}
281
282
		if ( ! empty( $qv['product_in'] ) ) {
283
			$product_in         = implode( ',', wp_parse_id_list( $qv['product_in'] ) );
284
			$this->query_where .= " AND $table.`product_id` IN ($product_in)";
285
		} elseif ( ! empty( $qv['product_not_in'] ) ) {
286
			$product_not_in     = implode( ',', wp_parse_id_list( $qv['product_not_in'] ) );
287
			$this->query_where .= " AND $table.`product_id` NOT IN ($product_not_in)";
288
		}
289
290
		if ( ! empty( $qv['include'] ) ) {
291
			$include            = implode( ',', wp_parse_id_list( $qv['include'] ) );
292
			$this->query_where .= " AND $table.`id` IN ($include)";
293
		} elseif ( ! empty( $qv['exclude'] ) ) {
294
			$exclude            = implode( ',', wp_parse_id_list( $qv['exclude'] ) );
295
			$this->query_where .= " AND $table.`id` NOT IN ($exclude)";
296
		}
297
298
		// Date queries are allowed for the subscription creation date.
299
		if ( ! empty( $qv['date_created_query'] ) && is_array( $qv['date_created_query'] ) ) {
300
			$date_created_query = new WP_Date_Query( $qv['date_created_query'], "$table.created" );
301
			$this->query_where .= $date_created_query->get_sql();
302
		}
303
304
		// Date queries are also allowed for the subscription expiration date.
305
		if ( ! empty( $qv['date_expires_query'] ) && is_array( $qv['date_expires_query'] ) ) {
306
			$date_expires_query = new WP_Date_Query( $qv['date_expires_query'], "$table.expiration" );
307
			$this->query_where .= $date_expires_query->get_sql();
308
		}
309
310
	}
311
312
	/**
313
	 * Prepares the query order.
314
	 *
315
	 * @since 1.0.19
316
	 *
317
	 * @param array $qv Query vars.
318
	 * @param string $table Table name.
319
	 */
320
	protected function prepare_query_order( &$qv, $table ) {
321
322
		// sorting.
323
		$qv['order'] = isset( $qv['order'] ) ? strtoupper( $qv['order'] ) : '';
324
		$order       = $this->parse_order( $qv['order'] );
325
326
		// Default order is by 'id' (latest subscriptions).
327
		if ( empty( $qv['orderby'] ) ) {
328
			$ordersby = array( 'id' );
0 ignored issues
show
Unused Code introduced by
The assignment to $ordersby is dead and can be removed.
Loading history...
329
		}
330
331
		// 'orderby' values may be an array, comma- or space-separated list.
332
		$ordersby      = array_filter( wpinv_parse_list(  $qv['orderby'] ) );
333
334
		$orderby_array = array();
335
		foreach ( $ordersby as $_key => $_value ) {
336
337
			if ( is_int( $_key ) ) {
338
				// Integer key means this is a flat array of 'orderby' fields.
339
				$_orderby = $_value;
340
				$_order   = $order;
341
			} else {
342
				// Non-integer key means that the key is the field and the value is ASC/DESC.
343
				$_orderby = $_key;
344
				$_order   = $_value;
345
			}
346
347
			$parsed = $this->parse_orderby( $_orderby, $table );
348
349
			if ( $parsed ) {
350
				$orderby_array[] = $parsed . ' ' . $this->parse_order( $_order );
351
			}
352
353
		}
354
355
		// If no valid clauses were found, order by id.
356
		if ( empty( $orderby_array ) ) {
357
			$orderby_array[] = "id $order";
358
		}
359
360
		$this->query_orderby = 'ORDER BY ' . implode( ', ', $orderby_array );
361
362
	}
363
364
	/**
365
	 * Execute the query, with the current variables.
366
	 *
367
	 * @since 1.0.19
368
	 *
369
	 * @global wpdb $wpdb WordPress database abstraction object.
370
	 */
371
	public function query() {
372
		global $wpdb;
373
374
		$qv =& $this->query_vars;
375
376
		// Return a non-null value to bypass the default GetPaid subscriptions query and remember to set the
377
		// total_subscriptions property.
378
		$this->results = apply_filters_ref_array( 'getpaid_subscriptions_pre_query', array( null, &$this ) );
379
380
		if ( null === $this->results ) {
381
			$this->request = "SELECT $this->query_fields $this->query_from $this->query_where $this->query_orderby $this->query_limit";
382
383
			if ( ( is_array( $qv['fields'] ) && 1 != count( $qv['fields'] ) ) || 'all' == $qv['fields'] ) {
384
				$this->results = $wpdb->get_results( $this->request );
385
			} else {
386
				$this->results = $wpdb->get_col( $this->request );
387
			}
388
389
			if ( isset( $qv['count_total'] ) && $qv['count_total'] ) {
390
				$found_subscriptions_query = apply_filters( 'getpaid_found_subscriptions_query', 'SELECT FOUND_ROWS()', $this );
391
				$this->total_subscriptions   = (int) $wpdb->get_var( $found_subscriptions_query );
392
			}
393
		}
394
395
		if ( 'all' == $qv['fields'] ) {
396
			foreach ( $this->results as $key => $subscription ) {
397
				$this->results[ $key ] = new WPInv_Subscription( $subscription );
398
			}
399
		}
400
401
	}
402
403
	/**
404
	 * Retrieve query variable.
405
	 *
406
	 * @since 1.0.19
407
	 *
408
	 * @param string $query_var Query variable key.
409
	 * @return mixed
410
	 */
411
	public function get( $query_var ) {
412
		if ( isset( $this->query_vars[ $query_var ] ) ) {
413
			return $this->query_vars[ $query_var ];
414
		}
415
416
		return null;
417
	}
418
419
	/**
420
	 * Set query variable.
421
	 *
422
	 * @since 1.0.19
423
	 *
424
	 * @param string $query_var Query variable key.
425
	 * @param mixed $value Query variable value.
426
	 */
427
	public function set( $query_var, $value ) {
428
		$this->query_vars[ $query_var ] = $value;
429
	}
430
431
	/**
432
	 * Return the list of subscriptions.
433
	 *
434
	 * @since 1.0.19
435
	 *
436
	 * @return WPInv_Subscription[]|array Found subscriptions.
437
	 */
438
	public function get_results() {
439
		return $this->results;
440
	}
441
442
	/**
443
	 * Return the total number of subscriptions for the current query.
444
	 *
445
	 * @since 1.0.19
446
	 *
447
	 * @return int Number of total subscriptions.
448
	 */
449
	public function get_total() {
450
		return $this->total_subscriptions;
451
	}
452
453
	/**
454
	 * Parse and sanitize 'orderby' keys passed to the subscriptions query.
455
	 *
456
	 * @since 1.0.19
457
	 *
458
	 * @param string $orderby Alias for the field to order by.
459
	 *  @param string $table The current table.
460
	 * @return string Value to use in the ORDER clause, if `$orderby` is valid.
461
	 */
462
	protected function parse_orderby( $orderby, $table ) {
463
464
		$_orderby = '';
465
		if ( in_array( $orderby, array( 'customer_id', 'frequency', 'period', 'initial_amount', 'recurring_amount', 'bill_times', 'transaction_id', 'parent_payment_id', 'product_id', 'created', 'expiration', 'trial_period', 'status', 'profile_id' ) ) ) {
466
			$_orderby = "$table.`$orderby`";
467
		} elseif ( 'id' === strtolower( $orderby ) ) {
468
			$_orderby = "$table.id";
469
		} elseif ( 'include' === $orderby && ! empty( $this->query_vars['include'] ) ) {
470
			$include     = wp_parse_id_list( $this->query_vars['include'] );
471
			$include_sql = implode( ',', $include );
472
			$_orderby    = "FIELD( $table.id, $include_sql )";
473
		}
474
475
		return $_orderby;
476
	}
477
478
	/**
479
	 * Parse an 'order' query variable and cast it to ASC or DESC as necessary.
480
	 *
481
	 * @since 1.0.19
482
	 *
483
	 * @param string $order The 'order' query variable.
484
	 * @return string The sanitized 'order' query variable.
485
	 */
486
	protected function parse_order( $order ) {
487
		if ( ! is_string( $order ) || empty( $order ) ) {
0 ignored issues
show
introduced by
The condition is_string($order) is always true.
Loading history...
488
			return 'DESC';
489
		}
490
491
		if ( 'ASC' === strtoupper( $order ) ) {
492
			return 'ASC';
493
		} else {
494
			return 'DESC';
495
		}
496
	}
497
498
}
499