Changeset 29923
- Timestamp:
- 10/16/2014 07:33:24 PM (9 years ago)
- Location:
- trunk
- Files:
-
- 3 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/date.php
r29797 r29923 1 1 <?php 2 2 /** 3 * WP_Date_Query will generate a MySQL WHERE clause for the specified date-based parameters.3 * Class for generating SQL clauses that filter a primary query according to date. 4 4 * 5 * Initialize the class by passing an array of arrays of parameters. 5 * `WP_Date_Query` is a helper that allows primary query classes, such as {@see WP_Query}, 6 * to filter their results by date columns, by generating `WHERE` subclauses to be attached 7 * to the primary SQL query string. 6 8 * 7 9 * @link http://codex.wordpress.org/Function_Reference/WP_Query Codex page. … … 11 13 class WP_Date_Query { 12 14 /** 13 * List of date queries. 15 * Array of date queries. 16 * 17 * See {@see WP_Date_Query::__construct()} for information on date query arguments. 14 18 * 15 19 * @since 3.7.0 … … 20 24 21 25 /** 22 * The relation between the queries. Can be either 'AND' or 'OR' and can be changed via the query arguments.26 * The default relation between top-level queries. Can be either 'AND' or 'OR'. 23 27 * 24 28 * @since 3.7.0 … … 47 51 48 52 /** 53 * Supported time-related parameter keys. 54 * 55 * @since 4.1.0 56 * @access public 57 * @var array 58 */ 59 public $time_keys = array( 'after', 'before', 'year', 'month', 'monthnum', 'week', 'w', 'dayofyear', 'day', 'dayofweek', 'hour', 'minute', 'second' ); 60 61 /** 49 62 * Constructor. 50 63 * 51 64 * @since 3.7.0 52 65 * @since 4.0.0 The $inclusive logic was updated to include all times within the date range. 66 * @access public 53 67 * 54 68 * @param array $date_query { 55 * One or more associative arrays of date query parameters.69 * Array of date query clauses. 56 70 * 57 71 * @type array { … … 61 75 * 'comment_date_gmt'. 62 76 * @type string $compare Optional. The comparison operator. 63 * Default '='. Accepts '=', '!=', '>', '>=', '<', '<=', 'IN', 'NOT IN',77 * Accepts '=', '!=', '>', '>=', '<', '<=', 'IN', 'NOT IN'. Default '='. 64 78 * 'BETWEEN', 'NOT BETWEEN'. 65 * @type string $relation Optional. The boolean relationship between the date quer yies.66 * Default 'OR'. Accepts 'OR', 'AND'.79 * @type string $relation Optional. The boolean relationship between the date queries. 80 * Accepts 'OR', 'AND'. Default 'OR'. 67 81 * @type array { 82 Optional. An array of first-order clause parameters, or another fully-formed date query. 68 83 * @type string|array $before Optional. Date to retrieve posts before. Accepts strtotime()-compatible 69 84 * string, or array of 'year', 'month', 'day' values. { … … 110 125 */ 111 126 public function __construct( $date_query, $default_column = 'post_date' ) { 112 if ( empty( $date_query ) || ! is_array( $date_query ) ) 127 128 if ( isset( $date_query['relation'] ) && 'OR' === strtoupper( $date_query['relation'] ) ) { 129 $this->relation = 'OR'; 130 } else { 131 $this->relation = 'AND'; 132 } 133 134 if ( ! is_array( $date_query ) ) { 113 135 return; 114 115 if ( isset( $date_query['relation'] ) && strtoupper( $date_query['relation'] ) == 'OR' ) 116 $this->relation = 'OR'; 117 else 118 $this->relation = 'AND'; 119 120 if ( ! empty( $date_query['column'] ) ) 121 $this->column = esc_sql( $date_query['column'] ); 122 else 123 $this->column = esc_sql( $default_column ); 136 } 137 138 // Support for passing time-based keys in the top level of the $date_query array. 139 if ( ! isset( $date_query[0] ) && ! empty( $date_query ) ) { 140 $date_query = array( $date_query ); 141 } 142 143 if ( empty( $date_query ) ) { 144 return; 145 } 146 147 if ( ! empty( $date_query['column'] ) ) { 148 $date_query['column'] = esc_sql( $date_query['column'] ); 149 } else { 150 $date_query['column'] = esc_sql( $default_column ); 151 } 124 152 125 153 $this->column = $this->validate_column( $this->column ); … … 127 155 $this->compare = $this->get_compare( $date_query ); 128 156 129 // If an array of arrays wasn't passed, fix it 130 if ( ! isset( $date_query[0] ) ) 131 $date_query = array( $date_query ); 132 133 $this->queries = array(); 134 foreach ( $date_query as $key => $query ) { 135 if ( ! is_array( $query ) ) 157 $this->queries = $this->sanitize_query( $date_query ); 158 159 return; 160 } 161 162 /** 163 * Recursive-friendly query sanitizer. 164 * 165 * Ensures that each query-level clause has a 'relation' key, and that 166 * each first-order clause contains all the necessary keys from 167 * $defaults. 168 * 169 * @since 4.1.0 170 * @access public 171 * 172 * @param array $query A tax_query query clause. 173 * @return array Sanitized queries. 174 */ 175 public function sanitize_query( $queries, $parent_query = null ) { 176 $cleaned_query = array(); 177 178 $defaults = array( 179 'column' => 'post_date', 180 'compare' => '=', 181 'relation' => 'AND', 182 ); 183 184 // Numeric keys should always have array values. 185 foreach ( $queries as $qkey => $qvalue ) { 186 if ( is_numeric( $qkey ) && ! is_array( $qvalue ) ) { 187 unset( $queries[ $qkey ] ); 188 } 189 } 190 191 // Each query should have a value for each default key. Inherit from the parent when possible. 192 foreach ( $defaults as $dkey => $dvalue ) { 193 if ( isset( $queries[ $dkey ] ) ) { 136 194 continue; 137 138 $this->queries[$key] = $query; 139 } 195 } 196 197 if ( isset( $parent_query[ $dkey ] ) ) { 198 $queries[ $dkey ] = $parent_query[ $dkey ]; 199 } else { 200 $queries[ $dkey ] = $dvalue; 201 } 202 } 203 204 foreach ( $queries as $key => $q ) { 205 if ( ! is_array( $q ) || in_array( $key, $this->time_keys, true ) ) { 206 // This is a first-order query. Trust the values and sanitize when building SQL. 207 $cleaned_query[ $key ] = $q; 208 } else { 209 // Any array without a time key is another query, so we recurse. 210 $cleaned_query[] = $this->sanitize_query( $q, $queries ); 211 } 212 } 213 214 return $cleaned_query; 215 } 216 217 /** 218 * Determine whether this is a first-order clause. 219 * 220 * Checks to see if the current clause has any time-related keys. 221 * If so, it's first-order. 222 * 223 * @param array $query Query clause. 224 * @return bool True if this is a first-order clause. 225 */ 226 protected function is_first_order_clause( $query ) { 227 $time_keys = array_intersect( $this->time_keys, array_keys( $query ) ); 228 return ! empty( $time_keys ); 140 229 } 141 230 … … 146 235 * @access public 147 236 * 148 * @param array $query A date query or a date subquery 149 * @return string The comparison operator 237 * @param array $query A date query or a date subquery. 238 * @return string The comparison operator. 150 239 */ 151 240 public function get_compare( $query ) { … … 185 274 186 275 /** 187 * Turns an array of date query parameters into a MySQL string.188 * 189 * @since 3.7.0 190 * @access public 191 * 192 * @return string MySQL WHERE parameters276 * Generate WHERE clause to be appended to a main query. 277 * 278 * @since 3.7.0 279 * @access public 280 * 281 * @return string MySQL WHERE clause. 193 282 */ 194 283 public function get_sql() { 195 // The parts of the final query 196 $where = array(); 197 198 foreach ( $this->queries as $key => $query ) { 199 $where_parts = $this->get_sql_for_subquery( $query ); 200 if ( $where_parts ) { 201 // Combine the parts of this subquery into a single string 202 $where[ $key ] = '( ' . implode( ' AND ', $where_parts ) . ' )'; 203 } 204 } 205 206 // Combine the subquery strings into a single string 207 if ( $where ) 208 $where = ' AND ( ' . implode( " {$this->relation} ", $where ) . ' )'; 209 else 210 $where = ''; 284 $sql = $this->get_sql_clauses(); 285 286 $where = $sql['where']; 211 287 212 288 /** … … 222 298 223 299 /** 224 * Turns a single date subquery into pieces for a WHERE clause. 225 * 226 * @since 3.7.0 227 * return array 300 * Generate SQL clauses to be appended to a main query. 301 * 302 * Called by the public {@see WP_Date_Query::get_sql()}, this method 303 * is abstracted out to maintain parity with the other Query classes. 304 * 305 * @since 4.1.0 306 * @access protected 307 * 308 * @return array { 309 * Array containing JOIN and WHERE SQL clauses to append to the main query. 310 * 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 protected function get_sql_clauses() { 316 $sql = $this->get_sql_for_query( $this->queries ); 317 318 if ( ! empty( $sql['where'] ) ) { 319 $sql['where'] = ' AND ' . $sql['where']; 320 } 321 322 return $sql; 323 } 324 325 /** 326 * Generate SQL clauses for a single query array. 327 * 328 * If nested subqueries are found, this method recurses the tree to 329 * produce the properly nested SQL. 330 * 331 * @since 4.1.0 332 * @access protected 333 * 334 * @param array $query Query to parse. 335 * @param int $depth Optional. Number of tree levels deep we currently are. 336 * Used to calculate indentation. 337 * @return array { 338 * Array containing JOIN and WHERE SQL clauses to append to a single query array. 339 * 340 * @type string $join SQL fragment to append to the main JOIN clause. 341 * @type string $where SQL fragment to append to the main WHERE clause. 342 * } 343 */ 344 protected function get_sql_for_query( $query, $depth = 0 ) { 345 $sql_chunks = array( 346 'join' => array(), 347 'where' => array(), 348 ); 349 350 $sql = array( 351 'join' => '', 352 'where' => '', 353 ); 354 355 $indent = ''; 356 for ( $i = 0; $i < $depth; $i++ ) { 357 $indent .= " "; 358 } 359 360 foreach ( $query as $key => $clause ) { 361 if ( 'relation' === $key ) { 362 $relation = $query['relation']; 363 } else if ( is_array( $clause ) ) { 364 365 // This is a first-order clause. 366 if ( $this->is_first_order_clause( $clause ) ) { 367 $clause_sql = $this->get_sql_for_clause( $clause, $query ); 368 369 $where_count = count( $clause_sql['where'] ); 370 if ( ! $where_count ) { 371 $sql_chunks['where'][] = ''; 372 } else if ( 1 === $where_count ) { 373 $sql_chunks['where'][] = $clause_sql['where'][0]; 374 } else { 375 $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )'; 376 } 377 378 $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] ); 379 // This is a subquery, so we recurse. 380 } else { 381 $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 ); 382 383 $sql_chunks['where'][] = $clause_sql['where']; 384 $sql_chunks['join'][] = $clause_sql['join']; 385 } 386 } 387 } 388 389 // Filter to remove empties. 390 $sql_chunks['join'] = array_filter( $sql_chunks['join'] ); 391 $sql_chunks['where'] = array_filter( $sql_chunks['where'] ); 392 393 if ( empty( $relation ) ) { 394 $relation = 'AND'; 395 } 396 397 // Filter duplicate JOIN clauses and combine into a single string. 398 if ( ! empty( $sql_chunks['join'] ) ) { 399 $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) ); 400 } 401 402 // Generate a single WHERE clause with proper brackets and indentation. 403 if ( ! empty( $sql_chunks['where'] ) ) { 404 $sql['where'] = '( ' . "\n " . $indent . implode( ' ' . "\n " . $indent . $relation . ' ' . "\n " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')'; 405 } 406 407 return $sql; 408 } 409 410 /** 411 * Turns a single date clause into pieces for a WHERE clause. 412 * 413 * A wrapper for get_sql_for_clause(), included here for backward 414 * compatibility while retaining the naming convention across Query classes. 415 * 416 * @since 3.7.0 417 * @access protected 418 * 419 * @param array $query Date query arguments. 420 * @return array { 421 * Array containing JOIN and WHERE SQL clauses to append to the main query. 422 * 423 * @type string $join SQL fragment to append to the main JOIN clause. 424 * @type string $where SQL fragment to append to the main WHERE clause. 425 * } 228 426 */ 229 427 protected function get_sql_for_subquery( $query ) { 428 return $this->get_sql_for_clause( $query, '' ); 429 } 430 431 /** 432 * Turns a first-order date query into SQL for a WHERE clause. 433 * 434 * @since 4.1.0 435 * @access protected 436 * 437 * @param array $query Date query clause. 438 * @param array $parent_query Parent query of the current date query. 439 * @return array { 440 * Array containing JOIN and WHERE SQL clauses to append to the main query. 441 * 442 * @type string $join SQL fragment to append to the main JOIN clause. 443 * @type string $where SQL fragment to append to the main WHERE clause. 444 * } 445 */ 446 protected function get_sql_for_clause( $query, $parent_query ) { 230 447 global $wpdb; 231 448 232 // The sub-parts of a $where part 449 // The sub-parts of a $where part. 233 450 $where_parts = array(); 234 451 … … 250 467 } 251 468 252 // Range queries 469 // Range queries. 253 470 if ( ! empty( $query['after'] ) ) 254 471 $where_parts[] = $wpdb->prepare( "$column $gt %s", $this->build_mysql_datetime( $query['after'], ! $inclusive ) ); … … 257 474 $where_parts[] = $wpdb->prepare( "$column $lt %s", $this->build_mysql_datetime( $query['before'], $inclusive ) ); 258 475 259 // Specific value queries 476 // Specific value queries. 260 477 261 478 if ( isset( $query['year'] ) && $value = $this->build_value( $compare, $query['year'] ) ) … … 282 499 283 500 if ( isset( $query['hour'] ) || isset( $query['minute'] ) || isset( $query['second'] ) ) { 284 // Avoid notices 501 // Avoid notices. 285 502 foreach ( array( 'hour', 'minute', 'second' ) as $unit ) { 286 if ( ! isset( $query[ $unit] ) ) {287 $query[ $unit] = null;503 if ( ! isset( $query[ $unit ] ) ) { 504 $query[ $unit ] = null; 288 505 } 289 506 } … … 294 511 } 295 512 296 return $where_parts; 513 /* 514 * Return an array of 'join' and 'where' for compatibility 515 * with other query classes. 516 */ 517 return array( 518 'where' => $where_parts, 519 'join' => array(), 520 ); 297 521 } 298 522 -
trunk/tests/phpunit/tests/date/query.php
r29906 r29923 56 56 'month' => 6, 57 57 ), 58 'column' => 'post_date', 59 'compare' => '=', 60 'relation' => 'AND', 58 61 ), 62 'column' => 'post_date', 63 'compare' => '=', 64 'relation' => 'AND', 59 65 ); 60 66 … … 74 80 ) ); 75 81 76 // Note: WP_Date_Query does not reset indexes77 82 $expected = array( 78 2 =>array(83 array( 79 84 'before' => array( 80 85 'year' => 2008, 81 86 'month' => 6, 82 87 ), 88 'column' => 'post_date', 89 'compare' => '=', 90 'relation' => 'AND', 83 91 ), 92 'column' => 'post_date', 93 'compare' => '=', 94 'relation' => 'AND', 84 95 ); 85 96 86 $this->assert Same( $expected, $q->queries );97 $this->assertEquals( $expected, $q->queries ); 87 98 } 88 99 -
trunk/tests/phpunit/tests/query/dateQuery.php
r29885 r29923 630 630 $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) ); 631 631 632 $this->assertContains( "AND ( ( MONTH( post_date ) = 5 ) ) AND", $this->q->request ); 633 634 $this->assertNotContains( "AND ( ( MONTH( post_date ) = 5 AND MONTH( post_date ) = 9 ) ) AND", $this->q->request ); 632 $this->assertContains( "MONTH( post_date ) = 5", $this->q->request ); 633 $this->assertNotContains( "MONTH( post_date ) = 9", $this->q->request ); 635 634 } 636 635 … … 654 653 $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) ); 655 654 656 $this->assertContains( "AND ( ( WEEK( post_date, 1 ) = 21 ) ) AND", $this->q->request ); 657 658 $this->assertNotContains( "AND ( ( WEEK( post_date, 1 ) = 21 AND WEEK( post_date, 1 ) = 22 ) ) AND", $this->q->request ); 655 $this->assertContains( "WEEK( post_date, 1 ) = 21", $this->q->request ); 656 $this->assertNotContains( "WEEK( post_date, 1 ) = 22", $this->q->request ); 657 } 658 659 /** 660 * @ticket 29822 661 */ 662 public function test_date_query_one_nested_query() { 663 $this->create_posts(); 664 665 $posts = $this->_get_query_result( array( 666 'date_query' => array( 667 'relation' => 'OR', 668 array( 669 'relation' => 'AND', 670 array( 671 'year' => 2004, 672 ), 673 array( 674 'month' => 1, 675 ), 676 ), 677 array( 678 'year' => 1984, 679 ), 680 ), 681 ) ); 682 683 $expected_dates = array( 684 '1984-07-28 19:28:56', 685 '2004-01-03 08:54:10', 686 ); 687 688 $this->assertEquals( $expected_dates, wp_list_pluck( $posts, 'post_date' ) ); 689 } 690 691 /** 692 * @ticket 29822 693 */ 694 public function test_date_query_one_nested_query_multiple_columns_relation_and() { 695 $p1 = $this->factory->post->create( array( 696 'post_date' => '2012-03-05 15:30:55', 697 ) ); 698 $this->update_post_modified( $p1, '2014-11-03 14:43:00' ); 699 700 $p2 = $this->factory->post->create( array( 701 'post_date' => '2012-05-05 15:30:55', 702 ) ); 703 $this->update_post_modified( $p2, '2014-10-03 14:43:00' ); 704 705 $p3 = $this->factory->post->create( array( 706 'post_date' => '2013-05-05 15:30:55', 707 ) ); 708 $this->update_post_modified( $p3, '2014-10-03 14:43:00' ); 709 710 $p4 = $this->factory->post->create( array( 711 'post_date' => '2012-02-05 15:30:55', 712 ) ); 713 $this->update_post_modified( $p4, '2012-12-03 14:43:00' ); 714 715 $q = new WP_Query( array( 716 'date_query' => array( 717 'relation' => 'AND', 718 array( 719 'column' => 'post_date', 720 array( 721 'year' => 2012, 722 ), 723 ), 724 array( 725 'column' => 'post_modified', 726 array( 727 'year' => 2014, 728 ), 729 ), 730 ), 731 'fields' => 'ids', 732 'update_post_meta_cache' => false, 733 'update_post_term_cache' => false, 734 'post_status' => 'publish', 735 ) ); 736 737 $expected = array( $p1, $p2, ); 738 739 $this->assertEqualSets( $expected, $q->posts ); 740 } 741 742 /** 743 * @ticket 29822 744 */ 745 public function test_date_query_nested_query_multiple_columns_mixed_relations() { 746 $p1 = $this->factory->post->create( array( 747 'post_date' => '2012-03-05 15:30:55', 748 ) ); 749 $this->update_post_modified( $p1, '2014-11-03 14:43:00' ); 750 751 $p2 = $this->factory->post->create( array( 752 'post_date' => '2012-05-05 15:30:55', 753 ) ); 754 $this->update_post_modified( $p2, '2014-10-03 14:43:00' ); 755 756 $p3 = $this->factory->post->create( array( 757 'post_date' => '2013-05-05 15:30:55', 758 ) ); 759 $this->update_post_modified( $p3, '2014-10-03 14:43:00' ); 760 761 $p4 = $this->factory->post->create( array( 762 'post_date' => '2012-02-05 15:30:55', 763 ) ); 764 $this->update_post_modified( $p4, '2012-12-03 14:43:00' ); 765 766 $p5 = $this->factory->post->create( array( 767 'post_date' => '2014-02-05 15:30:55', 768 ) ); 769 $this->update_post_modified( $p5, '2013-12-03 14:43:00' ); 770 771 $q = new WP_Query( array( 772 'date_query' => array( 773 'relation' => 'OR', 774 array( 775 'relation' => 'AND', 776 array( 777 'column' => 'post_date', 778 array( 779 'day' => 05, 780 ), 781 ), 782 array( 783 'column' => 'post_date', 784 array( 785 'before' => array( 786 'year' => 2012, 787 'month' => 4, 788 ), 789 ), 790 ), 791 ), 792 array( 793 'column' => 'post_modified', 794 array( 795 'month' => 12, 796 ), 797 ), 798 ), 799 'fields' => 'ids', 800 'update_post_meta_cache' => false, 801 'update_post_term_cache' => false, 802 'post_status' => 'publish', 803 ) ); 804 805 $expected = array( $p1, $p4, $p5, ); 806 $this->assertEqualSets( $expected, $q->posts ); 659 807 } 660 808 … … 693 841 } 694 842 } 843 844 /** 845 * There's no way to change post_modified through the API. 846 */ 847 protected function update_post_modified( $post_id, $date ) { 848 global $wpdb; 849 return $wpdb->update( 850 $wpdb->posts, 851 array( 852 'post_modified' => $date, 853 'post_modified_gmt' => $date, 854 ), 855 array( 856 'ID' => $post_id, 857 ), 858 array( 859 '%s', 860 '%s', 861 ), 862 array( 863 '%d', 864 ) 865 ); 866 } 695 867 }
Note: See TracChangeset
for help on using the changeset viewer.