Skip to content
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
35ce576
cache result of validation
shmax Jul 11, 2025
03db33d
remove ttl
shmax Jul 11, 2025
c3a7eb4
stanning
shmax Jul 11, 2025
a2c38f4
stanning
shmax Jul 11, 2025
6782bad
downgrade to satisfy 7.4
shmax Jul 11, 2025
6317931
use custom interface
shmax Jul 12, 2025
73c9774
files
shmax Jul 12, 2025
6d7ace0
cleanup
shmax Jul 12, 2025
708c85b
cleanup
shmax Jul 12, 2025
f35cb3c
remove trailing comma
shmax Jul 12, 2025
d71e123
fix tests
shmax Jul 12, 2025
cf20ef3
formatting
shmax Jul 14, 2025
60bffe6
move to file
shmax Jul 14, 2025
40c000b
use shorthand, formatting
shmax Jul 14, 2025
7d515fd
assert both results
shmax Jul 14, 2025
c2a82a4
ignore specific error
shmax Jul 14, 2025
51d1087
documentation
shmax Jul 15, 2025
a745345
Autofix
autofix-ci[bot] Jul 15, 2025
1277bb3
use serialize
shmax Jul 16, 2025
59a0cd7
simplify conditional
spawnia Jul 17, 2025
6679cb8
unify formatting
spawnia Jul 17, 2025
8d21f38
Merge branch 'master' into validation-cache
spawnia Jul 17, 2025
de245ff
Polish tests and docs
spawnia Jul 17, 2025
d166e41
Clean up types
spawnia Jul 17, 2025
4b3aafe
include rules in the fun
shmax Jul 18, 2025
9683c02
Autofix
autofix-ci[bot] Jul 18, 2025
02013c3
update comments
shmax Jul 18, 2025
1e55f48
Merge branch 'validation-cache' of github.com:shmax/graphql-php into …
shmax Jul 18, 2025
127b68f
remove comment
shmax Jul 18, 2025
b4865e9
add comments about keys
shmax Jul 23, 2025
4ec12df
pass rules to markValidated
shmax Jul 23, 2025
707f257
add docs
shmax Jul 23, 2025
4c3d419
formatting
shmax Jul 23, 2025
b5fdd3e
remove redundant
shmax Jul 23, 2025
5d9e714
Autofix
autofix-ci[bot] Jul 23, 2025
b8c62dc
Improve docs
spawnia Jul 23, 2025
7a2b3dc
Autofix
autofix-ci[bot] Jul 23, 2025
e8fedcc
make it more clear that samples are using hand-rolled classes
shmax Jul 23, 2025
c91420c
Merge branch 'validation-cache' of github.com:shmax/graphql-php into …
shmax Jul 23, 2025
22e15cc
remove see reference
shmax Jul 23, 2025
cbebfce
Autofix
autofix-ci[bot] Jul 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@
"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",
"symfony/cache": "^5.4",
"symfony/polyfill-php81": "^1.23",
"symfony/var-exporter": "^5 || ^6 || ^7",
"thecodingmachine/safe": "^1.3 || ^2 || ^3"
Expand Down
9 changes: 6 additions & 3 deletions docs/class-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down Expand Up @@ -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
```

Expand Down Expand Up @@ -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
```

Expand Down
12 changes: 8 additions & 4 deletions src/GraphQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\ValidationRule;
use GraphQL\Validator\ValidationCache;

/**
* This is the primary facade for fulfilling GraphQL operations.
Expand Down Expand Up @@ -90,7 +91,8 @@ public static function executeQuery(
?array $variableValues = null,
?string $operationName = null,
?callable $fieldResolver = null,
?array $validationRules = null
?array $validationRules = null,
?ValidationCache $cache = null
): ExecutionResult {
$promiseAdapter = new SyncPromiseAdapter();

Expand All @@ -103,7 +105,8 @@ public static function executeQuery(
$variableValues,
$operationName,
$fieldResolver,
$validationRules
$validationRules,
$cache
);

return $promiseAdapter->wait($promise);
Expand Down Expand Up @@ -132,7 +135,8 @@ public static function promiseToExecute(
?array $variableValues = null,
?string $operationName = null,
?callable $fieldResolver = null,
?array $validationRules = null
?array $validationRules = null,
?ValidationCache $cache = null
): Promise {
try {
$documentNode = $source instanceof DocumentNode
Expand All @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions src/Server/ServerConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public static function create(array $config = []): self
private bool $queryBatching = false;

/**
* @var array<ValidationRule>|callable|null
* @var array|callable|null
*
* @phpstan-var ValidationRulesOption
*/
Expand Down Expand Up @@ -315,7 +315,7 @@ public function getPromiseAdapter(): ?PromiseAdapter
}

/**
* @return array<ValidationRule>|callable|null
* @return array|callable|null
*
* @phpstan-return ValidationRulesOption
*/
Expand Down
21 changes: 16 additions & 5 deletions src/Validator/DocumentValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,22 @@ public static function validate(
Schema $schema,
DocumentNode $ast,
?array $rules = null,
?TypeInfo $typeInfo = null
?TypeInfo $typeInfo = null,
?ValidationCache $cache = null
): array {
$rules ??= static::allRules();
if (isset($cache)) {
$cached = $cache->isValidated($schema, $ast, $rules);
if ($cached) {
return [];
}
}

$rules ??= static::allRules();
if ($rules === []) {
return [];
}

$typeInfo ??= new TypeInfo($schema);

$context = new QueryValidationContext($schema, $ast, $typeInfo);

$visitors = [];
Expand All @@ -124,7 +130,13 @@ public static function validate(
)
);

return $context->getErrors();
$errors = $context->getErrors();

if (isset($cache) && $errors === []) {
$cache->markValidated($schema, $ast);
}

return $errors;
}

/**
Expand Down Expand Up @@ -273,7 +285,6 @@ public static function validateSDL(
?array $rules = null
): array {
$rules ??= self::sdlRules();

if ($rules === []) {
return [];
}
Expand Down
41 changes: 41 additions & 0 deletions src/Validator/ValidationCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php declare(strict_types=1);

namespace GraphQL\Validator;

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.
*
* 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.
*/
interface ValidationCache
{
/**
* 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.
*
* @param array<ValidationRule>|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, ?array $rules = null): bool;

/**
* @param array<ValidationRule>|null $rules
*
* 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.
*/
public function markValidated(Schema $schema, DocumentNode $ast, ?array $rules = null): void;
}
28 changes: 28 additions & 0 deletions tests/Executor/TestClasses/SpyValidationCacheAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types=1);

namespace GraphQL\Tests\Executor\TestClasses;

use GraphQL\Language\AST\DocumentNode;
use GraphQL\Tests\PsrValidationCacheAdapter;
use GraphQL\Type\Schema;

final class SpyValidationCacheAdapter extends PsrValidationCacheAdapter
{
public int $isValidatedCalls = 0;

public int $markValidatedCalls = 0;

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
{
++$this->markValidatedCalls;

parent::markValidated($schema, $ast);
}
}
101 changes: 101 additions & 0 deletions tests/Executor/ValidationWithCacheTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php declare(strict_types=1);

namespace GraphQL\Tests\Executor;

use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts;
use GraphQL\Deferred;
use GraphQL\GraphQL;
use GraphQL\Tests\Executor\TestClasses\Cat;
use GraphQL\Tests\Executor\TestClasses\Dog;
use GraphQL\Tests\Executor\TestClasses\SpyValidationCacheAdapter;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Psr16Cache;

final class ValidationWithCacheTest extends TestCase
{
use ArraySubsetAsserts;

public function testIsValidationCachedWithAdapter(): void
{
$cache = new SpyValidationCacheAdapter(new Psr16Cache(new ArrayAdapter()));
$petType = new InterfaceType([
'name' => 'Pet',
'fields' => [
'name' => 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::string(),
'woofs' => 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::string(),
'meows' => 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
}
}
}';

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

// ✅ Assert that validation only happened once
self::assertSame(2, $cache->isValidatedCalls, 'Should check cache twice');
self::assertSame(1, $cache->markValidatedCalls, 'Should mark as validated once');

$expected = [
'data' => [
'pets' => [
['name' => 'Odie', 'woofs' => true],
['name' => 'Garfield', 'meows' => false],
],
],
];

self::assertSame($expected, $resultA);
self::assertSame($expected, $resultB);
}
}
Loading
Loading