diff --git a/Resources/introspection.graphql b/Resources/introspection.graphql new file mode 100644 index 00000000..20b56781 --- /dev/null +++ b/Resources/introspection.graphql @@ -0,0 +1,91 @@ +query IntrospectionQuery { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + description + locations + args { + ...InputValue + } + } + } +} + +fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } +} + +fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue +} + +fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } +} diff --git a/composer.json b/composer.json index 67c87aec..9cfb0e30 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,8 @@ "./src/Type/definition.php", "./src/Type/directives.php", "./src/Type/scalars.php", - "./src/Util/helpers.php" + "./src/Util/helpers.php", + "./src/graphql.php" ], "psr-4": { "Digia\\GraphQL\\": "./src" diff --git a/src/Execution/ExecutionResult.php b/src/Execution/ExecutionResult.php index 413f128c..13fb1ae8 100644 --- a/src/Execution/ExecutionResult.php +++ b/src/Execution/ExecutionResult.php @@ -2,10 +2,12 @@ namespace Digia\GraphQL\Execution; +use Digia\GraphQL\Contract\SerializationInterface; use Digia\GraphQL\Error\GraphQLError; -class ExecutionResult +class ExecutionResult implements SerializationInterface { + /** * @var GraphQLError[] */ @@ -18,10 +20,11 @@ class ExecutionResult /** * ExecutionResult constructor. - * @param mixed[] $data + * + * @param mixed[] $data * @param GraphQLError[] $errors */ - public function __construct(array $data, array $errors) + public function __construct(?array $data, ?array $errors) { $this->errors = $errors; $this->data = $data; @@ -44,4 +47,15 @@ public function addError(GraphQLError $error): ExecutionResult $this->errors[] = $error; return $this; } + + /** + * @inheritdoc + */ + public function toArray(): array + { + return [ + 'data' => $this->data, + 'errors' => $this->errors, + ]; + } } diff --git a/src/Language/AST/Node/Behavior/NameTrait.php b/src/Language/AST/Node/Behavior/NameTrait.php index 4910eb44..1ec940bf 100644 --- a/src/Language/AST/Node/Behavior/NameTrait.php +++ b/src/Language/AST/Node/Behavior/NameTrait.php @@ -21,11 +21,11 @@ public function getName(): ?NameNode } /** - * @return string + * @return string|null */ - public function getNameValue(): string + public function getNameValue(): ?string { - return $this->name->getValue(); + return null !== $this->name ? $this->name->getValue() : null; } /** diff --git a/src/Type/Definition/Behavior/FieldsTrait.php b/src/Type/Definition/Behavior/FieldsTrait.php index b14f72ad..acf276ea 100644 --- a/src/Type/Definition/Behavior/FieldsTrait.php +++ b/src/Type/Definition/Behavior/FieldsTrait.php @@ -27,33 +27,6 @@ trait FieldsTrait */ private $_isFieldMapDefined = false; - /** - * @param Field $field - * @return $this - * @throws \Exception - */ - public function addField(Field $field) - { - $this->_fieldMap[$field->getName()] = $field; - - return $this; - } - - /** - * @param array $fields - * @return $this - * - * @throws \Exception - */ - public function addFields(array $fields) - { - foreach ($fields as $field) { - $this->addField($field); - } - - return $this; - } - /** * @return Field[] * @throws \Exception diff --git a/src/Type/TypeKindEnum.php b/src/Type/TypeKindEnum.php new file mode 100644 index 00000000..ae7aef9f --- /dev/null +++ b/src/Type/TypeKindEnum.php @@ -0,0 +1,25 @@ +getConstants()); + } +} diff --git a/src/Type/definition.php b/src/Type/definition.php index d46fa754..bae1ab18 100644 --- a/src/Type/definition.php +++ b/src/Type/definition.php @@ -11,6 +11,7 @@ use Digia\GraphQL\Type\Definition\Contract\TypeInterface; use Digia\GraphQL\Type\Definition\Contract\WrappingTypeInterface; use Digia\GraphQL\Type\Definition\EnumType; +use Digia\GraphQL\Type\Definition\Field; use Digia\GraphQL\Type\Definition\InputObjectType; use Digia\GraphQL\Type\Definition\InterfaceType; use Digia\GraphQL\Type\Definition\ListType; diff --git a/src/Type/introspection.php b/src/Type/introspection.php new file mode 100644 index 00000000..9e8ac04e --- /dev/null +++ b/src/Type/introspection.php @@ -0,0 +1,559 @@ + '__Schema', + 'isIntrospection' => true, + 'description' => + 'A GraphQL Schema defines the capabilities of a GraphQL server. It ' . + 'exposes all available types and directives on the server, as well as ' . + 'the entry points for query, mutation, and subscription operations.', + 'fields' => function () { + return [ + 'types' => [ + 'description' => 'A list of all types supported by this server.', + 'type' => GraphQLNonNull(GraphQLList(GraphQLNonNull(__Type()))), + 'resolve' => function (SchemaInterface $schema): array { + return array_values($schema->getTypeMap()); + }, + ], + 'queryType' => [ + 'description' => 'The type that query operations will be rooted at.', + 'type' => GraphQLNonNull(__Type()), + 'resolve' => function (SchemaInterface $schema): ObjectType { + return $schema->getQuery(); + }, + ], + 'mutationType' => [ + 'description' => + 'If this server supports mutation, the type that ' . + 'mutation operations will be rooted at.', + 'type' => __Type(), + 'resolve' => function (SchemaInterface $schema): ObjectType { + return $schema->getMutation(); + }, + ], + 'subscriptionType' => [ + 'description' => + 'If this server support subscription, the type that ' . + 'subscription operations will be rooted at.', + 'type' => __Type(), + 'resolve' => function (SchemaInterface $schema): ObjectType { + return $schema->getSubscription(); + }, + ], + 'directives' => [ + 'description' => 'A list of all directives supported by this server.', + 'type' => GraphQLNonNull(GraphQLList(GraphQLNonNull(__Directive()))), + 'resolve' => function (SchemaInterface $schema): array { + return $schema->getDirectives(); + }, + ], + ]; + } + ]); + } + + return $instance; +} + +/** + * @return ObjectType + */ +function __Directive(): ObjectType +{ + static $instance = null; + + if (null === $instance) { + $instance = GraphQLObjectType([ + 'name' => '__Directive', + 'isIntrospection' => true, + 'description' => + 'A Directive provides a way to describe alternate runtime execution and ' . + 'type validation behavior in a GraphQL document.' . + "\n\nIn some cases, you need to provide options to alter GraphQL's " . + 'execution behavior in ways field arguments will not suffice, such as ' . + 'conditionally including or skipping a field. Directives provide this by ' . + 'describing additional information to the executor.', + 'fields' => function () { + return [ + 'name' => ['type' => GraphQLNonNull(GraphQLString())], + 'description' => ['type' => GraphQLString()], + 'locations' => [ + 'type' => GraphQLNonNull(GraphQLList(GraphQLNonNull(__DirectiveLocation()))), + ], + 'args' => [ + 'type' => GraphQLNonNull(GraphQLList(GraphQLNonNull(__InputValue()))), + 'resolve' => function (DirectiveInterface $directive): array { + return $directive->getArgs() ?: []; + }, + ], + ]; + } + ]); + } + + return $instance; +} + +/** + * @return EnumType + */ +function __DirectiveLocation(): EnumType +{ + static $instance = null; + + if (null === $instance) { + $instance = GraphQLEnumType([ + 'name' => '__DirectiveLocation', + 'isIntrospection' => true, + 'description' => + 'A Directive can be adjacent to many parts of the GraphQL language, a ' . + '__DirectiveLocation describes one such possible adjacencies.', + 'values' => [ + DirectiveLocationEnum::QUERY => [ + 'description' => 'Location adjacent to a query operation.', + ], + DirectiveLocationEnum::MUTATION => [ + 'description' => 'Location adjacent to a mutation operation.', + ], + DirectiveLocationEnum::SUBSCRIPTION => [ + 'description' => 'Location adjacent to a subscription operation.', + ], + DirectiveLocationEnum::FIELD => [ + 'description' => 'Location adjacent to a field.', + ], + DirectiveLocationEnum::FRAGMENT_DEFINITION => [ + 'description' => 'Location adjacent to a fragment definition.', + ], + DirectiveLocationEnum::FRAGMENT_SPREAD => [ + 'description' => 'Location adjacent to a fragment spread.', + ], + DirectiveLocationEnum::INLINE_FRAGMENT => [ + 'description' => 'Location adjacent to an inline fragment.', + ], + DirectiveLocationEnum::SCHEMA => [ + 'description' => 'Location adjacent to a schema definition.', + ], + DirectiveLocationEnum::SCALAR => [ + 'description' => 'Location adjacent to a scalar definition.', + ], + DirectiveLocationEnum::OBJECT => [ + 'description' => 'Location adjacent to an object type definition.', + ], + DirectiveLocationEnum::FIELD_DEFINITION => [ + 'description' => 'Location adjacent to a field definition.', + ], + DirectiveLocationEnum::ARGUMENT_DEFINITION => [ + 'description' => 'Location adjacent to an argument definition.', + ], + DirectiveLocationEnum::INTERFACE => [ + 'description' => 'Location adjacent to an interface definition.', + ], + DirectiveLocationEnum::UNION => [ + 'description' => 'Location adjacent to a union definition.', + ], + DirectiveLocationEnum::ENUM => [ + 'description' => 'Location adjacent to an enum definition.', + ], + DirectiveLocationEnum::ENUM_VALUE => [ + 'description' => 'Location adjacent to an enum value definition.', + ], + DirectiveLocationEnum::INPUT_OBJECT => [ + 'description' => 'Location adjacent to an input object type definition.', + ], + DirectiveLocationEnum::INPUT_FIELD_DEFINITION => [ + 'description' => 'Location adjacent to an input object field definition.', + ], + ], + ]); + } + + return $instance; +} + +/** + * @return ObjectType + */ +function __Type(): ObjectType +{ + static $instance = null; + + if (null === $instance) { + $instance = GraphQLObjectType([ + 'name' => '__Type', + 'isIntrospection' => true, + 'description' => + 'The fundamental unit of any GraphQL Schema is the type. There are ' . + 'many kinds of types in GraphQL as represented by the `__TypeKind` enum.' . + '\n\nDepending on the kind of a type, certain fields describe ' . + 'information about that type. Scalar types provide no information ' . + 'beyond a name and description, while Enum types provide their values. ' . + 'Object and Interface types provide the fields they describe. Abstract ' . + 'types, Union and Interface, provide the Object types possible ' . + 'at runtime. List and NonNull types compose other types.', + 'fields' => function () { + return [ + 'kind' => [ + 'type' => GraphQLNonNull(__TypeKind()), + 'resolve' => function (TypeInterface $type) { + if ($type instanceof ScalarType) { + return TypeKindEnum::SCALAR; + } + if ($type instanceof ObjectType) { + return TypeKindEnum::OBJECT; + } + if ($type instanceof InterfaceType) { + return TypeKindEnum::INTERFACE; + } + if ($type instanceof UnionType) { + return TypeKindEnum::UNION; + } + if ($type instanceof EnumType) { + return TypeKindEnum::ENUM; + } + if ($type instanceof InputObjectType) { + return TypeKindEnum::INPUT_OBJECT; + } + if ($type instanceof ListType) { + return TypeKindEnum::LIST; + } + if ($type instanceof NonNullType) { + return TypeKindEnum::NON_NULL; + } + + throw new \Exception(sprintf('Unknown kind of type: %s', $type)); + }, + ], + 'name' => ['type' => GraphQLString()], + 'description' => ['type' => GraphQLString()], + 'fields' => [ + 'type' => GraphQLList(GraphQLNonNull(__Field())), + 'args' => [ + 'includeDeprecated' => ['type' => GraphQLBoolean(), 'defaultValue' => false], + ], + 'resolve' => function (TypeInterface $type, array $args): ?array { + list ($includeDeprecated) = $args; + + if ($type instanceof ObjectType || $type instanceof InterfaceType) { + $fields = array_values($type->getFields()); + + if (!$includeDeprecated) { + $fields = array_filter($fields, function (Field $field) { + return !$field->isDeprecated(); + }); + } + + return $fields; + } + + return null; + }, + ], + 'interfaces' => [ + 'type' => GraphQLList(GraphQLNonNull(__Type())), + 'resolve' => function (TypeInterface $type): ?array { + return $type instanceof ObjectType ? $type->getInterfaces() : null; + }, + ], + 'possibleTypes' => [ + 'type' => GraphQLList(GraphQLNonNull(__Type())), + 'resolve' => function (TypeInterface $type, $args, $context, $info): ?array { + /** @var SchemaInterface $schema */ + list ($schema) = $info; + return $type instanceof AbstractType ? $schema->getPossibleTypes($type) : null; + }, + ], + 'enumValues' => [ + 'type' => GraphQLList(GraphQLNonNull(__EnumValue())), + 'args' => [ + 'includeDeprecated' => ['type' => GraphQLBoolean(), 'defaultValue' => false], + ], + 'resolve' => function (TypeInterface $type, array $args): ?array { + list ($includeDeprecated) = $args; + + if ($type instanceof EnumType) { + $values = array_values($type->getValues()); + + if (!$includeDeprecated) { + $values = array_filter($values, function (Field $field) { + return !$field->isDeprecated(); + }); + } + + return $values; + } + + return null; + }, + ], + 'inputFields' => [ + 'type' => GraphQLList(GraphQLNonNull(__InputValue())), + 'resolve' => function (TypeInterface $type): ?array { + return $type instanceof InputObjectType ? $type->getFields() : null; + }, + ], + 'ofType' => ['type' => __Type()], + ]; + } + ]); + } + + return $instance; +} + +/** + * @return ObjectType + */ +function __Field(): ObjectType +{ + static $instance = null; + + if (null === $instance) { + $instance = GraphQLObjectType([ + 'name' => '__Field', + 'isIntrospection' => true, + 'description' => + 'Object and Interface types are described by a list of Fields, each of ' . + 'which has a name, potentially a list of arguments, and a return type.', + 'fields' => function () { + return [ + 'name' => ['type' => GraphQLNonNull(GraphQLString())], + 'description' => ['type' => GraphQLString()], + 'args' => [ + 'type' => GraphQLNonNull(GraphQLList(GraphQLNonNull(__InputValue()))), + 'resolve' => function (DirectiveInterface $directive): array { + return $directive->getArgs() ?: []; + }, + ], + 'type' => ['type' => GraphQLNonNull(__Type())], + 'isDeprecated' => ['type' => GraphQLNonNull(GraphQLBoolean())], + 'deprecationReason' => ['type' => GraphQLString()], + ]; + } + ]); + } + + return $instance; +} + +/** + * @return ObjectType + */ +function __InputValue(): ObjectType +{ + static $instance = null; + + if (null === $instance) { + $instance = GraphQLObjectType([ + 'name' => '__InputValue', + 'isIntrospection' => true, + 'description' => + 'Arguments provided to Fields or Directives and the input fields of an ' . + 'InputObject are represented as Input Values which describe their type ' . + 'and optionally a default value.', + 'fields' => function () { + return [ + 'name' => ['type' => GraphQLNonNull(GraphQLString())], + 'description' => ['type' => GraphQLString()], + 'type' => ['type' => GraphQLNonNull(__Type())], + 'defaultValue' => [ + 'type' => GraphQLString(), + 'description' => + 'A GraphQL-formatted string representing the default value for this ' . + 'input value.', + 'resolve' => function ($inputValue) { + // TODO: Implement this when we have support for printing AST. + return null; + } + ], + ]; + } + ]); + } + + return $instance; +} + +/** + * @return ObjectType + */ +function __EnumValue(): ObjectType +{ + static $instance = null; + + if (null === $instance) { + $instance = GraphQLObjectType([ + 'name' => '__EnumValue', + 'isIntrospection' => true, + 'description' => + 'One possible value for a given Enum. Enum values are unique values, not ' . + 'a placeholder for a string or numeric value. However an Enum value is ' . + 'returned in a JSON response as a string.', + 'fields' => function () { + return [ + 'name' => ['type' => GraphQLNonNull(GraphQLString())], + 'description' => ['type' => GraphQLString()], + 'isDeprecated' => ['type' => GraphQLNonNull(GraphQLBoolean())], + 'deprecationReason' => ['type' => GraphQLString()], + ]; + } + ]); + } + + return $instance; +} + +function __TypeKind(): EnumType +{ + static $instance = null; + + if (null === $instance) { + $instance = GraphQLEnumType([ + 'name' => '__TypeKind', + 'isIntrospection' => true, + 'description' => 'An enum describing what kind of type a given `__Type` is.', + 'values' => [ + TypeKindEnum::SCALAR => [ + 'description' => 'Indicates this type is a scalar.', + ], + TypeKindEnum::OBJECT => [ + 'description' => 'Indicates this type is an object. `fields` and `interfaces` are valid fields.', + ], + TypeKindEnum::INTERFACE => [ + 'description' => 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.', + ], + TypeKindEnum::UNION => [ + 'description' => 'Indicates this type is a union. `possibleTypes` is a valid field.', + ], + TypeKindEnum::ENUM => [ + 'description' => 'Indicates this type is an enum. `enumValues` is a valid field.', + ], + TypeKindEnum::INPUT_OBJECT => [ + 'description' => 'Indicates this type is an input object. `inputFields` is a valid field.', + ], + TypeKindEnum::LIST => [ + 'description' => 'Indicates this type is a list. `ofType` is a valid field.', + ], + TypeKindEnum::NON_NULL => [ + 'description' => 'Indicates this type is a non-null. `ofType` is a valid field.', + ], + ], + ]); + } + + return $instance; +} + +/** + * @return Field + * @throws \TypeError + */ +function SchemaMetaFieldDefinition(): Field +{ + return new Field([ + 'name' => '__schema', + 'type' => GraphQLNonNull(__Schema()), + 'description' => 'Access the current type schema of this server.', + 'resolve' => function ($source, $args, $context, $info): SchemaInterface { + list ($schema) = $info; + return $schema; + } + ]); +} + +/** + * @return Field + * @throws \TypeError + */ +function TypeMetaFieldDefinition(): Field +{ + return new Field([ + 'name' => '__type', + 'type' => __Type(), + 'description' => 'Request the type information of a single type.', + 'args' => [ + 'name' => ['type' => GraphQLNonNull(GraphQLString())], + ], + 'resolve' => function ($source, $args, $context, $info): TypeInterface { + /** @var SchemaInterface $schema */ + list ($name) = $args; + list ($schema) = $info; + return $schema->getType($name); + } + ]); +} + +/** + * @return Field + * @throws \TypeError + */ +function TypeNameMetaFieldDefinition(): Field +{ + return new Field([ + 'name' => '__typename', + 'type' => GraphQLNonNull(GraphQLString()), + 'description' => 'The name of the current Object type at runtime.', + 'resolve' => function ($source, $args, $context, $info): string { + /** @var TypeInterface $parentType */ + list ($parentType) = $info; + return $parentType->getName(); + } + ]); +} + +/** + * @return array + */ +function introspectionTypes(): array +{ + return [ + __Schema(), + __Directive(), + __DirectiveLocation(), + __Type(), + __Field(), + __InputValue(), + __EnumValue(), + __TypeKind(), + ]; +} + +/** + * @param TypeInterface $type + * @return bool + */ +function isIntrospectionType(TypeInterface $type): bool +{ + return arraySome( + introspectionTypes(), + function (TypeInterface $introspectionType) use ($type) { + return $type->getName() === $introspectionType->getName(); + } + ); +} diff --git a/src/graphql.php b/src/graphql.php new file mode 100644 index 00000000..8f131d5c --- /dev/null +++ b/src/graphql.php @@ -0,0 +1,40 @@ +introspectionQuery = readFile(__DIR__ . '/../../../Resources/introspection.graphql'); + } + + /** + * @throws \Digia\GraphQL\Error\GraphQLError + * @throws \Exception + */ + public function testExecutesAnIntrospectionQuery() + { + $emptySchema = GraphQLSchema([ + 'query' => GraphQLObjectType([ + 'name' => 'QueryRoot', + 'fields' => [ + 'onlyField' => ['type' => GraphQLString()], + ], + ]), + ]); + + $result = graphql($emptySchema, $this->introspectionQuery); + +// var_dump($result->toArray());die; + } +}