Skip to content

Commit

Permalink
Add support for void return type (#574)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
oprypkhantc committed Mar 13, 2023
1 parent db4d604 commit 5a9c03b
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 19 deletions.
42 changes: 42 additions & 0 deletions src/Mappers/Root/LastDelegatingTypeMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Mappers\Root;

use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\NamedType;
use GraphQL\Type\Definition\OutputType;
use GraphQL\Type\Definition\Type as GraphQLType;
use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\Type;
use ReflectionMethod;
use ReflectionProperty;

/**
* The last root type mapper that always calls the dynamicly set "next" mapper.
*/
class LastDelegatingTypeMapper implements RootTypeMapperInterface
{
private RootTypeMapperInterface $next;

public function setNext(RootTypeMapperInterface $next): void
{
$this->next = $next;
}

public function toGraphQLOutputType(Type $type, OutputType|null $subType, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): OutputType&GraphQLType
{
return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj);
}

public function toGraphQLInputType(Type $type, InputType|null $subType, string $argumentName, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): InputType&GraphQLType
{
return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj);
}

public function mapNameToType(string $typeName): NamedType&GraphQLType
{
return $this->next->mapNameToType($typeName);
}
}
11 changes: 5 additions & 6 deletions src/Mappers/Root/NullableTypeMapperAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,14 @@
use function iterator_to_array;

/**
* This root type mapper is the very first type mapper that must be called.
* It handles the "compound" types and is in charge of creating Union Types and detecting subTypes (for arrays)
* This root type mapper wraps types as "non nullable" if the corresponding PHPDoc type doesn't allow null.
*/
class NullableTypeMapperAdapter implements RootTypeMapperInterface
{
private RootTypeMapperInterface $next;

public function setNext(RootTypeMapperInterface $next): void
public function __construct(
private readonly RootTypeMapperInterface $next,
)
{
$this->next = $next;
}

public function toGraphQLOutputType(Type $type, OutputType|GraphQLType|null $subType, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): OutputType&GraphQLType
Expand Down Expand Up @@ -109,6 +107,7 @@ private function isNullable(Type $docBlockTypeHint): bool
if ($docBlockTypeHint instanceof Null_ || $docBlockTypeHint instanceof Nullable) {
return true;
}

if ($docBlockTypeHint instanceof Compound) {
foreach ($docBlockTypeHint as $type) {
if ($this->isNullable($type)) {
Expand Down
59 changes: 59 additions & 0 deletions src/Mappers/Root/VoidTypeMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Mappers\Root;

use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\NamedType;
use GraphQL\Type\Definition\OutputType;
use GraphQL\Type\Definition\Type as GraphQLType;
use phpDocumentor\Reflection\DocBlock;
use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\Types\Void_;
use ReflectionMethod;
use ReflectionProperty;
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException;
use TheCodingMachine\GraphQLite\Types\VoidType;

class VoidTypeMapper implements RootTypeMapperInterface
{
private static VoidType $voidType;

public function __construct(
private readonly RootTypeMapperInterface $next,
)
{
}

public function toGraphQLOutputType(Type $type, OutputType|null $subType, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): OutputType&GraphQLType
{
if (! $type instanceof Void_) {
return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj);
}

return self::getVoidType();
}

public function toGraphQLInputType(Type $type, InputType|null $subType, string $argumentName, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): InputType&GraphQLType
{
if (! $type instanceof Void_) {
return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj);
}

throw CannotMapTypeException::mustBeOutputType(self::getVoidType()->name);
}

public function mapNameToType(string $typeName): NamedType&GraphQLType
{
return match ($typeName) {
self::getVoidType()->name => self::getVoidType(),
default => $this->next->mapNameToType($typeName),
};
}

private static function getVoidType(): VoidType
{
return self::$voidType ??= new VoidType();
}
}
8 changes: 6 additions & 2 deletions src/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@
use TheCodingMachine\GraphQLite\Mappers\Root\EnumTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\FinalRootTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\IteratorTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\LastDelegatingTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\MyCLabsEnumTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\NullableTypeMapperAdapter;
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperFactoryContext;
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperFactoryInterface;
use TheCodingMachine\GraphQLite\Mappers\Root\VoidTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\TypeMapperFactoryInterface;
use TheCodingMachine\GraphQLite\Mappers\TypeMapperInterface;
use TheCodingMachine\GraphQLite\Middlewares\AuthorizationFieldMiddleware;
Expand Down Expand Up @@ -376,7 +378,9 @@ public function createSchema(): Schema
$compositeTypeMapper = new CompositeTypeMapper();
$recursiveTypeMapper = new RecursiveTypeMapper($compositeTypeMapper, $namingStrategy, $namespacedCache, $typeRegistry, $annotationReader);

$topRootTypeMapper = new NullableTypeMapperAdapter();
$lastTopRootTypeMapper = new LastDelegatingTypeMapper();
$topRootTypeMapper = new NullableTypeMapperAdapter($lastTopRootTypeMapper);
$topRootTypeMapper = new VoidTypeMapper($topRootTypeMapper);

$errorRootTypeMapper = new FinalRootTypeMapper($recursiveTypeMapper);
$rootTypeMapper = new BaseTypeMapper($errorRootTypeMapper, $recursiveTypeMapper, $topRootTypeMapper);
Expand Down Expand Up @@ -410,7 +414,7 @@ public function createSchema(): Schema
$rootTypeMapper = new CompoundTypeMapper($rootTypeMapper, $topRootTypeMapper, $namingStrategy, $typeRegistry, $recursiveTypeMapper);
$rootTypeMapper = new IteratorTypeMapper($rootTypeMapper, $topRootTypeMapper);

$topRootTypeMapper->setNext($rootTypeMapper);
$lastTopRootTypeMapper->setNext($rootTypeMapper);

$argumentResolver = new ArgumentResolver();

Expand Down
32 changes: 32 additions & 0 deletions src/Types/VoidType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Types;

use GraphQL\Language\AST\Node;
use GraphQL\Type\Definition\ScalarType;
use TheCodingMachine\GraphQLite\GraphQLRuntimeException;

class VoidType extends ScalarType
{
public string $name = 'Void';

public string|null $description = 'The `Void` scalar type represents no value being returned.';

public function serialize(mixed $value): bool|null
{
// Return type contains `bool` because `null` is only allowed as a standalone type since PHP 8.2.
return null;
}

public function parseValue(mixed $value): never
{
throw new GraphQLRuntimeException();
}

public function parseLiteral(Node $valueNode, array|null $variables = null): never
{
throw new GraphQLRuntimeException();
}
}
9 changes: 7 additions & 2 deletions tests/AbstractQueryProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@
use TheCodingMachine\GraphQLite\Mappers\Root\EnumTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\FinalRootTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\IteratorTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\LastDelegatingTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\MyCLabsEnumTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\NullableTypeMapperAdapter;
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperInterface;
use TheCodingMachine\GraphQLite\Mappers\Root\VoidTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\TypeMapperInterface;
use TheCodingMachine\GraphQLite\Containers\BasicAutoWiringContainer;
use TheCodingMachine\GraphQLite\Middlewares\AuthorizationFieldMiddleware;
Expand Down Expand Up @@ -324,7 +326,9 @@ protected function buildRootTypeMapper(): RootTypeMapperInterface
$arrayAdapter = new ArrayAdapter();
$arrayAdapter->setLogger(new ExceptionLogger());

$topRootTypeMapper = new NullableTypeMapperAdapter();
$lastTopRootTypeMapper = new LastDelegatingTypeMapper();
$topRootTypeMapper = new NullableTypeMapperAdapter($lastTopRootTypeMapper);
$topRootTypeMapper = new VoidTypeMapper($topRootTypeMapper);

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

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

$topRootTypeMapper->setNext($rootTypeMapper);
$lastTopRootTypeMapper->setNext($rootTypeMapper);

return $topRootTypeMapper;
}

Expand Down
2 changes: 1 addition & 1 deletion tests/AggregateControllerQueryProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ public function has($id):bool
$this->assertCount(9, $queries);

$mutations = $aggregateQueryProvider->getMutations();
$this->assertCount(1, $mutations);
$this->assertCount(2, $mutations);
}
}
7 changes: 6 additions & 1 deletion tests/FieldsBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
use TheCodingMachine\GraphQLite\Security\VoidAuthorizationService;
use TheCodingMachine\GraphQLite\Annotations\Query;
use TheCodingMachine\GraphQLite\Types\DateTimeType;
use TheCodingMachine\GraphQLite\Types\VoidType;

class FieldsBuilderTest extends AbstractQueryProviderTest
{
Expand Down Expand Up @@ -136,7 +137,8 @@ public function testMutations(): void

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

$this->assertCount(1, $mutations);
$this->assertCount(2, $mutations);

$mutation = $mutations['mutation'];
$this->assertSame('mutation', $mutation->name);

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

$this->assertInstanceOf(TestObject::class, $result);
$this->assertEquals('42', $result->getTest());

$testVoidMutation = $mutations['testVoid'];
$this->assertInstanceOf(VoidType::class, $testVoidMutation->getType());
}

public function testErrors(): void
Expand Down
7 changes: 7 additions & 0 deletions tests/Fixtures/TestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,11 @@ public function testFixComplexReturnType(): array
{
return ['42'];
}

/**
* @Mutation
*/
public function testVoid(): void
{
}
}
6 changes: 6 additions & 0 deletions tests/Fixtures81/Integration/Controllers/ButtonController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use TheCodingMachine\GraphQLite\Fixtures81\Integration\Models\Color;
use TheCodingMachine\GraphQLite\Fixtures81\Integration\Models\Position;
use TheCodingMachine\GraphQLite\Fixtures81\Integration\Models\Size;
use TheCodingMachine\GraphQLite\Types\ID;

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

#[Mutation]
public function deleteButton(ID $id): void
{
}

#[Mutation]
public function singleEnum(Size $size): Size
{
Expand Down
2 changes: 1 addition & 1 deletion tests/GlobControllerQueryProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function has($id):bool
$this->assertCount(9, $queries);

$mutations = $globControllerQueryProvider->getMutations();
$this->assertCount(1, $mutations);
$this->assertCount(2, $mutations);

}
}
37 changes: 35 additions & 2 deletions tests/Integration/EndToEndTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@
use TheCodingMachine\GraphQLite\Mappers\Root\EnumTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\FinalRootTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\IteratorTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\LastDelegatingTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\MyCLabsEnumTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\NullableTypeMapperAdapter;
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperInterface;
use TheCodingMachine\GraphQLite\Mappers\Root\VoidTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\TypeMapperInterface;
use TheCodingMachine\GraphQLite\Middlewares\AuthorizationFieldMiddleware;
use TheCodingMachine\GraphQLite\Middlewares\AuthorizationInputFieldMiddleware;
Expand Down Expand Up @@ -297,7 +299,14 @@ public function createContainer(array $overloadedServices = []): ContainerInterf
return new CachedDocBlockFactory(new Psr16Cache($arrayAdapter));
},
RootTypeMapperInterface::class => static function (ContainerInterface $container) {
return new NullableTypeMapperAdapter();
return new VoidTypeMapper(
new NullableTypeMapperAdapter(
$container->get('topRootTypeMapper')
)
);
},
'topRootTypeMapper' => static function () {
return new LastDelegatingTypeMapper();
},
'rootTypeMapper' => static function (ContainerInterface $container) {
// These are in reverse order of execution
Expand Down Expand Up @@ -363,7 +372,7 @@ public function createContainer(array $overloadedServices = []): ContainerInterf
}
$container->get(TypeMapperInterface::class)->addTypeMapper($container->get(PorpaginasTypeMapper::class));

$container->get(RootTypeMapperInterface::class)->setNext($container->get('rootTypeMapper'));
$container->get('topRootTypeMapper')->setNext($container->get('rootTypeMapper'));
/*$container->get(CompositeRootTypeMapper::class)->addRootTypeMapper(new CompoundTypeMapper($container->get(RootTypeMapperInterface::class), $container->get(TypeRegistry::class), $container->get(RecursiveTypeMapperInterface::class)));
$container->get(CompositeRootTypeMapper::class)->addRootTypeMapper(new IteratorTypeMapper($container->get(RootTypeMapperInterface::class), $container->get(TypeRegistry::class), $container->get(RecursiveTypeMapperInterface::class)));
$container->get(CompositeRootTypeMapper::class)->addRootTypeMapper(new IteratorTypeMapper($container->get(RootTypeMapperInterface::class), $container->get(TypeRegistry::class), $container->get(RecursiveTypeMapperInterface::class)));
Expand Down Expand Up @@ -2490,4 +2499,28 @@ public function isAllowed(string $right, $subject = null): bool
$data = $this->getSuccessResult($result);
$this->assertSame(['graph', 'ql'], $data['updateTrickyProduct']['list']);
}

public function testEndToEndVoidResult(): void
{
$schema = $this->mainContainer->get(Schema::class);
assert($schema instanceof Schema);

$gql = '
mutation($id: ID!) {
deleteButton(id: $id)
}
';

$result = GraphQL::executeQuery(
$schema,
$gql,
variableValues: [
'id' => 123,
],
);

self::assertSame([
'deleteButton' => null,
], $this->getSuccessResult($result));
}
}
8 changes: 4 additions & 4 deletions tests/Mappers/Root/NullableTypeMapperAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,7 @@ public function testOnlyNull2(): void

public function testNonNullableReturnedByWrappedMapper(): void
{
$typeMapper = new NullableTypeMapperAdapter();

$typeMapper->setNext(new class implements RootTypeMapperInterface {
$next = new class implements RootTypeMapperInterface {

public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector, DocBlock $docBlockObj): OutputType&GraphQLType
{
Expand All @@ -80,7 +78,9 @@ public function mapNameToType(string $typeName): NamedType&GraphQLType
{
throw new \RuntimeException('Not implemented');
}
});
};

$typeMapper = new NullableTypeMapperAdapter($next);


$this->expectException(CannotMapTypeException::class);
Expand Down
Loading

0 comments on commit 5a9c03b

Please sign in to comment.