Skip to content

Commit 38bb248

Browse files
committed
Introduce Undefined::VALUE
1 parent cb7c195 commit 38bb248

File tree

17 files changed

+330
-33
lines changed

17 files changed

+330
-33
lines changed

src/FieldsBuilder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1140,7 +1140,7 @@ private function getInputFieldsByPropertyAnnotations(
11401140
$name = $annotation->getName() ?: $refProperty->getName();
11411141
$inputType = $annotation->getInputType();
11421142
$constructerParameters = $this->getClassConstructParameterNames($refClass);
1143-
$inputProperty = $this->typeMapper->mapInputProperty($refProperty, $docBlock, $name, $inputType, $defaultProperties[$refProperty->getName()] ?? null, $isUpdate ? true : null);
1143+
$inputProperty = $this->typeMapper->mapInputProperty($refProperty, $docBlock, $name, $inputType, $defaultProperties[$refProperty->getName()] ?? null, $isUpdate ? true : null, isset($defaultProperties[$refProperty->getName()]));
11441144

11451145
if (! $description) {
11461146
$description = $inputProperty->getDescription();

src/Mappers/Parameters/ResolveInfoParameterHandler.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,13 @@
1313
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
1414
use TheCodingMachine\GraphQLite\Parameters\ResolveInfoParameter;
1515

16-
use function assert;
17-
1816
class ResolveInfoParameterHandler implements ParameterMiddlewareInterface
1917
{
2018
public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, Type|null $paramTagType, ParameterAnnotations $parameterAnnotations, ParameterHandlerInterface $parameterMapper): ParameterInterface
2119
{
2220
$type = $parameter->getType();
23-
assert($type === null || $type instanceof ReflectionNamedType);
24-
if ($type !== null && $type->getName() === ResolveInfo::class) {
21+
22+
if ($type instanceof ReflectionNamedType && $type->getName() === ResolveInfo::class) {
2523
return new ResolveInfoParameter();
2624
}
2725

src/Mappers/Parameters/TypeHandler.php

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@
3939
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException;
4040
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface;
4141
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperInterface;
42+
use TheCodingMachine\GraphQLite\Mappers\Root\UndefinedTypeMapper;
4243
use TheCodingMachine\GraphQLite\Parameters\DefaultValueParameter;
4344
use TheCodingMachine\GraphQLite\Parameters\InputTypeParameter;
4445
use TheCodingMachine\GraphQLite\Parameters\InputTypeProperty;
4546
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
4647
use TheCodingMachine\GraphQLite\Reflection\DocBlock\DocBlockFactory;
4748
use TheCodingMachine\GraphQLite\Types\ArgumentResolver;
4849
use TheCodingMachine\GraphQLite\Types\TypeResolver;
50+
use TheCodingMachine\GraphQLite\Undefined;
4951

5052
use function array_map;
5153
use function array_unique;
@@ -177,6 +179,19 @@ public function mapParameter(
177179
return new DefaultValueParameter($parameter->getDefaultValue());
178180
}
179181

182+
$parameterType = $parameter->getType();
183+
$allowsNull = $parameterType === null || $parameterType->allowsNull();
184+
185+
if ($parameterType === null) {
186+
$phpdocType = new Mixed_();
187+
$allowsNull = false;
188+
//throw MissingTypeHintException::missingTypeHint($parameter);
189+
} else {
190+
$declaringClass = $parameter->getDeclaringClass();
191+
assert($declaringClass !== null);
192+
$phpdocType = $this->reflectionTypeToPhpDocType($parameterType, $declaringClass);
193+
}
194+
180195
$useInputType = $parameterAnnotations->getAnnotationByType(UseInputType::class);
181196
if ($useInputType !== null) {
182197
try {
@@ -186,19 +201,6 @@ public function mapParameter(
186201
throw $e;
187202
}
188203
} else {
189-
$parameterType = $parameter->getType();
190-
$allowsNull = $parameterType === null || $parameterType->allowsNull();
191-
192-
if ($parameterType === null) {
193-
$phpdocType = new Mixed_();
194-
$allowsNull = false;
195-
//throw MissingTypeHintException::missingTypeHint($parameter);
196-
} else {
197-
$declaringClass = $parameter->getDeclaringClass();
198-
assert($declaringClass !== null);
199-
$phpdocType = $this->reflectionTypeToPhpDocType($parameterType, $declaringClass);
200-
}
201-
202204
try {
203205
$declaringFunction = $parameter->getDeclaringFunction();
204206
if (! $declaringFunction instanceof ReflectionMethod) {
@@ -220,24 +222,33 @@ public function mapParameter(
220222
}
221223
}
222224

223-
$description = $this->getParameterDescriptionFromDocBlock($docBlock, $parameter);
224-
225225
$hasDefaultValue = false;
226226
$defaultValue = null;
227-
if ($parameter->allowsNull()) {
228-
$hasDefaultValue = true;
229-
}
227+
230228
if ($parameter->isDefaultValueAvailable()) {
231229
$hasDefaultValue = true;
232230
$defaultValue = $parameter->getDefaultValue();
233231
}
234232

233+
if (! $hasDefaultValue && UndefinedTypeMapper::containsUndefined($phpdocType)) {
234+
$hasDefaultValue = true;
235+
$defaultValue = Undefined::VALUE;
236+
}
237+
238+
if (! $hasDefaultValue && $parameter->allowsNull()) {
239+
$hasDefaultValue = true;
240+
$defaultValue = null;
241+
}
242+
243+
$description = $this->getParameterDescriptionFromDocBlock($docBlock, $parameter);
244+
235245
return new InputTypeParameter(
236246
name: $parameter->getName(),
237247
type: $type,
238248
description: $description,
239249
hasDefaultValue: $hasDefaultValue,
240250
defaultValue: $defaultValue,
251+
defaultValueImplicit: $defaultValue === Undefined::VALUE,
241252
argumentResolver: $this->argumentResolver,
242253
);
243254
}
@@ -307,6 +318,7 @@ public function mapInputProperty(
307318
string|null $inputTypeName = null,
308319
mixed $defaultValue = null,
309320
bool|null $isNullable = null,
321+
bool $hasDefaultValue = false,
310322
): InputTypeProperty
311323
{
312324
$docBlockComment = $docBlock->getSummary() . PHP_EOL . $docBlock->getDescription()->render();
@@ -329,23 +341,40 @@ public function mapInputProperty(
329341
$isNullable = $refProperty->getType()?->allowsNull() ?? false;
330342
}
331343

344+
$propertyType = $refProperty->getType();
345+
if ($propertyType !== null) {
346+
$phpdocType = $this->reflectionTypeToPhpDocType($propertyType, $refProperty->getDeclaringClass());
347+
} else {
348+
$phpdocType = new Mixed_();
349+
}
350+
332351
if ($inputTypeName) {
333352
$inputType = $this->typeResolver->mapNameToInputType($inputTypeName);
334353
} else {
335354
$inputType = $this->mapPropertyType($refProperty, $docBlock, true, $argumentName, $isNullable);
336355
assert($inputType instanceof InputType);
337356
}
338357

339-
$hasDefault = $defaultValue !== null || $isNullable;
358+
if (! $hasDefaultValue && $isNullable) {
359+
$hasDefaultValue = true;
360+
$defaultValue = null;
361+
}
362+
363+
if (! $hasDefaultValue && UndefinedTypeMapper::containsUndefined($phpdocType)) {
364+
$hasDefaultValue = true;
365+
$defaultValue = Undefined::VALUE;
366+
}
367+
340368
$fieldName = $argumentName ?? $refProperty->getName();
341369

342370
return new InputTypeProperty(
343371
propertyName: $refProperty->getName(),
344372
fieldName: $fieldName,
345373
type: $inputType,
346374
description: trim($docBlockComment),
347-
hasDefaultValue: $hasDefault,
375+
hasDefaultValue: $hasDefaultValue,
348376
defaultValue: $defaultValue,
377+
defaultValueImplicit: $defaultValue === Undefined::VALUE,
349378
argumentResolver: $this->argumentResolver,
350379
);
351380
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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\Compound;
14+
use phpDocumentor\Reflection\Types\Null_;
15+
use phpDocumentor\Reflection\Types\Nullable;
16+
use phpDocumentor\Reflection\Types\Object_;
17+
use ReflectionMethod;
18+
use ReflectionProperty;
19+
use TheCodingMachine\GraphQLite\Undefined;
20+
21+
use function array_map;
22+
use function array_values;
23+
use function iterator_to_array;
24+
use function mb_ltrim;
25+
26+
/**
27+
* A root type mapper for {@see Undefined} that maps replaces those with `null` as if Undefined wasn't part of the type at all.
28+
*/
29+
class UndefinedTypeMapper implements RootTypeMapperInterface
30+
{
31+
public function __construct(
32+
private readonly RootTypeMapperInterface $next,
33+
) {
34+
}
35+
36+
public function toGraphQLOutputType(Type $type, OutputType|null $subType, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): OutputType&GraphQLType
37+
{
38+
return $this->next->toGraphQLOutputType($type, $subType, $reflector, $docBlockObj);
39+
}
40+
41+
public function toGraphQLInputType(Type $type, InputType|null $subType, string $argumentName, ReflectionMethod|ReflectionProperty $reflector, DocBlock $docBlockObj): InputType&GraphQLType
42+
{
43+
$type = self::replaceUndefinedWith($type);
44+
45+
return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj);
46+
}
47+
48+
public function mapNameToType(string $typeName): NamedType&GraphQLType
49+
{
50+
return $this->next->mapNameToType($typeName);
51+
}
52+
53+
/**
54+
* Replaces types like this: `int|Undefined` to `int|null`
55+
*/
56+
public static function replaceUndefinedWith(Type $type, Type $replaceWith = new Null_()): Type
57+
{
58+
if ($type instanceof Object_ && mb_ltrim((string) $type->getFqsen(), '\\') === Undefined::class) {
59+
return $replaceWith;
60+
}
61+
62+
if ($type instanceof Nullable) {
63+
return new Nullable(self::replaceUndefinedWith($type->getActualType(), $replaceWith));
64+
}
65+
66+
if ($type instanceof Compound) {
67+
$types = array_map(static fn (Type $type) => self::replaceUndefinedWith($type, $replaceWith), iterator_to_array($type));
68+
69+
return new Compound(array_values($types));
70+
}
71+
72+
return $type;
73+
}
74+
75+
public static function containsUndefined(Type $type): bool
76+
{
77+
return (string) $type !== (string) self::replaceUndefinedWith($type);
78+
}
79+
}

src/Parameters/InputTypeParameter.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
use TheCodingMachine\GraphQLite\Types\ArgumentResolver;
1111
use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputObjectType;
1212

13+
use function array_key_exists;
14+
1315
class InputTypeParameter implements InputTypeParameterInterface
1416
{
1517
public function __construct(
@@ -18,6 +20,7 @@ public function __construct(
1820
private readonly string|null $description,
1921
private readonly bool $hasDefaultValue,
2022
private readonly mixed $defaultValue,
23+
private readonly bool $defaultValueImplicit,
2124
private readonly ArgumentResolver $argumentResolver,
2225
)
2326
{
@@ -26,7 +29,7 @@ public function __construct(
2629
/** @param array<string, mixed> $args */
2730
public function resolve(object|null $source, array $args, mixed $context, ResolveInfo $info): mixed
2831
{
29-
if (isset($args[$this->name])) {
32+
if (array_key_exists($this->name, $args)) {
3033
return $this->argumentResolver->resolve($source, $args[$this->name], $context, $info, $this->type);
3134
}
3235

@@ -55,12 +58,18 @@ public function getType(): InputType&Type
5558

5659
public function hasDefaultValue(): bool
5760
{
58-
return $this->hasDefaultValue;
61+
// Unfortunately, we can't treat Undefined as a regular kind of default value. In this context,
62+
// $defaultValue refers to the default value on GraphQL level - e.g. the value that's printed
63+
// into the schema, returned in introspection and substituted by webonyx/graphql when a GraphQL
64+
// query is executed. Unlike regular defaults, this one shouldn't be treated as such -
65+
// because GraphQL itself doesn't have a concept of undefined values, at least not on Schema level.
66+
// It would fail to serialize during printing/introspection.
67+
return $this->hasDefaultValue && ! $this->defaultValueImplicit;
5968
}
6069

6170
public function getDefaultValue(): mixed
6271
{
63-
return $this->defaultValue;
72+
return ! $this->defaultValueImplicit ? $this->defaultValue : null;
6473
}
6574

6675
public function getDescription(): string

src/Parameters/InputTypeProperty.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public function __construct(
1717
string $description,
1818
bool $hasDefaultValue,
1919
mixed $defaultValue,
20+
bool $defaultValueImplicit,
2021
ArgumentResolver $argumentResolver,
2122
)
2223
{
@@ -26,6 +27,7 @@ public function __construct(
2627
$description,
2728
$hasDefaultValue,
2829
$defaultValue,
30+
$defaultValueImplicit,
2931
$argumentResolver,
3032
);
3133
}

src/SchemaFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
use TheCodingMachine\GraphQLite\Mappers\Root\NullableTypeMapperAdapter;
4646
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperFactoryContext;
4747
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperFactoryInterface;
48+
use TheCodingMachine\GraphQLite\Mappers\Root\UndefinedTypeMapper;
4849
use TheCodingMachine\GraphQLite\Mappers\Root\VoidTypeMapper;
4950
use TheCodingMachine\GraphQLite\Mappers\TypeMapperFactoryInterface;
5051
use TheCodingMachine\GraphQLite\Mappers\TypeMapperInterface;
@@ -399,6 +400,7 @@ public function createSchema(): Schema
399400

400401
$lastTopRootTypeMapper = new LastDelegatingTypeMapper();
401402
$topRootTypeMapper = new NullableTypeMapperAdapter($lastTopRootTypeMapper);
403+
$topRootTypeMapper = new UndefinedTypeMapper($topRootTypeMapper);
402404
$topRootTypeMapper = new VoidTypeMapper($topRootTypeMapper);
403405
$topRootTypeMapper = new ClosureTypeMapper($topRootTypeMapper, $lastTopRootTypeMapper);
404406

src/Types/ArgumentResolver.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ class ArgumentResolver
3232
*/
3333
public function resolve(object|null $source, mixed $val, mixed $context, ResolveInfo $resolveInfo, InputType&Type $type): mixed
3434
{
35+
if ($val === null && ! $type instanceof NonNull) {
36+
return null;
37+
}
38+
3539
$type = $this->stripNonNullType($type);
40+
3641
if ($type instanceof ListOfType) {
3742
if (! is_array($val)) {
3843
throw new InvalidArgumentException('Expected GraphQL List but value passed is not an array.');

src/Undefined.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite;
6+
7+
/**
8+
* Represents a special marker type used to distinguish between an explicitly
9+
* provided `null` value and an absent (missing) field in the input payload.
10+
*/
11+
enum Undefined
12+
{
13+
case VALUE;
14+
}

tests/AbstractQueryProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
use TheCodingMachine\GraphQLite\Mappers\Root\MyCLabsEnumTypeMapper;
5151
use TheCodingMachine\GraphQLite\Mappers\Root\NullableTypeMapperAdapter;
5252
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperInterface;
53+
use TheCodingMachine\GraphQLite\Mappers\Root\UndefinedTypeMapper;
5354
use TheCodingMachine\GraphQLite\Mappers\Root\VoidTypeMapper;
5455
use TheCodingMachine\GraphQLite\Mappers\TypeMapperInterface;
5556
use TheCodingMachine\GraphQLite\Middlewares\AuthorizationFieldMiddleware;
@@ -359,6 +360,7 @@ protected function buildRootTypeMapper(): RootTypeMapperInterface
359360

360361
$lastTopRootTypeMapper = new LastDelegatingTypeMapper();
361362
$topRootTypeMapper = new NullableTypeMapperAdapter($lastTopRootTypeMapper);
363+
$topRootTypeMapper = new UndefinedTypeMapper($topRootTypeMapper);
362364
$topRootTypeMapper = new VoidTypeMapper($topRootTypeMapper);
363365
$topRootTypeMapper = new ClosureTypeMapper($topRootTypeMapper, $lastTopRootTypeMapper);
364366

0 commit comments

Comments
 (0)