From ffef4f608594efad34df52773fd0ae0d680827e1 Mon Sep 17 00:00:00 2001 From: Akmal Suhaimi Date: Sun, 2 Aug 2026 23:01:46 +0800 Subject: [PATCH] Cache query AST as an array in store mode Cache stores serialize their values, and Laravel 13 lets applications limit which classes unserialize() will accept via cache.serializable_classes. Storing the DocumentNode itself meant a restricted list turned the cached AST back into __PHP_Incomplete_Class, so fromStoreOrParse() failed its return type. Store the array form instead, mirroring what the opcache mode already does, and reparse whatever else is found under the key so existing caches recover on their own. --- CHANGELOG.md | 4 ++++ src/Cache/QueryCache.php | 18 ++++++++++++++- tests/Integration/QueryCacheTest.php | 33 ++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 058eab2b4..fc93e8a59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +### Fixed + +- Cache the query AST as an array in `query_cache.mode: store`, so restricting `cache.serializable_classes` no longer makes the cached AST come back as `__PHP_Incomplete_Class` https://github.com/nuwave/lighthouse/pull/2785 + ## v6.69.2 ### Fixed diff --git a/src/Cache/QueryCache.php b/src/Cache/QueryCache.php index 2ea7d6af9..9dc0e4778 100644 --- a/src/Cache/QueryCache.php +++ b/src/Cache/QueryCache.php @@ -80,8 +80,24 @@ public function fromCacheOrParse(string $hash, \Closure $parse): DocumentNode protected function fromStoreOrParse(string $hash, \Closure $parse): DocumentNode { $store = $this->makeCacheStore(); + $key = "lighthouse:query:{$hash}"; + + // The AST is cached as an array rather than as a DocumentNode instance. + // Cache stores serialize objects, and applications may restrict which classes + // unserialize() accepts, which would turn the AST into __PHP_Incomplete_Class. + // Anything else found under this key predates that and is simply reparsed. + $astArray = $store->get(key: $key); + if (is_array($astArray)) { + $astInstance = AST::fromArray($astArray); + assert($astInstance instanceof DocumentNode, 'The cached AST array is expected to convert to a DocumentNode.'); + + return $astInstance; + } + + $query = $parse(); + $store->put(key: $key, value: $query->toArray(), ttl: $this->ttl); - return $store->remember(key: "lighthouse:query:{$hash}", ttl: $this->ttl, callback: $parse); + return $query; } /** @param \Closure(): DocumentNode $parse */ diff --git a/tests/Integration/QueryCacheTest.php b/tests/Integration/QueryCacheTest.php index 8f17003c7..ba87db611 100644 --- a/tests/Integration/QueryCacheTest.php +++ b/tests/Integration/QueryCacheTest.php @@ -54,6 +54,39 @@ public function testEnabledWithDefaults(): void $event->assertDispatchedTimes(KeyWritten::class, 1); } + public function testStoreModeWithRestrictedSerializableClasses(): void + { + $config = $this->app->make(ConfigRepository::class); + $config->set('lighthouse.query_cache.enable', true); + $config->set('lighthouse.validation_cache.enable', false); + + // Laravel 13 lets applications restrict which classes may be unserialized from the cache. + // Any store that serializes values then calls unserialize() with `allowed_classes`, + // turning every disallowed object back into __PHP_Incomplete_Class. + $config->set('cache.stores.array.serialize', true); + $config->set('cache.serializable_classes', []); + + $query = /** @lang GraphQL */ <<<'GRAPHQL' + { + foo + } + GRAPHQL; + + // Populates the query cache. + $this->graphQL($query)->assertExactJson([ + 'data' => [ + 'foo' => Foo::THE_ANSWER, + ], + ]); + + // Reads the cached query back out of the store. + $this->graphQL($query)->assertExactJson([ + 'data' => [ + 'foo' => Foo::THE_ANSWER, + ], + ]); + } + public function testDifferentQueriesHasDifferentKeys(): void { $config = $this->app->make(ConfigRepository::class);