The earliest hook where the global product object is available in WooCommerce

In a perplexing journey that spanned several months, I encountered a peculiar issue that left me questioning whether it was a bug in WordPress core, a bug in Storefront, or simply not the expected way to handle certain operations in WooCommerce and WordPress.

The saga began when our site started experiencing fatal errors due to the global $product object, which I expected to be an object, either turning into a string or, in some cases, being completely unset or null whenever Yoast for WooCommerce was deactivated.

To illustrate, consider the following code snippet used in the wp_enqueue_scripts hook:

function trsdm_test() {
    if ( is_product() ) {
        global $product;

        // Here, $product may be a string or unset/null
        if ( ! $product->is_on_sale() ) {
            // This throws a fatal error because you can't call a method on a string or when $product is null.
        }
    }
}
add_action( 'wp_enqueue_scripts', 'trsdm_test' );Code language: PHP (php)

After an extensive debugging process, I discovered that this issue was linked to a method in the original Yoast SEO plugin that executed wp_reset_query() on the wp hook. Interestingly, removing this reset brought the error back, even with the plugin activated. However, replacing it with wp_reset_postdata() or adding it in the header file before wp_head() and deactivating Yoast for WooCommerce resolved the error.

This means that I found the culprit – or hidden “fix”. I have been developing with Yoast for WooCommerce active, so the global $product object was always available as a WC_Product in wp_enqueue_scripts. This is not the case on a stock install of WooCommerce without other plugins, but I did not realize that because the plugin was active.

Through detailed logging and experimentation, it became clear that the global product was initially unset, then set to a string (the product slug), and fluctuated between being unset and a string during the action firing sequence. After applying wp_reset_postdata(), the global product reverted to the expected WC_Product object.

This peculiar behavior raises a crucial point: wp_enqueue_scripts, even after checking for is_product(), may very well not be an appropriate place to rely on the global $product object being properly initialized. This issue occurs on StoreFront with only WooCommerce active.

So when is the earliest hook you can use the global $product object in WooCommerce?

Well, to answer this (hopefully), we need to look deeper.

After debugging more, it was further uncovered that the “fix” of the problem was only because Yoast SEO for WooCommerce does something normally not done in a WooCommerce environment. This revelation emphasizes the importance of understanding the side effects of plugins on global objects.

When doing the wp_reset_query() call, this core WooCommerce function is called:

/**
 * When the_post is called, put product data into a global.
 *
 * @param mixed $post Post Object.
 * @return WC_Product
 */
function wc_setup_product_data( $post ) {
	unset( $GLOBALS['product'] );

	if ( is_int( $post ) ) {
		$post = get_post( $post );
	}

	if ( empty( $post->post_type ) || ! in_array( $post->post_type, array( 'product', 'product_variation' ), true ) ) {
		return;
	}

	$GLOBALS['product'] = wc_get_product( $post );

	return $GLOBALS['product'];
}
add_action( 'the_post', 'wc_setup_product_data' );Code language: PHP (php)

This sets up the global $product object. As you can see, this happens on priority 10 of the_post. Any hook before this will not be a safe bet to rely on the existence of the global $product object as a WC_Product.

As for why the global product object suddenly is a string, I found the explanation in WordPress core. The global product object becomes a string due to WordPress’s handling of query variables and global objects:

public function register_globals() {
    global $wp_query;

    foreach ( (array) $wp_query->query_vars as $key => $value ) {
        $GLOBALS[ $key ] = $value;
    }

    // Additional code that sets up global variables.
}Code language: PHP (php)

This method sets the global $product ($GLOBALS['product']) to a string because the $wp_query contains query_vars for the product, which in this case is the slug.

I have not been able to uncover why the global $product object fluctuates between states of string, object, and not set. It’s hard to trace and debug globals in PHP, and I ran out of time for this one. But at least I uncovered some interesting information about the global $product object and the reason the fatal error was “hidden” from me during development.

I will be sure to test my code on plain installs of WooCommerce without active plugins in the future, to uncover potential problems that can be masked by things other plugins do.

In conclusion, this journey through debugging reveals the intricate and sometimes unpredictable nature of global objects in WordPress and WooCommerce. The fluctuating state of the global $product object highlights the necessity of cautious reliance on global objects, especially in hooks that fire before WordPress or WooCommerce has fully initialized post-data.

I would go as far as to say that one should be cautious even after that. The state of the global $product object fluctuates through most of the WordPress call stack. You can only know if you can use it by testing that it is indeed available in the hook you plan to use, on a stock install of WooCommerce with Storefront and without plugins.

Understanding these interactions is crucial for developers working within the WordPress ecosystem, reminding us of the importance of thorough testing and investigation when dealing with global objects and plugin interactions.

As for me, I will use the global $post object instead in wp_enqueue_scripts, and fetch the product object using wc_get_product( $post->ID );. I will also have a check that ensures the product is not empty so that the site does not crash if for some reason it is.

Get these knowledge bombs before they are released

Plus exclusive content only for newsletter family members! No spam or yucky sales tactics. The bombs only drop occasionally for a clutter-free inbox.