diff --git a/src/Parameters/PrefetchDataParameter.php b/src/Parameters/PrefetchDataParameter.php index fe81e4aceb..4b233c5e3c 100644 --- a/src/Parameters/PrefetchDataParameter.php +++ b/src/Parameters/PrefetchDataParameter.php @@ -43,7 +43,7 @@ public function resolve(object|null $source, array $args, mixed $context, Resolv } $prefetchBuffer = $context->getPrefetchBuffer($this); - $prefetchBuffer->register($source, $args); + $prefetchBuffer->register($source, $args, $info); // The way this works is simple: GraphQL first iterates over every requested field and calls ->resolve() // on it. That, in turn, calls this method. GraphQL doesn't need the actual value just yet; it simply @@ -53,20 +53,20 @@ public function resolve(object|null $source, array $args, mixed $context, Resolv // needed, GraphQL calls the callback of Deferred below. That's when we call the prefetch method, // already knowing all the requested fields (source-arguments combinations). return new Deferred(function () use ($info, $context, $args, $prefetchBuffer) { - if (! $prefetchBuffer->hasResult($args)) { + if (! $prefetchBuffer->hasResult($args, $info)) { $prefetchResult = $this->computePrefetch($args, $context, $info, $prefetchBuffer); - $prefetchBuffer->storeResult($prefetchResult, $args); + $prefetchBuffer->storeResult($prefetchResult, $args, $info); } - return $prefetchResult ?? $prefetchBuffer->getResult($args); + return $prefetchResult ?? $prefetchBuffer->getResult($args, $info); }); } /** @param array $args */ private function computePrefetch(array $args, mixed $context, ResolveInfo $info, PrefetchBuffer $prefetchBuffer): mixed { - $sources = $prefetchBuffer->getObjectsByArguments($args); + $sources = $prefetchBuffer->getObjectsByArguments($args, $info); $toPassPrefetchArgs = QueryField::paramsToArguments($this->fieldName, $this->parameters, null, $args, $context, $info, $this->resolver); return ($this->resolver)($sources, ...$toPassPrefetchArgs); diff --git a/src/PrefetchBuffer.php b/src/PrefetchBuffer.php index 70cc588af0..8b1afb02a0 100644 --- a/src/PrefetchBuffer.php +++ b/src/PrefetchBuffer.php @@ -4,6 +4,8 @@ namespace TheCodingMachine\GraphQLite; +use GraphQL\Type\Definition\ResolveInfo; + use function array_key_exists; use function md5; use function serialize; @@ -20,14 +22,27 @@ class PrefetchBuffer private array $results = []; /** @param array $arguments The input arguments passed from GraphQL to the field. */ - public function register(object $object, array $arguments): void - { - $this->objects[$this->computeHash($arguments)][] = $object; + public function register( + object $object, + array $arguments, + ResolveInfo|null $info = null, + ): void { + $this->objects[$this->computeHash($arguments, $info)][] = $object; } /** @param array $arguments The input arguments passed from GraphQL to the field. */ - private function computeHash(array $arguments): string - { + private function computeHash( + array $arguments, + ResolveInfo|null $info, + ): string { + if ( + $info instanceof ResolveInfo + && isset($info->operation) + && $info->operation->loc?->source?->body !== null + ) { + return md5(serialize($arguments) . $info->operation->loc->source->body); + } + return md5(serialize($arguments)); } @@ -36,32 +51,43 @@ private function computeHash(array $arguments): string * * @return array */ - public function getObjectsByArguments(array $arguments): array - { - return $this->objects[$this->computeHash($arguments)] ?? []; + public function getObjectsByArguments( + array $arguments, + ResolveInfo|null $info = null, + ): array { + return $this->objects[$this->computeHash($arguments, $info)] ?? []; } /** @param array $arguments The input arguments passed from GraphQL to the field. */ - public function purge(array $arguments): void - { - unset($this->objects[$this->computeHash($arguments)]); + public function purge( + array $arguments, + ResolveInfo|null $info = null, + ): void { + unset($this->objects[$this->computeHash($arguments, $info)]); } /** @param array $arguments The input arguments passed from GraphQL to the field. */ - public function storeResult(mixed $result, array $arguments): void - { - $this->results[$this->computeHash($arguments)] = $result; + public function storeResult( + mixed $result, + array $arguments, + ResolveInfo|null $info = null, + ): void { + $this->results[$this->computeHash($arguments, $info)] = $result; } /** @param array $arguments The input arguments passed from GraphQL to the field. */ - public function hasResult(array $arguments): bool - { - return array_key_exists($this->computeHash($arguments), $this->results); + public function hasResult( + array $arguments, + ResolveInfo|null $info = null, + ): bool { + return array_key_exists($this->computeHash($arguments, $info), $this->results); } /** @param array $arguments The input arguments passed from GraphQL to the field. */ - public function getResult(array $arguments): mixed - { - return $this->results[$this->computeHash($arguments)]; + public function getResult( + array $arguments, + ResolveInfo|null $info = null, + ): mixed { + return $this->results[$this->computeHash($arguments, $info)]; } } diff --git a/tests/Fixtures/Integration/Controllers/CompanyController.php b/tests/Fixtures/Integration/Controllers/CompanyController.php new file mode 100644 index 0000000000..ccb65bc384 --- /dev/null +++ b/tests/Fixtures/Integration/Controllers/CompanyController.php @@ -0,0 +1,17 @@ + new Contact('Joe'), + 'Bill' => new Contact('Bill'), + default => null, + }; + } + #[Mutation] public function saveContact(Contact $contact): Contact { diff --git a/tests/Fixtures/Integration/Models/Company.php b/tests/Fixtures/Integration/Models/Company.php new file mode 100644 index 0000000000..75c1e7467f --- /dev/null +++ b/tests/Fixtures/Integration/Models/Company.php @@ -0,0 +1,16 @@ +name; + } + + #[Field] + public function getContact( + Company $company, + #[Prefetch('prefetchContacts')] + array $contacts + ): ?Contact { + return $contacts[$company->name] ?? null; + } + + public static function prefetchContacts(array $companies): array + { + $contacts = []; + + foreach ($companies as $company) { + $contacts[$company->name] = new Contact('Kate'); + } + + return $contacts; + } +} diff --git a/tests/Fixtures/Integration/Types/ContactType.php b/tests/Fixtures/Integration/Types/ContactType.php index a347815879..33ff5449f9 100644 --- a/tests/Fixtures/Integration/Types/ContactType.php +++ b/tests/Fixtures/Integration/Types/ContactType.php @@ -3,6 +3,7 @@ namespace TheCodingMachine\GraphQLite\Fixtures\Integration\Types; +use TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Post; use TheCodingMachine\GraphQLite\Annotations\Prefetch; use function array_search; use function strtoupper; @@ -53,4 +54,56 @@ public static function prefetchContacts(iterable $contacts, string $prefix) 'prefix' => $prefix ]; } + + /** + * + * @return Post[]|null + */ + #[Field] + public function getPosts( + Contact $contact, + #[Prefetch('prefetchPosts')] + $posts + ): ?array { + return $posts[$contact->getName()] ?? null; + } + + public static function prefetchPosts(iterable $contacts): array + { + $posts = []; + foreach ($contacts as $contact) { + $contactPost = array_filter( + self::getContactPosts(), + fn(Post $post) => $post->author?->getName() === $contact->getName() + ); + + if (!$contactPost) { + continue; + } + + $posts[$contact->getName()] = $contactPost; + } + + return $posts; + } + + private static function getContactPosts(): array + { + return [ + self::generatePost('First Joe post', '1', new Contact('Joe')), + self::generatePost('First Bill post', '2', new Contact('Bill')), + self::generatePost('First Kate post', '3', new Contact('Kate')), + ]; + } + + private static function generatePost( + string $title, + string $id, + Contact $author, + ): Post { + $post = new Post($title); + $post->id = $id; + $post->author = $author; + return $post; + } } diff --git a/tests/Fixtures/Integration/Types/PostType.php b/tests/Fixtures/Integration/Types/PostType.php new file mode 100644 index 0000000000..565c783ecb --- /dev/null +++ b/tests/Fixtures/Integration/Types/PostType.php @@ -0,0 +1,25 @@ +id; + } + + #[Field] + public function getTitle(Post $post): string + { + return $post->title; + } +} diff --git a/tests/Integration/EndToEndTest.php b/tests/Integration/EndToEndTest.php index f58e80ffb2..d8522cc275 100644 --- a/tests/Integration/EndToEndTest.php +++ b/tests/Integration/EndToEndTest.php @@ -8,6 +8,9 @@ use GraphQL\Error\DebugFlag; use GraphQL\Executor\ExecutionResult; use GraphQL\GraphQL; +use GraphQL\Server\Helper; +use GraphQL\Server\OperationParams; +use GraphQL\Server\ServerConfig; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use stdClass; @@ -21,6 +24,7 @@ use TheCodingMachine\GraphQLite\Containers\EmptyContainer; use TheCodingMachine\GraphQLite\Containers\LazyContainer; use TheCodingMachine\GraphQLite\Context\Context; +use TheCodingMachine\GraphQLite\Exceptions\WebonyxErrorHandler; use TheCodingMachine\GraphQLite\FieldsBuilder; use TheCodingMachine\GraphQLite\Fixtures\Inputs\ValidationException; use TheCodingMachine\GraphQLite\Fixtures\Inputs\Validator; @@ -178,6 +182,70 @@ public function testEndToEnd(): void ], $this->getSuccessResult($result)); } + public function testBatchPrefetching(): void + { + $schema = $this->mainContainer->get(Schema::class); + assert($schema instanceof Schema); + + $schema->assertValid(); + + $queryContact = 'query { contact (name: "Joe") { name posts { id title } } } '; + $queryCompanyWithContact = 'query { company (id: "1"){ name contact { name posts { id title } } } } '; + + $config = ServerConfig::create( + [ + 'schema' => $schema, + 'context' => new Context(), + 'queryBatching' => true, + 'errorFormatter' => [WebonyxErrorHandler::class, 'errorFormatter'], + 'errorsHandler' => [WebonyxErrorHandler::class, 'errorHandler'], + ] + ); + + $result = (new Helper())->executeBatch( + $config, + [ + /** Set specific prefetch result to buffer */ + OperationParams::create(['query' => $queryContact]), + /** Use prefetch data from previous operation instead of getting specific prefetch */ + OperationParams::create(['query' => $queryCompanyWithContact]), + ] + ); + + $this->assertSame( + [ + 'contact' => [ + 'name' => 'Joe', + 'posts' => [ + [ + 'id' => 1, + 'title' => 'First Joe post', + ], + ], + ], + ], + $this->getSuccessResult($result[0]) + ); + + $this->assertSame( + [ + 'company' => [ + 'name' => 'Company', + 'contact' => [ + 'name' => 'Kate', + 'posts' => [ + [ + 'id' => 3, + 'title' => 'First Kate post', + ], + ], + ], + ], + ], + $this->getSuccessResult($result[1]) + ); + } + public function testDeprecatedField(): void { $schema = $this->mainContainer->get(Schema::class); diff --git a/tests/SchemaFactoryTest.php b/tests/SchemaFactoryTest.php index 908f265f57..9ac3edceb9 100644 --- a/tests/SchemaFactoryTest.php +++ b/tests/SchemaFactoryTest.php @@ -18,12 +18,16 @@ use TheCodingMachine\GraphQLite\Containers\BasicAutoWiringContainer; use TheCodingMachine\GraphQLite\Containers\EmptyContainer; use TheCodingMachine\GraphQLite\Fixtures\Integration\Controllers\ContactController; +use TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Company; use TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Contact; +use TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Post; use TheCodingMachine\GraphQLite\Fixtures\Integration\Models\User; +use TheCodingMachine\GraphQLite\Fixtures\Integration\Types\CompanyType; use TheCodingMachine\GraphQLite\Fixtures\Integration\Types\ContactFactory; use TheCodingMachine\GraphQLite\Fixtures\Integration\Types\ContactOtherType; use TheCodingMachine\GraphQLite\Fixtures\Integration\Types\ContactType; use TheCodingMachine\GraphQLite\Fixtures\Integration\Types\ExtendedContactType; +use TheCodingMachine\GraphQLite\Fixtures\Integration\Types\PostType; use TheCodingMachine\GraphQLite\Fixtures\TestSelfType; use TheCodingMachine\GraphQLite\Mappers\CompositeTypeMapper; use TheCodingMachine\GraphQLite\Mappers\DuplicateMappingException; @@ -118,6 +122,10 @@ public function testCreateSchemaOnlyWithFactories(): void ContactFactory::class, ContactOtherType::class, ContactType::class, + Post::class, + PostType::class, + Company::class, + CompanyType::class, ExtendedContactType::class, User::class, ]));