Changeset 33760 for trunk/src/wp-includes/taxonomy-functions.php
- Timestamp:
- 08/26/2015 12:48:11 PM (9 years ago)
- File:
-
- 1 copied
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/taxonomy-functions.php
r33758 r33760 652 652 $tax_query_obj = new WP_Tax_Query( $tax_query ); 653 653 return $tax_query_obj->get_sql( $primary_table, $primary_id_column ); 654 }655 656 /**657 * Class for generating SQL clauses that filter a primary query according to object taxonomy terms.658 *659 * `WP_Tax_Query` is a helper that allows primary query classes, such as WP_Query, to filter660 * their results by object metadata, by generating `JOIN` and `WHERE` subclauses to be attached661 * to the primary SQL query string.662 *663 * @since 3.1.0664 */665 class WP_Tax_Query {666 667 /**668 * Array of taxonomy queries.669 *670 * See {@see WP_Tax_Query::__construct()} for information on tax query arguments.671 *672 * @since 3.1.0673 * @access public674 * @var array675 */676 public $queries = array();677 678 /**679 * The relation between the queries. Can be one of 'AND' or 'OR'.680 *681 * @since 3.1.0682 * @access public683 * @var string684 */685 public $relation;686 687 /**688 * Standard response when the query should not return any rows.689 *690 * @since 3.2.0691 *692 * @static693 * @access private694 * @var string695 */696 private static $no_results = array( 'join' => array( '' ), 'where' => array( '0 = 1' ) );697 698 /**699 * A flat list of table aliases used in the JOIN clauses.700 *701 * @since 4.1.0702 * @access protected703 * @var array704 */705 protected $table_aliases = array();706 707 /**708 * Terms and taxonomies fetched by this query.709 *710 * We store this data in a flat array because they are referenced in a711 * number of places by WP_Query.712 *713 * @since 4.1.0714 * @access public715 * @var array716 */717 public $queried_terms = array();718 719 /**720 * Database table that where the metadata's objects are stored (eg $wpdb->users).721 *722 * @since 4.1.0723 * @access public724 * @var string725 */726 public $primary_table;727 728 /**729 * Column in 'primary_table' that represents the ID of the object.730 *731 * @since 4.1.0732 * @access public733 * @var string734 */735 public $primary_id_column;736 737 /**738 * Constructor.739 *740 * @since 3.1.0741 * @since 4.1.0 Added support for `$operator` 'NOT EXISTS' and 'EXISTS' values.742 * @access public743 *744 * @param array $tax_query {745 * Array of taxonomy query clauses.746 *747 * @type string $relation Optional. The MySQL keyword used to join748 * the clauses of the query. Accepts 'AND', or 'OR'. Default 'AND'.749 * @type array {750 * Optional. An array of first-order clause parameters, or another fully-formed tax query.751 *752 * @type string $taxonomy Taxonomy being queried. Optional when field=term_taxonomy_id.753 * @type string|int|array $terms Term or terms to filter by.754 * @type string $field Field to match $terms against. Accepts 'term_id', 'slug',755 * 'name', or 'term_taxonomy_id'. Default: 'term_id'.756 * @type string $operator MySQL operator to be used with $terms in the WHERE clause.757 * Accepts 'AND', 'IN', 'NOT IN', 'EXISTS', 'NOT EXISTS'.758 * Default: 'IN'.759 * @type bool $include_children Optional. Whether to include child terms.760 * Requires a $taxonomy. Default: true.761 * }762 * }763 */764 public function __construct( $tax_query ) {765 if ( isset( $tax_query['relation'] ) ) {766 $this->relation = $this->sanitize_relation( $tax_query['relation'] );767 } else {768 $this->relation = 'AND';769 }770 771 $this->queries = $this->sanitize_query( $tax_query );772 }773 774 /**775 * Ensure the 'tax_query' argument passed to the class constructor is well-formed.776 *777 * Ensures that each query-level clause has a 'relation' key, and that778 * each first-order clause contains all the necessary keys from `$defaults`.779 *780 * @since 4.1.0781 * @access public782 *783 * @param array $queries Array of queries clauses.784 * @return array Sanitized array of query clauses.785 */786 public function sanitize_query( $queries ) {787 $cleaned_query = array();788 789 $defaults = array(790 'taxonomy' => '',791 'terms' => array(),792 'field' => 'term_id',793 'operator' => 'IN',794 'include_children' => true,795 );796 797 foreach ( $queries as $key => $query ) {798 if ( 'relation' === $key ) {799 $cleaned_query['relation'] = $this->sanitize_relation( $query );800 801 // First-order clause.802 } elseif ( self::is_first_order_clause( $query ) ) {803 804 $cleaned_clause = array_merge( $defaults, $query );805 $cleaned_clause['terms'] = (array) $cleaned_clause['terms'];806 $cleaned_query[] = $cleaned_clause;807 808 /*809 * Keep a copy of the clause in the flate810 * $queried_terms array, for use in WP_Query.811 */812 if ( ! empty( $cleaned_clause['taxonomy'] ) && 'NOT IN' !== $cleaned_clause['operator'] ) {813 $taxonomy = $cleaned_clause['taxonomy'];814 if ( ! isset( $this->queried_terms[ $taxonomy ] ) ) {815 $this->queried_terms[ $taxonomy ] = array();816 }817 818 /*819 * Backward compatibility: Only store the first820 * 'terms' and 'field' found for a given taxonomy.821 */822 if ( ! empty( $cleaned_clause['terms'] ) && ! isset( $this->queried_terms[ $taxonomy ]['terms'] ) ) {823 $this->queried_terms[ $taxonomy ]['terms'] = $cleaned_clause['terms'];824 }825 826 if ( ! empty( $cleaned_clause['field'] ) && ! isset( $this->queried_terms[ $taxonomy ]['field'] ) ) {827 $this->queried_terms[ $taxonomy ]['field'] = $cleaned_clause['field'];828 }829 }830 831 // Otherwise, it's a nested query, so we recurse.832 } elseif ( is_array( $query ) ) {833 $cleaned_subquery = $this->sanitize_query( $query );834 835 if ( ! empty( $cleaned_subquery ) ) {836 // All queries with children must have a relation.837 if ( ! isset( $cleaned_subquery['relation'] ) ) {838 $cleaned_subquery['relation'] = 'AND';839 }840 841 $cleaned_query[] = $cleaned_subquery;842 }843 }844 }845 846 return $cleaned_query;847 }848 849 /**850 * Sanitize a 'relation' operator.851 *852 * @since 4.1.0853 * @access public854 *855 * @param string $relation Raw relation key from the query argument.856 * @return string Sanitized relation ('AND' or 'OR').857 */858 public function sanitize_relation( $relation ) {859 if ( 'OR' === strtoupper( $relation ) ) {860 return 'OR';861 } else {862 return 'AND';863 }864 }865 866 /**867 * Determine whether a clause is first-order.868 *869 * A "first-order" clause is one that contains any of the first-order870 * clause keys ('terms', 'taxonomy', 'include_children', 'field',871 * 'operator'). An empty clause also counts as a first-order clause,872 * for backward compatibility. Any clause that doesn't meet this is873 * determined, by process of elimination, to be a higher-order query.874 *875 * @since 4.1.0876 *877 * @static878 * @access protected879 *880 * @param array $query Tax query arguments.881 * @return bool Whether the query clause is a first-order clause.882 */883 protected static function is_first_order_clause( $query ) {884 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 ) );885 }886 887 /**888 * Generates SQL clauses to be appended to a main query.889 *890 * @since 3.1.0891 *892 * @static893 * @access public894 *895 * @param string $primary_table Database table where the object being filtered is stored (eg wp_users).896 * @param string $primary_id_column ID column for the filtered object in $primary_table.897 * @return array {898 * Array containing JOIN and WHERE SQL clauses to append to the main query.899 *900 * @type string $join SQL fragment to append to the main JOIN clause.901 * @type string $where SQL fragment to append to the main WHERE clause.902 * }903 */904 public function get_sql( $primary_table, $primary_id_column ) {905 $this->primary_table = $primary_table;906 $this->primary_id_column = $primary_id_column;907 908 return $this->get_sql_clauses();909 }910 911 /**912 * Generate SQL clauses to be appended to a main query.913 *914 * Called by the public WP_Tax_Query::get_sql(), this method915 * is abstracted out to maintain parity with the other Query classes.916 *917 * @since 4.1.0918 * @access protected919 *920 * @return array {921 * Array containing JOIN and WHERE SQL clauses to append to the main query.922 *923 * @type string $join SQL fragment to append to the main JOIN clause.924 * @type string $where SQL fragment to append to the main WHERE clause.925 * }926 */927 protected function get_sql_clauses() {928 /*929 * $queries are passed by reference to get_sql_for_query() for recursion.930 * To keep $this->queries unaltered, pass a copy.931 */932 $queries = $this->queries;933 $sql = $this->get_sql_for_query( $queries );934 935 if ( ! empty( $sql['where'] ) ) {936 $sql['where'] = ' AND ' . $sql['where'];937 }938 939 return $sql;940 }941 942 /**943 * Generate SQL clauses for a single query array.944 *945 * If nested subqueries are found, this method recurses the tree to946 * produce the properly nested SQL.947 *948 * @since 4.1.0949 * @access protected950 *951 * @param array $query Query to parse, passed by reference.952 * @param int $depth Optional. Number of tree levels deep we currently are.953 * Used to calculate indentation. Default 0.954 * @return array {955 * Array containing JOIN and WHERE SQL clauses to append to a single query array.956 *957 * @type string $join SQL fragment to append to the main JOIN clause.958 * @type string $where SQL fragment to append to the main WHERE clause.959 * }960 */961 protected function get_sql_for_query( &$query, $depth = 0 ) {962 $sql_chunks = array(963 'join' => array(),964 'where' => array(),965 );966 967 $sql = array(968 'join' => '',969 'where' => '',970 );971 972 $indent = '';973 for ( $i = 0; $i < $depth; $i++ ) {974 $indent .= " ";975 }976 977 foreach ( $query as $key => &$clause ) {978 if ( 'relation' === $key ) {979 $relation = $query['relation'];980 } elseif ( is_array( $clause ) ) {981 982 // This is a first-order clause.983 if ( $this->is_first_order_clause( $clause ) ) {984 $clause_sql = $this->get_sql_for_clause( $clause, $query );985 986 $where_count = count( $clause_sql['where'] );987 if ( ! $where_count ) {988 $sql_chunks['where'][] = '';989 } elseif ( 1 === $where_count ) {990 $sql_chunks['where'][] = $clause_sql['where'][0];991 } else {992 $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';993 }994 995 $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );996 // This is a subquery, so we recurse.997 } else {998 $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );999 1000 $sql_chunks['where'][] = $clause_sql['where'];1001 $sql_chunks['join'][] = $clause_sql['join'];1002 }1003 }1004 }1005 1006 // Filter to remove empties.1007 $sql_chunks['join'] = array_filter( $sql_chunks['join'] );1008 $sql_chunks['where'] = array_filter( $sql_chunks['where'] );1009 1010 if ( empty( $relation ) ) {1011 $relation = 'AND';1012 }1013 1014 // Filter duplicate JOIN clauses and combine into a single string.1015 if ( ! empty( $sql_chunks['join'] ) ) {1016 $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );1017 }1018 1019 // Generate a single WHERE clause with proper brackets and indentation.1020 if ( ! empty( $sql_chunks['where'] ) ) {1021 $sql['where'] = '( ' . "\n " . $indent . implode( ' ' . "\n " . $indent . $relation . ' ' . "\n " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')';1022 }1023 1024 return $sql;1025 }1026 1027 /**1028 * Generate SQL JOIN and WHERE clauses for a "first-order" query clause.1029 *1030 * @since 4.1.01031 * @access public1032 *1033 * @global wpdb $wpdb The WordPress database abstraction object.1034 *1035 * @param array $clause Query clause, passed by reference.1036 * @param array $parent_query Parent query array.1037 * @return array {1038 * Array containing JOIN and WHERE SQL clauses to append to a first-order query.1039 *1040 * @type string $join SQL fragment to append to the main JOIN clause.1041 * @type string $where SQL fragment to append to the main WHERE clause.1042 * }1043 */1044 public function get_sql_for_clause( &$clause, $parent_query ) {1045 global $wpdb;1046 1047 $sql = array(1048 'where' => array(),1049 'join' => array(),1050 );1051 1052 $join = $where = '';1053 1054 $this->clean_query( $clause );1055 1056 if ( is_wp_error( $clause ) ) {1057 return self::$no_results;1058 }1059 1060 $terms = $clause['terms'];1061 $operator = strtoupper( $clause['operator'] );1062 1063 if ( 'IN' == $operator ) {1064 1065 if ( empty( $terms ) ) {1066 return self::$no_results;1067 }1068 1069 $terms = implode( ',', $terms );1070 1071 /*1072 * Before creating another table join, see if this clause has a1073 * sibling with an existing join that can be shared.1074 */1075 $alias = $this->find_compatible_table_alias( $clause, $parent_query );1076 if ( false === $alias ) {1077 $i = count( $this->table_aliases );1078 $alias = $i ? 'tt' . $i : $wpdb->term_relationships;1079 1080 // Store the alias as part of a flat array to build future iterators.1081 $this->table_aliases[] = $alias;1082 1083 // Store the alias with this clause, so later siblings can use it.1084 $clause['alias'] = $alias;1085 1086 $join .= " INNER JOIN $wpdb->term_relationships";1087 $join .= $i ? " AS $alias" : '';1088 $join .= " ON ($this->primary_table.$this->primary_id_column = $alias.object_id)";1089 }1090 1091 1092 $where = "$alias.term_taxonomy_id $operator ($terms)";1093 1094 } elseif ( 'NOT IN' == $operator ) {1095 1096 if ( empty( $terms ) ) {1097 return $sql;1098 }1099 1100 $terms = implode( ',', $terms );1101 1102 $where = "$this->primary_table.$this->primary_id_column NOT IN (1103 SELECT object_id1104 FROM $wpdb->term_relationships1105 WHERE term_taxonomy_id IN ($terms)1106 )";1107 1108 } elseif ( 'AND' == $operator ) {1109 1110 if ( empty( $terms ) ) {1111 return $sql;1112 }1113 1114 $num_terms = count( $terms );1115 1116 $terms = implode( ',', $terms );1117 1118 $where = "(1119 SELECT COUNT(1)1120 FROM $wpdb->term_relationships1121 WHERE term_taxonomy_id IN ($terms)1122 AND object_id = $this->primary_table.$this->primary_id_column1123 ) = $num_terms";1124 1125 } elseif ( 'NOT EXISTS' === $operator || 'EXISTS' === $operator ) {1126 1127 $where = $wpdb->prepare( "$operator (1128 SELECT 11129 FROM $wpdb->term_relationships1130 INNER JOIN $wpdb->term_taxonomy1131 ON $wpdb->term_taxonomy.term_taxonomy_id = $wpdb->term_relationships.term_taxonomy_id1132 WHERE $wpdb->term_taxonomy.taxonomy = %s1133 AND $wpdb->term_relationships.object_id = $this->primary_table.$this->primary_id_column1134 )", $clause['taxonomy'] );1135 1136 }1137 1138 $sql['join'][] = $join;1139 $sql['where'][] = $where;1140 return $sql;1141 }1142 1143 /**1144 * Identify an existing table alias that is compatible with the current query clause.1145 *1146 * We avoid unnecessary table joins by allowing each clause to look for1147 * an existing table alias that is compatible with the query that it1148 * needs to perform.1149 *1150 * An existing alias is compatible if (a) it is a sibling of `$clause`1151 * (ie, it's under the scope of the same relation), and (b) the combination1152 * of operator and relation between the clauses allows for a shared table1153 * join. In the case of WP_Tax_Query, this only applies to 'IN'1154 * clauses that are connected by the relation 'OR'.1155 *1156 * @since 4.1.01157 * @access protected1158 *1159 * @param array $clause Query clause.1160 * @param array $parent_query Parent query of $clause.1161 * @return string|false Table alias if found, otherwise false.1162 */1163 protected function find_compatible_table_alias( $clause, $parent_query ) {1164 $alias = false;1165 1166 // Sanity check. Only IN queries use the JOIN syntax .1167 if ( ! isset( $clause['operator'] ) || 'IN' !== $clause['operator'] ) {1168 return $alias;1169 }1170 1171 // Since we're only checking IN queries, we're only concerned with OR relations.1172 if ( ! isset( $parent_query['relation'] ) || 'OR' !== $parent_query['relation'] ) {1173 return $alias;1174 }1175 1176 $compatible_operators = array( 'IN' );1177 1178 foreach ( $parent_query as $sibling ) {1179 if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) {1180 continue;1181 }1182 1183 if ( empty( $sibling['alias'] ) || empty( $sibling['operator'] ) ) {1184 continue;1185 }1186 1187 // The sibling must both have compatible operator to share its alias.1188 if ( in_array( strtoupper( $sibling['operator'] ), $compatible_operators ) ) {1189 $alias = $sibling['alias'];1190 break;1191 }1192 }1193 1194 return $alias;1195 }1196 1197 /**1198 * Validates a single query.1199 *1200 * @since 3.2.01201 * @access private1202 *1203 * @param array &$query The single query.1204 */1205 private function clean_query( &$query ) {1206 if ( empty( $query['taxonomy'] ) ) {1207 if ( 'term_taxonomy_id' !== $query['field'] ) {1208 $query = new WP_Error( 'Invalid taxonomy' );1209 return;1210 }1211 1212 // so long as there are shared terms, include_children requires that a taxonomy is set1213 $query['include_children'] = false;1214 } elseif ( ! taxonomy_exists( $query['taxonomy'] ) ) {1215 $query = new WP_Error( 'Invalid taxonomy' );1216 return;1217 }1218 1219 $query['terms'] = array_unique( (array) $query['terms'] );1220 1221 if ( is_taxonomy_hierarchical( $query['taxonomy'] ) && $query['include_children'] ) {1222 $this->transform_query( $query, 'term_id' );1223 1224 if ( is_wp_error( $query ) )1225 return;1226 1227 $children = array();1228 foreach ( $query['terms'] as $term ) {1229 $children = array_merge( $children, get_term_children( $term, $query['taxonomy'] ) );1230 $children[] = $term;1231 }1232 $query['terms'] = $children;1233 }1234 1235 $this->transform_query( $query, 'term_taxonomy_id' );1236 }1237 1238 /**1239 * Transforms a single query, from one field to another.1240 *1241 * @since 3.2.01242 *1243 * @global wpdb $wpdb The WordPress database abstraction object.1244 *1245 * @param array &$query The single query.1246 * @param string $resulting_field The resulting field. Accepts 'slug', 'name', 'term_taxonomy_id',1247 * or 'term_id'. Default 'term_id'.1248 */1249 public function transform_query( &$query, $resulting_field ) {1250 global $wpdb;1251 1252 if ( empty( $query['terms'] ) )1253 return;1254 1255 if ( $query['field'] == $resulting_field )1256 return;1257 1258 $resulting_field = sanitize_key( $resulting_field );1259 1260 switch ( $query['field'] ) {1261 case 'slug':1262 case 'name':1263 foreach ( $query['terms'] as &$term ) {1264 /*1265 * 0 is the $term_id parameter. We don't have a term ID yet, but it doesn't1266 * matter because `sanitize_term_field()` ignores the $term_id param when the1267 * context is 'db'.1268 */1269 $term = "'" . esc_sql( sanitize_term_field( $query['field'], $term, 0, $query['taxonomy'], 'db' ) ) . "'";1270 }1271 1272 $terms = implode( ",", $query['terms'] );1273 1274 $terms = $wpdb->get_col( "1275 SELECT $wpdb->term_taxonomy.$resulting_field1276 FROM $wpdb->term_taxonomy1277 INNER JOIN $wpdb->terms USING (term_id)1278 WHERE taxonomy = '{$query['taxonomy']}'1279 AND $wpdb->terms.{$query['field']} IN ($terms)1280 " );1281 break;1282 case 'term_taxonomy_id':1283 $terms = implode( ',', array_map( 'intval', $query['terms'] ) );1284 $terms = $wpdb->get_col( "1285 SELECT $resulting_field1286 FROM $wpdb->term_taxonomy1287 WHERE term_taxonomy_id IN ($terms)1288 " );1289 break;1290 default:1291 $terms = implode( ',', array_map( 'intval', $query['terms'] ) );1292 $terms = $wpdb->get_col( "1293 SELECT $resulting_field1294 FROM $wpdb->term_taxonomy1295 WHERE taxonomy = '{$query['taxonomy']}'1296 AND term_id IN ($terms)1297 " );1298 }1299 1300 if ( 'AND' == $query['operator'] && count( $terms ) < count( $query['terms'] ) ) {1301 $query = new WP_Error( 'Inexistent terms' );1302 return;1303 }1304 1305 $query['terms'] = $terms;1306 $query['field'] = $resulting_field;1307 }1308 654 } 1309 655
Note: See TracChangeset
for help on using the changeset viewer.