Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -1140,7 +1140,7 @@ private function getInputFieldsByPropertyAnnotations(
$name = $annotation->getName() ?: $refProperty->getName();
$inputType = $annotation->getInputType();
$constructerParameters = $this->getClassConstructParameterNames($refClass);
$inputProperty = $this->typeMapper->mapInputProperty($refProperty, $docBlock, $name, $inputType, $defaultProperties[$refProperty->getName()] ?? null, $isUpdate ? true : null);
$inputProperty = $this->typeMapper->mapInputProperty($refProperty, $docBlock, $name, $inputType, $defaultProperties[$refProperty->getName()] ?? null, $isUpdate ? true : null, isset($defaultProperties[$refProperty->getName()]));

if (! $description) {
$description = $inputProperty->getDescription();
Expand Down
6 changes: 2 additions & 4 deletions src/Mappers/Parameters/ResolveInfoParameterHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
use TheCodingMachine\GraphQLite\Parameters\ResolveInfoParameter;

use function assert;

class ResolveInfoParameterHandler implements ParameterMiddlewareInterface
{
public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, Type|null $paramTagType, ParameterAnnotations $parameterAnnotations, ParameterHandlerInterface $parameterMapper): ParameterInterface
{
$type = $parameter->getType();
assert($type === null || $type instanceof ReflectionNamedType);
if ($type !== null && $type->getName() === ResolveInfo::class) {

if ($type instanceof ReflectionNamedType && $type->getName() === ResolveInfo::class) {
return new ResolveInfoParameter();
}

Expand Down
69 changes: 49 additions & 20 deletions src/Mappers/Parameters/TypeHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException;
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface;
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperInterface;
use TheCodingMachine\GraphQLite\Mappers\Root\UndefinedTypeMapper;
use TheCodingMachine\GraphQLite\Parameters\DefaultValueParameter;
use TheCodingMachine\GraphQLite\Parameters\InputTypeParameter;
use TheCodingMachine\GraphQLite\Parameters\InputTypeProperty;
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
use TheCodingMachine\GraphQLite\Reflection\DocBlock\DocBlockFactory;
use TheCodingMachine\GraphQLite\Types\ArgumentResolver;
use TheCodingMachine\GraphQLite\Types\TypeResolver;
use TheCodingMachine\GraphQLite\Undefined;

use function array_map;
use function array_unique;
Expand Down Expand Up @@ -177,6 +179,19 @@ public function mapParameter(
return new DefaultValueParameter($parameter->getDefaultValue());
}

$parameterType = $parameter->getType();
$allowsNull = $parameterType === null || $parameterType->allowsNull();

if ($parameterType === null) {
$phpdocType = new Mixed_();
$allowsNull = false;
//throw MissingTypeHintException::missingTypeHint($parameter);
} else {
$declaringClass = $parameter->getDeclaringClass();
assert($declaringClass !== null);
$phpdocType = $this->reflectionTypeToPhpDocType($parameterType, $declaringClass);
}

$useInputType = $parameterAnnotations->getAnnotationByType(UseInputType::class);
if ($useInputType !== null) {
try {
Expand All @@ -186,19 +201,6 @@ public function mapParameter(
throw $e;
}
} else {
$parameterType = $parameter->getType();
$allowsNull = $parameterType === null || $parameterType->allowsNull();

if ($parameterType === null) {
$phpdocType = new Mixed_();
$allowsNull = false;
//throw MissingTypeHintException::missingTypeHint($parameter);
} else {
$declaringClass = $parameter->getDeclaringClass();
assert($declaringClass !== null);
$phpdocType = $this->reflectionTypeToPhpDocType($parameterType, $declaringClass);
}

try {
$declaringFunction = $parameter->getDeclaringFunction();
if (! $declaringFunction instanceof ReflectionMethod) {
Expand All @@ -220,24 +222,33 @@ public function mapParameter(
}
}

$description = $this->getParameterDescriptionFromDocBlock($docBlock, $parameter);

$hasDefaultValue = false;
$defaultValue = null;
if ($parameter->allowsNull()) {
$hasDefaultValue = true;
}

if ($parameter->isDefaultValueAvailable()) {
$hasDefaultValue = true;
$defaultValue = $parameter->getDefaultValue();
}

if (! $hasDefaultValue && UndefinedTypeMapper::containsUndefined($phpdocType)) {
$hasDefaultValue = true;
$defaultValue = Undefined::VALUE;
}

if (! $hasDefaultValue && $parameter->allowsNull()) {
$hasDefaultValue = true;
$defaultValue = null;
}

$description = $this->getParameterDescriptionFromDocBlock($docBlock, $parameter);

return new InputTypeParameter(
name: $parameter->getName(),
type: $type,
description: $description,
hasDefaultValue: $hasDefaultValue,
defaultValue: $defaultValue,
defaultValueImplicit: $defaultValue === Undefined::VALUE,
argumentResolver: $this->argumentResolver,
);
}
Expand Down Expand Up @@ -307,6 +318,7 @@ public function mapInputProperty(
string|null $inputTypeName = null,
mixed $defaultValue = null,
bool|null $isNullable = null,
bool $hasDefaultValue = false,
): InputTypeProperty
{
$docBlockComment = $docBlock->getSummary() . PHP_EOL . $docBlock->getDescription()->render();
Expand All @@ -329,23 +341,40 @@ public function mapInputProperty(
$isNullable = $refProperty->getType()?->allowsNull() ?? false;
}

$propertyType = $refProperty->getType();
if ($propertyType !== null) {
$phpdocType = $this->reflectionTypeToPhpDocType($propertyType, $refProperty->getDeclaringClass());
} else {
$phpdocType = new Mixed_();
}

if ($inputTypeName) {
$inputType = $this->typeResolver->mapNameToInputType($inputTypeName);
} else {
$inputType = $this->mapPropertyType($refProperty, $docBlock, true, $argumentName, $isNullable);
assert($inputType instanceof InputType);
}

$hasDefault = $defaultValue !== null || $isNullable;
if (! $hasDefaultValue && $isNullable) {
$hasDefaultValue = true;
$defaultValue = null;
}

if (! $hasDefaultValue && UndefinedTypeMapper::containsUndefined($phpdocType)) {
$hasDefaultValue = true;
$defaultValue = Undefined::VALUE;
}

$fieldName = $argumentName ?? $refProperty->getName();

return new InputTypeProperty(
propertyName: $refProperty->getName(),
fieldName: $fieldName,
type: $inputType,
description: trim($docBlockComment),
hasDefaultValue: $hasDefault,
hasDefaultValue: $hasDefaultValue,
defaultValue: $defaultValue,
defaultValueImplicit: $defaultValue === Undefined::VALUE,
argumentResolver: $this->argumentResolver,
);
}
Expand Down
79 changes: 79 additions & 0 deletions src/Mappers/Root/UndefinedTypeMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?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\Compound;
use phpDocumentor\Reflection\Types\Null_;
use phpDocumentor\Reflection\Types\Nullable;
use phpDocumentor\Reflection\Types\Object_;
use ReflectionMethod;
use ReflectionProperty;
use TheCodingMachine\GraphQLite\Undefined;

use function array_map;
use function array_values;
use function iterator_to_array;
use function ltrim;

/**
* A root type mapper for {@see Undefined} that maps replaces those with `null` as if Undefined wasn't part of the type at all.
*/
class UndefinedTypeMapper implements RootTypeMapperInterface
{
public function __construct(
private readonly RootTypeMapperInterface $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
{
$type = self::replaceUndefinedWith($type);

return $this->next->toGraphQLInputType($type, $subType, $argumentName, $reflector, $docBlockObj);
}

public function mapNameToType(string $typeName): NamedType&GraphQLType
{
return $this->next->mapNameToType($typeName);
}

/**
* Replaces types like this: `int|Undefined` to `int|null`
*/
public static function replaceUndefinedWith(Type $type, Type $replaceWith = new Null_()): Type
{
if ($type instanceof Object_ && ltrim((string) $type->getFqsen(), '\\') === Undefined::class) {
return $replaceWith;
}

if ($type instanceof Nullable) {
return new Nullable(self::replaceUndefinedWith($type->getActualType(), $replaceWith));
}

if ($type instanceof Compound) {
$types = array_map(static fn (Type $type) => self::replaceUndefinedWith($type, $replaceWith), iterator_to_array($type));

return new Compound(array_values($types));
}

return $type;
}

public static function containsUndefined(Type $type): bool
{
return (string) $type !== (string) self::replaceUndefinedWith($type);
}
}
15 changes: 12 additions & 3 deletions src/Parameters/InputTypeParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use TheCodingMachine\GraphQLite\Types\ArgumentResolver;
use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputObjectType;

use function array_key_exists;

class InputTypeParameter implements InputTypeParameterInterface
{
public function __construct(
Expand All @@ -18,6 +20,7 @@ public function __construct(
private readonly string|null $description,
private readonly bool $hasDefaultValue,
private readonly mixed $defaultValue,
private readonly bool $defaultValueImplicit,
private readonly ArgumentResolver $argumentResolver,
)
{
Expand All @@ -26,7 +29,7 @@ public function __construct(
/** @param array<string, mixed> $args */
public function resolve(object|null $source, array $args, mixed $context, ResolveInfo $info): mixed
{
if (isset($args[$this->name])) {
if (array_key_exists($this->name, $args)) {
return $this->argumentResolver->resolve($source, $args[$this->name], $context, $info, $this->type);
}

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

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

public function getDefaultValue(): mixed
{
return $this->defaultValue;
return ! $this->defaultValueImplicit ? $this->defaultValue : null;
}

public function getDescription(): string
Expand Down
2 changes: 2 additions & 0 deletions src/Parameters/InputTypeProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public function __construct(
string $description,
bool $hasDefaultValue,
mixed $defaultValue,
bool $defaultValueImplicit,
ArgumentResolver $argumentResolver,
)
{
Expand All @@ -26,6 +27,7 @@ public function __construct(
$description,
$hasDefaultValue,
$defaultValue,
$defaultValueImplicit,
$argumentResolver,
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
use TheCodingMachine\GraphQLite\Mappers\Root\NullableTypeMapperAdapter;
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperFactoryContext;
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperFactoryInterface;
use TheCodingMachine\GraphQLite\Mappers\Root\UndefinedTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\VoidTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\TypeMapperFactoryInterface;
use TheCodingMachine\GraphQLite\Mappers\TypeMapperInterface;
Expand Down Expand Up @@ -399,6 +400,7 @@ public function createSchema(): Schema

$lastTopRootTypeMapper = new LastDelegatingTypeMapper();
$topRootTypeMapper = new NullableTypeMapperAdapter($lastTopRootTypeMapper);
$topRootTypeMapper = new UndefinedTypeMapper($topRootTypeMapper);
$topRootTypeMapper = new VoidTypeMapper($topRootTypeMapper);
$topRootTypeMapper = new ClosureTypeMapper($topRootTypeMapper, $lastTopRootTypeMapper);

Expand Down
5 changes: 5 additions & 0 deletions src/Types/ArgumentResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ class ArgumentResolver
*/
public function resolve(object|null $source, mixed $val, mixed $context, ResolveInfo $resolveInfo, InputType&Type $type): mixed
{
if ($val === null && ! $type instanceof NonNull) {
return null;
}

$type = $this->stripNonNullType($type);

if ($type instanceof ListOfType) {
if (! is_array($val)) {
throw new InvalidArgumentException('Expected GraphQL List but value passed is not an array.');
Expand Down
14 changes: 14 additions & 0 deletions src/Undefined.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite;

/**
* Represents a special marker type used to distinguish between an explicitly
* provided `null` value and an absent (missing) field in the input payload.
*/
enum Undefined
{
case VALUE;
}
2 changes: 2 additions & 0 deletions tests/AbstractQueryProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
use TheCodingMachine\GraphQLite\Mappers\Root\MyCLabsEnumTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\NullableTypeMapperAdapter;
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperInterface;
use TheCodingMachine\GraphQLite\Mappers\Root\UndefinedTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\Root\VoidTypeMapper;
use TheCodingMachine\GraphQLite\Mappers\TypeMapperInterface;
use TheCodingMachine\GraphQLite\Middlewares\AuthorizationFieldMiddleware;
Expand Down Expand Up @@ -359,6 +360,7 @@ protected function buildRootTypeMapper(): RootTypeMapperInterface

$lastTopRootTypeMapper = new LastDelegatingTypeMapper();
$topRootTypeMapper = new NullableTypeMapperAdapter($lastTopRootTypeMapper);
$topRootTypeMapper = new UndefinedTypeMapper($topRootTypeMapper);
$topRootTypeMapper = new VoidTypeMapper($topRootTypeMapper);
$topRootTypeMapper = new ClosureTypeMapper($topRootTypeMapper, $lastTopRootTypeMapper);

Expand Down
8 changes: 7 additions & 1 deletion tests/Fixtures/Integration/Controllers/ArticleController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use TheCodingMachine\GraphQLite\Annotations\Query;
use TheCodingMachine\GraphQLite\Fixtures\Integration\Models\Article;
use TheCodingMachine\GraphQLite\Fixtures\Integration\Models\UpdateArticleInput;
use TheCodingMachine\GraphQLite\Undefined;

class ArticleController
{
Expand All @@ -32,9 +33,14 @@ public function createArticle(Article $article): Article
public function updateArticle(UpdateArticleInput $input): Article
{
$article = new Article('test');
$article->magazine = $input->magazine;
$article->magazine = 'The New Yorker';

$article->summary = $input->summary;

if ($input->magazine !== Undefined::VALUE) {
$article->magazine = $input->magazine;
}

return $article;
}
}
Loading
Loading