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.
 	 */