Skip to content

Commit e33f587

Browse files
authored
Fix callable type mapping to be Closure mapping (#753)
* Fix callable type mapping to be Closure mapping * Fix broken callable mapping
1 parent 5403473 commit e33f587

File tree

11 files changed

+188
-103
lines changed

11 files changed

+188
-103
lines changed

src/Mappers/CannotMapTypeException.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -181,18 +181,23 @@ public static function createForNonNullReturnByTypeMapper(): self
181181
return new self('a type mapper returned a GraphQL\Type\Definition\NonNull instance. All instances returned by type mappers should be nullable. It is the role of the NullableTypeMapperAdapter class to make a GraphQL type in a "NonNull". Note: this is an error in the TypeMapper code or in GraphQLite itself. Please check your custom type mappers or open an issue on GitHub if you don\'t have any custom type mapper.');
182182
}
183183

184-
public static function createForUnexpectedCallableParameters(): self
184+
public static function createForUnexpectedCallable(): self
185185
{
186-
return new self('callable() type-hint must not specify any parameters.');
186+
return new self('callable() type-hint is not supported. Use Closure: Closure(): int');
187187
}
188188

189-
public static function createForMissingCallableReturnType(): self
189+
public static function createForUnexpectedClosureParameters(): self
190190
{
191-
return new self('callable() type-hint must specify its return type. For instance: callable(): int');
191+
return new self('Closure() type-hint must not specify any parameters.');
192192
}
193193

194-
public static function createForCallableAsInput(): self
194+
public static function createForMissingClosureReturnType(): self
195195
{
196-
return new self('callable() type-hint can only be used as output type.');
196+
return new self('Closure() type-hint must specify its return type. For instance: Closure(): int');
197+
}
198+
199+
public static function createForClosureAsInput(): self
200+
{
201+
return new self('Closure() type-hint can only be used as output type.');
197202
}
198203
}

src/Mappers/Root/CallableTypeMapper.php

Lines changed: 0 additions & 61 deletions
This file was deleted.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Mappers\Root;
6+
7+
use Closure;
8+
use GraphQL\Type\Definition\InputType;
9+
use GraphQL\Type\Definition\NamedType;
10+
use GraphQL\Type\Definition\OutputType;
11+
use GraphQL\Type\Definition\Type as GraphQLType;
12+
use phpDocumentor\Reflection\DocBlock;
13+
use phpDocumentor\Reflection\Fqsen;
14+
use phpDocumentor\Reflection\Type;
15+
use phpDocumentor\Reflection\Types\Callable_;
16+
use phpDocumentor\Reflection\Types\Compound;
17+
use phpDocumentor\Reflection\Types\Object_;
18+
use ReflectionMethod;
19+
use ReflectionProperty;
20+
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException;
21+
22+
use function count;
23+
use function iterator_to_array;
24+
25+
/**
26+
* This mapper maps callable types into their return types, so that fields can defer their execution.
27+
*/
28+
class ClosureTypeMapper implements RootTypeMapperInterface
29+
{
30+
private Object_ $closureType;
31+
32+
public function __construct(
33+
private readonly RootTypeMapperInterface $next,
34+
private readonly RootTypeMapperInterface $topRootTypeMapper,
35+
) {
36+
$this->closureType = new Object_(new Fqsen('\\' . Closure::class));
37+
}
38+
39+
public function toGraphQLOutputType(Type $type, OutputType|null $subType, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): OutputType&GraphQLType
40+
{
41+
// This check exists because any string may be a callable (referring to a global function),
42+
// so if a string that looks like a callable is returned from a resolver, it will get wrapped
43+
// in `Deferred`, even though it wasn't supposed to be a deferred value. This could be fixed
44+
// by combining `QueryField`'s resolver and `CallableTypeMapper` into one place, but
45+
// that's not currently possible with GraphQLite's design.
46+
if ($type instanceof Callable_) {
47+
throw CannotMapTypeException::createForUnexpectedCallable();
48+
}
49+
50+
if (! $type instanceof Compound || ! $type->contains($this->closureType)) {
51+
return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj);
52+
}
53+
54+
$allTypes = iterator_to_array($type);
55+
56+
if (count($allTypes) !== 2) {
57+
return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj);
58+
}
59+
60+
$callableType = $this->findCallableType($allTypes);
61+
$returnType = $callableType?->getReturnType();
62+
63+
if (! $returnType) {
64+
throw CannotMapTypeException::createForMissingClosureReturnType();
65+
}
66+
67+
if ($callableType->getParameters()) {
68+
throw CannotMapTypeException::createForUnexpectedClosureParameters();
69+
}
70+
71+
return $this->topRootTypeMapper->toGraphQLOutputType($returnType, null, $reflector, $docBlockObj);
72+
}
73+
74+
public function toGraphQLInputType(Type $type, InputType|null $subType, string $argumentName, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): InputType&GraphQLType
75+
{
76+
if (! $type instanceof Callable_) {
77+
return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj);
78+
}
79+
80+
throw CannotMapTypeException::createForClosureAsInput();
81+
}
82+
83+
public function mapNameToType(string $typeName): NamedType&GraphQLType
84+
{
85+
return $this->next->mapNameToType($typeName);
86+
}
87+
88+
/** @param array<int, Type> $types */
89+
private function findCallableType(array $types): Callable_|null
90+
{
91+
foreach ($types as $type) {
92+
if ($type instanceof Callable_) {
93+
return $type;
94+
}
95+
}
96+
97+
return null;
98+
}
99+
}

src/QueryField.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
2222
use TheCodingMachine\GraphQLite\Parameters\SourceParameter;
2323

24-
use function is_callable;
25-
2624
/**
2725
* A GraphQL field that maps to a PHP method automatically.
2826
*
@@ -97,8 +95,8 @@ private function resolveWithPromise(mixed $result, ResolverInterface $originalRe
9795
{
9896
// Shorthand for deferring field execution. This does two things:
9997
// - removes the dependency on `GraphQL\Deferred` from user land code
100-
// - allows inferring the type from PHPDoc (callable(): Type), unlike Deferred, which is not generic
101-
if (is_callable($result)) {
98+
// - allows inferring the type from PHPDoc (Closure(): Type), unlike Deferred, which is not generic
99+
if ($result instanceof Closure) {
102100
$result = new Deferred($result);
103101
}
104102

src/SchemaFactory.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
use TheCodingMachine\GraphQLite\Mappers\PorpaginasTypeMapper;
3636
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapper;
3737
use TheCodingMachine\GraphQLite\Mappers\Root\BaseTypeMapper;
38-
use TheCodingMachine\GraphQLite\Mappers\Root\CallableTypeMapper;
38+
use TheCodingMachine\GraphQLite\Mappers\Root\ClosureTypeMapper;
3939
use TheCodingMachine\GraphQLite\Mappers\Root\CompoundTypeMapper;
4040
use TheCodingMachine\GraphQLite\Mappers\Root\EnumTypeMapper;
4141
use TheCodingMachine\GraphQLite\Mappers\Root\FinalRootTypeMapper;
@@ -400,7 +400,7 @@ public function createSchema(): Schema
400400
$lastTopRootTypeMapper = new LastDelegatingTypeMapper();
401401
$topRootTypeMapper = new NullableTypeMapperAdapter($lastTopRootTypeMapper);
402402
$topRootTypeMapper = new VoidTypeMapper($topRootTypeMapper);
403-
$topRootTypeMapper = new CallableTypeMapper($topRootTypeMapper, $lastTopRootTypeMapper);
403+
$topRootTypeMapper = new ClosureTypeMapper($topRootTypeMapper, $lastTopRootTypeMapper);
404404

405405
$errorRootTypeMapper = new FinalRootTypeMapper($recursiveTypeMapper);
406406
$rootTypeMapper = new BaseTypeMapper($errorRootTypeMapper, $recursiveTypeMapper, $topRootTypeMapper);

tests/AbstractQueryProvider.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
use TheCodingMachine\GraphQLite\Mappers\Parameters\ResolveInfoParameterHandler;
4242
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapper;
4343
use TheCodingMachine\GraphQLite\Mappers\Root\BaseTypeMapper;
44-
use TheCodingMachine\GraphQLite\Mappers\Root\CallableTypeMapper;
44+
use TheCodingMachine\GraphQLite\Mappers\Root\ClosureTypeMapper;
4545
use TheCodingMachine\GraphQLite\Mappers\Root\CompoundTypeMapper;
4646
use TheCodingMachine\GraphQLite\Mappers\Root\EnumTypeMapper;
4747
use TheCodingMachine\GraphQLite\Mappers\Root\FinalRootTypeMapper;
@@ -360,7 +360,7 @@ protected function buildRootTypeMapper(): RootTypeMapperInterface
360360
$lastTopRootTypeMapper = new LastDelegatingTypeMapper();
361361
$topRootTypeMapper = new NullableTypeMapperAdapter($lastTopRootTypeMapper);
362362
$topRootTypeMapper = new VoidTypeMapper($topRootTypeMapper);
363-
$topRootTypeMapper = new CallableTypeMapper($topRootTypeMapper, $lastTopRootTypeMapper);
363+
$topRootTypeMapper = new ClosureTypeMapper($topRootTypeMapper, $lastTopRootTypeMapper);
364364

365365
$errorRootTypeMapper = new FinalRootTypeMapper($this->getTypeMapper());
366366
$rootTypeMapper = new BaseTypeMapper(

tests/Fixtures/Integration/Models/Blog.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace TheCodingMachine\GraphQLite\Fixtures\Integration\Models;
66

7+
use Closure;
78
use GraphQL\Deferred;
89
use TheCodingMachine\GraphQLite\Annotations\Field;
910
use TheCodingMachine\GraphQLite\Annotations\Prefetch;
@@ -81,9 +82,9 @@ public static function prefetchSubBlogs(iterable $blogs): array
8182
return $subBlogs;
8283
}
8384

84-
/** @return callable(): User */
85+
/** @return Closure(): User */
8586
#[Field]
86-
public function author(): callable {
87+
public function author(): Closure {
8788
return fn () => new User('Author', 'author@graphqlite');
8889
}
8990
}

tests/Integration/IntegrationTestCase.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapper;
4242
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;
4343
use TheCodingMachine\GraphQLite\Mappers\Root\BaseTypeMapper;
44-
use TheCodingMachine\GraphQLite\Mappers\Root\CallableTypeMapper;
44+
use TheCodingMachine\GraphQLite\Mappers\Root\ClosureTypeMapper;
4545
use TheCodingMachine\GraphQLite\Mappers\Root\CompoundTypeMapper;
4646
use TheCodingMachine\GraphQLite\Mappers\Root\EnumTypeMapper;
4747
use TheCodingMachine\GraphQLite\Mappers\Root\FinalRootTypeMapper;
@@ -297,7 +297,7 @@ public function createContainer(array $overloadedServices = []): ContainerInterf
297297
);
298298
},
299299
RootTypeMapperInterface::class => static function (ContainerInterface $container) {
300-
return new CallableTypeMapper(
300+
return new ClosureTypeMapper(
301301
new VoidTypeMapper(
302302
new NullableTypeMapperAdapter(
303303
$container->get('topRootTypeMapper')

0 commit comments

Comments
 (0)