Skip to content

Commit 5a9c03b

Browse files
authored
Add support for void return type (#574)
* Add support for void return type * Extract VoidTypeMapper separately to make sure it only matches standalone usages in return types * Revert Types class and rename LastTopRootTypeMapper
1 parent db4d604 commit 5a9c03b

14 files changed

+245
-19
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Mappers\Root;
6+
7+
use GraphQL\Type\Definition\InputType;
8+
use GraphQL\Type\Definition\NamedType;
9+
use GraphQL\Type\Definition\OutputType;
10+
use GraphQL\Type\Definition\Type as GraphQLType;
11+
use phpDocumentor\Reflection\DocBlock;
12+
use phpDocumentor\Reflection\Type;
13+
use ReflectionMethod;
14+
use ReflectionProperty;
15+
16+
/**
17+
* The last root type mapper that always calls the dynamicly set "next" mapper.
18+
*/
19+
class LastDelegatingTypeMapper implements RootTypeMapperInterface
20+
{
21+
private RootTypeMapperInterface $next;
22+
23+
public function setNext(RootTypeMapperInterface $next): void
24+
{
25+
$this->next = $next;
26+
}
27+
28+
public function toGraphQLOutputType(Type $type, OutputType|null $subType, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): OutputType&GraphQLType
29+
{
30+
return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj);
31+
}
32+
33+
public function toGraphQLInputType(Type $type, InputType|null $subType, string $argumentName, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): InputType&GraphQLType
34+
{
35+
return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj);
36+
}
37+
38+
public function mapNameToType(string $typeName): NamedType&GraphQLType
39+
{
40+
return $this->next->mapNameToType($typeName);
41+
}
42+
}

src/Mappers/Root/NullableTypeMapperAdapter.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,14 @@
2727
use function iterator_to_array;
2828

2929
/**
30-
* This root type mapper is the very first type mapper that must be called.
31-
* It handles the "compound" types and is in charge of creating Union Types and detecting subTypes (for arrays)
30+
* This root type mapper wraps types as "non nullable" if the corresponding PHPDoc type doesn't allow null.
3231
*/
3332
class NullableTypeMapperAdapter implements RootTypeMapperInterface
3433
{
35-
private RootTypeMapperInterface $next;
36-
37-
public function setNext(RootTypeMapperInterface $next): void
34+
public function __construct(
35+
private readonly RootTypeMapperInterface $next,
36+
)
3837
{
39-
$this->next = $next;
4038
}
4139

4240
public function toGraphQLOutputType(Type $type, OutputType|GraphQLType|null $subType, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): OutputType&GraphQLType
@@ -109,6 +107,7 @@ private function isNullable(Type $docBlockTypeHint): bool
109107
if ($docBlockTypeHint instanceof Null_ || $docBlockTypeHint instanceof Nullable) {
110108
return true;
111109
}
110+
112111
if ($docBlockTypeHint instanceof Compound) {
113112
foreach ($docBlockTypeHint as $type) {
114113
if ($this->isNullable($type)) {

src/Mappers/Root/VoidTypeMapper.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Mappers\Root;
6+
7+
use GraphQL\Type\Definition\InputType;
8+
use GraphQL\Type\Definition\NamedType;
9+
use GraphQL\Type\Definition\OutputType;
10+
use GraphQL\Type\Definition\Type as GraphQLType;
11+
use phpDocumentor\Reflection\DocBlock;
12+
use phpDocumentor\Reflection\Type;
13+
use phpDocumentor\Reflection\Types\Void_;
14+
use ReflectionMethod;
15+
use ReflectionProperty;
16+
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException;
17+
use TheCodingMachine\GraphQLite\Types\VoidType;
18+
19+
class VoidTypeMapper implements RootTypeMapperInterface
20+
{
21+
private static VoidType $voidType;
22+
23+
public function __construct(
24+
private readonly RootTypeMapperInterface $next,
25+
)
26+
{
27+
}
28+
29+
public function toGraphQLOutputType(Type $type, OutputType|null $subType, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): OutputType&GraphQLType
30+
{
31+
if (! $type instanceof Void_) {
32+
return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj);
33+
}
34+
35+
return self::getVoidType();
36+
}
37+
38+
public function toGraphQLInputType(Type $type, InputType|null $subType, string $argumentName, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): InputType&GraphQLType
39+
{
40+
if (! $type instanceof Void_) {
41+
return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj);
42+
}
43+
44+
throw CannotMapTypeException::mustBeOutputType(self::getVoidType()->name);
45+
}
46+
47+
public function mapNameToType(string $typeName): NamedType&GraphQLType
48+
{
49+
return match ($typeName) {
50+
self::getVoidType()->name => self::getVoidType(),
51+
default => $this->next->mapNameToType($typeName),
52+
};
53+
}
54+
55+
private static function getVoidType(): VoidType
56+
{
57+
return self::$voidType ??= new VoidType();
58+
}
59+
}

src/SchemaFactory.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@
3131
use TheCodingMachine\GraphQLite\Mappers\Root\EnumTypeMapper;
3232
use TheCodingMachine\GraphQLite\Mappers\Root\FinalRootTypeMapper;
3333
use TheCodingMachine\GraphQLite\Mappers\Root\IteratorTypeMapper;
34+
use TheCodingMachine\GraphQLite\Mappers\Root\LastDelegatingTypeMapper;
3435
use TheCodingMachine\GraphQLite\Mappers\Root\MyCLabsEnumTypeMapper;
3536
use TheCodingMachine\GraphQLite\Mappers\Root\NullableTypeMapperAdapter;
3637
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperFactoryContext;
3738
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperFactoryInterface;
39+
use TheCodingMachine\GraphQLite\Mappers\Root\VoidTypeMapper;
3840
use TheCodingMachine\GraphQLite\Mappers\TypeMapperFactoryInterface;
3941
use TheCodingMachine\GraphQLite\Mappers\TypeMapperInterface;
4042
use TheCodingMachine\GraphQLite\Middlewares\AuthorizationFieldMiddleware;
@@ -376,7 +378,9 @@ public function createSchema(): Schema
376378
$compositeTypeMapper = new CompositeTypeMapper();
377379
$recursiveTypeMapper = new RecursiveTypeMapper($compositeTypeMapper, $namingStrategy, $namespacedCache, $typeRegistry, $annotationReader);
378380

379-
$topRootTypeMapper = new NullableTypeMapperAdapter();
381+
$lastTopRootTypeMapper = new LastDelegatingTypeMapper();
382+
$topRootTypeMapper = new NullableTypeMapperAdapter($lastTopRootTypeMapper);
383+
$topRootTypeMapper = new VoidTypeMapper($topRootTypeMapper);
380384

381385
$errorRootTypeMapper = new FinalRootTypeMapper($recursiveTypeMapper);
382386
$rootTypeMapper = new BaseTypeMapper($errorRootTypeMapper, $recursiveTypeMapper, $topRootTypeMapper);
@@ -410,7 +414,7 @@ public function createSchema(): Schema
410414
$rootTypeMapper = new CompoundTypeMapper($rootTypeMapper, $topRootTypeMapper, $namingStrategy, $typeRegistry, $recursiveTypeMapper);
411415
$rootTypeMapper = new IteratorTypeMapper($rootTypeMapper, $topRootTypeMapper);
412416

413-
$topRootTypeMapper->setNext($rootTypeMapper);
417+
$lastTopRootTypeMapper->setNext($rootTypeMapper);
414418

415419
$argumentResolver = new ArgumentResolver();
416420

src/Types/VoidType.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Types;
6+
7+
use GraphQL\Language\AST\Node;
8+
use GraphQL\Type\Definition\ScalarType;
9+
use TheCodingMachine\GraphQLite\GraphQLRuntimeException;
10+
11+
class VoidType extends ScalarType
12+
{
13+
public string $name = 'Void';
14+
15+
public string|null $description = 'The `Void` scalar type represents no value being returned.';
16+
17+
public function serialize(mixed $value): bool|null
18+
{
19+
// Return type contains `bool` because `null` is only allowed as a standalone type since PHP 8.2.
20+
return null;
21+
}
22+
23+
public function parseValue(mixed $value): never
24+
{
25+
throw new GraphQLRuntimeException();
26+
}
27+
28+
public function parseLiteral(Node $valueNode, array|null $variables = null): never
29+
{
30+
throw new GraphQLRuntimeException();
31+
}
32+
}

tests/AbstractQueryProviderTest.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
use TheCodingMachine\GraphQLite\Mappers\Root\EnumTypeMapper;
3030
use TheCodingMachine\GraphQLite\Mappers\Root\FinalRootTypeMapper;
3131
use TheCodingMachine\GraphQLite\Mappers\Root\IteratorTypeMapper;
32+
use TheCodingMachine\GraphQLite\Mappers\Root\LastDelegatingTypeMapper;
3233
use TheCodingMachine\GraphQLite\Mappers\Root\MyCLabsEnumTypeMapper;
3334
use TheCodingMachine\GraphQLite\Mappers\Root\NullableTypeMapperAdapter;
3435
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperInterface;
36+
use TheCodingMachine\GraphQLite\Mappers\Root\VoidTypeMapper;
3537
use TheCodingMachine\GraphQLite\Mappers\TypeMapperInterface;
3638
use TheCodingMachine\GraphQLite\Containers\BasicAutoWiringContainer;
3739
use TheCodingMachine\GraphQLite\Middlewares\AuthorizationFieldMiddleware;
@@ -324,7 +326,9 @@ protected function buildRootTypeMapper(): RootTypeMapperInterface
324326
$arrayAdapter = new ArrayAdapter();
325327
$arrayAdapter->setLogger(new ExceptionLogger());
326328

327-
$topRootTypeMapper = new NullableTypeMapperAdapter();
329+
$lastTopRootTypeMapper = new LastDelegatingTypeMapper();
330+
$topRootTypeMapper = new NullableTypeMapperAdapter($lastTopRootTypeMapper);
331+
$topRootTypeMapper = new VoidTypeMapper($topRootTypeMapper);
328332

329333
$errorRootTypeMapper = new FinalRootTypeMapper($this->getTypeMapper());
330334
$rootTypeMapper = new BaseTypeMapper(
@@ -359,7 +363,8 @@ protected function buildRootTypeMapper(): RootTypeMapperInterface
359363

360364
$rootTypeMapper = new IteratorTypeMapper($rootTypeMapper, $topRootTypeMapper);
361365

362-
$topRootTypeMapper->setNext($rootTypeMapper);
366+
$lastTopRootTypeMapper->setNext($rootTypeMapper);
367+
363368
return $topRootTypeMapper;
364369
}
365370

tests/AggregateControllerQueryProviderTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,6 @@ public function has($id):bool
4040
$this->assertCount(9, $queries);
4141

4242
$mutations = $aggregateQueryProvider->getMutations();
43-
$this->assertCount(1, $mutations);
43+
$this->assertCount(2, $mutations);
4444
}
4545
}

tests/FieldsBuilderTest.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
use TheCodingMachine\GraphQLite\Security\VoidAuthorizationService;
7171
use TheCodingMachine\GraphQLite\Annotations\Query;
7272
use TheCodingMachine\GraphQLite\Types\DateTimeType;
73+
use TheCodingMachine\GraphQLite\Types\VoidType;
7374

7475
class FieldsBuilderTest extends AbstractQueryProviderTest
7576
{
@@ -136,7 +137,8 @@ public function testMutations(): void
136137

137138
$mutations = $queryProvider->getMutations($controller);
138139

139-
$this->assertCount(1, $mutations);
140+
$this->assertCount(2, $mutations);
141+
140142
$mutation = $mutations['mutation'];
141143
$this->assertSame('mutation', $mutation->name);
142144

@@ -145,6 +147,9 @@ public function testMutations(): void
145147

146148
$this->assertInstanceOf(TestObject::class, $result);
147149
$this->assertEquals('42', $result->getTest());
150+
151+
$testVoidMutation = $mutations['testVoid'];
152+
$this->assertInstanceOf(VoidType::class, $testVoidMutation->getType());
148153
}
149154

150155
public function testErrors(): void

tests/Fixtures/TestController.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,11 @@ public function testFixComplexReturnType(): array
137137
{
138138
return ['42'];
139139
}
140+
141+
/**
142+
* @Mutation
143+
*/
144+
public function testVoid(): void
145+
{
146+
}
140147
}

tests/Fixtures81/Integration/Controllers/ButtonController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use TheCodingMachine\GraphQLite\Fixtures81\Integration\Models\Color;
1111
use TheCodingMachine\GraphQLite\Fixtures81\Integration\Models\Position;
1212
use TheCodingMachine\GraphQLite\Fixtures81\Integration\Models\Size;
13+
use TheCodingMachine\GraphQLite\Types\ID;
1314

1415
final class ButtonController
1516
{
@@ -25,6 +26,11 @@ public function updateButton(Color $color, Size $size, Position $state): Button
2526
return new Button($color, $size, $state);
2627
}
2728

29+
#[Mutation]
30+
public function deleteButton(ID $id): void
31+
{
32+
}
33+
2834
#[Mutation]
2935
public function singleEnum(Size $size): Size
3036
{

tests/GlobControllerQueryProviderTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public function has($id):bool
4242
$this->assertCount(9, $queries);
4343

4444
$mutations = $globControllerQueryProvider->getMutations();
45-
$this->assertCount(1, $mutations);
45+
$this->assertCount(2, $mutations);
4646

4747
}
4848
}

tests/Integration/EndToEndTest.php

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@
4848
use TheCodingMachine\GraphQLite\Mappers\Root\EnumTypeMapper;
4949
use TheCodingMachine\GraphQLite\Mappers\Root\FinalRootTypeMapper;
5050
use TheCodingMachine\GraphQLite\Mappers\Root\IteratorTypeMapper;
51+
use TheCodingMachine\GraphQLite\Mappers\Root\LastDelegatingTypeMapper;
5152
use TheCodingMachine\GraphQLite\Mappers\Root\MyCLabsEnumTypeMapper;
5253
use TheCodingMachine\GraphQLite\Mappers\Root\NullableTypeMapperAdapter;
5354
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperInterface;
55+
use TheCodingMachine\GraphQLite\Mappers\Root\VoidTypeMapper;
5456
use TheCodingMachine\GraphQLite\Mappers\TypeMapperInterface;
5557
use TheCodingMachine\GraphQLite\Middlewares\AuthorizationFieldMiddleware;
5658
use TheCodingMachine\GraphQLite\Middlewares\AuthorizationInputFieldMiddleware;
@@ -297,7 +299,14 @@ public function createContainer(array $overloadedServices = []): ContainerInterf
297299
return new CachedDocBlockFactory(new Psr16Cache($arrayAdapter));
298300
},
299301
RootTypeMapperInterface::class => static function (ContainerInterface $container) {
300-
return new NullableTypeMapperAdapter();
302+
return new VoidTypeMapper(
303+
new NullableTypeMapperAdapter(
304+
$container->get('topRootTypeMapper')
305+
)
306+
);
307+
},
308+
'topRootTypeMapper' => static function () {
309+
return new LastDelegatingTypeMapper();
301310
},
302311
'rootTypeMapper' => static function (ContainerInterface $container) {
303312
// These are in reverse order of execution
@@ -363,7 +372,7 @@ public function createContainer(array $overloadedServices = []): ContainerInterf
363372
}
364373
$container->get(TypeMapperInterface::class)->addTypeMapper($container->get(PorpaginasTypeMapper::class));
365374

366-
$container->get(RootTypeMapperInterface::class)->setNext($container->get('rootTypeMapper'));
375+
$container->get('topRootTypeMapper')->setNext($container->get('rootTypeMapper'));
367376
/*$container->get(CompositeRootTypeMapper::class)->addRootTypeMapper(new CompoundTypeMapper($container->get(RootTypeMapperInterface::class), $container->get(TypeRegistry::class), $container->get(RecursiveTypeMapperInterface::class)));
368377
$container->get(CompositeRootTypeMapper::class)->addRootTypeMapper(new IteratorTypeMapper($container->get(RootTypeMapperInterface::class), $container->get(TypeRegistry::class), $container->get(RecursiveTypeMapperInterface::class)));
369378
$container->get(CompositeRootTypeMapper::class)->addRootTypeMapper(new IteratorTypeMapper($container->get(RootTypeMapperInterface::class), $container->get(TypeRegistry::class), $container->get(RecursiveTypeMapperInterface::class)));
@@ -2490,4 +2499,28 @@ public function isAllowed(string $right, $subject = null): bool
24902499
$data = $this->getSuccessResult($result);
24912500
$this->assertSame(['graph', 'ql'], $data['updateTrickyProduct']['list']);
24922501
}
2502+
2503+
public function testEndToEndVoidResult(): void
2504+
{
2505+
$schema = $this->mainContainer->get(Schema::class);
2506+
assert($schema instanceof Schema);
2507+
2508+
$gql = '
2509+
mutation($id: ID!) {
2510+
deleteButton(id: $id)
2511+
}
2512+
';
2513+
2514+
$result = GraphQL::executeQuery(
2515+
$schema,
2516+
$gql,
2517+
variableValues: [
2518+
'id' => 123,
2519+
],
2520+
);
2521+
2522+
self::assertSame([
2523+
'deleteButton' => null,
2524+
], $this->getSuccessResult($result));
2525+
}
24932526
}

tests/Mappers/Root/NullableTypeMapperAdapterTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,7 @@ public function testOnlyNull2(): void
6262

6363
public function testNonNullableReturnedByWrappedMapper(): void
6464
{
65-
$typeMapper = new NullableTypeMapperAdapter();
66-
67-
$typeMapper->setNext(new class implements RootTypeMapperInterface {
65+
$next = new class implements RootTypeMapperInterface {
6866

6967
public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector, DocBlock $docBlockObj): OutputType&GraphQLType
7068
{
@@ -80,7 +78,9 @@ public function mapNameToType(string $typeName): NamedType&GraphQLType
8078
{
8179
throw new \RuntimeException('Not implemented');
8280
}
83-
});
81+
};
82+
83+
$typeMapper = new NullableTypeMapperAdapter($next);
8484

8585

8686
$this->expectException(CannotMapTypeException::class);

0 commit comments

Comments
 (0)