Skip to content

Commit ec0dec6

Browse files
committed
Fix callable type mapping to be Closure mapping
1 parent 5403473 commit ec0dec6

5 files changed

Lines changed: 34 additions & 9 deletions

File tree

src/Mappers/Root/CallableTypeMapper.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ public function toGraphQLOutputType(Type $type, OutputType|null $subType, Reflec
4242
throw CannotMapTypeException::createForMissingCallableReturnType();
4343
}
4444

45+
// It would also be a good idea to check if the type-hint is actually `Closure(): something`,
46+
// not `callable(): something`, because the latter is currently not supported. But to do so,
47+
// `phpDocumentor` would need to pass in the type of callable, which it doesn't. All
48+
// types that look like callables - are reported as `callable` by phpDocumentor.
49+
// The reason for such a check is that any string may be a callable (referring to a global function),
50+
// so if a string that looks like a callable is returned from a resolver, it will get wrapped
51+
// in `Deferred`, even though it wasn't supposed to be a deferred value. This could be fixed
52+
// by combining `QueryField`'s resolver and `CallableTypeMapper` into one place, but
53+
// that's not currently possible with GraphQLite's design.
54+
4555
return $this->topRootTypeMapper->toGraphQLOutputType($returnType, null, $reflector, $docBlockObj);
4656
}
4757

src/QueryField.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ private function resolveWithPromise(mixed $result, ResolverInterface $originalRe
9797
{
9898
// Shorthand for deferring field execution. This does two things:
9999
// - 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)) {
100+
// - allows inferring the type from PHPDoc (Closure(): Type), unlike Deferred, which is not generic
101+
if ($result instanceof Closure) {
102102
$result = new Deferred($result);
103103
}
104104

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/QueryFieldTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function testParametersDescription(): void
4949
$this->assertEquals('Foo argument', $queryField->args[0]->description);
5050
}
5151

52-
public function testWrapsCallableInDeferred(): void
52+
public function testWrapsClosureInDeferred(): void
5353
{
5454
$sourceResolver = new ServiceResolver(static fn () => function () {
5555
return 123;

website/docs/type-mapping.mdx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,22 @@ query {
332332

333333
## Promise mapping
334334

335-
You can defer execution of fields by returning a callable. To specify the field type, add a `@return` PHPDoc annotation
336-
with a return type, like so: `@return callable(): YourTypeHere`. The callable must not have any parameters.
335+
You can defer execution of fields by returning a `Closure`. To specify the field type, add a `@return` PHPDoc annotation
336+
with a return type, like so: `@return Closure(): YourTypeHere`. The closure must not have any parameters.
337+
338+
:::caution
339+
340+
Only `Closure` type is supported, which means that all of the following will work:
341+
- arrow functions: `fn () => 123`
342+
- anonymous functions: `function () { return 123; }`
343+
- first-class callables: `random_int(...)`, `Integer::random(...)` etc
344+
345+
But other callables **will not**:
346+
- callable strings: `'random_int'`, `'Integer::random'`
347+
- callable arrays: `[Integer::class, 'random']`, `[$object, 'method']`
348+
- invokable objects: `new class { function __invoke() { return 123; } }`
349+
350+
:::
337351

338352
An alternative way is to return `\GraphQL\Deferred` instances, along with specifying the type through the
339353
`outputType` parameter of field attributes: `#[Field(outputType: SomeGQLType)]`.
@@ -348,10 +362,10 @@ class Product
348362
// ...
349363

350364
/**
351-
* @return callable(): string
365+
* @return Closure(): string
352366
*/
353367
#[Field]
354-
public function getName(): callable
368+
public function getName(): Closure
355369
{
356370
return fn() => $this->name;
357371
}

0 commit comments

Comments
 (0)