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);