Commit 4147321daf8 for woocommerce
commit 4147321daf816fed021fdd598a4a8915213b2b3b
Author: Daniel Mallory <daniel.mallory@automattic.com>
Date: Wed Jul 22 11:23:51 2026 +0100
Avoid repeated payment provider checks in admin requests (#66321)
* perf(payments): memoize payment provider details within a request
The Payments settings service re-derived the full payment provider
details on every call: gateway enumeration, per-gateway account and
state checks, extension suggestion matching, shell-gateway removal
and grouping. The onboarding Payments task alone triggers this chain
from most of its helpers, so an incomplete setup task list made every
wp-admin dashboard load run the derivation many times over. With
gateways like WooPayments this added seconds to the dashboard TTFB.
Memoize the derived details for the duration of the request:
- PaymentsProviders::get_payment_gateway_details() now memoizes per
gateway and country, patching the call-specific order on hits.
- Payments::get_payment_providers() now memoizes per location and
flags.
Mutators that influence the list (provider ordering, attaching or
hiding suggestions, dismissing incentives) reset the memo so
mid-request changes remain visible.
Refs #66317
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Improve payment provider request memoization
* perf: centralize payment provider request caching
* docs: describe shared payment provider request caching
* refactor: align payment provider cache terminology
* Add missing cache prefix return annotation
* refactor: simplify payment provider request caching
* Harden payment provider request cache behaviour
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com>
Co-authored-by: Vlad Olaru <vlad.olaru@automattic.com>
diff --git a/plugins/woocommerce/changelog/66317-perf-memoize-payments-providers b/plugins/woocommerce/changelog/66317-perf-memoize-payments-providers
new file mode 100644
index 00000000000..0f028510735
--- /dev/null
+++ b/plugins/woocommerce/changelog/66317-perf-memoize-payments-providers
@@ -0,0 +1,4 @@
+Significance: patch
+Type: performance
+
+Cache payment provider lists and gateway details in a shared request cache so the dashboard setup widget, onboarding task list, and Payments settings do not repeat expensive provider checks.
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings/Payments.php b/plugins/woocommerce/src/Internal/Admin/Settings/Payments.php
index 2087a3afb1b..a702a04c9f7 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings/Payments.php
+++ b/plugins/woocommerce/src/Internal/Admin/Settings/Payments.php
@@ -29,6 +29,9 @@ class Payments {
const FROM_ADDITIONAL_PAYMENTS_TASK = 'WCADMIN_ADDITIONAL_PAYMENT_TASK';
const FROM_PROVIDER_ONBOARDING = 'PROVIDER_ONBOARDING';
+ private const PROVIDERS_REQUEST_CACHE_GROUP = 'woocommerce_payments_providers';
+ private const PROVIDERS_REQUEST_CACHE_KEY = 'provider_lists';
+
/**
* The payment providers service.
*
@@ -54,6 +57,8 @@ class Payments {
final public function init( PaymentsProviders $payment_providers, ExtensionSuggestions $payment_extension_suggestions ): void {
$this->providers = $payment_providers;
$this->extension_suggestions = $payment_extension_suggestions;
+
+ wp_cache_add_non_persistent_groups( array( self::PROVIDERS_REQUEST_CACHE_GROUP ) );
}
/**
@@ -76,6 +81,13 @@ class Payments {
* @throws Exception If there are malformed or invalid suggestions.
*/
public function get_payment_providers( string $location, bool $for_display = true, bool $remove_shells = false ): array {
+ $can_install_plugins = current_user_can( 'install_plugins' );
+ $cache_key = get_current_user_id() . '__' . ( $can_install_plugins ? '1' : '0' ) . '__' . strtoupper( $location ) . '__' . ( $for_display ? '1' : '0' ) . ( $remove_shells ? '1' : '0' );
+ $cached_provider_lists = wp_cache_get( self::PROVIDERS_REQUEST_CACHE_KEY, self::PROVIDERS_REQUEST_CACHE_GROUP );
+ if ( is_array( $cached_provider_lists ) && isset( $cached_provider_lists[ $cache_key ] ) && is_array( $cached_provider_lists[ $cache_key ] ) ) {
+ return $cached_provider_lists[ $cache_key ];
+ }
+
$payment_gateways = $this->providers->get_payment_gateways( $for_display );
if ( ! $for_display && $remove_shells ) {
$payment_gateways = $this->providers->remove_shell_payment_gateways( $payment_gateways, $location );
@@ -87,7 +99,7 @@ class Payments {
// Only include suggestions if the requesting user can install plugins.
$suggestions = array();
- if ( current_user_can( 'install_plugins' ) ) {
+ if ( $can_install_plugins ) {
$suggestions = $this->providers->get_extension_suggestions( $location, self::SUGGESTIONS_CONTEXT );
}
// If we have preferred suggestions, add them to the providers list.
@@ -204,6 +216,12 @@ class Payments {
$this->process_payment_provider_states( $payment_providers );
}
+ if ( ! is_array( $cached_provider_lists ) ) {
+ $cached_provider_lists = array();
+ }
+ $cached_provider_lists[ $cache_key ] = $payment_providers;
+ wp_cache_set( self::PROVIDERS_REQUEST_CACHE_KEY, $cached_provider_lists, self::PROVIDERS_REQUEST_CACHE_GROUP );
+
return $payment_providers;
}
@@ -287,6 +305,9 @@ class Payments {
$result = $this->providers->update_payment_providers_order_map( $order_map );
if ( $result ) {
+ // The order map influences the providers list, so clear the cached data.
+ $this->clear_cache();
+
// Record an event that the payment providers order map was updated.
$this->record_event(
'payment_providers_order_map_updated',
@@ -313,6 +334,9 @@ class Payments {
$result = $this->providers->attach_extension_suggestion( $id );
if ( $result ) {
+ // The attachment influences the providers list, so clear the cached data.
+ $this->clear_cache();
+
// Record an event that the suggestion was attached.
$this->record_event(
'extension_suggestion_attached',
@@ -337,6 +361,9 @@ class Payments {
$result = $this->providers->hide_extension_suggestion( $id );
if ( $result ) {
+ // Hidden suggestions are excluded from the providers list, so clear the cached data.
+ $this->clear_cache();
+
// Record an event that the suggestion was hidden.
$this->record_event(
'extension_suggestion_hidden',
@@ -365,6 +392,11 @@ class Payments {
public function dismiss_extension_suggestion_incentive( string $suggestion_id, string $incentive_id, string $context = 'all', bool $do_not_track = false ): bool {
$result = $this->extension_suggestions->dismiss_incentive( $incentive_id, $suggestion_id, $context );
+ if ( $result ) {
+ // Incentives are embedded in the providers list details, so clear the cached data.
+ $this->clear_cache();
+ }
+
if ( ! $do_not_track && $result ) {
// Record an event that the incentive was dismissed.
$this->record_event(
@@ -380,6 +412,22 @@ class Payments {
return $result;
}
+ /**
+ * Clear cached payment provider data.
+ *
+ * Call after changing provider ordering, suggestions, incentives, gateway registration,
+ * settings, or account state during a request. Also useful for testing purposes.
+ *
+ * @since 11.1.0
+ *
+ * @internal
+ * @return void
+ */
+ public function clear_cache(): void {
+ wp_cache_delete( self::PROVIDERS_REQUEST_CACHE_KEY, self::PROVIDERS_REQUEST_CACHE_GROUP );
+ $this->providers->clear_cache();
+ }
+
/**
* Send a Tracks event.
*
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders.php b/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders.php
index 9e9125bf4c8..529e1156b14 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders.php
+++ b/plugins/woocommerce/src/Internal/Admin/Settings/PaymentsProviders.php
@@ -81,6 +81,9 @@ class PaymentsProviders {
public const CATEGORY_CRYPTO = 'crypto';
public const CATEGORY_PSP = 'psp';
+ private const GATEWAY_DETAILS_REQUEST_CACHE_GROUP = 'woocommerce_payment_gateway_details';
+ private const GATEWAY_DETAILS_REQUEST_CACHE_KEY = 'gateway_details';
+
/*
* The provider link types.
*
@@ -184,20 +187,20 @@ class PaymentsProviders {
private array $instances = array();
/**
- * The memoized payment gateways to avoid computing the list multiple times during a request.
+ * The cached payment gateways, used to avoid computing the list multiple times during a request.
*
* @var array
*/
- private array $payment_gateways_memo = array();
+ private array $payment_gateways_cache = array();
/**
- * The memoized payment gateways for display to avoid computing the list multiple times during a request.
+ * The cached payment gateways for display, used to avoid computing the list multiple times during a request.
*
* This is especially important since it avoids triggering the legacy action multiple times during a request.
*
* @var array
*/
- private array $payment_gateways_for_display_memo = array();
+ private array $payment_gateways_for_display_cache = array();
/**
* The payment extension suggestions service.
@@ -224,6 +227,8 @@ class PaymentsProviders {
final public function init( ExtensionSuggestions $payment_extension_suggestions, LegacyProxy $proxy ): void {
$this->extension_suggestions = $payment_extension_suggestions;
$this->proxy = $proxy;
+
+ wp_cache_add_non_persistent_groups( array( self::GATEWAY_DETAILS_REQUEST_CACHE_GROUP ) );
}
/**
@@ -247,8 +252,8 @@ class PaymentsProviders {
// If we are asked for a display gateways list, we need to fire legacy actions and filter out "shells".
if ( $for_display ) {
- if ( isset( $this->payment_gateways_for_display_memo[ $country_code ] ) ) {
- return $this->payment_gateways_for_display_memo[ $country_code ];
+ if ( isset( $this->payment_gateways_for_display_cache[ $country_code ] ) ) {
+ return $this->payment_gateways_for_display_cache[ $country_code ];
}
// We don't want to output anything from the action. So we buffer it and discard it.
@@ -273,14 +278,14 @@ class PaymentsProviders {
$payment_gateways = $this->remove_shell_payment_gateways( $payment_gateways, $country_code );
// Store the entire payment gateways list for display for later use.
- $this->payment_gateways_for_display_memo[ $country_code ] = $payment_gateways;
+ $this->payment_gateways_for_display_cache[ $country_code ] = $payment_gateways;
return $payment_gateways;
}
// We were asked for the raw payment gateways list.
- if ( isset( $this->payment_gateways_memo[ $country_code ] ) ) {
- return $this->payment_gateways_memo[ $country_code ];
+ if ( isset( $this->payment_gateways_cache[ $country_code ] ) ) {
+ return $this->payment_gateways_cache[ $country_code ];
}
// Get all payment gateways, ordered by the user.
@@ -290,7 +295,7 @@ class PaymentsProviders {
$payment_gateways = $this->handle_non_standard_registration_for_payment_gateways( $payment_gateways );
// Store the entire payment gateways list for later use.
- $this->payment_gateways_memo[ $country_code ] = $payment_gateways;
+ $this->payment_gateways_cache[ $country_code ] = $payment_gateways;
return $payment_gateways;
}
@@ -479,11 +484,27 @@ class PaymentsProviders {
// Normalize the country code to uppercase.
$country_code = strtoupper( $country_code );
- return $this->enhance_payment_gateway_details(
- $this->get_payment_gateway_base_details( $payment_gateway, $payment_gateway_order, $country_code ),
- $payment_gateway,
- $country_code
- );
+ $cache_key = get_current_user_id() . '__' . $payment_gateway->id . '__' . $country_code;
+ $cached_gateway_details = wp_cache_get( self::GATEWAY_DETAILS_REQUEST_CACHE_KEY, self::GATEWAY_DETAILS_REQUEST_CACHE_GROUP );
+ if ( is_array( $cached_gateway_details ) && isset( $cached_gateway_details[ $cache_key ] ) && is_array( $cached_gateway_details[ $cache_key ] ) ) {
+ $details = $cached_gateway_details[ $cache_key ];
+ } else {
+ $details = $this->enhance_payment_gateway_details(
+ $this->get_payment_gateway_base_details( $payment_gateway, 0, $country_code ),
+ $payment_gateway,
+ $country_code
+ );
+
+ if ( ! is_array( $cached_gateway_details ) ) {
+ $cached_gateway_details = array();
+ }
+ $cached_gateway_details[ $cache_key ] = $details;
+ wp_cache_set( self::GATEWAY_DETAILS_REQUEST_CACHE_KEY, $cached_gateway_details, self::GATEWAY_DETAILS_REQUEST_CACHE_GROUP );
+ }
+
+ $details['_order'] = $payment_gateway_order;
+
+ return $details;
}
/**
@@ -1225,14 +1246,34 @@ class PaymentsProviders {
}
/**
- * Reset the memoized data. Useful for testing purposes.
+ * Clear cached payment gateway data.
+ *
+ * Call after changing gateway registration, settings, or account state during a request.
+ * Also useful for testing purposes.
+ *
+ * @since 11.1.0
+ *
+ * @internal
+ * @return void
+ */
+ public function clear_cache(): void {
+ $this->payment_gateways_cache = array();
+ $this->payment_gateways_for_display_cache = array();
+ wp_cache_delete( self::GATEWAY_DETAILS_REQUEST_CACHE_KEY, self::GATEWAY_DETAILS_REQUEST_CACHE_GROUP );
+ }
+
+ /**
+ * Reset cached payment gateway data.
+ *
+ * @deprecated 11.1.0 Use clear_cache() instead.
*
* @internal
* @return void
*/
public function reset_memo(): void {
- $this->payment_gateways_memo = array();
- $this->payment_gateways_for_display_memo = array();
+ wc_deprecated_function( __METHOD__, '11.1.0', 'clear_cache' );
+
+ $this->clear_cache();
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPayments/WooPaymentsRestControllerIntegrationTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPayments/WooPaymentsRestControllerIntegrationTest.php
index 2a805cd66c7..cf09468a6fd 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPayments/WooPaymentsRestControllerIntegrationTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProviders/WooPayments/WooPaymentsRestControllerIntegrationTest.php
@@ -1573,7 +1573,7 @@ class WooPaymentsRestControllerIntegrationTest extends WC_Unit_Test_Case {
WC()->payment_gateways()->payment_gateways = array();
WC()->payment_gateways()->init();
- $this->providers_service->reset_memo();
+ $this->providers_service->clear_cache();
}
/**
@@ -1588,7 +1588,7 @@ class WooPaymentsRestControllerIntegrationTest extends WC_Unit_Test_Case {
WC()->payment_gateways()->init();
if ( isset( $this->providers_service ) ) {
- $this->providers_service->reset_memo();
+ $this->providers_service->clear_cache();
}
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProvidersTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProvidersTest.php
index a40fb6a1ac3..920731b7904 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProvidersTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProvidersTest.php
@@ -64,19 +64,22 @@ class PaymentsProvidersTest extends WC_Unit_Test_Case {
->getMock();
$this->sut = new PaymentsProviders();
- $this->sut->init( $this->mock_extension_suggestions, wc_get_container()->get( LegacyProxy::class ) );
+ $this->sut->init(
+ $this->mock_extension_suggestions,
+ wc_get_container()->get( LegacyProxy::class )
+ );
}
/**
* Tear down test.
*/
public function tearDown(): void {
- // Reset gateways/hooks and controller memo between tests.
+ // Reset gateways, hooks, and cached provider data between tests.
remove_all_actions( 'wc_payment_gateways_initialized' );
WC()->payment_gateways()->payment_gateways = array();
WC()->payment_gateways()->init();
if ( isset( $this->sut ) ) {
- $this->sut->reset_memo();
+ $this->sut->clear_cache();
}
// Restore the previous currency to prevent test leakage.
@@ -964,6 +967,156 @@ class PaymentsProvidersTest extends WC_Unit_Test_Case {
$this->assertSame( 'Special offer', $gateway_details['_incentive']['description'], 'Incentive description should match' );
}
+ /**
+ * @testdox Gateway details are derived once with a neutral order and receive the requested order on every call.
+ */
+ public function test_get_payment_gateway_details_is_cached(): void {
+ $fake_gateway = new FakePaymentGateway(
+ 'fake-gateway-id',
+ array(
+ 'plugin_slug' => 'fake-plugin-slug',
+ 'plugin_file' => 'fake-plugin-slug/fake-plugin-file',
+ ),
+ );
+
+ $provider = $this->createMock( PaymentGateway::class );
+ $provider
+ ->expects( $this->once() )
+ ->method( 'get_details' )
+ ->with( $fake_gateway, 0, 'US' )
+ ->willReturn(
+ array(
+ 'id' => 'fake-gateway-id',
+ '_order' => 0,
+ 'title' => 'Derived details',
+ 'plugin' => array(
+ 'slug' => 'fake-plugin-slug',
+ ),
+ )
+ );
+ $this->set_payment_gateway_provider_instance( 'fake-gateway-id', $provider );
+
+ $this->mock_extension_suggestions
+ ->expects( $this->once() )
+ ->method( 'get_by_plugin_slug' )
+ ->willReturn( null );
+
+ $first = $this->sut->get_payment_gateway_details( $fake_gateway, 1, 'US' );
+ $second = $this->sut->get_payment_gateway_details( $fake_gateway, 5, 'US' );
+
+ $this->assertSame( 1, $first['_order'], 'The first call should use the requested order' );
+ $this->assertSame( 5, $second['_order'], 'Cached details should use the latest requested order' );
+ unset( $first['_order'], $second['_order'] );
+ $this->assertSame( $first, $second, 'Cached details should match the originally derived details' );
+ }
+
+ /**
+ * @testdox Gateway details are cached separately for each user.
+ */
+ public function test_get_payment_gateway_details_cache_per_user(): void {
+ $fake_gateway = new FakePaymentGateway(
+ 'fake-gateway-id',
+ array(
+ 'plugin_slug' => 'fake-plugin-slug',
+ 'plugin_file' => 'fake-plugin-slug/fake-plugin-file',
+ ),
+ );
+
+ $provider = $this->createMock( PaymentGateway::class );
+ $provider
+ ->expects( $this->exactly( 2 ) )
+ ->method( 'get_details' )
+ ->willReturnCallback(
+ function ( $gateway, $order ) {
+ return array(
+ 'id' => $gateway->id,
+ '_order' => $order,
+ 'title' => (string) get_current_user_id(),
+ 'plugin' => array(
+ 'slug' => 'fake-plugin-slug',
+ ),
+ );
+ }
+ );
+ $this->set_payment_gateway_provider_instance( 'fake-gateway-id', $provider );
+
+ $this->mock_extension_suggestions
+ ->expects( $this->exactly( 2 ) )
+ ->method( 'get_by_plugin_slug' )
+ ->willReturn( null );
+
+ $first_user_details = $this->sut->get_payment_gateway_details( $fake_gateway, 1, 'US' );
+ $first_user_cached_details = $this->sut->get_payment_gateway_details( $fake_gateway, 2, 'US' );
+
+ $second_user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
+ wp_set_current_user( $second_user_id );
+ $second_user_details = $this->sut->get_payment_gateway_details( $fake_gateway, 3, 'US' );
+ $second_user_cached_details = $this->sut->get_payment_gateway_details( $fake_gateway, 4, 'US' );
+
+ $this->assertSame( (string) $this->store_admin_id, $first_user_details['title'] );
+ $this->assertSame( (string) $this->store_admin_id, $first_user_cached_details['title'] );
+ $this->assertSame( (string) $second_user_id, $second_user_details['title'] );
+ $this->assertSame( (string) $second_user_id, $second_user_cached_details['title'] );
+ $this->assertSame( 2, $first_user_cached_details['_order'] );
+ $this->assertSame( 4, $second_user_cached_details['_order'] );
+ }
+
+ /**
+ * @testdox Gateway details are cached per country and recomputed after the cache is cleared.
+ */
+ public function test_get_payment_gateway_details_cache_per_country_and_clear(): void {
+ $fake_gateway = new FakePaymentGateway(
+ 'fake-gateway-id',
+ array(
+ 'plugin_slug' => 'fake-plugin-slug',
+ 'plugin_file' => 'fake-plugin-slug/fake-plugin-file',
+ ),
+ );
+
+ $generation = 0;
+ $provider = $this->createMock( PaymentGateway::class );
+ $provider
+ ->expects( $this->exactly( 4 ) )
+ ->method( 'get_details' )
+ ->willReturnCallback(
+ function ( $gateway, $order, $country_code ) use ( &$generation ) {
+ $this->assertSame( 0, $order, 'Gateway details should always be derived with a neutral order' );
+ ++$generation;
+ return array(
+ 'id' => $gateway->id,
+ '_order' => $order,
+ 'title' => $country_code . '-' . $generation,
+ 'plugin' => array(
+ 'slug' => 'fake-plugin-slug',
+ ),
+ );
+ }
+ );
+ $this->set_payment_gateway_provider_instance( 'fake-gateway-id', $provider );
+
+ $this->mock_extension_suggestions
+ ->expects( $this->exactly( 4 ) )
+ ->method( 'get_by_plugin_slug' )
+ ->willReturn( null );
+
+ $first_us = $this->sut->get_payment_gateway_details( $fake_gateway, 1, 'US' );
+ $first_de = $this->sut->get_payment_gateway_details( $fake_gateway, 2, 'DE' );
+ $this->sut->clear_cache();
+ $second_us = $this->sut->get_payment_gateway_details( $fake_gateway, 3, 'US' );
+ $this->setExpectedDeprecated( PaymentsProviders::class . '::reset_memo' );
+ $this->sut->reset_memo();
+ $third_us = $this->sut->get_payment_gateway_details( $fake_gateway, 4, 'US' );
+
+ $this->assertSame( 'US-1', $first_us['title'] );
+ $this->assertSame( 'DE-2', $first_de['title'] );
+ $this->assertSame( 'US-3', $second_us['title'] );
+ $this->assertSame( 'US-4', $third_us['title'] );
+ $this->assertSame( 1, $first_us['_order'] );
+ $this->assertSame( 2, $first_de['_order'] );
+ $this->assertSame( 3, $second_us['_order'] );
+ $this->assertSame( 4, $third_us['_order'] );
+ }
+
/**
* Test that get_payment_gateway_details does not override gateway details with those from the suggestion
* when they exist.
@@ -2878,7 +3031,7 @@ class PaymentsProvidersTest extends WC_Unit_Test_Case {
WC()->payment_gateways()->init();
- $this->sut->reset_memo();
+ $this->sut->clear_cache();
}
/**
@@ -5951,6 +6104,21 @@ class PaymentsProvidersTest extends WC_Unit_Test_Case {
}
}
+ /**
+ * Set a payment gateway provider instance for testing.
+ *
+ * @param string $gateway_id The gateway ID.
+ * @param PaymentGateway $provider The provider instance.
+ * @param PaymentsProviders|null $service Optional service instance to update.
+ */
+ private function set_payment_gateway_provider_instance( string $gateway_id, PaymentGateway $provider, ?PaymentsProviders $service = null ): void {
+ $service = $service ?? $this->sut;
+ $reflection = new \ReflectionClass( $service );
+ $property = $reflection->getProperty( 'instances' );
+ $property->setAccessible( true );
+ $property->setValue( $service, array( $gateway_id => $provider ) );
+ }
+
/**
* Load the WC core PayPal gateway but not enable it.
*
@@ -5970,8 +6138,8 @@ class PaymentsProvidersTest extends WC_Unit_Test_Case {
WC()->payment_gateways()->payment_gateways = array();
WC()->payment_gateways()->init();
- // Reset the controller memo to pick up the new gateway details.
- $this->sut->reset_memo();
+ // Clear cached provider data to pick up the new gateway details.
+ $this->sut->clear_cache();
}
/**
@@ -5993,8 +6161,8 @@ class PaymentsProvidersTest extends WC_Unit_Test_Case {
WC()->payment_gateways()->payment_gateways = array();
WC()->payment_gateways()->init();
- // Reset the controller memo to pick up the new gateway details.
- $this->sut->reset_memo();
+ // Clear cached provider data to pick up the new gateway details.
+ $this->sut->clear_cache();
}
/**
@@ -6004,7 +6172,7 @@ class PaymentsProvidersTest extends WC_Unit_Test_Case {
delete_option( 'woocommerce_paypal_settings' );
delete_option( 'woocommerce_currency' );
- $this->sut->reset_memo();
+ $this->sut->clear_cache();
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerIntegrationTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerIntegrationTest.php
index 29cf960457e..e239c2aa75b 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerIntegrationTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerIntegrationTest.php
@@ -1510,6 +1510,8 @@ class PaymentsRestControllerIntegrationTest extends WC_Unit_Test_Case {
// Delete the user meta.
delete_user_meta( $this->store_admin_id, Payments::PAYMENTS_NOX_PROFILE_KEY );
+ // We changed the underlying data directly, so clear the cache to simulate a fresh request.
+ $this->service->clear_cache();
// Act.
$request = new WP_REST_Request( 'POST', self::ENDPOINT . '/providers' );
@@ -1568,6 +1570,8 @@ class PaymentsRestControllerIntegrationTest extends WC_Unit_Test_Case {
// Delete the user meta.
delete_user_meta( get_current_user_id(), Incentive::PREFIX . 'dismissed' );
+ // We changed the underlying data directly, so clear the cache to simulate a fresh request.
+ $this->service->clear_cache();
// Act.
$request = new WP_REST_Request( 'POST', self::ENDPOINT . '/providers' );
@@ -1932,8 +1936,8 @@ class PaymentsRestControllerIntegrationTest extends WC_Unit_Test_Case {
update_option( 'woocommerce_currency', 'USD' );
WC()->payment_gateways()->init();
- // Reset the controller memo to pick up the new gateway details.
- $this->providers_service->reset_memo();
+ // Clear cached provider data to pick up the new gateway details.
+ $this->providers_service->clear_cache();
}
/**
@@ -1952,8 +1956,8 @@ class PaymentsRestControllerIntegrationTest extends WC_Unit_Test_Case {
update_option( 'woocommerce_currency', 'USD' );
WC()->payment_gateways()->init();
- // Reset the service memo to pick up the new gateway details.
- $this->providers_service->reset_memo();
+ // Clear cached provider data to pick up the new gateway details.
+ $this->providers_service->clear_cache();
}
/**
@@ -1985,7 +1989,7 @@ class PaymentsRestControllerIntegrationTest extends WC_Unit_Test_Case {
WC()->payment_gateways()->payment_gateways = array();
WC()->payment_gateways()->init();
- $this->providers_service->reset_memo();
+ $this->providers_service->clear_cache();
$active_plugin_paths = array( 'woocommerce/woocommerce.php' );
$active_plugin_slugs = array( 'woocommerce' );
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsTest.php
index 4fdfe1bcc9c..859bde26b11 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsTest.php
@@ -59,8 +59,10 @@ class PaymentsTest extends WC_Unit_Test_Case {
->onlyMethods(
array(
'get_payment_gateways',
+ 'remove_shell_payment_gateways',
'get_extension_suggestions',
'get_extension_suggestion_categories',
+ 'attach_extension_suggestion',
'hide_extension_suggestion',
'get_order_map',
'save_order_map',
@@ -78,6 +80,16 @@ class PaymentsTest extends WC_Unit_Test_Case {
$this->sut = new Payments();
$this->sut->init( $this->mock_providers, $this->mock_extension_suggestions );
+ $this->sut->clear_cache();
+ }
+
+ /**
+ * Tear down test fixtures.
+ */
+ public function tearDown(): void {
+ $this->sut->clear_cache();
+
+ parent::tearDown();
}
/**
@@ -596,6 +608,240 @@ class PaymentsTest extends WC_Unit_Test_Case {
$this->assertTrue( $result );
}
+ /**
+ * @testdox Payment providers are cached during a request.
+ */
+ public function test_get_payment_providers_is_cached(): void {
+ $location = 'US';
+
+ $this->mock_providers
+ ->expects( $this->once() )
+ ->method( 'get_payment_gateways' )
+ ->willReturn( array() );
+
+ $this->mock_providers
+ ->expects( $this->once() )
+ ->method( 'get_extension_suggestions' )
+ ->willReturn( array() );
+
+ $first = $this->sut->get_payment_providers( $location );
+ $second = $this->sut->get_payment_providers( $location );
+
+ $this->assertSame( $first, $second, 'Repeated calls should return the cached providers' );
+ }
+
+ /**
+ * @testdox Payment providers are cached across service instances during a request.
+ */
+ public function test_get_payment_providers_is_cached_across_service_instances(): void {
+ $this->mock_providers
+ ->expects( $this->once() )
+ ->method( 'get_payment_gateways' )
+ ->willReturn( array() );
+
+ $this->mock_providers
+ ->expects( $this->once() )
+ ->method( 'get_extension_suggestions' )
+ ->willReturn( array() );
+
+ $other_service = new Payments();
+ $other_service->init( $this->mock_providers, $this->mock_extension_suggestions );
+
+ $first = $this->sut->get_payment_providers( 'US' );
+ $second = $other_service->get_payment_providers( 'US' );
+
+ $this->assertSame( $first, $second, 'Service instances should share the request-cached providers.' );
+ }
+
+ /**
+ * @testdox Payment providers are cached separately for each argument combination.
+ */
+ public function test_get_payment_providers_is_cached_per_arguments(): void {
+ $this->mock_providers
+ ->expects( $this->exactly( 4 ) )
+ ->method( 'get_payment_gateways' )
+ ->willReturn( array() );
+
+ $this->mock_providers
+ ->expects( $this->once() )
+ ->method( 'remove_shell_payment_gateways' )
+ ->willReturn( array() );
+
+ $this->mock_providers
+ ->expects( $this->exactly( 4 ) )
+ ->method( 'get_extension_suggestions' )
+ ->willReturnOnConsecutiveCalls(
+ self::get_preferred_suggestions( 'us-display' ),
+ self::get_preferred_suggestions( 'de-display' ),
+ self::get_preferred_suggestions( 'us-logic' ),
+ self::get_preferred_suggestions( 'us-logic-without-shells' )
+ );
+
+ $this->mock_providers
+ ->method( 'enhance_order_map' )
+ ->willReturnCallback( static fn( array $order_map ): array => $order_map );
+
+ $us_display = $this->sut->get_payment_providers( 'US' );
+ $de_display = $this->sut->get_payment_providers( 'DE' );
+ $us_logic = $this->sut->get_payment_providers( 'US', false );
+ $us_logic_without_shells = $this->sut->get_payment_providers( 'US', false, true );
+
+ $this->sut->get_payment_providers( 'US' );
+ $this->sut->get_payment_providers( 'DE' );
+ $this->sut->get_payment_providers( 'US', false );
+ $this->sut->get_payment_providers( 'US', false, true );
+
+ $this->assertSame( array( PaymentsProviders::SUGGESTION_ORDERING_PREFIX . 'us-display' ), array_column( $us_display, 'id' ) );
+ $this->assertSame( array( PaymentsProviders::SUGGESTION_ORDERING_PREFIX . 'de-display' ), array_column( $de_display, 'id' ) );
+ $this->assertSame( array( PaymentsProviders::SUGGESTION_ORDERING_PREFIX . 'us-logic' ), array_column( $us_logic, 'id' ) );
+ $this->assertSame( array( PaymentsProviders::SUGGESTION_ORDERING_PREFIX . 'us-logic-without-shells' ), array_column( $us_logic_without_shells, 'id' ) );
+ }
+
+ /**
+ * @testdox Payment providers are cached separately for each user.
+ */
+ public function test_get_payment_providers_is_cached_per_user(): void {
+ $this->mock_providers
+ ->expects( $this->exactly( 2 ) )
+ ->method( 'get_payment_gateways' )
+ ->willReturn( array() );
+
+ $this->mock_providers
+ ->expects( $this->exactly( 2 ) )
+ ->method( 'get_extension_suggestions' )
+ ->willReturnOnConsecutiveCalls(
+ self::get_preferred_suggestions( 'first-user' ),
+ self::get_preferred_suggestions( 'second-user' )
+ );
+
+ $this->mock_providers
+ ->method( 'enhance_order_map' )
+ ->willReturnCallback( static fn( array $order_map ): array => $order_map );
+
+ $first_user_result = $this->sut->get_payment_providers( 'US' );
+
+ $second_user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
+ wp_set_current_user( $second_user_id );
+ $second_user_result = $this->sut->get_payment_providers( 'US' );
+ $this->sut->get_payment_providers( 'US' );
+
+ $this->assertSame( array( PaymentsProviders::SUGGESTION_ORDERING_PREFIX . 'first-user' ), array_column( $first_user_result, 'id' ) );
+ $this->assertSame( array( PaymentsProviders::SUGGESTION_ORDERING_PREFIX . 'second-user' ), array_column( $second_user_result, 'id' ) );
+ }
+
+ /**
+ * @testdox Payment providers are cached separately for users who cannot install plugins.
+ */
+ public function test_get_payment_providers_is_cached_per_install_plugins_capability(): void {
+ $this->mock_providers
+ ->expects( $this->exactly( 2 ) )
+ ->method( 'get_payment_gateways' )
+ ->willReturn( array() );
+
+ $this->mock_providers
+ ->expects( $this->once() )
+ ->method( 'get_extension_suggestions' )
+ ->willReturn( self::get_preferred_suggestions( 'installable' ) );
+
+ $this->mock_providers
+ ->method( 'enhance_order_map' )
+ ->willReturnCallback( static fn( array $order_map ): array => $order_map );
+
+ $with_capability = $this->sut->get_payment_providers( 'US' );
+
+ $deny_install_plugins = static function ( array $allcaps ): array {
+ $allcaps['install_plugins'] = false;
+ return $allcaps;
+ };
+ add_filter( 'user_has_cap', $deny_install_plugins );
+ $without_capability = $this->sut->get_payment_providers( 'US' );
+ remove_filter( 'user_has_cap', $deny_install_plugins );
+
+ $this->assertSame( array( PaymentsProviders::SUGGESTION_ORDERING_PREFIX . 'installable' ), array_column( $with_capability, 'id' ) );
+ $this->assertSame( array(), $without_capability, 'Users without install_plugins should not receive extension suggestions' );
+ }
+
+ /**
+ * @testdox Clearing the cache recomputes the payment providers.
+ */
+ public function test_clear_cache_recomputes_payment_providers(): void {
+ $this->expect_payment_providers_to_be_recomputed();
+
+ $before = $this->sut->get_payment_providers( 'US' );
+ $this->sut->clear_cache();
+ $after = $this->sut->get_payment_providers( 'US' );
+
+ $this->assert_payment_providers_were_recomputed( $before, $after );
+ }
+
+ /**
+ * @testdox Updating the payment providers order map clears the cached providers.
+ */
+ public function test_update_payment_providers_order_map_clears_cache(): void {
+ $this->expect_payment_providers_to_be_recomputed();
+
+ $this->mock_providers
+ ->method( 'update_payment_providers_order_map' )
+ ->willReturn( true );
+
+ $before = $this->sut->get_payment_providers( 'US' );
+ $this->sut->update_payment_providers_order_map( array( 'gateway1' => 1 ) );
+ $after = $this->sut->get_payment_providers( 'US' );
+
+ $this->assert_payment_providers_were_recomputed( $before, $after );
+ }
+
+ /**
+ * @testdox Attaching a payment extension suggestion clears the cached providers.
+ */
+ public function test_attach_payment_extension_suggestion_clears_cache(): void {
+ $this->expect_payment_providers_to_be_recomputed();
+
+ $this->mock_providers
+ ->method( 'attach_extension_suggestion' )
+ ->willReturn( true );
+
+ $before = $this->sut->get_payment_providers( 'US' );
+ $this->sut->attach_payment_extension_suggestion( 'suggestion1' );
+ $after = $this->sut->get_payment_providers( 'US' );
+
+ $this->assert_payment_providers_were_recomputed( $before, $after );
+ }
+
+ /**
+ * @testdox Hiding a payment extension suggestion clears the cached providers.
+ */
+ public function test_hide_payment_extension_suggestion_clears_cache(): void {
+ $this->expect_payment_providers_to_be_recomputed();
+
+ $this->mock_providers
+ ->method( 'hide_extension_suggestion' )
+ ->willReturn( true );
+
+ $before = $this->sut->get_payment_providers( 'US' );
+ $this->sut->hide_payment_extension_suggestion( 'suggestion1' );
+ $after = $this->sut->get_payment_providers( 'US' );
+
+ $this->assert_payment_providers_were_recomputed( $before, $after );
+ }
+
+ /**
+ * @testdox Dismissing an extension suggestion incentive clears the cached providers.
+ */
+ public function test_dismiss_extension_suggestion_incentive_clears_cache(): void {
+ $this->expect_payment_providers_to_be_recomputed();
+
+ $this->mock_extension_suggestions
+ ->method( 'dismiss_incentive' )
+ ->willReturn( true );
+
+ $before = $this->sut->get_payment_providers( 'US' );
+ $this->sut->dismiss_extension_suggestion_incentive( 'suggestion1', 'incentive1' );
+ $after = $this->sut->get_payment_providers( 'US' );
+
+ $this->assert_payment_providers_were_recomputed( $before, $after );
+ }
+
/**
* Test that new gateways are placed above offline PMs when offline group is last.
*/
@@ -716,4 +962,61 @@ class PaymentsTest extends WC_Unit_Test_Case {
// Stripe should be the last non-offline-PM provider.
$this->assertSame( count( $provider_ids ) - 1, $stripe_index, 'stripe should be the last provider' );
}
+
+ /**
+ * Expect two provider derivations with distinct results.
+ */
+ private function expect_payment_providers_to_be_recomputed(): void {
+ $this->mock_providers
+ ->expects( $this->exactly( 2 ) )
+ ->method( 'get_payment_gateways' )
+ ->willReturn( array() );
+
+ $this->mock_providers
+ ->expects( $this->exactly( 2 ) )
+ ->method( 'get_extension_suggestions' )
+ ->willReturnOnConsecutiveCalls(
+ self::get_preferred_suggestions( 'before-reset' ),
+ self::get_preferred_suggestions( 'after-reset' )
+ );
+
+ $this->mock_providers
+ ->method( 'enhance_order_map' )
+ ->willReturnCallback( static fn( array $order_map ): array => $order_map );
+ }
+
+ /**
+ * Assert that a provider list was recomputed.
+ *
+ * @param array $before The providers before invalidation.
+ * @param array $after The providers after invalidation.
+ */
+ private function assert_payment_providers_were_recomputed( array $before, array $after ): void {
+ $this->assertSame( array( PaymentsProviders::SUGGESTION_ORDERING_PREFIX . 'before-reset' ), array_column( $before, 'id' ) );
+ $this->assertSame( array( PaymentsProviders::SUGGESTION_ORDERING_PREFIX . 'after-reset' ), array_column( $after, 'id' ) );
+ }
+
+ /**
+ * Create a preferred suggestions response with one suggestion.
+ *
+ * @param string $id The suggestion ID.
+ * @return array The suggestions response.
+ */
+ private static function get_preferred_suggestions( string $id ): array {
+ return array(
+ 'preferred' => array(
+ array(
+ 'id' => $id,
+ '_priority' => 0,
+ 'title' => $id,
+ 'description' => '',
+ 'plugin' => array(
+ '_type' => PaymentsExtensionSuggestions::PLUGIN_TYPE_WPORG,
+ 'slug' => $id,
+ ),
+ ),
+ ),
+ 'other' => array(),
+ );
+ }
}