Skip to content

Cache query AST as an array in store mode - #2785

Open
dxiiren wants to merge 1 commit into
nuwave:masterfrom
dxiiren:fix-query-cache-serializable-classes
Open

Cache query AST as an array in store mode#2785
dxiiren wants to merge 1 commit into
nuwave:masterfrom
dxiiren:fix-query-cache-serializable-classes

Conversation

@dxiiren

@dxiiren dxiiren commented Aug 2, 2026

Copy link
Copy Markdown

Resolves #2782

  • Added or updated tests
  • Documented user facing changes
  • Updated CHANGELOG.md (skip for docs-only changes)

Changes

query_cache.mode: store handed the DocumentNode straight to the cache store:

return $store->remember(key: "lighthouse:query:{$hash}", ttl: $this->ttl, callback: $parse);

Cache stores serialize what they are given. Laravel 13 lets an application limit which
classes come back out again through cache.serializable_classes, which every serializing
store reads before unserializing:

// Illuminate\Cache\RedisStore::unserialize(), same in DatabaseStore, FileStore, ArrayStore
if ($this->serializableClasses !== null) {
    return unserialize($value, ['allowed_classes' => $this->serializableClasses]);
}

Once that list is set and does not name every AST node class, the cached query comes back
as __PHP_Incomplete_Class and fromStoreOrParse() fails its return type on the second
request. Whitelisting the AST is not a practical workaround, since it would mean naming
every node class in webonyx/graphql-php and keeping that list current.

So this caches the array form instead of the object, which is what mode: opcache already
does via opcacheFileContents() and requireOPcacheFile(). Arrays are unaffected by
allowed_classes, so the AST survives regardless of how a store is configured.

The remember() call is replaced with the explicit get()/put() pair that
fromHybridOrParse() already uses, so anything found under the key that is not an array
is simply reparsed and overwritten. Caches populated by an older version therefore recover
by themselves on the next request, with no flush needed on upgrade.

Verified on PHP 8.4.20 / Laravel 13.23.0 / webonyx/graphql-php 15.37.1. The added test
fails on current master with exactly the reported error and passes with this change:

TypeError: Nuwave\Lighthouse\Cache\QueryCache::fromStoreOrParse(): Return value must be
of type GraphQL\Language\AST\DocumentNode, __PHP_Incomplete_Class returned
src/Cache/QueryCache.php:84
src/Cache/QueryCache.php:72
src/GraphQL.php:287

It reproduces through the array store with serialize enabled, so it needs no Redis or
database. The rest of QueryCacheTest is unchanged by this commit.

Breaking changes

None. The cache key is unchanged, entries written by older versions are detected and
reparsed rather than failing, and the public API is untouched.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

QueryCache returns __PHP_Incomplete_Class on Laravel 13

1 participant