Commit 79f197bec3c for woocommerce
commit 79f197bec3c63fcb9316fb49a50225cc11fc5ebd
Author: Alba Rincón <albarin@users.noreply.github.com>
Date: Thu Apr 30 13:01:41 2026 +0200
Add ObjectCache toggle to GraphQL settings (#64415)
Co-authored-by: Nestor Soriano <konamiman@konamiman.com>
diff --git a/plugins/woocommerce/changelog/64415-64297-graphql-settings-object-cache b/plugins/woocommerce/changelog/64415-64297-graphql-settings-object-cache
new file mode 100644
index 00000000000..1faad556355
--- /dev/null
+++ b/plugins/woocommerce/changelog/64415-64297-graphql-settings-object-cache
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add a setting to toggle the ObjectCache-backed parsed-query cache on the GraphQL settings page.
\ No newline at end of file
diff --git a/plugins/woocommerce/src/Internal/Api/Main.php b/plugins/woocommerce/src/Internal/Api/Main.php
index 6b115f4a1bf..7ef7a9d76f3 100644
--- a/plugins/woocommerce/src/Internal/Api/Main.php
+++ b/plugins/woocommerce/src/Internal/Api/Main.php
@@ -49,6 +49,11 @@ class Main {
*/
public const OPTION_MAX_QUERY_COMPLEXITY = 'woocommerce_graphql_max_query_complexity';
+ /**
+ * Option name for the "ObjectCache-based caching" setting.
+ */
+ public const OPTION_OBJECT_CACHE_ENABLED = 'woocommerce_graphql_object_cache_enabled';
+
/**
* Check whether the Dual Code & GraphQL API feature is active.
*
@@ -72,6 +77,15 @@ class Main {
return wc_string_to_bool( get_option( self::OPTION_GET_ENDPOINT_ENABLED, 'yes' ) );
}
+ /**
+ * Whether the ObjectCache-backed query cache is enabled.
+ *
+ * Defaults to true.
+ */
+ public static function is_object_cache_enabled(): bool {
+ return wc_string_to_bool( get_option( self::OPTION_OBJECT_CACHE_ENABLED, 'yes' ) );
+ }
+
/**
* Apply the GraphQL-scoped site settings to a caller-declared list of HTTP
* methods.
diff --git a/plugins/woocommerce/src/Internal/Api/QueryCache.php b/plugins/woocommerce/src/Internal/Api/QueryCache.php
index f304ac8978b..5925afa17ad 100644
--- a/plugins/woocommerce/src/Internal/Api/QueryCache.php
+++ b/plugins/woocommerce/src/Internal/Api/QueryCache.php
@@ -61,6 +61,11 @@ class QueryCache {
return $this->error_response( 'No query provided.', 'BAD_REQUEST' );
}
+ // APQ keeps using the cache; it has its own settings toggle.
+ if ( ! Main::is_object_cache_enabled() ) {
+ return $this->parse( $query );
+ }
+
$hash = hash( 'sha256', $query );
$doc = $this->get_cached_document( $hash );
if ( false !== $doc ) {
@@ -119,6 +124,21 @@ class QueryCache {
return AST::fromArray( $cached );
}
+ /**
+ * Parse a query and return the DocumentNode, or a GraphQL-shaped error
+ * array if the query has a syntax error.
+ *
+ * @param string $query The GraphQL query string.
+ * @return DocumentNode|array
+ */
+ private function parse( string $query ) {
+ try {
+ return Parser::parse( $query, array( 'noLocation' => true ) );
+ } catch ( \Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError $e ) {
+ return $this->error_response( 'GraphQL syntax error: ' . $e->getMessage(), 'GRAPHQL_PARSE_ERROR' );
+ }
+ }
+
/**
* Parse a query, cache the resulting AST, and return the DocumentNode.
*
@@ -129,10 +149,9 @@ class QueryCache {
* @return DocumentNode|array
*/
private function parse_and_cache( string $query, string $hash ) {
- try {
- $document = Parser::parse( $query, array( 'noLocation' => true ) );
- } catch ( \Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError $e ) {
- return $this->error_response( 'GraphQL syntax error: ' . $e->getMessage(), 'GRAPHQL_PARSE_ERROR' );
+ $document = $this->parse( $query );
+ if ( ! $document instanceof DocumentNode ) {
+ return $document;
}
wp_cache_set( $this->build_cache_key( $hash ), $document->toArray(), self::CACHE_GROUP, self::get_cache_ttl() );
diff --git a/plugins/woocommerce/src/Internal/Api/Settings.php b/plugins/woocommerce/src/Internal/Api/Settings.php
index b4e7c0b0e55..c642a9d519f 100644
--- a/plugins/woocommerce/src/Internal/Api/Settings.php
+++ b/plugins/woocommerce/src/Internal/Api/Settings.php
@@ -86,6 +86,13 @@ class Settings {
'type' => 'number',
'custom_attributes' => array( 'min' => '1' ),
),
+ array(
+ 'title' => __( 'Enable ObjectCache-based caching', 'woocommerce' ),
+ 'desc' => __( 'Cache parsed queries in the WP object cache', 'woocommerce' ),
+ 'id' => Main::OPTION_OBJECT_CACHE_ENABLED,
+ 'default' => 'yes',
+ 'type' => 'checkbox',
+ ),
array(
'title' => __( 'Endpoint URL', 'woocommerce' ),
'desc' => __( 'Path relative to /wp-json/ where the GraphQL endpoint is exposed. Needs at least two segments (namespace/route), e.g. wc/graphql.', 'woocommerce' ),
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/QueryCacheTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/QueryCacheTest.php
index d5a4704219d..b3ba2b912a9 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Api/QueryCacheTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Api/QueryCacheTest.php
@@ -1,40 +1,50 @@
<?php
-
-declare(strict_types=1);
+declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Internal\Api;
+use Automattic\WooCommerce\Internal\Api\Main;
use Automattic\WooCommerce\Internal\Api\QueryCache;
use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
use WC_Unit_Test_Case;
/**
- * Tests for {@see QueryCache} — the AST cache backing both the
+ * Tests for {@see QueryCache} — covers the AST cache backing both the
* standard "parse + cache" path and the Apollo Automatic Persisted Queries
- * (APQ) protocol used by clients that send a hash instead of the full query.
+ * (APQ) protocol, as well as the OPTION_OBJECT_CACHE_ENABLED toggle.
*/
class QueryCacheTest extends WC_Unit_Test_Case {
/**
- * The system under test.
+ * The System Under Test.
*
* @var QueryCache
*/
private QueryCache $sut;
/**
- * Set up.
+ * Set up before each test.
+ *
+ * Skips on PHP < 8.1 because the GraphQL stack (vendor parser, QueryCache
+ * dependencies) is only autoloaded after {@see Main::is_enabled()} gates
+ * on PHP 8.1+. Replicate that gate here so the autoload never triggers a
+ * parse error on older PHP.
*/
public function setUp(): void {
parent::setUp();
+ if ( PHP_VERSION_ID < 80100 ) {
+ $this->markTestSkipped( 'QueryCache tests require PHP 8.1+.' );
+ }
+
wp_cache_flush();
$this->sut = new QueryCache();
}
/**
- * Tear down.
+ * Clean up the option and cache between tests.
*/
public function tearDown(): void {
+ delete_option( Main::OPTION_OBJECT_CACHE_ENABLED );
wp_cache_flush();
parent::tearDown();
}
@@ -160,4 +170,44 @@ class QueryCacheTest extends WC_Unit_Test_Case {
public function test_get_cache_ttl_is_a_day(): void {
$this->assertSame( DAY_IN_SECONDS, QueryCache::get_cache_ttl() );
}
+
+ /**
+ * @testdox resolve writes the parsed document to the object cache when the toggle is on.
+ */
+ public function test_resolve_writes_to_cache_when_toggle_on(): void {
+ update_option( Main::OPTION_OBJECT_CACHE_ENABLED, 'yes' );
+
+ $result = $this->sut->resolve( '{ __typename }', array() );
+
+ $this->assertInstanceOf( DocumentNode::class, $result );
+ $this->assertNotFalse(
+ wp_cache_get( $this->cache_key_for( '{ __typename }' ), 'wc-graphql' ),
+ 'Standard parse should persist the AST in the object cache.'
+ );
+ }
+
+ /**
+ * @testdox resolve does not write to the object cache when the toggle is off.
+ */
+ public function test_resolve_does_not_write_to_cache_when_toggle_off(): void {
+ update_option( Main::OPTION_OBJECT_CACHE_ENABLED, 'no' );
+
+ $result = $this->sut->resolve( '{ __typename }', array() );
+
+ $this->assertInstanceOf( DocumentNode::class, $result );
+ $this->assertFalse(
+ wp_cache_get( $this->cache_key_for( '{ __typename }' ), 'wc-graphql' ),
+ 'No cache entry should be written when the ObjectCache toggle is off.'
+ );
+ }
+
+ /**
+ * Build the QueryCache cache key for a query string. Prefix kept in sync
+ * with QueryCache::CACHE_KEY_PREFIX.
+ *
+ * @param string $query The GraphQL query string.
+ */
+ private function cache_key_for( string $query ): string {
+ return 'graphql_ast_v15_' . hash( 'sha256', $query );
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Api/SettingsTest.php b/plugins/woocommerce/tests/php/src/Internal/Api/SettingsTest.php
index 79fea62814c..5968c021f40 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Api/SettingsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Api/SettingsTest.php
@@ -192,6 +192,18 @@ class SettingsTest extends WC_Unit_Test_Case {
$this->assertSame( $input, $result );
}
+ /**
+ * @testdox add_settings defines the ObjectCache checkbox with a 'yes' default.
+ */
+ public function test_add_settings_defines_object_cache_checkbox(): void {
+ $fields = $this->sut->add_settings( array(), Settings::SECTION_ID );
+ $by_id = array_column( $fields, null, 'id' );
+
+ $this->assertArrayHasKey( Main::OPTION_OBJECT_CACHE_ENABLED, $by_id );
+ $this->assertSame( 'checkbox', $by_id[ Main::OPTION_OBJECT_CACHE_ENABLED ]['type'] );
+ $this->assertSame( 'yes', $by_id[ Main::OPTION_OBJECT_CACHE_ENABLED ]['default'] );
+ }
+
/**
* @testdox add_settings defines the max query depth field with min=1 and the default constant as default.
*/