Skip to content

Lazy schema becomes eager on every request with graphql-php >= 15.31: built-in scalar lookup resolves possibleTypes() #2771

Description

@tmoitie

Summary

On webonyx/graphql-php >= 15.31.0, Lighthouse's lazy schema silently becomes eager on essentially every request. The executor's first built-in scalar lookup (Schema::getType('Boolean') etc. — this happens for virtually any operation, including { __typename }) now triggers scalar-override discovery inside graphql-php, which resolves the schema's types callable. In Lighthouse that callable is TypeRegistry::possibleTypes(), which builds every type in the schema from the AST. Since the Schema is rebuilt per request under PHP-FPM, the documented lazy design is defeated on each request.

Mechanism

SchemaBuilder::build() configures:

// Use lazy type loading to prevent unnecessary work
$config->setTypeLoader(
    fn (string $name): ?Type => $this->typeRegistry->search($name),
);

// Enables introspection to list all types in the schema
$config->setTypes(
    /** @return array<string, \GraphQL\Type\Definition\Type> */
    fn (): array => $this->typeRegistry->possibleTypes(),
);

graphql-php introduced scalar overrides in 15.31.0 (webonyx/graphql-php#1869) and in 15.31.2 moved override discovery exclusively to scanning types (webonyx/graphql-php#1884, see also webonyx/graphql-php#1874). With a lazy types callable, that scan forces full resolution the first time any built-in scalar is looked up.

Measured impact

Production app on Lighthouse 6.66, upgrading graphql-php 15.30.2 → 15.32.3:

  • every operation 1.5–3.4× slower wall-clock
  • ~6× more PHP work (xdebug cachegrind: 9.1 MB → 57 MB for an authenticated { __typename })
  • ablation: patching Lighthouse to setTypes(fn () => []) restores the 15.30.2 baseline

Standalone repro and benchmark table: webonyx/graphql-php#1927.

Affected users currently have only bad options: pin graphql-php to 15.30.2 (re-exposes GHSA-r7cg-qjjm-xhqq, GHSA-fc86-6rv6-2jpm, CVE-2026-40476) or accept the per-request cost.

Proposed change

webonyx/graphql-php#1927 (draft) adds SchemaConfig::setScalarOverrides(?array): null (default) keeps scan-based discovery; an explicit array — including [] — skips the scan entirely.

Lighthouse can determine the overrides cheaply from the AST without building any types: a scalar override is a user-defined type named after a built-in scalar, and $documentAST->types is keyed by name. Sketch (in build(), after setTypes):

// TODO remove the guard when requiring the graphql-php release that ships setScalarOverrides
if (method_exists($config, 'setScalarOverrides')) {
    $scalarOverrides = [];
    foreach (Type::BUILT_IN_SCALAR_NAMES as $name) {
        if (isset($documentAST->types[$name])) {
            $type = $this->typeRegistry->get($name);
            assert($type instanceof ScalarType);
            $scalarOverrides[] = $type;
        }
    }
    $config->setScalarOverrides($scalarOverrides);
}

The method_exists guard keeps the current webonyx/graphql-php: ^15 constraint intact and follows the same pattern as the query-complexity guard added in #2637; it can be dropped whenever the constraint is eventually bumped past the release containing the new API.

For the common case (no built-in scalar names redefined in the SDL) this is five isset() checks and an explicit empty array — the hot path stays fully lazy. The loop is not dead code: TypeRegistry::search() consults $documentAST->types before Type::getStandardTypes(), so a user SDL can redefine e.g. String.

I'm happy to submit a PR with this change plus a regression test asserting that a simple query execution does not invoke possibleTypes(), once the upstream API lands.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions