diff --git a/Makefile b/Makefile
index 04e388edd..55d700aa4 100644
--- a/Makefile
+++ b/Makefile
@@ -31,6 +31,7 @@ bench: ## Runs benchmarks with phpbench
.PHONY: docs
docs: ## Generate the class-reference docs
php generate-class-reference.php
+ prettier --write docs/class-reference.md
vendor: composer.json composer.lock
composer install
diff --git a/docs/type-definitions/object-types.md b/docs/type-definitions/object-types.md
index e073d2418..a5a98bf34 100644
--- a/docs/type-definitions/object-types.md
+++ b/docs/type-definitions/object-types.md
@@ -58,14 +58,15 @@ This example uses **inline** style for Object Type definitions, but you can also
## Configuration options
-| Option | Type | Notes |
-| ------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| name | `string` | **Required.** Unique name of this object type within Schema |
-| fields | `array` or `callable` | **Required**. An array describing object fields or callable returning such an array. See [field configuration options](#field-configuration-options) section below for expected structure of each array entry. See also the section on [Circular types](#recurring-and-circular-types) for an explanation of when to use callable for this option. |
-| description | `string` | Plain-text description of this type for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation) |
-| interfaces | `array` or `callable` | List of interfaces implemented by this type or callable returning such a list. See [Interface Types](interfaces.md) for details. See also the section on [Circular types](#recurring-and-circular-types) for an explanation of when to use callable for this option. |
-| isTypeOf | `callable` | **function ($value, $context, [ResolveInfo](../class-reference.md#graphqltypedefinitionresolveinfo) $info): bool**
Expected to return **true** if **$value** qualifies for this type (see section about [Abstract Type Resolution](interfaces.md#interface-role-in-data-fetching) for explanation). |
-| resolveField | `callable` | **function ($value, array $args, $context, [ResolveInfo](../class-reference.md#graphqltypedefinitionresolveinfo) $info): mixed**
Given the **$value** of this type, it is expected to return value for a field defined in **$info->fieldName**. A good place to define a type-specific strategy for field resolution. See section on [Data Fetching](../data-fetching.md) for details. |
+| Option | Type | Notes |
+|--------------|------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| name | `string` | **Required.** Unique name of this object type within Schema |
+| fields | `array` or `callable` | **Required**. An array describing object fields or callable returning such an array. See [field configuration options](#field-configuration-options) section below for expected structure of each array entry. See also the section on [Circular types](#recurring-and-circular-types) for an explanation of when to use callable for this option. |
+| description | `string` | Plain-text description of this type for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation) |
+| interfaces | `array` or `callable` | List of interfaces implemented by this type or callable returning such a list. See [Interface Types](interfaces.md) for details. See also the section on [Circular types](#recurring-and-circular-types) for an explanation of when to use callable for this option. |
+| isTypeOf | `callable` | **function ($value, $context, [ResolveInfo](../class-reference.md#graphqltypedefinitionresolveinfo) $info): bool**
Expected to return **true** if **$value** qualifies for this type (see section about [Abstract Type Resolution](interfaces.md#interface-role-in-data-fetching) for explanation). |
+| resolveField | `callable` | **function ($value, array $args, $context, [ResolveInfo](../class-reference.md#graphqltypedefinitionresolveinfo) $info): mixed**
Given the **$value** of this type, it is expected to return value for a field defined in **$info->fieldName**. A good place to define a type-specific strategy for field resolution. See section on [Data Fetching](../data-fetching.md) for details. |
+| visible | `bool` or `callable` | Defaults to `true`. The given callable receives no arguments and is expected to return a `bool`, it is called once when the field may be accessed. The field is treated as if it were not defined at all when this is `false`. |
### Field configuration options
diff --git a/src/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php
index fa3c63379..2aeed90b7 100644
--- a/src/Executor/ReferenceExecutor.php
+++ b/src/Executor/ReferenceExecutor.php
@@ -597,7 +597,7 @@ protected function resolveField(ObjectType $parentType, $rootValue, \ArrayObject
$fieldName = $fieldNode->name->value;
$fieldDef = $this->getFieldDef($exeContext->schema, $parentType, $fieldName);
- if ($fieldDef === null) {
+ if ($fieldDef === null || ! $fieldDef->isVisible()) {
return static::$UNDEFINED;
}
diff --git a/src/Type/Definition/FieldDefinition.php b/src/Type/Definition/FieldDefinition.php
index 7b9389b11..c36f76275 100644
--- a/src/Type/Definition/FieldDefinition.php
+++ b/src/Type/Definition/FieldDefinition.php
@@ -16,12 +16,14 @@
*
* @phpstan-type FieldType (Type&OutputType)|callable(): (Type&OutputType)
* @phpstan-type ComplexityFn callable(int, array): int
+ * @phpstan-type VisibilityFn callable(): bool
* @phpstan-type FieldDefinitionConfig array{
* name: string,
* type: FieldType,
* resolve?: FieldResolver|null,
* args?: ArgumentListConfig|null,
* description?: string|null,
+ * visible?: VisibilityFn|bool,
* deprecationReason?: string|null,
* astNode?: FieldDefinitionNode|null,
* complexity?: ComplexityFn|null
@@ -31,6 +33,7 @@
* resolve?: FieldResolver|null,
* args?: ArgumentListConfig|null,
* description?: string|null,
+ * visible?: VisibilityFn|bool,
* deprecationReason?: string|null,
* astNode?: FieldDefinitionNode|null,
* complexity?: ComplexityFn|null
@@ -64,6 +67,13 @@ class FieldDefinition
public ?string $description;
+ /**
+ * @var callable|bool
+ *
+ * @phpstan-var VisibilityFn|bool
+ */
+ public $visible;
+
public ?string $deprecationReason;
public ?FieldDefinitionNode $astNode;
@@ -94,6 +104,7 @@ public function __construct(array $config)
? Argument::listFromConfig($config['args'])
: [];
$this->description = $config['description'] ?? null;
+ $this->visible = $config['visible'] ?? true;
$this->deprecationReason = $config['deprecationReason'] ?? null;
$this->astNode = $config['astNode'] ?? null;
$this->complexityFn = $config['complexity'] ?? null;
@@ -181,6 +192,15 @@ public function getType(): Type
return $this->type ??= Schema::resolveType($this->config['type']);
}
+ public function isVisible(): bool
+ {
+ if (is_bool($this->visible)) {
+ return $this->visible;
+ }
+
+ return $this->visible = ($this->visible)();
+ }
+
public function isDeprecated(): bool
{
return (bool) $this->deprecationReason;
diff --git a/src/Type/Definition/HasFieldsType.php b/src/Type/Definition/HasFieldsType.php
index bbbfb3270..4bab0bac9 100644
--- a/src/Type/Definition/HasFieldsType.php
+++ b/src/Type/Definition/HasFieldsType.php
@@ -21,6 +21,15 @@ public function findField(string $name): ?FieldDefinition;
public function getFields(): array;
/**
+ * @throws InvariantViolation
+ *
+ * @return array
+ */
+ public function getVisibleFields(): array;
+
+ /**
+ * Get all field names, including only visible fields.
+ *
* @throws InvariantViolation
*
* @return array
diff --git a/src/Type/Definition/HasFieldsTypeImplementation.php b/src/Type/Definition/HasFieldsTypeImplementation.php
index d7478f634..023fae17a 100644
--- a/src/Type/Definition/HasFieldsTypeImplementation.php
+++ b/src/Type/Definition/HasFieldsTypeImplementation.php
@@ -78,11 +78,24 @@ public function getFields(): array
return $this->fields;
}
+ public function getVisibleFields(): array
+ {
+ return array_filter(
+ $this->getFields(),
+ fn (FieldDefinition $fieldDefinition): bool => $fieldDefinition->isVisible()
+ );
+ }
+
/** @throws InvariantViolation */
public function getFieldNames(): array
{
$this->initializeFields();
- return \array_keys($this->fields);
+ $visibleFieldNames = array_map(
+ fn (FieldDefinition $fieldDefinition): string => $fieldDefinition->getName(),
+ $this->getVisibleFields()
+ );
+
+ return array_values($visibleFieldNames);
}
}
diff --git a/src/Type/Introspection.php b/src/Type/Introspection.php
index ab6a39b98..9aaa5341a 100644
--- a/src/Type/Introspection.php
+++ b/src/Type/Introspection.php
@@ -354,13 +354,12 @@ public static function _type(): ObjectType
],
'resolve' => static function (Type $type, $args): ?array {
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
- $fields = $type->getFields();
+ $fields = $type->getVisibleFields();
if (! ($args['includeDeprecated'] ?? false)) {
- $fields = \array_filter(
+ return \array_filter(
$fields,
- static fn (FieldDefinition $field): bool => $field->deprecationReason === null
- || $field->deprecationReason === ''
+ static fn (FieldDefinition $field): bool => ! $field->isDeprecated()
);
}
@@ -397,10 +396,7 @@ public static function _type(): ObjectType
if (! ($args['includeDeprecated'] ?? false)) {
return \array_filter(
$values,
- static function (EnumValueDefinition $value): bool {
- return $value->deprecationReason === null
- || $value->deprecationReason === '';
- }
+ static fn (EnumValueDefinition $value): bool => ! $value->isDeprecated()
);
}
@@ -425,8 +421,7 @@ static function (EnumValueDefinition $value): bool {
if (! ($args['includeDeprecated'] ?? false)) {
return \array_filter(
$fields,
- static fn (InputObjectField $field): bool => $field->deprecationReason === null
- || $field->deprecationReason === '',
+ static fn (InputObjectField $field): bool => ! $field->isDeprecated(),
);
}
@@ -521,8 +516,7 @@ public static function _field(): ObjectType
if (! ($args['includeDeprecated'] ?? false)) {
return \array_filter(
$values,
- static fn (Argument $value): bool => $value->deprecationReason === null
- || $value->deprecationReason === '',
+ static fn (Argument $value): bool => ! $value->isDeprecated(),
);
}
@@ -535,8 +529,7 @@ public static function _field(): ObjectType
],
'isDeprecated' => [
'type' => Type::nonNull(Type::boolean()),
- 'resolve' => static fn (FieldDefinition $field): bool => $field->deprecationReason !== null
- && $field->deprecationReason !== '',
+ 'resolve' => static fn (FieldDefinition $field): bool => $field->isDeprecated(),
],
'deprecationReason' => [
'type' => Type::string(),
@@ -593,8 +586,7 @@ public static function _inputValue(): ObjectType
'isDeprecated' => [
'type' => Type::nonNull(Type::boolean()),
/** @param Argument|InputObjectField $inputValue */
- 'resolve' => static fn ($inputValue): bool => $inputValue->deprecationReason !== null
- && $inputValue->deprecationReason !== '',
+ 'resolve' => static fn ($inputValue): bool => $inputValue->isDeprecated(),
],
'deprecationReason' => [
'type' => Type::string(),
@@ -625,8 +617,7 @@ public static function _enumValue(): ObjectType
],
'isDeprecated' => [
'type' => Type::nonNull(Type::boolean()),
- 'resolve' => static fn (EnumValueDefinition $enumValue): bool => $enumValue->deprecationReason !== null
- && $enumValue->deprecationReason !== '',
+ 'resolve' => static fn (EnumValueDefinition $enumValue): bool => $enumValue->isDeprecated(),
],
'deprecationReason' => [
'type' => Type::string(),
diff --git a/src/Validator/Rules/FieldsOnCorrectType.php b/src/Validator/Rules/FieldsOnCorrectType.php
index 80ad50c3f..38902dbbc 100644
--- a/src/Validator/Rules/FieldsOnCorrectType.php
+++ b/src/Validator/Rules/FieldsOnCorrectType.php
@@ -20,7 +20,7 @@ public function getVisitor(QueryValidationContext $context): array
return [
NodeKind::FIELD => function (FieldNode $node) use ($context): void {
$fieldDef = $context->getFieldDef();
- if ($fieldDef !== null) {
+ if ($fieldDef !== null && $fieldDef->isVisible()) {
return;
}
diff --git a/tests/Executor/ExecutorSchemaTest.php b/tests/Executor/ExecutorSchemaTest.php
index 0fed1d087..2350b58d5 100644
--- a/tests/Executor/ExecutorSchemaTest.php
+++ b/tests/Executor/ExecutorSchemaTest.php
@@ -28,8 +28,22 @@ public function testExecutesUsingASchema(): void
'name' => 'Image',
'fields' => [
'url' => ['type' => Type::string()],
- 'width' => ['type' => Type::int()],
- 'height' => ['type' => Type::int()],
+ 'width' => [
+ 'type' => Type::int(),
+ 'visible' => fn (): bool => true,
+ ],
+ 'height' => [
+ 'type' => Type::int(),
+ 'visible' => true,
+ ],
+ 'mimetype' => [
+ 'type' => Type::string(),
+ 'visible' => fn (): bool => false,
+ ],
+ 'size' => [
+ 'type' => Type::string(),
+ 'visible' => false,
+ ],
],
]);
@@ -107,7 +121,8 @@ public function testExecutesUsingASchema(): void
pic(width: 640, height: 480) {
url,
width,
- height
+ height,
+ mimetype
},
recentArticle {
...articleFields,
@@ -222,6 +237,7 @@ private function article(string $id): array
'url' => "cdn://{$uid}",
'width' => $width,
'height' => $height,
+ 'mimetype' => 'image/gif',
];
$johnSmith = [
diff --git a/tests/StarWarsSchema.php b/tests/StarWarsSchema.php
index ccd67c33e..a2db75671 100644
--- a/tests/StarWarsSchema.php
+++ b/tests/StarWarsSchema.php
@@ -129,6 +129,11 @@ public static function build(): Schema
'type' => Type::string(),
'description' => 'All secrets about their past.',
],
+ 'secretName' => [
+ 'type' => Type::string(),
+ 'description' => 'The secret name of the character.',
+ 'visible' => false,
+ ],
];
},
'resolveType' => static function (array $obj) use (&$humanType, &$droidType): ObjectType {
diff --git a/tests/StarWarsValidationTest.php b/tests/StarWarsValidationTest.php
index 07e7a59d4..3e1a6abf3 100644
--- a/tests/StarWarsValidationTest.php
+++ b/tests/StarWarsValidationTest.php
@@ -69,6 +69,19 @@ public function testThatNonExistentFieldsAreInvalid(): void
self::assertCount(1, $errors);
}
+ public function testThatInvisibleFieldsAreInvalid(): void
+ {
+ $query = '
+ query HeroSpaceshipQuery {
+ hero {
+ secretName
+ }
+ }
+ ';
+ $errors = $this->validationErrors($query);
+ self::assertCount(1, $errors);
+ }
+
/** @see it('Requires fields on objects') */
public function testRequiresFieldsOnObjects(): void
{
diff --git a/tests/Type/IntrospectionTest.php b/tests/Type/IntrospectionTest.php
index a6402d9dc..c484db3a5 100644
--- a/tests/Type/IntrospectionTest.php
+++ b/tests/Type/IntrospectionTest.php
@@ -7,6 +7,7 @@
use GraphQL\Language\SourceLocation;
use GraphQL\Tests\ErrorHelper;
use GraphQL\Type\Definition\EnumType;
+use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
@@ -19,6 +20,7 @@
use function Safe\json_encode;
+/** @phpstan-import-type VisibilityFn from FieldDefinition */
final class IntrospectionTest extends TestCase
{
use ArraySubsetAsserts;
@@ -1725,4 +1727,59 @@ public function testExecutesAnIntrospectionQueryWithoutCallingGlobalFieldResolve
GraphQL::executeQuery($schema, $source, null, null, null, null, $fieldResolver);
self::assertEmpty($calledForFields);
}
+
+ /**
+ * @param VisibilityFn|bool $visible
+ *
+ * @dataProvider invisibleFieldDataProvider
+ */
+ public function testDoesNotExposeInvisibleFields($visible): void
+ {
+ $TestType = new ObjectType([
+ 'name' => 'TestType',
+ 'fields' => [
+ 'nonVisible' => [
+ 'type' => Type::string(),
+ 'visible' => $visible,
+ ],
+ 'visible' => [
+ 'type' => Type::string(),
+ ],
+ ],
+ ]);
+
+ $schema = new Schema(['query' => $TestType]);
+ $request = '
+ {
+ __type(name: "TestType") {
+ name
+ fields {
+ name
+ }
+ }
+ }
+ ';
+
+ $expected = [
+ 'data' => [
+ '__type' => [
+ 'name' => 'TestType',
+ 'fields' => [
+ [
+ 'name' => 'visible',
+ ],
+ ],
+ ],
+ ],
+ ];
+
+ self::assertSame($expected, GraphQL::executeQuery($schema, $request)->toArray());
+ }
+
+ /** @return iterable */
+ public static function invisibleFieldDataProvider(): iterable
+ {
+ yield [fn (): bool => false];
+ yield [false];
+ }
}
diff --git a/tests/Validator/FieldsOnCorrectTypeTest.php b/tests/Validator/FieldsOnCorrectTypeTest.php
index 8aef374f2..c0f70f344 100644
--- a/tests/Validator/FieldsOnCorrectTypeTest.php
+++ b/tests/Validator/FieldsOnCorrectTypeTest.php
@@ -367,4 +367,18 @@ public function testLimitsLotsOfFieldSuggestions(): void
)
);
}
+
+ public function testFailsValidationOnInvisibleField(): void
+ {
+ $this->expectFailsRule(
+ new FieldsOnCorrectType(),
+ <<<'GRAPHQL'
+ fragment DogWithInvisibleField on Dog {
+ __typename
+ secretName
+ }
+ GRAPHQL,
+ [$this->undefinedField('secretName', 'Dog', [], ['nickname'], 3, 3)]
+ );
+ }
}
diff --git a/tests/Validator/ValidatorTestCase.php b/tests/Validator/ValidatorTestCase.php
index 78ca2da5c..3db1c2047 100644
--- a/tests/Validator/ValidatorTestCase.php
+++ b/tests/Validator/ValidatorTestCase.php
@@ -118,6 +118,10 @@ public static function getTestSchema(): Schema
'type' => Type::boolean(),
'args' => ['x' => ['type' => Type::int()], 'y' => ['type' => Type::int()]],
],
+ 'secretName' => [
+ 'type' => Type::string(),
+ 'visible' => false,
+ ],
],
'interfaces' => [$Being, $Pet, $Canine],
]);