Commit 2c17a3cdf85 for woocommerce
commit 2c17a3cdf85fa1fd2a5bfc59dc6aff24e3f8ad56
Author: Luigi Teschio <gigitux@gmail.com>
Date: Tue Sep 1 09:44:54 2026 +0200
Fix stale product term caches after reordering (#67829)
* Fix stale product term caches after reordering
* Add changelog entry for product term cache fix
* Add ProductTermCacheInvalidator to handle cache invalidation for product terms
- Introduced ProductTermCacheInvalidator class to invalidate cached product terms when taxonomy caches are cleaned.
- Updated WC_AJAX to remove cache invalidation logic, relying on the new invalidator.
- Added tests for ProductTermCacheInvalidator to ensure proper functionality.
* consolidate unt tests
diff --git a/plugins/woocommerce/changelog/67709-invalidate-product-term-cache b/plugins/woocommerce/changelog/67709-invalidate-product-term-cache
new file mode 100644
index 00000000000..6699c4db8f5
--- /dev/null
+++ b/plugins/woocommerce/changelog/67709-invalidate-product-term-cache
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Invalidate cached product terms when their taxonomy terms are reordered.
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index e91dcef4100..9791b115d7c 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -13,6 +13,7 @@ use Automattic\WooCommerce\Internal\AssignDefaultCategory;
use Automattic\WooCommerce\Internal\TermCount;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
use Automattic\WooCommerce\Internal\Caches\ProductCacheController;
+use Automattic\WooCommerce\Internal\Caches\ProductTermCacheInvalidator;
use Automattic\WooCommerce\Internal\ComingSoon\ComingSoonAdminBarBadge;
use Automattic\WooCommerce\Internal\ComingSoon\ComingSoonCacheInvalidator;
use Automattic\WooCommerce\Internal\ComingSoon\ComingSoonRequestHandler;
@@ -385,6 +386,7 @@ final class WooCommerce {
$container->get( RestockRefundedItemsAdjuster::class );
$container->get( CustomOrdersTableController::class );
$container->get( ProductCacheController::class );
+ $container->get( ProductTermCacheInvalidator::class );
$container->get( OptionSanitizer::class );
$container->get( BatchProcessingController::class );
$container->get( FeaturesController::class );
diff --git a/plugins/woocommerce/includes/wc-term-functions.php b/plugins/woocommerce/includes/wc-term-functions.php
index c0b24a935ec..f74c91954ed 100644
--- a/plugins/woocommerce/includes/wc-term-functions.php
+++ b/plugins/woocommerce/includes/wc-term-functions.php
@@ -152,7 +152,7 @@ function wc_get_object_terms( $object_id, $taxonomy, $field = null, $index_key =
* @return array
*/
function _wc_get_cached_product_terms( $product_id, $taxonomy, $args = array() ) {
- $cache_key = 'wc_' . $taxonomy . md5( wp_json_encode( $args ) );
+ $cache_key = WC_Cache_Helper::get_cache_prefix( 'product_terms_' . $taxonomy ) . 'wc_' . $taxonomy . md5( wp_json_encode( $args ) );
$cache_group = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . $product_id;
$terms = wp_cache_get( $cache_key, $cache_group );
diff --git a/plugins/woocommerce/src/Internal/Caches/ProductTermCacheInvalidator.php b/plugins/woocommerce/src/Internal/Caches/ProductTermCacheInvalidator.php
new file mode 100644
index 00000000000..1c2c665ddfd
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/Caches/ProductTermCacheInvalidator.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * ProductTermCacheInvalidator class file.
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\Caches;
+
+use WC_Cache_Helper;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Invalidates cached product terms when taxonomy caches are cleaned.
+ *
+ * @since 11.2.0
+ *
+ * @internal
+ */
+class ProductTermCacheInvalidator {
+ /**
+ * Register cache invalidation hooks.
+ *
+ * @since 11.2.0
+ *
+ * @internal
+ */
+ final public function init(): void {
+ add_action( 'clean_taxonomy_cache', array( $this, 'handle_clean_taxonomy_cache' ) );
+ }
+
+ /**
+ * Invalidate cached product terms after a taxonomy cache is cleaned.
+ *
+ * @since 11.2.0
+ *
+ * @internal
+ *
+ * @param mixed $taxonomy Taxonomy name.
+ */
+ public function handle_clean_taxonomy_cache( $taxonomy ): void {
+ if ( ! is_string( $taxonomy ) || ! is_object_in_taxonomy( 'product', $taxonomy ) ) {
+ return;
+ }
+
+ WC_Cache_Helper::invalidate_cache_group( 'product_terms_' . $taxonomy );
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Caches/ProductTermCacheInvalidatorTest.php b/plugins/woocommerce/tests/php/src/Internal/Caches/ProductTermCacheInvalidatorTest.php
new file mode 100644
index 00000000000..23f6525af05
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/Caches/ProductTermCacheInvalidatorTest.php
@@ -0,0 +1,93 @@
+<?php
+/**
+ * ProductTermCacheInvalidator tests.
+ *
+ * @package WooCommerce\Tests\Internal\Caches
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\Caches;
+
+use Automattic\WooCommerce\Internal\Caches\ProductTermCacheInvalidator;
+use WC_Cache_Helper;
+use WC_Helper_Product;
+use WC_Product;
+use WC_Unit_Test_Case;
+use WP_Term;
+
+/**
+ * Tests for ProductTermCacheInvalidator.
+ */
+final class ProductTermCacheInvalidatorTest extends WC_Unit_Test_Case {
+ /**
+ * The System Under Test.
+ *
+ * @var ProductTermCacheInvalidator
+ */
+ private $sut;
+
+ /**
+ * Set up test fixtures.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->sut = wc_get_container()->get( ProductTermCacheInvalidator::class );
+ }
+
+ /**
+ * @testdox Reordering product terms invalidates cached results.
+ */
+ public function test_reordering_product_terms_invalidates_cached_results(): void {
+ $taxonomy = 'product_tag';
+ $suffix = (string) wp_rand( 1000, 9999 );
+ $names = array( 'First tag ' . $suffix, 'Second tag ' . $suffix );
+ $term_ids = array();
+ $product = null;
+
+ try {
+ foreach ( $names as $index => $name ) {
+ $term = wp_insert_term( $name, $taxonomy );
+ $this->assertIsArray( $term, "The {$name} term should be created." );
+
+ $term_ids[] = $term['term_id'];
+ update_term_meta( $term['term_id'], 'order', $index + 1 );
+ }
+
+ $product = WC_Helper_Product::create_simple_product();
+ wp_set_object_terms( $product->get_id(), $term_ids, $taxonomy );
+
+ $args = array(
+ 'fields' => 'all',
+ 'menu_order' => 'ASC',
+ );
+ $cached_terms = wc_get_product_terms( $product->get_id(), $taxonomy, $args );
+ $this->assertSame(
+ $names,
+ wp_list_pluck( $cached_terms, 'name' ),
+ 'Product terms should initially use the configured order.'
+ );
+
+ $term_to_move = get_term( $term_ids[1], $taxonomy );
+ $this->assertInstanceOf( WP_Term::class, $term_to_move );
+
+ wc_reorder_terms( $term_to_move, $term_ids[0], $taxonomy );
+
+ $reordered_terms = wc_get_product_terms( $product->get_id(), $taxonomy, $args );
+ $this->assertSame(
+ array_reverse( $names ),
+ wp_list_pluck( $reordered_terms, 'name' ),
+ 'Product terms should use the new order without saving the product.'
+ );
+ } finally {
+ if ( $product instanceof WC_Product ) {
+ $product->delete( true );
+ }
+
+ foreach ( $term_ids as $term_id ) {
+ wp_delete_term( $term_id, $taxonomy );
+ }
+ }
+ }
+}