Commit 16d7b866f1 for woocommerce
commit 16d7b866f1d202fce7cc3f65826b38dcb8553c54
Author: Néstor Soriano <konamiman@konamiman.com>
Date: Fri Jan 30 12:45:19 2026 +0100
Add caching for the taxes REST API endpoints (#62931)
diff --git a/plugins/woocommerce/changelog/pr-62931 b/plugins/woocommerce/changelog/pr-62931
new file mode 100644
index 0000000000..d57c5ca234
--- /dev/null
+++ b/plugins/woocommerce/changelog/pr-62931
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add caching for the taxes REST API endpoints
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index 8d61cbdfba..8efb7e9b3e 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -35,6 +35,7 @@ use Automattic\WooCommerce\Utilities\{LoggingUtil, TimeUtil};
use Automattic\WooCommerce\Internal\Logging\RemoteLogger;
use Automattic\WooCommerce\Caches\OrderCountCacheService;
use Automattic\WooCommerce\Internal\Caches\ProductVersionStringInvalidator;
+use Automattic\WooCommerce\Internal\Caches\TaxRateVersionStringInvalidator;
use Automattic\WooCommerce\Internal\StockNotifications\StockNotifications;
use Automattic\Jetpack\Constants;
@@ -360,6 +361,7 @@ final class WooCommerce {
$container->get( AbilitiesRegistry::class );
$container->get( MCPAdapterProvider::class );
$container->get( ProductVersionStringInvalidator::class );
+ $container->get( TaxRateVersionStringInvalidator::class );
// Feature flags.
if ( Constants::is_true( 'WOOCOMMERCE_BIS_ALPHA_ENABLED' ) ) {
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php
index 575f4a934b..22f23f9ed1 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php
@@ -8,6 +8,8 @@
* @since 3.0.0
*/
+use Automattic\WooCommerce\Internal\Traits\RestApiCache;
+
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@@ -20,6 +22,8 @@ if ( ! defined( 'ABSPATH' ) ) {
*/
class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
+ use RestApiCache;
+
/**
* Endpoint namespace.
*
@@ -34,6 +38,13 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
*/
protected $rest_base = 'taxes';
+ /**
+ * Constructor.
+ */
+ public function __construct() {
+ $this->initialize_rest_api_cache();
+ }
+
/**
* Register the routes for taxes.
*/
@@ -44,7 +55,13 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
array(
array(
'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_items' ),
+ 'callback' => $this->with_cache(
+ array( $this, 'get_items' ),
+ array(
+ 'endpoint_id' => 'get_tax_rates',
+ 'relevant_version_strings' => array( 'list_tax_rates' ),
+ )
+ ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
@@ -70,7 +87,7 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
),
array(
'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_item' ),
+ 'callback' => $this->with_cache( array( $this, 'get_item' ) ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
@@ -765,4 +782,38 @@ class WC_REST_Taxes_V1_Controller extends WC_REST_Controller {
return $params;
}
+
+ /**
+ * Get the default entity type for response caching.
+ *
+ * @return string|null The entity type.
+ */
+ protected function get_default_response_entity_type(): ?string {
+ return 'tax_rate';
+ }
+
+ /**
+ * Whether the response cache should vary by user.
+ *
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
+ * @param string|null $endpoint_id Optional endpoint identifier.
+ * @return bool False since tax data doesn't vary by user.
+ */
+ protected function response_cache_vary_by_user( WP_REST_Request $request, ?string $endpoint_id = null ): bool { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
+ return false;
+ }
+
+ /**
+ * Get the hooks relevant to response caching.
+ *
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
+ * @param string|null $endpoint_id Optional endpoint identifier.
+ * @return array Array of hook names to track for cache invalidation.
+ */
+ protected function get_hooks_relevant_to_caching( WP_REST_Request $request, ?string $endpoint_id = null ): array { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
+ return array(
+ 'woocommerce_rest_prepare_tax',
+ 'woocommerce_rest_tax_query',
+ );
+ }
}
diff --git a/plugins/woocommerce/src/Internal/Caches/TaxRateVersionStringInvalidator.php b/plugins/woocommerce/src/Internal/Caches/TaxRateVersionStringInvalidator.php
new file mode 100644
index 0000000000..bb4883e2c8
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/Caches/TaxRateVersionStringInvalidator.php
@@ -0,0 +1,129 @@
+<?php
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\Caches;
+
+/**
+ * Tax rate version string invalidation handler.
+ *
+ * This class provides an 'invalidate' method that will invalidate
+ * the version string for a given tax rate, which in turn invalidates
+ * any cached REST API responses containing that tax rate.
+ *
+ * @since 10.6.0
+ */
+class TaxRateVersionStringInvalidator {
+
+ /**
+ * Initialize the invalidator and register hooks.
+ *
+ * Hooks are only registered when both conditions are met:
+ * - The REST API caching feature is enabled
+ * - The backend caching setting is active
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ *
+ * @internal
+ */
+ final public function init(): void {
+ // We can't use FeaturesController::feature_is_enabled at this point
+ // (before the 'init' action is triggered) because that would cause
+ // "Translation loading for the woocommerce domain was triggered too early" warnings.
+ if ( 'yes' !== get_option( 'woocommerce_feature_rest_api_caching_enabled' ) ) {
+ return;
+ }
+
+ if ( 'yes' === get_option( 'woocommerce_rest_api_enable_backend_caching', 'no' ) ) {
+ $this->register_hooks();
+ }
+ }
+
+ /**
+ * Register all tax rate-related hooks.
+ *
+ * Registers hooks for tax rate CRUD operations fired by WC_Tax class.
+ *
+ * @return void
+ */
+ private function register_hooks(): void {
+ add_action( 'woocommerce_tax_rate_added', array( $this, 'handle_woocommerce_tax_rate_added' ), 10, 1 );
+ add_action( 'woocommerce_tax_rate_updated', array( $this, 'handle_woocommerce_tax_rate_updated' ), 10, 1 );
+ add_action( 'woocommerce_tax_rate_deleted', array( $this, 'handle_woocommerce_tax_rate_deleted' ), 10, 1 );
+ }
+
+ /**
+ * Handle the woocommerce_tax_rate_added hook.
+ *
+ * @param int $tax_rate_id The tax rate ID.
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ *
+ * @internal
+ */
+ public function handle_woocommerce_tax_rate_added( $tax_rate_id ): void {
+ $this->invalidate( (int) $tax_rate_id );
+ $this->invalidate_tax_rates_list();
+ }
+
+ /**
+ * Handle the woocommerce_tax_rate_updated hook.
+ *
+ * @param int $tax_rate_id The tax rate ID.
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ *
+ * @internal
+ */
+ public function handle_woocommerce_tax_rate_updated( $tax_rate_id ): void {
+ $this->invalidate( (int) $tax_rate_id );
+ $this->invalidate_tax_rates_list();
+ }
+
+ /**
+ * Handle the woocommerce_tax_rate_deleted hook.
+ *
+ * @param int $tax_rate_id The tax rate ID.
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ *
+ * @internal
+ */
+ public function handle_woocommerce_tax_rate_deleted( $tax_rate_id ): void {
+ $this->invalidate( (int) $tax_rate_id );
+ $this->invalidate_tax_rates_list();
+ }
+
+ /**
+ * Invalidate the tax rates list version string.
+ *
+ * Called when tax rates are added, updated, or deleted,
+ * as these operations affect collection/list endpoints.
+ *
+ * @return void
+ */
+ private function invalidate_tax_rates_list(): void {
+ wc_get_container()->get( VersionStringGenerator::class )->delete_version( 'list_tax_rates' );
+ }
+
+ /**
+ * Invalidate a tax rate version string.
+ *
+ * @param int $tax_rate_id The tax rate ID.
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ */
+ public function invalidate( int $tax_rate_id ): void {
+ wc_get_container()->get( VersionStringGenerator::class )->delete_version( "tax_rate_{$tax_rate_id}" );
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Caches/TaxRateVersionStringInvalidatorTest.php b/plugins/woocommerce/tests/php/src/Internal/Caches/TaxRateVersionStringInvalidatorTest.php
new file mode 100644
index 0000000000..a1d0caf632
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/Caches/TaxRateVersionStringInvalidatorTest.php
@@ -0,0 +1,283 @@
+<?php
+/**
+ * TaxRateVersionStringInvalidatorTest class file.
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\Caches;
+
+use Automattic\WooCommerce\Internal\Caches\TaxRateVersionStringInvalidator;
+use Automattic\WooCommerce\Internal\Caches\VersionStringGenerator;
+
+/**
+ * Tests for the TaxRateVersionStringInvalidator class.
+ */
+class TaxRateVersionStringInvalidatorTest extends \WC_Unit_Test_Case {
+
+ /**
+ * The System Under Test.
+ *
+ * @var TaxRateVersionStringInvalidator
+ */
+ private $sut;
+
+ /**
+ * Version string generator.
+ *
+ * @var VersionStringGenerator
+ */
+ private $version_generator;
+
+ /**
+ * Setup test.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->sut = new TaxRateVersionStringInvalidator();
+ $this->version_generator = wc_get_container()->get( VersionStringGenerator::class );
+ }
+
+ /**
+ * Tear down test.
+ */
+ public function tearDown(): void {
+ delete_option( 'woocommerce_feature_rest_api_caching_enabled' );
+ delete_option( 'woocommerce_rest_api_enable_backend_caching' );
+ parent::tearDown();
+ }
+
+ /**
+ * Enable the feature and backend caching, and initialize a new invalidator with hooks registered.
+ *
+ * @return TaxRateVersionStringInvalidator The initialized invalidator.
+ */
+ private function get_invalidator_with_hooks_enabled(): TaxRateVersionStringInvalidator {
+ update_option( 'woocommerce_feature_rest_api_caching_enabled', 'yes' );
+ update_option( 'woocommerce_rest_api_enable_backend_caching', 'yes' );
+
+ $invalidator = new TaxRateVersionStringInvalidator();
+ $invalidator->init();
+
+ return $invalidator;
+ }
+
+ /**
+ * @testdox Invalidate method deletes the tax rate version string from cache.
+ */
+ public function test_invalidate_deletes_version_string() {
+ $tax_rate_id = 123;
+
+ $this->version_generator->generate_version( "tax_rate_{$tax_rate_id}" );
+
+ $version_before = $this->version_generator->get_version( "tax_rate_{$tax_rate_id}", false );
+ $this->assertNotNull( $version_before, 'Version string should exist before invalidation' );
+
+ $this->sut->invalidate( $tax_rate_id );
+
+ $version_after = $this->version_generator->get_version( "tax_rate_{$tax_rate_id}", false );
+ $this->assertNull( $version_after, 'Version string should be deleted after invalidation' );
+ }
+
+ /**
+ * @testdox Hooks are registered when feature is enabled and backend caching is active.
+ */
+ public function test_hooks_registered_when_feature_and_setting_enabled() {
+ $invalidator = $this->get_invalidator_with_hooks_enabled();
+
+ $this->assertNotFalse( has_action( 'woocommerce_tax_rate_added', array( $invalidator, 'handle_woocommerce_tax_rate_added' ) ) );
+ $this->assertNotFalse( has_action( 'woocommerce_tax_rate_updated', array( $invalidator, 'handle_woocommerce_tax_rate_updated' ) ) );
+ $this->assertNotFalse( has_action( 'woocommerce_tax_rate_deleted', array( $invalidator, 'handle_woocommerce_tax_rate_deleted' ) ) );
+ }
+
+ /**
+ * @testdox Hooks are not registered when feature is disabled.
+ */
+ public function test_hooks_not_registered_when_feature_disabled() {
+ update_option( 'woocommerce_feature_rest_api_caching_enabled', 'no' );
+ update_option( 'woocommerce_rest_api_enable_backend_caching', 'yes' );
+
+ $invalidator = new TaxRateVersionStringInvalidator();
+ $invalidator->init();
+
+ $this->assertFalse( has_action( 'woocommerce_tax_rate_added', array( $invalidator, 'handle_woocommerce_tax_rate_added' ) ) );
+ $this->assertFalse( has_action( 'woocommerce_tax_rate_updated', array( $invalidator, 'handle_woocommerce_tax_rate_updated' ) ) );
+ $this->assertFalse( has_action( 'woocommerce_tax_rate_deleted', array( $invalidator, 'handle_woocommerce_tax_rate_deleted' ) ) );
+ }
+
+ /**
+ * @testdox Hooks are not registered when backend caching setting is disabled.
+ */
+ public function test_hooks_not_registered_when_backend_caching_disabled() {
+ update_option( 'woocommerce_feature_rest_api_caching_enabled', 'yes' );
+ update_option( 'woocommerce_rest_api_enable_backend_caching', 'no' );
+
+ $invalidator = new TaxRateVersionStringInvalidator();
+ $invalidator->init();
+
+ $this->assertFalse( has_action( 'woocommerce_tax_rate_added', array( $invalidator, 'handle_woocommerce_tax_rate_added' ) ) );
+ $this->assertFalse( has_action( 'woocommerce_tax_rate_updated', array( $invalidator, 'handle_woocommerce_tax_rate_updated' ) ) );
+ $this->assertFalse( has_action( 'woocommerce_tax_rate_deleted', array( $invalidator, 'handle_woocommerce_tax_rate_deleted' ) ) );
+ }
+
+ /**
+ * @testdox Hooks are not registered when backend caching setting is not set (defaults to no).
+ */
+ public function test_hooks_not_registered_when_backend_caching_not_set() {
+ update_option( 'woocommerce_feature_rest_api_caching_enabled', 'yes' );
+ delete_option( 'woocommerce_rest_api_enable_backend_caching' );
+
+ $invalidator = new TaxRateVersionStringInvalidator();
+ $invalidator->init();
+
+ $this->assertFalse( has_action( 'woocommerce_tax_rate_added', array( $invalidator, 'handle_woocommerce_tax_rate_added' ) ) );
+ $this->assertFalse( has_action( 'woocommerce_tax_rate_updated', array( $invalidator, 'handle_woocommerce_tax_rate_updated' ) ) );
+ $this->assertFalse( has_action( 'woocommerce_tax_rate_deleted', array( $invalidator, 'handle_woocommerce_tax_rate_deleted' ) ) );
+ }
+
+ /**
+ * @testdox Creating a new tax rate invalidates the version string via hook.
+ */
+ public function test_tax_rate_creation_invalidates_version_string() {
+ $this->get_invalidator_with_hooks_enabled();
+
+ $tax_rate_id = 456;
+
+ // Create version string that should be deleted when the hook fires.
+ $this->version_generator->generate_version( "tax_rate_{$tax_rate_id}" );
+ $version_before = $this->version_generator->get_version( "tax_rate_{$tax_rate_id}", false );
+ $this->assertNotNull( $version_before, 'Version string should exist before hook fires' );
+
+ // Trigger the hook.
+ // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+ do_action( 'woocommerce_tax_rate_added', $tax_rate_id );
+
+ $version_after = $this->version_generator->get_version( "tax_rate_{$tax_rate_id}", false );
+ $this->assertNull( $version_after, 'Version string should be deleted after tax rate added hook fires' );
+ }
+
+ /**
+ * @testdox Updating a tax rate invalidates the version string via hook.
+ */
+ public function test_tax_rate_update_invalidates_version_string() {
+ $this->get_invalidator_with_hooks_enabled();
+
+ $tax_rate_id = 789;
+
+ $this->version_generator->generate_version( "tax_rate_{$tax_rate_id}" );
+ $version_before = $this->version_generator->get_version( "tax_rate_{$tax_rate_id}", false );
+ $this->assertNotNull( $version_before, 'Version string should exist before update' );
+
+ // Trigger the hook.
+ // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+ do_action( 'woocommerce_tax_rate_updated', $tax_rate_id );
+
+ $version_after = $this->version_generator->get_version( "tax_rate_{$tax_rate_id}", false );
+ $this->assertNull( $version_after, 'Version string should be deleted after tax rate updated hook fires' );
+ }
+
+ /**
+ * @testdox Deleting a tax rate invalidates the version string via hook.
+ */
+ public function test_tax_rate_deletion_invalidates_version_string() {
+ $this->get_invalidator_with_hooks_enabled();
+
+ $tax_rate_id = 101;
+
+ $this->version_generator->generate_version( "tax_rate_{$tax_rate_id}" );
+ $version_before = $this->version_generator->get_version( "tax_rate_{$tax_rate_id}", false );
+ $this->assertNotNull( $version_before, 'Version string should exist before deletion' );
+
+ // Trigger the hook.
+ // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+ do_action( 'woocommerce_tax_rate_deleted', $tax_rate_id );
+
+ $version_after = $this->version_generator->get_version( "tax_rate_{$tax_rate_id}", false );
+ $this->assertNull( $version_after, 'Version string should be deleted after tax rate deleted hook fires' );
+ }
+
+ /**
+ * @testdox Hook handlers accept string IDs and cast them to integers.
+ */
+ public function test_handlers_accept_string_ids() {
+ $tax_rate_id = '123';
+
+ $this->version_generator->generate_version( 'tax_rate_123' );
+ $version_before = $this->version_generator->get_version( 'tax_rate_123', false );
+ $this->assertNotNull( $version_before, 'Version string should exist before invalidation' );
+
+ $this->sut->handle_woocommerce_tax_rate_added( $tax_rate_id );
+
+ $version_after = $this->version_generator->get_version( 'tax_rate_123', false );
+ $this->assertNull( $version_after, 'Version string should be deleted after invalidation with string ID' );
+ }
+
+ /**
+ * @testdox All hook handlers correctly invalidate version strings.
+ */
+ public function test_all_handlers_invalidate_correctly() {
+ $this->version_generator->generate_version( 'tax_rate_111' );
+ $this->sut->handle_woocommerce_tax_rate_added( 111 );
+ $this->assertNull(
+ $this->version_generator->get_version( 'tax_rate_111', false ),
+ 'Added handler should invalidate version string'
+ );
+
+ $this->version_generator->generate_version( 'tax_rate_222' );
+ $this->sut->handle_woocommerce_tax_rate_updated( 222 );
+ $this->assertNull(
+ $this->version_generator->get_version( 'tax_rate_222', false ),
+ 'Updated handler should invalidate version string'
+ );
+
+ $this->version_generator->generate_version( 'tax_rate_333' );
+ $this->sut->handle_woocommerce_tax_rate_deleted( 333 );
+ $this->assertNull(
+ $this->version_generator->get_version( 'tax_rate_333', false ),
+ 'Deleted handler should invalidate version string'
+ );
+ }
+
+ /**
+ * @testdox Creating a new tax rate invalidates the list version string.
+ */
+ public function test_tax_rate_creation_invalidates_list_version_string() {
+ $this->version_generator->generate_version( 'list_tax_rates' );
+ $version_before = $this->version_generator->get_version( 'list_tax_rates', false );
+ $this->assertNotNull( $version_before, 'List version string should exist before creation' );
+
+ $this->sut->handle_woocommerce_tax_rate_added( 456 );
+
+ $version_after = $this->version_generator->get_version( 'list_tax_rates', false );
+ $this->assertNull( $version_after, 'List version string should be deleted after tax rate added' );
+ }
+
+ /**
+ * @testdox Updating a tax rate invalidates the list version string.
+ */
+ public function test_tax_rate_update_invalidates_list_version_string() {
+ $this->version_generator->generate_version( 'list_tax_rates' );
+ $version_before = $this->version_generator->get_version( 'list_tax_rates', false );
+ $this->assertNotNull( $version_before, 'List version string should exist before update' );
+
+ $this->sut->handle_woocommerce_tax_rate_updated( 789 );
+
+ $version_after = $this->version_generator->get_version( 'list_tax_rates', false );
+ $this->assertNull( $version_after, 'List version string should be deleted after tax rate updated' );
+ }
+
+ /**
+ * @testdox Deleting a tax rate invalidates the list version string.
+ */
+ public function test_tax_rate_deletion_invalidates_list_version_string() {
+ $this->version_generator->generate_version( 'list_tax_rates' );
+ $version_before = $this->version_generator->get_version( 'list_tax_rates', false );
+ $this->assertNotNull( $version_before, 'List version string should exist before deletion' );
+
+ $this->sut->handle_woocommerce_tax_rate_deleted( 101 );
+
+ $version_after = $this->version_generator->get_version( 'list_tax_rates', false );
+ $this->assertNull( $version_after, 'List version string should be deleted after tax rate deleted' );
+ }
+}