Skip to content

Commit

Permalink
Allow deprecating input fields and arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
TaProhm committed May 9, 2023
1 parent dbc3ef0 commit c77dd83
Show file tree
Hide file tree
Showing 15 changed files with 533 additions and 27 deletions.
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ parameters:
path: src/Language/Visitor.php

-
message: "#^Parameter \\#1 \\$config of class GraphQL\\\\Type\\\\Definition\\\\Argument constructor expects array\\{name\\: string, type\\: \\(callable\\(\\)\\: GraphQL\\\\Type\\\\Definition\\\\InputType&GraphQL\\\\Type\\\\Definition\\\\Type\\)\\|\\(GraphQL\\\\Type\\\\Definition\\\\InputType&GraphQL\\\\Type\\\\Definition\\\\Type\\), defaultValue\\?\\: mixed, description\\?\\: string\\|null, astNode\\?\\: GraphQL\\\\Language\\\\AST\\\\InputValueDefinitionNode\\|null\\}, non\\-empty\\-array given\\.$#"
message: "#^Parameter \\#1 \\$config of class GraphQL\\\\Type\\\\Definition\\\\Argument constructor expects array\\{name\\: string, type\\: \\(callable\\(\\)\\: GraphQL\\\\Type\\\\Definition\\\\InputType&GraphQL\\\\Type\\\\Definition\\\\Type\\)\\|\\(GraphQL\\\\Type\\\\Definition\\\\InputType&GraphQL\\\\Type\\\\Definition\\\\Type\\), defaultValue\\?\\: mixed, description\\?\\: string\\|null, deprecationReason\\?\\: string\\|null, astNode\\?\\: GraphQL\\\\Language\\\\AST\\\\InputValueDefinitionNode\\|null\\}, non\\-empty\\-array given\\.$#"
count: 1
path: src/Type/Definition/Argument.php

Expand Down
14 changes: 14 additions & 0 deletions src/Type/Definition/Argument.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
* type: ArgumentType,
* defaultValue?: mixed,
* description?: string|null,
* deprecationReason?: string|null,
* astNode?: InputValueDefinitionNode|null
* }
* @phpstan-type ArgumentConfig array{
* name: string,
* type: ArgumentType,
* defaultValue?: mixed,
* description?: string|null,
* deprecationReason?: string|null,
* astNode?: InputValueDefinitionNode|null
* }
* @phpstan-type ArgumentListConfig iterable<ArgumentConfig|ArgumentType>|iterable<UnnamedArgumentConfig>
Expand All @@ -34,6 +36,8 @@ class Argument

public ?string $description;

public ?string $deprecationReason;

/** @var Type&InputType */
private Type $type;

Expand All @@ -48,6 +52,7 @@ public function __construct(array $config)
$this->name = $config['name'];
$this->defaultValue = $config['defaultValue'] ?? null;
$this->description = $config['description'] ?? null;
$this->deprecationReason = $config['deprecationReason'] ?? null;
// Do nothing for type, it is lazy loaded in getType()
$this->astNode = $config['astNode'] ?? null;

Expand Down Expand Up @@ -95,6 +100,11 @@ public function isRequired(): bool
&& ! $this->defaultValueExists();
}

public function isDeprecated(): bool
{
return (bool) $this->deprecationReason;
}

/**
* @param Type&NamedType $parentType
*
Expand All @@ -113,5 +123,9 @@ public function assertValid(FieldDefinition $parentField, Type $parentType): voi
$notInputType = Utils::printSafe($this->type);
throw new InvariantViolation("{$parentType->name}.{$parentField->name}({$this->name}): argument type must be Input Type but got: {$notInputType}");
}

if ($this->isRequired() && $this->isDeprecated()) {
throw new InvariantViolation("Required argument {$parentType->name}.{$parentField->name}({$this->name}:) cannot be deprecated.");
}
}
}
2 changes: 2 additions & 0 deletions src/Type/Definition/Directive.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ public static function getInternalDirectives(): array
'locations' => [
DirectiveLocation::FIELD_DEFINITION,
DirectiveLocation::ENUM_VALUE,
DirectiveLocation::ARGUMENT_DEFINITION,
DirectiveLocation::INPUT_FIELD_DEFINITION,
],
'args' => [
self::REASON_ARGUMENT_NAME => [
Expand Down
14 changes: 14 additions & 0 deletions src/Type/Definition/InputObjectField.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
* type: ArgumentType,
* defaultValue?: mixed,
* description?: string|null,
* deprecationReason?: string|null,
* astNode?: InputValueDefinitionNode|null
* }
* @phpstan-type UnnamedInputObjectFieldConfig array{
* name?: string,
* type: ArgumentType,
* defaultValue?: mixed,
* description?: string|null,
* deprecationReason?: string|null,
* astNode?: InputValueDefinitionNode|null
* }
*/
Expand All @@ -33,6 +35,8 @@ class InputObjectField

public ?string $description;

public ?string $deprecationReason;

/** @var Type&InputType */
private Type $type;

Expand All @@ -47,6 +51,7 @@ public function __construct(array $config)
$this->name = $config['name'];
$this->defaultValue = $config['defaultValue'] ?? null;
$this->description = $config['description'] ?? null;
$this->deprecationReason = $config['deprecationReason'] ?? null;
// Do nothing for type, it is lazy loaded in getType()
$this->astNode = $config['astNode'] ?? null;

Expand Down Expand Up @@ -74,6 +79,11 @@ public function isRequired(): bool
&& ! $this->defaultValueExists();
}

public function isDeprecated(): bool
{
return (bool) $this->deprecationReason;
}

/**
* @param Type&NamedType $parentType
*
Expand All @@ -97,5 +107,9 @@ public function assertValid(Type $parentType): void
if (\array_key_exists('resolve', $this->config)) {
throw new InvariantViolation("{$parentType->name}.{$this->name} field has a resolve property, but Input Types cannot define resolvers.");
}

if ($this->isRequired() && $this->isDeprecated()) {
throw new InvariantViolation("Required input field {$parentType->name}.{$this->name} cannot be deprecated.");
}
}
}
67 changes: 61 additions & 6 deletions src/Type/Introspection.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public static function getIntrospectionQuery(array $options = []): string
fields(includeDeprecated: true) {
name
{$descriptions}
args {
args(includeDeprecated: true) {
...InputValue
}
type {
Expand All @@ -105,7 +105,7 @@ public static function getIntrospectionQuery(array $options = []): string
isDeprecated
deprecationReason
}
inputFields {
inputFields(includeDeprecated: true) {
...InputValue
}
interfaces {
Expand All @@ -127,6 +127,8 @@ enumValues(includeDeprecated: true) {
{$descriptions}
type { ...TypeRef }
defaultValue
isDeprecated
deprecationReason
}
fragment TypeRef on __Type {
Expand Down Expand Up @@ -392,9 +394,31 @@ static function (EnumValueDefinition $value): bool {
],
'inputFields' => [
'type' => Type::listOf(Type::nonNull(self::_inputValue())),
'resolve' => static fn ($type): ?array => $type instanceof InputObjectType
? $type->getFields()
: null,
'args' => [
'includeDeprecated' => [
'type' => Type::boolean(),
'defaultValue' => false,
],
],
'resolve' => static function ($type, $args): ?array {
if ($type instanceof InputObjectType) {
$fields = $type->getFields();

if (! ($args['includeDeprecated'] ?? false)) {
return \array_filter(
$fields,
static function (InputObjectField $field): bool {
return $field->deprecationReason === null
|| $field->deprecationReason === '';
}
);
}

return $fields;
}

return null;
},
],
'ofType' => [
'type' => self::_type(),
Expand Down Expand Up @@ -469,7 +493,27 @@ public static function _field(): ObjectType
],
'args' => [
'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))),
'resolve' => static fn (FieldDefinition $field): array => $field->args,
'args' => [
'includeDeprecated' => [
'type' => Type::boolean(),
'defaultValue' => false,
],
],
'resolve' => static function (FieldDefinition $field, $args): array {
$values = $field->args;

if (! ($args['includeDeprecated'] ?? false)) {
return \array_filter(
$values,
static function (Argument $value): bool {
return $value->deprecationReason === null
|| $value->deprecationReason === '';
}
);
}

return $values;
},
],
'type' => [
'type' => Type::nonNull(self::_type()),
Expand Down Expand Up @@ -532,6 +576,17 @@ public static function _inputValue(): ObjectType
return null;
},
],
'isDeprecated' => [
'type' => Type::nonNull(Type::boolean()),
/** @param Argument|InputObjectField $inputValue */
'resolve' => static fn ($inputValue): bool => $inputValue->deprecationReason !== null
&& $inputValue->deprecationReason !== '',
],
'deprecationReason' => [
'type' => Type::string(),
/** @param Argument|InputObjectField $inputValue */
'resolve' => static fn ($inputValue): ?string => $inputValue->deprecationReason,
],
],
]);
}
Expand Down
3 changes: 2 additions & 1 deletion src/Utils/ASTDefinitionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ private function makeInputValues(NodeList $values): array
'name' => $value->name->value,
'type' => $type,
'description' => $value->description->value ?? null,
'deprecationReason' => $this->getDeprecationReason($value),
'astNode' => $value,
];

Expand Down Expand Up @@ -388,7 +389,7 @@ public function buildField(FieldDefinitionNode $field): array
* Given a collection of directives, returns the string value for the
* deprecation reason.
*
* @param EnumValueDefinitionNode|FieldDefinitionNode $node
* @param EnumValueDefinitionNode|FieldDefinitionNode|InputValueDefinitionNode $node
*
* @throws \Exception
* @throws \ReflectionException
Expand Down
2 changes: 2 additions & 0 deletions src/Utils/SchemaExtender.php
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ protected function extendInputFieldMap(InputObjectType $type): array
$newFieldConfig = [
'description' => $field->description,
'type' => $extendedType,
'deprecationReason' => $field->deprecationReason,
'astNode' => $field->astNode,
];

Expand Down Expand Up @@ -459,6 +460,7 @@ protected function extendArgs(array $args): array
$def = [
'type' => $extendedType,
'description' => $arg->description,
'deprecationReason' => $arg->deprecationReason,
'astNode' => $arg->astNode,
];

Expand Down
8 changes: 4 additions & 4 deletions src/Utils/SchemaPrinter.php
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ protected static function printArgs(array $options, array $args, string $indenta
*/
protected static function printInputValue($arg): string
{
$argDecl = "{$arg->name}: {$arg->getType()->toString()}";
$argDecl = "{$arg->name}: {$arg->getType()->toString()}" . static::printDeprecated($arg);

if ($arg->defaultValueExists()) {
$defaultValueAST = AST::astFromValue($arg->defaultValue, $arg->getType());
Expand Down Expand Up @@ -424,15 +424,15 @@ protected static function printFields(array $options, $type): string
}

/**
* @param FieldDefinition|EnumValueDefinition $fieldOrEnumVal
* @param FieldDefinition|EnumValueDefinition|InputObjectField|Argument $deprecation
*
* @throws \JsonException
* @throws InvariantViolation
* @throws SerializationError
*/
protected static function printDeprecated($fieldOrEnumVal): string
protected static function printDeprecated($deprecation): string
{
$reason = $fieldOrEnumVal->deprecationReason;
$reason = $deprecation->deprecationReason;
if ($reason === null) {
return '';
}
Expand Down
20 changes: 17 additions & 3 deletions tests/Type/DefinitionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -920,13 +920,19 @@ public function testAcceptsAnObjectTypeWithFieldArgs(): void
'goodField' => [
'type' => Type::string(),
'args' => [
'goodArg' => ['type' => Type::string()],
'goodArg' => [
'type' => Type::string(),
'deprecationReason' => 'Just because',
],
],
],
],
]);
$objType->assertValid();
self::assertDidNotCrash();
$argument = $objType->getField('goodField')->getArg('goodArg');
self::assertInstanceOf(Argument::class, $argument);
self::assertEquals(true, $argument->isDeprecated());
self::assertSame('Just because', $argument->deprecationReason);
}

// Object interfaces must be array
Expand Down Expand Up @@ -1436,12 +1442,16 @@ public function testAcceptsAnInputObjectTypeWithFields(): void
'fields' => [
$fieldName => [
'type' => Type::string(),
'deprecationReason' => 'Just because',
],
],
]);

$inputObjType->assertValid();
self::assertSame(Type::string(), $inputObjType->getField($fieldName)->getType());
$field = $inputObjType->getField($fieldName);
self::assertSame(Type::string(), $field->getType());
self::assertEquals(true, $field->isDeprecated());
self::assertSame('Just because', $field->deprecationReason);
}

/** @see it('accepts an Input Object type with a field function') */
Expand All @@ -1453,12 +1463,16 @@ public function testAcceptsAnInputObjectTypeWithAFieldFunction(): void
'fields' => static fn (): array => [
$fieldName => [
'type' => Type::string(),
'deprecationReason' => 'Just because',
],
],
]);

$inputObjType->assertValid();
$field = $inputObjType->getField($fieldName);
self::assertSame(Type::string(), $inputObjType->getField($fieldName)->getType());
self::assertEquals(true, $field->isDeprecated());
self::assertSame('Just because', $field->deprecationReason);
}

/** @see it('accepts an Input Object type with a field type function') */
Expand Down

0 comments on commit c77dd83

Please sign in to comment.