Commit 7f77a18b6e9 for woocommerce

commit 7f77a18b6e981b99569f49d9debb84b3d02edd5d
Author: Michael Pretty <prettyboymp@users.noreply.github.com>
Date:   Tue Jun 2 20:04:32 2026 -0400

    Enable product object caching by default for new installs (#65445)

    * Enable product object caching by default for new installs

    * Run unit and e2e tests with product object caching enabled

    Force product_instance_caching on in the PHPUnit bootstrap and the e2e site
    setup so both suites exercise the cache-on path that new installs now get by
    default. This surfaces any core code that bypasses the product CRUD/cache
    interfaces (raw SQL or direct postmeta writes without invalidation) as a test
    failure instead of a silent production bug.

    * Flush object cache in download and COGS tests for cache-on runs

    With product object caching enabled by default in the test bootstrap, these
    tests need a fresh product read after changing state that the cached object
    wouldn't otherwise reflect: disabling an Approved Download Directory rule (the
    download enabled state is computed at product load) and registering a
    woocommerce_load_product_cogs_value filter after the product was already saved
    into the cache. Flush the object cache so wc_get_product() re-reads via the data
    store. Behavior is unchanged when the feature is off.

    * Update @since to 11.0.0 for product caching new-install handler

diff --git a/plugins/woocommerce/changelog/enable-product-object-caching-new-installs b/plugins/woocommerce/changelog/enable-product-object-caching-new-installs
new file mode 100644
index 00000000000..36d3ef59bd3
--- /dev/null
+++ b/plugins/woocommerce/changelog/enable-product-object-caching-new-installs
@@ -0,0 +1,4 @@
+Significance: minor
+Type: tweak
+
+Enable the product object caching feature by default for newly installed stores. Existing stores are unaffected and keep the opt-in default.
diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index f2a9ab9d378..c290f9ec4ef 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -10,6 +10,7 @@ use Automattic\Jetpack\Constants;
 use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore;
 use Automattic\WooCommerce\Enums\ProductType;
 use Automattic\WooCommerce\Internal\Admin\EmailImprovements\EmailImprovements;
+use Automattic\WooCommerce\Internal\Caches\ProductCacheController;
 use Automattic\WooCommerce\Internal\TransientFiles\TransientFilesEngine;
 use Automattic\WooCommerce\Internal\DataStores\Orders\{ CustomOrdersTableController, DataSynchronizer, OrdersTableDataStore };
 use Automattic\WooCommerce\Internal\DataStores\StockNotifications\StockNotificationsDataStore;
@@ -374,6 +375,7 @@ class WC_Install {
 		add_action( 'woocommerce_newly_installed', array( __CLASS__, 'enable_email_improvements_for_newly_installed' ), 20 );
 		add_action( 'woocommerce_newly_installed', array( __CLASS__, 'enable_customer_stock_notifications_signups' ), 20 );
 		add_action( 'woocommerce_newly_installed', array( __CLASS__, 'enable_analytics_scheduled_import' ), 20 );
+		add_action( 'woocommerce_newly_installed', array( __CLASS__, 'enable_product_instance_caching_for_newly_installed' ), 20 );
 		add_action( 'woocommerce_updated', array( __CLASS__, 'enable_email_improvements_for_existing_merchants' ), 20 );
 		add_action( 'woocommerce_run_update_callback', array( __CLASS__, 'run_update_callback' ) );
 		add_action( 'woocommerce_update_db_to_current_version', array( __CLASS__, 'update_db_version' ) );
@@ -1308,6 +1310,21 @@ class WC_Install {
 		add_option( 'woocommerce_analytics_scheduled_import', 'yes' );
 	}

+	/**
+	 * Enable product object caching by default for new shops.
+	 *
+	 * Only newly installed stores get the feature enabled here; existing installs keep the
+	 * opt-in default so their behavior is unchanged on upgrade.
+	 *
+	 * @since 11.0.0
+	 *
+	 * @return void
+	 */
+	public static function enable_product_instance_caching_for_newly_installed(): void {
+		$feature_controller = wc_get_container()->get( FeaturesController::class );
+		$feature_controller->change_feature_enable( ProductCacheController::FEATURE_NAME, true );
+	}
+
 	/**
 	 * Enable email improvements by default for existing shops if conditions are met.
 	 *
diff --git a/plugins/woocommerce/tests/e2e-pw/fixtures/site.setup.ts b/plugins/woocommerce/tests/e2e-pw/fixtures/site.setup.ts
index f2d36892de0..bed5b3c6e1f 100644
--- a/plugins/woocommerce/tests/e2e-pw/fixtures/site.setup.ts
+++ b/plugins/woocommerce/tests/e2e-pw/fixtures/site.setup.ts
@@ -1,6 +1,7 @@
 /**
  * External dependencies
  */
+import { request } from '@playwright/test';
 import { WC_API_PATH } from '@woocommerce/e2e-utils-playwright';

 /**
@@ -9,6 +10,7 @@ import { WC_API_PATH } from '@woocommerce/e2e-utils-playwright';
 import { test as setup } from './fixtures';
 import { setComingSoon } from '../utils/coming-soon';
 import { skipOnboardingWizard } from '../utils/onboarding';
+import { setOption } from '../utils/options';

 setup( 'setup site', async ( { baseURL, restApi } ) => {
 	await setup.step( 'configure HPOS', async () => {
@@ -64,6 +66,19 @@ setup( 'setup site', async ( { baseURL, restApi } ) => {
 		);
 	} );

+	await setup.step( 'enable product object caching', async () => {
+		// Always run e2e with product object caching on (the new-install default), so any
+		// flow that bypasses the product CRUD/cache interfaces surfaces as a failure. Set
+		// explicitly rather than relying on the new-install default, so the state is
+		// deterministic regardless of how the test site's DB was provisioned.
+		await setOption(
+			request,
+			baseURL,
+			'woocommerce_feature_product_instance_caching_enabled',
+			'yes'
+		);
+	} );
+
 	await setup.step( 'disable coming soon', async () => {
 		await setComingSoon( { baseURL, enabled: 'no' } );
 	} );
diff --git a/plugins/woocommerce/tests/legacy/bootstrap.php b/plugins/woocommerce/tests/legacy/bootstrap.php
index c7f5f5bf562..6928f4214ca 100644
--- a/plugins/woocommerce/tests/legacy/bootstrap.php
+++ b/plugins/woocommerce/tests/legacy/bootstrap.php
@@ -284,6 +284,13 @@ class WC_Unit_Tests_Bootstrap {

 		WC_Install::install();

+		// Run the test suite with product object caching enabled (the new-install default).
+		// This ensures tests exercise the cache-on path and fail loudly if any code bypasses
+		// the product CRUD/cache interfaces (e.g. raw SQL or direct postmeta writes without
+		// invalidation). install_wc() runs on `setup_theme`, before `init`, so the option is
+		// set in time for ProductCacheController::on_init() to register its invalidation hooks.
+		update_option( 'woocommerce_feature_product_instance_caching_enabled', 'yes' );
+
 		// Reload capabilities after install, see https://core.trac.wordpress.org/ticket/28374.
 		if ( version_compare( $GLOBALS['wp_version'], '4.7', '<' ) ) {
 			$GLOBALS['wp_roles']->reinit();
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/install.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/install.php
index 3dd003f872c..6dacbcf264c 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/install.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/install.php
@@ -237,6 +237,22 @@ class WC_Admin_Tests_Install extends WP_UnitTestCase {
 		$this->assertEquals( 'no', get_option( 'woocommerce_analytics_scheduled_import' ) );
 	}

+	/**
+	 * Test product object caching is enabled by default for new installations.
+	 *
+	 * @return void
+	 */
+	public function test_enable_product_instance_caching_for_new_installation() {
+		// Ensure the feature option doesn't exist before testing.
+		delete_option( 'woocommerce_feature_product_instance_caching_enabled' );
+
+		// Call the method to enable the feature for new installs.
+		WC_Install::enable_product_instance_caching_for_newly_installed();
+
+		// Verify the feature option was set to 'yes'.
+		$this->assertEquals( 'yes', get_option( 'woocommerce_feature_product_instance_caching_enabled' ) );
+	}
+
 	/**
 	 * Test migrate_options();
 	 * @return void
diff --git a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php
index 7d7c250975a..4344030499f 100644
--- a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php
+++ b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-product-test.php
@@ -78,6 +78,11 @@ class WC_Abstract_Product_Test extends WC_Unit_Test_Case {
 		);

 		$this->download_directories->disable_by_id( $this->download_directories->get_by_url( 'https://new.supplier/' )->get_id() );
+
+		// Approved Download Directory rule changes don't invalidate the product object cache, so
+		// flush to force a fresh read that reflects the updated rules.
+		wp_cache_flush();
+
 		$product_downloads = wc_get_product( $this->product->get_id() )->get_downloads();

 		$this->assertCount(
@@ -92,6 +97,7 @@ class WC_Abstract_Product_Test extends WC_Unit_Test_Case {
 		);

 		$this->download_directories->set_mode( Download_Directories::MODE_DISABLED );
+		wp_cache_flush();

 		$this->assertCount(
 			2,
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-download-handler-tests.php b/plugins/woocommerce/tests/php/includes/class-wc-download-handler-tests.php
index 80f681b0383..b11e00a976c 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-download-handler-tests.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-download-handler-tests.php
@@ -112,6 +112,11 @@ class WC_Download_Handler_Tests extends \WC_Unit_Test_Case {

 		// And now with one of the approved directory rules disabled...
 		$approved_directories->disable_by_id( $approved_directory_rule_id );
+
+		// Approved Download Directory rule changes don't invalidate the product object cache, so
+		// flush to force a fresh read that reflects the updated rules.
+		wp_cache_flush();
+
 		$_GET['key']     = $download_keys[1];
 		$wp_die_happened = false;

diff --git a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php
index a33051f623e..27c688253fa 100644
--- a/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php
+++ b/plugins/woocommerce/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php
@@ -299,6 +299,10 @@ class WC_Product_Data_Store_CPT_Test extends WC_Unit_Test_Case {

 		add_filter( 'woocommerce_load_product_cogs_value', fn( $value, $product ) => $value + $product->get_id(), 10, 2 );

+		// The save above populates the product object cache; flush so the re-read goes through the
+		// data store and applies the load filter registered after the save.
+		wp_cache_flush();
+
 		$product = wc_get_product( $product->get_id() );
 		$this->assertEquals( 12.34 + $product->get_id(), $product->get_cogs_value() );
 	}