Revert improved filtering for variations.

This commit reverts the functionality introduced in PR #26260
(later refined by #27175, #27190, #27508) in which filtering by
attribute using the layered nav widget was improved to handle the
cases of variations out of stock. The revert is a response to the
numerous problems reported by users in Woo 4.4 and 4.5

Not all the code has been reverted, only the code that resulted in
visible functionality changes. Thus, the code that generates
term relationships for variations is still in place to keep database
consistency and to keep the reverting changes to the minimum needed.
This commit is contained in:
Nestor Soriano 2020-09-09 12:24:41 +02:00
parent d9990b752e
commit 70a1cb2f1f
6 changed files with 28 additions and 593 deletions

View File

@ -593,94 +593,6 @@ class WC_Product_Variable extends WC_Product {
return true; 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();
}
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -362,23 +362,10 @@ class WC_Query {
if ( 'product_query' !== $query->get( 'wc_query' ) ) { if ( 'product_query' !== $query->get( 'wc_query' ) ) {
return $posts; return $posts;
} }
$this->adjust_total_pages();
$this->remove_product_query_filters( $posts ); $this->remove_product_query_filters( $posts );
return $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 * 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; 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. * 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. // Additonal hooks to change WP Query.
add_filter( 'posts_clauses', array( $this, 'price_filter_post_clauses' ), 10, 2 ); 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( '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 ); do_action( 'woocommerce_product_query', $q, $this );
} }

View File

@ -344,93 +344,51 @@ class WC_Widget_Layered_Nav extends WC_Widget {
protected function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) { protected function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) {
global $wpdb; global $wpdb;
$main_tax_query = $this->get_main_tax_query(); $tax_query = $this->get_main_tax_query();
$meta_query = $this->get_main_meta_query(); $meta_query = $this->get_main_meta_query();
$non_variable_tax_query_sql = array( 'where' => '' ); if ( 'or' === $query_type ) {
$is_and_query = 'and' === $query_type; foreach ( $tax_query as $key => $query ) {
if ( is_array( $query ) && $taxonomy === $query['taxonomy'] ) {
foreach ( $main_tax_query as $key => $query ) { unset( $tax_query[ $key ] );
if ( is_array( $query ) && $taxonomy === $query['taxonomy'] ) {
if ( $is_and_query ) {
$non_variable_tax_query_sql = $this->convert_tax_query_to_sql( array( $query ) );
} }
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 = new WP_Meta_Query( $meta_query );
$tax_query = new WP_Tax_Query( $tax_query );
$meta_query_sql = ( new WP_Meta_Query( $meta_query ) )->get_sql( 'post', $wpdb->posts, 'ID' ); $meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' );
$main_tax_query_sql = $this->convert_tax_query_to_sql( $main_tax_query ); $tax_query_sql = $tax_query->get_sql( $wpdb->posts, 'ID' );
$term_ids_sql = '(' . implode( ',', array_map( 'absint', $term_ids ) ) . ')'; $term_ids_sql = '(' . implode( ',', array_map( 'absint', $term_ids ) ) . ')';
// Generate the first part of the query. // Generate the first part of the query.
// This one will return non-variable products and variable products with concrete values for the attributes. // This one will return non-variable products and variable products with concrete values for the attributes.
$query = array(); $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['from'] = "FROM {$wpdb->posts}";
$query['join'] = " $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->term_taxonomy} AS term_taxonomy USING( term_taxonomy_id )
INNER JOIN {$wpdb->terms} AS terms USING( term_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. " . $tax_query_sql['join'] . $meta_query_sql['join'];
$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;
}
$query['where'] = " $query['where'] = "
WHERE WHERE {$wpdb->posts}.post_type IN ( 'product' )
{$wpdb->posts}.post_status = 'publish' AND {$wpdb->posts}.post_status = 'publish'
{$main_tax_query_sql['where']} {$meta_query_sql['where']} {$tax_query_sql['where']} {$meta_query_sql['where']}
AND ( AND terms.term_id IN $term_ids_sql";
(
{$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}";
$search = $this->get_main_search_query_sql(); $search = $this->get_main_search_query_sql();
if ( $search ) { if ( $search ) {
$query['where'] .= ' AND ' . $search; $query['where'] .= ' AND ' . $search;
} }
$query = apply_filters( 'woocommerce_get_filtered_term_product_counts_query', $query ); $query['group_by'] = 'GROUP BY terms.term_id';
$main_query_sql = implode( ' ', $query ); $query = apply_filters( 'woocommerce_get_filtered_term_product_counts_query', $query );
$query_sql = implode( ' ', $query );
// Generate the second part of the query. // We have a query - let's see if cached results of this query already exist.
// This one will return products having "Any..." as the value of the attribute. $query_hash = md5( $query_sql );
$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 );
// Maybe store a transient of the count values. // Maybe store a transient of the count values.
$cache = apply_filters( 'woocommerce_layered_nav_count_maybe_cache', true ); $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 ] ) ) { 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; $cached_counts[ $query_hash ] = $counts;
if ( true === $cache ) { if ( true === $cache ) {
set_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ), $cached_counts, DAY_IN_SECONDS ); 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 ] ); 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. * 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(); 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. * Show list based layered nav.
* *

View File

@ -20,7 +20,7 @@ defined( 'ABSPATH' ) || exit;
global $product; global $product;
// Ensure visibility. // Ensure visibility.
if ( empty( $product ) || false === wc_get_loop_product_visibility( $product->get_id() ) || ! $product->is_visible() ) { if ( empty( $product ) || ! $product->is_visible() ) {
return; return;
} }
?> ?>

View File

@ -176,199 +176,4 @@ class WC_Tests_Product_Variable extends WC_Unit_Test_Case {
$this->assertEquals( $expected_stock_status, $product->get_stock_status() ); $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() );
}
} }

View File

@ -437,124 +437,4 @@ class WC_Tests_WC_Query extends WC_Unit_Test_Case {
WC()->query->remove_ordering_args(); 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 ) );
}
} }