Skip to content

Commit d093249

Browse files
authored
Prefetch refactor (#588)
* Make parts of graphqlite immutable * Revert previous versions docs * Refactor getters, PHPStan, codestyle * Fix phpcbf's broken Cloneable * Fix return types in docs * Fix "test" class names used for resolvers * Refactor GlobAnnotationsCache to be immutable * Refactor prefetch * Reformat * Change docs for new prefetch * Tests coverage for new Prefetch * Reformat using cs-fix * Fix failing test on --prefer-lowest * Fix failing test on --prefer-lowest * Add test for ParameterizedCallableResolverTest
1 parent 92ed2ab commit d093249

31 files changed

+848
-345
lines changed

src/Annotations/Field.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
use Attribute;
88

9+
use function trigger_error;
10+
11+
use const E_USER_DEPRECATED;
12+
913
/**
1014
* @Annotation
1115
* @Target({"PROPERTY", "METHOD"})
@@ -44,6 +48,7 @@ class Field extends AbstractRequest
4448
public function __construct(array $attributes = [], string|null $name = null, string|null $outputType = null, string|null $prefetchMethod = null, string|array|null $for = null, string|null $description = null, string|null $inputType = null)
4549
{
4650
parent::__construct($attributes, $name, $outputType);
51+
4752
$this->prefetchMethod = $prefetchMethod ?? $attributes['prefetchMethod'] ?? null;
4853
$this->description = $description ?? $attributes['description'] ?? null;
4954
$this->inputType = $inputType ?? $attributes['inputType'] ?? null;
@@ -54,6 +59,16 @@ public function __construct(array $attributes = [], string|null $name = null, st
5459
}
5560

5661
$this->for = (array) $forValue;
62+
63+
if (! $this->prefetchMethod) {
64+
return;
65+
}
66+
67+
trigger_error(
68+
"Using #[Field(prefetchMethod='" . $this->prefetchMethod . "')] on fields is deprecated in favor " .
69+
"of #[Prefetch('" . $this->prefetchMethod . "')] \$data attribute on the parameter itself.",
70+
E_USER_DEPRECATED,
71+
);
5772
}
5873

5974
/**

src/Annotations/Prefetch.php

Lines changed: 23 additions & 0 deletions
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\Annotations;
6+
7+
use Attribute;
8+
use TheCodingMachine\GraphQLite\GraphQLRuntimeException;
9+
10+
#[Attribute(Attribute::TARGET_PARAMETER)]
11+
class Prefetch implements ParameterAnnotationInterface
12+
{
13+
/** @param string|(callable&array{class-string, string}) $callable */
14+
public function __construct(public readonly string|array $callable)
15+
{
16+
}
17+
18+
public function getTarget(): string
19+
{
20+
// This is only needed for using #[Prefetch] as a Doctrine attribute, which it doesn't support.
21+
throw new GraphQLRuntimeException();
22+
}
23+
}

src/FieldsBuilder.php

Lines changed: 67 additions & 72 deletions
Large diffs are not rendered by default.

src/InputTypeUtils.php

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
use ReflectionMethod;
1818
use ReflectionNamedType;
1919
use RuntimeException;
20+
use TheCodingMachine\GraphQLite\Parameters\ExpandsInputTypeParameters;
2021
use TheCodingMachine\GraphQLite\Parameters\InputTypeParameterInterface;
2122
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
2223

23-
use function array_filter;
2424
use function array_map;
2525
use function assert;
2626
use function ltrim;
@@ -99,6 +99,33 @@ private function resolveSelf(Type $type, ReflectionClass $reflectionClass): Type
9999
return $type;
100100
}
101101

102+
/**
103+
* @param array<string, ParameterInterface> $parameters
104+
*
105+
* @return array<string, InputTypeParameterInterface>
106+
*/
107+
public static function toInputParameters(array $parameters): array
108+
{
109+
$result = [];
110+
111+
foreach ($parameters as $name => $parameter) {
112+
if ($parameter instanceof InputTypeParameterInterface) {
113+
$result[$name] = $parameter;
114+
}
115+
116+
if (! ($parameter instanceof ExpandsInputTypeParameters)) {
117+
continue;
118+
}
119+
120+
$result = [
121+
...$result,
122+
...$parameter->toInputTypeParameters(),
123+
];
124+
}
125+
126+
return $result;
127+
}
128+
102129
/**
103130
* Maps an array of ParameterInterface to an array of field descriptors as accepted by Webonyx.
104131
*
@@ -108,9 +135,7 @@ private function resolveSelf(Type $type, ReflectionClass $reflectionClass): Type
108135
*/
109136
public static function getInputTypeArgs(array $args): array
110137
{
111-
$inputTypeArgs = array_filter($args, static function (ParameterInterface $parameter) {
112-
return $parameter instanceof InputTypeParameterInterface;
113-
});
138+
$inputTypeArgs = self::toInputParameters($args);
114139

115140
return array_map(static function (InputTypeParameterInterface $parameter): array {
116141
$desc = [
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite;
6+
7+
use Throwable;
8+
9+
class InvalidCallableRuntimeException extends GraphQLRuntimeException
10+
{
11+
public static function methodNotFound(string $className, string $methodName, Throwable|null $previous = null): self
12+
{
13+
return new self('Method ' . $className . '::' . $methodName . " wasn't found or isn't accessible.", 0, $previous);
14+
}
15+
}

src/InvalidPrefetchMethodRuntimeException.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,22 @@
1111

1212
class InvalidPrefetchMethodRuntimeException extends GraphQLRuntimeException
1313
{
14+
/** @deprecated Remove with the removal of old #[Field(prefetchMethod)] */
1415
public static function methodNotFound(ReflectionMethod|ReflectionProperty $reflector, ReflectionClass $reflectionClass, string $methodName, ReflectionException $previous): self
1516
{
1617
throw new self('The @Field annotation in ' . $reflector->getDeclaringClass()->getName() . '::' . $reflector->getName() . ' specifies a "prefetch method" that could not be found. Unable to find method ' . $reflectionClass->getName() . '::' . $methodName . '.', 0, $previous);
1718
}
1819

19-
public static function prefetchDataIgnored(ReflectionMethod $annotationMethod, bool $isSecond): self
20+
public static function fromInvalidCallable(
21+
ReflectionMethod $reflector,
22+
string $parameterName,
23+
InvalidCallableRuntimeException $e,
24+
): self
2025
{
21-
throw new self('The @Field annotation in ' . $annotationMethod->getDeclaringClass()->getName() . '::' . $annotationMethod->getName() . ' specifies a "prefetch method" but the data from the prefetch method is not gathered. The "' . $annotationMethod->getName() . '" method should accept a ' . ($isSecond ? 'second' : 'first') . ' parameter that will contain data returned by the prefetch method.');
26+
return new self(
27+
'#[Prefetch] attribute on parameter $' . $parameterName . ' in ' . $reflector->getDeclaringClass()->getName() . '::' . $reflector->getName() .
28+
' specifies a callable that is invalid: ' . $e->getMessage(),
29+
previous: $e,
30+
);
2231
}
2332
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Mappers\Parameters;
6+
7+
use phpDocumentor\Reflection\DocBlock;
8+
use phpDocumentor\Reflection\Type;
9+
use ReflectionMethod;
10+
use ReflectionParameter;
11+
use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations;
12+
use TheCodingMachine\GraphQLite\Annotations\Prefetch;
13+
use TheCodingMachine\GraphQLite\InvalidCallableRuntimeException;
14+
use TheCodingMachine\GraphQLite\InvalidPrefetchMethodRuntimeException;
15+
use TheCodingMachine\GraphQLite\ParameterizedCallableResolver;
16+
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
17+
use TheCodingMachine\GraphQLite\Parameters\PrefetchDataParameter;
18+
19+
use function assert;
20+
21+
/**
22+
* Handles {@see Prefetch} annotated parameters.
23+
*/
24+
class PrefetchParameterMiddleware implements ParameterMiddlewareInterface
25+
{
26+
public function __construct(
27+
private readonly ParameterizedCallableResolver $parameterizedCallableResolver,
28+
)
29+
{
30+
}
31+
32+
public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, Type|null $paramTagType, ParameterAnnotations $parameterAnnotations, ParameterHandlerInterface $next): ParameterInterface
33+
{
34+
$prefetch = $parameterAnnotations->getAnnotationByType(Prefetch::class);
35+
36+
if ($prefetch === null) {
37+
return $next->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations);
38+
}
39+
40+
$method = $parameter->getDeclaringFunction();
41+
42+
assert($method instanceof ReflectionMethod);
43+
44+
// Map callable specified by #[Prefetch] into a real callable and parse all of the GraphQL parameters.
45+
try {
46+
[$resolver, $parameters] = $this->parameterizedCallableResolver->resolve($prefetch->callable, $method->getDeclaringClass(), 1);
47+
} catch (InvalidCallableRuntimeException $e) {
48+
throw InvalidPrefetchMethodRuntimeException::fromInvalidCallable($method, $parameter->getName(), $e);
49+
}
50+
51+
return new PrefetchDataParameter(
52+
fieldName: $method->getName(),
53+
resolver: $resolver,
54+
parameters: $parameters,
55+
);
56+
}
57+
}

src/ParameterizedCallableResolver.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite;
6+
7+
use Psr\Container\ContainerInterface;
8+
use ReflectionClass;
9+
use ReflectionException;
10+
use ReflectionMethod;
11+
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
12+
13+
use function assert;
14+
use function is_callable;
15+
use function is_string;
16+
17+
class ParameterizedCallableResolver
18+
{
19+
public function __construct(
20+
private readonly FieldsBuilder $fieldsBuilder,
21+
private readonly ContainerInterface $container,
22+
)
23+
{
24+
}
25+
26+
/**
27+
* @param string|array{class-string, string} $callable
28+
*
29+
* @return array{callable, array<string, ParameterInterface>}
30+
*/
31+
public function resolve(string|array $callable, string|ReflectionClass $classContext, int $skip = 0): array
32+
{
33+
if ($classContext instanceof ReflectionClass) {
34+
$classContext = $classContext->getName();
35+
}
36+
37+
// If string method is given, it's equivalent to [self::class, 'method']
38+
if (is_string($callable)) {
39+
$callable = [$classContext, $callable];
40+
}
41+
42+
try {
43+
$refMethod = new ReflectionMethod($callable[0], $callable[1]);
44+
} catch (ReflectionException $e) {
45+
throw InvalidCallableRuntimeException::methodNotFound($callable[0], $callable[1], $e);
46+
}
47+
48+
// If method isn't static, then we should try to resolve the class name through the container.
49+
if (! $refMethod->isStatic()) {
50+
$callable = fn (...$args) => $this->container->get($callable[0])->{$callable[1]}(...$args);
51+
}
52+
53+
assert(is_callable($callable));
54+
55+
// Map all parameters of the callable.
56+
$parameters = $this->fieldsBuilder->getParameters($refMethod, $skip);
57+
58+
return [$callable, $parameters];
59+
}
60+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Parameters;
6+
7+
interface ExpandsInputTypeParameters
8+
{
9+
/** @return array<string, InputTypeParameterInterface> */
10+
public function toInputTypeParameters(): array;
11+
}

0 commit comments

Comments
 (0)