Commit 5e728d26ad3 for woocommerce
commit 5e728d26ad3c2bd1f91cc06705ba609a25384ac3
Author: Sam Najian <dev@najian.info>
Date: Thu Oct 15 18:36:53 2026 +0200
Revert "Memoize shipping package calculation within a request to avoid repeated woocommerce_shipping_packages runs" (#69867)
Revert "Memoize shipping package calculation within a request to avoid repeat…"
This reverts commit 92f42edc489be34a5586a98e3458faa8bc9f4278.
diff --git a/plugins/woocommerce/changelog/fix-wooplug-6486-shipping-packages-memoization b/plugins/woocommerce/changelog/fix-wooplug-6486-shipping-packages-memoization
deleted file mode 100644
index f3876506c63..00000000000
--- a/plugins/woocommerce/changelog/fix-wooplug-6486-shipping-packages-memoization
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: performance
-
-Allow extensions to opt into request-scoped memoization of the woocommerce_shipping_packages filter when every callback is safe to cache.
diff --git a/plugins/woocommerce/includes/class-wc-shipping.php b/plugins/woocommerce/includes/class-wc-shipping.php
index 3f87ed7b57c..1c9e335f90e 100644
--- a/plugins/woocommerce/includes/class-wc-shipping.php
+++ b/plugins/woocommerce/includes/class-wc-shipping.php
@@ -48,20 +48,6 @@ class WC_Shipping {
*/
public $packages = array();
- /**
- * Cache key for the last `woocommerce_shipping_packages` filter result.
- *
- * @var string|null
- */
- private $shipping_packages_filter_cache_key = null;
-
- /**
- * Last filtered shipping packages for cache-hit reuse.
- *
- * @var array
- */
- private $shipping_packages_filter_cache = array();
-
/**
* The single instance of the class
*
@@ -270,9 +256,7 @@ class WC_Shipping {
$this->packages = array();
if ( ! $this->enabled || empty( $packages ) ) {
- $this->shipping_packages_filter_cache_key = null;
- $this->shipping_packages_filter_cache = array();
- return $this->packages;
+ return array();
}
// Calculate costs for passed packages.
@@ -280,29 +264,6 @@ class WC_Shipping {
$this->packages[ $package_key ] = $this->calculate_shipping_for_package( $package, $package_key );
}
- /**
- * Filters whether the shipping packages filter result may be cached for the current request.
- *
- * Enable this only when every callback on `woocommerce_shipping_packages` depends solely on the
- * calculated packages. The filter continues to run on every call by default.
- *
- * @since 11.3.0
- *
- * @param bool $enabled Whether to cache the filtered packages. Default false.
- * @param array $packages The calculated packages before filtering.
- */
- $cache_enabled = 'yes' !== get_option( 'woocommerce_shipping_debug_mode', 'no' ) && (bool) apply_filters( 'woocommerce_shipping_packages_cache_enabled', false, $this->packages );
- $cache_key = null;
-
- if ( $cache_enabled ) {
- $cache_key = $this->get_shipping_packages_filter_cache_key( $this->packages );
- }
-
- if ( null !== $cache_key && $cache_key === $this->shipping_packages_filter_cache_key ) {
- $this->packages = $this->shipping_packages_filter_cache;
- return $this->packages;
- }
-
/**
* Allow packages to be reorganized after calculating the shipping.
*
@@ -316,96 +277,9 @@ class WC_Shipping {
*/
$this->packages = array_filter( (array) apply_filters( 'woocommerce_shipping_packages', $this->packages ) );
- $this->shipping_packages_filter_cache_key = $cache_key;
- $this->shipping_packages_filter_cache = $this->packages;
-
return $this->packages;
}
- /**
- * Get a cache key that includes mutable data objects visible to package filters.
- *
- * @param array $packages Calculated shipping packages.
- * @return string|null
- */
- private function get_shipping_packages_filter_cache_key( array $packages ): ?string {
- $cacheable = true;
- $normalized_packages = self::normalize_shipping_packages_filter_cache_value( $packages, $cacheable, new SplObjectStorage() );
-
- if ( ! $cacheable ) {
- return null;
- }
-
- $encoded_packages = wp_json_encode( $normalized_packages );
- return false === $encoded_packages ? null : md5( $encoded_packages );
- }
-
- /**
- * Normalize filter-visible values for the shipping packages cache key.
- *
- * @param mixed $value Value to normalize.
- * @param bool $cacheable Whether the value can be represented safely.
- * @param SplObjectStorage $seen_objects Objects in the current recursion path.
- * @phpstan-param SplObjectStorage<object, null> $seen_objects
- * @return mixed
- */
- private static function normalize_shipping_packages_filter_cache_value( $value, bool &$cacheable, SplObjectStorage $seen_objects ) {
- if ( is_array( $value ) ) {
- foreach ( $value as $key => $nested_value ) {
- $value[ $key ] = self::normalize_shipping_packages_filter_cache_value( $nested_value, $cacheable, $seen_objects );
-
- if ( ! $cacheable ) {
- return null;
- }
- }
-
- return $value;
- }
-
- if ( ! is_object( $value ) ) {
- return $value;
- }
-
- if ( $seen_objects->contains( $value ) ) {
- $cacheable = false;
- return null;
- }
-
- $seen_objects->attach( $value );
-
- if ( $value instanceof WC_Data ) {
- $normalized_value = array(
- 'class' => get_class( $value ),
- 'data' => array_replace( $value->get_data(), $value->get_changes() ),
- );
- } elseif ( $value instanceof WC_Meta_Data ) {
- $normalized_value = array(
- 'class' => get_class( $value ),
- 'data' => array_replace( $value->get_data(), $value->get_changes() ),
- );
- } elseif ( $value instanceof WC_DateTime ) {
- $normalized_value = array(
- 'class' => get_class( $value ),
- 'timestamp' => $value->getTimestamp(),
- 'timezone' => $value->getTimezone()->getName(),
- 'offset' => $value->getOffset(),
- );
- } elseif ( $value instanceof JsonSerializable ) {
- $normalized_value = array(
- 'class' => get_class( $value ),
- 'data' => $value->jsonSerialize(),
- );
- } else {
- $cacheable = false;
- return null;
- }
-
- $normalized_value = self::normalize_shipping_packages_filter_cache_value( $normalized_value, $cacheable, $seen_objects );
- $seen_objects->detach( $value );
-
- return $normalized_value;
- }
-
/**
* See if package is shippable.
*
@@ -592,9 +466,7 @@ class WC_Shipping {
*/
public function reset_shipping() {
unset( WC()->session->chosen_shipping_methods );
- $this->packages = array();
- $this->shipping_packages_filter_cache_key = null;
- $this->shipping_packages_filter_cache = array();
+ $this->packages = array();
}
/**
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-shipping-test.php b/plugins/woocommerce/tests/php/includes/class-wc-shipping-test.php
index 532a7079b00..0ef2b133c72 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-shipping-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-shipping-test.php
@@ -359,340 +359,6 @@ class WC_Shipping_Test extends WC_Unit_Test_Case {
);
}
- /**
- * @testdox The shipping packages filter runs on every call unless caching is explicitly enabled.
- */
- public function test_calculate_shipping_does_not_memoize_packages_filter_by_default(): void {
- update_option( 'woocommerce_shipping_debug_mode', 'no' );
-
- $packages = array( $this->get_package_hash_test_package() );
- $filter_calls = 0;
- $filter = function ( $shipping_packages ) use ( &$filter_calls ) {
- ++$filter_calls;
- return $shipping_packages;
- };
-
- add_filter( 'woocommerce_shipping_packages', $filter );
-
- $this->sut->calculate_shipping( $packages );
- $this->sut->calculate_shipping( $packages );
- $this->sut->calculate_shipping( $packages );
-
- $this->assertSame( 3, $filter_calls, 'The public filter should retain its existing timing by default.' );
- }
-
- /**
- * @testdox Package calculation exposes the packages completed so far through get_packages().
- */
- public function test_calculate_shipping_exposes_in_progress_packages(): void {
- $observed_packages = array();
- $packages = array(
- 'first' => array(
- 'package_id' => 'first',
- 'rates' => array(),
- ),
- 'second' => array(
- 'package_id' => 'second',
- 'rates' => array(),
- ),
- );
- $this->sut = $this->getMockBuilder( WC_Shipping::class )
- ->onlyMethods( array( 'calculate_shipping_for_package' ) )
- ->getMock();
- $this->sut->method( 'calculate_shipping_for_package' )
- ->willReturnCallback(
- function ( $package ) use ( &$observed_packages ) {
- $observed_packages[] = $this->sut->get_packages();
- return $package;
- }
- );
-
- $this->sut->calculate_shipping( $packages );
-
- $this->assertSame(
- array(
- array(),
- array( 'first' => $packages['first'] ),
- ),
- $observed_packages,
- 'Callbacks should see only packages completed before the current package.'
- );
- }
-
- /**
- * @testdox The shipping packages filter result is memoized when caching is explicitly enabled.
- */
- public function test_calculate_shipping_memoizes_packages_filter_when_enabled(): void {
- update_option( 'woocommerce_shipping_debug_mode', 'no' );
-
- $packages = array( $this->get_package_hash_test_package() );
- $filter_calls = 0;
- $filter = function ( $shipping_packages ) use ( &$filter_calls ) {
- ++$filter_calls;
- $shipping_packages['reorganized'] = true;
- return $shipping_packages;
- };
-
- add_filter( 'woocommerce_shipping_packages_cache_enabled', '__return_true' );
- add_filter( 'woocommerce_shipping_packages', $filter );
-
- $first = $this->sut->calculate_shipping( $packages );
- $second = $this->sut->calculate_shipping( $packages );
- $third = $this->sut->calculate_shipping( $packages );
-
- $this->assertSame( 1, $filter_calls, 'The filter should run once for repeated identical calls.' );
- $this->assertArrayHasKey( 'reorganized', $first, 'The first call should return the filtered packages.' );
- $this->assertSame( $first, $second, 'The memoized result should preserve the filter output.' );
- $this->assertSame( $first, $third, 'The memoized result should remain stable.' );
-
- $packages[0]['destination']['country'] = 'GB';
- $packages[0]['destination']['state'] = '';
- $this->sut->calculate_shipping( $packages );
-
- $this->assertSame( 2, $filter_calls, 'Material package changes should invalidate the memo.' );
-
- $this->sut->reset_shipping();
- $this->sut->calculate_shipping( $packages );
-
- $this->assertSame( 3, $filter_calls, 'Resetting shipping should invalidate the memo.' );
- }
-
- /**
- * @testdox An empty shipping packages filter result is memoized when caching is enabled.
- */
- public function test_calculate_shipping_memoizes_empty_packages_filter_result_when_enabled(): void {
- update_option( 'woocommerce_shipping_debug_mode', 'no' );
-
- $packages = array( $this->get_package_hash_test_package() );
- $filter_calls = 0;
- $filter = function () use ( &$filter_calls ) {
- ++$filter_calls;
- return array();
- };
-
- add_filter( 'woocommerce_shipping_packages_cache_enabled', '__return_true' );
- add_filter( 'woocommerce_shipping_packages', $filter );
-
- $first = $this->sut->calculate_shipping( $packages );
- $second = $this->sut->calculate_shipping( $packages );
-
- $this->assertSame( 1, $filter_calls, 'An empty filtered result should still be cached.' );
- $this->assertSame( array(), $first, 'The first call should return the empty filtered result.' );
- $this->assertSame( $first, $second, 'The empty result should be returned from the memo.' );
- }
-
- /**
- * @testdox Changing a package key invalidates the shipping packages filter memo.
- */
- public function test_calculate_shipping_invalidates_filter_memo_when_package_key_changes(): void {
- update_option( 'woocommerce_shipping_debug_mode', 'no' );
-
- $package = $this->get_package_hash_test_package();
- $filter_calls = 0;
- $filter = function ( $shipping_packages ) use ( &$filter_calls ) {
- ++$filter_calls;
- return $shipping_packages;
- };
-
- add_filter( 'woocommerce_shipping_packages_cache_enabled', '__return_true' );
- add_filter( 'woocommerce_shipping_packages', $filter );
-
- $this->sut->calculate_shipping( array( 0 => $package ) );
- $result = $this->sut->calculate_shipping( array( 'replacement' => $package ) );
-
- $this->assertSame( 2, $filter_calls, 'Changing the package key should invalidate the memo.' );
- $this->assertArrayHasKey( 'replacement', $result, 'The returned packages should use the current key.' );
- $this->assertArrayNotHasKey( 0, $result, 'The previous package key should not be returned.' );
- }
-
- /**
- * @testdox Changing non-rate package metadata invalidates the shipping packages filter memo.
- */
- public function test_calculate_shipping_invalidates_filter_memo_when_non_rate_metadata_changes(): void {
- update_option( 'woocommerce_shipping_debug_mode', 'no' );
-
- $packages = array( $this->get_package_hash_test_package() );
- $filter_calls = 0;
- $filter = function ( $shipping_packages ) use ( &$filter_calls ) {
- ++$filter_calls;
- return $shipping_packages;
- };
-
- add_filter( 'woocommerce_shipping_packages_cache_enabled', '__return_true' );
- add_filter( 'woocommerce_shipping_packages', $filter );
-
- $this->sut->calculate_shipping( $packages );
- $packages[0]['package_name'] = 'Replacement package';
- $this->sut->calculate_shipping( $packages );
-
- $this->assertSame( 2, $filter_calls, 'Filter-visible metadata should invalidate the memo.' );
- }
-
- /**
- * @testdox Changing product data invalidates the shipping packages filter memo.
- */
- public function test_calculate_shipping_invalidates_filter_memo_when_product_data_changes(): void {
- update_option( 'woocommerce_shipping_debug_mode', 'no' );
-
- $packages = array( $this->get_package_hash_test_package() );
- $product = $packages[0]['contents']['test_item']['data'];
- $filter_calls = 0;
- $filter = function ( $shipping_packages ) use ( &$filter_calls ) {
- ++$filter_calls;
- return $shipping_packages;
- };
-
- $product->set_object_read( true );
- $product->set_weight( '1' );
- add_filter( 'woocommerce_shipping_packages_cache_enabled', '__return_true' );
- add_filter( 'woocommerce_shipping_packages', $filter );
-
- $this->sut->calculate_shipping( $packages );
- $product->set_weight( '2' );
- $this->sut->calculate_shipping( $packages );
-
- $this->assertSame( 2, $filter_calls, 'Mutable product data should invalidate the memo.' );
- }
-
- /**
- * @testdox Removing nested product data invalidates the shipping packages filter memo.
- */
- public function test_calculate_shipping_invalidates_filter_memo_when_nested_product_data_is_removed(): void {
- update_option( 'woocommerce_shipping_debug_mode', 'no' );
-
- $packages = array( $this->get_package_hash_test_package() );
- $product = $packages[0]['contents']['test_item']['data'];
- $filter_calls = 0;
- $filter = function ( $shipping_packages ) use ( &$filter_calls ) {
- ++$filter_calls;
- return $shipping_packages;
- };
-
- $product->set_object_read( true );
- $product->set_category_ids( array( 123 ) );
- $product->apply_changes();
- add_filter( 'woocommerce_shipping_packages_cache_enabled', '__return_true' );
- add_filter( 'woocommerce_shipping_packages', $filter );
-
- $this->sut->calculate_shipping( $packages );
- $product->set_category_ids( array() );
- $this->sut->calculate_shipping( $packages );
-
- $this->assertSame( 2, $filter_calls, 'Removing an array-valued product property should invalidate the memo.' );
- }
-
- /**
- * @testdox Product dates are included in the shipping packages filter memo.
- */
- public function test_calculate_shipping_memoizes_product_dates(): void {
- update_option( 'woocommerce_shipping_debug_mode', 'no' );
-
- $packages = array( $this->get_package_hash_test_package() );
- $product = $packages[0]['contents']['test_item']['data'];
- $date_created = new WC_DateTime( '2026-01-01 00:00:00', new DateTimeZone( 'UTC' ) );
- $filter_calls = 0;
- $filter = function ( $shipping_packages ) use ( &$filter_calls ) {
- ++$filter_calls;
- return $shipping_packages;
- };
-
- $product->set_object_read( true );
- $product->set_date_created( $date_created );
- $product->apply_changes();
- add_filter( 'woocommerce_shipping_packages_cache_enabled', '__return_true' );
- add_filter( 'woocommerce_shipping_packages', $filter );
-
- $this->sut->calculate_shipping( $packages );
- $this->sut->calculate_shipping( $packages );
-
- $this->assertSame( 1, $filter_calls, 'A product date should not disable memoization.' );
-
- $date_created->modify( '+1 day' );
- $this->sut->calculate_shipping( $packages );
-
- $this->assertSame( 2, $filter_calls, 'Changing a product date should invalidate the memo.' );
- }
-
- /**
- * @testdox Pending product metadata changes invalidate the shipping packages filter memo.
- */
- public function test_calculate_shipping_invalidates_filter_memo_when_product_metadata_changes(): void {
- update_option( 'woocommerce_shipping_debug_mode', 'no' );
-
- $packages = array( $this->get_package_hash_test_package() );
- $product = $packages[0]['contents']['test_item']['data'];
- $filter_calls = 0;
- $filter = function ( $shipping_packages ) use ( &$filter_calls ) {
- ++$filter_calls;
- $shipping_packages[0]['observed_metadata'] = $shipping_packages[0]['contents']['test_item']['data']->get_meta( 'shipping_cache_test' );
- return $shipping_packages;
- };
-
- $product->set_object_read( true );
- $product->add_meta_data( 'shipping_cache_test', 'before', true );
- $metadata = $product->get_meta_data()[0];
- add_filter( 'woocommerce_shipping_packages_cache_enabled', '__return_true' );
- add_filter( 'woocommerce_shipping_packages', $filter );
-
- $first = $this->sut->calculate_shipping( $packages );
- $metadata->value = 'after';
- $second = $this->sut->calculate_shipping( $packages );
-
- $this->assertSame( 2, $filter_calls, 'Pending metadata changes should invalidate the memo.' );
- $this->assertSame( 'before', $first[0]['observed_metadata'], 'The first result should use the original metadata.' );
- $this->assertSame( 'after', $second[0]['observed_metadata'], 'The second result should use the pending metadata.' );
- }
-
- /**
- * @testdox Unsupported objects anywhere in package metadata disable the shipping packages filter memo.
- */
- public function test_calculate_shipping_does_not_memoize_unsupported_package_metadata_objects(): void {
- update_option( 'woocommerce_shipping_debug_mode', 'no' );
-
- $metadata = new SplObjectStorage();
- $packages = array( $this->get_package_hash_test_package() );
- $packages[0]['extension_metadata'] = array( 'state' => $metadata );
- $filter_calls = 0;
- $filter = function ( $shipping_packages ) use ( &$filter_calls ) {
- ++$filter_calls;
- $shipping_packages[0]['metadata_count'] = count( $shipping_packages[0]['extension_metadata']['state'] );
- return $shipping_packages;
- };
-
- $metadata->attach( new stdClass() );
- add_filter( 'woocommerce_shipping_packages_cache_enabled', '__return_true' );
- add_filter( 'woocommerce_shipping_packages', $filter );
-
- $first = $this->sut->calculate_shipping( $packages );
- $metadata->attach( new stdClass() );
- $second = $this->sut->calculate_shipping( $packages );
-
- $this->assertSame( 2, $filter_calls, 'Unsupported nested objects should disable memoization.' );
- $this->assertSame( 1, $first[0]['metadata_count'], 'The first filter result should expose the initial object state.' );
- $this->assertSame( 2, $second[0]['metadata_count'], 'The second filter result should expose the changed object state.' );
- }
-
- /**
- * @testdox Shipping debug mode bypasses the shipping packages filter memo.
- */
- public function test_calculate_shipping_does_not_memoize_in_debug_mode(): void {
- $packages = array( $this->get_package_hash_test_package() );
- $filter_calls = 0;
- $filter = function ( $shipping_packages ) use ( &$filter_calls ) {
- ++$filter_calls;
- return $shipping_packages;
- };
-
- add_filter( 'woocommerce_shipping_packages_cache_enabled', '__return_true' );
- add_filter( 'woocommerce_shipping_packages', $filter );
-
- $this->sut->calculate_shipping( $packages );
- $this->sut->calculate_shipping( $packages );
- $this->sut->calculate_shipping( $packages );
-
- $this->assertSame( 3, $filter_calls, 'Debug mode should run the filter on every call.' );
- }
-
/**
* Data provider for ignored package hash fields.
*