home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress2 / wp-includes / class-wp-meta-query.php < prev    next >
Encoding:
PHP Script  |  2017-10-02  |  21.7 KB  |  731 lines

  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.  */
  22. class WP_Meta_Query {
  23.     /**
  24.      * Array of metadata queries.
  25.      *
  26.      * See WP_Meta_Query::__construct() for information on meta query arguments.
  27.      *
  28.      * @since 3.2.0
  29.      * @var array
  30.      */
  31.     public $queries = array();
  32.  
  33.     /**
  34.      * The relation between the queries. Can be one of 'AND' or 'OR'.
  35.      *
  36.      * @since 3.2.0
  37.      * @var string
  38.      */
  39.     public $relation;
  40.  
  41.     /**
  42.      * Database table to query for the metadata.
  43.      *
  44.      * @since 4.1.0
  45.      * @var string
  46.      */
  47.     public $meta_table;
  48.  
  49.     /**
  50.      * Column in meta_table that represents the ID of the object the metadata belongs to.
  51.      *
  52.      * @since 4.1.0
  53.      * @var string
  54.      */
  55.     public $meta_id_column;
  56.  
  57.     /**
  58.      * Database table that where the metadata's objects are stored (eg $wpdb->users).
  59.      *
  60.      * @since 4.1.0
  61.      * @var string
  62.      */
  63.     public $primary_table;
  64.  
  65.     /**
  66.      * Column in primary_table that represents the ID of the object.
  67.      *
  68.      * @since 4.1.0
  69.      * @var string
  70.      */
  71.     public $primary_id_column;
  72.  
  73.     /**
  74.      * A flat list of table aliases used in JOIN clauses.
  75.      *
  76.      * @since 4.1.0
  77.      * @var array
  78.      */
  79.     protected $table_aliases = array();
  80.  
  81.     /**
  82.      * A flat list of clauses, keyed by clause 'name'.
  83.      *
  84.      * @since 4.2.0
  85.      * @var array
  86.      */
  87.     protected $clauses = array();
  88.  
  89.     /**
  90.      * Whether the query contains any OR relations.
  91.      *
  92.      * @since 4.3.0
  93.      * @var bool
  94.      */
  95.     protected $has_or_relation = false;
  96.  
  97.     /**
  98.      * Constructor.
  99.      *
  100.      * @since 3.2.0
  101.      * @since 4.2.0 Introduced support for naming query clauses by associative array keys.
  102.      *
  103.      *
  104.      * @param array $meta_query {
  105.      *     Array of meta query clauses. When first-order clauses or sub-clauses use strings as
  106.      *     their array keys, they may be referenced in the 'orderby' parameter of the parent query.
  107.      *
  108.      *     @type string $relation Optional. The MySQL keyword used to join
  109.      *                            the clauses of the query. Accepts 'AND', or 'OR'. Default 'AND'.
  110.      *     @type array {
  111.      *         Optional. An array of first-order clause parameters, or another fully-formed meta query.
  112.      *
  113.      *         @type string $key     Meta key to filter by.
  114.      *         @type string $value   Meta value to filter by.
  115.      *         @type string $compare MySQL operator used for comparing the $value. Accepts '=',
  116.      *                               '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE',
  117.      *                               'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'REGEXP',
  118.      *                               'NOT REGEXP', 'RLIKE', 'EXISTS' or 'NOT EXISTS'.
  119.      *                               Default is 'IN' when `$value` is an array, '=' otherwise.
  120.      *         @type string $type    MySQL data type that the meta_value column will be CAST to for
  121.      *                               comparisons. Accepts 'NUMERIC', 'BINARY', 'CHAR', 'DATE',
  122.      *                               'DATETIME', 'DECIMAL', 'SIGNED', 'TIME', or 'UNSIGNED'.
  123.      *                               Default is 'CHAR'.
  124.      *     }
  125.      * }
  126.      */
  127.     public function __construct( $meta_query = false ) {
  128.         if ( !$meta_query )
  129.             return;
  130.  
  131.         if ( isset( $meta_query['relation'] ) && strtoupper( $meta_query['relation'] ) == 'OR' ) {
  132.             $this->relation = 'OR';
  133.         } else {
  134.             $this->relation = 'AND';
  135.         }
  136.  
  137.         $this->queries = $this->sanitize_query( $meta_query );
  138.     }
  139.  
  140.     /**
  141.      * Ensure the 'meta_query' argument passed to the class constructor is well-formed.
  142.      *
  143.      * Eliminates empty items and ensures that a 'relation' is set.
  144.      *
  145.      * @since 4.1.0
  146.      *
  147.      * @param array $queries Array of query clauses.
  148.      * @return array Sanitized array of query clauses.
  149.      */
  150.     public function sanitize_query( $queries ) {
  151.         $clean_queries = array();
  152.  
  153.         if ( ! is_array( $queries ) ) {
  154.             return $clean_queries;
  155.         }
  156.  
  157.         foreach ( $queries as $key => $query ) {
  158.             if ( 'relation' === $key ) {
  159.                 $relation = $query;
  160.  
  161.             } elseif ( ! is_array( $query ) ) {
  162.                 continue;
  163.  
  164.             // First-order clause.
  165.             } elseif ( $this->is_first_order_clause( $query ) ) {
  166.                 if ( isset( $query['value'] ) && array() === $query['value'] ) {
  167.                     unset( $query['value'] );
  168.                 }
  169.  
  170.                 $clean_queries[ $key ] = $query;
  171.  
  172.             // Otherwise, it's a nested query, so we recurse.
  173.             } else {
  174.                 $cleaned_query = $this->sanitize_query( $query );
  175.  
  176.                 if ( ! empty( $cleaned_query ) ) {
  177.                     $clean_queries[ $key ] = $cleaned_query;
  178.                 }
  179.             }
  180.         }
  181.  
  182.         if ( empty( $clean_queries ) ) {
  183.             return $clean_queries;
  184.         }
  185.  
  186.         // Sanitize the 'relation' key provided in the query.
  187.         if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) {
  188.             $clean_queries['relation'] = 'OR';
  189.             $this->has_or_relation = true;
  190.  
  191.         /*
  192.          * If there is only a single clause, call the relation 'OR'.
  193.          * This value will not actually be used to join clauses, but it
  194.          * simplifies the logic around combining key-only queries.
  195.          */
  196.         } elseif ( 1 === count( $clean_queries ) ) {
  197.             $clean_queries['relation'] = 'OR';
  198.  
  199.         // Default to AND.
  200.         } else {
  201.             $clean_queries['relation'] = 'AND';
  202.         }
  203.  
  204.         return $clean_queries;
  205.     }
  206.  
  207.     /**
  208.      * Determine whether a query clause is first-order.
  209.      *
  210.      * A first-order meta query clause is one that has either a 'key' or
  211.      * a 'value' array key.
  212.      *
  213.      * @since 4.1.0
  214.      *
  215.      * @param array $query Meta query arguments.
  216.      * @return bool Whether the query clause is a first-order clause.
  217.      */
  218.     protected function is_first_order_clause( $query ) {
  219.         return isset( $query['key'] ) || isset( $query['value'] );
  220.     }
  221.  
  222.     /**
  223.      * Constructs a meta query based on 'meta_*' query vars
  224.      *
  225.      * @since 3.2.0
  226.      *
  227.      * @param array $qv The query variables
  228.      */
  229.     public function parse_query_vars( $qv ) {
  230.         $meta_query = array();
  231.  
  232.         /*
  233.          * For orderby=meta_value to work correctly, simple query needs to be
  234.          * first (so that its table join is against an unaliased meta table) and
  235.          * needs to be its own clause (so it doesn't interfere with the logic of
  236.          * the rest of the meta_query).
  237.          */
  238.         $primary_meta_query = array();
  239.         foreach ( array( 'key', 'compare', 'type' ) as $key ) {
  240.             if ( ! empty( $qv[ "meta_$key" ] ) ) {
  241.                 $primary_meta_query[ $key ] = $qv[ "meta_$key" ];
  242.             }
  243.         }
  244.  
  245.         // WP_Query sets 'meta_value' = '' by default.
  246.         if ( isset( $qv['meta_value'] ) && '' !== $qv['meta_value'] && ( ! is_array( $qv['meta_value'] ) || $qv['meta_value'] ) ) {
  247.             $primary_meta_query['value'] = $qv['meta_value'];
  248.         }
  249.  
  250.         $existing_meta_query = isset( $qv['meta_query'] ) && is_array( $qv['meta_query'] ) ? $qv['meta_query'] : array();
  251.  
  252.         if ( ! empty( $primary_meta_query ) && ! empty( $existing_meta_query ) ) {
  253.             $meta_query = array(
  254.                 'relation' => 'AND',
  255.                 $primary_meta_query,
  256.                 $existing_meta_query,
  257.             );
  258.         } elseif ( ! empty( $primary_meta_query ) ) {
  259.             $meta_query = array(
  260.                 $primary_meta_query,
  261.             );
  262.         } elseif ( ! empty( $existing_meta_query ) ) {
  263.             $meta_query = $existing_meta_query;
  264.         }
  265.  
  266.         $this->__construct( $meta_query );
  267.     }
  268.  
  269.     /**
  270.      * Return the appropriate alias for the given meta type if applicable.
  271.      *
  272.      * @since 3.7.0
  273.      *
  274.      * @param string $type MySQL type to cast meta_value.
  275.      * @return string MySQL type.
  276.      */
  277.     public function get_cast_for_type( $type = '' ) {
  278.         if ( empty( $type ) )
  279.             return 'CHAR';
  280.  
  281.         $meta_type = strtoupper( $type );
  282.  
  283.         if ( ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $meta_type ) )
  284.             return 'CHAR';
  285.  
  286.         if ( 'NUMERIC' == $meta_type )
  287.             $meta_type = 'SIGNED';
  288.  
  289.         return $meta_type;
  290.     }
  291.  
  292.     /**
  293.      * Generates SQL clauses to be appended to a main query.
  294.      *
  295.      * @since 3.2.0
  296.      *
  297.      * @param string $type              Type of meta, eg 'user', 'post'.
  298.      * @param string $primary_table     Database table where the object being filtered is stored (eg wp_users).
  299.      * @param string $primary_id_column ID column for the filtered object in $primary_table.
  300.      * @param object $context           Optional. The main query object.
  301.      * @return false|array {
  302.      *     Array containing JOIN and WHERE SQL clauses to append to the main query.
  303.      *
  304.      *     @type string $join  SQL fragment to append to the main JOIN clause.
  305.      *     @type string $where SQL fragment to append to the main WHERE clause.
  306.      * }
  307.      */
  308.     public function get_sql( $type, $primary_table, $primary_id_column, $context = null ) {
  309.         if ( ! $meta_table = _get_meta_table( $type ) ) {
  310.             return false;
  311.         }
  312.  
  313.         $this->table_aliases = array();
  314.  
  315.         $this->meta_table     = $meta_table;
  316.         $this->meta_id_column = sanitize_key( $type . '_id' );
  317.  
  318.         $this->primary_table     = $primary_table;
  319.         $this->primary_id_column = $primary_id_column;
  320.  
  321.         $sql = $this->get_sql_clauses();
  322.  
  323.         /*
  324.          * If any JOINs are LEFT JOINs (as in the case of NOT EXISTS), then all JOINs should
  325.          * be LEFT. Otherwise posts with no metadata will be excluded from results.
  326.          */
  327.         if ( false !== strpos( $sql['join'], 'LEFT JOIN' ) ) {
  328.             $sql['join'] = str_replace( 'INNER JOIN', 'LEFT JOIN', $sql['join'] );
  329.         }
  330.  
  331.         /**
  332.          * Filters the meta query's generated SQL.
  333.          *
  334.          * @since 3.1.0
  335.          *
  336.          * @param array  $clauses           Array containing the query's JOIN and WHERE clauses.
  337.          * @param array  $queries           Array of meta queries.
  338.          * @param string $type              Type of meta.
  339.          * @param string $primary_table     Primary table.
  340.          * @param string $primary_id_column Primary column ID.
  341.          * @param object $context           The main query object.
  342.          */
  343.         return apply_filters_ref_array( 'get_meta_sql', array( $sql, $this->queries, $type, $primary_table, $primary_id_column, $context ) );
  344.     }
  345.  
  346.     /**
  347.      * Generate SQL clauses to be appended to a main query.
  348.      *
  349.      * Called by the public WP_Meta_Query::get_sql(), this method is abstracted
  350.      * out to maintain parity with the other Query classes.
  351.      *
  352.      * @since 4.1.0
  353.      *
  354.      * @return array {
  355.      *     Array containing JOIN and WHERE SQL clauses to append to the main query.
  356.      *
  357.      *     @type string $join  SQL fragment to append to the main JOIN clause.
  358.      *     @type string $where SQL fragment to append to the main WHERE clause.
  359.      * }
  360.      */
  361.     protected function get_sql_clauses() {
  362.         /*
  363.          * $queries are passed by reference to get_sql_for_query() for recursion.
  364.          * To keep $this->queries unaltered, pass a copy.
  365.          */
  366.         $queries = $this->queries;
  367.         $sql = $this->get_sql_for_query( $queries );
  368.  
  369.         if ( ! empty( $sql['where'] ) ) {
  370.             $sql['where'] = ' AND ' . $sql['where'];
  371.         }
  372.  
  373.         return $sql;
  374.     }
  375.  
  376.     /**
  377.      * Generate SQL clauses for a single query array.
  378.      *
  379.      * If nested subqueries are found, this method recurses the tree to
  380.      * produce the properly nested SQL.
  381.      *
  382.      * @since 4.1.0
  383.      *
  384.      * @param array $query Query to parse (passed by reference).
  385.      * @param int   $depth Optional. Number of tree levels deep we currently are.
  386.      *                     Used to calculate indentation. Default 0.
  387.      * @return array {
  388.      *     Array containing JOIN and WHERE SQL clauses to append to a single query array.
  389.      *
  390.      *     @type string $join  SQL fragment to append to the main JOIN clause.
  391.      *     @type string $where SQL fragment to append to the main WHERE clause.
  392.      * }
  393.      */
  394.     protected function get_sql_for_query( &$query, $depth = 0 ) {
  395.         $sql_chunks = array(
  396.             'join'  => array(),
  397.             'where' => array(),
  398.         );
  399.  
  400.         $sql = array(
  401.             'join'  => '',
  402.             'where' => '',
  403.         );
  404.  
  405.         $indent = '';
  406.         for ( $i = 0; $i < $depth; $i++ ) {
  407.             $indent .= "  ";
  408.         }
  409.  
  410.         foreach ( $query as $key => &$clause ) {
  411.             if ( 'relation' === $key ) {
  412.                 $relation = $query['relation'];
  413.             } elseif ( is_array( $clause ) ) {
  414.  
  415.                 // This is a first-order clause.
  416.                 if ( $this->is_first_order_clause( $clause ) ) {
  417.                     $clause_sql = $this->get_sql_for_clause( $clause, $query, $key );
  418.  
  419.                     $where_count = count( $clause_sql['where'] );
  420.                     if ( ! $where_count ) {
  421.                         $sql_chunks['where'][] = '';
  422.                     } elseif ( 1 === $where_count ) {
  423.                         $sql_chunks['where'][] = $clause_sql['where'][0];
  424.                     } else {
  425.                         $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
  426.                     }
  427.  
  428.                     $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
  429.                 // This is a subquery, so we recurse.
  430.                 } else {
  431.                     $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
  432.  
  433.                     $sql_chunks['where'][] = $clause_sql['where'];
  434.                     $sql_chunks['join'][]  = $clause_sql['join'];
  435.                 }
  436.             }
  437.         }
  438.  
  439.         // Filter to remove empties.
  440.         $sql_chunks['join']  = array_filter( $sql_chunks['join'] );
  441.         $sql_chunks['where'] = array_filter( $sql_chunks['where'] );
  442.  
  443.         if ( empty( $relation ) ) {
  444.             $relation = 'AND';
  445.         }
  446.  
  447.         // Filter duplicate JOIN clauses and combine into a single string.
  448.         if ( ! empty( $sql_chunks['join'] ) ) {
  449.             $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
  450.         }
  451.  
  452.         // Generate a single WHERE clause with proper brackets and indentation.
  453.         if ( ! empty( $sql_chunks['where'] ) ) {
  454.             $sql['where'] = '( ' . "\n  " . $indent . implode( ' ' . "\n  " . $indent . $relation . ' ' . "\n  " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')';
  455.         }
  456.  
  457.         return $sql;
  458.     }
  459.  
  460.     /**
  461.      * Generate SQL JOIN and WHERE clauses for a first-order query clause.
  462.      *
  463.      * "First-order" means that it's an array with a 'key' or 'value'.
  464.      *
  465.      * @since 4.1.0
  466.      *
  467.      * @global wpdb $wpdb WordPress database abstraction object.
  468.      *
  469.      * @param array  $clause       Query clause (passed by reference).
  470.      * @param array  $parent_query Parent query array.
  471.      * @param string $clause_key   Optional. The array key used to name the clause in the original `$meta_query`
  472.      *                             parameters. If not provided, a key will be generated automatically.
  473.      * @return array {
  474.      *     Array containing JOIN and WHERE SQL clauses to append to a first-order query.
  475.      *
  476.      *     @type string $join  SQL fragment to append to the main JOIN clause.
  477.      *     @type string $where SQL fragment to append to the main WHERE clause.
  478.      * }
  479.      */
  480.     public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' ) {
  481.         global $wpdb;
  482.  
  483.         $sql_chunks = array(
  484.             'where' => array(),
  485.             'join' => array(),
  486.         );
  487.  
  488.         if ( isset( $clause['compare'] ) ) {
  489.             $clause['compare'] = strtoupper( $clause['compare'] );
  490.         } else {
  491.             $clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '=';
  492.         }
  493.  
  494.         if ( ! in_array( $clause['compare'], array(
  495.             '=', '!=', '>', '>=', '<', '<=',
  496.             'LIKE', 'NOT LIKE',
  497.             'IN', 'NOT IN',
  498.             'BETWEEN', 'NOT BETWEEN',
  499.             'EXISTS', 'NOT EXISTS',
  500.             'REGEXP', 'NOT REGEXP', 'RLIKE'
  501.         ) ) ) {
  502.             $clause['compare'] = '=';
  503.         }
  504.  
  505.         $meta_compare = $clause['compare'];
  506.  
  507.         // First build the JOIN clause, if one is required.
  508.         $join = '';
  509.  
  510.         // We prefer to avoid joins if possible. Look for an existing join compatible with this clause.
  511.         $alias = $this->find_compatible_table_alias( $clause, $parent_query );
  512.         if ( false === $alias ) {
  513.             $i = count( $this->table_aliases );
  514.             $alias = $i ? 'mt' . $i : $this->meta_table;
  515.  
  516.             // JOIN clauses for NOT EXISTS have their own syntax.
  517.             if ( 'NOT EXISTS' === $meta_compare ) {
  518.                 $join .= " LEFT JOIN $this->meta_table";
  519.                 $join .= $i ? " AS $alias" : '';
  520.                 $join .= $wpdb->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key = %s )", $clause['key'] );
  521.  
  522.             // All other JOIN clauses.
  523.             } else {
  524.                 $join .= " INNER JOIN $this->meta_table";
  525.                 $join .= $i ? " AS $alias" : '';
  526.                 $join .= " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column )";
  527.             }
  528.  
  529.             $this->table_aliases[] = $alias;
  530.             $sql_chunks['join'][] = $join;
  531.         }
  532.  
  533.         // Save the alias to this clause, for future siblings to find.
  534.         $clause['alias'] = $alias;
  535.  
  536.         // Determine the data type.
  537.         $_meta_type = isset( $clause['type'] ) ? $clause['type'] : '';
  538.         $meta_type  = $this->get_cast_for_type( $_meta_type );
  539.         $clause['cast'] = $meta_type;
  540.  
  541.         // Fallback for clause keys is the table alias. Key must be a string.
  542.         if ( is_int( $clause_key ) || ! $clause_key ) {
  543.             $clause_key = $clause['alias'];
  544.         }
  545.  
  546.         // Ensure unique clause keys, so none are overwritten.
  547.         $iterator = 1;
  548.         $clause_key_base = $clause_key;
  549.         while ( isset( $this->clauses[ $clause_key ] ) ) {
  550.             $clause_key = $clause_key_base . '-' . $iterator;
  551.             $iterator++;
  552.         }
  553.  
  554.         // Store the clause in our flat array.
  555.         $this->clauses[ $clause_key ] =& $clause;
  556.  
  557.         // Next, build the WHERE clause.
  558.  
  559.         // meta_key.
  560.         if ( array_key_exists( 'key', $clause ) ) {
  561.             if ( 'NOT EXISTS' === $meta_compare ) {
  562.                 $sql_chunks['where'][] = $alias . '.' . $this->meta_id_column . ' IS NULL';
  563.             } else {
  564.                 $sql_chunks['where'][] = $wpdb->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) );
  565.             }
  566.         }
  567.  
  568.         // meta_value.
  569.         if ( array_key_exists( 'value', $clause ) ) {
  570.             $meta_value = $clause['value'];
  571.  
  572.             if ( in_array( $meta_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
  573.                 if ( ! is_array( $meta_value ) ) {
  574.                     $meta_value = preg_split( '/[,\s]+/', $meta_value );
  575.                 }
  576.             } else {
  577.                 $meta_value = trim( $meta_value );
  578.             }
  579.  
  580.             switch ( $meta_compare ) {
  581.                 case 'IN' :
  582.                 case 'NOT IN' :
  583.                     $meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')';
  584.                     $where = $wpdb->prepare( $meta_compare_string, $meta_value );
  585.                     break;
  586.  
  587.                 case 'BETWEEN' :
  588.                 case 'NOT BETWEEN' :
  589.                     $meta_value = array_slice( $meta_value, 0, 2 );
  590.                     $where = $wpdb->prepare( '%s AND %s', $meta_value );
  591.                     break;
  592.  
  593.                 case 'LIKE' :
  594.                 case 'NOT LIKE' :
  595.                     $meta_value = '%' . $wpdb->esc_like( $meta_value ) . '%';
  596.                     $where = $wpdb->prepare( '%s', $meta_value );
  597.                     break;
  598.  
  599.                 // EXISTS with a value is interpreted as '='.
  600.                 case 'EXISTS' :
  601.                     $meta_compare = '=';
  602.                     $where = $wpdb->prepare( '%s', $meta_value );
  603.                     break;
  604.  
  605.                 // 'value' is ignored for NOT EXISTS.
  606.                 case 'NOT EXISTS' :
  607.                     $where = '';
  608.                     break;
  609.  
  610.                 default :
  611.                     $where = $wpdb->prepare( '%s', $meta_value );
  612.                     break;
  613.  
  614.             }
  615.  
  616.             if ( $where ) {
  617.                 if ( 'CHAR' === $meta_type ) {
  618.                     $sql_chunks['where'][] = "$alias.meta_value {$meta_compare} {$where}";
  619.                 } else {
  620.                     $sql_chunks['where'][] = "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$where}";
  621.                 }
  622.             }
  623.         }
  624.  
  625.         /*
  626.          * Multiple WHERE clauses (for meta_key and meta_value) should
  627.          * be joined in parentheses.
  628.          */
  629.         if ( 1 < count( $sql_chunks['where'] ) ) {
  630.             $sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' );
  631.         }
  632.  
  633.         return $sql_chunks;
  634.     }
  635.  
  636.     /**
  637.      * Get a flattened list of sanitized meta clauses.
  638.      *
  639.      * This array should be used for clause lookup, as when the table alias and CAST type must be determined for
  640.      * a value of 'orderby' corresponding to a meta clause.
  641.      *
  642.      * @since 4.2.0
  643.      *
  644.      * @return array Meta clauses.
  645.      */
  646.     public function get_clauses() {
  647.         return $this->clauses;
  648.     }
  649.  
  650.     /**
  651.      * Identify an existing table alias that is compatible with the current
  652.      * query clause.
  653.      *
  654.      * We avoid unnecessary table joins by allowing each clause to look for
  655.      * an existing table alias that is compatible with the query that it
  656.      * needs to perform.
  657.      *
  658.      * An existing alias is compatible if (a) it is a sibling of `$clause`
  659.      * (ie, it's under the scope of the same relation), and (b) the combination
  660.      * of operator and relation between the clauses allows for a shared table join.
  661.      * In the case of WP_Meta_Query, this only applies to 'IN' clauses that are
  662.      * connected by the relation 'OR'.
  663.      *
  664.      * @since 4.1.0
  665.      *
  666.      * @param  array       $clause       Query clause.
  667.      * @param  array       $parent_query Parent query of $clause.
  668.      * @return string|bool Table alias if found, otherwise false.
  669.      */
  670.     protected function find_compatible_table_alias( $clause, $parent_query ) {
  671.         $alias = false;
  672.  
  673.         foreach ( $parent_query as $sibling ) {
  674.             // If the sibling has no alias yet, there's nothing to check.
  675.             if ( empty( $sibling['alias'] ) ) {
  676.                 continue;
  677.             }
  678.  
  679.             // We're only interested in siblings that are first-order clauses.
  680.             if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) {
  681.                 continue;
  682.             }
  683.  
  684.             $compatible_compares = array();
  685.  
  686.             // Clauses connected by OR can share joins as long as they have "positive" operators.
  687.             if ( 'OR' === $parent_query['relation'] ) {
  688.                 $compatible_compares = array( '=', 'IN', 'BETWEEN', 'LIKE', 'REGEXP', 'RLIKE', '>', '>=', '<', '<=' );
  689.  
  690.             // Clauses joined by AND with "negative" operators share a join only if they also share a key.
  691.             } elseif ( isset( $sibling['key'] ) && isset( $clause['key'] ) && $sibling['key'] === $clause['key'] ) {
  692.                 $compatible_compares = array( '!=', 'NOT IN', 'NOT LIKE' );
  693.             }
  694.  
  695.             $clause_compare  = strtoupper( $clause['compare'] );
  696.             $sibling_compare = strtoupper( $sibling['compare'] );
  697.             if ( in_array( $clause_compare, $compatible_compares ) && in_array( $sibling_compare, $compatible_compares ) ) {
  698.                 $alias = $sibling['alias'];
  699.                 break;
  700.             }
  701.         }
  702.  
  703.         /**
  704.          * Filters the table alias identified as compatible with the current clause.
  705.          *
  706.          * @since 4.1.0
  707.          *
  708.          * @param string|bool $alias        Table alias, or false if none was found.
  709.          * @param array       $clause       First-order query clause.
  710.          * @param array       $parent_query Parent of $clause.
  711.          * @param object      $this         WP_Meta_Query object.
  712.          */
  713.         return apply_filters( 'meta_query_find_compatible_table_alias', $alias, $clause, $parent_query, $this ) ;
  714.     }
  715.  
  716.     /**
  717.      * Checks whether the current query has any OR relations.
  718.      *
  719.      * In some cases, the presence of an OR relation somewhere in the query will require
  720.      * the use of a `DISTINCT` or `GROUP BY` keyword in the `SELECT` clause. The current
  721.      * method can be used in these cases to determine whether such a clause is necessary.
  722.      *
  723.      * @since 4.3.0
  724.      *
  725.      * @return bool True if the query contains any `OR` relations, otherwise false.
  726.      */
  727.     public function has_or_relation() {
  728.         return $this->has_or_relation;
  729.     }
  730. }
  731.