Skip to content

Commit ee84251

Browse files
author
Andrii Sudiev
committed
Fix bug with prefetching on different nesting levels
Allow Promise to be returned from prefetchMethod with returnRequested: true
1 parent 592568c commit ee84251

File tree

13 files changed

+335
-54
lines changed

13 files changed

+335
-54
lines changed

src/Annotations/Prefetch.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@
1010
#[Attribute(Attribute::TARGET_PARAMETER)]
1111
class Prefetch implements ParameterAnnotationInterface
1212
{
13-
/** @param string|(callable&array{class-string, string}) $callable */
14-
public function __construct(public readonly string|array $callable)
13+
/**
14+
* @param string|(callable&array{class-string, string}) $callable
15+
* @param bool $returnRequested return value mapped to requested method
16+
*/
17+
public function __construct(
18+
public readonly string|array $callable,
19+
public readonly bool $returnRequested = false,
20+
)
1521
{
1622
}
1723

src/Mappers/Parameters/PrefetchParameterMiddleware.php

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock,
5252
fieldName: $method->getName(),
5353
resolver: $resolver,
5454
parameters: $parameters,
55+
returnRequested: $prefetch->returnRequested,
5556
);
5657
}
5758
}

src/Parameters/PrefetchDataParameter.php

+26-8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use TheCodingMachine\GraphQLite\PrefetchBuffer;
1313
use TheCodingMachine\GraphQLite\QueryField;
1414

15+
use function array_key_exists;
1516
use function assert;
1617

1718
/**
@@ -27,6 +28,7 @@ public function __construct(
2728
private readonly string $fieldName,
2829
private readonly mixed $resolver,
2930
public readonly array $parameters,
31+
public readonly bool $returnRequested = false,
3032
)
3133
{
3234
}
@@ -52,24 +54,40 @@ public function resolve(object|null $source, array $args, mixed $context, Resolv
5254
// So we record all of these ->resolve() calls, collect them together and when a value is actually
5355
// needed, GraphQL calls the callback of Deferred below. That's when we call the prefetch method,
5456
// already knowing all the requested fields (source-arguments combinations).
55-
return new Deferred(function () use ($info, $context, $args, $prefetchBuffer) {
56-
if (! $prefetchBuffer->hasResult($args, $info)) {
57-
$prefetchResult = $this->computePrefetch($args, $context, $info, $prefetchBuffer);
58-
59-
$prefetchBuffer->storeResult($prefetchResult, $args, $info);
57+
return new Deferred(function () use ($source, $info, $context, $args, $prefetchBuffer) {
58+
if (! $prefetchBuffer->hasResult($source)) {
59+
$this->computePrefetch($args, $context, $info, $prefetchBuffer);
6060
}
6161

62-
return $prefetchResult ?? $prefetchBuffer->getResult($args, $info);
62+
return $prefetchBuffer->getResult($source);
6363
});
6464
}
6565

6666
/** @param array<string, mixed> $args */
67-
private function computePrefetch(array $args, mixed $context, ResolveInfo $info, PrefetchBuffer $prefetchBuffer): mixed
67+
private function computePrefetch(array $args, mixed $context, ResolveInfo $info, PrefetchBuffer $prefetchBuffer): void
6868
{
6969
$sources = $prefetchBuffer->getObjectsByArguments($args, $info);
70+
$prefetchBuffer->purge($args, $info);
7071
$toPassPrefetchArgs = QueryField::paramsToArguments($this->fieldName, $this->parameters, null, $args, $context, $info, $this->resolver);
7172

72-
return ($this->resolver)($sources, ...$toPassPrefetchArgs);
73+
$resolvedValues = ($this->resolver)($sources, ...$toPassPrefetchArgs);
74+
if ($this->returnRequested) {
75+
foreach ($resolvedValues as $key => $resolvedValue) {
76+
if (! array_key_exists($key, $sources)) {
77+
throw new GraphQLRuntimeException(
78+
'Called by Prefetch function should accept ' .
79+
'Array<key> and return Array<value>, but the function did ' .
80+
'not return an Array of the same length as the Array of keys.',
81+
);
82+
}
83+
$prefetchBuffer->storeResult($sources[$key], $resolvedValue);
84+
}
85+
} else {
86+
foreach ($sources as $source) {
87+
// map results to each source to support old prefetch behavior
88+
$prefetchBuffer->storeResult($source, $resolvedValues);
89+
}
90+
}
7391
}
7492

7593
/** @inheritDoc */

src/PrefetchBuffer.php

+14-15
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
namespace TheCodingMachine\GraphQLite;
66

77
use GraphQL\Type\Definition\ResolveInfo;
8+
use SplObjectStorage;
89

9-
use function array_key_exists;
1010
use function md5;
1111
use function serialize;
1212

@@ -18,8 +18,13 @@ class PrefetchBuffer
1818
/** @var array<string, array<int, object>> An array of buffered, indexed by hash of arguments. */
1919
private array $objects = [];
2020

21-
/** @var array<string, mixed> An array of prefetch method results, indexed by hash of arguments. */
22-
private array $results = [];
21+
/** @var SplObjectStorage A Storage of prefetch method results, holds source to resolved values. */
22+
private SplObjectStorage $results;
23+
24+
public function __construct()
25+
{
26+
$this->results = new SplObjectStorage();
27+
}
2328

2429
/** @param array<array-key, mixed> $arguments The input arguments passed from GraphQL to the field. */
2530
public function register(
@@ -66,28 +71,22 @@ public function purge(
6671
unset($this->objects[$this->computeHash($arguments, $info)]);
6772
}
6873

69-
/** @param array<array-key, mixed> $arguments The input arguments passed from GraphQL to the field. */
7074
public function storeResult(
75+
object $source,
7176
mixed $result,
72-
array $arguments,
73-
ResolveInfo|null $info = null,
7477
): void {
75-
$this->results[$this->computeHash($arguments, $info)] = $result;
78+
$this->results->offsetSet($source, $result);
7679
}
7780

78-
/** @param array<array-key, mixed> $arguments The input arguments passed from GraphQL to the field. */
7981
public function hasResult(
80-
array $arguments,
81-
ResolveInfo|null $info = null,
82+
object $source,
8283
): bool {
83-
return array_key_exists($this->computeHash($arguments, $info), $this->results);
84+
return $this->results->offsetExists($source);
8485
}
8586

86-
/** @param array<array-key, mixed> $arguments The input arguments passed from GraphQL to the field. */
8787
public function getResult(
88-
array $arguments,
89-
ResolveInfo|null $info = null,
88+
object $source,
9089
): mixed {
91-
return $this->results[$this->computeHash($arguments, $info)];
90+
return $this->results->offsetGet($source);
9291
}
9392
}

src/QueryField.php

+19-19
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@
44

55
namespace TheCodingMachine\GraphQLite;
66

7-
use GraphQL\Deferred;
7+
use Closure;
88
use GraphQL\Error\ClientAware;
99
use GraphQL\Executor\Promise\Adapter\SyncPromise;
10-
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
11-
use GraphQL\Executor\Promise\Promise;
1210
use GraphQL\Language\AST\FieldDefinitionNode;
1311
use GraphQL\Type\Definition\FieldDefinition;
1412
use GraphQL\Type\Definition\ListOfType;
@@ -22,9 +20,6 @@
2220
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
2321
use TheCodingMachine\GraphQLite\Parameters\SourceParameter;
2422

25-
use function array_filter;
26-
use function array_map;
27-
2823
/**
2924
* A GraphQL field that maps to a PHP method automatically.
3025
*
@@ -90,24 +85,12 @@ public function __construct(
9085
return $result;
9186
};
9287

93-
$deferred = (bool) array_filter($toPassArgs, static fn (mixed $value) => $value instanceof SyncPromise);
94-
9588
// GraphQL allows deferring resolving the field's value using promises, i.e. they call the resolve
9689
// function ahead of time for all of the fields (allowing us to gather all calls and do something
9790
// in batch, like prefetch) and then resolve the promises as needed. To support that for prefetch,
9891
// we're checking if any of the resolved parameters returned a promise. If they did, we know
9992
// that the value should also be resolved using a promise, so we're wrapping it in one.
100-
return $deferred ? new Deferred(static function () use ($toPassArgs, $callResolver) {
101-
$syncPromiseAdapter = new SyncPromiseAdapter();
102-
103-
// Wait for every deferred parameter.
104-
$toPassArgs = array_map(
105-
static fn (mixed $value) => $value instanceof SyncPromise ? $syncPromiseAdapter->wait(new Promise($value, $syncPromiseAdapter)) : $value,
106-
$toPassArgs,
107-
);
108-
109-
return $callResolver(...$toPassArgs);
110-
}) : $callResolver(...$toPassArgs);
93+
return $this->deferred($toPassArgs, $callResolver);
11194
};
11295

11396
$config += $additionalConfig;
@@ -204,4 +187,21 @@ public static function paramsToArguments(string $name, array $parameters, object
204187

205188
return $toPassArgs;
206189
}
190+
191+
/**
192+
* @param array<int, mixed> $toPassArgs
193+
* Create Deferred if any of arguments contain promise
194+
*/
195+
public static function deferred(array $toPassArgs, Closure $callResolver): mixed
196+
{
197+
foreach ($toPassArgs as $position => $toPassArg) {
198+
if ($toPassArg instanceof SyncPromise) {
199+
return $toPassArg->then(static function ($resolvedValue) use ($toPassArgs, $position, $callResolver) {
200+
$toPassArgs[$position] = $resolvedValue;
201+
return self::deferred($toPassArgs, $callResolver);
202+
});
203+
}
204+
}
205+
return $callResolver(...$toPassArgs);
206+
}
207207
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Fixtures\Integration\Controllers;
6+
7+
use TheCodingMachine\GraphQLite\Annotations\Query;
8+
use TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Blog;
9+
10+
class BlogController
11+
{
12+
/** @return Blog[] */
13+
#[Query]
14+
public function getBlogs(): array
15+
{
16+
return [
17+
new Blog(1),
18+
new Blog(2),
19+
];
20+
}
21+
}
+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Fixtures\Integration\Models;
6+
7+
use TheCodingMachine\GraphQLite\Annotations\Field;
8+
use TheCodingMachine\GraphQLite\Annotations\Prefetch;
9+
use TheCodingMachine\GraphQLite\Annotations\Type;
10+
11+
#[Type]
12+
class Blog
13+
{
14+
public function __construct(
15+
private readonly int $id,
16+
) {
17+
}
18+
19+
#[Field(outputType: 'ID!')]
20+
public function getId(): int
21+
{
22+
return $this->id;
23+
}
24+
25+
/**
26+
* @param Post[] $prefetchedPosts
27+
*
28+
* @return Post[]
29+
*/
30+
#[Field]
31+
public function getPosts(
32+
#[Prefetch('prefetchPosts', true)]
33+
array $prefetchedPosts,
34+
): array {
35+
return $prefetchedPosts;
36+
}
37+
38+
/**
39+
* @param Blog[] $prefetchedSubBlogs
40+
*
41+
* @return Blog[]
42+
*/
43+
#[Field]
44+
public function getSubBlogs(
45+
#[Prefetch('prefetchSubBlogs', true)]
46+
array $prefetchedSubBlogs,
47+
): array {
48+
return $prefetchedSubBlogs;
49+
}
50+
51+
/**
52+
* @param Blog[] $blogs
53+
*
54+
* @return Post[][]
55+
*/
56+
public static function prefetchPosts(iterable $blogs): array
57+
{
58+
$posts = [];
59+
foreach ($blogs as $key => $blog) {
60+
$blogId = $blog->getId();
61+
$posts[$key] = [
62+
new Post('post-' . $blogId . '.1'),
63+
new Post('post-' . $blogId . '.2'),
64+
];
65+
}
66+
67+
return $posts;
68+
}
69+
70+
/**
71+
* @param Blog[] $blogs
72+
*
73+
* @return Blog[][]
74+
*/
75+
public static function prefetchSubBlogs(iterable $blogs): array
76+
{
77+
$subBlogs = [];
78+
foreach ($blogs as $key => $blog) {
79+
$blogId = $blog->getId();
80+
$subBlogId = $blogId * 10;
81+
$subBlogs[$key] = [new Blog($subBlogId)];
82+
}
83+
84+
return $subBlogs;
85+
}
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Fixtures\Integration\Models;
6+
7+
use TheCodingMachine\GraphQLite\Annotations\Field;
8+
use TheCodingMachine\GraphQLite\Annotations\Type;
9+
10+
#[Type]
11+
class Comment
12+
{
13+
public function __construct(
14+
private readonly string $text,
15+
) {
16+
}
17+
18+
#[Field]
19+
public function getText(): string
20+
{
21+
return $this->text;
22+
}
23+
}

0 commit comments

Comments
 (0)