WP_Meta_Query::get_sql()   B
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 37
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 12
c 1
b 0
f 0
nc 3
nop 4
dl 0
loc 37
rs 8.8571
1
<?php
2
/**
3
 * Meta API: WP_Meta_Query class
4
 *
5
 * @package WordPress
6
 * @subpackage Meta
7
 * @since 4.4.0
8
 */
9
10
/**
11
 * Core class used to implement meta queries for the Meta API.
12
 *
13
 * Used for generating SQL clauses that filter a primary query according to metadata keys and values.
14
 *
15
 * WP_Meta_Query is a helper that allows primary query classes, such as WP_Query and WP_User_Query,
16
 *
17
 * to filter their results by object metadata, by generating `JOIN` and `WHERE` subclauses to be attached
18
 * to the primary SQL query string.
19
 *
20
 * @since 3.2.0
21
 * @package WordPress
22
 * @subpackage Meta
23
 */
24
class WP_Meta_Query {
25
	/**
26
	 * Array of metadata queries.
27
	 *
28
	 * See WP_Meta_Query::__construct() for information on meta query arguments.
29
	 *
30
	 * @since 3.2.0
31
	 * @access public
32
	 * @var array
33
	 */
34
	public $queries = array();
35
36
	/**
37
	 * The relation between the queries. Can be one of 'AND' or 'OR'.
38
	 *
39
	 * @since 3.2.0
40
	 * @access public
41
	 * @var string
42
	 */
43
	public $relation;
44
45
	/**
46
	 * Database table to query for the metadata.
47
	 *
48
	 * @since 4.1.0
49
	 * @access public
50
	 * @var string
51
	 */
52
	public $meta_table;
53
54
	/**
55
	 * Column in meta_table that represents the ID of the object the metadata belongs to.
56
	 *
57
	 * @since 4.1.0
58
	 * @access public
59
	 * @var string
60
	 */
61
	public $meta_id_column;
62
63
	/**
64
	 * Database table that where the metadata's objects are stored (eg $wpdb->users).
65
	 *
66
	 * @since 4.1.0
67
	 * @access public
68
	 * @var string
69
	 */
70
	public $primary_table;
71
72
	/**
73
	 * Column in primary_table that represents the ID of the object.
74
	 *
75
	 * @since 4.1.0
76
	 * @access public
77
	 * @var string
78
	 */
79
	public $primary_id_column;
80
81
	/**
82
	 * A flat list of table aliases used in JOIN clauses.
83
	 *
84
	 * @since 4.1.0
85
	 * @access protected
86
	 * @var array
87
	 */
88
	protected $table_aliases = array();
89
90
	/**
91
	 * A flat list of clauses, keyed by clause 'name'.
92
	 *
93
	 * @since 4.2.0
94
	 * @access protected
95
	 * @var array
96
	 */
97
	protected $clauses = array();
98
99
	/**
100
	 * Whether the query contains any OR relations.
101
	 *
102
	 * @since 4.3.0
103
	 * @access protected
104
	 * @var bool
105
	 */
106
	protected $has_or_relation = false;
107
108
	/**
109
	 * @since 4.7.0
110
	 * @access protected
111
	 * @var wpdb
112
	 */
113
	protected $db;
114
115
	/**
116
	 * Constructor.
117
	 *
118
	 * @since 3.2.0
119
	 * @since 4.2.0 Introduced support for naming query clauses by associative array keys.
120
	 *
121
	 * @access public
122
	 *
123
	 * @param array $meta_query {
124
	 *     Array of meta query clauses. When first-order clauses or sub-clauses use strings as
125
	 *     their array keys, they may be referenced in the 'orderby' parameter of the parent query.
126
	 *
127
	 *     @type string $relation Optional. The MySQL keyword used to join
128
	 *                            the clauses of the query. Accepts 'AND', or 'OR'. Default 'AND'.
129
	 *     @type array {
130
	 *         Optional. An array of first-order clause parameters, or another fully-formed meta query.
131
	 *
132
	 *         @type string $key     Meta key to filter by.
133
	 *         @type string $value   Meta value to filter by.
134
	 *         @type string $compare MySQL operator used for comparing the $value. Accepts '=',
135
	 *                               '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE',
136
	 *                               'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'REGEXP',
137
	 *                               'NOT REGEXP', 'RLIKE', 'EXISTS' or 'NOT EXISTS'.
138
	 *                               Default is 'IN' when `$value` is an array, '=' otherwise.
139
	 *         @type string $type    MySQL data type that the meta_value column will be CAST to for
140
	 *                               comparisons. Accepts 'NUMERIC', 'BINARY', 'CHAR', 'DATE',
141
	 *                               'DATETIME', 'DECIMAL', 'SIGNED', 'TIME', or 'UNSIGNED'.
142
	 *                               Default is 'CHAR'.
143
	 *     }
144
	 * }
145
	 */
146
	public function __construct( $meta_query = false ) {
147
		$this->db = $GLOBALS['wpdb'];
148
149
		if ( ! $meta_query ) {
150
			return;
151
		}
152
153 View Code Duplication
		if ( isset( $meta_query['relation'] ) && strtoupper( $meta_query['relation'] ) == 'OR' ) {
154
			$this->relation = 'OR';
155
		} else {
156
			$this->relation = 'AND';
157
		}
158
159
		$this->queries = $this->sanitize_query( $meta_query );
160
	}
161
162
	/**
163
	 * Ensure the 'meta_query' argument passed to the class constructor is well-formed.
164
	 *
165
	 * Eliminates empty items and ensures that a 'relation' is set.
166
	 *
167
	 * @since 4.1.0
168
	 * @access public
169
	 *
170
	 * @param array $queries Array of query clauses.
171
	 * @return array Sanitized array of query clauses.
172
	 */
173
	public function sanitize_query( $queries ) {
174
		$clean_queries = array();
175
176
		if ( ! is_array( $queries ) ) {
177
			return $clean_queries;
178
		}
179
180
		foreach ( $queries as $key => $query ) {
181
			if ( 'relation' === $key ) {
182
				$relation = $query;
183
184
			} elseif ( ! is_array( $query ) ) {
185
				continue;
186
187
			// First-order clause.
188
			} elseif ( $this->is_first_order_clause( $query ) ) {
189
				if ( isset( $query['value'] ) && array() === $query['value'] ) {
190
					unset( $query['value'] );
191
				}
192
193
				$clean_queries[ $key ] = $query;
194
195
			// Otherwise, it's a nested query, so we recurse.
196
			} else {
197
				$cleaned_query = $this->sanitize_query( $query );
198
199
				if ( ! empty( $cleaned_query ) ) {
200
					$clean_queries[ $key ] = $cleaned_query;
201
				}
202
			}
203
		}
204
205
		if ( empty( $clean_queries ) ) {
206
			return $clean_queries;
207
		}
208
209
		// Sanitize the 'relation' key provided in the query.
210
		if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) {
211
			$clean_queries['relation'] = 'OR';
212
			$this->has_or_relation = true;
213
214
		/*
215
		 * If there is only a single clause, call the relation 'OR'.
216
		 * This value will not actually be used to join clauses, but it
217
		 * simplifies the logic around combining key-only queries.
218
		 */
219
		} elseif ( 1 === count( $clean_queries ) ) {
220
			$clean_queries['relation'] = 'OR';
221
222
		// Default to AND.
223
		} else {
224
			$clean_queries['relation'] = 'AND';
225
		}
226
227
		return $clean_queries;
228
	}
229
230
	/**
231
	 * Determine whether a query clause is first-order.
232
	 *
233
	 * A first-order meta query clause is one that has either a 'key' or
234
	 * a 'value' array key.
235
	 *
236
	 * @since 4.1.0
237
	 * @access protected
238
	 *
239
	 * @param array $query Meta query arguments.
240
	 * @return bool Whether the query clause is a first-order clause.
241
	 */
242
	protected function is_first_order_clause( $query ) {
243
		return isset( $query['key'] ) || isset( $query['value'] );
244
	}
245
246
	/**
247
	 * Constructs a meta query based on 'meta_*' query vars
248
	 *
249
	 * @since 3.2.0
250
	 * @access public
251
	 *
252
	 * @param array $qv The query variables
253
	 */
254
	public function parse_query_vars( $qv ) {
255
		$meta_query = array();
256
257
		/*
258
		 * For orderby=meta_value to work correctly, simple query needs to be
259
		 * first (so that its table join is against an unaliased meta table) and
260
		 * needs to be its own clause (so it doesn't interfere with the logic of
261
		 * the rest of the meta_query).
262
		 */
263
		$primary_meta_query = array();
264
		foreach ( array( 'key', 'compare', 'type' ) as $key ) {
265
			if ( ! empty( $qv[ "meta_$key" ] ) ) {
266
				$primary_meta_query[ $key ] = $qv[ "meta_$key" ];
267
			}
268
		}
269
270
		// WP_Query sets 'meta_value' = '' by default.
271
		if ( isset( $qv['meta_value'] ) && '' !== $qv['meta_value'] && ( ! is_array( $qv['meta_value'] ) || $qv['meta_value'] ) ) {
272
			$primary_meta_query['value'] = $qv['meta_value'];
273
		}
274
275
		$existing_meta_query = isset( $qv['meta_query'] ) && is_array( $qv['meta_query'] ) ? $qv['meta_query'] : array();
276
277
		if ( ! empty( $primary_meta_query ) && ! empty( $existing_meta_query ) ) {
278
			$meta_query = array(
279
				'relation' => 'AND',
280
				$primary_meta_query,
281
				$existing_meta_query,
282
			);
283
		} elseif ( ! empty( $primary_meta_query ) ) {
284
			$meta_query = array(
285
				$primary_meta_query,
286
			);
287
		} elseif ( ! empty( $existing_meta_query ) ) {
288
			$meta_query = $existing_meta_query;
289
		}
290
291
		$this->__construct( $meta_query );
292
	}
293
294
	/**
295
	 * Return the appropriate alias for the given meta type if applicable.
296
	 *
297
	 * @since 3.7.0
298
	 * @access public
299
	 *
300
	 * @param string $type MySQL type to cast meta_value.
301
	 * @return string MySQL type.
302
	 */
303
	public function get_cast_for_type( $type = '' ) {
304
		if ( empty( $type ) )
305
			return 'CHAR';
306
307
		$meta_type = strtoupper( $type );
308
309
		if ( ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $meta_type ) )
310
			return 'CHAR';
311
312
		if ( 'NUMERIC' == $meta_type )
313
			$meta_type = 'SIGNED';
314
315
		return $meta_type;
316
	}
317
318
	/**
319
	 * Generates SQL clauses to be appended to a main query.
320
	 *
321
	 * @since 3.2.0
322
	 * @access public
323
	 *
324
	 * @param string $type              Type of meta, eg 'user', 'post'.
325
	 * @param string $primary_table     Database table where the object being filtered is stored (eg wp_users).
326
	 * @param string $primary_id_column ID column for the filtered object in $primary_table.
327
	 * @param object $context           Optional. The main query object.
328
	 * @return false|array {
329
	 *     Array containing JOIN and WHERE SQL clauses to append to the main query.
330
	 *
331
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
332
	 *     @type string $where SQL fragment to append to the main WHERE clause.
333
	 * }
334
	 */
335
	public function get_sql( $type, $primary_table, $primary_id_column, $context = null ) {
336
		if ( ! $meta_table = _get_meta_table( $type ) ) {
337
			return false;
338
		}
339
340
		$this->table_aliases = array();
341
342
		$this->meta_table     = $meta_table;
343
		$this->meta_id_column = sanitize_key( $type . '_id' );
344
345
		$this->primary_table     = $primary_table;
346
		$this->primary_id_column = $primary_id_column;
347
348
		$sql = $this->get_sql_clauses();
349
350
		/*
351
		 * If any JOINs are LEFT JOINs (as in the case of NOT EXISTS), then all JOINs should
352
		 * be LEFT. Otherwise posts with no metadata will be excluded from results.
353
		 */
354
		if ( false !== strpos( $sql['join'], 'LEFT JOIN' ) ) {
355
			$sql['join'] = str_replace( 'INNER JOIN', 'LEFT JOIN', $sql['join'] );
356
		}
357
358
		/**
359
		 * Filters the meta query's generated SQL.
360
		 *
361
		 * @since 3.1.0
362
		 *
363
		 * @param array  $clauses           Array containing the query's JOIN and WHERE clauses.
364
		 * @param array  $queries           Array of meta queries.
365
		 * @param string $type              Type of meta.
366
		 * @param string $primary_table     Primary table.
367
		 * @param string $primary_id_column Primary column ID.
368
		 * @param object $context           The main query object.
369
		 */
370
		return apply_filters_ref_array( 'get_meta_sql', array( $sql, $this->queries, $type, $primary_table, $primary_id_column, $context ) );
371
	}
372
373
	/**
374
	 * Generate SQL clauses to be appended to a main query.
375
	 *
376
	 * Called by the public WP_Meta_Query::get_sql(), this method is abstracted
377
	 * out to maintain parity with the other Query classes.
378
	 *
379
	 * @since 4.1.0
380
	 * @access protected
381
	 *
382
	 * @return array {
383
	 *     Array containing JOIN and WHERE SQL clauses to append to the main query.
384
	 *
385
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
386
	 *     @type string $where SQL fragment to append to the main WHERE clause.
387
	 * }
388
	 */
389 View Code Duplication
	protected function get_sql_clauses() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
390
		/*
391
		 * $queries are passed by reference to get_sql_for_query() for recursion.
392
		 * To keep $this->queries unaltered, pass a copy.
393
		 */
394
		$queries = $this->queries;
395
		$sql = $this->get_sql_for_query( $queries );
396
397
		if ( ! empty( $sql['where'] ) ) {
398
			$sql['where'] = ' AND ' . $sql['where'];
399
		}
400
401
		return $sql;
402
	}
403
404
	/**
405
	 * Generate SQL clauses for a single query array.
406
	 *
407
	 * If nested subqueries are found, this method recurses the tree to
408
	 * produce the properly nested SQL.
409
	 *
410
	 * @since 4.1.0
411
	 * @access protected
412
	 *
413
	 * @param array $query Query to parse, passed by reference.
414
	 * @param int   $depth Optional. Number of tree levels deep we currently are.
415
	 *                     Used to calculate indentation. Default 0.
416
	 * @return array {
417
	 *     Array containing JOIN and WHERE SQL clauses to append to a single query array.
418
	 *
419
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
420
	 *     @type string $where SQL fragment to append to the main WHERE clause.
421
	 * }
422
	 */
423 View Code Duplication
	protected function get_sql_for_query( &$query, $depth = 0 ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
424
		$sql_chunks = array(
425
			'join'  => array(),
426
			'where' => array(),
427
		);
428
429
		$sql = array(
430
			'join'  => '',
431
			'where' => '',
432
		);
433
434
		$indent = '';
435
		for ( $i = 0; $i < $depth; $i++ ) {
436
			$indent .= "  ";
437
		}
438
439
		foreach ( $query as $key => &$clause ) {
440
			if ( 'relation' === $key ) {
441
				$relation = $query['relation'];
442
			} elseif ( is_array( $clause ) ) {
443
444
				// This is a first-order clause.
445
				if ( $this->is_first_order_clause( $clause ) ) {
446
					$clause_sql = $this->get_sql_for_clause( $clause, $query, $key );
447
448
					$where_count = count( $clause_sql['where'] );
449
					if ( ! $where_count ) {
450
						$sql_chunks['where'][] = '';
451
					} elseif ( 1 === $where_count ) {
452
						$sql_chunks['where'][] = $clause_sql['where'][0];
453
					} else {
454
						$sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
455
					}
456
457
					$sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
458
				// This is a subquery, so we recurse.
459
				} else {
460
					$clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
461
462
					$sql_chunks['where'][] = $clause_sql['where'];
463
					$sql_chunks['join'][]  = $clause_sql['join'];
464
				}
465
			}
466
		}
467
468
		// Filter to remove empties.
469
		$sql_chunks['join']  = array_filter( $sql_chunks['join'] );
470
		$sql_chunks['where'] = array_filter( $sql_chunks['where'] );
471
472
		if ( empty( $relation ) ) {
473
			$relation = 'AND';
474
		}
475
476
		// Filter duplicate JOIN clauses and combine into a single string.
477
		if ( ! empty( $sql_chunks['join'] ) ) {
478
			$sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
479
		}
480
481
		// Generate a single WHERE clause with proper brackets and indentation.
482
		if ( ! empty( $sql_chunks['where'] ) ) {
483
			$sql['where'] = '( ' . "\n  " . $indent . implode( ' ' . "\n  " . $indent . $relation . ' ' . "\n  " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')';
484
		}
485
486
		return $sql;
487
	}
488
489
	/**
490
	 * Generate SQL JOIN and WHERE clauses for a first-order query clause.
491
	 *
492
	 * "First-order" means that it's an array with a 'key' or 'value'.
493
	 *
494
	 * @since 4.1.0
495
	 * @access public
496
	 *
497
	 * @param array  $clause       Query clause, passed by reference.
498
	 * @param array  $parent_query Parent query array.
499
	 * @param string $clause_key   Optional. The array key used to name the clause in the original `$meta_query`
500
	 *                             parameters. If not provided, a key will be generated automatically.
501
	 * @return array {
502
	 *     Array containing JOIN and WHERE SQL clauses to append to a first-order query.
503
	 *
504
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
505
	 *     @type string $where SQL fragment to append to the main WHERE clause.
506
	 * }
507
	 */
508
	public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' ) {
509
		$sql_chunks = array(
510
			'where' => array(),
511
			'join' => array(),
512
		);
513
514
		if ( isset( $clause['compare'] ) ) {
515
			$clause['compare'] = strtoupper( $clause['compare'] );
516
		} else {
517
			$clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '=';
518
		}
519
520
		if ( ! in_array( $clause['compare'], array(
521
			'=', '!=', '>', '>=', '<', '<=',
522
			'LIKE', 'NOT LIKE',
523
			'IN', 'NOT IN',
524
			'BETWEEN', 'NOT BETWEEN',
525
			'EXISTS', 'NOT EXISTS',
526
			'REGEXP', 'NOT REGEXP', 'RLIKE'
527
		) ) ) {
528
			$clause['compare'] = '=';
529
		}
530
531
		$meta_compare = $clause['compare'];
532
533
		// First build the JOIN clause, if one is required.
534
		$join = '';
535
536
		// We prefer to avoid joins if possible. Look for an existing join compatible with this clause.
537
		$alias = $this->find_compatible_table_alias( $clause, $parent_query );
538
		if ( false === $alias ) {
539
			$i = count( $this->table_aliases );
540
			$alias = $i ? 'mt' . $i : $this->meta_table;
541
542
			// JOIN clauses for NOT EXISTS have their own syntax.
543
			if ( 'NOT EXISTS' === $meta_compare ) {
544
				$join .= " LEFT JOIN $this->meta_table";
545
				$join .= $i ? " AS $alias" : '';
546
				$join .= $this->db->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key = %s )", $clause['key'] );
547
548
			// All other JOIN clauses.
549
			} else {
550
				$join .= " INNER JOIN $this->meta_table";
551
				$join .= $i ? " AS $alias" : '';
552
				$join .= " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column )";
553
			}
554
555
			$this->table_aliases[] = $alias;
556
			$sql_chunks['join'][] = $join;
557
		}
558
559
		// Save the alias to this clause, for future siblings to find.
560
		$clause['alias'] = $alias;
561
562
		// Determine the data type.
563
		$_meta_type = isset( $clause['type'] ) ? $clause['type'] : '';
564
		$meta_type  = $this->get_cast_for_type( $_meta_type );
0 ignored issues
show
Bug introduced by
It seems like $_meta_type defined by isset($clause['type']) ? $clause['type'] : '' on line 563 can also be of type boolean; however, WP_Meta_Query::get_cast_for_type() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
565
		$clause['cast'] = $meta_type;
566
567
		// Fallback for clause keys is the table alias. Key must be a string.
568
		if ( is_int( $clause_key ) || ! $clause_key ) {
569
			$clause_key = $clause['alias'];
570
		}
571
572
		// Ensure unique clause keys, so none are overwritten.
573
		$iterator = 1;
574
		$clause_key_base = $clause_key;
575
		while ( isset( $this->clauses[ $clause_key ] ) ) {
576
			$clause_key = $clause_key_base . '-' . $iterator;
577
			$iterator++;
578
		}
579
580
		// Store the clause in our flat array.
581
		$this->clauses[ $clause_key ] =& $clause;
582
583
		// Next, build the WHERE clause.
584
585
		// meta_key.
586
		if ( array_key_exists( 'key', $clause ) ) {
587
			if ( 'NOT EXISTS' === $meta_compare ) {
588
				$sql_chunks['where'][] = $alias . '.' . $this->meta_id_column . ' IS NULL';
589
			} else {
590
				$sql_chunks['where'][] = $this->db->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) );
591
			}
592
		}
593
594
		// meta_value.
595
		if ( array_key_exists( 'value', $clause ) ) {
596
			$meta_value = $clause['value'];
597
598
			if ( in_array( $meta_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
599
				if ( ! is_array( $meta_value ) ) {
600
					$meta_value = preg_split( '/[,\s]+/', $meta_value );
601
				}
602
			} else {
603
				$meta_value = trim( $meta_value );
604
			}
605
606
			switch ( $meta_compare ) {
607
				case 'IN' :
608
				case 'NOT IN' :
609
					$meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')';
610
					$where = $this->db->prepare( $meta_compare_string, $meta_value );
611
					break;
612
613
				case 'BETWEEN' :
614
				case 'NOT BETWEEN' :
615
					$meta_value = array_slice( $meta_value, 0, 2 );
616
					$where = $this->db->prepare( '%s AND %s', $meta_value );
617
					break;
618
619
				case 'LIKE' :
620
				case 'NOT LIKE' :
621
					$meta_value = '%' . $this->db->esc_like( $meta_value ) . '%';
0 ignored issues
show
Bug introduced by
It seems like $meta_value can also be of type array; however, wpdb::esc_like() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
622
					$where = $this->db->prepare( '%s', $meta_value );
623
					break;
624
625
				// EXISTS with a value is interpreted as '='.
626
				case 'EXISTS' :
627
					$meta_compare = '=';
628
					$where = $this->db->prepare( '%s', $meta_value );
629
					break;
630
631
				// 'value' is ignored for NOT EXISTS.
632
				case 'NOT EXISTS' :
633
					$where = '';
634
					break;
635
636
				default :
637
					$where = $this->db->prepare( '%s', $meta_value );
638
					break;
639
640
			}
641
642
			if ( $where ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $where of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
643
				if ( 'CHAR' === $meta_type ) {
644
					$sql_chunks['where'][] = "$alias.meta_value {$meta_compare} {$where}";
645
				} else {
646
					$sql_chunks['where'][] = "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$where}";
647
				}
648
			}
649
		}
650
651
		/*
652
		 * Multiple WHERE clauses (for meta_key and meta_value) should
653
		 * be joined in parentheses.
654
		 */
655
		if ( 1 < count( $sql_chunks['where'] ) ) {
656
			$sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' );
657
		}
658
659
		return $sql_chunks;
660
	}
661
662
	/**
663
	 * Get a flattened list of sanitized meta clauses.
664
	 *
665
	 * This array should be used for clause lookup, as when the table alias and CAST type must be determined for
666
	 * a value of 'orderby' corresponding to a meta clause.
667
	 *
668
	 * @since 4.2.0
669
	 * @access public
670
	 *
671
	 * @return array Meta clauses.
672
	 */
673
	public function get_clauses() {
674
		return $this->clauses;
675
	}
676
677
	/**
678
	 * Identify an existing table alias that is compatible with the current
679
	 * query clause.
680
	 *
681
	 * We avoid unnecessary table joins by allowing each clause to look for
682
	 * an existing table alias that is compatible with the query that it
683
	 * needs to perform.
684
	 *
685
	 * An existing alias is compatible if (a) it is a sibling of `$clause`
686
	 * (ie, it's under the scope of the same relation), and (b) the combination
687
	 * of operator and relation between the clauses allows for a shared table join.
688
	 * In the case of WP_Meta_Query, this only applies to 'IN' clauses that are
689
	 * connected by the relation 'OR'.
690
	 *
691
	 * @since 4.1.0
692
	 * @access protected
693
	 *
694
	 * @param  array       $clause       Query clause.
695
	 * @param  array       $parent_query Parent query of $clause.
696
	 * @return string|bool Table alias if found, otherwise false.
697
	 */
698
	protected function find_compatible_table_alias( $clause, $parent_query ) {
699
		$alias = false;
700
701
		foreach ( $parent_query as $sibling ) {
702
			// If the sibling has no alias yet, there's nothing to check.
703
			if ( empty( $sibling['alias'] ) ) {
704
				continue;
705
			}
706
707
			// We're only interested in siblings that are first-order clauses.
708
			if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) {
709
				continue;
710
			}
711
712
			$compatible_compares = array();
713
714
			// Clauses connected by OR can share joins as long as they have "positive" operators.
715
			if ( 'OR' === $parent_query['relation'] ) {
716
				$compatible_compares = array( '=', 'IN', 'BETWEEN', 'LIKE', 'REGEXP', 'RLIKE', '>', '>=', '<', '<=' );
717
718
			// Clauses joined by AND with "negative" operators share a join only if they also share a key.
719
			} elseif ( isset( $sibling['key'] ) && isset( $clause['key'] ) && $sibling['key'] === $clause['key'] ) {
720
				$compatible_compares = array( '!=', 'NOT IN', 'NOT LIKE' );
721
			}
722
723
			$clause_compare  = strtoupper( $clause['compare'] );
724
			$sibling_compare = strtoupper( $sibling['compare'] );
725
			if ( in_array( $clause_compare, $compatible_compares ) && in_array( $sibling_compare, $compatible_compares ) ) {
726
				$alias = $sibling['alias'];
727
				break;
728
			}
729
		}
730
731
		/**
732
		 * Filters the table alias identified as compatible with the current clause.
733
		 *
734
		 * @since 4.1.0
735
		 *
736
		 * @param string|bool $alias        Table alias, or false if none was found.
737
		 * @param array       $clause       First-order query clause.
738
		 * @param array       $parent_query Parent of $clause.
739
		 * @param object      $this         WP_Meta_Query object.
740
		 */
741
		return apply_filters( 'meta_query_find_compatible_table_alias', $alias, $clause, $parent_query, $this ) ;
742
	}
743
744
	/**
745
	 * Checks whether the current query has any OR relations.
746
	 *
747
	 * In some cases, the presence of an OR relation somewhere in the query will require
748
	 * the use of a `DISTINCT` or `GROUP BY` keyword in the `SELECT` clause. The current
749
	 * method can be used in these cases to determine whether such a clause is necessary.
750
	 *
751
	 * @since 4.3.0
752
	 *
753
	 * @return bool True if the query contains any `OR` relations, otherwise false.
754
	 */
755
	public function has_or_relation() {
756
		return $this->has_or_relation;
757
	}
758
}
759