diff --git a/includes/class-wc-product-variable.php b/includes/class-wc-product-variable.php index 135705e81d5..7ff3dd8744e 100644 --- a/includes/class-wc-product-variable.php +++ b/includes/class-wc-product-variable.php @@ -593,94 +593,6 @@ class WC_Product_Variable extends WC_Product { return true; } - /** - * Returns whether or not the product is visible in the catalog (doesn't trigger filters). - * - * @return bool - */ - protected function is_visible_core() { - if ( ! $this->parent_is_visible_core() ) { - return false; - } - - $query_filters = $this->get_layered_nav_chosen_attributes(); - if ( empty( $query_filters ) ) { - return true; - } - - /** - * If there are attribute filters in the request, a variable product will be visible - * if at least one of the available variations matches the filters. - */ - - $attributes_with_terms = array(); - array_walk( - $query_filters, - function( $value, $key ) use ( &$attributes_with_terms ) { - $attributes_with_terms[ $key ] = $value['terms']; - } - ); - - $variations = $this->get_available_variations( 'objects' ); - foreach ( $variations as $variation ) { - if ( $this->variation_matches_filters( $variation, $attributes_with_terms ) ) { - return true; - } - } - - return false; - } - - /** - * Checks if a given variation matches the active attribute filters. - * - * @param WC_Product_Variation $variation The variation to check. - * @param array $query_filters The active filters as an array of attribute_name => [term1, term2...]. - * - * @return bool True if the variation matches the active attribute filters. - */ - private function variation_matches_filters( WC_Product_Variation $variation, array $query_filters ) { - // Get the variation attributes as an array of attribute_name => attribute_value. - // The array_filter will filter out attributes having a value of '', these correspond - // to "Any..." variations that don't participate in filtering. - $variation_attributes = array_filter( $variation->get_variation_attributes( false ) ); - - $variation_attribute_names_in_filters = array_intersect( array_keys( $query_filters ), array_keys( $variation_attributes ) ); - if ( empty( $variation_attribute_names_in_filters ) ) { - // The variation doesn't have any attribute that participates in filtering so we consider it a match. - return true; - } - - foreach ( $variation_attribute_names_in_filters as $attribute_name ) { - if ( ! in_array( $variation_attributes[ $attribute_name ], $query_filters[ $attribute_name ], true ) ) { - // Multiple filters interact with AND logic, so as soon as one of them - // doesn't match then the variation doesn't match. - return false; - } - } - - return true; - } - - /** - * What does is_visible_core in the parent class say? - * This method exists to ease unit testing. - * - * @return bool - */ - protected function parent_is_visible_core() { - return parent::is_visible_core(); - } - - /** - * Get an array of attributes and terms selected with the layered nav widget. - * This method exists to ease unit testing. - * - * @return array - */ - protected function get_layered_nav_chosen_attributes() { - return WC()->query::get_layered_nav_chosen_attributes(); - } /* |-------------------------------------------------------------------------- diff --git a/includes/class-wc-query.php b/includes/class-wc-query.php index 915d2ef4a92..3677c6893e0 100644 --- a/includes/class-wc-query.php +++ b/includes/class-wc-query.php @@ -362,23 +362,10 @@ class WC_Query { if ( 'product_query' !== $query->get( 'wc_query' ) ) { return $posts; } - $this->adjust_total_pages(); $this->remove_product_query_filters( $posts ); return $posts; } - /** - * The 'adjust_posts_count' method that handles the 'found_posts' filter indirectly initializes - * the loop properties with a call to 'wc_setup_loop'. This includes setting 'total_pages' to - * '$GLOBALS['wp_query']->max_num_pages', which at that point has a value of zero. - * Thus we need to set the real value from the 'the_posts' filter, where $GLOBALS['wp_query']->max_num_pages' - * will aready have been initialized. - */ - private function adjust_total_pages() { - if ( 0 === wc_get_loop_prop( 'total_pages' ) ) { - wc_set_loop_prop( 'total_pages', $GLOBALS['wp_query']->max_num_pages ); - } - } /** * Pre_get_posts above may adjust the main query to add WooCommerce logic. When this query is done, we need to ensure @@ -395,52 +382,6 @@ class WC_Query { return $posts; } - /** - * When we are listing products and the request is filtering by attributes via layered nav plugin - * we need to adjust the total posts count to account for variable products having stock - * in some variations but not in others. - * We do that by just checking if each product is visible. - * - * We also cache the post visibility so that it isn't checked again when displaying the posts list. - * - * @since 4.4.0 - * @param int $count Original posts count, as supplied by the found_posts filter. - * @param WP_Query $query The current WP_Query object. - * - * @return int Adjusted posts count. - */ - public function adjust_posts_count( $count, $query ) { - if ( ! $query->get( 'wc_query' ) ) { - return $count; - } - - $posts = $this->get_current_posts(); - if ( is_null( $posts ) ) { - return $count; - } - - foreach ( $posts as $post ) { - if ( is_object( $post ) && 'product' !== $post->post_type ) { - continue; - } - - $product_id = is_object( $post ) ? $post->ID : $post; - $product = wc_get_product( $product_id ); - if ( ! is_object( $product ) ) { - continue; - } - - if ( $product->is_visible() ) { - wc_set_loop_product_visibility( $product_id, true ); - } else { - wc_set_loop_product_visibility( $product_id, false ); - $count--; - } - } - - wc_set_loop_prop( 'total', $count ); - return $count; - } /** * Instance version of get_layered_nav_chosen_attributes, needed for unit tests. @@ -514,7 +455,7 @@ class WC_Query { // Additonal hooks to change WP Query. add_filter( 'posts_clauses', array( $this, 'price_filter_post_clauses' ), 10, 2 ); add_filter( 'the_posts', array( $this, 'handle_get_posts' ), 10, 2 ); - add_filter( 'found_posts', array( $this, 'adjust_posts_count' ), 10, 2 ); + do_action( 'woocommerce_product_query', $q, $this ); } diff --git a/includes/widgets/class-wc-widget-layered-nav.php b/includes/widgets/class-wc-widget-layered-nav.php index 41568504f42..77f456c0c9d 100644 --- a/includes/widgets/class-wc-widget-layered-nav.php +++ b/includes/widgets/class-wc-widget-layered-nav.php @@ -344,93 +344,51 @@ class WC_Widget_Layered_Nav extends WC_Widget { protected function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) { global $wpdb; - $main_tax_query = $this->get_main_tax_query(); - $meta_query = $this->get_main_meta_query(); + $tax_query = $this->get_main_tax_query(); + $meta_query = $this->get_main_meta_query(); - $non_variable_tax_query_sql = array( 'where' => '' ); - $is_and_query = 'and' === $query_type; - - foreach ( $main_tax_query as $key => $query ) { - if ( is_array( $query ) && $taxonomy === $query['taxonomy'] ) { - if ( $is_and_query ) { - $non_variable_tax_query_sql = $this->convert_tax_query_to_sql( array( $query ) ); + if ( 'or' === $query_type ) { + foreach ( $tax_query as $key => $query ) { + if ( is_array( $query ) && $taxonomy === $query['taxonomy'] ) { + unset( $tax_query[ $key ] ); } - unset( $main_tax_query[ $key ] ); } } - $exclude_variable_products_tax_query_sql = $this->get_extra_tax_query_sql( 'product_type', array( 'variable' ), 'NOT IN' ); - - $meta_query_sql = ( new WP_Meta_Query( $meta_query ) )->get_sql( 'post', $wpdb->posts, 'ID' ); - $main_tax_query_sql = $this->convert_tax_query_to_sql( $main_tax_query ); - $term_ids_sql = '(' . implode( ',', array_map( 'absint', $term_ids ) ) . ')'; + $meta_query = new WP_Meta_Query( $meta_query ); + $tax_query = new WP_Tax_Query( $tax_query ); + $meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' ); + $tax_query_sql = $tax_query->get_sql( $wpdb->posts, 'ID' ); + $term_ids_sql = '(' . implode( ',', array_map( 'absint', $term_ids ) ) . ')'; // Generate the first part of the query. // This one will return non-variable products and variable products with concrete values for the attributes. $query = array(); - $query['select'] = "SELECT IF({$wpdb->posts}.post_type='product_variation', {$wpdb->posts}.post_parent, {$wpdb->posts}.ID) AS product_id, terms.term_id AS term_count_id"; + $query['select'] = "SELECT COUNT( DISTINCT {$wpdb->posts}.ID ) AS term_count, terms.term_id AS term_count_id"; $query['from'] = "FROM {$wpdb->posts}"; $query['join'] = " - INNER JOIN {$wpdb->term_relationships} AS tr ON {$wpdb->posts}.ID = tr.object_id + INNER JOIN {$wpdb->term_relationships} AS term_relationships ON {$wpdb->posts}.ID = term_relationships.object_id INNER JOIN {$wpdb->term_taxonomy} AS term_taxonomy USING( term_taxonomy_id ) INNER JOIN {$wpdb->terms} AS terms USING( term_id ) - {$main_tax_query_sql['join']} {$meta_query_sql['join']}"; // Not an omission, really no more JOINs required. - - $variable_where_part = " - OR ({$wpdb->posts}.post_type = 'product_variation' - AND NOT EXISTS ( - SELECT ID FROM {$wpdb->posts} AS parent - WHERE parent.ID = {$wpdb->posts}.post_parent AND parent.post_status NOT IN ('publish') - )) - "; - - $search_sql = ''; - $search = $this->get_main_search_query_sql(); - if ( $search ) { - $search_sql = ' AND ' . $search; - } + " . $tax_query_sql['join'] . $meta_query_sql['join']; $query['where'] = " - WHERE - {$wpdb->posts}.post_status = 'publish' - {$main_tax_query_sql['where']} {$meta_query_sql['where']} - AND ( - ( - {$wpdb->posts}.post_type = 'product' - {$exclude_variable_products_tax_query_sql['where']} - {$non_variable_tax_query_sql['where']} - ) - {$variable_where_part} - ) - AND terms.term_id IN {$term_ids_sql} - {$search_sql}"; + WHERE {$wpdb->posts}.post_type IN ( 'product' ) + AND {$wpdb->posts}.post_status = 'publish' + {$tax_query_sql['where']} {$meta_query_sql['where']} + AND terms.term_id IN $term_ids_sql"; $search = $this->get_main_search_query_sql(); if ( $search ) { $query['where'] .= ' AND ' . $search; } - $query = apply_filters( 'woocommerce_get_filtered_term_product_counts_query', $query ); - $main_query_sql = implode( ' ', $query ); + $query['group_by'] = 'GROUP BY terms.term_id'; + $query = apply_filters( 'woocommerce_get_filtered_term_product_counts_query', $query ); + $query_sql = implode( ' ', $query ); - // Generate the second part of the query. - // This one will return products having "Any..." as the value of the attribute. - - $query_sql_for_attributes_with_any_value = " - SELECT {$wpdb->posts}.ID AS product_id, {$wpdb->term_relationships}.term_taxonomy_id as term_count_id FROM {$wpdb->posts} - JOIN {$wpdb->posts} variations ON variations.post_parent = {$wpdb->posts}.ID - LEFT JOIN {$wpdb->postmeta} ON variations.ID = {$wpdb->postmeta}.post_id AND {$wpdb->postmeta}.meta_key = 'attribute_$taxonomy' - JOIN {$wpdb->term_relationships} ON {$wpdb->term_relationships}.object_id = {$wpdb->posts}.ID - WHERE ( {$wpdb->postmeta}.meta_key IS NULL OR {$wpdb->postmeta}.meta_value = '') - AND {$wpdb->posts}.post_type = 'product' - AND {$wpdb->posts}.post_status = 'publish' - AND variations.post_status = 'publish' - AND variations.post_type = 'product_variation' - AND {$wpdb->term_relationships}.term_taxonomy_id in $term_ids_sql - {$main_tax_query_sql['where']}"; - - // We have two queries - let's see if cached results of this query already exist. - $query_hash = md5( $main_query_sql . $query_sql_for_attributes_with_any_value ); + // We have a query - let's see if cached results of this query already exist. + $query_hash = md5( $query_sql ); // Maybe store a transient of the count values. $cache = apply_filters( 'woocommerce_layered_nav_count_maybe_cache', true ); @@ -441,7 +399,9 @@ class WC_Widget_Layered_Nav extends WC_Widget { } if ( ! isset( $cached_counts[ $query_hash ] ) ) { - $counts = $this->get_term_product_counts_from_queries( $main_query_sql, $query_sql_for_attributes_with_any_value ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $results = $wpdb->get_results( $query_sql, ARRAY_A ); + $counts = array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) ); $cached_counts[ $query_hash ] = $counts; if ( true === $cache ) { set_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ), $cached_counts, DAY_IN_SECONDS ); @@ -451,30 +411,6 @@ class WC_Widget_Layered_Nav extends WC_Widget { return array_map( 'absint', (array) $cached_counts[ $query_hash ] ); } - /** - * Get the count of terms for products, using a set of SQL queries that are return pairs of product id - term id. - * - * @param string $main_query_sql The SQL query to use in order to count products with concrete values for attributes, must return a "product_id" column and a "terms_count_id" column. - * @param string $query_sql_for_attributes_with_any_value The SQL query to use in order to count products with "Any" values for attributes, must return a "product_id" column and a "terms_count_id" column. - * - * @return array An array where the keys are term ids, and the values are term counts. - */ - private function get_term_product_counts_from_queries( $main_query_sql, $query_sql_for_attributes_with_any_value ) { - global $wpdb; - - $total_counts = null; - - $query = " - SELECT COUNT(DISTINCT(product_id)) AS term_count, term_count_id FROM ( - {$main_query_sql} - UNION ALL - {$query_sql_for_attributes_with_any_value} - ) AS x GROUP BY term_count_id"; - - $results = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - return array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) ); - } - /** * Wrapper for WC_Query::get_main_tax_query() to ease unit testing. * @@ -505,45 +441,6 @@ class WC_Widget_Layered_Nav extends WC_Widget { return WC_Query::get_main_meta_query(); } - /** - * Get a tax query SQL for a given set of taxonomy, terms and operator. - * Uses an intermediate WP_Tax_Query object. - * - * @since 4.4.0 - * @param string $taxonomy Taxonomy name. - * @param array $terms Terms to include in the query. - * @param string $operator Query operator, as supported by WP_Tax_Query; e.g. "NOT IN". - * - * @return array - */ - private function get_extra_tax_query_sql( $taxonomy, $terms, $operator ) { - $query = array( - array( - 'taxonomy' => $taxonomy, - 'field' => 'slug', - 'terms' => $terms, - 'operator' => $operator, - 'include_children' => false, - ), - ); - - return $this->convert_tax_query_to_sql( $query ); - } - - /** - * Convert a tax query array to SQL using an intermediate WP_Tax_Query object. - * - * @since 4.4.0 - * @param array $query Query array in the same format accepted by WP_Tax_Query constructor. - * - * @return array Query SQL as returned by WP_Tax_Query->get_sql. - */ - private function convert_tax_query_to_sql( $query ) { - global $wpdb; - - return ( new WP_Tax_Query( $query ) )->get_sql( $wpdb->posts, 'ID' ); - } - /** * Show list based layered nav. * diff --git a/templates/content-product.php b/templates/content-product.php index 6058ebbebbd..e354ae2e929 100644 --- a/templates/content-product.php +++ b/templates/content-product.php @@ -20,7 +20,7 @@ defined( 'ABSPATH' ) || exit; global $product; // Ensure visibility. -if ( empty( $product ) || false === wc_get_loop_product_visibility( $product->get_id() ) || ! $product->is_visible() ) { +if ( empty( $product ) || ! $product->is_visible() ) { return; } ?> diff --git a/tests/legacy/unit-tests/product/product-variable.php b/tests/legacy/unit-tests/product/product-variable.php index 66de936995c..95a45fd5574 100644 --- a/tests/legacy/unit-tests/product/product-variable.php +++ b/tests/legacy/unit-tests/product/product-variable.php @@ -176,199 +176,4 @@ class WC_Tests_Product_Variable extends WC_Unit_Test_Case { $this->assertEquals( $expected_stock_status, $product->get_stock_status() ); } - - /** - * Setup for a test for is_visible. - * - * @param array $filtering_attributes Simulated filtering attributes as an array of attribute_name => [term1, term2...]. - * @param bool $hide_out_of_stock_products Should the woocommerce_hide_out_of_stock_items option be set?. - * @param bool $is_visible_from_parent Return value of is_visible from base class. - * - * @return WC_Product_Variable A properly configured instance of WC_Product_Variable to test. - */ - private function prepare_visibility_test( $filtering_attributes, $hide_out_of_stock_products = true, $is_visible_from_parent = true ) { - foreach ( $filtering_attributes as $attribute_name => $terms ) { - $filtering_attributes[ $attribute_name ]['query_type'] = 'ANY_QUERY_TYPE'; - $filtering_attributes[ $attribute_name ]['terms'] = $terms; - } - - update_option( 'woocommerce_hide_out_of_stock_items', $hide_out_of_stock_products ? 'yes' : 'no' ); - - $sut = $this - ->getMockBuilder( WC_Product_Variable::class ) - ->setMethods( array( 'parent_is_visible_core', 'get_layered_nav_chosen_attributes' ) ) - ->getMock(); - - $sut = WC_Helper_Product::create_variation_product( $sut, true ); - $sut->save(); - - $sut->method( 'parent_is_visible_core' )->willReturn( $is_visible_from_parent ); - $sut->method( 'get_layered_nav_chosen_attributes' )->willReturn( $filtering_attributes ); - - return $sut; - } - - /** - * Configure the stock status for the attribute-based variations of a product. - * - * @param WC_Product_Variable $product Product with the variations to configure. - * @param array $attributes An array of attribute_name => [attribute_values], only the matching variations will have stock. - */ - private function set_variations_with_stock( $product, $attributes ) { - $variation_ids = $product->get_children(); - foreach ( $variation_ids as $id ) { - $variation = wc_get_product( $id ); - $attribute_matches = true; - foreach ( $attributes as $name => $values ) { - if ( ! in_array( $variation->get_attribute( $name ), $values, true ) ) { - $attribute_matches = false; - } - } - $variation->set_stock_status( $attribute_matches ? 'instock' : 'outofstock' ); - $variation->save(); - } - } - - /** - * @testdox The product should be invisible when the parent 'is_visible' method returns false. - */ - public function test_is_invisible_when_parent_is_visible_returns_false() { - $sut = $this->prepare_visibility_test( array(), '', false, false ); - - $this->assertFalse( $sut->is_visible() ); - } - - /** - * @testdox The product should be visible when no nav filtering is supplied if at least one variation has stock. - * - * Note that if no variations have stock the base is_visible will already return false. - */ - public function test_is_visible_when_no_filtering_supplied_and_at_least_one_variation_has_stock() { - $sut = $this->prepare_visibility_test( array(), '' ); - - $this->set_variations_with_stock( $sut, array( 'pa_size' => array( 'small' ) ) ); - - $this->assertTrue( $sut->is_visible() ); - } - - /** - * @testdox Test product visibility when the variation requested in nav filtering has no stock, result depends on woocommerce_hide_out_of_stock_items option. - * - * @param bool $hide_out_of_stock Value for woocommerce_hide_out_of_stock_items. - * @param bool $expected_visibility Expected value of is_visible for the tested product. - * - * @testWith [true, false] - * [false, true] - */ - public function test_visibility_when_supplied_filter_has_no_stock( $hide_out_of_stock, $expected_visibility ) { - $sut = $this->prepare_visibility_test( array( 'pa_size' => array( 'large' ) ), $hide_out_of_stock ); - - $this->set_variations_with_stock( $sut, array( 'pa_size' => array( 'small' ) ) ); - - $this->assertEquals( $expected_visibility, $sut->is_visible() ); - } - - /** - * @testdox Product should always be visible when only one of the variations requested in nav filtering has stock. - * - * @param bool $hide_out_of_stock Value for woocommerce_hide_out_of_stock_items. - * - * @testWith [true] - * [false] - */ - public function test_visibility_when_multiple_filter_values_supplied_and_only_one_has_stock( $hide_out_of_stock ) { - $sut = $this->prepare_visibility_test( array( 'pa_size' => array( 'small', 'large' ) ), $hide_out_of_stock ); - - $this->set_variations_with_stock( $sut, array( 'pa_size' => array( 'small' ) ) ); - - $this->assertTrue( $sut->is_visible() ); - } - - /** - * @testdox Product should be visible when all of the variations requested in nav filtering have stock. - * - * @param bool $hide_out_of_stock Value for woocommerce_hide_out_of_stock_items. - * - * @testWith [true] - * [false] - */ - public function test_visibility_when_multiple_filter_values_supplied_and_all_of_them_have_stock( $hide_out_of_stock ) { - $sut = $this->prepare_visibility_test( array( 'pa_size' => array( 'small', 'large' ) ), $hide_out_of_stock ); - - $this->set_variations_with_stock( $sut, array( 'pa_size' => array( 'small', 'large' ) ) ); - - $this->assertTrue( $sut->is_visible() ); - } - - /** - * @testdox Product should be visible when multiple filters are present, and there's a variation matching all of them. - */ - public function test_visibility_when_multiple_filters_are_used_and_all_of_them_match() { - $sut = $this->prepare_visibility_test( - array( - 'pa_size' => array( 'huge' ), - 'pa_colour' => array( 'blue' ), - ), - true - ); - - $this->set_variations_with_stock( - $sut, - array( - 'pa_size' => array( 'huge' ), - 'pa_colour' => array( 'blue' ), - 'pa_number' => array( '2' ), - ) - ); - - $this->assertTrue( $sut->is_visible() ); - } - - /** - * @testdox Product should not be visible when multiple filters are present, and there are no variations matching all of them. - */ - public function test_visibility_when_multiple_filters_are_used_and_one_of_them_does_not_match() { - $sut = $this->prepare_visibility_test( - array( - 'pa_size' => array( 'small', 'huge' ), - 'pa_colour' => array( 'red' ), - ), - true - ); - - $this->set_variations_with_stock( - $sut, - array( - 'pa_size' => array( 'huge' ), - 'pa_colour' => array( 'blue' ), - 'pa_number' => array( '2' ), - ) - ); - - $this->assertFalse( $sut->is_visible() ); - } - - /** - * @testdox Attributes having "Any..." as value should not count when searching for matching attributes. - */ - public function test_visibility_when_multiple_filters_are_used_and_an_attribute_has_any_value() { - $sut = $this->prepare_visibility_test( - array( - 'pa_size' => array( 'huge' ), - 'pa_number' => array( '34' ), - ), - true - ); - - $this->set_variations_with_stock( - $sut, - array( - 'pa_size' => array( 'huge' ), - 'pa_colour' => array( 'blue' ), - 'pa_number' => array( '' ), - ) - ); - - $this->assertTrue( $sut->is_visible() ); - } } diff --git a/tests/legacy/unit-tests/util/class-wc-tests-wc-query.php b/tests/legacy/unit-tests/util/class-wc-tests-wc-query.php index b7202b2115e..da9b8e751c2 100644 --- a/tests/legacy/unit-tests/util/class-wc-tests-wc-query.php +++ b/tests/legacy/unit-tests/util/class-wc-tests-wc-query.php @@ -437,124 +437,4 @@ class WC_Tests_WC_Query extends WC_Unit_Test_Case { WC()->query->remove_ordering_args(); } - - /** - * Setup for a test for adjust_posts. - * - * @param bool $with_nav_filtering_data Should WC_Query::get_layered_nav_chosen_attributes return filtering data?. - * @param bool $use_objects If true, get_current_posts will return objects with an ID property; if false, it will returns the ids. - * @param string $post_type The value of the 'post_type' property for the objects generated when $use_objects is true. - * - * @return array An array where the first element is the instance of WC_Query, and the second is an array of sample products created. - */ - private function setup_adjust_posts_test( $with_nav_filtering_data, $use_objects, $post_type = 'product' ) { - update_option( 'woocommerce_hide_out_of_stock_items', 'yes' ); - - if ( $with_nav_filtering_data ) { - $nav_filtering_data = array( 'pa_something' => array( 'terms' => array( 'foo', 'bar' ) ) ); - } else { - $nav_filtering_data = array(); - } - - $products = array(); - $posts = array(); - for ( $i = 0; $i < 5; $i++ ) { - $product = WC_Helper_Product::create_simple_product(); - array_push( $products, $product ); - $post = $use_objects ? (object) array( - 'ID' => $product->get_id(), - 'post_type' => $post_type, - ) : $product->get_id(); - array_push( $posts, $post ); - } - - $products[0]->set_stock_status( 'outofstock' ); - - $sut = $this - ->getMockBuilder( WC_Query::class ) - ->setMethods( array( 'get_current_posts', 'get_layered_nav_chosen_attributes_inst' ) ) - ->getMock(); - - $sut->method( 'get_current_posts' )->willReturn( $posts ); - $sut->method( 'get_layered_nav_chosen_attributes_inst' )->willReturn( $nav_filtering_data ); - - return array( $sut, $products ); - } - - /** - * @param bool $with_nav_filtering_data Should WC_Query::get_layered_nav_chosen_attributes return filtering data?. - * @param bool $use_objects If true, get_current_posts will return objects with an ID property; if false, it will returns the ids. - * - * @testdox adjust_posts should return the number of visible products and create product visibility loop variables - * @testWith [true, true] - * [false, false] - * [true, false] - * [false, true] - */ - public function test_adjust_posts_count_with_nav_filtering_attributes( $with_nav_filtering_data, $use_objects ) { - global $wp_query; - - list($sut, $products) = $this->setup_adjust_posts_test( $with_nav_filtering_data, $use_objects ); - - $products[0]->set_stock_status( 'outofstock' ); - $products[0]->save(); - $products[1]->set_stock_status( 'outofstock' ); - $products[1]->save(); - - $wp_query->set( 'wc_query', 'product_query' ); - $this->assertEquals( 32, $sut->adjust_posts_count( 34, $wp_query ) ); - $this->assertEquals( 32, wc_get_loop_prop( 'total' ) ); - $this->assertEquals( false, wc_get_loop_product_visibility( $products[0]->get_id() ) ); - $this->assertEquals( false, wc_get_loop_product_visibility( $products[1]->get_id() ) ); - foreach ( array_slice( $products, 2 ) as $product ) { - $this->assertEquals( true, wc_get_loop_product_visibility( $product->get_id() ) ); - } - } - - /** - * @testdox adjust_posts should return the input unmodified if get_current_posts returns null. - */ - public function test_adjust_posts_count_when_there_are_no_posts() { - global $wp_query; - - $sut = $this - ->getMockBuilder( WC_Query::class ) - ->setMethods( array( 'get_current_posts', 'get_layered_nav_chosen_attributes_inst' ) ) - ->getMock(); - - $sut->method( 'get_current_posts' )->willReturn( null ); - - $wp_query->set( 'wc_query', 'product_query' ); - $this->assertEquals( 34, $sut->adjust_posts_count( 34, $wp_query ) ); - } - - /** - * @testdox adjust_posts should return the input unmodified if the posts do not represent products. - */ - public function test_adjust_posts_count_when_the_posts_are_not_products() { - global $wp_query; - - list( $sut, $products ) = $this->setup_adjust_posts_test( true, true, 'page' ); - - $products[0]->set_stock_status( 'outofstock' ); - $products[0]->save(); - - $wp_query->set( 'wc_query', 'product_query' ); - $this->assertEquals( 34, $sut->adjust_posts_count( 34, $wp_query ) ); - } - - /** - * @testdox adjust_posts should return the input unmodified if not in the main product query. - */ - public function test_adjust_posts_count_when_not_in_the_main_product_query() { - global $wp_query; - - list( $sut, $products ) = $this->setup_adjust_posts_test( true, true ); - - $products[0]->set_stock_status( 'outofstock' ); - $products[0]->save(); - - $wp_query->set( 'wc_query', null ); - $this->assertEquals( 34, $sut->adjust_posts_count( 34, $wp_query ) ); - } }