diff --git a/includes/class-wc-shortcodes.php b/includes/class-wc-shortcodes.php index 97d171ccef5..ab0ab740e20 100644 --- a/includes/class-wc-shortcodes.php +++ b/includes/class-wc-shortcodes.php @@ -261,8 +261,6 @@ class WC_Shortcodes { $type = 'best_selling_products'; } elseif ( isset( $atts['top_rated'] ) && wc_string_to_bool( $atts['top_rated'] ) ) { $type = 'top_rated_products'; - } elseif ( isset( $atts['featured'] ) && wc_string_to_bool( $atts['featured'] ) ) { - $type = 'featured_products'; } $shortcode = new WC_Shortcode_Products( $atts, $type ); @@ -456,6 +454,8 @@ class WC_Shortcodes { 'cat_operator' => 'IN', ), (array) $atts ); + $atts['visibility'] = 'featured'; + $shortcode = new WC_Shortcode_Products( $atts, 'featured_products' ); return $shortcode->get_content(); diff --git a/includes/shortcodes/class-wc-shortcode-products.php b/includes/shortcodes/class-wc-shortcode-products.php index 9332797bfa8..e66db2ea43b 100644 --- a/includes/shortcodes/class-wc-shortcode-products.php +++ b/includes/shortcodes/class-wc-shortcode-products.php @@ -41,6 +41,14 @@ class WC_Shortcode_Products { */ protected $query_args = array(); + /** + * Set custom visibility. + * + * @since 3.2.0 + * @var bool + */ + protected $custom_visibility = false; + /** * Initialize shortcode. * @@ -105,18 +113,19 @@ class WC_Shortcode_Products { $attributes = $this->parse_legacy_attributes( $attributes ); return shortcode_atts( array( - 'limit' => '-1', // Results limit. - 'columns' => '4', // Number of columns. - 'orderby' => 'title', // menu_order, title, date, rand, price, popularity, rating, or id. - 'order' => 'ASC', // ASC or DESC. - 'ids' => '', // Comma separated IDs. - 'skus' => '', // Comma separated SKUs. - 'category' => '', // Comma separated category slugs. - 'cat_operator' => 'IN', // Operator to compare categories. Possible values are 'IN', 'NOT IN', 'AND'. - 'attribute' => '', // Single attribute slug. - 'terms' => '', // Comma separated term slugs. - 'terms_operator' => 'IN', // Operator to compare terms. Possible values are 'IN', 'NOT IN', 'AND'. - 'class' => '', // HTML class. + 'limit' => '-1', // Results limit. + 'columns' => '4', // Number of columns. + 'orderby' => 'title', // menu_order, title, date, rand, price, popularity, rating, or id. + 'order' => 'ASC', // ASC or DESC. + 'ids' => '', // Comma separated IDs. + 'skus' => '', // Comma separated SKUs. + 'category' => '', // Comma separated category slugs. + 'cat_operator' => 'IN', // Operator to compare categories. Possible values are 'IN', 'NOT IN', 'AND'. + 'attribute' => '', // Single attribute slug. + 'terms' => '', // Comma separated term slugs. + 'terms_operator' => 'IN', // Operator to compare terms. Possible values are 'IN', 'NOT IN', 'AND'. + 'visibility' => 'visible', // Possible values are 'visible', 'catalog', 'search', 'hidden'. + 'class' => '', // HTML class. ), $attributes, $this->type ); } @@ -163,9 +172,12 @@ class WC_Shortcode_Products { // @codingStandardsIgnoreStart $query_args['posts_per_page'] = (int) $this->attributes['limit']; $query_args['meta_query'] = WC()->query->get_meta_query(); - $query_args['tax_query'] = WC()->query->get_tax_query(); + $query_args['tax_query'] = array(); // @codingStandardsIgnoreEnd + // Visibility. + $this->set_visibility_query_args( $query_args ); + // SKUs. $this->set_skus_query_args( $query_args ); @@ -228,7 +240,7 @@ class WC_Shortcode_Products { * @param array $query_args Query args. */ protected function set_attributes_query_args( &$query_args ) { - if ( ! empty( $this->attributes['attribute'] ) || ! empty( $this->attributes['filter'] ) ) { + if ( ! empty( $this->attributes['attribute'] ) || ! empty( $this->attributes['terms'] ) ) { $query_args['tax_query'][] = array( 'taxonomy' => strstr( $this->attributes['attribute'], 'pa_' ) ? sanitize_title( $this->attributes['attribute'] ) : 'pa_' . sanitize_title( $this->attributes['attribute'] ), 'terms' => array_map( 'sanitize_title', explode( ',', $this->attributes['terms'] ) ), @@ -290,18 +302,87 @@ class WC_Shortcode_Products { } /** - * Set featured products query args. + * Set visibility query args. * * @since 3.2.0 * @param array $query_args Query args. */ - protected function set_featured_products_query_args( &$query_args ) { - $query_args['tax_query'][] = array( - 'taxonomy' => 'product_visibility', - 'terms' => 'featured', - 'field' => 'name', - 'operator' => 'IN', - ); + protected function set_visibility_query_args( &$query_args ) { + switch ( $this->attributes['visibility'] ) { + case 'hidden' : + $this->custom_visibility = true; + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ), + 'field' => 'name', + 'operator' => 'AND', + 'include_children' => false, + ); + break; + case 'catalog' : + $this->custom_visibility = true; + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => 'exclude-from-search', + 'field' => 'name', + 'operator' => 'IN', + 'include_children' => false, + ); + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => 'exclude-from-catalog', + 'field' => 'name', + 'operator' => 'NOT IN', + 'include_children' => false, + ); + break; + case 'search' : + $this->custom_visibility = true; + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => 'exclude-from-catalog', + 'field' => 'name', + 'operator' => 'IN', + 'include_children' => false, + ); + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => 'exclude-from-search', + 'field' => 'name', + 'operator' => 'NOT IN', + 'include_children' => false, + ); + break; + case 'featured' : + $query_args['tax_query'] = array_merge( $query_args['tax_query'], WC()->query->get_tax_query() ); + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => 'featured', + 'field' => 'name', + 'operator' => 'IN', + 'include_children' => false, + ); + break; + + default : + $query_args['tax_query'] = array_merge( $query_args['tax_query'], WC()->query->get_tax_query() ); + break; + } + } + + /** + * Set product as visible when quering for hidden products. + * + * @since 3.2.0 + * @param bool $visibility Product visibility. + * @return bool + */ + public function set_product_as_visible( $visibility ) { + if ( $this->custom_visibility ) { + return true; + } + + return $visibility; } /** @@ -323,6 +404,36 @@ class WC_Shortcode_Products { return $classes; } + /** + * Get products. + * + * @since 3.2.0 + * @return array + */ + protected function get_products() { + $transient_name = 'wc_loop' . substr( md5( wp_json_encode( $this->query_args ) . $this->type ), 28 ) . WC_Cache_Helper::get_transient_version( 'product_query' ); + $products = get_transient( $transient_name ); + + if ( false === $products || ! is_a( $products, 'WP_Query' ) ) { + if ( 'top_rated_products' === $this->type ) { + add_filter( 'posts_clauses', array( __CLASS__, 'order_by_rating_post_clauses' ) ); + $products = new WP_Query( $this->query_args ); + remove_filter( 'posts_clauses', array( __CLASS__, 'order_by_rating_post_clauses' ) ); + } else { + $products = new WP_Query( $this->query_args ); + } + + // Remove ordering query arguments. + if ( ! empty( $this->attributes['category'] ) ) { + WC()->query->remove_ordering_args(); + } + + set_transient( $transient_name, $products, DAY_IN_SECONDS * 30 ); + } + + return $products; + } + /** * Loop over found products. * @@ -336,25 +447,7 @@ class WC_Shortcode_Products { $classes = $this->get_wrapper_classes( $columns ); $woocommerce_loop['columns'] = $columns; $woocommerce_loop['name'] = $this->type; - $transient_name = 'wc_loop' . substr( md5( wp_json_encode( $this->query_args ) . $this->type ), 28 ) . WC_Cache_Helper::get_transient_version( 'product_query' ); - $products = get_transient( $transient_name ); - - if ( false === $products || ! is_a( $products, 'WP_Query' ) ) { - if ( 'top_rated_products' === $this->type ) { - add_filter( 'posts_clauses', array( __CLASS__, 'order_by_rating_post_clauses' ) ); - $products = new WP_Query( $this->query_args ); - remove_filter( 'posts_clauses', array( __CLASS__, 'order_by_rating_post_clauses' ) ); - } else { - $products = new WP_Query( $this->query_args ); - } - - set_transient( $transient_name, $products, DAY_IN_SECONDS * 30 ); - } - - // Remove ordering query arguments. - if ( ! empty( $this->attributes['category'] ) ) { - WC()->query->remove_ordering_args(); - } + $products = $this->get_products(); ob_start(); @@ -368,7 +461,15 @@ class WC_Shortcode_Products { while ( $products->have_posts() ) { $products->the_post(); + + // Set custom product visibility when quering hidden products. + add_action( 'woocommerce_product_is_visible', array( $this, 'set_product_as_visible' ) ); + + // Render product template. wc_get_template_part( 'content', 'product' ); + + // Restore product visibility. + remove_action( 'woocommerce_product_is_visible', array( $this, 'set_product_as_visible' ) ); } woocommerce_product_loop_end(); diff --git a/tests/unit-tests/shortcodes/products.php b/tests/unit-tests/shortcodes/products.php index 18ed0f9ddc8..025967b6941 100644 --- a/tests/unit-tests/shortcodes/products.php +++ b/tests/unit-tests/shortcodes/products.php @@ -24,6 +24,7 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { 'attribute' => '', 'terms' => '', 'terms_operator' => 'IN', + 'visibility' => 'visible', 'class' => '', ); $this->assertEquals( $expected, $shortcode->get_attributes() ); @@ -44,6 +45,7 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { 'attribute' => '', 'terms' => '', 'terms_operator' => 'IN', + 'visibility' => 'visible', 'class' => '', ); $this->assertEquals( $expected2, $shortcode2->get_attributes() ); @@ -89,11 +91,11 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { ); $this->assertEquals( $expected2, $shortcode2->get_query_args() ); - $shortcode2 = new WC_Shortcode_Products( array( + $shortcode3 = new WC_Shortcode_Products( array( 'ids' => '1,2,3', 'skus' => 'foo,bar', ) ); - $expected2 = array( + $expected3 = array( 'post_type' => 'product', 'post_status' => 'publish', 'ignore_sticky_posts' => true, @@ -105,16 +107,16 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { 'tax_query' => $tax_query, 'post__in' => array( '1', '2', '3' ), ); - $expected2['meta_query'][] = array( + $expected3['meta_query'][] = array( 'key' => '_sku', 'value' => array( 'foo', 'bar' ), 'compare' => 'IN', ); - $this->assertEquals( $expected2, $shortcode2->get_query_args() ); + $this->assertEquals( $expected3, $shortcode3->get_query_args() ); // product_category shortcode. - $shortcode3 = new WC_Shortcode_Products( array( + $shortcode4 = new WC_Shortcode_Products( array( 'per_page' => '12', 'columns' => '4', 'orderby' => 'menu_order title', @@ -122,7 +124,7 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { 'category' => 'clothing', 'operator' => 'IN', ), 'product_category' ); - $expected3 = array( + $expected4 = array( 'post_type' => 'product', 'post_status' => 'publish', 'ignore_sticky_posts' => true, @@ -134,16 +136,17 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { 'tax_query' => $tax_query, 'meta_key' => '', ); - $expected3['tax_query'][] = array( + $expected4['tax_query'][] = array( 'taxonomy' => 'product_cat', 'terms' => array( 'clothing' ), 'field' => 'slug', 'operator' => 'IN', ); - $this->assertEquals( $expected3, $shortcode3->get_query_args() ); + + $this->assertEquals( $expected4, $shortcode4->get_query_args() ); // recent_products shortcode. - $shortcode4 = new WC_Shortcode_Products( array( + $shortcode5 = new WC_Shortcode_Products( array( 'per_page' => '12', 'columns' => '4', 'orderby' => 'date', @@ -151,7 +154,7 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { 'category' => '', 'operator' => 'IN', ), 'recent_products' ); - $expected4 = array( + $expected5 = array( 'post_type' => 'product', 'post_status' => 'publish', 'ignore_sticky_posts' => true, @@ -163,14 +166,14 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { 'tax_query' => $tax_query, ); - $this->assertEquals( $expected4, $shortcode4->get_query_args() ); + $this->assertEquals( $expected5, $shortcode5->get_query_args() ); // product shortcode. - $shortcode5 = new WC_Shortcode_Products( array( + $shortcode6 = new WC_Shortcode_Products( array( 'ids' => '1', 'per_page' => '1', ), 'product' ); - $expected5 = array( + $expected6 = array( 'post_type' => 'product', 'post_status' => 'publish', 'ignore_sticky_posts' => true, @@ -183,10 +186,10 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { 'p' => '1', ); - $this->assertEquals( $expected5, $shortcode5->get_query_args() ); + $this->assertEquals( $expected6, $shortcode6->get_query_args() ); // sale_products shortcode. - $shortcode6 = new WC_Shortcode_Products( array( + $shortcode7 = new WC_Shortcode_Products( array( 'per_page' => '12', 'columns' => '4', 'orderby' => 'title', @@ -194,7 +197,7 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { 'category' => '', 'operator' => 'IN', ), 'sale_products' ); - $expected6 = array( + $expected7 = array( 'post_type' => 'product', 'post_status' => 'publish', 'ignore_sticky_posts' => true, @@ -207,16 +210,16 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { 'post__in' => array_merge( array( 0 ), wc_get_product_ids_on_sale() ), ); - $this->assertEquals( $expected6, $shortcode6->get_query_args() ); + $this->assertEquals( $expected7, $shortcode7->get_query_args() ); // best_selling_products shortcode. - $shortcode7 = new WC_Shortcode_Products( array( + $shortcode8 = new WC_Shortcode_Products( array( 'per_page' => '12', 'columns' => '4', 'category' => '', 'operator' => 'IN', ), 'best_selling_products' ); - $expected7 = array( + $expected8 = array( 'post_type' => 'product', 'post_status' => 'publish', 'ignore_sticky_posts' => true, @@ -229,10 +232,10 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { 'meta_key' => 'total_sales', ); - $this->assertEquals( $expected7, $shortcode7->get_query_args() ); + $this->assertEquals( $expected8, $shortcode8->get_query_args() ); // top_rated_products shortcode. - $shortcode8 = new WC_Shortcode_Products( array( + $shortcode9 = new WC_Shortcode_Products( array( 'per_page' => '12', 'columns' => '4', 'orderby' => 'title', @@ -240,7 +243,7 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { 'category' => '', 'operator' => 'IN', ), 'top_rated_products' ); - $expected8 = array( + $expected9 = array( 'post_type' => 'product', 'post_status' => 'publish', 'ignore_sticky_posts' => true, @@ -252,16 +255,19 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { 'tax_query' => $tax_query, ); + $this->assertEquals( $expected9, $shortcode9->get_query_args() ); + // featured_products shortcode. - $shortcode9 = new WC_Shortcode_Products( array( - 'per_page' => '12', - 'columns' => '4', - 'orderby' => 'date', - 'order' => 'DESC', - 'category' => '', - 'operator' => 'IN', - ), 'featured_products' ); - $expected9 = array( + $shortcode10 = new WC_Shortcode_Products( array( + 'per_page' => '12', + 'columns' => '4', + 'orderby' => 'date', + 'order' => 'DESC', + 'category' => '', + 'operator' => 'IN', + 'visibility' => 'featured', + ) ); + $expected10 = array( 'post_type' => 'product', 'post_status' => 'publish', 'ignore_sticky_posts' => true, @@ -272,16 +278,19 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { 'meta_query' => $meta_query, 'tax_query' => array_merge( $tax_query, array( array( - 'taxonomy' => 'product_visibility', - 'field' => 'name', - 'terms' => 'featured', - 'operator' => 'IN', + 'taxonomy' => 'product_visibility', + 'terms' => 'featured', + 'field' => 'name', + 'operator' => 'IN', + 'include_children' => false, ), ) ), ); + $this->assertEquals( $expected10, $shortcode10->get_query_args() ); + // product_attribute shortcode. - $shortcode10 = new WC_Shortcode_Products( array( + $shortcode11 = new WC_Shortcode_Products( array( 'per_page' => '12', 'columns' => '4', 'orderby' => 'title', @@ -289,7 +298,7 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { 'attribute' => 'color', 'filter' => 'black', ), 'product_attribute' ); - $expected10 = array( + $expected11 = array( 'post_type' => 'product', 'post_status' => 'publish', 'ignore_sticky_posts' => true, @@ -308,7 +317,97 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { ) ), ); - $this->assertEquals( $expected10, $shortcode10->get_query_args() ); + $this->assertEquals( $expected11, $shortcode11->get_query_args() ); + + // Check for visibility shortcode. + $shortcode12 = new WC_Shortcode_Products( array( + 'visibility' => 'hidden', + ) ); + $expected12 = array( + 'post_type' => 'product', + 'post_status' => 'publish', + 'ignore_sticky_posts' => true, + 'no_found_rows' => true, + 'orderby' => 'title', + 'order' => 'ASC', + 'posts_per_page' => -1, + 'meta_query' => $meta_query, + 'tax_query' => array( + array( + 'taxonomy' => 'product_visibility', + 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ), + 'field' => 'name', + 'operator' => 'AND', + 'include_children' => false, + ), + ), + ); + + $this->assertEquals( $expected12, $shortcode12->get_query_args() ); + + $shortcode13 = new WC_Shortcode_Products( array( + 'visibility' => 'catalog', + ) ); + $expected13 = array( + 'post_type' => 'product', + 'post_status' => 'publish', + 'ignore_sticky_posts' => true, + 'no_found_rows' => true, + 'orderby' => 'title', + 'order' => 'ASC', + 'posts_per_page' => -1, + 'meta_query' => $meta_query, + 'tax_query' => array( + array( + 'taxonomy' => 'product_visibility', + 'terms' => 'exclude-from-search', + 'field' => 'name', + 'operator' => 'IN', + 'include_children' => false, + ), + array( + 'taxonomy' => 'product_visibility', + 'terms' => 'exclude-from-catalog', + 'field' => 'name', + 'operator' => 'NOT IN', + 'include_children' => false, + ), + ), + ); + + $this->assertEquals( $expected13, $shortcode13->get_query_args() ); + + $shortcode14 = new WC_Shortcode_Products( array( + 'visibility' => 'search', + ) ); + $expected14 = array( + 'post_type' => 'product', + 'post_status' => 'publish', + 'ignore_sticky_posts' => true, + 'no_found_rows' => true, + 'orderby' => 'title', + 'order' => 'ASC', + 'posts_per_page' => -1, + 'meta_query' => $meta_query, + 'tax_query' => array( + array( + 'taxonomy' => 'product_visibility', + 'terms' => 'exclude-from-catalog', + 'field' => 'name', + 'operator' => 'IN', + 'include_children' => false, + ), + array( + 'taxonomy' => 'product_visibility', + 'terms' => 'exclude-from-search', + 'field' => 'name', + 'operator' => 'NOT IN', + 'include_children' => false, + ), + ), + ); + + $this->assertEquals( $expected14, $shortcode14->get_query_args() ); } /** @@ -330,6 +429,19 @@ class WC_Test_Shortcode_Products extends WC_Unit_Test_Case { $this->assertTrue( ! empty( $result ) ); } + /** + * Test: WC_Shortcode_Products::set_product_as_visible. + */ + public function test_set_product_as_visible() { + $shortcode = new WC_Shortcode_Products(); + $this->assertFalse( $shortcode->set_product_as_visible( false ) ); + + $shortcode2 = new WC_Shortcode_Products( array( + 'visibility' => 'hidden', + ) ); + $this->assertTrue( $shortcode2->set_product_as_visible( false ) ); + } + /** * Test: WC_Shortcode_Products::order_by_rating_post_clauses. */