diff --git a/includes/class-wc-breadcrumb.php b/includes/class-wc-breadcrumb.php
index 1a3aa6dc909..6bc2d104792 100644
--- a/includes/class-wc-breadcrumb.php
+++ b/includes/class-wc-breadcrumb.php
@@ -8,6 +8,8 @@
defined( 'ABSPATH' ) || exit;
+use Automattic\WooCommerce\Loop;
+
/**
* Breadcrumb class.
*/
@@ -20,6 +22,20 @@ class WC_Breadcrumb {
*/
protected $crumbs = array();
+ /**
+ * @var Loop Reference to the instance of the Loop class.
+ */
+ protected $loop;
+
+ /**
+ * Create an instance of the class.
+ *
+ * @param string $loop Usually null, a mocked instance of Loop can be passed for unit testing.
+ */
+ public function __construct( $loop = null ) {
+ $this->loop = is_null( $loop ) ? Loop::get_instance() : $loop;
+ }
+
/**
* Add a crumb so we don't get lost.
*
@@ -371,7 +387,7 @@ class WC_Breadcrumb {
* Add a breadcrumb for pagination.
*/
protected function paged_trail() {
- if ( get_query_var( 'paged' ) && 'subcategories' !== woocommerce_get_loop_display_mode() ) {
+ if ( get_query_var( 'paged' ) && 'subcategories' !== $this->loop->get_loop_display_mode() ) {
/* translators: %d: page number */
$this->add_crumb( sprintf( __( 'Page %d', 'woocommerce' ), get_query_var( 'paged' ) ) );
}
diff --git a/includes/class-wc-shortcodes.php b/includes/class-wc-shortcodes.php
index a26419ae89e..f5590142c5e 100644
--- a/includes/class-wc-shortcodes.php
+++ b/includes/class-wc-shortcodes.php
@@ -150,6 +150,8 @@ class WC_Shortcodes {
* @return string
*/
public static function product_categories( $atts ) {
+ $loop = Loop::get_instance();
+
if ( isset( $atts['number'] ) ) {
$atts['limit'] = $atts['number'];
}
@@ -210,13 +212,13 @@ class WC_Shortcodes {
$columns = absint( $atts['columns'] );
- wc_set_loop_prop( 'columns', $columns );
- wc_set_loop_prop( 'is_shortcode', true );
+ $loop->set_loop_prop( 'columns', $columns );
+ $loop->set_loop_prop( 'is_shortcode', true );
ob_start();
if ( $product_categories ) {
- woocommerce_product_loop_start();
+ $loop->product_loop_start();
foreach ( $product_categories as $category ) {
wc_get_template(
@@ -227,10 +229,10 @@ class WC_Shortcodes {
);
}
- woocommerce_product_loop_end();
+ $loop->product_loop_end();
}
- woocommerce_reset_loop();
+ $loop->reset_loop();
return '
' . ob_get_clean() . '
';
}
diff --git a/includes/class-woocommerce.php b/includes/class-woocommerce.php
index f5acc2122c2..961647c829d 100644
--- a/includes/class-woocommerce.php
+++ b/includes/class-woocommerce.php
@@ -8,6 +8,8 @@
defined( 'ABSPATH' ) || exit;
+use Automattic\WooCommerce\Loop;
+
/**
* Main WooCommerce Class.
*
@@ -154,6 +156,7 @@ final class WooCommerce {
$this->define_constants();
$this->define_tables();
$this->includes();
+ $this->init_singletons();
$this->init_hooks();
}
@@ -472,6 +475,10 @@ final class WooCommerce {
$this->api->init();
}
+ private function init_singletons() {
+ Loop::get_instance();
+ }
+
/**
* Include classes for theme support.
*
diff --git a/includes/shortcodes/class-wc-shortcode-products.php b/includes/shortcodes/class-wc-shortcode-products.php
index 4e31f8418ef..d71382129a4 100644
--- a/includes/shortcodes/class-wc-shortcode-products.php
+++ b/includes/shortcodes/class-wc-shortcode-products.php
@@ -6,6 +6,8 @@
* @version 3.2.4
*/
+use Automattic\WooCommerce\Loop;
+
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@@ -47,17 +49,25 @@ class WC_Shortcode_Products {
*/
protected $custom_visibility = false;
+ /**
+ * @var Loop Reference to the instance of the Loop class.
+ */
+ protected $loop;
+
/**
* Initialize shortcode.
*
* @since 3.2.0
- * @param array $attributes Shortcode attributes.
- * @param string $type Shortcode type.
+ * @param array $attributes Shortcode attributes.
+ * @param string $type Shortcode type.
+ * @param Loop|null $loop Usually null, a mocked instance of Loop can be passed for unit testing.
*/
- public function __construct( $attributes = array(), $type = 'products' ) {
+ public function __construct( $attributes = array(), $type = 'products', $loop = null ) {
$this->type = $type;
$this->attributes = $this->parse_attributes( $attributes );
$this->query_args = $this->parse_query_args();
+
+ $this->loop = is_null( $loop ) ? Loop::get_instance() : $loop;
}
/**
@@ -623,7 +633,7 @@ class WC_Shortcode_Products {
}
// Setup the loop.
- wc_setup_loop(
+ $this->loop->setup_loop(
array(
'columns' => $columns,
'name' => $this->type,
@@ -646,9 +656,9 @@ class WC_Shortcode_Products {
do_action( 'woocommerce_before_shop_loop' );
}
- woocommerce_product_loop_start();
+ $this->loop->product_loop_start();
- if ( wc_get_loop_prop( 'total' ) ) {
+ if ( $this->loop->get_loop_prop( 'total' ) ) {
foreach ( $products->ids as $product_id ) {
$GLOBALS['post'] = get_post( $product_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
setup_postdata( $GLOBALS['post'] );
@@ -665,7 +675,7 @@ class WC_Shortcode_Products {
}
$GLOBALS['post'] = $original_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
- woocommerce_product_loop_end();
+ $this->loop->product_loop_end();
// Fire standard shop loop hooks when paginating results so we can show result counts and so on.
if ( wc_string_to_bool( $this->attributes['paginate'] ) ) {
@@ -675,7 +685,7 @@ class WC_Shortcode_Products {
do_action( "woocommerce_shortcode_after_{$this->type}_loop", $this->attributes );
wp_reset_postdata();
- wc_reset_loop();
+ $this->loop->reset_loop();
} else {
do_action( "woocommerce_shortcode_{$this->type}_loop_no_results", $this->attributes );
}
diff --git a/includes/wc-template-functions.php b/includes/wc-template-functions.php
index 26a2f26c45a..c57bdcd8a1a 100644
--- a/includes/wc-template-functions.php
+++ b/includes/wc-template-functions.php
@@ -9,6 +9,7 @@
*/
use Automattic\Jetpack\Constants;
+use Automattic\WooCommerce\Loop;
defined( 'ABSPATH' ) || exit;
@@ -154,55 +155,21 @@ add_action( 'the_post', 'wc_setup_product_data' );
*
* @since 3.3.0
* @param array $args Args to pass into the global.
+ * @deprecated Use setup_loop in Loop class instead.
*/
function wc_setup_loop( $args = array() ) {
- $default_args = array(
- 'loop' => 0,
- 'columns' => wc_get_default_products_per_row(),
- 'name' => '',
- 'is_shortcode' => false,
- 'is_paginated' => true,
- 'is_search' => false,
- 'is_filtered' => false,
- 'total' => 0,
- 'total_pages' => 0,
- 'per_page' => 0,
- 'current_page' => 1,
- );
-
- // If this is a main WC query, use global args as defaults.
- if ( $GLOBALS['wp_query']->get( 'wc_query' ) ) {
- $default_args = array_merge(
- $default_args,
- array(
- 'is_search' => $GLOBALS['wp_query']->is_search(),
- 'is_filtered' => is_filtered(),
- 'total' => $GLOBALS['wp_query']->found_posts,
- 'total_pages' => $GLOBALS['wp_query']->max_num_pages,
- 'per_page' => $GLOBALS['wp_query']->get( 'posts_per_page' ),
- 'current_page' => max( 1, $GLOBALS['wp_query']->get( 'paged', 1 ) ),
- )
- );
- }
-
- // Merge any existing values.
- if ( isset( $GLOBALS['woocommerce_loop'] ) ) {
- $default_args = array_merge( $default_args, $GLOBALS['woocommerce_loop'] );
- }
-
- $GLOBALS['woocommerce_loop'] = wp_parse_args( $args, $default_args );
+ Loop::get_instance()->setup_loop( $args );
}
-add_action( 'woocommerce_before_shop_loop', 'wc_setup_loop' );
/**
* Resets the woocommerce_loop global.
*
* @since 3.3.0
+ * @deprecated Use reset_loop in Loop class instead.
*/
function wc_reset_loop() {
- unset( $GLOBALS['woocommerce_loop'] );
+ Loop::get_instance()->reset_loop();
}
-add_action( 'woocommerce_after_shop_loop', 'woocommerce_reset_loop', 999 );
/**
* Gets a property from the woocommerce_loop global.
@@ -211,11 +178,10 @@ add_action( 'woocommerce_after_shop_loop', 'woocommerce_reset_loop', 999 );
* @param string $prop Prop to get.
* @param string $default Default if the prop does not exist.
* @return mixed
+ * @deprecated Use get_loop_prop in Loop class instead.
*/
function wc_get_loop_prop( $prop, $default = '' ) {
- wc_setup_loop(); // Ensure shop loop is setup.
-
- return isset( $GLOBALS['woocommerce_loop'], $GLOBALS['woocommerce_loop'][ $prop ] ) ? $GLOBALS['woocommerce_loop'][ $prop ] : $default;
+ return Loop::get_instance()->get_loop_prop( $prop, $default );
}
/**
@@ -224,12 +190,10 @@ function wc_get_loop_prop( $prop, $default = '' ) {
* @since 3.3.0
* @param string $prop Prop to set.
* @param string $value Value to set.
+ * @deprecated Use get_loop_prop in Loop class instead.
*/
function wc_set_loop_prop( $prop, $value = '' ) {
- if ( ! isset( $GLOBALS['woocommerce_loop'] ) ) {
- wc_setup_loop();
- }
- $GLOBALS['woocommerce_loop'][ $prop ] = $value;
+ Loop::get_instance()->set_loop_prop( $prop, $value );
}
/**
@@ -239,9 +203,10 @@ function wc_set_loop_prop( $prop, $value = '' ) {
*
* @since 3.4.0
* @return bool
+ * @deprecated Use in_product_loop in Loop class instead.
*/
function woocommerce_product_loop() {
- return have_posts() || 'products' !== woocommerce_get_loop_display_mode();
+ return Loop::get_instance()->in_product_loop();
}
/**
@@ -417,23 +382,10 @@ add_action( 'after_switch_theme', 'wc_reset_product_grid_settings' );
*
* @since 2.6.0
* @return string
+ * @deprecated Use get_loop_class in Loop class instead.
*/
function wc_get_loop_class() {
- $loop_index = wc_get_loop_prop( 'loop', 0 );
- $columns = absint( max( 1, wc_get_loop_prop( 'columns', wc_get_default_products_per_row() ) ) );
-
- $loop_index ++;
- wc_set_loop_prop( 'loop', $loop_index );
-
- if ( 0 === ( $loop_index - 1 ) % $columns || 1 === $columns ) {
- return 'first';
- }
-
- if ( 0 === $loop_index % $columns ) {
- return 'last';
- }
-
- return '';
+ return Loop::get_instance()->get_loop_class();
}
@@ -448,10 +400,12 @@ function wc_get_loop_class() {
* @return array
*/
function wc_get_product_cat_class( $class = '', $category = null ) {
+ $loop = Loop::get_instance();
+
$classes = is_array( $class ) ? $class : array_map( 'trim', explode( ' ', $class ) );
$classes[] = 'product-category';
$classes[] = 'product';
- $classes[] = wc_get_loop_class();
+ $classes[] = $loop->get_loop_class();
$classes = apply_filters( 'product_cat_class', $classes, $class, $category );
return array_unique( array_filter( $classes ) );
@@ -479,8 +433,10 @@ function wc_product_post_class( $classes, $class = '', $post_id = 0 ) {
return $classes;
}
+ $loop = Loop::get_instance();
+
$classes[] = 'product';
- $classes[] = wc_get_loop_class();
+ $classes[] = $loop->get_loop_class();
$classes[] = $product->get_stock_status();
if ( $product->is_on_sale() ) {
@@ -927,6 +883,7 @@ if ( ! function_exists( 'woocommerce_content' ) ) {
* without hooks or modifying core templates.
*/
function woocommerce_content() {
+ $loop = Loop::get_instance();
if ( is_singular( 'product' ) ) {
@@ -946,20 +903,20 @@ if ( ! function_exists( 'woocommerce_content' ) ) {
-
+ in_product_loop() ) : ?>
-
+ product_loop_start(); ?>
-
+ get_loop_prop( 'total' ) ) : ?>
-
+ product_loop_end(); ?>
@@ -1076,21 +1033,10 @@ if ( ! function_exists( 'woocommerce_product_loop_start' ) ) {
*
* @param bool $echo Should echo?.
* @return string
+ * @deprecated Use product_loop_start in Loop class instead.
*/
function woocommerce_product_loop_start( $echo = true ) {
- ob_start();
-
- wc_set_loop_prop( 'loop', 0 );
-
- wc_get_template( 'loop/loop-start.php' );
-
- $loop_start = apply_filters( 'woocommerce_product_loop_start', ob_get_clean() );
-
- if ( $echo ) {
- echo $loop_start; // WPCS: XSS ok.
- } else {
- return $loop_start;
- }
+ Loop::get_instance()->product_loop_start( $echo );
}
}
@@ -1101,19 +1047,10 @@ if ( ! function_exists( 'woocommerce_product_loop_end' ) ) {
*
* @param bool $echo Should echo?.
* @return string
+ * @deprecated Use product_loop_end in Loop class instead.
*/
function woocommerce_product_loop_end( $echo = true ) {
- ob_start();
-
- wc_get_template( 'loop/loop-end.php' );
-
- $loop_end = apply_filters( 'woocommerce_product_loop_end', ob_get_clean() );
-
- if ( $echo ) {
- echo $loop_end; // WPCS: XSS ok.
- } else {
- return $loop_end;
- }
+ Loop::get_instance()->product_loop_end( $echo );
}
}
if ( ! function_exists( 'woocommerce_template_loop_product_title' ) ) {
@@ -1332,13 +1269,15 @@ if ( ! function_exists( 'woocommerce_result_count' ) ) {
* Output the result count text (Showing x - x of x results).
*/
function woocommerce_result_count() {
- if ( ! wc_get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) {
+ $loop = Loop::get_instance();
+
+ if ( ! $loop->get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) {
return;
}
$args = array(
- 'total' => wc_get_loop_prop( 'total' ),
- 'per_page' => wc_get_loop_prop( 'per_page' ),
- 'current' => wc_get_loop_prop( 'current_page' ),
+ 'total' => $loop->get_loop_prop( 'total' ),
+ 'per_page' => $loop->get_loop_prop( 'per_page' ),
+ 'current' => $loop->get_loop_prop( 'current_page' ),
);
wc_get_template( 'loop/result-count.php', $args );
@@ -1351,7 +1290,9 @@ if ( ! function_exists( 'woocommerce_catalog_ordering' ) ) {
* Output the product sorting options.
*/
function woocommerce_catalog_ordering() {
- if ( ! wc_get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) {
+ $loop = Loop::get_instance();
+
+ if ( ! $loop->get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) {
return;
}
$show_default_orderby = 'menu_order' === apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', 'menu_order' ) );
@@ -1367,10 +1308,10 @@ if ( ! function_exists( 'woocommerce_catalog_ordering' ) ) {
)
);
- $default_orderby = wc_get_loop_prop( 'is_search' ) ? 'relevance' : apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', '' ) );
+ $default_orderby = $loop->get_loop_prop( 'is_search' ) ? 'relevance' : apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', '' ) );
$orderby = isset( $_GET['orderby'] ) ? wc_clean( wp_unslash( $_GET['orderby'] ) ) : $default_orderby; // WPCS: sanitization ok, input var ok, CSRF ok.
- if ( wc_get_loop_prop( 'is_search' ) ) {
+ if ( $loop->get_loop_prop( 'is_search' ) ) {
$catalog_orderby_options = array_merge( array( 'relevance' => __( 'Relevance', 'woocommerce' ) ), $catalog_orderby_options );
unset( $catalog_orderby_options['menu_order'] );
@@ -1405,18 +1346,20 @@ if ( ! function_exists( 'woocommerce_pagination' ) ) {
* Output the pagination.
*/
function woocommerce_pagination() {
- if ( ! wc_get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) {
+ $loop = Loop::get_instance();
+
+ if ( ! $loop->get_loop_prop( 'is_paginated' ) || ! woocommerce_products_will_display() ) {
return;
}
$args = array(
- 'total' => wc_get_loop_prop( 'total_pages' ),
- 'current' => wc_get_loop_prop( 'current_page' ),
+ 'total' => $loop->get_loop_prop( 'total_pages' ),
+ 'current' => $loop->get_loop_prop( 'current_page' ),
'base' => esc_url_raw( add_query_arg( 'product-page', '%#%', false ) ),
'format' => '?product-page=%#%',
);
- if ( ! wc_get_loop_prop( 'is_shortcode' ) ) {
+ if ( ! $loop->get_loop_prop( 'is_shortcode' ) ) {
$args['format'] = '';
$args['base'] = esc_url_raw( str_replace( 999999999, '%#%', remove_query_arg( 'add-to-cart', get_pagenum_link( 999999999, false ) ) ) );
}
@@ -2266,9 +2209,11 @@ if ( ! function_exists( 'woocommerce_products_will_display' ) ) {
* @return bool
*/
function woocommerce_products_will_display() {
- $display_type = woocommerce_get_loop_display_mode();
+ $loop = Loop::get_instance();
- return 0 < wc_get_loop_prop( 'total', 0 ) && 'subcategories' !== $display_type;
+ $display_type = $loop->get_loop_display_mode();
+
+ return 0 < $loop->get_loop_prop( 'total', 0 ) && 'subcategories' !== $display_type;
}
}
@@ -2279,43 +2224,10 @@ if ( ! function_exists( 'woocommerce_get_loop_display_mode' ) ) {
*
* @since 3.3.0
* @return string Either products, subcategories, or both, based on current page.
+ * @deprecated Use get_loop_prop in Loop class instead.Use get_loop_prop in Loop class instead.
*/
function woocommerce_get_loop_display_mode() {
- // Only return products when filtering things.
- if ( wc_get_loop_prop( 'is_search' ) || wc_get_loop_prop( 'is_filtered' ) ) {
- return 'products';
- }
-
- $parent_id = 0;
- $display_type = '';
-
- if ( is_shop() ) {
- $display_type = get_option( 'woocommerce_shop_page_display', '' );
- } elseif ( is_product_category() ) {
- $parent_id = get_queried_object_id();
- $display_type = get_term_meta( $parent_id, 'display_type', true );
- $display_type = '' === $display_type ? get_option( 'woocommerce_category_archive_display', '' ) : $display_type;
- }
-
- if ( ( ! is_shop() || 'subcategories' !== $display_type ) && 1 < wc_get_loop_prop( 'current_page' ) ) {
- return 'products';
- }
-
- // Ensure valid value.
- if ( '' === $display_type || ! in_array( $display_type, array( 'products', 'subcategories', 'both' ), true ) ) {
- $display_type = 'products';
- }
-
- // If we're showing categories, ensure we actually have something to show.
- if ( in_array( $display_type, array( 'subcategories', 'both' ), true ) ) {
- $subcategories = woocommerce_get_product_subcategories( $parent_id );
-
- if ( empty( $subcategories ) ) {
- $display_type = 'products';
- }
- }
-
- return $display_type;
+ return Loop::get_instance()->get_loop_display_mode();
}
}
@@ -2329,11 +2241,13 @@ if ( ! function_exists( 'woocommerce_maybe_show_product_subcategories' ) ) {
* @return string
*/
function woocommerce_maybe_show_product_subcategories( $loop_html = '' ) {
- if ( wc_get_loop_prop( 'is_shortcode' ) && ! WC_Template_Loader::in_content_filter() ) {
+ $loop = Loop::get_instance();
+
+ if ( $loop->get_loop_prop( 'is_shortcode' ) && ! WC_Template_Loader::in_content_filter() ) {
return $loop_html;
}
- $display_type = woocommerce_get_loop_display_mode();
+ $display_type = $loop->get_loop_display_mode();
// If displaying categories, append to the loop.
if ( 'subcategories' === $display_type || 'both' === $display_type ) {
@@ -2346,9 +2260,9 @@ if ( ! function_exists( 'woocommerce_maybe_show_product_subcategories' ) ) {
$loop_html .= ob_get_clean();
if ( 'subcategories' === $display_type ) {
- wc_set_loop_prop( 'total', 0 );
+ $loop->set_loop_prop( 'total', 0 );
- // This removes pagination and products from display for themes not using wc_get_loop_prop in their product loops. @todo Remove in future major version.
+ // This removes pagination and products from display for themes not using get_loop_prop in their product loops. @todo Remove in future major version.
global $wp_query;
if ( $wp_query->is_main_query() ) {
@@ -2379,6 +2293,8 @@ if ( ! function_exists( 'woocommerce_product_subcategories' ) ) {
* @return null|boolean
*/
function woocommerce_product_subcategories( $args = array() ) {
+ $loop = Loop::get_instance();
+
$defaults = array(
'before' => '',
'after' => '',
@@ -2399,10 +2315,10 @@ if ( ! function_exists( 'woocommerce_product_subcategories' ) ) {
return true;
} else {
// Output nothing. woocommerce_maybe_show_product_subcategories will handle the output of cats.
- $display_type = woocommerce_get_loop_display_mode();
+ $display_type = $loop->get_loop_display_mode();
if ( 'subcategories' === $display_type ) {
- // This removes pagination and products from display for themes not using wc_get_loop_prop in their product loops. @todo Remove in future major version.
+ // This removes pagination and products from display for themes not using get_loop_prop in their product loops. @todo Remove in future major version.
global $wp_query;
if ( $wp_query->is_main_query() ) {
@@ -3658,7 +3574,7 @@ if ( ! function_exists( 'woocommerce_reset_loop' ) ) {
* @deprecated 3.3
*/
function woocommerce_reset_loop() {
- wc_reset_loop();
+ Loop::get_instance()->reset_loop();
}
}
diff --git a/phpunit.xml b/phpunit.xml
index 75b6a007d51..1464f41e098 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -49,4 +49,7 @@
+
+
+
diff --git a/src/Autoloader.php b/src/Autoloader.php
index df56062b10e..39bc7209bdc 100644
--- a/src/Autoloader.php
+++ b/src/Autoloader.php
@@ -9,6 +9,8 @@ namespace Automattic\WooCommerce;
defined( 'ABSPATH' ) || exit;
+require_once dirname( __DIR__ ) . '/src/Loop.php';
+
/**
* Autoloader class.
*
diff --git a/src/Loop.php b/src/Loop.php
new file mode 100644
index 00000000000..375f48cc8a2
--- /dev/null
+++ b/src/Loop.php
@@ -0,0 +1,255 @@
+ 0,
+ 'columns' => wc_get_default_products_per_row(),
+ 'name' => '',
+ 'is_shortcode' => false,
+ 'is_paginated' => true,
+ 'is_search' => false,
+ 'is_filtered' => false,
+ 'total' => 0,
+ 'total_pages' => 0,
+ 'per_page' => 0,
+ 'current_page' => 1,
+ );
+
+ // If this is a main WC query, use global args as defaults.
+ if ( $GLOBALS['wp_query']->get( 'wc_query' ) ) {
+ $default_args = array_merge(
+ $default_args,
+ array(
+ 'is_search' => $GLOBALS['wp_query']->is_search(),
+ 'is_filtered' => is_filtered(),
+ 'total' => $GLOBALS['wp_query']->found_posts,
+ 'total_pages' => $GLOBALS['wp_query']->max_num_pages,
+ 'per_page' => $GLOBALS['wp_query']->get( 'posts_per_page' ),
+ 'current_page' => max( 1, $GLOBALS['wp_query']->get( 'paged', 1 ) ),
+ )
+ );
+ }
+
+ // Merge any existing values.
+ if ( isset( $GLOBALS['woocommerce_loop'] ) ) {
+ $default_args = array_merge( $default_args, $GLOBALS['woocommerce_loop'] );
+ }
+
+ $GLOBALS['woocommerce_loop'] = wp_parse_args( $args, $default_args );
+ }
+
+ /**
+ * Resets the woocommerce_loop global.
+ *
+ * @since 3.3.0
+ */
+ public function reset_loop() {
+ unset( $GLOBALS['woocommerce_loop'] );
+ }
+
+ /**
+ * Gets a property from the woocommerce_loop global.
+ *
+ * @since 3.3.0
+ * @param string $prop Prop to get.
+ * @param string $default Default if the prop does not exist.
+ * @return mixed
+ */
+ public function get_loop_prop( $prop, $default = '' ) {
+ $this->setup_loop(); // Ensure shop loop is setup.
+
+ return isset( $GLOBALS['woocommerce_loop'], $GLOBALS['woocommerce_loop'][ $prop ] ) ? $GLOBALS['woocommerce_loop'][ $prop ] : $default;
+ }
+
+ /**
+ * Sets a property in the woocommerce_loop global.
+ *
+ * @since 3.3.0
+ * @param string $prop Prop to set.
+ * @param string $value Value to set.
+ */
+ public function set_loop_prop( $prop, $value = '' ) {
+ if ( ! isset( $GLOBALS['woocommerce_loop'] ) ) {
+ $this->setup_loop();
+ }
+ $GLOBALS['woocommerce_loop'][ $prop ] = $value;
+ }
+
+ /**
+ * See what is going to display in the loop.
+ *
+ * @since 3.3.0
+ * @return string Either products, subcategories, or both, based on current page.
+ */
+ public function get_loop_display_mode() {
+ // Only return products when filtering things.
+ if ( $this->get_loop_prop( 'is_search' ) || $this->get_loop_prop( 'is_filtered' ) ) {
+ return 'products';
+ }
+
+ $parent_id = 0;
+ $display_type = '';
+
+ if ( is_shop() ) {
+ $display_type = get_option( 'woocommerce_shop_page_display', '' );
+ } elseif ( is_product_category() ) {
+ $parent_id = get_queried_object_id();
+ $display_type = get_term_meta( $parent_id, 'display_type', true );
+ $display_type = '' === $display_type ? get_option( 'woocommerce_category_archive_display', '' ) : $display_type;
+ }
+
+ if ( ( ! is_shop() || 'subcategories' !== $display_type ) && 1 < wc_get_loop_prop( 'current_page' ) ) {
+ return 'products';
+ }
+
+ // Ensure valid value.
+ if ( '' === $display_type || ! in_array( $display_type, array( 'products', 'subcategories', 'both' ), true ) ) {
+ $display_type = 'products';
+ }
+
+ // If we're showing categories, ensure we actually have something to show.
+ if ( in_array( $display_type, array( 'subcategories', 'both' ), true ) ) {
+ $subcategories = woocommerce_get_product_subcategories( $parent_id );
+
+ if ( empty( $subcategories ) ) {
+ $display_type = 'products';
+ }
+ }
+
+ return $display_type;
+ }
+
+ /**
+ * Should the WooCommerce loop be displayed?
+ *
+ * This will return true if we have posts (products) or if we have subcats to display.
+ *
+ * @since 3.4.0
+ * @return bool
+ */
+ public function in_product_loop() {
+ return have_posts() || 'products' !== $this->get_loop_display_mode();
+ }
+
+ /**
+ * Get classname for woocommerce loops.
+ *
+ * @since 2.6.0
+ * @return string
+ * @deprecated Use get_loop_class in Loop class instead.
+ */
+ public function get_loop_class() {
+ $loop_index = $this->get_loop_prop( 'loop', 0 );
+ $columns = absint( max( 1, wc_get_loop_prop( 'columns', wc_get_default_products_per_row() ) ) );
+
+ $loop_index ++;
+ $this->set_loop_prop( 'loop', $loop_index );
+
+ if ( 0 === ( $loop_index - 1 ) % $columns || 1 === $columns ) {
+ return 'first';
+ }
+
+ if ( 0 === $loop_index % $columns ) {
+ return 'last';
+ }
+
+ return '';
+ }
+
+ /**
+ * Output the start of a product loop. By default this is a UL.
+ *
+ * @param bool $echo Should echo?.
+ * @return string
+ */
+ public function product_loop_start( $echo = true ) {
+ ob_start();
+
+ $this->set_loop_prop( 'loop', 0 );
+
+ wc_get_template( 'loop/loop-start.php' );
+
+ $loop_start = apply_filters( 'woocommerce_product_loop_start', ob_get_clean() );
+
+ if ( $echo ) {
+ echo $loop_start; // WPCS: XSS ok.
+ } else {
+ return $loop_start;
+ }
+ }
+
+ /**
+ * Output the end of a product loop. By default this is a UL.
+ *
+ * @param bool $echo Should echo?.
+ * @return string
+ */
+ public function product_loop_end( $echo = true ) {
+ ob_start();
+
+ wc_get_template( 'loop/loop-end.php' );
+
+ $loop_end = apply_filters( 'woocommerce_product_loop_end', ob_get_clean() );
+
+ if ( $echo ) {
+ echo $loop_end; // WPCS: XSS ok.
+ } else {
+ return $loop_end;
+ }
+ }
+}
diff --git a/templates/archive-product.php b/templates/archive-product.php
index e4f3938b3e4..9b9f12a6ce3 100644
--- a/templates/archive-product.php
+++ b/templates/archive-product.php
@@ -17,6 +17,8 @@
defined( 'ABSPATH' ) || exit;
+use Automattic\WooCommerce\Loop;
+
get_header( 'shop' );
/**
@@ -28,6 +30,8 @@ get_header( 'shop' );
*/
do_action( 'woocommerce_before_main_content' );
+$loop = Loop::get_instance();
+
?>
in_product_loop() ) {
/**
* Hook: woocommerce_before_shop_loop.
@@ -56,9 +60,9 @@ if ( woocommerce_product_loop() ) {
*/
do_action( 'woocommerce_before_shop_loop' );
- woocommerce_product_loop_start();
+ $loop->product_loop_start();
- if ( wc_get_loop_prop( 'total' ) ) {
+ if ( $loop->get_loop_prop( 'total' ) ) {
while ( have_posts() ) {
the_post();
@@ -71,7 +75,7 @@ if ( woocommerce_product_loop() ) {
}
}
- woocommerce_product_loop_end();
+ $loop->product_loop_end();
/**
* Hook: woocommerce_after_shop_loop.
diff --git a/templates/cart/cross-sells.php b/templates/cart/cross-sells.php
index c541a0c9d98..15a51e3cf8f 100644
--- a/templates/cart/cross-sells.php
+++ b/templates/cart/cross-sells.php
@@ -17,13 +17,17 @@
defined( 'ABSPATH' ) || exit;
+use Automattic\WooCommerce\Loop;
+
if ( $cross_sells ) : ?>
-
+
+
+ product_loop_start(); ?>
@@ -37,7 +41,7 @@ if ( $cross_sells ) : ?>
-
+ product_loop_end(); ?>
-
+
diff --git a/templates/loop/pagination.php b/templates/loop/pagination.php
index cfa3763dce0..cccbd08facd 100644
--- a/templates/loop/pagination.php
+++ b/templates/loop/pagination.php
@@ -19,8 +19,11 @@ if ( ! defined( 'ABSPATH' ) ) {
exit;
}
-$total = isset( $total ) ? $total : wc_get_loop_prop( 'total_pages' );
-$current = isset( $current ) ? $current : wc_get_loop_prop( 'current_page' );
+use Automattic\WooCommerce\Loop;
+
+$loop = Loop::get_instance();
+$total = isset( $total ) ? $total : $loop->get_loop_prop( 'total_pages' );
+$current = isset( $current ) ? $current : $loop->get_loop_prop( 'current_page' );
$base = isset( $base ) ? $base : esc_url_raw( str_replace( 999999999, '%#%', remove_query_arg( 'add-to-cart', get_pagenum_link( 999999999, false ) ) ) );
$format = isset( $format ) ? $format : '';
diff --git a/templates/single-product/related.php b/templates/single-product/related.php
index a9b4210be57..1a649c13f97 100644
--- a/templates/single-product/related.php
+++ b/templates/single-product/related.php
@@ -19,19 +19,22 @@ if ( ! defined( 'ABSPATH' ) ) {
exit;
}
+use Automattic\WooCommerce\Loop;
+
if ( $related_products ) : ?>
@@ -30,7 +32,9 @@ if ( $upsells ) : ?>
-
+
+
+ product_loop_start(); ?>
@@ -44,7 +48,7 @@ if ( $upsells ) : ?>
-
+ product_loop_end(); ?>