Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prefetch refactor #588

Merged
merged 17 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions src/Annotations/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

use Attribute;

use function trigger_error;

use const E_USER_DEPRECATED;

/**
* @Annotation
* @Target({"PROPERTY", "METHOD"})
Expand Down Expand Up @@ -44,6 +48,7 @@ class Field extends AbstractRequest
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)
{
parent::__construct($attributes, $name, $outputType);

$this->prefetchMethod = $prefetchMethod ?? $attributes['prefetchMethod'] ?? null;
$this->description = $description ?? $attributes['description'] ?? null;
$this->inputType = $inputType ?? $attributes['inputType'] ?? null;
Expand All @@ -54,6 +59,16 @@ public function __construct(array $attributes = [], string|null $name = null, st
}

$this->for = (array) $forValue;

if (! $this->prefetchMethod) {
return;
}

trigger_error(
"Using #[Field(prefetchMethod='" . $this->prefetchMethod . "')] on fields is deprecated in favor " .
"of #[Prefetch('" . $this->prefetchMethod . "')] \$data attribute on the parameter itself.",
E_USER_DEPRECATED,
);
}

/**
Expand Down
23 changes: 23 additions & 0 deletions src/Annotations/Prefetch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Annotations;

use Attribute;
use TheCodingMachine\GraphQLite\GraphQLRuntimeException;

#[Attribute(Attribute::TARGET_PARAMETER)]
class Prefetch implements ParameterAnnotationInterface
{
/** @param string|(callable&array{class-string, string}) $callable */
public function __construct(public readonly string|array $callable)
{
}

public function getTarget(): string
{
// This is only needed for using #[Prefetch] as a Doctrine attribute, which it doesn't support.
throw new GraphQLRuntimeException();
}
}
139 changes: 67 additions & 72 deletions src/FieldsBuilder.php

Large diffs are not rendered by default.

33 changes: 29 additions & 4 deletions src/InputTypeUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
use ReflectionMethod;
use ReflectionNamedType;
use RuntimeException;
use TheCodingMachine\GraphQLite\Parameters\ExpandsInputTypeParameters;
use TheCodingMachine\GraphQLite\Parameters\InputTypeParameterInterface;
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;

use function array_filter;
use function array_map;
use function assert;
use function ltrim;
Expand Down Expand Up @@ -99,6 +99,33 @@ private function resolveSelf(Type $type, ReflectionClass $reflectionClass): Type
return $type;
}

/**
* @param array<string, ParameterInterface> $parameters
*
* @return array<string, InputTypeParameterInterface>
*/
public static function toInputParameters(array $parameters): array
{
$result = [];

foreach ($parameters as $name => $parameter) {
if ($parameter instanceof InputTypeParameterInterface) {
$result[$name] = $parameter;
}

if (! ($parameter instanceof ExpandsInputTypeParameters)) {
continue;
}

$result = [
...$result,
...$parameter->toInputTypeParameters(),
];
}

return $result;
}

/**
* Maps an array of ParameterInterface to an array of field descriptors as accepted by Webonyx.
*
Expand All @@ -108,9 +135,7 @@ private function resolveSelf(Type $type, ReflectionClass $reflectionClass): Type
*/
public static function getInputTypeArgs(array $args): array
{
$inputTypeArgs = array_filter($args, static function (ParameterInterface $parameter) {
return $parameter instanceof InputTypeParameterInterface;
});
$inputTypeArgs = self::toInputParameters($args);

return array_map(static function (InputTypeParameterInterface $parameter): array {
$desc = [
Expand Down
15 changes: 15 additions & 0 deletions src/InvalidCallableRuntimeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite;

use Throwable;

class InvalidCallableRuntimeException extends GraphQLRuntimeException
{
public static function methodNotFound(string $className, string $methodName, Throwable|null $previous = null): self
{
return new self('Method ' . $className . '::' . $methodName . " wasn't found or isn't accessible.", 0, $previous);
}
}
13 changes: 11 additions & 2 deletions src/InvalidPrefetchMethodRuntimeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,22 @@

class InvalidPrefetchMethodRuntimeException extends GraphQLRuntimeException
{
/** @deprecated Remove with the removal of old #[Field(prefetchMethod)] */
public static function methodNotFound(ReflectionMethod|ReflectionProperty $reflector, ReflectionClass $reflectionClass, string $methodName, ReflectionException $previous): self
{
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);
}

public static function prefetchDataIgnored(ReflectionMethod $annotationMethod, bool $isSecond): self
public static function fromInvalidCallable(
ReflectionMethod $reflector,
string $parameterName,
InvalidCallableRuntimeException $e,
): self
{
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.');
return new self(
'#[Prefetch] attribute on parameter $' . $parameterName . ' in ' . $reflector->getDeclaringClass()->getName() . '::' . $reflector->getName() .
' specifies a callable that is invalid: ' . $e->getMessage(),
previous: $e,
);
}
}
57 changes: 57 additions & 0 deletions src/Mappers/Parameters/PrefetchParameterMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Mappers\Parameters;

use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\Type;
use ReflectionMethod;
use ReflectionParameter;
use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations;
use TheCodingMachine\GraphQLite\Annotations\Prefetch;
use TheCodingMachine\GraphQLite\InvalidCallableRuntimeException;
use TheCodingMachine\GraphQLite\InvalidPrefetchMethodRuntimeException;
use TheCodingMachine\GraphQLite\ParameterizedCallableResolver;
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
use TheCodingMachine\GraphQLite\Parameters\PrefetchDataParameter;

use function assert;

/**
* Handles {@see Prefetch} annotated parameters.
*/
class PrefetchParameterMiddleware implements ParameterMiddlewareInterface
{
public function __construct(
private readonly ParameterizedCallableResolver $parameterizedCallableResolver,
)
{
}

public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, Type|null $paramTagType, ParameterAnnotations $parameterAnnotations, ParameterHandlerInterface $next): ParameterInterface
{
$prefetch = $parameterAnnotations->getAnnotationByType(Prefetch::class);

if ($prefetch === null) {
return $next->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations);
}

$method = $parameter->getDeclaringFunction();

assert($method instanceof ReflectionMethod);

// Map callable specified by #[Prefetch] into a real callable and parse all of the GraphQL parameters.
try {
[$resolver, $parameters] = $this->parameterizedCallableResolver->resolve($prefetch->callable, $method->getDeclaringClass(), 1);
} catch (InvalidCallableRuntimeException $e) {
throw InvalidPrefetchMethodRuntimeException::fromInvalidCallable($method, $parameter->getName(), $e);
}

return new PrefetchDataParameter(
fieldName: $method->getName(),
resolver: $resolver,
parameters: $parameters,
);
}
}
60 changes: 60 additions & 0 deletions src/ParameterizedCallableResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite;

use Psr\Container\ContainerInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;

use function assert;
use function is_callable;
use function is_string;

class ParameterizedCallableResolver
{
public function __construct(
private readonly FieldsBuilder $fieldsBuilder,
private readonly ContainerInterface $container,
)
{
}

/**
* @param string|array{class-string, string} $callable
*
* @return array{callable, array<string, ParameterInterface>}
*/
public function resolve(string|array $callable, string|ReflectionClass $classContext, int $skip = 0): array
{
if ($classContext instanceof ReflectionClass) {
$classContext = $classContext->getName();
}

// If string method is given, it's equivalent to [self::class, 'method']
if (is_string($callable)) {
$callable = [$classContext, $callable];
}

try {
$refMethod = new ReflectionMethod($callable[0], $callable[1]);
} catch (ReflectionException $e) {
throw InvalidCallableRuntimeException::methodNotFound($callable[0], $callable[1], $e);
}

// If method isn't static, then we should try to resolve the class name through the container.
if (! $refMethod->isStatic()) {
$callable = fn (...$args) => $this->container->get($callable[0])->{$callable[1]}(...$args);
}

assert(is_callable($callable));

// Map all parameters of the callable.
$parameters = $this->fieldsBuilder->getParameters($refMethod, $skip);

return [$callable, $parameters];
}
}
11 changes: 11 additions & 0 deletions src/Parameters/ExpandsInputTypeParameters.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Parameters;

interface ExpandsInputTypeParameters
{
/** @return array<string, InputTypeParameterInterface> */
public function toInputTypeParameters(): array;
}
Loading