From 35ce5769aecb66738c08711ee1efa60093ff516c Mon Sep 17 00:00:00 2001 From: shmax Date: Thu, 10 Jul 2025 20:24:59 -0700 Subject: [PATCH 01/38] cache result of validation --- composer.json | 4 +++- src/GraphQL.php | 12 ++++++++---- src/Validator/DocumentValidator.php | 25 +++++++++++++++++++++---- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 39bfebf3a..9da692db6 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ "require": { "php": "^7.4 || ^8", "ext-json": "*", - "ext-mbstring": "*" + "ext-mbstring": "*", + "psr/simple-cache": "^3.0" }, "require-dev": { "amphp/amp": "^2.6", @@ -31,6 +32,7 @@ "react/http": "^1.6", "react/promise": "^2.0 || ^3.0", "rector/rector": "^2.0", + "symfony/cache": "^6.4", "symfony/polyfill-php81": "^1.23", "symfony/var-exporter": "^5 || ^6 || ^7", "thecodingmachine/safe": "^1.3 || ^2 || ^3" diff --git a/src/GraphQL.php b/src/GraphQL.php index 919b09dec..d0d5c85f0 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -19,6 +19,7 @@ use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\ValidationRule; +use Psr\SimpleCache\CacheInterface; /** * This is the primary facade for fulfilling GraphQL operations. @@ -90,7 +91,8 @@ public static function executeQuery( ?array $variableValues = null, ?string $operationName = null, ?callable $fieldResolver = null, - ?array $validationRules = null + ?array $validationRules = null, + ?CacheInterface $cache = null, ): ExecutionResult { $promiseAdapter = new SyncPromiseAdapter(); @@ -103,7 +105,8 @@ public static function executeQuery( $variableValues, $operationName, $fieldResolver, - $validationRules + $validationRules, + $cache ); return $promiseAdapter->wait($promise); @@ -132,7 +135,8 @@ public static function promiseToExecute( ?array $variableValues = null, ?string $operationName = null, ?callable $fieldResolver = null, - ?array $validationRules = null + ?array $validationRules = null, + ?CacheInterface $cache = null ): Promise { try { $documentNode = $source instanceof DocumentNode @@ -152,7 +156,7 @@ public static function promiseToExecute( } } - $validationErrors = DocumentValidator::validate($schema, $documentNode, $validationRules); + $validationErrors = DocumentValidator::validate($schema, $documentNode, $validationRules, null, $cache); if ($validationErrors !== []) { return $promiseAdapter->createFulfilled( diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index 0c4eb498f..0591f230b 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -49,6 +49,7 @@ use GraphQL\Validator\Rules\ValuesOfCorrectType; use GraphQL\Validator\Rules\VariablesAreInputTypes; use GraphQL\Validator\Rules\VariablesInAllowedPosition; +use Psr\SimpleCache\CacheInterface; /** * Implements the "Validation" section of the spec. @@ -99,16 +100,25 @@ public static function validate( Schema $schema, DocumentNode $ast, ?array $rules = null, - ?TypeInfo $typeInfo = null + ?TypeInfo $typeInfo = null, + ?CacheInterface $cache = null ): array { - $rules ??= static::allRules(); + $cacheKey = null; + + if ($cache) { + $cacheKey = 'gql_validation_' . md5($ast->__toString()); + if ($cache->has($cacheKey)) { + return $cache->get($cacheKey); + } + } + + $rules ??= static::allRules(); if ($rules === []) { return []; } $typeInfo ??= new TypeInfo($schema); - $context = new QueryValidationContext($schema, $ast, $typeInfo); $visitors = []; @@ -124,7 +134,14 @@ public static function validate( ) ); - return $context->getErrors(); + $errors = $context->getErrors(); + + // Only cache clean results + if ($cache && $cacheKey && count($errors) === 0) { + $cache->set($cacheKey, $errors, 300); // TTL = 5 min + } + + return $errors; } /** From 03db33dea1314701e5d6f45644983fa982c243b0 Mon Sep 17 00:00:00 2001 From: shmax Date: Thu, 10 Jul 2025 20:31:46 -0700 Subject: [PATCH 02/38] remove ttl --- src/Validator/DocumentValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index 0591f230b..39863623b 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -138,7 +138,7 @@ public static function validate( // Only cache clean results if ($cache && $cacheKey && count($errors) === 0) { - $cache->set($cacheKey, $errors, 300); // TTL = 5 min + $cache->set($cacheKey, $errors); } return $errors; From c3a7eb445c5a31230e8ae203e18d05503112f50a Mon Sep 17 00:00:00 2001 From: shmax Date: Thu, 10 Jul 2025 20:51:54 -0700 Subject: [PATCH 03/38] stanning --- src/Validator/DocumentValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index 39863623b..07aa8f783 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -105,7 +105,7 @@ public static function validate( ): array { $cacheKey = null; - if ($cache) { + if (isset($cache)) { $cacheKey = 'gql_validation_' . md5($ast->__toString()); if ($cache->has($cacheKey)) { From a2c38f477e768c3b68db310444434c5995776df5 Mon Sep 17 00:00:00 2001 From: shmax Date: Thu, 10 Jul 2025 20:55:02 -0700 Subject: [PATCH 04/38] stanning --- src/Validator/DocumentValidator.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index 07aa8f783..29129997c 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -50,6 +50,7 @@ use GraphQL\Validator\Rules\VariablesAreInputTypes; use GraphQL\Validator\Rules\VariablesInAllowedPosition; use Psr\SimpleCache\CacheInterface; +use Psr\SimpleCache\InvalidArgumentException; /** * Implements the "Validation" section of the spec. @@ -108,9 +109,11 @@ public static function validate( if (isset($cache)) { $cacheKey = 'gql_validation_' . md5($ast->__toString()); - if ($cache->has($cacheKey)) { - return $cache->get($cacheKey); - } + try { + if ($cache->has($cacheKey)) { + return $cache->get($cacheKey); + } + } catch (InvalidArgumentException $e) {} } $rules ??= static::allRules(); @@ -137,9 +140,11 @@ public static function validate( $errors = $context->getErrors(); // Only cache clean results - if ($cache && $cacheKey && count($errors) === 0) { - $cache->set($cacheKey, $errors); - } + try { + if (isset($cacheKey) && count($errors) === 0) { + $cache->set($cacheKey, $errors); + } + } catch (InvalidArgumentException $e) {} return $errors; } From 6782bad6214265d08b1340d03eefc333f7c1ed4a Mon Sep 17 00:00:00 2001 From: shmax Date: Thu, 10 Jul 2025 21:04:11 -0700 Subject: [PATCH 05/38] downgrade to satisfy 7.4 --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 9da692db6..b72872de8 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "php": "^7.4 || ^8", "ext-json": "*", "ext-mbstring": "*", - "psr/simple-cache": "^3.0" + "psr/simple-cache": "^1.0" }, "require-dev": { "amphp/amp": "^2.6", @@ -32,7 +32,7 @@ "react/http": "^1.6", "react/promise": "^2.0 || ^3.0", "rector/rector": "^2.0", - "symfony/cache": "^6.4", + "symfony/cache": "^5.4", "symfony/polyfill-php81": "^1.23", "symfony/var-exporter": "^5 || ^6 || ^7", "thecodingmachine/safe": "^1.3 || ^2 || ^3" From 63179316c11ceba9d30104d9b0077ebbe7f2f4d6 Mon Sep 17 00:00:00 2001 From: shmax Date: Fri, 11 Jul 2025 17:04:58 -0700 Subject: [PATCH 06/38] use custom interface --- composer.json | 4 ++-- src/GraphQL.php | 6 +++--- src/Validator/DocumentValidator.php | 25 ++++++++----------------- src/Validator/ValidationCache.php | 25 +++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 22 deletions(-) create mode 100644 src/Validator/ValidationCache.php diff --git a/composer.json b/composer.json index b72872de8..c022200bb 100644 --- a/composer.json +++ b/composer.json @@ -11,8 +11,7 @@ "require": { "php": "^7.4 || ^8", "ext-json": "*", - "ext-mbstring": "*", - "psr/simple-cache": "^1.0" + "ext-mbstring": "*" }, "require-dev": { "amphp/amp": "^2.6", @@ -29,6 +28,7 @@ "phpstan/phpstan-strict-rules": "2.0.4", "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11", "psr/http-message": "^1 || ^2", + "psr/simple-cache": "^1.0", "react/http": "^1.6", "react/promise": "^2.0 || ^3.0", "rector/rector": "^2.0", diff --git a/src/GraphQL.php b/src/GraphQL.php index d0d5c85f0..e9b4130f3 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -19,7 +19,7 @@ use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\ValidationRule; -use Psr\SimpleCache\CacheInterface; +use GraphQL\Validator\ValidationCache; /** * This is the primary facade for fulfilling GraphQL operations. @@ -92,7 +92,7 @@ public static function executeQuery( ?string $operationName = null, ?callable $fieldResolver = null, ?array $validationRules = null, - ?CacheInterface $cache = null, + ?ValidationCache $cache = null, ): ExecutionResult { $promiseAdapter = new SyncPromiseAdapter(); @@ -136,7 +136,7 @@ public static function promiseToExecute( ?string $operationName = null, ?callable $fieldResolver = null, ?array $validationRules = null, - ?CacheInterface $cache = null + ?ValidationCache $cache = null ): Promise { try { $documentNode = $source instanceof DocumentNode diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index 29129997c..50037d58a 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -49,8 +49,6 @@ use GraphQL\Validator\Rules\ValuesOfCorrectType; use GraphQL\Validator\Rules\VariablesAreInputTypes; use GraphQL\Validator\Rules\VariablesInAllowedPosition; -use Psr\SimpleCache\CacheInterface; -use Psr\SimpleCache\InvalidArgumentException; /** * Implements the "Validation" section of the spec. @@ -102,18 +100,13 @@ public static function validate( DocumentNode $ast, ?array $rules = null, ?TypeInfo $typeInfo = null, - ?CacheInterface $cache = null + ?ValidationCache $cache = null ): array { - $cacheKey = null; - if (isset($cache)) { - $cacheKey = 'gql_validation_' . md5($ast->__toString()); - - try { - if ($cache->has($cacheKey)) { - return $cache->get($cacheKey); - } - } catch (InvalidArgumentException $e) {} + $cached = $cache->isValidated($schema, $ast); + if ($cached) { + return []; + } } $rules ??= static::allRules(); @@ -140,11 +133,9 @@ public static function validate( $errors = $context->getErrors(); // Only cache clean results - try { - if (isset($cacheKey) && count($errors) === 0) { - $cache->set($cacheKey, $errors); - } - } catch (InvalidArgumentException $e) {} + if (isset($cache) && count($errors) === 0) { + $cache->markValidated($schema, $ast); + } return $errors; } diff --git a/src/Validator/ValidationCache.php b/src/Validator/ValidationCache.php new file mode 100644 index 000000000..d69195cfd --- /dev/null +++ b/src/Validator/ValidationCache.php @@ -0,0 +1,25 @@ + Date: Fri, 11 Jul 2025 17:32:42 -0700 Subject: [PATCH 07/38] files --- tests/Executor/ValidationWithCacheTest.php | 101 +++++++++++++++++++++ tests/PsrValidationCacheAdapter.php | 74 +++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 tests/Executor/ValidationWithCacheTest.php create mode 100644 tests/PsrValidationCacheAdapter.php diff --git a/tests/Executor/ValidationWithCacheTest.php b/tests/Executor/ValidationWithCacheTest.php new file mode 100644 index 000000000..4d7ccb646 --- /dev/null +++ b/tests/Executor/ValidationWithCacheTest.php @@ -0,0 +1,101 @@ + 'Pet', + 'fields' => [ + 'name' => ['type' => Type::string()], + ], + ]); + + $DogType = new ObjectType([ + 'name' => 'Dog', + 'interfaces' => [$petType], + 'isTypeOf' => static fn ($obj): Deferred => new Deferred(static fn (): bool => $obj instanceof Dog), + 'fields' => [ + 'name' => ['type' => Type::string()], + 'woofs' => ['type' => Type::boolean()], + ], + ]); + + $CatType = new ObjectType([ + 'name' => 'Cat', + 'interfaces' => [$petType], + 'isTypeOf' => static fn ($obj): Deferred => new Deferred(static fn (): bool => $obj instanceof Cat), + 'fields' => [ + 'name' => ['type' => Type::string()], + 'meows' => ['type' => Type::boolean()], + ], + ]); + + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'pets' => [ + 'type' => Type::listOf($petType), + 'resolve' => static fn (): array => [ + new Dog('Odie', true), + new Cat('Garfield', false), + ], + ], + ], + ]), + 'types' => [$CatType, $DogType], + ]); + + $query = '{ + pets { + name + ... on Dog { + woofs + } + ... on Cat { + meows + } + } + }'; + + GraphQL::executeQuery( $schema, $query, null, null, null, null, null, null, $cache)->toArray(); + + // TODO: use a spy or something to prove that the validation only happens once + $result = GraphQL::executeQuery( $schema, $query, null, null, null, null, null, null, $cache)->toArray(); + + $expected = [ + 'data' => [ + 'pets' => [ + ['name' => 'Odie', 'woofs' => true], + ['name' => 'Garfield', 'meows' => false], + ], + ], + ]; + + self::assertEquals($expected, $result); + } +} diff --git a/tests/PsrValidationCacheAdapter.php b/tests/PsrValidationCacheAdapter.php new file mode 100644 index 000000000..c05d648f6 --- /dev/null +++ b/tests/PsrValidationCacheAdapter.php @@ -0,0 +1,74 @@ + + * } + */ +final class PsrValidationCacheAdapter implements ValidationCache +{ + private const KEY_PREFIX = 'gql_validation_'; + private int $ttl; + private CacheInterface $cache; + + public function __construct( + CacheInterface $cache, + int $ttlSeconds = 300 + ) { + $this->ttl = $ttlSeconds; + $this->cache = $cache; + } + + /** + * @throws InvalidArgumentException + */ + public function isValidated(Schema $schema, DocumentNode $ast): bool + { + try { + $key = $this->buildKey($schema, $ast); + /** @phpstan-ignore-next-line */ + return $this->cache->has($key); + } catch (\Throwable $e) { + return false; + } + } + + /** + * @throws InvalidArgumentException + */ + public function markValidated(Schema $schema, DocumentNode $ast): void + { + try { + $key = $this->buildKey($schema, $ast); + /** @phpstan-ignore-next-line */ + $this->cache->set($key, true, $this->ttl); + } catch (\Throwable $e) { + // ignore silently + } + } + + /** + * @throws \GraphQL\Error\Error + * @throws \GraphQL\Error\InvariantViolation + * @throws \GraphQL\Error\SerializationError + * @throws \JsonException + */ + private function buildKey(Schema $schema, DocumentNode $ast): string + { + // NOTE: You can override this strategy if you want to make schema fingerprinting cheaper + $schemaHash = md5(SchemaPrinter::doPrint($schema)); + $astHash = md5($ast->__toString()); + + return self::KEY_PREFIX . $schemaHash . '_' . $astHash; + } +} From 6d7ace0ecd634726a694b7fc8625a865bf02f080 Mon Sep 17 00:00:00 2001 From: shmax Date: Fri, 11 Jul 2025 17:41:34 -0700 Subject: [PATCH 08/38] cleanup --- tests/PsrValidationCacheAdapter.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/PsrValidationCacheAdapter.php b/tests/PsrValidationCacheAdapter.php index c05d648f6..f9c529f6f 100644 --- a/tests/PsrValidationCacheAdapter.php +++ b/tests/PsrValidationCacheAdapter.php @@ -9,12 +9,6 @@ use Psr\SimpleCache\CacheInterface; use Symfony\Component\String\Exception\InvalidArgumentException; -/** - * @phpstan-type ErrorArray array{ - * message: string, - * locations?: array - * } - */ final class PsrValidationCacheAdapter implements ValidationCache { private const KEY_PREFIX = 'gql_validation_'; From 708c85b125cdf0430745b7494c3214abcc08e6c2 Mon Sep 17 00:00:00 2001 From: shmax Date: Fri, 11 Jul 2025 17:43:44 -0700 Subject: [PATCH 09/38] cleanup --- tests/Executor/ValidationWithCacheTest.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/Executor/ValidationWithCacheTest.php b/tests/Executor/ValidationWithCacheTest.php index 4d7ccb646..a3f9f3465 100644 --- a/tests/Executor/ValidationWithCacheTest.php +++ b/tests/Executor/ValidationWithCacheTest.php @@ -20,13 +20,9 @@ final class ValidationWithCacheTest extends TestCase { use ArraySubsetAsserts; - /** @see it('isTypeOf used to resolve runtime type for Interface') */ public function testIsValidationCachedWithAdapter(): void { $cache = new PsrValidationCacheAdapter(new Psr16Cache(new ArrayAdapter())); - - - $petType = new InterfaceType([ 'name' => 'Pet', 'fields' => [ From f35cb3c26472852eeea9321885d33c66ce90a2d1 Mon Sep 17 00:00:00 2001 From: shmax Date: Fri, 11 Jul 2025 18:53:26 -0700 Subject: [PATCH 10/38] remove trailing comma --- src/GraphQL.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GraphQL.php b/src/GraphQL.php index e9b4130f3..80e65803e 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -92,7 +92,7 @@ public static function executeQuery( ?string $operationName = null, ?callable $fieldResolver = null, ?array $validationRules = null, - ?ValidationCache $cache = null, + ?ValidationCache $cache = null ): ExecutionResult { $promiseAdapter = new SyncPromiseAdapter(); From d71e123c8c5c9199c7e8b5b9945f6300d0f551b0 Mon Sep 17 00:00:00 2001 From: shmax Date: Fri, 11 Jul 2025 19:48:11 -0700 Subject: [PATCH 11/38] fix tests --- tests/Executor/ValidationWithCacheTest.php | 29 +++++++++++++++++++--- tests/PsrValidationCacheAdapter.php | 2 +- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/tests/Executor/ValidationWithCacheTest.php b/tests/Executor/ValidationWithCacheTest.php index a3f9f3465..8b8c1bad9 100644 --- a/tests/Executor/ValidationWithCacheTest.php +++ b/tests/Executor/ValidationWithCacheTest.php @@ -5,6 +5,7 @@ use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; use GraphQL\Deferred; use GraphQL\GraphQL; +use GraphQL\Language\AST\DocumentNode; use GraphQL\Tests\Executor\TestClasses\Cat; use GraphQL\Tests\Executor\TestClasses\Dog; use GraphQL\Tests\PsrValidationCacheAdapter; @@ -16,13 +17,32 @@ use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Psr16Cache; +final class SpyValidationCacheAdapter extends PsrValidationCacheAdapter +{ + public int $isValidatedCalls = 0; + public int $markValidatedCalls = 0; + + public function isValidated(Schema $schema, DocumentNode $ast): bool + { + $this->isValidatedCalls++; + return parent::isValidated($schema, $ast); + } + + public function markValidated(Schema $schema, DocumentNode $ast): void + { + $this->markValidatedCalls++; + parent::markValidated($schema, $ast); + } +} + + final class ValidationWithCacheTest extends TestCase { use ArraySubsetAsserts; public function testIsValidationCachedWithAdapter(): void { - $cache = new PsrValidationCacheAdapter(new Psr16Cache(new ArrayAdapter())); + $cache = new SpyValidationCacheAdapter(new Psr16Cache(new ArrayAdapter())); $petType = new InterfaceType([ 'name' => 'Pet', 'fields' => [ @@ -78,11 +98,14 @@ public function testIsValidationCachedWithAdapter(): void } }'; + // make the same call twice in a row. We'll then inspect the cache object to count calls GraphQL::executeQuery( $schema, $query, null, null, null, null, null, null, $cache)->toArray(); - - // TODO: use a spy or something to prove that the validation only happens once $result = GraphQL::executeQuery( $schema, $query, null, null, null, null, null, null, $cache)->toArray(); + // ✅ Assert that validation only happened once + self::assertEquals(2, $cache->isValidatedCalls, 'Should check cache twice'); + self::assertEquals(1, $cache->markValidatedCalls, 'Should mark as validated once'); + $expected = [ 'data' => [ 'pets' => [ diff --git a/tests/PsrValidationCacheAdapter.php b/tests/PsrValidationCacheAdapter.php index f9c529f6f..e43b8db61 100644 --- a/tests/PsrValidationCacheAdapter.php +++ b/tests/PsrValidationCacheAdapter.php @@ -9,7 +9,7 @@ use Psr\SimpleCache\CacheInterface; use Symfony\Component\String\Exception\InvalidArgumentException; -final class PsrValidationCacheAdapter implements ValidationCache +class PsrValidationCacheAdapter implements ValidationCache { private const KEY_PREFIX = 'gql_validation_'; private int $ttl; From cf20ef32fad61638ff8f9a8e373c9180489cfffa Mon Sep 17 00:00:00 2001 From: shmax Date: Mon, 14 Jul 2025 07:13:59 -0700 Subject: [PATCH 12/38] formatting --- src/Validator/ValidationCache.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Validator/ValidationCache.php b/src/Validator/ValidationCache.php index d69195cfd..7e7de3c4b 100644 --- a/src/Validator/ValidationCache.php +++ b/src/Validator/ValidationCache.php @@ -7,14 +7,15 @@ use GraphQL\Type\Schema; /** - * Implement this interface and pass an instance to GraphQL::executeQuery to cache validation of ASTs. The details - * of how to compute any keys (or whether to validate at all) are left up to you. + * Implement this interface and pass an instance to GraphQL::executeQuery to cache validation of ASTs. + * The details of how to compute any keys (or whether to validate at all) are left up to you. */ interface ValidationCache { /** * Return true if the given schema + AST pair has previously been validated successfully. - * Only successful validations are cached. A return value of false means the pair is either unknown or has not been validated yet. + * Only successful validations are cached. + * A return value of false means the pair is either unknown or has not been validated yet. */ public function isValidated(Schema $schema, DocumentNode $ast): bool; From 60bffe6dd3d0ec0dabab6e57b8369912c44bb9d0 Mon Sep 17 00:00:00 2001 From: shmax Date: Mon, 14 Jul 2025 07:18:52 -0700 Subject: [PATCH 13/38] move to file --- src/Validator/ValidationCache.php | 1 - .../TestClasses/SpyValidationCacheAdapter.php | 26 +++++++++++++++++++ tests/Executor/ValidationWithCacheTest.php | 20 +------------- 3 files changed, 27 insertions(+), 20 deletions(-) create mode 100644 tests/Executor/TestClasses/SpyValidationCacheAdapter.php diff --git a/src/Validator/ValidationCache.php b/src/Validator/ValidationCache.php index 7e7de3c4b..939a16b16 100644 --- a/src/Validator/ValidationCache.php +++ b/src/Validator/ValidationCache.php @@ -2,7 +2,6 @@ namespace GraphQL\Validator; -use GraphQL\GraphQL; use GraphQL\Language\AST\DocumentNode; use GraphQL\Type\Schema; diff --git a/tests/Executor/TestClasses/SpyValidationCacheAdapter.php b/tests/Executor/TestClasses/SpyValidationCacheAdapter.php new file mode 100644 index 000000000..bf1daa10a --- /dev/null +++ b/tests/Executor/TestClasses/SpyValidationCacheAdapter.php @@ -0,0 +1,26 @@ +isValidatedCalls++; + return parent::isValidated($schema, $ast); + } + + public function markValidated(Schema $schema, DocumentNode $ast): void + { + $this->markValidatedCalls++; + parent::markValidated($schema, $ast); + } +} \ No newline at end of file diff --git a/tests/Executor/ValidationWithCacheTest.php b/tests/Executor/ValidationWithCacheTest.php index 8b8c1bad9..9d95f0cef 100644 --- a/tests/Executor/ValidationWithCacheTest.php +++ b/tests/Executor/ValidationWithCacheTest.php @@ -8,6 +8,7 @@ use GraphQL\Language\AST\DocumentNode; use GraphQL\Tests\Executor\TestClasses\Cat; use GraphQL\Tests\Executor\TestClasses\Dog; +use GraphQL\Tests\Executor\TestClasses\SpyValidationCacheAdapter; use GraphQL\Tests\PsrValidationCacheAdapter; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; @@ -17,25 +18,6 @@ use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Psr16Cache; -final class SpyValidationCacheAdapter extends PsrValidationCacheAdapter -{ - public int $isValidatedCalls = 0; - public int $markValidatedCalls = 0; - - public function isValidated(Schema $schema, DocumentNode $ast): bool - { - $this->isValidatedCalls++; - return parent::isValidated($schema, $ast); - } - - public function markValidated(Schema $schema, DocumentNode $ast): void - { - $this->markValidatedCalls++; - parent::markValidated($schema, $ast); - } -} - - final class ValidationWithCacheTest extends TestCase { use ArraySubsetAsserts; From 40c000bbd7c6cfcc16fe43fa4681cc13d1467531 Mon Sep 17 00:00:00 2001 From: shmax Date: Mon, 14 Jul 2025 07:21:26 -0700 Subject: [PATCH 14/38] use shorthand, formatting --- .../TestClasses/SpyValidationCacheAdapter.php | 2 ++ tests/Executor/ValidationWithCacheTest.php | 12 +++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/Executor/TestClasses/SpyValidationCacheAdapter.php b/tests/Executor/TestClasses/SpyValidationCacheAdapter.php index bf1daa10a..4683cba6e 100644 --- a/tests/Executor/TestClasses/SpyValidationCacheAdapter.php +++ b/tests/Executor/TestClasses/SpyValidationCacheAdapter.php @@ -15,12 +15,14 @@ final class SpyValidationCacheAdapter extends PsrValidationCacheAdapter public function isValidated(Schema $schema, DocumentNode $ast): bool { $this->isValidatedCalls++; + return parent::isValidated($schema, $ast); } public function markValidated(Schema $schema, DocumentNode $ast): void { $this->markValidatedCalls++; + parent::markValidated($schema, $ast); } } \ No newline at end of file diff --git a/tests/Executor/ValidationWithCacheTest.php b/tests/Executor/ValidationWithCacheTest.php index 9d95f0cef..d335dd41d 100644 --- a/tests/Executor/ValidationWithCacheTest.php +++ b/tests/Executor/ValidationWithCacheTest.php @@ -5,11 +5,9 @@ use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; use GraphQL\Deferred; use GraphQL\GraphQL; -use GraphQL\Language\AST\DocumentNode; use GraphQL\Tests\Executor\TestClasses\Cat; use GraphQL\Tests\Executor\TestClasses\Dog; use GraphQL\Tests\Executor\TestClasses\SpyValidationCacheAdapter; -use GraphQL\Tests\PsrValidationCacheAdapter; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; @@ -28,7 +26,7 @@ public function testIsValidationCachedWithAdapter(): void $petType = new InterfaceType([ 'name' => 'Pet', 'fields' => [ - 'name' => ['type' => Type::string()], + 'name' => Type::string(), ], ]); @@ -37,8 +35,8 @@ public function testIsValidationCachedWithAdapter(): void 'interfaces' => [$petType], 'isTypeOf' => static fn ($obj): Deferred => new Deferred(static fn (): bool => $obj instanceof Dog), 'fields' => [ - 'name' => ['type' => Type::string()], - 'woofs' => ['type' => Type::boolean()], + 'name' => Type::string(), + 'woofs' => Type::boolean(), ], ]); @@ -47,8 +45,8 @@ public function testIsValidationCachedWithAdapter(): void 'interfaces' => [$petType], 'isTypeOf' => static fn ($obj): Deferred => new Deferred(static fn (): bool => $obj instanceof Cat), 'fields' => [ - 'name' => ['type' => Type::string()], - 'meows' => ['type' => Type::boolean()], + 'name' => Type::string(), + 'meows' => Type::boolean(), ], ]); From 7d515fd3569619949d91fdc5560d8bf2343edc24 Mon Sep 17 00:00:00 2001 From: shmax Date: Mon, 14 Jul 2025 07:22:49 -0700 Subject: [PATCH 15/38] assert both results --- tests/Executor/ValidationWithCacheTest.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/Executor/ValidationWithCacheTest.php b/tests/Executor/ValidationWithCacheTest.php index d335dd41d..c6bfd2137 100644 --- a/tests/Executor/ValidationWithCacheTest.php +++ b/tests/Executor/ValidationWithCacheTest.php @@ -79,8 +79,9 @@ public function testIsValidationCachedWithAdapter(): void }'; // make the same call twice in a row. We'll then inspect the cache object to count calls - GraphQL::executeQuery( $schema, $query, null, null, null, null, null, null, $cache)->toArray(); - $result = GraphQL::executeQuery( $schema, $query, null, null, null, null, null, null, $cache)->toArray(); + $resultA = GraphQL::executeQuery( $schema, $query, null, null, null, null, null, null, $cache)->toArray(); + $resultB = GraphQL::executeQuery( $schema, $query, null, null, null, null, null, null, $cache)->toArray(); + // ✅ Assert that validation only happened once self::assertEquals(2, $cache->isValidatedCalls, 'Should check cache twice'); @@ -95,6 +96,7 @@ public function testIsValidationCachedWithAdapter(): void ], ]; - self::assertEquals($expected, $result); + self::assertEquals($expected, $resultA); + self::assertEquals($expected, $resultB); } } From c2a82a47ae63e94537ea55b16d0670a207c105c0 Mon Sep 17 00:00:00 2001 From: shmax Date: Mon, 14 Jul 2025 07:26:16 -0700 Subject: [PATCH 16/38] ignore specific error --- tests/PsrValidationCacheAdapter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PsrValidationCacheAdapter.php b/tests/PsrValidationCacheAdapter.php index e43b8db61..8bd647226 100644 --- a/tests/PsrValidationCacheAdapter.php +++ b/tests/PsrValidationCacheAdapter.php @@ -30,7 +30,7 @@ public function isValidated(Schema $schema, DocumentNode $ast): bool { try { $key = $this->buildKey($schema, $ast); - /** @phpstan-ignore-next-line */ + /** @phpstan-ignore missingType.checkedException */ return $this->cache->has($key); } catch (\Throwable $e) { return false; @@ -44,7 +44,7 @@ public function markValidated(Schema $schema, DocumentNode $ast): void { try { $key = $this->buildKey($schema, $ast); - /** @phpstan-ignore-next-line */ + /** @phpstan-ignore missingType.checkedException */ $this->cache->set($key, true, $this->ttl); } catch (\Throwable $e) { // ignore silently From 51d1087949ce4fb25a6bde9283727c2160776f27 Mon Sep 17 00:00:00 2001 From: shmax Date: Mon, 14 Jul 2025 19:22:04 -0700 Subject: [PATCH 17/38] documentation --- src/Validator/ValidationCache.php | 27 +++++++++++++++++++++------ tests/PsrValidationCacheAdapter.php | 6 +++++- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/Validator/ValidationCache.php b/src/Validator/ValidationCache.php index 939a16b16..f467450be 100644 --- a/src/Validator/ValidationCache.php +++ b/src/Validator/ValidationCache.php @@ -3,23 +3,38 @@ namespace GraphQL\Validator; use GraphQL\Language\AST\DocumentNode; +use GraphQL\Tests\PsrValidationCacheAdapter; use GraphQL\Type\Schema; /** - * Implement this interface and pass an instance to GraphQL::executeQuery to cache validation of ASTs. - * The details of how to compute any keys (or whether to validate at all) are left up to you. + * Implement this interface and pass an instance to GraphQL::executeQuery to enable caching of successful query validations. + * + * This can improve performance by skipping validation for known-good query and schema combinations. + * You are responsible for defining how cache keys are computed, and when validation should be skipped. + * + * @see PsrValidationCacheAdapter for a toy implementation. */ interface ValidationCache { /** - * Return true if the given schema + AST pair has previously been validated successfully. - * Only successful validations are cached. - * A return value of false means the pair is either unknown or has not been validated yet. + * Determine whether the given schema + AST pair has already been successfully validated. + * + * This method should return true if the query has previously passed validation for the provided schema. + * Only successful validations should be considered "cached" — failed validations are not cached. + * + * Note: This allows for optimizations in systems where validation may not be necessary on every request — + * for example, when using persisted queries that are known to be valid ahead of time. In such cases, you + * can implement this method to always return true. + * + * @return bool True if validation for the given schema + AST is already known to be valid; false otherwise. */ public function isValidated(Schema $schema, DocumentNode $ast): bool; /** - * Cache validation status for this schema/query. + * Mark the given schema + AST pair as successfully validated. + * + * This is typically called after a query passes validation. + * You should store enough information to recognize this combination on future requests. */ public function markValidated(Schema $schema, DocumentNode $ast): void; } \ No newline at end of file diff --git a/tests/PsrValidationCacheAdapter.php b/tests/PsrValidationCacheAdapter.php index 8bd647226..9ede8ff1e 100644 --- a/tests/PsrValidationCacheAdapter.php +++ b/tests/PsrValidationCacheAdapter.php @@ -59,7 +59,11 @@ public function markValidated(Schema $schema, DocumentNode $ast): void */ private function buildKey(Schema $schema, DocumentNode $ast): string { - // NOTE: You can override this strategy if you want to make schema fingerprinting cheaper + /** + * NOTE: This default strategy generates a cache key by hashing the printed schema and AST. + * You'll likely want to replace this with a more stable or efficient method for fingerprinting + * the schema -- for example, a build-time hash, schema version number, or an environment-based identifier. + */ $schemaHash = md5(SchemaPrinter::doPrint($schema)); $astHash = md5($ast->__toString()); From a745345e5e2726ab758f2e135bfab3fab22527d6 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 02:25:33 +0000 Subject: [PATCH 18/38] Autofix --- docs/class-reference.md | 9 ++++++--- src/Validator/ValidationCache.php | 4 ++-- .../TestClasses/SpyValidationCacheAdapter.php | 10 +++++----- tests/Executor/ValidationWithCacheTest.php | 5 ++--- tests/PsrValidationCacheAdapter.php | 13 ++++++------- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/docs/class-reference.md b/docs/class-reference.md index 5b79dba4e..3fd6e1434 100644 --- a/docs/class-reference.md +++ b/docs/class-reference.md @@ -70,7 +70,8 @@ static function executeQuery( ?array $variableValues = null, ?string $operationName = null, ?callable $fieldResolver = null, - ?array $validationRules = null + ?array $validationRules = null, + ?GraphQL\Validator\ValidationCache $cache = null ): GraphQL\Executor\ExecutionResult ``` @@ -98,7 +99,8 @@ static function promiseToExecute( ?array $variableValues = null, ?string $operationName = null, ?callable $fieldResolver = null, - ?array $validationRules = null + ?array $validationRules = null, + ?GraphQL\Validator\ValidationCache $cache = null ): GraphQL\Executor\Promise\Promise ``` @@ -1811,7 +1813,8 @@ static function validate( GraphQL\Type\Schema $schema, GraphQL\Language\AST\DocumentNode $ast, ?array $rules = null, - ?GraphQL\Utils\TypeInfo $typeInfo = null + ?GraphQL\Utils\TypeInfo $typeInfo = null, + ?GraphQL\Validator\ValidationCache $cache = null ): array ``` diff --git a/src/Validator/ValidationCache.php b/src/Validator/ValidationCache.php index f467450be..2f7019196 100644 --- a/src/Validator/ValidationCache.php +++ b/src/Validator/ValidationCache.php @@ -26,7 +26,7 @@ interface ValidationCache * for example, when using persisted queries that are known to be valid ahead of time. In such cases, you * can implement this method to always return true. * - * @return bool True if validation for the given schema + AST is already known to be valid; false otherwise. + * @return bool true if validation for the given schema + AST is already known to be valid; false otherwise */ public function isValidated(Schema $schema, DocumentNode $ast): bool; @@ -37,4 +37,4 @@ public function isValidated(Schema $schema, DocumentNode $ast): bool; * You should store enough information to recognize this combination on future requests. */ public function markValidated(Schema $schema, DocumentNode $ast): void; -} \ No newline at end of file +} diff --git a/tests/Executor/TestClasses/SpyValidationCacheAdapter.php b/tests/Executor/TestClasses/SpyValidationCacheAdapter.php index 4683cba6e..76bcee362 100644 --- a/tests/Executor/TestClasses/SpyValidationCacheAdapter.php +++ b/tests/Executor/TestClasses/SpyValidationCacheAdapter.php @@ -1,5 +1,4 @@ -isValidatedCalls++; + ++$this->isValidatedCalls; return parent::isValidated($schema, $ast); } public function markValidated(Schema $schema, DocumentNode $ast): void { - $this->markValidatedCalls++; + ++$this->markValidatedCalls; parent::markValidated($schema, $ast); } -} \ No newline at end of file +} diff --git a/tests/Executor/ValidationWithCacheTest.php b/tests/Executor/ValidationWithCacheTest.php index c6bfd2137..a69c2eadf 100644 --- a/tests/Executor/ValidationWithCacheTest.php +++ b/tests/Executor/ValidationWithCacheTest.php @@ -79,9 +79,8 @@ public function testIsValidationCachedWithAdapter(): void }'; // make the same call twice in a row. We'll then inspect the cache object to count calls - $resultA = GraphQL::executeQuery( $schema, $query, null, null, null, null, null, null, $cache)->toArray(); - $resultB = GraphQL::executeQuery( $schema, $query, null, null, null, null, null, null, $cache)->toArray(); - + $resultA = GraphQL::executeQuery($schema, $query, null, null, null, null, null, null, $cache)->toArray(); + $resultB = GraphQL::executeQuery($schema, $query, null, null, null, null, null, null, $cache)->toArray(); // ✅ Assert that validation only happened once self::assertEquals(2, $cache->isValidatedCalls, 'Should check cache twice'); diff --git a/tests/PsrValidationCacheAdapter.php b/tests/PsrValidationCacheAdapter.php index 9ede8ff1e..79ca80fb7 100644 --- a/tests/PsrValidationCacheAdapter.php +++ b/tests/PsrValidationCacheAdapter.php @@ -12,24 +12,25 @@ class PsrValidationCacheAdapter implements ValidationCache { private const KEY_PREFIX = 'gql_validation_'; + private int $ttl; + private CacheInterface $cache; public function __construct( - CacheInterface $cache, + CacheInterface $cache, int $ttlSeconds = 300 ) { $this->ttl = $ttlSeconds; $this->cache = $cache; } - /** - * @throws InvalidArgumentException - */ + /** @throws InvalidArgumentException */ public function isValidated(Schema $schema, DocumentNode $ast): bool { try { $key = $this->buildKey($schema, $ast); + /** @phpstan-ignore missingType.checkedException */ return $this->cache->has($key); } catch (\Throwable $e) { @@ -37,9 +38,7 @@ public function isValidated(Schema $schema, DocumentNode $ast): bool } } - /** - * @throws InvalidArgumentException - */ + /** @throws InvalidArgumentException */ public function markValidated(Schema $schema, DocumentNode $ast): void { try { From 1277bb3b1a5835ae7e7cdd8439024a97faefb78f Mon Sep 17 00:00:00 2001 From: shmax Date: Wed, 16 Jul 2025 07:13:28 -0700 Subject: [PATCH 19/38] use serialize --- tests/PsrValidationCacheAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PsrValidationCacheAdapter.php b/tests/PsrValidationCacheAdapter.php index 79ca80fb7..dfa9bdddb 100644 --- a/tests/PsrValidationCacheAdapter.php +++ b/tests/PsrValidationCacheAdapter.php @@ -64,7 +64,7 @@ private function buildKey(Schema $schema, DocumentNode $ast): string * the schema -- for example, a build-time hash, schema version number, or an environment-based identifier. */ $schemaHash = md5(SchemaPrinter::doPrint($schema)); - $astHash = md5($ast->__toString()); + $astHash = md5(serialize($ast)); return self::KEY_PREFIX . $schemaHash . '_' . $astHash; } From 59a0cd7c3706b1194ff6cb99fb2838b375943f1c Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Thu, 17 Jul 2025 09:25:13 +0200 Subject: [PATCH 20/38] simplify conditional --- src/Validator/DocumentValidator.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index 50037d58a..d0487147b 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -132,8 +132,7 @@ public static function validate( $errors = $context->getErrors(); - // Only cache clean results - if (isset($cache) && count($errors) === 0) { + if (isset($cache) && $errors === []) { $cache->markValidated($schema, $ast); } From 6679cb89e22479604ba3168514b0c979130371b2 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Thu, 17 Jul 2025 09:25:25 +0200 Subject: [PATCH 21/38] unify formatting --- src/Validator/DocumentValidator.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index d0487147b..f2d501071 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -285,7 +285,6 @@ public static function validateSDL( ?array $rules = null ): array { $rules ??= self::sdlRules(); - if ($rules === []) { return []; } From de245ff39548e6da6845043b4ad25d8aeec2afe1 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Thu, 17 Jul 2025 09:51:25 +0200 Subject: [PATCH 22/38] Polish tests and docs --- src/Validator/ValidationCache.php | 5 +- tests/Executor/ValidationWithCacheTest.php | 8 +-- tests/PsrValidationCacheAdapter.php | 62 ++++++++++++---------- 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/Validator/ValidationCache.php b/src/Validator/ValidationCache.php index 2f7019196..4075d442a 100644 --- a/src/Validator/ValidationCache.php +++ b/src/Validator/ValidationCache.php @@ -22,9 +22,8 @@ interface ValidationCache * This method should return true if the query has previously passed validation for the provided schema. * Only successful validations should be considered "cached" — failed validations are not cached. * - * Note: This allows for optimizations in systems where validation may not be necessary on every request — - * for example, when using persisted queries that are known to be valid ahead of time. In such cases, you - * can implement this method to always return true. + * This allows for optimizations in systems where validation may not be necessary on every request. + * For example, you can always return true for persisted queries that are known to be valid ahead of time. * * @return bool true if validation for the given schema + AST is already known to be valid; false otherwise */ diff --git a/tests/Executor/ValidationWithCacheTest.php b/tests/Executor/ValidationWithCacheTest.php index a69c2eadf..4b6d60b43 100644 --- a/tests/Executor/ValidationWithCacheTest.php +++ b/tests/Executor/ValidationWithCacheTest.php @@ -83,8 +83,8 @@ public function testIsValidationCachedWithAdapter(): void $resultB = GraphQL::executeQuery($schema, $query, null, null, null, null, null, null, $cache)->toArray(); // ✅ Assert that validation only happened once - self::assertEquals(2, $cache->isValidatedCalls, 'Should check cache twice'); - self::assertEquals(1, $cache->markValidatedCalls, 'Should mark as validated once'); + self::assertSame(2, $cache->isValidatedCalls, 'Should check cache twice'); + self::assertSame(1, $cache->markValidatedCalls, 'Should mark as validated once'); $expected = [ 'data' => [ @@ -95,7 +95,7 @@ public function testIsValidationCachedWithAdapter(): void ], ]; - self::assertEquals($expected, $resultA); - self::assertEquals($expected, $resultB); + self::assertSame($expected, $resultA); + self::assertSame($expected, $resultB); } } diff --git a/tests/PsrValidationCacheAdapter.php b/tests/PsrValidationCacheAdapter.php index dfa9bdddb..0ce0b6b11 100644 --- a/tests/PsrValidationCacheAdapter.php +++ b/tests/PsrValidationCacheAdapter.php @@ -2,70 +2,74 @@ namespace GraphQL\Tests; +use GraphQL\Error\Error; +use GraphQL\Error\InvariantViolation; +use GraphQL\Error\SerializationError; use GraphQL\Language\AST\DocumentNode; use GraphQL\Type\Schema; use GraphQL\Utils\SchemaPrinter; use GraphQL\Validator\ValidationCache; use Psr\SimpleCache\CacheInterface; -use Symfony\Component\String\Exception\InvalidArgumentException; +use Psr\SimpleCache\InvalidArgumentException; class PsrValidationCacheAdapter implements ValidationCache { - private const KEY_PREFIX = 'gql_validation_'; - - private int $ttl; - private CacheInterface $cache; + private int $ttlSeconds; + public function __construct( CacheInterface $cache, int $ttlSeconds = 300 ) { - $this->ttl = $ttlSeconds; $this->cache = $cache; + $this->ttlSeconds = $ttlSeconds; } - /** @throws InvalidArgumentException */ + /** + * @throws \JsonException + * @throws Error + * @throws InvalidArgumentException&\Throwable + * @throws InvariantViolation + * @throws SerializationError + */ public function isValidated(Schema $schema, DocumentNode $ast): bool { - try { - $key = $this->buildKey($schema, $ast); + $key = $this->buildKey($schema, $ast); - /** @phpstan-ignore missingType.checkedException */ - return $this->cache->has($key); - } catch (\Throwable $e) { - return false; - } + return $this->cache->has($key); // @phpstan-ignore missingType.checkedException (annotated as a union with Throwable) } - /** @throws InvalidArgumentException */ + /** + * @throws \JsonException + * @throws Error + * @throws InvalidArgumentException&\Throwable + * @throws InvariantViolation + * @throws SerializationError + */ public function markValidated(Schema $schema, DocumentNode $ast): void { - try { - $key = $this->buildKey($schema, $ast); - /** @phpstan-ignore missingType.checkedException */ - $this->cache->set($key, true, $this->ttl); - } catch (\Throwable $e) { - // ignore silently - } + $key = $this->buildKey($schema, $ast); + + $this->cache->set($key, true, $this->ttlSeconds); // @phpstan-ignore missingType.checkedException (annotated as a union with Throwable) } /** - * @throws \GraphQL\Error\Error - * @throws \GraphQL\Error\InvariantViolation - * @throws \GraphQL\Error\SerializationError * @throws \JsonException + * @throws Error + * @throws InvariantViolation + * @throws SerializationError */ private function buildKey(Schema $schema, DocumentNode $ast): string { /** - * NOTE: This default strategy generates a cache key by hashing the printed schema and AST. - * You'll likely want to replace this with a more stable or efficient method for fingerprinting - * the schema -- for example, a build-time hash, schema version number, or an environment-based identifier. + * This default strategy generates a cache key by hashing the printed schema and AST. + * You'll likely want to replace this with a more stable or efficient method for fingerprinting the schema. + * For example, you may use a build-time hash, schema version number, or an environment-based identifier. */ $schemaHash = md5(SchemaPrinter::doPrint($schema)); $astHash = md5(serialize($ast)); - return self::KEY_PREFIX . $schemaHash . '_' . $astHash; + return "graphql_validation_{$schemaHash}_{$astHash}"; } } From d166e4139f1933d8657ff56809d88cee238ec30e Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Thu, 17 Jul 2025 09:56:58 +0200 Subject: [PATCH 23/38] Clean up types --- src/Server/ServerConfig.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Server/ServerConfig.php b/src/Server/ServerConfig.php index 08d7517ee..5ffdb600c 100644 --- a/src/Server/ServerConfig.php +++ b/src/Server/ServerConfig.php @@ -124,7 +124,7 @@ public static function create(array $config = []): self private bool $queryBatching = false; /** - * @var array|callable|null + * @var array|callable|null * * @phpstan-var ValidationRulesOption */ @@ -315,7 +315,7 @@ public function getPromiseAdapter(): ?PromiseAdapter } /** - * @return array|callable|null + * @return array|callable|null * * @phpstan-return ValidationRulesOption */ From 4b3aafedec28fc19f2584e296ee0848e92ec687f Mon Sep 17 00:00:00 2001 From: shmax Date: Thu, 17 Jul 2025 19:17:15 -0700 Subject: [PATCH 24/38] include rules in the fun --- src/Validator/DocumentValidator.php | 2 +- src/Validator/ValidationCache.php | 13 +++++++++---- .../TestClasses/SpyValidationCacheAdapter.php | 4 ++-- tests/PsrValidationCacheAdapter.php | 16 ++++++++++++---- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index f2d501071..144c7e1f1 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -103,7 +103,7 @@ public static function validate( ?ValidationCache $cache = null ): array { if (isset($cache)) { - $cached = $cache->isValidated($schema, $ast); + $cached = $cache->isValidated($schema, $ast, $rules); if ($cached) { return []; } diff --git a/src/Validator/ValidationCache.php b/src/Validator/ValidationCache.php index 4075d442a..334ce6ec0 100644 --- a/src/Validator/ValidationCache.php +++ b/src/Validator/ValidationCache.php @@ -5,6 +5,7 @@ use GraphQL\Language\AST\DocumentNode; use GraphQL\Tests\PsrValidationCacheAdapter; use GraphQL\Type\Schema; +use GraphQL\Validator\Rules\ValidationRule; /** * Implement this interface and pass an instance to GraphQL::executeQuery to enable caching of successful query validations. @@ -17,7 +18,7 @@ interface ValidationCache { /** - * Determine whether the given schema + AST pair has already been successfully validated. + * Determine whether the given schema/AST/rules set has already been successfully validated. * * This method should return true if the query has previously passed validation for the provided schema. * Only successful validations should be considered "cached" — failed validations are not cached. @@ -25,15 +26,19 @@ interface ValidationCache * This allows for optimizations in systems where validation may not be necessary on every request. * For example, you can always return true for persisted queries that are known to be valid ahead of time. * - * @return bool true if validation for the given schema + AST is already known to be valid; false otherwise + * @param array|null $rules + * + * @return bool true if validation for the given schema + AST + rules is already known to be valid; false otherwise */ - public function isValidated(Schema $schema, DocumentNode $ast): bool; + public function isValidated(Schema $schema, DocumentNode $ast, array $rules = null): bool; /** + * @param array|null $rules + * * Mark the given schema + AST pair as successfully validated. * * This is typically called after a query passes validation. * You should store enough information to recognize this combination on future requests. */ - public function markValidated(Schema $schema, DocumentNode $ast): void; + public function markValidated(Schema $schema, DocumentNode $ast, array $rules = null): void; } diff --git a/tests/Executor/TestClasses/SpyValidationCacheAdapter.php b/tests/Executor/TestClasses/SpyValidationCacheAdapter.php index 76bcee362..6e2c04fee 100644 --- a/tests/Executor/TestClasses/SpyValidationCacheAdapter.php +++ b/tests/Executor/TestClasses/SpyValidationCacheAdapter.php @@ -12,14 +12,14 @@ final class SpyValidationCacheAdapter extends PsrValidationCacheAdapter public int $markValidatedCalls = 0; - public function isValidated(Schema $schema, DocumentNode $ast): bool + public function isValidated(Schema $schema, DocumentNode $ast, array $rules = null): bool { ++$this->isValidatedCalls; return parent::isValidated($schema, $ast); } - public function markValidated(Schema $schema, DocumentNode $ast): void + public function markValidated(Schema $schema, DocumentNode $ast, array $rules = null): void { ++$this->markValidatedCalls; diff --git a/tests/PsrValidationCacheAdapter.php b/tests/PsrValidationCacheAdapter.php index 0ce0b6b11..f81dbea34 100644 --- a/tests/PsrValidationCacheAdapter.php +++ b/tests/PsrValidationCacheAdapter.php @@ -8,6 +8,7 @@ use GraphQL\Language\AST\DocumentNode; use GraphQL\Type\Schema; use GraphQL\Utils\SchemaPrinter; +use GraphQL\Validator\Rules\ValidationRule; use GraphQL\Validator\ValidationCache; use Psr\SimpleCache\CacheInterface; use Psr\SimpleCache\InvalidArgumentException; @@ -27,13 +28,15 @@ public function __construct( } /** + * @param array|null $rules + * * @throws \JsonException * @throws Error * @throws InvalidArgumentException&\Throwable * @throws InvariantViolation * @throws SerializationError */ - public function isValidated(Schema $schema, DocumentNode $ast): bool + public function isValidated(Schema $schema, DocumentNode $ast, array $rules = null): bool { $key = $this->buildKey($schema, $ast); @@ -41,13 +44,15 @@ public function isValidated(Schema $schema, DocumentNode $ast): bool } /** + * @param array|null $rules + * * @throws \JsonException * @throws Error * @throws InvalidArgumentException&\Throwable * @throws InvariantViolation * @throws SerializationError */ - public function markValidated(Schema $schema, DocumentNode $ast): void + public function markValidated(Schema $schema, DocumentNode $ast, array $rules = null): void { $key = $this->buildKey($schema, $ast); @@ -55,12 +60,14 @@ public function markValidated(Schema $schema, DocumentNode $ast): void } /** + * @param array|null $rules + * * @throws \JsonException * @throws Error * @throws InvariantViolation * @throws SerializationError */ - private function buildKey(Schema $schema, DocumentNode $ast): string + private function buildKey(Schema $schema, DocumentNode $ast, array $rules = null): string { /** * This default strategy generates a cache key by hashing the printed schema and AST. @@ -69,7 +76,8 @@ private function buildKey(Schema $schema, DocumentNode $ast): string */ $schemaHash = md5(SchemaPrinter::doPrint($schema)); $astHash = md5(serialize($ast)); + $rulesHash = md5(serialize($rules)); - return "graphql_validation_{$schemaHash}_{$astHash}"; + return "graphql_validation_{$schemaHash}_{$astHash}_$rulesHash"; } } From 9683c022ee0817db9ef8a60f4e0d89a1f9506947 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 02:18:14 +0000 Subject: [PATCH 25/38] Autofix --- src/Validator/ValidationCache.php | 4 ++-- tests/Executor/TestClasses/SpyValidationCacheAdapter.php | 4 ++-- tests/PsrValidationCacheAdapter.php | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Validator/ValidationCache.php b/src/Validator/ValidationCache.php index 334ce6ec0..30d5839db 100644 --- a/src/Validator/ValidationCache.php +++ b/src/Validator/ValidationCache.php @@ -30,7 +30,7 @@ interface ValidationCache * * @return bool true if validation for the given schema + AST + rules is already known to be valid; false otherwise */ - public function isValidated(Schema $schema, DocumentNode $ast, array $rules = null): bool; + public function isValidated(Schema $schema, DocumentNode $ast, ?array $rules = null): bool; /** * @param array|null $rules @@ -40,5 +40,5 @@ public function isValidated(Schema $schema, DocumentNode $ast, array $rules = nu * This is typically called after a query passes validation. * You should store enough information to recognize this combination on future requests. */ - public function markValidated(Schema $schema, DocumentNode $ast, array $rules = null): void; + public function markValidated(Schema $schema, DocumentNode $ast, ?array $rules = null): void; } diff --git a/tests/Executor/TestClasses/SpyValidationCacheAdapter.php b/tests/Executor/TestClasses/SpyValidationCacheAdapter.php index 6e2c04fee..655676138 100644 --- a/tests/Executor/TestClasses/SpyValidationCacheAdapter.php +++ b/tests/Executor/TestClasses/SpyValidationCacheAdapter.php @@ -12,14 +12,14 @@ final class SpyValidationCacheAdapter extends PsrValidationCacheAdapter public int $markValidatedCalls = 0; - public function isValidated(Schema $schema, DocumentNode $ast, array $rules = null): bool + public function isValidated(Schema $schema, DocumentNode $ast, ?array $rules = null): bool { ++$this->isValidatedCalls; return parent::isValidated($schema, $ast); } - public function markValidated(Schema $schema, DocumentNode $ast, array $rules = null): void + public function markValidated(Schema $schema, DocumentNode $ast, ?array $rules = null): void { ++$this->markValidatedCalls; diff --git a/tests/PsrValidationCacheAdapter.php b/tests/PsrValidationCacheAdapter.php index f81dbea34..0b35893de 100644 --- a/tests/PsrValidationCacheAdapter.php +++ b/tests/PsrValidationCacheAdapter.php @@ -36,7 +36,7 @@ public function __construct( * @throws InvariantViolation * @throws SerializationError */ - public function isValidated(Schema $schema, DocumentNode $ast, array $rules = null): bool + public function isValidated(Schema $schema, DocumentNode $ast, ?array $rules = null): bool { $key = $this->buildKey($schema, $ast); @@ -52,7 +52,7 @@ public function isValidated(Schema $schema, DocumentNode $ast, array $rules = nu * @throws InvariantViolation * @throws SerializationError */ - public function markValidated(Schema $schema, DocumentNode $ast, array $rules = null): void + public function markValidated(Schema $schema, DocumentNode $ast, ?array $rules = null): void { $key = $this->buildKey($schema, $ast); @@ -67,7 +67,7 @@ public function markValidated(Schema $schema, DocumentNode $ast, array $rules = * @throws InvariantViolation * @throws SerializationError */ - private function buildKey(Schema $schema, DocumentNode $ast, array $rules = null): string + private function buildKey(Schema $schema, DocumentNode $ast, ?array $rules = null): string { /** * This default strategy generates a cache key by hashing the printed schema and AST. @@ -78,6 +78,6 @@ private function buildKey(Schema $schema, DocumentNode $ast, array $rules = null $astHash = md5(serialize($ast)); $rulesHash = md5(serialize($rules)); - return "graphql_validation_{$schemaHash}_{$astHash}_$rulesHash"; + return "graphql_validation_{$schemaHash}_{$astHash}_{$rulesHash}"; } } From 02013c3bbd8488a8215ee0b94c07be9160c5956d Mon Sep 17 00:00:00 2001 From: shmax Date: Thu, 17 Jul 2025 19:42:51 -0700 Subject: [PATCH 26/38] update comments --- src/Validator/ValidationCache.php | 6 +++--- tests/PsrValidationCacheAdapter.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Validator/ValidationCache.php b/src/Validator/ValidationCache.php index 334ce6ec0..ea352e932 100644 --- a/src/Validator/ValidationCache.php +++ b/src/Validator/ValidationCache.php @@ -10,8 +10,8 @@ /** * Implement this interface and pass an instance to GraphQL::executeQuery to enable caching of successful query validations. * - * This can improve performance by skipping validation for known-good query and schema combinations. - * You are responsible for defining how cache keys are computed, and when validation should be skipped. + * This can improve performance by skipping validation for known-good query, schema, and rules combinations. + * You are responsible for defining how cache keys are computed. * * @see PsrValidationCacheAdapter for a toy implementation. */ @@ -35,7 +35,7 @@ public function isValidated(Schema $schema, DocumentNode $ast, array $rules = nu /** * @param array|null $rules * - * Mark the given schema + AST pair as successfully validated. + * Mark the given schema/AST/rules set as successfully validated. * * This is typically called after a query passes validation. * You should store enough information to recognize this combination on future requests. diff --git a/tests/PsrValidationCacheAdapter.php b/tests/PsrValidationCacheAdapter.php index f81dbea34..cedc38838 100644 --- a/tests/PsrValidationCacheAdapter.php +++ b/tests/PsrValidationCacheAdapter.php @@ -70,7 +70,7 @@ public function markValidated(Schema $schema, DocumentNode $ast, array $rules = private function buildKey(Schema $schema, DocumentNode $ast, array $rules = null): string { /** - * This default strategy generates a cache key by hashing the printed schema and AST. + * This default strategy generates a cache key by hashing the printed schema, AST, and any custom rules. * You'll likely want to replace this with a more stable or efficient method for fingerprinting the schema. * For example, you may use a build-time hash, schema version number, or an environment-based identifier. */ From 127b68fc31f3716caf681154a14b6baa6e911e87 Mon Sep 17 00:00:00 2001 From: shmax Date: Thu, 17 Jul 2025 21:44:00 -0700 Subject: [PATCH 27/38] remove comment --- src/Validator/ValidationCache.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Validator/ValidationCache.php b/src/Validator/ValidationCache.php index 4adbb41ce..63a9f224e 100644 --- a/src/Validator/ValidationCache.php +++ b/src/Validator/ValidationCache.php @@ -23,9 +23,6 @@ interface ValidationCache * This method should return true if the query has previously passed validation for the provided schema. * Only successful validations should be considered "cached" — failed validations are not cached. * - * This allows for optimizations in systems where validation may not be necessary on every request. - * For example, you can always return true for persisted queries that are known to be valid ahead of time. - * * @param array|null $rules * * @return bool true if validation for the given schema + AST + rules is already known to be valid; false otherwise From b4865e9974dd9b2f05952070a9ee0b653e7c4c20 Mon Sep 17 00:00:00 2001 From: shmax Date: Tue, 22 Jul 2025 18:55:05 -0700 Subject: [PATCH 28/38] add comments about keys --- src/Validator/ValidationCache.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Validator/ValidationCache.php b/src/Validator/ValidationCache.php index 63a9f224e..1a35275d7 100644 --- a/src/Validator/ValidationCache.php +++ b/src/Validator/ValidationCache.php @@ -13,6 +13,14 @@ * This can improve performance by skipping validation for known-good query, schema, and rules combinations. * You are responsible for defining how cache keys are computed. * + * Some things to keep in mind when generating keys: + * - PHP's `serialize` method is fast, but can't handle certain structures such as closures. + * - If your `schema` does include closures or is quite large and complex, consider + * using some kind of build-time version number or other environment variable. + * - Keep in mind that there are internal `rules` that are applied in addition to any you pass in, + * and it's possible these may shift or expand as the library evolves, so it might make sense + * to include the library version number in your keys. + * * @see PsrValidationCacheAdapter for a toy implementation. */ interface ValidationCache From 4ec12df60dd51b918829c914b5caebd6b6c8e5ce Mon Sep 17 00:00:00 2001 From: shmax Date: Tue, 22 Jul 2025 19:06:41 -0700 Subject: [PATCH 29/38] pass rules to markValidated --- src/Validator/DocumentValidator.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index 144c7e1f1..785743e90 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -103,14 +103,13 @@ public static function validate( ?ValidationCache $cache = null ): array { if (isset($cache)) { - $cached = $cache->isValidated($schema, $ast, $rules); - if ($cached) { + if ($cache->isValidated($schema, $ast, $rules)) { return []; } } - $rules ??= static::allRules(); - if ($rules === []) { + $finalRules = $rules ?? static::allRules(); + if ($finalRules === []) { return []; } @@ -118,7 +117,7 @@ public static function validate( $context = new QueryValidationContext($schema, $ast, $typeInfo); $visitors = []; - foreach ($rules as $rule) { + foreach ($finalRules as $rule) { $visitors[] = $rule->getVisitor($context); } @@ -133,7 +132,7 @@ public static function validate( $errors = $context->getErrors(); if (isset($cache) && $errors === []) { - $cache->markValidated($schema, $ast); + $cache->markValidated($schema, $ast, $rules); } return $errors; From 707f2571ce52346e0767a9c3301f8f81e26ddee0 Mon Sep 17 00:00:00 2001 From: shmax Date: Tue, 22 Jul 2025 20:07:21 -0700 Subject: [PATCH 30/38] add docs --- docs/executing-queries.md | 98 +++++++++++++++++++++++++++++++ src/Validator/ValidationCache.php | 8 +-- 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/docs/executing-queries.md b/docs/executing-queries.md index 7c5ad5282..a56de9cef 100644 --- a/docs/executing-queries.md +++ b/docs/executing-queries.md @@ -210,3 +210,101 @@ $server = new StandardServer([ 'validationRules' => $myValidationRules ]); ``` +## Validation Caching + +Validation is a required step in GraphQL execution, but it can become a performance bottleneck when the same queries are +run repeatedly — especially in production environments where queries are often static or pre-generated (e.g., persisted +queries or queries emitted by client libraries). + +To optimize for this, graphql-php supports pluggable validation caching. By implementing the GraphQL\Validator\ValidationCache +interface and passing it to GraphQL::executeQuery(), you can skip validation for queries already known to be valid: + +To optimize for this, `graphql-php` supports pluggable validation caching. By implementing the `GraphQL\Validator\ValidationCache` interface and passing it to +`GraphQL::executeQuery()`, you can skip validation for queries that are already known to be valid. + +```php +use GraphQL\Validator\ValidationCache; +use GraphQL\GraphQL; +use GraphQL\Tests\PsrValidationCacheAdapter; + +$validationCache = new PsrValidationCacheAdapter(); + +$result = GraphQL::executeQuery( + $schema, + $queryString, + $rootValue, + $context, + $variableValues, + $operationName, + $fieldResolver, + $validationRules, + $validationCache +); +``` + +### Key Generation Tips +You are responsible for generating your own cache keys in a way that uniquely identifies the schema, the query, and +(optionally) any custom validation rules. Here are some tips: + +* Hash your schema once at build time and store the result in an environment variable or constant. +* Avoid using serialize() on schema objects — closures and internal references may cause errors. +* If using custom validation rules, be sure to account for them in your key (e.g., by serializing or listing their class names). +* Consider including the graphql-php version number to account for internal rule changes across versions. + +### Sample Implementation +```php +use GraphQL\Validator\ValidationCache; +use GraphQL\Language\AST\DocumentNode; +use GraphQL\Type\Schema; +use GraphQL\Utils\SchemaPrinter; +use Psr\SimpleCache\CacheInterface; +use Composer\InstalledVersions; + +/** + * Reference implementation of ValidationCache using PSR-16 cache. + * + * @see GraphQl\Tests\PsrValidationCacheAdapter + */ +class PsrValidationCacheAdapter implements ValidationCache +{ + private CacheInterface $cache; + + private int $ttlSeconds; + + public function __construct( + CacheInterface $cache, + int $ttlSeconds = 300 + ) { + $this->cache = $cache; + $this->ttlSeconds = $ttlSeconds; + } + + public function isValidated(Schema $schema, DocumentNode $ast, ?array $rules = null): bool + { + $key = $this->buildKey($schema, $ast); + return $this->cache->has($key); + } + + public function markValidated(Schema $schema, DocumentNode $ast, ?array $rules = null): void + { + $key = $this->buildKey($schema, $ast); + $this->cache->set($key, true, $this->ttlSeconds); + } + + private function buildKey(Schema $schema, DocumentNode $ast, ?array $rules = null): string + { + // Use a stable hash for schema. In production, prefer a build-time constant: + // $schemaHash = $_ENV['SCHEMA_VERSION'] ?? 'v1'; + $schemaHash = md5(SchemaPrinter::doPrint($schema)); + + // Serialize AST and rules — both are predictable and safe in this context + $astHash = md5(serialize($ast)); + $rulesHash = md5(serialize($rules)); + + // Include graphql-php version to account for internal changes + $libraryVersion = \Composer\InstalledVersions::getVersion('webonyx/graphql-php') ?: 'unknown'; + + return "graphql_validation_{$libraryVersion}_{$schemaHash}_{$astHash}_{$rulesHash}"; + } +} +``` \ No newline at end of file diff --git a/src/Validator/ValidationCache.php b/src/Validator/ValidationCache.php index 1a35275d7..6073a1672 100644 --- a/src/Validator/ValidationCache.php +++ b/src/Validator/ValidationCache.php @@ -10,18 +10,18 @@ /** * Implement this interface and pass an instance to GraphQL::executeQuery to enable caching of successful query validations. * - * This can improve performance by skipping validation for known-good query, schema, and rules combinations. + * This can improve performance by skipping validation for known-good combinations of query, schema, and rules. * You are responsible for defining how cache keys are computed. * * Some things to keep in mind when generating keys: * - PHP's `serialize` method is fast, but can't handle certain structures such as closures. - * - If your `schema` does include closures or is quite large and complex, consider - * using some kind of build-time version number or other environment variable. + * - If your `schema` includes closures or is too large or complex to serialize, + * consider using a build-time version number or environment-based fingerprint instead. * - Keep in mind that there are internal `rules` that are applied in addition to any you pass in, * and it's possible these may shift or expand as the library evolves, so it might make sense * to include the library version number in your keys. * - * @see PsrValidationCacheAdapter for a toy implementation. + * @see PsrValidationCacheAdapter for a simple reference implementation. */ interface ValidationCache { From 4c3d419e303a00549af0541c8e8fa98bd27a2331 Mon Sep 17 00:00:00 2001 From: shmax Date: Tue, 22 Jul 2025 20:15:54 -0700 Subject: [PATCH 31/38] formatting --- docs/executing-queries.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/executing-queries.md b/docs/executing-queries.md index a56de9cef..12203a28f 100644 --- a/docs/executing-queries.md +++ b/docs/executing-queries.md @@ -216,8 +216,8 @@ Validation is a required step in GraphQL execution, but it can become a performa run repeatedly — especially in production environments where queries are often static or pre-generated (e.g., persisted queries or queries emitted by client libraries). -To optimize for this, graphql-php supports pluggable validation caching. By implementing the GraphQL\Validator\ValidationCache -interface and passing it to GraphQL::executeQuery(), you can skip validation for queries already known to be valid: +To optimize for this, graphql-php supports pluggable validation caching. By implementing the `GraphQL\Validator\ValidationCache` +interface and passing it to `GraphQL::executeQuery()`, you can skip validation for queries already known to be valid: To optimize for this, `graphql-php` supports pluggable validation caching. By implementing the `GraphQL\Validator\ValidationCache` interface and passing it to `GraphQL::executeQuery()`, you can skip validation for queries that are already known to be valid. From b5fdd3e6dca84ef76c50f759c7d57a964390ef5a Mon Sep 17 00:00:00 2001 From: shmax Date: Tue, 22 Jul 2025 20:16:45 -0700 Subject: [PATCH 32/38] remove redundant --- docs/executing-queries.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/executing-queries.md b/docs/executing-queries.md index 12203a28f..5811a9f64 100644 --- a/docs/executing-queries.md +++ b/docs/executing-queries.md @@ -216,9 +216,6 @@ Validation is a required step in GraphQL execution, but it can become a performa run repeatedly — especially in production environments where queries are often static or pre-generated (e.g., persisted queries or queries emitted by client libraries). -To optimize for this, graphql-php supports pluggable validation caching. By implementing the `GraphQL\Validator\ValidationCache` -interface and passing it to `GraphQL::executeQuery()`, you can skip validation for queries already known to be valid: - To optimize for this, `graphql-php` supports pluggable validation caching. By implementing the `GraphQL\Validator\ValidationCache` interface and passing it to `GraphQL::executeQuery()`, you can skip validation for queries that are already known to be valid. From 5d9e714cc77d4e2fa92fed4d4f6c52cbc29593b8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 03:17:40 +0000 Subject: [PATCH 33/38] Autofix --- docs/executing-queries.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/executing-queries.md b/docs/executing-queries.md index 5811a9f64..c5d9fd495 100644 --- a/docs/executing-queries.md +++ b/docs/executing-queries.md @@ -210,13 +210,14 @@ $server = new StandardServer([ 'validationRules' => $myValidationRules ]); ``` + ## Validation Caching -Validation is a required step in GraphQL execution, but it can become a performance bottleneck when the same queries are -run repeatedly — especially in production environments where queries are often static or pre-generated (e.g., persisted +Validation is a required step in GraphQL execution, but it can become a performance bottleneck when the same queries are +run repeatedly — especially in production environments where queries are often static or pre-generated (e.g., persisted queries or queries emitted by client libraries). -To optimize for this, `graphql-php` supports pluggable validation caching. By implementing the `GraphQL\Validator\ValidationCache` interface and passing it to +To optimize for this, `graphql-php` supports pluggable validation caching. By implementing the `GraphQL\Validator\ValidationCache` interface and passing it to `GraphQL::executeQuery()`, you can skip validation for queries that are already known to be valid. ```php @@ -240,15 +241,17 @@ $result = GraphQL::executeQuery( ``` ### Key Generation Tips + You are responsible for generating your own cache keys in a way that uniquely identifies the schema, the query, and (optionally) any custom validation rules. Here are some tips: -* Hash your schema once at build time and store the result in an environment variable or constant. -* Avoid using serialize() on schema objects — closures and internal references may cause errors. -* If using custom validation rules, be sure to account for them in your key (e.g., by serializing or listing their class names). -* Consider including the graphql-php version number to account for internal rule changes across versions. +- Hash your schema once at build time and store the result in an environment variable or constant. +- Avoid using serialize() on schema objects — closures and internal references may cause errors. +- If using custom validation rules, be sure to account for them in your key (e.g., by serializing or listing their class names). +- Consider including the graphql-php version number to account for internal rule changes across versions. ### Sample Implementation + ```php use GraphQL\Validator\ValidationCache; use GraphQL\Language\AST\DocumentNode; @@ -259,7 +262,7 @@ use Composer\InstalledVersions; /** * Reference implementation of ValidationCache using PSR-16 cache. - * + * * @see GraphQl\Tests\PsrValidationCacheAdapter */ class PsrValidationCacheAdapter implements ValidationCache @@ -300,8 +303,8 @@ class PsrValidationCacheAdapter implements ValidationCache // Include graphql-php version to account for internal changes $libraryVersion = \Composer\InstalledVersions::getVersion('webonyx/graphql-php') ?: 'unknown'; - + return "graphql_validation_{$libraryVersion}_{$schemaHash}_{$astHash}_{$rulesHash}"; } } -``` \ No newline at end of file +``` From b8c62dc9acf25793cbb1c2ecfd4623dbfe67d535 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Wed, 23 Jul 2025 09:50:46 +0200 Subject: [PATCH 34/38] Improve docs --- docs/executing-queries.md | 67 +++++++++++++++++++++++-------- src/Validator/ValidationCache.php | 10 ++--- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/docs/executing-queries.md b/docs/executing-queries.md index c5d9fd495..63ffaad7c 100644 --- a/docs/executing-queries.md +++ b/docs/executing-queries.md @@ -213,12 +213,12 @@ $server = new StandardServer([ ## Validation Caching -Validation is a required step in GraphQL execution, but it can become a performance bottleneck when the same queries are -run repeatedly — especially in production environments where queries are often static or pre-generated (e.g., persisted -queries or queries emitted by client libraries). +Validation is a required step in GraphQL execution, but it can become a performance bottleneck. +In production environments, queries are often static or pre-generated (e.g. persisted queries or queries emitted by client libraries). +This means that many queries will be identical and their validation results can be reused. -To optimize for this, `graphql-php` supports pluggable validation caching. By implementing the `GraphQL\Validator\ValidationCache` interface and passing it to -`GraphQL::executeQuery()`, you can skip validation for queries that are already known to be valid. +To optimize for this, `graphql-php` allows skipping validation for known valid queries. +Leverage pluggable validation caching by passing an implementation of the `GraphQL\Validator\ValidationCache` interface to `GraphQL::executeQuery()`: ```php use GraphQL\Validator\ValidationCache; @@ -242,13 +242,20 @@ $result = GraphQL::executeQuery( ### Key Generation Tips -You are responsible for generating your own cache keys in a way that uniquely identifies the schema, the query, and -(optionally) any custom validation rules. Here are some tips: - -- Hash your schema once at build time and store the result in an environment variable or constant. -- Avoid using serialize() on schema objects — closures and internal references may cause errors. -- If using custom validation rules, be sure to account for them in your key (e.g., by serializing or listing their class names). -- Consider including the graphql-php version number to account for internal rule changes across versions. +You are responsible for generating cache keys that are unique and dependent on the following inputs: +- the client-given query +- the current schema +- the passed validation rules and their implementation +- the implementation of `graphql-php` + +Here are some tips: +- Using `serialize()` directly on the schema object may error due to closures or circular references. + Instead, use `GraphQL\Utils\SchemaPrinter::doPrint($schema)` to get a stable string representation of the schema. +- If using custom validation rules, be sure to account for them in your key (e.g., by serializing or listing their class names and versioning them). +- Include the version number of the `webonyx/graphql-php` package to account for implementation changes in the library. +- Use a stable hash function like `md5()` or `sha256()` to generate the key from the schema, AST, and rules. +- Improve performance even further by hashing inputs known before deploying such as the schema or the installed package version. + You may store the hash in an environment variable or a constant to avoid recalculating it on every request. ### Sample Implementation @@ -293,18 +300,44 @@ class PsrValidationCacheAdapter implements ValidationCache private function buildKey(Schema $schema, DocumentNode $ast, ?array $rules = null): string { - // Use a stable hash for schema. In production, prefer a build-time constant: - // $schemaHash = $_ENV['SCHEMA_VERSION'] ?? 'v1'; + // Include package version to account for implementation changes + $libraryVersion = \Composer\InstalledVersions::getVersion('webonyx/graphql-php') + ?? throw new \RuntimeException('webonyx/graphql-php version not found. Ensure the package is installed.'); + + // Use a stable hash for the schema $schemaHash = md5(SchemaPrinter::doPrint($schema)); // Serialize AST and rules — both are predictable and safe in this context $astHash = md5(serialize($ast)); $rulesHash = md5(serialize($rules)); - // Include graphql-php version to account for internal changes - $libraryVersion = \Composer\InstalledVersions::getVersion('webonyx/graphql-php') ?: 'unknown'; - return "graphql_validation_{$libraryVersion}_{$schemaHash}_{$astHash}_{$rulesHash}"; } } ``` + +An optimized version of `buildKey` might leverage a key prefix for inputs known before deployment. +For example, you may run the following once during deployment and save the output in an environment variable `GRAPHQL_VALIDATION_KEY_PREFIX`: + +```php +$libraryVersion = \Composer\InstalledVersions::getVersion('webonyx/graphql-php') + ?? throw new \RuntimeException('webonyx/graphql-php version not found. Ensure the package is installed.'); + +$schemaHash = md5(SchemaPrinter::doPrint($schema)); + +echo "{$libraryVersion}_{$schemaHash}"; +``` + +Then use the environment variable in your key generation: + +```php + private function buildKey(Schema $schema, DocumentNode $ast, ?array $rules = null): string + { + $keyPrefix = getenv('GRAPHQL_VALIDATION_KEY_PREFIX') + ?? throw new \RuntimeException('Environment variable GRAPHQL_VALIDATION_KEY_PREFIX is not set.'); + $astHash = md5(serialize($ast)); + $rulesHash = md5(serialize($rules)); + + return "graphql_validation_{$keyPrefix}_{$astHash}_{$rulesHash}"; + } +``` diff --git a/src/Validator/ValidationCache.php b/src/Validator/ValidationCache.php index 6073a1672..3abc66189 100644 --- a/src/Validator/ValidationCache.php +++ b/src/Validator/ValidationCache.php @@ -14,12 +14,12 @@ * You are responsible for defining how cache keys are computed. * * Some things to keep in mind when generating keys: - * - PHP's `serialize` method is fast, but can't handle certain structures such as closures. - * - If your `schema` includes closures or is too large or complex to serialize, + * - PHP's `serialize()` function is fast, but can't handle certain structures such as closures. + * - If your `$schema` includes closures or is too large or complex to serialize, * consider using a build-time version number or environment-based fingerprint instead. - * - Keep in mind that there are internal `rules` that are applied in addition to any you pass in, - * and it's possible these may shift or expand as the library evolves, so it might make sense - * to include the library version number in your keys. + * - Keep in mind that there are internal `$rules` that are applied in addition to any you pass in, + * and it's possible these may shift or expand as the library evolves, + * so it might make sense to include the library version number in your keys. * * @see PsrValidationCacheAdapter for a simple reference implementation. */ From 7a2b3dcbe3c487ab0fc8b2101e5cf74a65762780 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 07:51:41 +0000 Subject: [PATCH 35/38] Autofix --- docs/executing-queries.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/executing-queries.md b/docs/executing-queries.md index 63ffaad7c..0f906e95f 100644 --- a/docs/executing-queries.md +++ b/docs/executing-queries.md @@ -243,12 +243,14 @@ $result = GraphQL::executeQuery( ### Key Generation Tips You are responsible for generating cache keys that are unique and dependent on the following inputs: + - the client-given query - the current schema - the passed validation rules and their implementation - the implementation of `graphql-php` Here are some tips: + - Using `serialize()` directly on the schema object may error due to closures or circular references. Instead, use `GraphQL\Utils\SchemaPrinter::doPrint($schema)` to get a stable string representation of the schema. - If using custom validation rules, be sure to account for them in your key (e.g., by serializing or listing their class names and versioning them). From e8fedcc4d6c0b6919298b5221e9f29ff270061b3 Mon Sep 17 00:00:00 2001 From: shmax Date: Wed, 23 Jul 2025 12:39:13 -0700 Subject: [PATCH 36/38] make it more clear that samples are using hand-rolled classes --- docs/executing-queries.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/executing-queries.md b/docs/executing-queries.md index 5811a9f64..8c8b2d7b3 100644 --- a/docs/executing-queries.md +++ b/docs/executing-queries.md @@ -222,9 +222,8 @@ To optimize for this, `graphql-php` supports pluggable validation caching. By im ```php use GraphQL\Validator\ValidationCache; use GraphQL\GraphQL; -use GraphQL\Tests\PsrValidationCacheAdapter; -$validationCache = new PsrValidationCacheAdapter(); +$validationCache = new MyPsrValidationCacheAdapter(); $result = GraphQL::executeQuery( $schema, @@ -262,7 +261,7 @@ use Composer\InstalledVersions; * * @see GraphQl\Tests\PsrValidationCacheAdapter */ -class PsrValidationCacheAdapter implements ValidationCache +class MyPsrValidationCacheAdapter implements ValidationCache { private CacheInterface $cache; From 22e15ccb090e7fd56d2d3e012aa7a150ba41b367 Mon Sep 17 00:00:00 2001 From: shmax Date: Wed, 23 Jul 2025 12:52:19 -0700 Subject: [PATCH 37/38] remove see reference --- src/Validator/ValidationCache.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Validator/ValidationCache.php b/src/Validator/ValidationCache.php index 3abc66189..9c6a4c5f3 100644 --- a/src/Validator/ValidationCache.php +++ b/src/Validator/ValidationCache.php @@ -21,7 +21,6 @@ * and it's possible these may shift or expand as the library evolves, * so it might make sense to include the library version number in your keys. * - * @see PsrValidationCacheAdapter for a simple reference implementation. */ interface ValidationCache { From cbebfce12bea32d2c3af64744a4a5c64efebbde3 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 19:53:21 +0000 Subject: [PATCH 38/38] Autofix --- src/Validator/ValidationCache.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Validator/ValidationCache.php b/src/Validator/ValidationCache.php index 9c6a4c5f3..597cfd9b3 100644 --- a/src/Validator/ValidationCache.php +++ b/src/Validator/ValidationCache.php @@ -3,7 +3,6 @@ namespace GraphQL\Validator; use GraphQL\Language\AST\DocumentNode; -use GraphQL\Tests\PsrValidationCacheAdapter; use GraphQL\Type\Schema; use GraphQL\Validator\Rules\ValidationRule; @@ -20,7 +19,6 @@ * - Keep in mind that there are internal `$rules` that are applied in addition to any you pass in, * and it's possible these may shift or expand as the library evolves, * so it might make sense to include the library version number in your keys. - * */ interface ValidationCache {