Changeset 29891 for trunk/src/wp-includes/taxonomy.php
- Timestamp:
- 10/14/2014 04:02:41 AM (9 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/taxonomy.php
r29867 r29891 628 628 629 629 /** 630 * Container class for a multiple taxonomy query. 630 * Class for generating SQL clauses that filter a primary query according to object taxonomy terms. 631 * 632 * `WP_Tax_Query` is a helper that allows primary query classes, such as {@see WP_Query}, to filter 633 * their results by object metadata, by generating `JOIN` and `WHERE` subclauses to be attached 634 * to the primary SQL query string. 631 635 * 632 636 * @since 3.1.0 … … 635 639 636 640 /** 637 * List of taxonomy queries. A single taxonomy query is an associative array: 638 * - 'taxonomy' string The taxonomy being queried. Optional when using the term_taxonomy_id field. 639 * - 'terms' string|array The list of terms 640 * - 'field' string (optional) Which term field is being used. 641 * Possible values: 'term_id', 'slug', 'name', or 'term_taxonomy_id' 642 * Default: 'term_id' 643 * - 'operator' string (optional) 644 * Possible values: 'AND', 'IN' or 'NOT IN'. 645 * Default: 'IN' 646 * - 'include_children' bool (optional) Whether to include child terms. Requires that a taxonomy be specified. 647 * Default: true 641 * Array of taxonomy queries. 642 * 643 * See {@see WP_Tax_Query::__construct()} for information on tax query arguments. 648 644 * 649 645 * @since 3.1.0 … … 669 665 * @var string 670 666 */ 671 private static $no_results = array( 'join' => '', 'where' => ' AND 0 = 1' ); 667 private static $no_results = array( 'join' => array( '' ), 'where' => array( '0 = 1' ) ); 668 669 /** 670 * A flat list of table aliases used in the JOIN clauses. 671 * 672 * @since 4.1.0 673 * @access protected 674 * @var array 675 */ 676 protected $table_aliases = array(); 677 678 /** 679 * Terms and taxonomies fetched by this query. 680 * 681 * We store this data in a flat array because they are referenced in a 682 * number of places by WP_Query. 683 * 684 * @since 4.1.0 685 * @access public 686 * @var array 687 */ 688 public $queried_terms = array(); 672 689 673 690 /** 674 691 * Constructor. 675 *676 * Parses a compact tax query and sets defaults.677 692 * 678 693 * @since 3.1.0 679 694 * @access public 680 695 * 681 * @param array $tax_query A compact tax query: 682 * array( 683 * 'relation' => 'OR', 684 * array( 685 * 'taxonomy' => 'tax1', 686 * 'terms' => array( 'term1', 'term2' ), 687 * 'field' => 'slug', 688 * ), 689 * array( 690 * 'taxonomy' => 'tax2', 691 * 'terms' => array( 'term-a', 'term-b' ), 692 * 'field' => 'slug', 693 * ), 694 * ) 696 * @param array $tax_query { 697 * Array of taxonoy query clauses. 698 * 699 * @type string $relation Optional. The MySQL keyword used to join 700 * the clauses of the query. Accepts 'AND', or 'OR'. Default 'AND'. 701 * @type array { 702 * Optional. An array of first-order clause parameters, or another fully-formed tax query. 703 * 704 * @type string $taxonomy Taxonomy being queried. Optional when field=term_taxonomy_id. 705 * @type string|int|array $terms Term or terms to filter by. 706 * @type string $field Field to match $terms against. Accepts 'term_id', 'slug', 707 * 'name', or 'term_taxonomy_id'. Default: 'term_id'. 708 * @type string $operator MySQL operator to be used with $terms in the WHERE clause. 709 * Accepts 'AND', 'IN', or 'OR. Default: 'IN'. 710 * @type bool $include_children Optional. Whether to include child terms. 711 * Requires a $taxonomy. Default: true. 712 * } 713 * } 695 714 */ 696 715 public function __construct( $tax_query ) { … … 701 720 } 702 721 722 $this->queries = $this->sanitize_query( $tax_query ); 723 } 724 725 /** 726 * Ensure the `tax_query` argument passed to the class constructor is well-formed. 727 * 728 * Ensures that each query-level clause has a 'relation' key, and that 729 * each first-order clause contains all the necessary keys from $defaults. 730 * 731 * @since 4.1.0 732 * @access public 733 * 734 * @param array $queries Array of queries clauses. 735 * @return array Sanitized array of query clauses. 736 */ 737 public function sanitize_query( $queries ) { 738 $cleaned_query = array(); 739 703 740 $defaults = array( 704 741 'taxonomy' => '', 705 742 'terms' => array(), 706 'include_children' => true,707 743 'field' => 'term_id', 708 744 'operator' => 'IN', 745 'include_children' => true, 709 746 ); 710 747 711 foreach ( $tax_query as $query ) { 712 if ( ! is_array( $query ) ) 713 continue; 714 715 $query = array_merge( $defaults, $query ); 716 717 $query['terms'] = (array) $query['terms']; 718 719 $this->queries[] = $query; 720 } 748 foreach ( $queries as $key => $query ) { 749 if ( 'relation' === $key ) { 750 $cleaned_query['relation'] = $query; 751 752 // First-order clause. 753 } else if ( self::is_first_order_clause( $query ) ) { 754 755 $cleaned_clause = array_merge( $defaults, $query ); 756 $cleaned_clause['terms'] = (array) $cleaned_clause['terms']; 757 $cleaned_query[] = $cleaned_clause; 758 759 /* 760 * Keep a copy of the clause in the flate 761 * $queried_terms array, for use in WP_Query. 762 */ 763 if ( ! empty( $cleaned_clause['taxonomy'] ) && 'NOT IN' !== $cleaned_clause['operator'] ) { 764 $taxonomy = $cleaned_clause['taxonomy']; 765 if ( ! isset( $this->queried_terms[ $taxonomy ] ) ) { 766 $this->queried_terms[ $taxonomy ] = array(); 767 } 768 769 /* 770 * Backward compatibility: Only store the first 771 * 'terms' and 'field' found for a given taxonomy. 772 */ 773 if ( ! empty( $cleaned_clause['terms'] ) && ! isset( $this->queried_terms[ $taxonomy ]['terms'] ) ) { 774 $this->queried_terms[ $taxonomy ]['terms'] = $cleaned_clause['terms']; 775 } 776 777 if ( ! empty( $cleaned_clause['field'] ) && ! isset( $this->queried_terms[ $taxonomy ]['field'] ) ) { 778 $this->queried_terms[ $taxonomy ]['field'] = $cleaned_clause['field']; 779 } 780 } 781 782 // Otherwise, it's a nested query, so we recurse. 783 } else if ( is_array( $query ) ) { 784 $cleaned_subquery = $this->sanitize_query( $query ); 785 786 if ( ! empty( $cleaned_subquery ) ) { 787 $cleaned_query[] = $cleaned_subquery; 788 } 789 } 790 } 791 792 return $cleaned_query; 793 } 794 795 /** 796 * Determine whether a clause is first-order. 797 * 798 * A "first-order" clause is one that contains any of the first-order 799 * clause keys ('terms', 'taxonomy', 'include_children', 'field', 800 * 'operator'). An empty clause also counts as a first-order clause, 801 * for backward compatibility. Any clause that doesn't meet this is 802 * determined, by process of elimination, to be a higher-order query. 803 * 804 * @since 4.1.0 805 * @access protected 806 * 807 * @param array $query Tax query arguments. 808 * @return bool Whether the query clause is a first-order clause. 809 */ 810 protected static function is_first_order_clause( $query ) { 811 return 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 ); 721 812 } 722 813 … … 727 818 * @access public 728 819 * 729 * @param string $primary_table 730 * @param string $primary_id_column 731 * @return array 820 * @param string $primary_table Database table where the object being filtered is stored (eg wp_users). 821 * @param string $primary_id_column ID column for the filtered object in $primary_table. 822 * @return array { 823 * Array containing JOIN and WHERE SQL clauses to append to the main query. 824 * 825 * @type string $join SQL fragment to append to the main JOIN clause. 826 * @type string $where SQL fragment to append to the main WHERE clause. 827 * } 732 828 */ 733 829 public function get_sql( $primary_table, $primary_id_column ) { 830 $this->primary_table = $primary_table; 831 $this->primary_id_column = $primary_id_column; 832 833 return $this->get_sql_clauses(); 834 } 835 836 /** 837 * Generate SQL clauses to be appended to a main query. 838 * 839 * Called by the public {@see WP_Tax_Query::get_sql()}, this method 840 * is abstracted out to maintain parity with the other Query classes. 841 * 842 * @since 4.1.0 843 * @access protected 844 * 845 * @return array { 846 * Array containing JOIN and WHERE SQL clauses to append to the main query. 847 * 848 * @type string $join SQL fragment to append to the main JOIN clause. 849 * @type string $where SQL fragment to append to the main WHERE clause. 850 * } 851 */ 852 protected function get_sql_clauses() { 853 $sql = $this->get_sql_for_query( $this->queries ); 854 855 if ( ! empty( $sql['where'] ) ) { 856 $sql['where'] = ' AND ' . $sql['where']; 857 } 858 859 return $sql; 860 } 861 862 /** 863 * Generate SQL clauses for a single query array. 864 * 865 * If nested subqueries are found, this method recurses the tree to 866 * produce the properly nested SQL. 867 * 868 * @since 4.1.0 869 * @access protected 870 * 871 * @param array $query Query to parse. 872 * @param int $depth Optional. Number of tree levels deep we currently are. 873 * Used to calculate indentation. 874 * @return array { 875 * Array containing JOIN and WHERE SQL clauses to append to a single query array. 876 * 877 * @type string $join SQL fragment to append to the main JOIN clause. 878 * @type string $where SQL fragment to append to the main WHERE clause. 879 * } 880 */ 881 protected function get_sql_for_query( $query, $depth = 0 ) { 882 $sql_chunks = array( 883 'join' => array(), 884 'where' => array(), 885 ); 886 887 $sql = array( 888 'join' => '', 889 'where' => '', 890 ); 891 892 $indent = ''; 893 for ( $i = 0; $i < $depth; $i++ ) { 894 $indent .= " "; 895 } 896 897 foreach ( $query as $key => $clause ) { 898 if ( 'relation' === $key ) { 899 $relation = $query['relation']; 900 } else if ( is_array( $clause ) ) { 901 902 // This is a first-order clause. 903 if ( $this->is_first_order_clause( $clause ) ) { 904 $clause_sql = $this->get_sql_for_clause( $clause, $query ); 905 906 $where_count = count( $clause_sql['where'] ); 907 if ( ! $where_count ) { 908 $sql_chunks['where'][] = ''; 909 } else if ( 1 === $where_count ) { 910 $sql_chunks['where'][] = $clause_sql['where'][0]; 911 } else { 912 $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )'; 913 } 914 915 $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] ); 916 // This is a subquery, so we recurse. 917 } else { 918 $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 ); 919 920 $sql_chunks['where'][] = $clause_sql['where']; 921 $sql_chunks['join'][] = $clause_sql['join']; 922 } 923 } 924 } 925 926 // Filter to remove empties. 927 $sql_chunks['join'] = array_filter( $sql_chunks['join'] ); 928 $sql_chunks['where'] = array_filter( $sql_chunks['where'] ); 929 930 if ( empty( $relation ) ) { 931 $relation = 'AND'; 932 } 933 934 // Filter duplicate JOIN clauses and combine into a single string. 935 if ( ! empty( $sql_chunks['join'] ) ) { 936 $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) ); 937 } 938 939 // Generate a single WHERE clause with proper brackets and indentation. 940 if ( ! empty( $sql_chunks['where'] ) ) { 941 $sql['where'] = '( ' . "\n " . $indent . implode( ' ' . "\n " . $indent . $relation . ' ' . "\n " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')'; 942 } 943 944 return $sql; 945 } 946 947 /** 948 * Generate SQL JOIN and WHERE clauses for a first-order query clause. 949 950 * @since 4.1.0 951 * @access public 952 * 953 * @param array $clause Query clause. 954 * @param array $parent_query Parent query array. 955 * @return array { 956 * Array containing JOIN and WHERE SQL clauses to append to a first-order query. 957 * 958 * @type string $join SQL fragment to append to the main JOIN clause. 959 * @type string $where SQL fragment to append to the main WHERE clause. 960 * } 961 */ 962 public function get_sql_for_clause( $clause, $parent_query ) { 734 963 global $wpdb; 735 964 965 $sql = array( 966 'where' => array(), 967 'join' => array(), 968 ); 969 736 970 $join = ''; 737 $where = array(); 738 $i = 0; 739 $count = count( $this->queries ); 740 741 foreach ( $this->queries as $index => $query ) { 742 $this->clean_query( $query ); 743 744 if ( is_wp_error( $query ) ) { 971 972 $this->clean_query( $clause ); 973 974 if ( is_wp_error( $clause ) ) { 975 return self::$no_results; 976 } 977 978 $terms = $clause['terms']; 979 $operator = strtoupper( $clause['operator'] ); 980 981 if ( 'IN' == $operator ) { 982 983 if ( empty( $terms ) ) { 745 984 return self::$no_results; 746 985 } 747 986 748 $terms = $query['terms']; 749 $operator = strtoupper( $query['operator'] ); 750 751 if ( 'IN' == $operator ) { 752 753 if ( empty( $terms ) ) { 754 if ( 'OR' == $this->relation ) { 755 if ( ( $index + 1 === $count ) && empty( $where ) ) { 756 return self::$no_results; 757 } 758 continue; 759 } else { 760 return self::$no_results; 761 } 762 } 763 764 $terms = implode( ',', $terms ); 765 766 $alias = $i ? 'tt' . $i : $wpdb->term_relationships; 767 768 $join .= " INNER JOIN $wpdb->term_relationships"; 769 $join .= $i ? " AS $alias" : ''; 770 $join .= " ON ($primary_table.$primary_id_column = $alias.object_id)"; 771 772 $where[] = "$alias.term_taxonomy_id $operator ($terms)"; 773 } elseif ( 'NOT IN' == $operator ) { 774 775 if ( empty( $terms ) ) { 776 continue; 777 } 778 779 $terms = implode( ',', $terms ); 780 781 $where[] = "$primary_table.$primary_id_column NOT IN ( 782 SELECT object_id 783 FROM $wpdb->term_relationships 784 WHERE term_taxonomy_id IN ($terms) 785 )"; 786 } elseif ( 'AND' == $operator ) { 787 788 if ( empty( $terms ) ) { 789 continue; 790 } 791 792 $num_terms = count( $terms ); 793 794 $terms = implode( ',', $terms ); 795 796 $where[] = "( 797 SELECT COUNT(1) 798 FROM $wpdb->term_relationships 799 WHERE term_taxonomy_id IN ($terms) 800 AND object_id = $primary_table.$primary_id_column 801 ) = $num_terms"; 987 $terms = implode( ',', $terms ); 988 989 $i = count( $this->table_aliases ); 990 $alias = $i ? 'tt' . $i : $wpdb->term_relationships; 991 $this->table_aliases[] = $alias; 992 993 $join .= " INNER JOIN $wpdb->term_relationships"; 994 $join .= $i ? " AS $alias" : ''; 995 $join .= " ON ($this->primary_table.$this->primary_id_column = $alias.object_id)"; 996 997 $where = "$alias.term_taxonomy_id $operator ($terms)"; 998 999 } elseif ( 'NOT IN' == $operator ) { 1000 1001 if ( empty( $terms ) ) { 1002 continue; 802 1003 } 803 1004 804 $i++; 805 } 806 807 if ( ! empty( $where ) ) { 808 $where = ' AND ( ' . implode( " $this->relation ", $where ) . ' )'; 809 } else { 810 $where = ''; 811 } 812 return compact( 'join', 'where' ); 813 } 1005 $terms = implode( ',', $terms ); 1006 1007 $where = "$this->primary_table.$this->primary_id_column NOT IN ( 1008 SELECT object_id 1009 FROM $wpdb->term_relationships 1010 WHERE term_taxonomy_id IN ($terms) 1011 )"; 1012 1013 } elseif ( 'AND' == $operator ) { 1014 1015 if ( empty( $terms ) ) { 1016 continue; 1017 } 1018 1019 $num_terms = count( $terms ); 1020 1021 $terms = implode( ',', $terms ); 1022 1023 $where = "( 1024 SELECT COUNT(1) 1025 FROM $wpdb->term_relationships 1026 WHERE term_taxonomy_id IN ($terms) 1027 AND object_id = $this->primary_table.$this->primary_id_column 1028 ) = $num_terms"; 1029 } 1030 1031 $sql['join'][] = $join; 1032 $sql['where'][] = $where; 1033 return $sql; 1034 } 1035 814 1036 815 1037 /** … … 819 1041 * @access private 820 1042 * 821 * @param array &$query The single query 1043 * @param array &$query The single query. 822 1044 */ 823 1045 private function clean_query( &$query ) { … … 859 1081 * @since 3.2.0 860 1082 * 861 * @param array &$query The single query 862 * @param string $resulting_field The resulting field 1083 * @param array &$query The single query. 1084 * @param string $resulting_field The resulting field. Accepts 'slug', 'name', 'term_taxonomy_id', 1085 * or 'term_id'. Default: 'term_id'. 863 1086 */ 864 1087 public function transform_query( &$query, $resulting_field ) {
Note: See TracChangeset
for help on using the changeset viewer.