From 2d737796d26476a06abcf2598574a5b4aaf5f2d3 Mon Sep 17 00:00:00 2001 From: Boone Gorges Date: Sat, 31 Jan 2015 15:47:51 +0000 Subject: [PATCH] Improve support for ordering `WP_Query` results by postmeta. `WP_Meta_Query` clauses now support a 'name' parameter. When building a `WP_Query` object, the value of 'orderby' can reference this 'name', so that it's possible to order by any clause in a meta_query, not just the first one (as when using 'orderby=meta_value'). This improvement also makes it possible to order by multiple meta query clauses (or by any other eligible field plus a meta query clause), using the array syntax for 'orderby' introduced in [29027]. Props Funkatronic, boonebgorges. Fixes #31045. git-svn-id: https://develop.svn.wordpress.org/trunk@31312 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/meta.php | 37 +++++++++- src/wp-includes/query.php | 41 +++++++---- tests/phpunit/tests/query/metaQuery.php | 97 +++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 17 deletions(-) diff --git a/src/wp-includes/meta.php b/src/wp-includes/meta.php index 8953aaa021..c0430f79d9 100644 --- a/src/wp-includes/meta.php +++ b/src/wp-includes/meta.php @@ -933,10 +933,20 @@ class WP_Meta_Query { */ protected $table_aliases = array(); + /** + * A flat list of clauses, keyed by clause 'name'. + * + * @since 4.2.0 + * @var array + */ + protected $clauses = array(); + /** * Constructor. * * @since 3.2.0 + * @since 4.2.0 Introduced the `$name` parameter, for improved `$orderby` support in the parent query. + * * @access public * * @param array $meta_query { @@ -957,6 +967,8 @@ class WP_Meta_Query { * comparisons. Accepts 'NUMERIC', 'BINARY', 'CHAR', 'DATE', * 'DATETIME', 'DECIMAL', 'SIGNED', 'TIME', or 'UNSIGNED'. * Default is 'CHAR'. + * @type string $name Optional. A unique identifier for the clause. If provided, `$name` can be + * referenced in the `$orderby` parameter of the parent query. * } * } */ @@ -1374,6 +1386,15 @@ class WP_Meta_Query { // Save the alias to this clause, for future siblings to find. $clause['alias'] = $alias; + // Determine the data type. + $_meta_type = isset( $clause['type'] ) ? $clause['type'] : ''; + $meta_type = $this->get_cast_for_type( $_meta_type ); + $clause['cast'] = $meta_type; + + // Store the clause in our flat array. + $clause_name = isset( $clause['name'] ) ? $clause['name'] : $clause['alias']; + $this->clauses[ $clause_name ] =& $clause; + // Next, build the WHERE clause. // meta_key. @@ -1388,7 +1409,6 @@ class WP_Meta_Query { // meta_value. if ( array_key_exists( 'value', $clause ) ) { $meta_value = $clause['value']; - $meta_type = $this->get_cast_for_type( isset( $clause['type'] ) ? $clause['type'] : '' ); if ( in_array( $meta_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) { if ( ! is_array( $meta_value ) ) { @@ -1450,6 +1470,21 @@ class WP_Meta_Query { return $sql_chunks; } + /** + * Get a flattened list of sanitized meta clauses, indexed by clause 'name'. + * + * This array should be used for clause lookup, as when the table alias and CAST type must be determined for + * a value of 'orderby' corresponding to a meta clause. + * + * @since 4.2.0 + * @access public + * + * @return array + */ + public function get_clauses() { + return $this->clauses; + } + /** * Identify an existing table alias that is compatible with the current * query clause. diff --git a/src/wp-includes/query.php b/src/wp-includes/query.php index 5a6774e06a..6a5aceaa80 100644 --- a/src/wp-includes/query.php +++ b/src/wp-includes/query.php @@ -2233,8 +2233,9 @@ class WP_Query { $primary_meta_key = ''; $primary_meta_query = false; - if ( ! empty( $this->meta_query->queries ) ) { - $primary_meta_query = reset( $this->meta_query->queries ); + $meta_clauses = $this->meta_query->get_clauses(); + if ( ! empty( $meta_clauses ) ) { + $primary_meta_query = reset( $meta_clauses ); if ( ! empty( $primary_meta_query['key'] ) ) { $primary_meta_key = $primary_meta_query['key']; @@ -2243,6 +2244,7 @@ class WP_Query { $allowed_keys[] = 'meta_value'; $allowed_keys[] = 'meta_value_num'; + $allowed_keys = array_merge( $allowed_keys, array_keys( $meta_clauses ) ); } if ( ! in_array( $orderby, $allowed_keys ) ) { @@ -2260,29 +2262,36 @@ class WP_Query { case 'ID': case 'menu_order': case 'comment_count': - $orderby = "$wpdb->posts.{$orderby}"; + $orderby_clause = "$wpdb->posts.{$orderby}"; break; case 'rand': - $orderby = 'RAND()'; + $orderby_clause = 'RAND()'; break; case $primary_meta_key: case 'meta_value': if ( ! empty( $primary_meta_query['type'] ) ) { - $sql_type = $this->meta_query->get_cast_for_type( $primary_meta_query['type'] ); - $orderby = "CAST($wpdb->postmeta.meta_value AS {$sql_type})"; + $orderby_clause = "CAST({$primary_meta_query['alias']}.meta_value AS {$primary_meta_query['cast']})"; } else { - $orderby = "$wpdb->postmeta.meta_value"; + $orderby_clause = "{$primary_meta_query['alias']}.meta_value"; } break; case 'meta_value_num': - $orderby = "$wpdb->postmeta.meta_value+0"; + $orderby_clause = "{$primary_meta_query['alias']}.meta_value+0"; break; default: - $orderby = "$wpdb->posts.post_" . $orderby; + if ( array_key_exists( $orderby, $meta_clauses ) ) { + // $orderby corresponds to a meta_query clause. + $meta_clause = $meta_clauses[ $orderby ]; + $orderby_clause = "CAST({$meta_clause['alias']}.meta_value AS {$meta_clause['cast']})"; + } else { + // Default: order by post field. + $orderby_clause = "$wpdb->posts.post_" . sanitize_key( $orderby ); + } + break; } - return $orderby; + return $orderby_clause; } /** @@ -2813,6 +2822,12 @@ class WP_Query { $where .= $search . $whichauthor . $whichmimetype; + if ( ! empty( $this->meta_query->queries ) ) { + $clauses = $this->meta_query->get_sql( 'post', $wpdb->posts, 'ID', $this ); + $join .= $clauses['join']; + $where .= $clauses['where']; + } + $rand = ( isset( $q['orderby'] ) && 'rand' === $q['orderby'] ); if ( ! isset( $q['order'] ) ) { $q['order'] = $rand ? '' : 'DESC'; @@ -3030,12 +3045,6 @@ class WP_Query { $where .= ')'; } - if ( !empty( $this->meta_query->queries ) ) { - $clauses = $this->meta_query->get_sql( 'post', $wpdb->posts, 'ID', $this ); - $join .= $clauses['join']; - $where .= $clauses['where']; - } - /* * Apply filters on where and join prior to paging so that any * manipulations to them are reflected in the paging by day queries. diff --git a/tests/phpunit/tests/query/metaQuery.php b/tests/phpunit/tests/query/metaQuery.php index 8f9b212d81..2eff8eb641 100644 --- a/tests/phpunit/tests/query/metaQuery.php +++ b/tests/phpunit/tests/query/metaQuery.php @@ -1558,4 +1558,101 @@ class Tests_Query_MetaQuery extends WP_UnitTestCase { $posts = wp_list_pluck( $posts, 'ID' ); $this->assertEqualSets( array( $post_id, $post_id3, $post_id4, $post_id5, $post_id6 ), $posts ); } + + /** + * @ticket 31045 + */ + public function test_orderby_name() { + $posts = $this->factory->post->create_many( 3 ); + add_post_meta( $posts[0], 'foo', 'aaa' ); + add_post_meta( $posts[1], 'foo', 'zzz' ); + add_post_meta( $posts[2], 'foo', 'jjj' ); + + $q = new WP_Query( array( + 'fields' => 'ids', + 'meta_query' => array( + array( + 'name' => 'foo_name', + 'key' => 'foo', + 'compare' => 'EXISTS', + ), + ), + 'orderby' => 'foo_name', + 'order' => 'DESC', + ) ); + + $this->assertEquals( array( $posts[1], $posts[2], $posts[0] ), $q->posts ); + } + + /** + * @ticket 31045 + */ + public function test_orderby_name_as_secondary_sort() { + $p1 = $this->factory->post->create( array( + 'post_date' => '2015-01-28 03:00:00', + ) ); + $p2 = $this->factory->post->create( array( + 'post_date' => '2015-01-28 05:00:00', + ) ); + $p3 = $this->factory->post->create( array( + 'post_date' => '2015-01-28 03:00:00', + ) ); + + add_post_meta( $p1, 'foo', 'jjj' ); + add_post_meta( $p2, 'foo', 'zzz' ); + add_post_meta( $p3, 'foo', 'aaa' ); + + $q = new WP_Query( array( + 'fields' => 'ids', + 'meta_query' => array( + array( + 'name' => 'foo_name', + 'key' => 'foo', + 'compare' => 'EXISTS', + ), + ), + 'orderby' => array( + 'post_date' => 'asc', + 'foo_name' => 'asc', + ), + ) ); + + $this->assertEquals( array( $p3, $p1, $p2 ), $q->posts ); + } + + /** + * @ticket 31045 + */ + public function test_orderby_more_than_one_name() { + $posts = $this->factory->post->create_many( 3 ); + + add_post_meta( $posts[0], 'foo', 'jjj' ); + add_post_meta( $posts[1], 'foo', 'zzz' ); + add_post_meta( $posts[2], 'foo', 'jjj' ); + add_post_meta( $posts[0], 'bar', 'aaa' ); + add_post_meta( $posts[1], 'bar', 'ccc' ); + add_post_meta( $posts[2], 'bar', 'bbb' ); + + $q = new WP_Query( array( + 'fields' => 'ids', + 'meta_query' => array( + array( + 'name' => 'foo_name', + 'key' => 'foo', + 'compare' => 'EXISTS', + ), + array( + 'name' => 'bar_name', + 'key' => 'bar', + 'compare' => 'EXISTS', + ), + ), + 'orderby' => array( + 'foo_name' => 'asc', + 'bar_name' => 'desc', + ), + ) ); + + $this->assertEquals( array( $posts[2], $posts[0], $posts[1] ), $q->posts ); + } }