WP_Tax_Query

Core class used to implement taxonomy queries for the Taxonomy API.

Defined (1)

The class is defined in the following location(s).

/wp-includes/class-wp-tax-query.php  
  1. class WP_Tax_Query { 
  2.  
  3. /** 
  4. * Array of taxonomy queries. 
  5. * See WP_Tax_Query::__construct() for information on tax query arguments. 
  6. * @since 3.1.0 
  7. * @access public 
  8. * @var array 
  9. */ 
  10. public $queries = array(); 
  11.  
  12. /** 
  13. * The relation between the queries. Can be one of 'AND' or 'OR'. 
  14. * @since 3.1.0 
  15. * @access public 
  16. * @var string 
  17. */ 
  18. public $relation; 
  19.  
  20. /** 
  21. * Standard response when the query should not return any rows. 
  22. * @since 3.2.0 
  23. * @static 
  24. * @access private 
  25. * @var string 
  26. */ 
  27. private static $no_results = array( 'join' => array( '' ), 'where' => array( '0 = 1' ) ); 
  28.  
  29. /** 
  30. * A flat list of table aliases used in the JOIN clauses. 
  31. * @since 4.1.0 
  32. * @access protected 
  33. * @var array 
  34. */ 
  35. protected $table_aliases = array(); 
  36.  
  37. /** 
  38. * Terms and taxonomies fetched by this query. 
  39. * We store this data in a flat array because they are referenced in a 
  40. * number of places by WP_Query. 
  41. * @since 4.1.0 
  42. * @access public 
  43. * @var array 
  44. */ 
  45. public $queried_terms = array(); 
  46.  
  47. /** 
  48. * Database table that where the metadata's objects are stored (eg $wpdb->users). 
  49. * @since 4.1.0 
  50. * @access public 
  51. * @var string 
  52. */ 
  53. public $primary_table; 
  54.  
  55. /** 
  56. * Column in 'primary_table' that represents the ID of the object. 
  57. * @since 4.1.0 
  58. * @access public 
  59. * @var string 
  60. */ 
  61. public $primary_id_column; 
  62.  
  63. /** 
  64. * Constructor. 
  65. * @since 3.1.0 
  66. * @since 4.1.0 Added support for `$operator` 'NOT EXISTS' and 'EXISTS' values. 
  67. * @access public 
  68. * @param array $tax_query { 
  69. * Array of taxonomy query clauses. 
  70. * @type string $relation Optional. The MySQL keyword used to join 
  71. * the clauses of the query. Accepts 'AND', or 'OR'. Default 'AND'. 
  72. * @type array { 
  73. * Optional. An array of first-order clause parameters, or another fully-formed tax query. 
  74. * @type string $taxonomy Taxonomy being queried. Optional when field=term_taxonomy_id. 
  75. * @type string|int|array $terms Term or terms to filter by. 
  76. * @type string $field Field to match $terms against. Accepts 'term_id', 'slug',  
  77. * 'name', or 'term_taxonomy_id'. Default: 'term_id'. 
  78. * @type string $operator MySQL operator to be used with $terms in the WHERE clause. 
  79. * Accepts 'AND', 'IN', 'NOT IN', 'EXISTS', 'NOT EXISTS'. 
  80. * Default: 'IN'. 
  81. * @type bool $include_children Optional. Whether to include child terms. 
  82. * Requires a $taxonomy. Default: true. 
  83. * } 
  84. * } 
  85. */ 
  86. public function __construct( $tax_query ) { 
  87. if ( isset( $tax_query['relation'] ) ) { 
  88. $this->relation = $this->sanitize_relation( $tax_query['relation'] ); 
  89. } else { 
  90. $this->relation = 'AND'; 
  91.  
  92. $this->queries = $this->sanitize_query( $tax_query ); 
  93.  
  94. /** 
  95. * Ensure the 'tax_query' argument passed to the class constructor is well-formed. 
  96. * Ensures that each query-level clause has a 'relation' key, and that 
  97. * each first-order clause contains all the necessary keys from `$defaults`. 
  98. * @since 4.1.0 
  99. * @access public 
  100. * @param array $queries Array of queries clauses. 
  101. * @return array Sanitized array of query clauses. 
  102. */ 
  103. public function sanitize_query( $queries ) { 
  104. $cleaned_query = array(); 
  105.  
  106. $defaults = array( 
  107. 'taxonomy' => '',  
  108. 'terms' => array(),  
  109. 'field' => 'term_id',  
  110. 'operator' => 'IN',  
  111. 'include_children' => true,  
  112. ); 
  113.  
  114. foreach ( $queries as $key => $query ) { 
  115. if ( 'relation' === $key ) { 
  116. $cleaned_query['relation'] = $this->sanitize_relation( $query ); 
  117.  
  118. // First-order clause. 
  119. } elseif ( self::is_first_order_clause( $query ) ) { 
  120.  
  121. $cleaned_clause = array_merge( $defaults, $query ); 
  122. $cleaned_clause['terms'] = (array) $cleaned_clause['terms']; 
  123. $cleaned_query[] = $cleaned_clause; 
  124.  
  125. /** 
  126. * Keep a copy of the clause in the flate 
  127. * $queried_terms array, for use in WP_Query. 
  128. */ 
  129. if ( ! empty( $cleaned_clause['taxonomy'] ) && 'NOT IN' !== $cleaned_clause['operator'] ) { 
  130. $taxonomy = $cleaned_clause['taxonomy']; 
  131. if ( ! isset( $this->queried_terms[ $taxonomy ] ) ) { 
  132. $this->queried_terms[ $taxonomy ] = array(); 
  133.  
  134. /** 
  135. * Backward compatibility: Only store the first 
  136. * 'terms' and 'field' found for a given taxonomy. 
  137. */ 
  138. if ( ! empty( $cleaned_clause['terms'] ) && ! isset( $this->queried_terms[ $taxonomy ]['terms'] ) ) { 
  139. $this->queried_terms[ $taxonomy ]['terms'] = $cleaned_clause['terms']; 
  140.  
  141. if ( ! empty( $cleaned_clause['field'] ) && ! isset( $this->queried_terms[ $taxonomy ]['field'] ) ) { 
  142. $this->queried_terms[ $taxonomy ]['field'] = $cleaned_clause['field']; 
  143.  
  144. // Otherwise, it's a nested query, so we recurse. 
  145. } elseif ( is_array( $query ) ) { 
  146. $cleaned_subquery = $this->sanitize_query( $query ); 
  147.  
  148. if ( ! empty( $cleaned_subquery ) ) { 
  149. // All queries with children must have a relation. 
  150. if ( ! isset( $cleaned_subquery['relation'] ) ) { 
  151. $cleaned_subquery['relation'] = 'AND'; 
  152.  
  153. $cleaned_query[] = $cleaned_subquery; 
  154.  
  155. return $cleaned_query; 
  156.  
  157. /** 
  158. * Sanitize a 'relation' operator. 
  159. * @since 4.1.0 
  160. * @access public 
  161. * @param string $relation Raw relation key from the query argument. 
  162. * @return string Sanitized relation ('AND' or 'OR'). 
  163. */ 
  164. public function sanitize_relation( $relation ) { 
  165. if ( 'OR' === strtoupper( $relation ) ) { 
  166. return 'OR'; 
  167. } else { 
  168. return 'AND'; 
  169.  
  170. /** 
  171. * Determine whether a clause is first-order. 
  172. * A "first-order" clause is one that contains any of the first-order 
  173. * clause keys ('terms', 'taxonomy', 'include_children', 'field',  
  174. * 'operator'). An empty clause also counts as a first-order clause,  
  175. * for backward compatibility. Any clause that doesn't meet this is 
  176. * determined, by process of elimination, to be a higher-order query. 
  177. * @since 4.1.0 
  178. * @static 
  179. * @access protected 
  180. * @param array $query Tax query arguments. 
  181. * @return bool Whether the query clause is a first-order clause. 
  182. */ 
  183. protected static function is_first_order_clause( $query ) { 
  184. return is_array( $query ) && ( empty( $query ) || array_key_exists( 'terms', $query ) || array_key_exists( 'taxonomy', $query ) || array_key_exists( 'include_children', $query ) || array_key_exists( 'field', $query ) || array_key_exists( 'operator', $query ) ); 
  185.  
  186. /** 
  187. * Generates SQL clauses to be appended to a main query. 
  188. * @since 3.1.0 
  189. * @static 
  190. * @access public 
  191. * @param string $primary_table Database table where the object being filtered is stored (eg wp_users). 
  192. * @param string $primary_id_column ID column for the filtered object in $primary_table. 
  193. * @return array { 
  194. * Array containing JOIN and WHERE SQL clauses to append to the main query. 
  195. * @type string $join SQL fragment to append to the main JOIN clause. 
  196. * @type string $where SQL fragment to append to the main WHERE clause. 
  197. * } 
  198. */ 
  199. public function get_sql( $primary_table, $primary_id_column ) { 
  200. $this->primary_table = $primary_table; 
  201. $this->primary_id_column = $primary_id_column; 
  202.  
  203. return $this->get_sql_clauses(); 
  204.  
  205. /** 
  206. * Generate SQL clauses to be appended to a main query. 
  207. * Called by the public WP_Tax_Query::get_sql(), this method 
  208. * is abstracted out to maintain parity with the other Query classes. 
  209. * @since 4.1.0 
  210. * @access protected 
  211. * @return array { 
  212. * Array containing JOIN and WHERE SQL clauses to append to the main query. 
  213. * @type string $join SQL fragment to append to the main JOIN clause. 
  214. * @type string $where SQL fragment to append to the main WHERE clause. 
  215. * } 
  216. */ 
  217. protected function get_sql_clauses() { 
  218. /** 
  219. * $queries are passed by reference to get_sql_for_query() for recursion. 
  220. * To keep $this->queries unaltered, pass a copy. 
  221. */ 
  222. $queries = $this->queries; 
  223. $sql = $this->get_sql_for_query( $queries ); 
  224.  
  225. if ( ! empty( $sql['where'] ) ) { 
  226. $sql['where'] = ' AND ' . $sql['where']; 
  227.  
  228. return $sql; 
  229.  
  230. /** 
  231. * Generate SQL clauses for a single query array. 
  232. * If nested subqueries are found, this method recurses the tree to 
  233. * produce the properly nested SQL. 
  234. * @since 4.1.0 
  235. * @access protected 
  236. * @param array $query Query to parse, passed by reference. 
  237. * @param int $depth Optional. Number of tree levels deep we currently are. 
  238. * Used to calculate indentation. Default 0. 
  239. * @return array { 
  240. * Array containing JOIN and WHERE SQL clauses to append to a single query array. 
  241. * @type string $join SQL fragment to append to the main JOIN clause. 
  242. * @type string $where SQL fragment to append to the main WHERE clause. 
  243. * } 
  244. */ 
  245. protected function get_sql_for_query( &$query, $depth = 0 ) { 
  246. $sql_chunks = array( 
  247. 'join' => array(),  
  248. 'where' => array(),  
  249. ); 
  250.  
  251. $sql = array( 
  252. 'join' => '',  
  253. 'where' => '',  
  254. ); 
  255.  
  256. $indent = ''; 
  257. for ( $i = 0; $i < $depth; $i++ ) { 
  258. $indent .= " "; 
  259.  
  260. foreach ( $query as $key => &$clause ) { 
  261. if ( 'relation' === $key ) { 
  262. $relation = $query['relation']; 
  263. } elseif ( is_array( $clause ) ) { 
  264.  
  265. // This is a first-order clause. 
  266. if ( $this->is_first_order_clause( $clause ) ) { 
  267. $clause_sql = $this->get_sql_for_clause( $clause, $query ); 
  268.  
  269. $where_count = count( $clause_sql['where'] ); 
  270. if ( ! $where_count ) { 
  271. $sql_chunks['where'][] = ''; 
  272. } elseif ( 1 === $where_count ) { 
  273. $sql_chunks['where'][] = $clause_sql['where'][0]; 
  274. } else { 
  275. $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )'; 
  276.  
  277. $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] ); 
  278. // This is a subquery, so we recurse. 
  279. } else { 
  280. $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 ); 
  281.  
  282. $sql_chunks['where'][] = $clause_sql['where']; 
  283. $sql_chunks['join'][] = $clause_sql['join']; 
  284.  
  285. // Filter to remove empties. 
  286. $sql_chunks['join'] = array_filter( $sql_chunks['join'] ); 
  287. $sql_chunks['where'] = array_filter( $sql_chunks['where'] ); 
  288.  
  289. if ( empty( $relation ) ) { 
  290. $relation = 'AND'; 
  291.  
  292. // Filter duplicate JOIN clauses and combine into a single string. 
  293. if ( ! empty( $sql_chunks['join'] ) ) { 
  294. $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) ); 
  295.  
  296. // Generate a single WHERE clause with proper brackets and indentation. 
  297. if ( ! empty( $sql_chunks['where'] ) ) { 
  298. $sql['where'] = '( ' . "\n " . $indent . implode( ' ' . "\n " . $indent . $relation . ' ' . "\n " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')'; 
  299.  
  300. return $sql; 
  301.  
  302. /** 
  303. * Generate SQL JOIN and WHERE clauses for a "first-order" query clause. 
  304. * @since 4.1.0 
  305. * @access public 
  306. * @global wpdb $wpdb The WordPress database abstraction object. 
  307. * @param array $clause Query clause, passed by reference. 
  308. * @param array $parent_query Parent query array. 
  309. * @return array { 
  310. * Array containing JOIN and WHERE SQL clauses to append to a first-order query. 
  311. * @type string $join SQL fragment to append to the main JOIN clause. 
  312. * @type string $where SQL fragment to append to the main WHERE clause. 
  313. * } 
  314. */ 
  315. public function get_sql_for_clause( &$clause, $parent_query ) { 
  316. global $wpdb; 
  317.  
  318. $sql = array( 
  319. 'where' => array(),  
  320. 'join' => array(),  
  321. ); 
  322.  
  323. $join = $where = ''; 
  324.  
  325. $this->clean_query( $clause ); 
  326.  
  327. if ( is_wp_error( $clause ) ) { 
  328. return self::$no_results; 
  329.  
  330. $terms = $clause['terms']; 
  331. $operator = strtoupper( $clause['operator'] ); 
  332.  
  333. if ( 'IN' == $operator ) { 
  334.  
  335. if ( empty( $terms ) ) { 
  336. return self::$no_results; 
  337.  
  338. $terms = implode( ', ', $terms ); 
  339.  
  340. /** 
  341. * Before creating another table join, see if this clause has a 
  342. * sibling with an existing join that can be shared. 
  343. */ 
  344. $alias = $this->find_compatible_table_alias( $clause, $parent_query ); 
  345. if ( false === $alias ) { 
  346. $i = count( $this->table_aliases ); 
  347. $alias = $i ? 'tt' . $i : $wpdb->term_relationships; 
  348.  
  349. // Store the alias as part of a flat array to build future iterators. 
  350. $this->table_aliases[] = $alias; 
  351.  
  352. // Store the alias with this clause, so later siblings can use it. 
  353. $clause['alias'] = $alias; 
  354.  
  355. $join .= " LEFT JOIN $wpdb->term_relationships"; 
  356. $join .= $i ? " AS $alias" : ''; 
  357. $join .= " ON ($this->primary_table.$this->primary_id_column = $alias.object_id)"; 
  358.  
  359.  
  360. $where = "$alias.term_taxonomy_id $operator ($terms)"; 
  361.  
  362. } elseif ( 'NOT IN' == $operator ) { 
  363.  
  364. if ( empty( $terms ) ) { 
  365. return $sql; 
  366.  
  367. $terms = implode( ', ', $terms ); 
  368.  
  369. $where = "$this->primary_table.$this->primary_id_column NOT IN ( 
  370. SELECT object_id 
  371. FROM $wpdb->term_relationships 
  372. WHERE term_taxonomy_id IN ($terms) 
  373. )"; 
  374.  
  375. } elseif ( 'AND' == $operator ) { 
  376.  
  377. if ( empty( $terms ) ) { 
  378. return $sql; 
  379.  
  380. $num_terms = count( $terms ); 
  381.  
  382. $terms = implode( ', ', $terms ); 
  383.  
  384. $where = "( 
  385. SELECT COUNT(1) 
  386. FROM $wpdb->term_relationships 
  387. WHERE term_taxonomy_id IN ($terms) 
  388. AND object_id = $this->primary_table.$this->primary_id_column 
  389. ) = $num_terms"; 
  390.  
  391. } elseif ( 'NOT EXISTS' === $operator || 'EXISTS' === $operator ) { 
  392.  
  393. $where = $wpdb->prepare( "$operator ( 
  394. SELECT 1 
  395. FROM $wpdb->term_relationships 
  396. INNER JOIN $wpdb->term_taxonomy 
  397. ON $wpdb->term_taxonomy.term_taxonomy_id = $wpdb->term_relationships.term_taxonomy_id 
  398. WHERE $wpdb->term_taxonomy.taxonomy = %s 
  399. AND $wpdb->term_relationships.object_id = $this->primary_table.$this->primary_id_column 
  400. )", $clause['taxonomy'] ); 
  401.  
  402.  
  403. $sql['join'][] = $join; 
  404. $sql['where'][] = $where; 
  405. return $sql; 
  406.  
  407. /** 
  408. * Identify an existing table alias that is compatible with the current query clause. 
  409. * We avoid unnecessary table joins by allowing each clause to look for 
  410. * an existing table alias that is compatible with the query that it 
  411. * needs to perform. 
  412. * An existing alias is compatible if (a) it is a sibling of `$clause` 
  413. * (ie, it's under the scope of the same relation), and (b) the combination 
  414. * of operator and relation between the clauses allows for a shared table 
  415. * join. In the case of WP_Tax_Query, this only applies to 'IN' 
  416. * clauses that are connected by the relation 'OR'. 
  417. * @since 4.1.0 
  418. * @access protected 
  419. * @param array $clause Query clause. 
  420. * @param array $parent_query Parent query of $clause. 
  421. * @return string|false Table alias if found, otherwise false. 
  422. */ 
  423. protected function find_compatible_table_alias( $clause, $parent_query ) { 
  424. $alias = false; 
  425.  
  426. // Sanity check. Only IN queries use the JOIN syntax . 
  427. if ( ! isset( $clause['operator'] ) || 'IN' !== $clause['operator'] ) { 
  428. return $alias; 
  429.  
  430. // Since we're only checking IN queries, we're only concerned with OR relations. 
  431. if ( ! isset( $parent_query['relation'] ) || 'OR' !== $parent_query['relation'] ) { 
  432. return $alias; 
  433.  
  434. $compatible_operators = array( 'IN' ); 
  435.  
  436. foreach ( $parent_query as $sibling ) { 
  437. if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) { 
  438. continue; 
  439.  
  440. if ( empty( $sibling['alias'] ) || empty( $sibling['operator'] ) ) { 
  441. continue; 
  442.  
  443. // The sibling must both have compatible operator to share its alias. 
  444. if ( in_array( strtoupper( $sibling['operator'] ), $compatible_operators ) ) { 
  445. $alias = $sibling['alias']; 
  446. break; 
  447.  
  448. return $alias; 
  449.  
  450. /** 
  451. * Validates a single query. 
  452. * @since 3.2.0 
  453. * @access private 
  454. * @param array $query The single query. Passed by reference. 
  455. */ 
  456. private function clean_query( &$query ) { 
  457. if ( empty( $query['taxonomy'] ) ) { 
  458. if ( 'term_taxonomy_id' !== $query['field'] ) { 
  459. $query = new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); 
  460. return; 
  461.  
  462. // so long as there are shared terms, include_children requires that a taxonomy is set 
  463. $query['include_children'] = false; 
  464. } elseif ( ! taxonomy_exists( $query['taxonomy'] ) ) { 
  465. $query = new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); 
  466. return; 
  467.  
  468. $query['terms'] = array_unique( (array) $query['terms'] ); 
  469.  
  470. if ( is_taxonomy_hierarchical( $query['taxonomy'] ) && $query['include_children'] ) { 
  471. $this->transform_query( $query, 'term_id' ); 
  472.  
  473. if ( is_wp_error( $query ) ) 
  474. return; 
  475.  
  476. $children = array(); 
  477. foreach ( $query['terms'] as $term ) { 
  478. $children = array_merge( $children, get_term_children( $term, $query['taxonomy'] ) ); 
  479. $children[] = $term; 
  480. $query['terms'] = $children; 
  481.  
  482. $this->transform_query( $query, 'term_taxonomy_id' ); 
  483.  
  484. /** 
  485. * Transforms a single query, from one field to another. 
  486. * @since 3.2.0 
  487. * @global wpdb $wpdb The WordPress database abstraction object. 
  488. * @param array $query The single query. Passed by reference. 
  489. * @param string $resulting_field The resulting field. Accepts 'slug', 'name', 'term_taxonomy_id',  
  490. * or 'term_id'. Default 'term_id'. 
  491. */ 
  492. public function transform_query( &$query, $resulting_field ) { 
  493. global $wpdb; 
  494.  
  495. if ( empty( $query['terms'] ) ) 
  496. return; 
  497.  
  498. if ( $query['field'] == $resulting_field ) 
  499. return; 
  500.  
  501. $resulting_field = sanitize_key( $resulting_field ); 
  502.  
  503. switch ( $query['field'] ) { 
  504. case 'slug': 
  505. case 'name': 
  506. foreach ( $query['terms'] as &$term ) { 
  507. /** 
  508. * 0 is the $term_id parameter. We don't have a term ID yet, but it doesn't 
  509. * matter because `sanitize_term_field()` ignores the $term_id param when the 
  510. * context is 'db'. 
  511. */ 
  512. $term = "'" . esc_sql( sanitize_term_field( $query['field'], $term, 0, $query['taxonomy'], 'db' ) ) . "'"; 
  513.  
  514. $terms = implode( ", ", $query['terms'] ); 
  515.  
  516. $terms = $wpdb->get_col( " 
  517. SELECT $wpdb->term_taxonomy.$resulting_field 
  518. FROM $wpdb->term_taxonomy 
  519. INNER JOIN $wpdb->terms USING (term_id) 
  520. WHERE taxonomy = '{$query['taxonomy']}' 
  521. AND $wpdb->terms.{$query['field']} IN ($terms) 
  522. " ); 
  523. break; 
  524. case 'term_taxonomy_id': 
  525. $terms = implode( ', ', array_map( 'intval', $query['terms'] ) ); 
  526. $terms = $wpdb->get_col( " 
  527. SELECT $resulting_field 
  528. FROM $wpdb->term_taxonomy 
  529. WHERE term_taxonomy_id IN ($terms) 
  530. " ); 
  531. break; 
  532. default: 
  533. $terms = implode( ', ', array_map( 'intval', $query['terms'] ) ); 
  534. $terms = $wpdb->get_col( " 
  535. SELECT $resulting_field 
  536. FROM $wpdb->term_taxonomy 
  537. WHERE taxonomy = '{$query['taxonomy']}' 
  538. AND term_id IN ($terms) 
  539. " ); 
  540.  
  541. if ( 'AND' == $query['operator'] && count( $terms ) < count( $query['terms'] ) ) { 
  542. $query = new WP_Error( 'inexistent_terms', __( 'Inexistent terms.' ) ); 
  543. return; 
  544.  
  545. $query['terms'] = $terms; 
  546. $query['field'] = $resulting_field;