Commit acb2050c862 for woocommerce
commit acb2050c862c301371cf46216f264f9c49f6ffcf
Author: Chris Huber <chubes@extrachill.com>
Date: Mon Jun 8 08:47:53 2026 -0400
Limit layered nav count cache growth (#65552)
* Limit layered nav count cache growth
* Address layered nav cache review cleanup
* Add changefile(s) from automation for the following project(s): woocommerce
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/65552-fix-layered-nav-count-cache-bound b/plugins/woocommerce/changelog/65552-fix-layered-nav-count-cache-bound
new file mode 100644
index 00000000000..af32d32eccf
--- /dev/null
+++ b/plugins/woocommerce/changelog/65552-fix-layered-nav-count-cache-bound
@@ -0,0 +1,4 @@
+Significance: patch
+Type: performance
+
+Limit layered navigation count cache growth for product attributes and brands.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/widgets/class-wc-widget-brand-nav.php b/plugins/woocommerce/includes/widgets/class-wc-widget-brand-nav.php
index 5ba848f1877..a42a3696ffd 100644
--- a/plugins/woocommerce/includes/widgets/class-wc-widget-brand-nav.php
+++ b/plugins/woocommerce/includes/widgets/class-wc-widget-brand-nav.php
@@ -1,6 +1,8 @@
<?php
-declare( strict_types = 1);
+declare( strict_types = 1 );
+
+use Automattic\WooCommerce\Internal\ProductAttributesLookup\Filterer;
/**
* Layered Navigation Widget for brands WC 2.6 version
@@ -542,6 +544,7 @@ class WC_Widget_Brand_Nav extends WC_Widget {
$counts = array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) );
$cached_counts[ $query_hash ] = $counts;
if ( true === $cache ) {
+ $cached_counts = Filterer::limit_layered_nav_count_cache_entries( $cached_counts, $query_hash );
set_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ), $cached_counts, HOUR_IN_SECONDS );
}
}
diff --git a/plugins/woocommerce/src/Internal/ProductAttributesLookup/Filterer.php b/plugins/woocommerce/src/Internal/ProductAttributesLookup/Filterer.php
index c5c404a2015..09e80bd6cab 100644
--- a/plugins/woocommerce/src/Internal/ProductAttributesLookup/Filterer.php
+++ b/plugins/woocommerce/src/Internal/ProductAttributesLookup/Filterer.php
@@ -211,12 +211,52 @@ class Filterer {
$counts = array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) );
$cached_counts[ $query_hash ] = $counts;
if ( true === $cache ) {
+ $cached_counts = self::limit_layered_nav_count_cache_entries( $cached_counts, $query_hash );
set_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ), $cached_counts, DAY_IN_SECONDS );
}
}
return array_map( 'absint', (array) $cached_counts[ $query_hash ] );
}
+ /**
+ * Limit the number of query result entries stored in a layered nav count transient.
+ *
+ * The layered nav count cache stores many query hashes inside a single transient per taxonomy.
+ * Without a cap, bot or faceted-search enumeration can grow that single option without bound.
+ *
+ * @since 10.9.0
+ *
+ * @param array $cached_counts Cached count entries keyed by query hash.
+ * @param string $current_hash Hash for the query that was just added or read.
+ * @return array Bounded cached count entries.
+ */
+ public static function limit_layered_nav_count_cache_entries( array $cached_counts, string $current_hash ): array {
+ /**
+ * Maximum number of query result entries to store in each layered nav count transient.
+ *
+ * Set to 0 to disable the cap.
+ *
+ * @hook woocommerce_layered_nav_count_cache_max_entries
+ * @since 10.9.0
+ *
+ * @param int $max_entries Maximum number of cached query result entries. Default 1000.
+ * @return int
+ */
+ $max_entries = (int) apply_filters( 'woocommerce_layered_nav_count_cache_max_entries', 1000 );
+
+ if ( $max_entries < 1 || count( $cached_counts ) <= $max_entries ) {
+ return $cached_counts;
+ }
+
+ if ( isset( $cached_counts[ $current_hash ] ) ) {
+ $current_counts = $cached_counts[ $current_hash ];
+ unset( $cached_counts[ $current_hash ] );
+ $cached_counts[ $current_hash ] = $current_counts;
+ }
+
+ return array_slice( $cached_counts, -$max_entries, null, true );
+ }
+
/**
* Get the query for counting products by terms using the product attributes lookup table.
*
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductAttributesLookup/FiltererTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductAttributesLookup/FiltererTest.php
index 8516a910082..e113eb87835 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductAttributesLookup/FiltererTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductAttributesLookup/FiltererTest.php
@@ -1,10 +1,13 @@
<?php
+declare( strict_types = 1 );
+
namespace Automattic\WooCommerce\Tests\Internal\ProductAttributesLookup;
use Automattic\WooCommerce\Enums\ProductTaxStatus;
use Automattic\WooCommerce\Enums\ProductType;
use Automattic\WooCommerce\Internal\AttributesHelper;
+use Automattic\WooCommerce\Internal\ProductAttributesLookup\Filterer;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Enums\ProductStockStatus;
@@ -55,6 +58,8 @@ class FiltererTest extends \WC_Unit_Test_Case {
public function tearDown(): void {
global $wpdb;
+ remove_all_filters( 'woocommerce_layered_nav_count_cache_max_entries' );
+
parent::tearDown();
// Unregister all product attributes.
@@ -97,6 +102,47 @@ class FiltererTest extends \WC_Unit_Test_Case {
\WC_Query::reset_chosen_attributes();
}
+ /**
+ * @testdox Layered nav count cache entries are capped per taxonomy transient.
+ */
+ public function test_layered_nav_count_cache_entries_are_capped() {
+ add_filter( 'woocommerce_layered_nav_count_cache_max_entries', fn() => 2 );
+
+ $cached_counts = array(
+ 'first' => array( 1 => 1 ),
+ 'second' => array( 2 => 2 ),
+ 'third' => array( 3 => 3 ),
+ );
+
+ $limited_counts = Filterer::limit_layered_nav_count_cache_entries( $cached_counts, 'third' );
+
+ $this->assertSame(
+ array(
+ 'second' => array( 2 => 2 ),
+ 'third' => array( 3 => 3 ),
+ ),
+ $limited_counts
+ );
+ }
+
+ /**
+ * @testdox The layered nav count cache cap can be disabled.
+ */
+ public function test_layered_nav_count_cache_cap_can_be_disabled() {
+ add_filter( 'woocommerce_layered_nav_count_cache_max_entries', '__return_zero' );
+
+ $cached_counts = array(
+ 'first' => array( 1 => 1 ),
+ 'second' => array( 2 => 2 ),
+ 'third' => array( 3 => 3 ),
+ );
+
+ $this->assertSame(
+ $cached_counts,
+ Filterer::limit_layered_nav_count_cache_entries( $cached_counts, 'third' )
+ );
+ }
+
/**
* Save a product and delete any lookup table data that may have been automatically inserted
* (for the purposes of unit testing we want to insert this data manually)