From b84a06a7839acf09f90622669dbd0ce184dd946b Mon Sep 17 00:00:00 2001 From: sk4rp <78820282+sk4rp@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:23:19 +0500 Subject: [PATCH] Encode argument values unambiguously in @cache keys --- src/Cache/CacheKeyAndTagsGenerator.php | 6 +- .../Integration/Cache/CacheDirectiveTest.php | 62 +++++++++++++++++ .../Cache/CacheKeyAndTagsGeneratorTest.php | 69 +++++++++++++++++++ 3 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/Cache/CacheKeyAndTagsGeneratorTest.php diff --git a/src/Cache/CacheKeyAndTagsGenerator.php b/src/Cache/CacheKeyAndTagsGenerator.php index f415bf0c22..2317899366 100644 --- a/src/Cache/CacheKeyAndTagsGenerator.php +++ b/src/Cache/CacheKeyAndTagsGenerator.php @@ -37,9 +37,9 @@ public function key( ksort($args); foreach ($args as $key => $value) { $parts[] = $key; - $parts[] = is_array($value) - ? \Safe\json_encode($value) - : $value; + $parts[] = is_object($value) + ? $value + : \Safe\json_encode($value); } return implode(self::SEPARATOR, $parts); diff --git a/tests/Integration/Cache/CacheDirectiveTest.php b/tests/Integration/Cache/CacheDirectiveTest.php index cee9be8d4a..4aa87075e5 100644 --- a/tests/Integration/Cache/CacheDirectiveTest.php +++ b/tests/Integration/Cache/CacheDirectiveTest.php @@ -523,4 +523,66 @@ public function testUseFalsyResultsInCache(): void ], ]); } + + public function testDistinguishesFalseAndNullArgs(): void + { + $this->mockResolver(static fn (mixed $root, array $args): string => var_export($args['flag'], true)); + + $this->schema = /** @lang GraphQL */ <<<'GRAPHQL' + type Query { + foo(flag: Boolean): String @mock @cache + } + GRAPHQL; + + $this->graphQL(/** @lang GraphQL */ <<<'GRAPHQL' + { + foo(flag: false) + } + GRAPHQL)->assertExactJson([ + 'data' => [ + 'foo' => 'false', + ], + ]); + + $this->graphQL(/** @lang GraphQL */ <<<'GRAPHQL' + { + foo(flag: null) + } + GRAPHQL)->assertExactJson([ + 'data' => [ + 'foo' => 'NULL', + ], + ]); + } + + public function testDistinguishesNullAndStringNullArgs(): void + { + $this->mockResolver(static fn (mixed $root, array $args): string => var_export($args['arg'], true)); + + $this->schema = /** @lang GraphQL */ <<<'GRAPHQL' + type Query { + foo(arg: String): String @mock @cache + } + GRAPHQL; + + $this->graphQL(/** @lang GraphQL */ <<<'GRAPHQL' + { + foo(arg: "null") + } + GRAPHQL)->assertExactJson([ + 'data' => [ + 'foo' => "'null'", + ], + ]); + + $this->graphQL(/** @lang GraphQL */ <<<'GRAPHQL' + { + foo(arg: null) + } + GRAPHQL)->assertExactJson([ + 'data' => [ + 'foo' => 'NULL', + ], + ]); + } } diff --git a/tests/Unit/Cache/CacheKeyAndTagsGeneratorTest.php b/tests/Unit/Cache/CacheKeyAndTagsGeneratorTest.php new file mode 100644 index 0000000000..61a7f1ffc7 --- /dev/null +++ b/tests/Unit/Cache/CacheKeyAndTagsGeneratorTest.php @@ -0,0 +1,69 @@ + $args */ + private function key(array $args): string + { + return (new CacheKeyAndTagsGenerator())->key( + user: null, + isPrivate: false, + parentName: 'Query', + id: null, + fieldName: 'foo', + args: $args, + path: ['foo'], + ); + } + + public function testEncodesScalarArgs(): void + { + $this->assertSame('lighthouse:Query::foo:bar:"baz":first:5', $this->key(['first' => 5, 'bar' => 'baz'])); + } + + public function testEncodesArrayArgs(): void + { + $this->assertSame('lighthouse:Query::foo:ids:[1,2]', $this->key(['ids' => [1, 2]])); + } + + public function testEncodesStringableArgs(): void + { + $value = new class implements \Stringable { + public function __toString(): string + { + return 'baz'; + } + }; + + $this->assertSame('lighthouse:Query::foo:bar:baz', $this->key(['bar' => $value])); + } + + public function testDistinguishesArgsOfDifferentTypes(): void + { + $keys = [ + $this->key(['arg' => false]), + $this->key(['arg' => null]), + $this->key(['arg' => '']), + $this->key(['arg' => true]), + $this->key(['arg' => 1]), + $this->key(['arg' => 1.5]), + $this->key(['arg' => 'null']), + $this->key(['arg' => 'true']), + $this->key(['arg' => 'false']), + $this->key(['arg' => '1']), + $this->key(['arg' => '1.5']), + $this->key(['arg' => '[1]']), + $this->key(['arg' => [1]]), + $this->key(['arg' => 'a:b:c']), + $this->key(['arg' => 'a', 'b' => 'c']), + $this->key([]), + ]; + + $this->assertSame($keys, array_unique($keys)); + } +}