diff --git a/includes/class-query-integration.php b/includes/class-query-integration.php index e6b06fd07..ebd0d55b8 100644 --- a/includes/class-query-integration.php +++ b/includes/class-query-integration.php @@ -341,26 +341,16 @@ private function get_sticky_query( $query ) { * @return string listing ids */ private function get_sticky_listing_ids( $query ) { - global $wpdb; - - $order_by = $query->get( 'orderby' ); - $order = $query->get( 'order' ); - $join_sort = in_array( $order_by, array( 'title', 'date', 'modified', 'author' ), true ); - - $query = "SELECT listing_id FROM {$wpdb->prefix}wpbdp_listings"; - if ( $join_sort ) { - $query .= " JOIN {$wpdb->posts} p ON p.ID = {$wpdb->prefix}wpbdp_listings.listing_id"; - } - $query .= ' WHERE is_sticky=1'; - if ( $join_sort ) { - $query .= " ORDER BY p.post_{$order_by} {$order}"; - } + $order_by = (string) $query->get( 'orderby' ); + $order = strtoupper( (string) $query->get( 'order' ) ); + $order = in_array( $order, array( 'ASC', 'DESC' ), true ) ? $order : 'ASC'; + $sql_query = $this->get_sticky_listing_ids_query( $order_by, $order ); $results = WPBDP_Utils::check_cache( array( - 'cache_key' => 'sticky_listing_idss', + 'cache_key' => $this->get_sticky_listing_ids_cache_key( $order_by, $order ), 'group' => 'wpbdp_listings', - 'query' => $query, + 'query' => $sql_query, 'type' => 'get_col', ) ); @@ -376,6 +366,79 @@ private function get_sticky_listing_ids( $query ) { return implode( ',', $results ); } + /** + * Get the SQL query for sticky listing ids. + * + * @since x.x + * + * @param string $order_by The orderby value from the current query. + * @param string $order The normalized order direction. + * + * @return string + */ + private function get_sticky_listing_ids_query( $order_by, $order ) { + global $wpdb; + + $query = "SELECT l.listing_id FROM {$wpdb->prefix}wpbdp_listings l"; + + switch ( $order_by ) { + case 'title': + case 'date': + case 'modified': + case 'author': + $query .= " JOIN {$wpdb->posts} p ON p.ID = l.listing_id"; + $query .= ' WHERE l.is_sticky=1'; + $query .= " ORDER BY p.post_{$order_by} {$order}"; + + break; + case 'paid': + case 'paid-title': + $next_order = 'paid' === $order_by ? 'post_date DESC' : 'post_title ASC'; + + $query .= " JOIN {$wpdb->posts} p ON p.ID = l.listing_id"; + $query .= ' WHERE l.is_sticky=1'; + $query .= " ORDER BY l.fee_price {$order}, p.{$next_order}"; + + break; + case 'plan-order-date': + case 'plan-order-title': + $plan_order = wpbdp_get_option( 'fee-order' ); + if ( is_array( $plan_order ) && isset( $plan_order['method'] ) && 'custom' === $plan_order['method'] ) { + $next_order = 'plan-order-date' === $order_by ? 'post_date' : 'post_title'; + + $query .= " JOIN {$wpdb->posts} p ON p.ID = l.listing_id"; + $query .= " LEFT JOIN {$wpdb->prefix}wpbdp_plans po ON po.id = l.fee_id"; + $query .= ' WHERE l.is_sticky=1'; + $query .= " ORDER BY po.weight DESC, p.{$next_order} {$order}"; + break; + } + + $query .= ' WHERE l.is_sticky=1'; + + break; + default: + $query .= ' WHERE l.is_sticky=1'; + + break; + } + + return $query; + } + + /** + * Get the cache key for sticky listing ids. + * + * @since x.x + * + * @param string $order_by The orderby value from the current query. + * @param string $order The normalized order direction. + * + * @return string + */ + private function get_sticky_listing_ids_cache_key( $order_by, $order ) { + return 'sticky_listing_ids_' . md5( $order_by . '|' . $order . '|' . maybe_serialize( wpbdp_get_option( 'fee-order' ) ) ); + } + // {{ Sort bar. public function sortbar_sort_options( $options ) { $sortbar_fields = wpbdp_sortbar_get_field_options(); diff --git a/tests/wpunit/Listing/ListingQuerySortTest.php b/tests/wpunit/Listing/ListingQuerySortTest.php new file mode 100644 index 000000000..a764028ea --- /dev/null +++ b/tests/wpunit/Listing/ListingQuerySortTest.php @@ -0,0 +1,190 @@ + 'custom', + 'order' => 'asc', + ) + ); + wpbdp_set_option( 'prevent-sticky-on-directory-view', array() ); + WPBDP_Utils::cache_delete_group( 'wpbdp_listings' ); + } + + /** + * @since x.x + */ + public function tearDown() : void { + foreach ( $this->created_listing_ids as $listing_id ) { + wp_delete_post( $listing_id, true ); + } + + foreach ( $this->created_plan_ids as $plan_id ) { + $plan = wpbdp_get_fee_plan( $plan_id ); + if ( $plan ) { + $plan->delete(); + } + } + + WPBDP_Utils::cache_delete_group( 'wpbdp_listings' ); + + parent::tearDown(); + } + + /** + * @since x.x + */ + public function testStickyListingsUsePlanOrderTitleSort() { + $plan = $this->create_fee_plan( 'sticky_plan_order_title', 10 ); + + $zebra = $this->create_listing_on_plan( 'Zebra Corp', $plan, '2026-01-01 00:00:00' ); + $alpha = $this->create_listing_on_plan( 'Alpha Inc', $plan, '2026-01-02 00:00:00' ); + $mango = $this->create_listing_on_plan( 'Mango Ltd', $plan, '2026-01-03 00:00:00' ); + + $this->assertSame( + array( $alpha, $mango, $zebra ), + $this->get_display_listing_ids_from_sticky_query( 'plan-order-title', 'ASC' ) + ); + + $this->assertSame( + array( $zebra, $mango, $alpha ), + $this->get_display_listing_ids_from_sticky_query( 'plan-order-title', 'DESC' ) + ); + } + + /** + * @since x.x + */ + public function testStickyListingsUsePlanOrderDateSort() { + $plan = $this->create_fee_plan( 'sticky_plan_order_date', 10 ); + + $newest = $this->create_listing_on_plan( 'Newest Listing', $plan, '2026-01-03 00:00:00' ); + $oldest = $this->create_listing_on_plan( 'Oldest Listing', $plan, '2026-01-01 00:00:00' ); + $middle = $this->create_listing_on_plan( 'Middle Listing', $plan, '2026-01-02 00:00:00' ); + + $this->assertSame( + array( $oldest, $middle, $newest ), + $this->get_display_listing_ids_from_sticky_query( 'plan-order-date', 'ASC' ) + ); + } + + /** + * @since x.x + * + * @param string $tag Fee plan tag. + * @param int $weight Fee plan weight. + * + * @return WPBDP__Fee_Plan + */ + private function create_fee_plan( $tag, $weight ) { + $fee = new WPBDP__Fee_Plan( + array( + 'label' => 'Sticky Plan ' . $weight, + 'amount' => 100, + 'days' => 365, + 'sticky' => 1, + 'recurring' => 0, + 'enabled' => 1, + 'images' => 0, + 'pricing_model' => 'flat', + 'supported_categories' => 'all', + 'tag' => $tag, + 'weight' => $weight, + ) + ); + + $result = $fee->save(); + if ( is_wp_error( $result ) ) { + $this->fail( 'Fee creation failed: ' . $result->get_error_message() ); + } + + $this->assertTrue( is_int( $fee->id ) ); + $this->created_plan_ids[] = $fee->id; + + return $fee; + } + + /** + * @since x.x + * + * @param string $title Post title. + * @param WPBDP__Fee_Plan $plan Fee plan. + * @param string $date Post date. + * + * @return int + */ + private function create_listing_on_plan( $title, $plan, $date ) { + $listing_id = wp_insert_post( + array( + 'post_author' => 1, + 'post_type' => WPBDP_POST_TYPE, + 'post_status' => 'publish', + 'post_title' => $title, + 'post_date' => $date, + 'post_date_gmt' => get_gmt_from_date( $date ), + ) + ); + + $this->assertTrue( is_int( $listing_id ) && 0 < $listing_id ); + $this->assertNotFalse( wpbdp_get_listing( $listing_id )->set_fee_plan( $plan ) ); + $this->created_listing_ids[] = $listing_id; + + return $listing_id; + } + + /** + * @since x.x + * + * @param string $order_by Order by value. + * @param string $order Order direction. + * + * @return int[] + */ + private function get_display_listing_ids_from_sticky_query( $order_by, $order ) { + $query = new WP_Query(); + $query->set( 'orderby', $order_by ); + $query->set( 'order', $order ); + + $reflection = new \ReflectionClass( 'WPBDP__Query_Integration' ); + $method = $reflection->getMethod( 'get_sticky_listing_ids' ); + $method->setAccessible( true ); + + $sticky_ids = $method->invoke( $reflection->newInstanceWithoutConstructor(), $query ); + + return array_reverse( array_map( 'intval', explode( ',', $sticky_ids ) ) ); + } +}