Merge pull request #248 from danez/012

Port 0.12.3 changes from graphql-js
This commit is contained in:
Vladimir Razuvaev 2018-03-26 11:43:59 +08:00 committed by GitHub
commit 45baa5f185
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
148 changed files with 11451 additions and 6590 deletions

View File

@ -1,4 +1,49 @@
## Upgrade v0.10.x > dev-master ## Upgrade v0.11.x > dev-master
### Breaking: Descriptions in comments are not used as descriptions by default anymore
Descriptions now need to be inside Strings or BlockStrings in order to be picked up as
description. If you want to keep the old behaviour you can supply the option `commentDescriptions`
to BuildSchema::buildAST(), BuildSchema::build() or Printer::doPrint().
Here is the official way now to define descriptions in the graphQL language:
Old:
```graphql
# Description
type Dog {
...
}
```
New:
```graphql
"Description"
type Dog {
...
}
"""
Long Description
"""
type Dog {
...
}
```
### Breaking: Custom types need to return `Utils::undefined()` or throw on invalid value
As null might be a valid value custom types need to return now `Utils::undefined()` or throw an
Exception inside `parseLiteral()`, `parseValue()` and `serialize()`.
Returning null from any of these methods will now be treated as valid result.
### Breaking: TypeConfigDecorator was removed from BuildSchema
TypeConfigDecorator was used as second argument in `BuildSchema::build()` and `BuildSchema::buildAST()` to
enable generated schemas with Unions or Interfaces to be used for resolving. This was fixed in a more
generalised approach so that the TypeConfigDecorator is not needed anymore and can be removed.
The concrete Types are now resolved based on the `__typename` field.
### Possibly Breaking: AST to array serialization excludes nulls ### Possibly Breaking: AST to array serialization excludes nulls
Most users won't be affected. It *may* affect you only if you do your own manipulations Most users won't be affected. It *may* affect you only if you do your own manipulations

View File

@ -171,7 +171,7 @@ static function float()
```php ```php
/** /**
* @api * @api
* @param ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType * @param Type|ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType
* @return ListOfType * @return ListOfType
*/ */
static function listOf($wrappedType) static function listOf($wrappedType)
@ -231,6 +231,15 @@ static function isCompositeType($type)
static function isAbstractType($type) static function isAbstractType($type)
``` ```
```php
/**
* @api
* @param Type $type
* @return bool
*/
static function isType($type)
```
```php ```php
/** /**
* @api * @api
@ -374,28 +383,28 @@ public $variableValues;
*/ */
function getFieldSelection($depth = 0) function getFieldSelection($depth = 0)
``` ```
# GraphQL\Type\Definition\DirectiveLocation # GraphQL\Language\DirectiveLocation
List of available directive locations List of available directive locations
**Class Constants:** **Class Constants:**
```php ```php
const IFACE = "INTERFACE";
const SUBSCRIPTION = "SUBSCRIPTION";
const FRAGMENT_SPREAD = "FRAGMENT_SPREAD";
const QUERY = "QUERY"; const QUERY = "QUERY";
const MUTATION = "MUTATION"; const MUTATION = "MUTATION";
const SUBSCRIPTION = "SUBSCRIPTION";
const FIELD = "FIELD";
const FRAGMENT_DEFINITION = "FRAGMENT_DEFINITION"; const FRAGMENT_DEFINITION = "FRAGMENT_DEFINITION";
const INPUT_OBJECT = "INPUT_OBJECT"; const FRAGMENT_SPREAD = "FRAGMENT_SPREAD";
const INLINE_FRAGMENT = "INLINE_FRAGMENT"; const INLINE_FRAGMENT = "INLINE_FRAGMENT";
const UNION = "UNION"; const SCHEMA = "SCHEMA";
const SCALAR = "SCALAR"; const SCALAR = "SCALAR";
const OBJECT = "OBJECT";
const FIELD_DEFINITION = "FIELD_DEFINITION"; const FIELD_DEFINITION = "FIELD_DEFINITION";
const ARGUMENT_DEFINITION = "ARGUMENT_DEFINITION"; const ARGUMENT_DEFINITION = "ARGUMENT_DEFINITION";
const IFACE = "INTERFACE";
const UNION = "UNION";
const ENUM = "ENUM"; const ENUM = "ENUM";
const OBJECT = "OBJECT";
const ENUM_VALUE = "ENUM_VALUE"; const ENUM_VALUE = "ENUM_VALUE";
const FIELD = "FIELD"; const INPUT_OBJECT = "INPUT_OBJECT";
const SCHEMA = "SCHEMA";
const INPUT_FIELD_DEFINITION = "INPUT_FIELD_DEFINITION"; const INPUT_FIELD_DEFINITION = "INPUT_FIELD_DEFINITION";
``` ```
@ -431,7 +440,7 @@ static function create(array $options = [])
* @param ObjectType $query * @param ObjectType $query
* @return SchemaConfig * @return SchemaConfig
*/ */
function setQuery(GraphQL\Type\Definition\ObjectType $query) function setQuery($query)
``` ```
```php ```php
@ -440,7 +449,7 @@ function setQuery(GraphQL\Type\Definition\ObjectType $query)
* @param ObjectType $mutation * @param ObjectType $mutation
* @return SchemaConfig * @return SchemaConfig
*/ */
function setMutation(GraphQL\Type\Definition\ObjectType $mutation) function setMutation($mutation)
``` ```
```php ```php
@ -449,7 +458,7 @@ function setMutation(GraphQL\Type\Definition\ObjectType $mutation)
* @param ObjectType $subscription * @param ObjectType $subscription
* @return SchemaConfig * @return SchemaConfig
*/ */
function setSubscription(GraphQL\Type\Definition\ObjectType $subscription) function setSubscription($subscription)
``` ```
```php ```php
@ -670,6 +679,18 @@ function getDirectives()
function getDirective($name) function getDirective($name)
``` ```
```php
/**
* Validates schema.
*
* This operation requires full schema scan. Do not use in production environment.
*
* @api
* @return InvariantViolation[]|Error[]
*/
function validate()
```
```php ```php
/** /**
* Validates schema. * Validates schema.
@ -697,10 +718,25 @@ Parses string containing GraphQL query or [type definition](type-system/type-lan
* in the source that they correspond to. This configuration flag * in the source that they correspond to. This configuration flag
* disables that behavior for performance or testing.) * disables that behavior for performance or testing.)
* *
* experimentalFragmentVariables: boolean,
* (If enabled, the parser will understand and parse variable definitions
* contained in a fragment definition. They'll be represented in the
* `variableDefinitions` field of the FragmentDefinitionNode.
*
* The syntax is identical to normal, query-defined variables. For example:
*
* fragment A($var: Boolean = false) on T {
* ...
* }
*
* Note: this feature is experimental and may change or be removed in the
* future.)
*
* @api * @api
* @param Source|string $source * @param Source|string $source
* @param array $options * @param array $options
* @return DocumentNode * @return DocumentNode
* @throws SyntaxError
*/ */
static function parse($source, array $options = []) static function parse($source, array $options = [])
``` ```
@ -936,7 +972,12 @@ const UNION_TYPE_DEFINITION = "UnionTypeDefinition";
const ENUM_TYPE_DEFINITION = "EnumTypeDefinition"; const ENUM_TYPE_DEFINITION = "EnumTypeDefinition";
const ENUM_VALUE_DEFINITION = "EnumValueDefinition"; const ENUM_VALUE_DEFINITION = "EnumValueDefinition";
const INPUT_OBJECT_TYPE_DEFINITION = "InputObjectTypeDefinition"; const INPUT_OBJECT_TYPE_DEFINITION = "InputObjectTypeDefinition";
const TYPE_EXTENSION_DEFINITION = "TypeExtensionDefinition"; const SCALAR_TYPE_EXTENSION = "ScalarTypeExtension";
const OBJECT_TYPE_EXTENSION = "ObjectTypeExtension";
const INTERFACE_TYPE_EXTENSION = "InterfaceTypeExtension";
const UNION_TYPE_EXTENSION = "UnionTypeExtension";
const ENUM_TYPE_EXTENSION = "EnumTypeExtension";
const INPUT_OBJECT_TYPE_EXTENSION = "InputObjectTypeExtension";
const DIRECTIVE_DEFINITION = "DirectiveDefinition"; const DIRECTIVE_DEFINITION = "DirectiveDefinition";
``` ```
@ -1319,7 +1360,6 @@ Also it is possible to override warning handler (which is **trigger_error()** by
**Class Constants:** **Class Constants:**
```php ```php
const WARNING_NAME = 1;
const WARNING_ASSIGN = 2; const WARNING_ASSIGN = 2;
const WARNING_CONFIG = 4; const WARNING_CONFIG = 4;
const WARNING_FULL_SCHEMA_SCAN = 8; const WARNING_FULL_SCHEMA_SCAN = 8;
@ -1352,7 +1392,7 @@ static function setWarningHandler(callable $warningHandler = null)
* @api * @api
* @param bool|int $suppress * @param bool|int $suppress
*/ */
static function suppress($suppress = false) static function suppress($suppress = true)
``` ```
```php ```php
@ -1367,7 +1407,7 @@ static function suppress($suppress = false)
* @api * @api
* @param bool|int $enable * @param bool|int $enable
*/ */
static function enable($enable = false) static function enable($enable = true)
``` ```
# GraphQL\Error\ClientAware # GraphQL\Error\ClientAware
This interface is used for [default error formatting](error-handling.md). This interface is used for [default error formatting](error-handling.md).
@ -1697,7 +1737,7 @@ function setPersistentQueryLoader(callable $persistentQueryLoader)
* @param bool|int $set * @param bool|int $set
* @return $this * @return $this
*/ */
function setDebug($set = false) function setDebug($set = true)
``` ```
```php ```php
@ -1927,13 +1967,19 @@ See [section in docs](type-system/type-language.md) for details.
* Given that AST it constructs a GraphQL\Type\Schema. The resulting schema * Given that AST it constructs a GraphQL\Type\Schema. The resulting schema
* has no resolve methods, so execution will use default resolvers. * has no resolve methods, so execution will use default resolvers.
* *
* Accepts options as a second argument:
*
* - commentDescriptions:
* Provide true to use preceding comments as the description.
*
*
* @api * @api
* @param DocumentNode $ast * @param DocumentNode $ast
* @param callable $typeConfigDecorator * @param array $options
* @return Schema * @return Schema
* @throws Error * @throws Error
*/ */
static function buildAST(GraphQL\Language\AST\DocumentNode $ast, callable $typeConfigDecorator = null) static function buildAST(GraphQL\Language\AST\DocumentNode $ast, array $options = [])
``` ```
```php ```php
@ -1943,10 +1989,10 @@ static function buildAST(GraphQL\Language\AST\DocumentNode $ast, callable $typeC
* *
* @api * @api
* @param DocumentNode|Source|string $source * @param DocumentNode|Source|string $source
* @param callable $typeConfigDecorator * @param array $options
* @return Schema * @return Schema
*/ */
static function build($source, callable $typeConfigDecorator = null) static function build($source, array $options = [])
``` ```
# GraphQL\Utils\AST # GraphQL\Utils\AST
Various utilities dealing with AST Various utilities dealing with AST
@ -2049,6 +2095,32 @@ static function astFromValue($value, GraphQL\Type\Definition\InputType $type)
static function valueFromAST($valueNode, GraphQL\Type\Definition\InputType $type, $variables = null) static function valueFromAST($valueNode, GraphQL\Type\Definition\InputType $type, $variables = null)
``` ```
```php
/**
* Produces a PHP value given a GraphQL Value AST.
*
* Unlike `valueFromAST()`, no type is provided. The resulting JavaScript value
* will reflect the provided GraphQL value AST.
*
* | GraphQL Value | PHP Value |
* | -------------------- | ------------- |
* | Input Object | Assoc Array |
* | List | Array |
* | Boolean | Boolean |
* | String | String |
* | Int / Float | Int / Float |
* | Enum | Mixed |
* | Null | null |
*
* @api
* @param Node $valueNode
* @param array|null $variables
* @return mixed
* @throws \Exception
*/
static function valueFromASTUntyped($valueNode, array $variables = null)
```
```php ```php
/** /**
* Returns type definition for given AST Type node * Returns type definition for given AST Type node
@ -2057,7 +2129,7 @@ static function valueFromAST($valueNode, GraphQL\Type\Definition\InputType $type
* @param Schema $schema * @param Schema $schema
* @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode * @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
* @return Type * @return Type
* @throws InvariantViolation * @throws \Exception
*/ */
static function typeFromAST(GraphQL\Type\Schema $schema, $inputTypeNode) static function typeFromAST(GraphQL\Type\Schema $schema, $inputTypeNode)
``` ```
@ -2079,11 +2151,15 @@ Given an instance of Schema, prints it in GraphQL type language.
**Class Methods:** **Class Methods:**
```php ```php
/** /**
* Accepts options as a second argument:
*
* - commentDescriptions:
* Provide true to use preceding comments as the description.
* @api * @api
* @param Schema $schema * @param Schema $schema
* @return string * @return string
*/ */
static function doPrint(GraphQL\Type\Schema $schema) static function doPrint(GraphQL\Type\Schema $schema, array $options = [])
``` ```
```php ```php
@ -2092,5 +2168,5 @@ static function doPrint(GraphQL\Type\Schema $schema)
* @param Schema $schema * @param Schema $schema
* @return string * @return string
*/ */
static function printIntrosepctionSchema(GraphQL\Type\Schema $schema) static function printIntrosepctionSchema(GraphQL\Type\Schema $schema, array $options = [])
``` ```

View File

@ -35,9 +35,9 @@ In **graphql-php** custom directive is an instance of `GraphQL\Type\Definition\D
```php ```php
<?php <?php
use GraphQL\Language\DirectiveLocation;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\DirectiveLocation;
use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\FieldArgument;
$trackDirective = new Directive([ $trackDirective = new Directive([

View File

@ -33,36 +33,11 @@ $contents = file_get_contents('schema.graphql');
$schema = BuildSchema::build($contents); $schema = BuildSchema::build($contents);
``` ```
By default, such schema is created without any resolvers. As a result, it doesn't support **Interfaces** and **Unions** By default, such schema is created without any resolvers.
because it is impossible to resolve actual implementations during execution.
Also, we have to rely on [default field resolver](../data-fetching.md#default-field-resolver) and **root value** in We have to rely on [default field resolver](../data-fetching.md#default-field-resolver) and **root value** in
order to execute a query against this schema. order to execute a query against this schema.
# Defining resolvers
Since 0.10.0
In order to enable **Interfaces**, **Unions** and custom field resolvers you can pass the second argument:
**type config decorator** to schema builder.
It accepts default type config produced by the builder and is expected to add missing options like
[**resolveType**](interfaces.md#configuration-options) for interface types or
[**resolveField**](object-types.md#configuration-options) for object types.
```php
<?php
use GraphQL\Utils\BuildSchema;
$typeConfigDecorator = function($typeConfig, $typeDefinitionNode) {
$name = $typeConfig['name'];
// ... add missing options to $typeConfig based on type $name
return $typeConfig;
};
$contents = file_get_contents('schema.graphql');
$schema = BuildSchema::build($contents, $typeConfigDecorator);
```
# Performance considerations # Performance considerations
Since 0.10.0 Since 0.10.0

View File

@ -42,20 +42,21 @@ class UrlType extends ScalarType
/** /**
* Parses an externally provided literal value to use as an input (e.g. in Query AST) * Parses an externally provided literal value to use as an input (e.g. in Query AST)
* *
* @param $ast Node * @param Node $valueNode
* @param array|null $variables
* @return null|string * @return null|string
* @throws Error * @throws Error
*/ */
public function parseLiteral($ast) public function parseLiteral($valueNode, array $variables = null)
{ {
// Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL // Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL
// error location in query: // error location in query:
if (!($ast instanceof StringValueNode)) { if (!($valueNode instanceof StringValueNode)) {
throw new Error('Query error: Can only parse strings got: ' . $ast->kind, [$ast]); throw new Error('Query error: Can only parse strings got: ' . $valueNode->kind, [$valueNode]);
} }
if (!is_string($ast->value) || !filter_var($ast->value, FILTER_VALIDATE_URL)) { if (!is_string($valueNode->value) || !filter_var($valueNode->value, FILTER_VALIDATE_URL)) {
throw new Error('Query error: Not a valid URL', [$ast]); throw new Error('Query error: Not a valid URL', [$valueNode]);
} }
return $ast->value; return $valueNode->value;
} }
} }

View File

@ -1,6 +1,7 @@
<?php <?php
namespace GraphQL\Error; namespace GraphQL\Error;
use GraphQL\Language\AST\Node;
use GraphQL\Language\Source; use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation; use GraphQL\Language\SourceLocation;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
@ -52,7 +53,10 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
public $nodes; public $nodes;
/** /**
* The source GraphQL document corresponding to this error. * The source GraphQL document for the first location of this error.
*
* Note that if this Error represents more than one node, the source may not
* represent nodes after the first node.
* *
* @var Source|null * @var Source|null
*/ */
@ -73,6 +77,11 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
*/ */
protected $category; protected $category;
/**
* @var array
*/
protected $extensions;
/** /**
* Given an arbitrary Error, presumably thrown while attempting to execute a * Given an arbitrary Error, presumably thrown while attempting to execute a
* GraphQL operation, produce a new GraphQLError aware of the location in the * GraphQL operation, produce a new GraphQLError aware of the location in the
@ -95,6 +104,7 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
} }
$source = $positions = $originalError = null; $source = $positions = $originalError = null;
$extensions = [];
if ($error instanceof self) { if ($error instanceof self) {
$message = $error->getMessage(); $message = $error->getMessage();
@ -102,6 +112,7 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
$nodes = $error->nodes ?: $nodes; $nodes = $error->nodes ?: $nodes;
$source = $error->source; $source = $error->source;
$positions = $error->positions; $positions = $error->positions;
$extensions = $error->extensions;
} else if ($error instanceof \Exception || $error instanceof \Throwable) { } else if ($error instanceof \Exception || $error instanceof \Throwable) {
$message = $error->getMessage(); $message = $error->getMessage();
$originalError = $error; $originalError = $error;
@ -115,7 +126,8 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
$source, $source,
$positions, $positions,
$path, $path,
$originalError $originalError,
$extensions
); );
} }
@ -131,11 +143,12 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
/** /**
* @param string $message * @param string $message
* @param array|null $nodes * @param array|Node|null $nodes
* @param Source $source * @param Source $source
* @param array|null $positions * @param array|null $positions
* @param array|null $path * @param array|null $path
* @param \Throwable $previous * @param \Throwable $previous
* @param array $extensions
*/ */
public function __construct( public function __construct(
$message, $message,
@ -143,19 +156,28 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
Source $source = null, Source $source = null,
$positions = null, $positions = null,
$path = null, $path = null,
$previous = null $previous = null,
array $extensions = []
) )
{ {
parent::__construct($message, 0, $previous); parent::__construct($message, 0, $previous);
// Compute list of blame nodes.
if ($nodes instanceof \Traversable) { if ($nodes instanceof \Traversable) {
$nodes = iterator_to_array($nodes); $nodes = iterator_to_array($nodes);
} else if ($nodes && !is_array($nodes)) {
$nodes = [$nodes];
} }
$this->nodes = $nodes; $this->nodes = $nodes;
$this->source = $source; $this->source = $source;
$this->positions = $positions; $this->positions = $positions;
$this->path = $path; $this->path = $path;
$this->extensions = $extensions ?: (
$previous && $previous instanceof self
? $previous->extensions
: []
);
if ($previous instanceof ClientAware) { if ($previous instanceof ClientAware) {
$this->isClientSafe = $previous->isClientSafe(); $this->isClientSafe = $previous->isClientSafe();
@ -235,11 +257,18 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
if (null === $this->locations) { if (null === $this->locations) {
$positions = $this->getPositions(); $positions = $this->getPositions();
$source = $this->getSource(); $source = $this->getSource();
$nodes = $this->nodes;
if ($positions && $source) { if ($positions && $source) {
$this->locations = array_map(function ($pos) use ($source) { $this->locations = array_map(function ($pos) use ($source) {
return $source->getLocation($pos); return $source->getLocation($pos);
}, $positions); }, $positions);
} else if ($nodes) {
$this->locations = array_filter(array_map(function ($node) {
if ($node->loc) {
return $node->loc->source->getLocation($node->loc->start);
}
}, $nodes));
} else { } else {
$this->locations = []; $this->locations = [];
} }
@ -248,6 +277,14 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
return $this->locations; return $this->locations;
} }
/**
* @return array|Node[]|null
*/
public function getNodes()
{
return $this->nodes;
}
/** /**
* Returns an array describing the path from the root value to the field which produced this error. * Returns an array describing the path from the root value to the field which produced this error.
* Only included for execution errors. * Only included for execution errors.
@ -260,6 +297,14 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
return $this->path; return $this->path;
} }
/**
* @return array
*/
public function getExtensions()
{
return $this->extensions;
}
/** /**
* Returns array representation of error suitable for serialization * Returns array representation of error suitable for serialization
* *
@ -272,6 +317,10 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
'message' => $this->getMessage() 'message' => $this->getMessage()
]; ];
if ($this->getExtensions()) {
$arr = array_merge($this->getExtensions(), $arr);
}
$locations = Utils::map($this->getLocations(), function(SourceLocation $loc) { $locations = Utils::map($this->getLocations(), function(SourceLocation $loc) {
return $loc->toSerializableArray(); return $loc->toSerializableArray();
}); });
@ -297,4 +346,12 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
{ {
return $this->toSerializableArray(); return $this->toSerializableArray();
} }
/**
* @return string
*/
public function __toString()
{
return FormattedError::printError($this);
}
} }

View File

@ -1,6 +1,8 @@
<?php <?php
namespace GraphQL\Error; namespace GraphQL\Error;
use GraphQL\Language\AST\Node;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation; use GraphQL\Language\SourceLocation;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\WrappingType; use GraphQL\Type\Definition\WrappingType;
@ -27,6 +29,98 @@ class FormattedError
self::$internalErrorMessage = $msg; self::$internalErrorMessage = $msg;
} }
/**
* Prints a GraphQLError to a string, representing useful location information
* about the error's position in the source.
*
* @param Error $error
* @return string
*/
public static function printError(Error $error)
{
$printedLocations = [];
if ($error->nodes) {
/** @var Node $node */
foreach($error->nodes as $node) {
if ($node->loc) {
$printedLocations[] = self::highlightSourceAtLocation(
$node->loc->source,
$node->loc->source->getLocation($node->loc->start)
);
}
}
} else if ($error->getSource() && $error->getLocations()) {
$source = $error->getSource();
foreach($error->getLocations() as $location) {
$printedLocations[] = self::highlightSourceAtLocation($source, $location);
}
}
return !$printedLocations
? $error->getMessage()
: join("\n\n", array_merge([$error->getMessage()], $printedLocations)) . "\n";
}
/**
* Render a helpful description of the location of the error in the GraphQL
* Source document.
*
* @param Source $source
* @param SourceLocation $location
* @return string
*/
private static function highlightSourceAtLocation(Source $source, SourceLocation $location)
{
$line = $location->line;
$lineOffset = $source->locationOffset->line - 1;
$columnOffset = self::getColumnOffset($source, $location);
$contextLine = $line + $lineOffset;
$contextColumn = $location->column + $columnOffset;
$prevLineNum = (string) ($contextLine - 1);
$lineNum = (string) $contextLine;
$nextLineNum = (string) ($contextLine + 1);
$padLen = strlen($nextLineNum);
$lines = preg_split('/\r\n|[\n\r]/', $source->body);
$lines[0] = self::whitespace($source->locationOffset->column - 1) . $lines[0];
$outputLines = [
"{$source->name} ($contextLine:$contextColumn)",
$line >= 2 ? (self::lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2]) : null,
self::lpad($padLen, $lineNum) . ': ' . $lines[$line - 1],
self::whitespace(2 + $padLen + $contextColumn - 1) . '^',
$line < count($lines)? self::lpad($padLen, $nextLineNum) . ': ' . $lines[$line] : null
];
return join("\n", array_filter($outputLines));
}
/**
* @param Source $source
* @param SourceLocation $location
* @return int
*/
private static function getColumnOffset(Source $source, SourceLocation $location)
{
return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
}
/**
* @param int $len
* @return string
*/
private static function whitespace($len) {
return str_repeat(' ', $len);
}
/**
* @param int $len
* @return string
*/
private static function lpad($len, $str) {
return self::whitespace($len - mb_strlen($str)) . $str;
}
/** /**
* Standard GraphQL error formatter. Converts any exception to array * Standard GraphQL error formatter. Converts any exception to array
* conforming to GraphQL spec. * conforming to GraphQL spec.
@ -66,6 +160,10 @@ class FormattedError
} }
if ($e instanceof Error) { if ($e instanceof Error) {
if ($e->getExtensions()) {
$formattedError = array_merge($e->getExtensions(), $formattedError);
}
$locations = Utils::map($e->getLocations(), function(SourceLocation $loc) { $locations = Utils::map($e->getLocations(), function(SourceLocation $loc) {
return $loc->toSerializableArray(); return $loc->toSerializableArray();
}); });

View File

@ -2,7 +2,6 @@
namespace GraphQL\Error; namespace GraphQL\Error;
use GraphQL\Language\Source; use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
class SyntaxError extends Error class SyntaxError extends Error
{ {
@ -13,59 +12,11 @@ class SyntaxError extends Error
*/ */
public function __construct(Source $source, $position, $description) public function __construct(Source $source, $position, $description)
{ {
$location = $source->getLocation($position); parent::__construct(
$line = $location->line + $source->locationOffset->line - 1; "Syntax Error: $description",
$columnOffset = self::getColumnOffset($source, $location); null,
$column = $location->column + $columnOffset; $source,
[$position]
$syntaxError = );
"Syntax Error {$source->name} ({$line}:{$column}) $description\n" .
"\n".
self::highlightSourceAtLocation($source, $location);
parent::__construct($syntaxError, null, $source, [$position]);
} }
/**
* @param Source $source
* @param SourceLocation $location
* @return string
*/
public static function highlightSourceAtLocation(Source $source, SourceLocation $location)
{
$line = $location->line;
$lineOffset = $source->locationOffset->line - 1;
$columnOffset = self::getColumnOffset($source, $location);
$contextLine = $line + $lineOffset;
$prevLineNum = (string) ($contextLine - 1);
$lineNum = (string) $contextLine;
$nextLineNum = (string) ($contextLine + 1);
$padLen = mb_strlen($nextLineNum, 'UTF-8');
$unicodeChars = json_decode('"\u2028\u2029"'); // Quick hack to get js-compatible representation of these chars
$lines = preg_split('/\r\n|[\n\r' . $unicodeChars . ']/su', $source->body);
$whitespace = function ($len) {
return str_repeat(' ', $len);
};
$lpad = function ($len, $str) {
return str_pad($str, $len - mb_strlen($str, 'UTF-8') + 1, ' ', STR_PAD_LEFT);
};
$lines[0] = $whitespace($source->locationOffset->column - 1) . $lines[0];
return
($line >= 2 ? $lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2] . "\n" : '') .
($lpad($padLen, $lineNum) . ': ' . $lines[$line - 1] . "\n") .
($whitespace(2 + $padLen + $location->column - 1 + $columnOffset) . "^\n") .
($line < count($lines) ? $lpad($padLen, $nextLineNum) . ': ' . $lines[$line] . "\n" : '');
}
public static function getColumnOffset(Source $source, SourceLocation $location)
{
return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
}
} }

View File

@ -9,7 +9,6 @@ namespace GraphQL\Error;
*/ */
final class Warning final class Warning
{ {
const WARNING_NAME = 1;
const WARNING_ASSIGN = 2; const WARNING_ASSIGN = 2;
const WARNING_CONFIG = 4; const WARNING_CONFIG = 4;
const WARNING_FULL_SCHEMA_SCAN = 8; const WARNING_FULL_SCHEMA_SCAN = 8;

View File

@ -100,9 +100,16 @@ class Executor
{ {
// TODO: deprecate (just always use SyncAdapter here) and have `promiseToExecute()` for other cases // TODO: deprecate (just always use SyncAdapter here) and have `promiseToExecute()` for other cases
$promiseAdapter = self::getPromiseAdapter(); $promiseAdapter = self::getPromiseAdapter();
$result = self::promiseToExecute(
$result = self::promiseToExecute($promiseAdapter, $schema, $ast, $rootValue, $contextValue, $promiseAdapter,
$variableValues, $operationName, $fieldResolver); $schema,
$ast,
$rootValue,
$contextValue,
$variableValues,
$operationName,
$fieldResolver
);
// Wait for promised results when using sync promises // Wait for promised results when using sync promises
if ($promiseAdapter instanceof SyncPromiseAdapter) { if ($promiseAdapter instanceof SyncPromiseAdapter) {
@ -140,11 +147,19 @@ class Executor
callable $fieldResolver = null callable $fieldResolver = null
) )
{ {
try { $exeContext = self::buildExecutionContext(
$exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $contextValue, $variableValues, $schema,
$operationName, $fieldResolver, $promiseAdapter); $ast,
} catch (Error $e) { $rootValue,
return $promiseAdapter->createFulfilled(new ExecutionResult(null, [$e])); $contextValue,
$variableValues,
$operationName,
$fieldResolver,
$promiseAdapter
);
if (is_array($exeContext)) {
return $promiseAdapter->createFulfilled(new ExecutionResult(null, $exeContext));
} }
$executor = new self($exeContext); $executor = new self($exeContext);
@ -159,13 +174,12 @@ class Executor
* @param DocumentNode $documentNode * @param DocumentNode $documentNode
* @param $rootValue * @param $rootValue
* @param $contextValue * @param $contextValue
* @param $rawVariableValues * @param array|\Traversable $rawVariableValues
* @param string $operationName * @param string $operationName
* @param callable $fieldResolver * @param callable $fieldResolver
* @param PromiseAdapter $promiseAdapter * @param PromiseAdapter $promiseAdapter
* *
* @return ExecutionContext * @return ExecutionContext|Error[]
* @throws Error
*/ */
private static function buildExecutionContext( private static function buildExecutionContext(
Schema $schema, Schema $schema,
@ -178,30 +192,17 @@ class Executor
PromiseAdapter $promiseAdapter = null PromiseAdapter $promiseAdapter = null
) )
{ {
if (null !== $rawVariableValues) {
Utils::invariant(
is_array($rawVariableValues) || $rawVariableValues instanceof \ArrayAccess,
"Variable values are expected to be array or instance of ArrayAccess, got " . Utils::getVariableType($rawVariableValues)
);
}
if (null !== $operationName) {
Utils::invariant(
is_string($operationName),
"Operation name is supposed to be string, got " . Utils::getVariableType($operationName)
);
}
$errors = []; $errors = [];
$fragments = []; $fragments = [];
/** @var OperationDefinitionNode $operation */
$operation = null; $operation = null;
$hasMultipleAssumedOperations = false;
foreach ($documentNode->definitions as $definition) { foreach ($documentNode->definitions as $definition) {
switch ($definition->kind) { switch ($definition->kind) {
case NodeKind::OPERATION_DEFINITION: case NodeKind::OPERATION_DEFINITION:
if (!$operationName && $operation) { if (!$operationName && $operation) {
throw new Error( $hasMultipleAssumedOperations = true;
'Must provide operation name if query contains multiple operations.'
);
} }
if (!$operationName || if (!$operationName ||
(isset($definition->name) && $definition->name->value === $operationName)) { (isset($definition->name) && $definition->name->value === $operationName)) {
@ -211,29 +212,45 @@ class Executor
case NodeKind::FRAGMENT_DEFINITION: case NodeKind::FRAGMENT_DEFINITION:
$fragments[$definition->name->value] = $definition; $fragments[$definition->name->value] = $definition;
break; break;
default:
throw new Error(
"GraphQL cannot execute a request containing a {$definition->kind}.",
[$definition]
);
} }
} }
if (!$operation) { if (!$operation) {
if ($operationName) { if ($operationName) {
throw new Error("Unknown operation named \"$operationName\"."); $errors[] = new Error("Unknown operation named \"$operationName\".");
} else { } else {
throw new Error('Must provide an operation.'); $errors[] = new Error('Must provide an operation.');
} }
} else if ($hasMultipleAssumedOperations) {
$errors[] = new Error(
'Must provide operation name if query contains multiple operations.'
);
} }
$variableValues = Values::getVariableValues( $variableValues = null;
if ($operation) {
$coercedVariableValues = Values::getVariableValues(
$schema, $schema,
$operation->variableDefinitions ?: [], $operation->variableDefinitions ?: [],
$rawVariableValues ?: [] $rawVariableValues ?: []
); );
$exeContext = new ExecutionContext( if ($coercedVariableValues['errors']) {
$errors = array_merge($errors, $coercedVariableValues['errors']);
} else {
$variableValues = $coercedVariableValues['coerced'];
}
}
if ($errors) {
return $errors;
}
Utils::invariant($operation, 'Has operation if no errors.');
Utils::invariant($variableValues !== null, 'Has variables if no errors.');
return new ExecutionContext(
$schema, $schema,
$fragments, $fragments,
$rootValue, $rootValue,
@ -244,7 +261,6 @@ class Executor
$fieldResolver ?: self::$defaultFieldResolver, $fieldResolver ?: self::$defaultFieldResolver,
$promiseAdapter ?: self::getPromiseAdapter() $promiseAdapter ?: self::getPromiseAdapter()
); );
return $exeContext;
} }
/** /**
@ -338,7 +354,6 @@ class Executor
} }
} }
/** /**
* Extracts the root type of the operation from the schema. * Extracts the root type of the operation from the schema.
* *
@ -351,12 +366,19 @@ class Executor
{ {
switch ($operation->operation) { switch ($operation->operation) {
case 'query': case 'query':
return $schema->getQueryType(); $queryType = $schema->getQueryType();
if (!$queryType) {
throw new Error(
'Schema does not define the required query root type.',
[$operation]
);
}
return $queryType;
case 'mutation': case 'mutation':
$mutationType = $schema->getMutationType(); $mutationType = $schema->getMutationType();
if (!$mutationType) { if (!$mutationType) {
throw new Error( throw new Error(
'Schema is not configured for mutations', 'Schema is not configured for mutations.',
[$operation] [$operation]
); );
} }
@ -365,14 +387,14 @@ class Executor
$subscriptionType = $schema->getSubscriptionType(); $subscriptionType = $schema->getSubscriptionType();
if (!$subscriptionType) { if (!$subscriptionType) {
throw new Error( throw new Error(
'Schema is not configured for subscriptions', 'Schema is not configured for subscriptions.',
[ $operation ] [ $operation ]
); );
} }
return $subscriptionType; return $subscriptionType;
default: default:
throw new Error( throw new Error(
'Can only execute queries, mutations and subscriptions', 'Can only execute queries, mutations and subscriptions.',
[$operation] [$operation]
); );
} }
@ -1053,15 +1075,6 @@ class Executor
$runtimeType = $returnType->resolveType($result, $exeContext->contextValue, $info); $runtimeType = $returnType->resolveType($result, $exeContext->contextValue, $info);
if (null === $runtimeType) { if (null === $runtimeType) {
if ($returnType instanceof InterfaceType && $info->schema->getConfig()->typeLoader) {
Warning::warnOnce(
"GraphQL Interface Type `{$returnType->name}` returned `null` from it`s `resolveType` function ".
'for value: ' . Utils::printSafe($result) . '. Switching to slow resolution method using `isTypeOf` ' .
'of all possible implementations. It requires full schema scan and degrades query performance significantly. '.
' Make sure your `resolveType` always returns valid implementation or throws.',
Warning::WARNING_FULL_SCHEMA_SCAN
);
}
$runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType); $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
} }
@ -1122,9 +1135,11 @@ class Executor
if (!$runtimeType instanceof ObjectType) { if (!$runtimeType instanceof ObjectType) {
throw new InvariantViolation( throw new InvariantViolation(
"Abstract type {$returnType} must resolve to an Object type at runtime " . "Abstract type {$returnType} must resolve to an Object type at " .
"for field {$info->parentType}.{$info->fieldName} with " . "runtime for field {$info->parentType}.{$info->fieldName} with " .
'value "' . Utils::printSafe($result) . '", received "'. Utils::printSafe($runtimeType) . '".' 'value "' . Utils::printSafe($result) . '", received "'. Utils::printSafe($runtimeType) . '".' .
'Either the ' . $returnType . ' type should provide a "resolveType" ' .
'function or each possible types should provide an "isTypeOf" function.'
); );
} }
@ -1194,7 +1209,7 @@ class Executor
{ {
$serializedResult = $returnType->serialize($result); $serializedResult = $returnType->serialize($result);
if ($serializedResult === null) { if (Utils::isInvalid($serializedResult)) {
throw new InvariantViolation( throw new InvariantViolation(
'Expected a value of type "'. Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result) 'Expected a value of type "'. Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result)
); );
@ -1307,7 +1322,12 @@ class Executor
/** /**
* If a resolveType function is not given, then a default resolve behavior is * If a resolveType function is not given, then a default resolve behavior is
* used which tests each possible type for the abstract type by calling * used which attempts two strategies:
*
* First, See if the provided value has a `__typename` field defined, if so, use
* that value as name of the resolved type.
*
* Otherwise, test each possible type for the abstract type by calling
* isTypeOf for the object being coerced, returning the first type that matches. * isTypeOf for the object being coerced, returning the first type that matches.
* *
* @param $value * @param $value
@ -1318,6 +1338,27 @@ class Executor
*/ */
private function defaultTypeResolver($value, $context, ResolveInfo $info, AbstractType $abstractType) private function defaultTypeResolver($value, $context, ResolveInfo $info, AbstractType $abstractType)
{ {
// First, look for `__typename`.
if (
$value !== null &&
is_array($value) &&
isset($value['__typename']) &&
is_string($value['__typename'])
) {
return $value['__typename'];
}
if ($abstractType instanceof InterfaceType && $info->schema->getConfig()->typeLoader) {
Warning::warnOnce(
"GraphQL Interface Type `{$abstractType->name}` returned `null` from it`s `resolveType` function ".
'for value: ' . Utils::printSafe($value) . '. Switching to slow resolution method using `isTypeOf` ' .
'of all possible implementations. It requires full schema scan and degrades query performance significantly. '.
' Make sure your `resolveType` always returns valid implementation or throws.',
Warning::WARNING_FULL_SCHEMA_SCAN
);
}
// Otherwise, test each possible type.
$possibleTypes = $info->schema->getPossibleTypes($abstractType); $possibleTypes = $info->schema->getPossibleTypes($abstractType);
$promisedIsTypeOfResults = []; $promisedIsTypeOfResults = [];

View File

@ -1,9 +1,7 @@
<?php <?php
namespace GraphQL\Executor; namespace GraphQL\Executor;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\ArgumentNode; use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\DirectiveNode; use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\EnumValueDefinitionNode; use GraphQL\Language\AST\EnumValueDefinitionNode;
@ -15,18 +13,20 @@ use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\VariableNode; use GraphQL\Language\AST\VariableNode;
use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Language\Printer; use GraphQL\Language\Printer;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Schema; use GraphQL\Type\Schema;
use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\LeafType;
use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Utils\AST; use GraphQL\Utils\AST;
use GraphQL\Utils\TypeInfo; use GraphQL\Utils\TypeInfo;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
use GraphQL\Utils\Value;
use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\DocumentValidator;
class Values class Values
@ -37,56 +37,62 @@ class Values
* to match the variable definitions, a Error will be thrown. * to match the variable definitions, a Error will be thrown.
* *
* @param Schema $schema * @param Schema $schema
* @param VariableDefinitionNode[] $definitionNodes * @param VariableDefinitionNode[] $varDefNodes
* @param array $inputs * @param array $inputs
* @return array * @return array
* @throws Error
*/ */
public static function getVariableValues(Schema $schema, $definitionNodes, array $inputs) public static function getVariableValues(Schema $schema, $varDefNodes, array $inputs)
{ {
$errors = [];
$coercedValues = []; $coercedValues = [];
foreach ($definitionNodes as $definitionNode) { foreach ($varDefNodes as $varDefNode) {
$varName = $definitionNode->variable->name->value; $varName = $varDefNode->variable->name->value;
$varType = TypeInfo::typeFromAST($schema, $definitionNode->type); /** @var InputType|Type $varType */
$varType = TypeInfo::typeFromAST($schema, $varDefNode->type);
if (!Type::isInputType($varType)) { if (!Type::isInputType($varType)) {
throw new Error( $errors[] = new Error(
'Variable "$'.$varName.'" expected value of type ' . "Variable \"\$$varName\" expected value of type " .
'"' . Printer::doPrint($definitionNode->type) . '" which cannot be used as an input type.', '"' . Printer::doPrint($varDefNode->type) . '" which cannot be used as an input type.',
[$definitionNode->type] [$varDefNode->type]
); );
} } else {
if (!array_key_exists($varName, $inputs)) { if (!array_key_exists($varName, $inputs)) {
$defaultValue = $definitionNode->defaultValue;
if ($defaultValue) {
$coercedValues[$varName] = AST::valueFromAST($defaultValue, $varType);
}
if ($varType instanceof NonNull) { if ($varType instanceof NonNull) {
throw new Error( $errors[] = new Error(
'Variable "$'.$varName .'" of required type ' . "Variable \"\$$varName\" of required type " .
'"'. Utils::printSafe($varType) . '" was not provided.', "\"{$varType}\" was not provided.",
[$definitionNode] [$varDefNode]
); );
} else if ($varDefNode->defaultValue) {
$coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType);
} }
} else { } else {
$value = $inputs[$varName]; $value = $inputs[$varName];
$errors = self::isValidPHPValue($value, $varType); $coerced = Value::coerceValue($value, $varType, $varDefNode);
if (!empty($errors)) { /** @var Error[] $coercionErrors */
$message = "\n" . implode("\n", $errors); $coercionErrors = $coerced['errors'];
throw new Error( if ($coercionErrors) {
'Variable "$' . $varName . '" got invalid value ' . $messagePrelude = "Variable \"\$$varName\" got invalid value " . Utils::printSafeJson($value) . '; ';
json_encode($value) . '.' . $message,
[$definitionNode] foreach($coercionErrors as $error) {
$errors[] = new Error(
$messagePrelude . $error->getMessage(),
$error->getNodes(),
$error->getSource(),
$error->getPositions(),
$error->getPath(),
$error,
$error->getExtensions()
); );
} }
} else {
$coercedValue = self::coerceValue($varType, $value); $coercedValues[$varName] = $coerced['value'];
Utils::invariant($coercedValue !== Utils::undefined(), 'Should have reported error.');
$coercedValues[$varName] = $coercedValue;
} }
} }
return $coercedValues; }
}
return ['errors' => $errors, 'coerced' => $errors ? null : $coercedValues];
} }
/** /**
@ -109,7 +115,6 @@ class Values
} }
$coercedValues = []; $coercedValues = [];
$undefined = Utils::undefined();
/** @var ArgumentNode[] $argNodeMap */ /** @var ArgumentNode[] $argNodeMap */
$argNodeMap = $argNodes ? Utils::keyMap($argNodes, function (ArgumentNode $arg) { $argNodeMap = $argNodes ? Utils::keyMap($argNodes, function (ArgumentNode $arg) {
@ -152,11 +157,12 @@ class Values
} else { } else {
$valueNode = $argumentNode->value; $valueNode = $argumentNode->value;
$coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues); $coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues);
if ($coercedValue === $undefined) { if (Utils::isInvalid($coercedValue)) {
$errors = DocumentValidator::isValidLiteralValue($argType, $valueNode); // Note: ValuesOfCorrectType validation should catch this before
$message = !empty($errors) ? ("\n" . implode("\n", $errors)) : ''; // execution. This is a runtime check to ensure execution does not
// continue with an invalid argument value.
throw new Error( throw new Error(
'Argument "' . $name . '" got invalid value ' . Printer::doPrint($valueNode) . '.' . $message, 'Argument "' . $name . '" has invalid value ' . Printer::doPrint($valueNode) . '.',
[ $argumentNode->value ] [ $argumentNode->value ]
); );
} }
@ -207,179 +213,16 @@ class Values
} }
/** /**
* Given a PHP value and a GraphQL type, determine if the value will be * @deprecated as of 0.12 (Use coerceValue() directly for richer information)
* accepted for that type. This is primarily useful for validating the
* runtime values of query variables.
*
* @param $value * @param $value
* @param InputType $type * @param InputType $type
* @return array * @return array
*/ */
public static function isValidPHPValue($value, InputType $type) public static function isValidPHPValue($value, InputType $type)
{ {
// A value must be provided if the type is non-null. $errors = Value::coerceValue($value, $type)['errors'];
if ($type instanceof NonNull) { return $errors
if (null === $value) { ? array_map(function(/*\Throwable */$error) { return $error->getMessage(); }, $errors)
return ['Expected "' . Utils::printSafe($type) . '", found null.']; : [];
}
return self::isValidPHPValue($value, $type->getWrappedType());
}
if (null === $value) {
return [];
}
// Lists accept a non-list value as a list of one.
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if (is_array($value)) {
$tmp = [];
foreach ($value as $index => $item) {
$errors = self::isValidPHPValue($item, $itemType);
$tmp = array_merge($tmp, Utils::map($errors, function ($error) use ($index) {
return "In element #$index: $error";
}));
}
return $tmp;
}
return self::isValidPHPValue($value, $itemType);
}
// Input objects check each defined field.
if ($type instanceof InputObjectType) {
if (!is_object($value) && !is_array($value)) {
return ["Expected \"{$type->name}\", found not an object."];
}
$fields = $type->getFields();
$errors = [];
// Ensure every provided field is defined.
$props = is_object($value) ? get_object_vars($value) : $value;
foreach ($props as $providedField => $tmp) {
if (!isset($fields[$providedField])) {
$errors[] = "In field \"{$providedField}\": Unknown field.";
}
}
// Ensure every defined field is valid.
foreach ($fields as $fieldName => $tmp) {
$newErrors = self::isValidPHPValue(isset($value[$fieldName]) ? $value[$fieldName] : null, $fields[$fieldName]->getType());
$errors = array_merge(
$errors,
Utils::map($newErrors, function ($error) use ($fieldName) {
return "In field \"{$fieldName}\": {$error}";
})
);
}
return $errors;
}
if ($type instanceof LeafType) {
try {
// Scalar/Enum input checks to ensure the type can parse the value to
// a non-null value.
$parseResult = $type->parseValue($value);
if (null === $parseResult && !$type->isValidValue($value)) {
$v = Utils::printSafeJson($value);
return [
"Expected type \"{$type->name}\", found $v."
];
}
return [];
} catch (\Exception $e) {
return [
"Expected type \"{$type->name}\", found " . Utils::printSafeJson($value) . ': ' .
$e->getMessage()
];
} catch (\Throwable $e) {
return [
"Expected type \"{$type->name}\", found " . Utils::printSafeJson($value) . ': ' .
$e->getMessage()
];
}
}
throw new InvariantViolation('Must be input type');
}
/**
* Given a type and any value, return a runtime value coerced to match the type.
*/
private static function coerceValue(Type $type, $value)
{
$undefined = Utils::undefined();
if ($value === $undefined) {
return $undefined;
}
if ($type instanceof NonNull) {
if ($value === null) {
// Intentionally return no value.
return $undefined;
}
return self::coerceValue($type->getWrappedType(), $value);
}
if (null === $value) {
return null;
}
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if (is_array($value) || $value instanceof \Traversable) {
$coercedValues = [];
foreach ($value as $item) {
$itemValue = self::coerceValue($itemType, $item);
if ($undefined === $itemValue) {
// Intentionally return no value.
return $undefined;
}
$coercedValues[] = $itemValue;
}
return $coercedValues;
} else {
$coercedValue = self::coerceValue($itemType, $value);
if ($coercedValue === $undefined) {
// Intentionally return no value.
return $undefined;
}
return [$coercedValue];
}
}
if ($type instanceof InputObjectType) {
$coercedObj = [];
$fields = $type->getFields();
foreach ($fields as $fieldName => $field) {
if (!array_key_exists($fieldName, $value)) {
if ($field->defaultValueExists()) {
$coercedObj[$fieldName] = $field->defaultValue;
} else if ($field->getType() instanceof NonNull) {
// Intentionally return no value.
return $undefined;
}
continue;
}
$fieldValue = self::coerceValue($field->getType(), $value[$fieldName]);
if ($fieldValue === $undefined) {
// Intentionally return no value.
return $undefined;
}
$coercedObj[$fieldName] = $fieldValue;
}
return $coercedObj;
}
if ($type instanceof LeafType) {
$parsed = $type->parseValue($value);
if (null === $parsed) {
// null or invalid values represent a failure to parse correctly,
// in which case no value is returned.
return $undefined;
}
return $parsed;
}
throw new InvariantViolation('Must be input type');
} }
} }

View File

@ -4,8 +4,8 @@ namespace GraphQL\Language\AST;
interface DefinitionNode interface DefinitionNode
{ {
/** /**
* export type DefinitionNode = OperationDefinitionNode * export type DefinitionNode =
* | FragmentDefinitionNode * | ExecutableDefinitionNode
* | TypeSystemDefinitionNode // experimental non-spec addition. * | TypeSystemDefinitionNode; // experimental non-spec addition.
*/ */
} }

View File

@ -22,4 +22,9 @@ class DirectiveDefinitionNode extends Node implements TypeSystemDefinitionNode
* @var NameNode[] * @var NameNode[]
*/ */
public $locations; public $locations;
/**
* @var StringValueNode|null
*/
public $description;
} }

View File

@ -19,12 +19,12 @@ class EnumTypeDefinitionNode extends Node implements TypeDefinitionNode
public $directives; public $directives;
/** /**
* @var EnumValueDefinitionNode[] * @var EnumValueDefinitionNode[]|null|NodeList
*/ */
public $values; public $values;
/** /**
* @var string * @var StringValueNode|null
*/ */
public $description; public $description;
} }

View File

@ -0,0 +1,25 @@
<?php
namespace GraphQL\Language\AST;
class EnumTypeExtensionNode extends Node implements TypeExtensionNode
{
/**
* @var string
*/
public $kind = NodeKind::ENUM_TYPE_EXTENSION;
/**
* @var NameNode
*/
public $name;
/**
* @var DirectiveNode[]|null
*/
public $directives;
/**
* @var EnumValueDefinitionNode[]|null
*/
public $values;
}

View File

@ -19,7 +19,7 @@ class EnumValueDefinitionNode extends Node
public $directives; public $directives;
/** /**
* @var string * @var StringValueNode|null
*/ */
public $description; public $description;
} }

View File

@ -0,0 +1,11 @@
<?php
namespace GraphQL\Language\AST;
interface ExecutableDefinitionNode extends DefinitionNode
{
/**
* export type ExecutableDefinitionNode =
* | OperationDefinitionNode
* | FragmentDefinitionNode;
*/
}

View File

@ -14,7 +14,7 @@ class FieldDefinitionNode extends Node
public $name; public $name;
/** /**
* @var InputValueDefinitionNode[] * @var InputValueDefinitionNode[]|NodeList
*/ */
public $arguments; public $arguments;
@ -24,12 +24,12 @@ class FieldDefinitionNode extends Node
public $type; public $type;
/** /**
* @var DirectiveNode[] * @var DirectiveNode[]|NodeList
*/ */
public $directives; public $directives;
/** /**
* @var string * @var StringValueNode|null
*/ */
public $description; public $description;
} }

View File

@ -1,7 +1,7 @@
<?php <?php
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
class FragmentDefinitionNode extends Node implements DefinitionNode, HasSelectionSet class FragmentDefinitionNode extends Node implements ExecutableDefinitionNode, HasSelectionSet
{ {
public $kind = NodeKind::FRAGMENT_DEFINITION; public $kind = NodeKind::FRAGMENT_DEFINITION;
@ -10,13 +10,21 @@ class FragmentDefinitionNode extends Node implements DefinitionNode, HasSelectio
*/ */
public $name; public $name;
/**
* Note: fragment variable definitions are experimental and may be changed
* or removed in the future.
*
* @var VariableDefinitionNode[]|NodeList
*/
public $variableDefinitions;
/** /**
* @var NamedTypeNode * @var NamedTypeNode
*/ */
public $typeCondition; public $typeCondition;
/** /**
* @var DirectiveNode[] * @var DirectiveNode[]|NodeList
*/ */
public $directives; public $directives;

View File

@ -14,17 +14,17 @@ class InputObjectTypeDefinitionNode extends Node implements TypeDefinitionNode
public $name; public $name;
/** /**
* @var DirectiveNode[] * @var DirectiveNode[]|null
*/ */
public $directives; public $directives;
/** /**
* @var InputValueDefinitionNode[] * @var InputValueDefinitionNode[]|null
*/ */
public $fields; public $fields;
/** /**
* @var string * @var StringValueNode|null
*/ */
public $description; public $description;
} }

View File

@ -0,0 +1,25 @@
<?php
namespace GraphQL\Language\AST;
class InputObjectTypeExtensionNode extends Node implements TypeExtensionNode
{
/**
* @var string
*/
public $kind = NodeKind::INPUT_OBJECT_TYPE_EXTENSION;
/**
* @var NameNode
*/
public $name;
/**
* @var DirectiveNode[]|null
*/
public $directives;
/**
* @var InputValueDefinitionNode[]|null
*/
public $fields;
}

View File

@ -29,7 +29,7 @@ class InputValueDefinitionNode extends Node
public $directives; public $directives;
/** /**
* @var string * @var StringValueNode|null
*/ */
public $description; public $description;
} }

View File

@ -14,17 +14,17 @@ class InterfaceTypeDefinitionNode extends Node implements TypeDefinitionNode
public $name; public $name;
/** /**
* @var DirectiveNode[] * @var DirectiveNode[]|null
*/ */
public $directives; public $directives;
/** /**
* @var FieldDefinitionNode[] * @var FieldDefinitionNode[]|null
*/ */
public $fields = []; public $fields;
/** /**
* @var string * @var StringValueNode|null
*/ */
public $description; public $description;
} }

View File

@ -0,0 +1,25 @@
<?php
namespace GraphQL\Language\AST;
class InterfaceTypeExtensionNode extends Node implements TypeExtensionNode
{
/**
* @var string
*/
public $kind = NodeKind::INTERFACE_TYPE_EXTENSION;
/**
* @var NameNode
*/
public $name;
/**
* @var DirectiveNode[]|null
*/
public $directives;
/**
* @var FieldDefinitionNode[]|null
*/
public $fields;
}

View File

@ -7,7 +7,7 @@ class ListValueNode extends Node implements ValueNode
public $kind = NodeKind::LST; public $kind = NodeKind::LST;
/** /**
* @var ValueNode[] * @var ValueNode[]|NodeList
*/ */
public $values; public $values;
} }

View File

@ -65,7 +65,12 @@ class NodeKind
// Type Extensions // Type Extensions
const TYPE_EXTENSION_DEFINITION = 'TypeExtensionDefinition'; const SCALAR_TYPE_EXTENSION = 'ScalarTypeExtension';
const OBJECT_TYPE_EXTENSION = 'ObjectTypeExtension';
const INTERFACE_TYPE_EXTENSION = 'InterfaceTypeExtension';
const UNION_TYPE_EXTENSION = 'UnionTypeExtension';
const ENUM_TYPE_EXTENSION = 'EnumTypeExtension';
const INPUT_OBJECT_TYPE_EXTENSION = 'InputObjectTypeExtension';
// Directive Definitions // Directive Definitions
@ -127,7 +132,12 @@ class NodeKind
NodeKind::INPUT_OBJECT_TYPE_DEFINITION =>InputObjectTypeDefinitionNode::class, NodeKind::INPUT_OBJECT_TYPE_DEFINITION =>InputObjectTypeDefinitionNode::class,
// Type Extensions // Type Extensions
NodeKind::TYPE_EXTENSION_DEFINITION => TypeExtensionDefinitionNode::class, NodeKind::SCALAR_TYPE_EXTENSION => ScalarTypeExtensionNode::class,
NodeKind::OBJECT_TYPE_EXTENSION => ObjectTypeExtensionNode::class,
NodeKind::INTERFACE_TYPE_EXTENSION => InterfaceTypeExtensionNode::class,
NodeKind::UNION_TYPE_EXTENSION => UnionTypeExtensionNode::class,
NodeKind::ENUM_TYPE_EXTENSION => EnumTypeExtensionNode::class,
NodeKind::INPUT_OBJECT_TYPE_EXTENSION => InputObjectTypeExtensionNode::class,
// Directive Definitions // Directive Definitions
NodeKind::DIRECTIVE_DEFINITION => DirectiveDefinitionNode::class NodeKind::DIRECTIVE_DEFINITION => DirectiveDefinitionNode::class

View File

@ -19,17 +19,17 @@ class ObjectTypeDefinitionNode extends Node implements TypeDefinitionNode
public $interfaces = []; public $interfaces = [];
/** /**
* @var DirectiveNode[] * @var DirectiveNode[]|null
*/ */
public $directives; public $directives;
/** /**
* @var FieldDefinitionNode[] * @var FieldDefinitionNode[]|null
*/ */
public $fields; public $fields;
/** /**
* @var string * @var StringValueNode|null
*/ */
public $description; public $description;
} }

View File

@ -0,0 +1,30 @@
<?php
namespace GraphQL\Language\AST;
class ObjectTypeExtensionNode extends Node implements TypeExtensionNode
{
/**
* @var string
*/
public $kind = NodeKind::OBJECT_TYPE_EXTENSION;
/**
* @var NameNode
*/
public $name;
/**
* @var NamedTypeNode[]
*/
public $interfaces = [];
/**
* @var DirectiveNode[]
*/
public $directives;
/**
* @var FieldDefinitionNode[]
*/
public $fields;
}

View File

@ -6,7 +6,7 @@ class ObjectValueNode extends Node implements ValueNode
public $kind = NodeKind::OBJECT; public $kind = NodeKind::OBJECT;
/** /**
* @var ObjectFieldNode[] * @var ObjectFieldNode[]|NodeList
*/ */
public $fields; public $fields;
} }

View File

@ -1,7 +1,7 @@
<?php <?php
namespace GraphQL\Language\AST; namespace GraphQL\Language\AST;
class OperationDefinitionNode extends Node implements DefinitionNode, HasSelectionSet class OperationDefinitionNode extends Node implements ExecutableDefinitionNode, HasSelectionSet
{ {
/** /**
* @var string * @var string

View File

@ -19,7 +19,7 @@ class ScalarTypeDefinitionNode extends Node implements TypeDefinitionNode
public $directives; public $directives;
/** /**
* @var string * @var StringValueNode|null
*/ */
public $description; public $description;
} }

View File

@ -0,0 +1,20 @@
<?php
namespace GraphQL\Language\AST;
class ScalarTypeExtensionNode extends Node implements TypeExtensionNode
{
/**
* @var string
*/
public $kind = NodeKind::SCALAR_TYPE_EXTENSION;
/**
* @var NameNode
*/
public $name;
/**
* @var DirectiveNode[]|null
*/
public $directives;
}

View File

@ -9,4 +9,9 @@ class StringValueNode extends Node implements ValueNode
* @var string * @var string
*/ */
public $value; public $value;
/**
* @var boolean|null
*/
public $block;
} }

View File

@ -1,15 +0,0 @@
<?php
namespace GraphQL\Language\AST;
class TypeExtensionDefinitionNode extends Node implements TypeSystemDefinitionNode
{
/**
* @var string
*/
public $kind = NodeKind::TYPE_EXTENSION_DEFINITION;
/**
* @var ObjectTypeDefinitionNode
*/
public $definition;
}

View File

@ -0,0 +1,15 @@
<?php
namespace GraphQL\Language\AST;
interface TypeExtensionNode extends TypeSystemDefinitionNode
{
/**
export type TypeExtensionNode =
| ScalarTypeExtensionNode
| ObjectTypeExtensionNode
| InterfaceTypeExtensionNode
| UnionTypeExtensionNode
| EnumTypeExtensionNode
| InputObjectTypeExtensionNode;
*/
}

View File

@ -4,9 +4,10 @@ namespace GraphQL\Language\AST;
interface TypeSystemDefinitionNode extends DefinitionNode interface TypeSystemDefinitionNode extends DefinitionNode
{ {
/** /**
export type TypeSystemDefinitionNode = SchemaDefinitionNode export type TypeSystemDefinitionNode =
| SchemaDefinitionNode
| TypeDefinitionNode | TypeDefinitionNode
| TypeExtensionDefinitionNode | TypeExtensionNode
| DirectiveDefinitionNode | DirectiveDefinitionNode
*/ */
} }

View File

@ -19,12 +19,12 @@ class UnionTypeDefinitionNode extends Node implements TypeDefinitionNode
public $directives; public $directives;
/** /**
* @var NamedTypeNode[] * @var NamedTypeNode[]|null
*/ */
public $types = []; public $types;
/** /**
* @var string * @var StringValueNode|null
*/ */
public $description; public $description;
} }

View File

@ -0,0 +1,25 @@
<?php
namespace GraphQL\Language\AST;
class UnionTypeExtensionNode extends Node implements TypeExtensionNode
{
/**
* @var string
*/
public $kind = NodeKind::UNION_TYPE_EXTENSION;
/**
* @var NameNode
*/
public $name;
/**
* @var DirectiveNode[]|null
*/
public $directives;
/**
* @var NamedTypeNode[]|null
*/
public $types;
}

View File

@ -0,0 +1,60 @@
<?php
namespace GraphQL\Language;
/**
* List of available directive locations
*/
class DirectiveLocation
{
// Request Definitions
const QUERY = 'QUERY';
const MUTATION = 'MUTATION';
const SUBSCRIPTION = 'SUBSCRIPTION';
const FIELD = 'FIELD';
const FRAGMENT_DEFINITION = 'FRAGMENT_DEFINITION';
const FRAGMENT_SPREAD = 'FRAGMENT_SPREAD';
const INLINE_FRAGMENT = 'INLINE_FRAGMENT';
// Type System Definitions
const SCHEMA = 'SCHEMA';
const SCALAR = 'SCALAR';
const OBJECT = 'OBJECT';
const FIELD_DEFINITION = 'FIELD_DEFINITION';
const ARGUMENT_DEFINITION = 'ARGUMENT_DEFINITION';
const IFACE = 'INTERFACE';
const UNION = 'UNION';
const ENUM = 'ENUM';
const ENUM_VALUE = 'ENUM_VALUE';
const INPUT_OBJECT = 'INPUT_OBJECT';
const INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION';
private static $locations = [
self::QUERY => self::QUERY,
self::MUTATION => self::MUTATION,
self::SUBSCRIPTION => self::SUBSCRIPTION,
self::FIELD => self::FIELD,
self::FRAGMENT_DEFINITION => self::FRAGMENT_DEFINITION,
self::FRAGMENT_SPREAD => self::FRAGMENT_SPREAD,
self::INLINE_FRAGMENT => self::INLINE_FRAGMENT,
self::SCHEMA => self::SCHEMA,
self::SCALAR => self::SCALAR,
self::OBJECT => self::OBJECT,
self::FIELD_DEFINITION => self::FIELD_DEFINITION,
self::ARGUMENT_DEFINITION => self::ARGUMENT_DEFINITION,
self::IFACE => self::IFACE,
self::UNION => self::UNION,
self::ENUM => self::ENUM,
self::ENUM_VALUE => self::ENUM_VALUE,
self::INPUT_OBJECT => self::INPUT_OBJECT,
self::INPUT_FIELD_DEFINITION => self::INPUT_FIELD_DEFINITION,
];
/**
* @param string $name
* @return bool
*/
public static function has($name)
{
return isset(self::$locations[$name]);
}
}

View File

@ -3,6 +3,7 @@ namespace GraphQL\Language;
use GraphQL\Error\SyntaxError; use GraphQL\Error\SyntaxError;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
use GraphQL\Utils\BlockString;
/** /**
* A Lexer is a stateful stream generator in that every time * A Lexer is a stateful stream generator in that every time
@ -91,13 +92,18 @@ class Lexer
*/ */
public function advance() public function advance()
{ {
$token = $this->lastToken = $this->token; $this->lastToken = $this->token;
$token = $this->token = $this->lookahead();
return $token;
}
public function lookahead()
{
$token = $this->token;
if ($token->kind !== Token::EOF) { if ($token->kind !== Token::EOF) {
do { do {
$token = $token->next = $this->readToken($token); $token = $token->next ?: ($token->next = $this->readToken($token));
} while ($token->kind === Token::COMMENT); } while ($token->kind === Token::COMMENT);
$this->token = $token;
} }
return $token; return $token;
} }
@ -201,7 +207,15 @@ class Lexer
->readNumber($line, $col, $prev); ->readNumber($line, $col, $prev);
// " // "
case 34: case 34:
return $this->moveStringCursor(-1, -1 * $bytes) list(,$nextCode) = $this->readChar();
list(,$nextNextCode) = $this->moveStringCursor(1, 1)->readChar();
if ($nextCode === 34 && $nextNextCode === 34) {
return $this->moveStringCursor(-2, (-1 * $bytes) - 1)
->readBlockString($line, $col, $prev);
}
return $this->moveStringCursor(-2, (-1 * $bytes) - 1)
->readString($line, $col, $prev); ->readString($line, $col, $prev);
} }
@ -370,12 +384,28 @@ class Lexer
$value = ''; $value = '';
while ( while (
$code && $code !== null &&
// not LineTerminator // not LineTerminator
$code !== 10 && $code !== 13 && $code !== 10 && $code !== 13
// not Quote (")
$code !== 34
) { ) {
// Closing Quote (")
if ($code === 34) {
$value .= $chunk;
// Skip quote
$this->moveStringCursor(1, 1);
return new Token(
Token::STRING,
$start,
$this->position,
$line,
$col,
$prev,
$value
);
}
$this->assertValidStringCharacterCode($code, $this->position); $this->assertValidStringCharacterCode($code, $this->position);
$this->moveStringCursor(1, $bytes); $this->moveStringCursor(1, $bytes);
@ -421,7 +451,6 @@ class Lexer
list ($char, $code, $bytes) = $this->readChar(); list ($char, $code, $bytes) = $this->readChar();
} }
if ($code !== 34) {
throw new SyntaxError( throw new SyntaxError(
$this->source, $this->source,
$this->position, $this->position,
@ -429,19 +458,76 @@ class Lexer
); );
} }
/**
* Reads a block string token from the source file.
*
* """("?"?(\\"""|\\(?!=""")|[^"\\]))*"""
*/
private function readBlockString($line, $col, Token $prev)
{
$start = $this->position;
// Skip leading quotes and read first string char:
list ($char, $code, $bytes) = $this->moveStringCursor(3, 3)->readChar();
$chunk = '';
$value = '';
while ($code !== null) {
// Closing Triple-Quote (""")
if ($code === 34) {
// Move 2 quotes
list(,$nextCode) = $this->moveStringCursor(1, 1)->readChar();
list(,$nextNextCode) = $this->moveStringCursor(1, 1)->readChar();
if ($nextCode === 34 && $nextNextCode === 34) {
$value .= $chunk; $value .= $chunk;
// Skip trailing quote:
$this->moveStringCursor(1, 1); $this->moveStringCursor(1, 1);
return new Token( return new Token(
Token::STRING, Token::BLOCK_STRING,
$start, $start,
$this->position, $this->position,
$line, $line,
$col, $col,
$prev, $prev,
$value BlockString::value($value)
);
} else {
// move cursor back to before the first quote
$this->moveStringCursor(-2, -2);
}
}
$this->assertValidBlockStringCharacterCode($code, $this->position);
$this->moveStringCursor(1, $bytes);
list(,$nextCode) = $this->readChar();
list(,$nextNextCode) = $this->moveStringCursor(1, 1)->readChar();
list(,$nextNextNextCode) = $this->moveStringCursor(1, 1)->readChar();
// Escape Triple-Quote (\""")
if ($code === 92 &&
$nextCode === 34 &&
$nextNextCode === 34 &&
$nextNextNextCode === 34
) {
$this->moveStringCursor(1, 1);
$value .= $chunk . '"""';
$chunk = '';
} else {
$this->moveStringCursor(-2, -2);
$chunk .= $char;
}
list ($char, $code, $bytes) = $this->readChar();
}
throw new SyntaxError(
$this->source,
$this->position,
'Unterminated string.'
); );
} }
@ -457,6 +543,18 @@ class Lexer
} }
} }
private function assertValidBlockStringCharacterCode($code, $position)
{
// SourceCharacter
if ($code < 0x0020 && $code !== 0x0009 && $code !== 0x000A && $code !== 0x000D) {
throw new SyntaxError(
$this->source,
$position,
'Invalid character within String: ' . Utils::printCharCode($code)
);
}
}
/** /**
* Reads from body starting at startPosition until it finds a non-whitespace * Reads from body starting at startPosition until it finds a non-whitespace
* or commented character, then places cursor to the position of that character. * or commented character, then places cursor to the position of that character.
@ -537,7 +635,7 @@ class Lexer
$byteStreamPosition = $this->byteStreamPosition; $byteStreamPosition = $this->byteStreamPosition;
} }
$code = 0; $code = null;
$utf8char = ''; $utf8char = '';
$bytes = 0; $bytes = 0;
$positionOffset = 0; $positionOffset = 0;

View File

@ -4,11 +4,15 @@ namespace GraphQL\Language;
use GraphQL\Language\AST\ArgumentNode; use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\DirectiveDefinitionNode; use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\EnumTypeDefinitionNode; use GraphQL\Language\AST\EnumTypeDefinitionNode;
use GraphQL\Language\AST\EnumTypeExtensionNode;
use GraphQL\Language\AST\EnumValueDefinitionNode; use GraphQL\Language\AST\EnumValueDefinitionNode;
use GraphQL\Language\AST\ExecutableDefinitionNode;
use GraphQL\Language\AST\FieldDefinitionNode; use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode; use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeExtensionNode;
use GraphQL\Language\AST\InputValueDefinitionNode; use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode; use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
use GraphQL\Language\AST\ListValueNode; use GraphQL\Language\AST\ListValueNode;
use GraphQL\Language\AST\BooleanValueNode; use GraphQL\Language\AST\BooleanValueNode;
use GraphQL\Language\AST\DirectiveNode; use GraphQL\Language\AST\DirectiveNode;
@ -33,12 +37,15 @@ use GraphQL\Language\AST\ObjectValueNode;
use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\OperationTypeDefinitionNode; use GraphQL\Language\AST\OperationTypeDefinitionNode;
use GraphQL\Language\AST\ScalarTypeDefinitionNode; use GraphQL\Language\AST\ScalarTypeDefinitionNode;
use GraphQL\Language\AST\ScalarTypeExtensionNode;
use GraphQL\Language\AST\SchemaDefinitionNode; use GraphQL\Language\AST\SchemaDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\AST\StringValueNode; use GraphQL\Language\AST\StringValueNode;
use GraphQL\Language\AST\TypeExtensionDefinitionNode; use GraphQL\Language\AST\ObjectTypeExtensionNode;
use GraphQL\Language\AST\TypeExtensionNode;
use GraphQL\Language\AST\TypeSystemDefinitionNode; use GraphQL\Language\AST\TypeSystemDefinitionNode;
use GraphQL\Language\AST\UnionTypeDefinitionNode; use GraphQL\Language\AST\UnionTypeDefinitionNode;
use GraphQL\Language\AST\UnionTypeExtensionNode;
use GraphQL\Language\AST\VariableNode; use GraphQL\Language\AST\VariableNode;
use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Error\SyntaxError; use GraphQL\Error\SyntaxError;
@ -59,10 +66,25 @@ class Parser
* in the source that they correspond to. This configuration flag * in the source that they correspond to. This configuration flag
* disables that behavior for performance or testing.) * disables that behavior for performance or testing.)
* *
* experimentalFragmentVariables: boolean,
* (If enabled, the parser will understand and parse variable definitions
* contained in a fragment definition. They'll be represented in the
* `variableDefinitions` field of the FragmentDefinitionNode.
*
* The syntax is identical to normal, query-defined variables. For example:
*
* fragment A($var: Boolean = false) on T {
* ...
* }
*
* Note: this feature is experimental and may change or be removed in the
* future.)
*
* @api * @api
* @param Source|string $source * @param Source|string $source
* @param array $options * @param array $options
* @return DocumentNode * @return DocumentNode
* @throws SyntaxError
*/ */
public static function parse($source, array $options = []) public static function parse($source, array $options = [])
{ {
@ -321,26 +343,20 @@ class Parser
} }
/** /**
* @return OperationDefinitionNode|FragmentDefinitionNode|TypeSystemDefinitionNode * @return ExecutableDefinitionNode|TypeSystemDefinitionNode
* @throws SyntaxError * @throws SyntaxError
*/ */
function parseDefinition() function parseDefinition()
{ {
if ($this->peek(Token::BRACE_L)) {
return $this->parseOperationDefinition();
}
if ($this->peek(Token::NAME)) { if ($this->peek(Token::NAME)) {
switch ($this->lexer->token->value) { switch ($this->lexer->token->value) {
case 'query': case 'query':
case 'mutation': case 'mutation':
case 'subscription': case 'subscription':
return $this->parseOperationDefinition();
case 'fragment': case 'fragment':
return $this->parseFragmentDefinition(); return $this->parseExecutableDefinition();
// Note: the Type System IDL is an experimental non-spec addition. // Note: The schema definition language is an experimental addition.
case 'schema': case 'schema':
case 'scalar': case 'scalar':
case 'type': case 'type':
@ -350,8 +366,37 @@ class Parser
case 'input': case 'input':
case 'extend': case 'extend':
case 'directive': case 'directive':
// Note: The schema definition language is an experimental addition.
return $this->parseTypeSystemDefinition(); return $this->parseTypeSystemDefinition();
} }
} else if ($this->peek(Token::BRACE_L)) {
return $this->parseExecutableDefinition();
} else if ($this->peekDescription()) {
// Note: The schema definition language is an experimental addition.
return $this->parseTypeSystemDefinition();
}
throw $this->unexpected();
}
/**
* @return ExecutableDefinitionNode
* @throws SyntaxError
*/
function parseExecutableDefinition()
{
if ($this->peek(Token::NAME)) {
switch ($this->lexer->token->value) {
case 'query':
case 'mutation':
case 'subscription':
return $this->parseOperationDefinition();
case 'fragment':
return $this->parseFragmentDefinition();
}
} else if ($this->peek(Token::BRACE_L)) {
return $this->parseOperationDefinition();
} }
throw $this->unexpected(); throw $this->unexpected();
@ -370,7 +415,7 @@ class Parser
return new OperationDefinitionNode([ return new OperationDefinitionNode([
'operation' => 'query', 'operation' => 'query',
'name' => null, 'name' => null,
'variableDefinitions' => null, 'variableDefinitions' => new NodeList([]),
'directives' => new NodeList([]), 'directives' => new NodeList([]),
'selectionSet' => $this->parseSelectionSet(), 'selectionSet' => $this->parseSelectionSet(),
'loc' => $this->loc($start) 'loc' => $this->loc($start)
@ -388,7 +433,7 @@ class Parser
'operation' => $operation, 'operation' => $operation,
'name' => $name, 'name' => $name,
'variableDefinitions' => $this->parseVariableDefinitions(), 'variableDefinitions' => $this->parseVariableDefinitions(),
'directives' => $this->parseDirectives(), 'directives' => $this->parseDirectives(false),
'selectionSet' => $this->parseSelectionSet(), 'selectionSet' => $this->parseSelectionSet(),
'loc' => $this->loc($start) 'loc' => $this->loc($start)
]); ]);
@ -404,7 +449,6 @@ class Parser
switch ($operationToken->value) { switch ($operationToken->value) {
case 'query': return 'query'; case 'query': return 'query';
case 'mutation': return 'mutation'; case 'mutation': return 'mutation';
// Note: subscription is an experimental non-spec addition.
case 'subscription': return 'subscription'; case 'subscription': return 'subscription';
} }
@ -490,6 +534,7 @@ class Parser
/** /**
* @return FieldNode * @return FieldNode
* @throws SyntaxError
*/ */
function parseField() function parseField()
{ {
@ -507,20 +552,23 @@ class Parser
return new FieldNode([ return new FieldNode([
'alias' => $alias, 'alias' => $alias,
'name' => $name, 'name' => $name,
'arguments' => $this->parseArguments(), 'arguments' => $this->parseArguments(false),
'directives' => $this->parseDirectives(), 'directives' => $this->parseDirectives(false),
'selectionSet' => $this->peek(Token::BRACE_L) ? $this->parseSelectionSet() : null, 'selectionSet' => $this->peek(Token::BRACE_L) ? $this->parseSelectionSet() : null,
'loc' => $this->loc($start) 'loc' => $this->loc($start)
]); ]);
} }
/** /**
* @param bool $isConst
* @return ArgumentNode[]|NodeList * @return ArgumentNode[]|NodeList
* @throws SyntaxError
*/ */
function parseArguments() function parseArguments($isConst)
{ {
$item = $isConst ? 'parseConstArgument' : 'parseArgument';
return $this->peek(Token::PAREN_L) ? return $this->peek(Token::PAREN_L) ?
$this->many(Token::PAREN_L, [$this, 'parseArgument'], Token::PAREN_R) : $this->many(Token::PAREN_L, [$this, $item], Token::PAREN_R) :
new NodeList([]); new NodeList([]);
} }
@ -543,6 +591,25 @@ class Parser
]); ]);
} }
/**
* @return ArgumentNode
* @throws SyntaxError
*/
function parseConstArgument()
{
$start = $this->lexer->token;
$name = $this->parseName();
$this->expect(Token::COLON);
$value = $this->parseConstValue();
return new ArgumentNode([
'name' => $name,
'value' => $value,
'loc' => $this->loc($start)
]);
}
// Implements the parsing rules in the Fragments section. // Implements the parsing rules in the Fragments section.
/** /**
@ -557,7 +624,7 @@ class Parser
if ($this->peek(Token::NAME) && $this->lexer->token->value !== 'on') { if ($this->peek(Token::NAME) && $this->lexer->token->value !== 'on') {
return new FragmentSpreadNode([ return new FragmentSpreadNode([
'name' => $this->parseFragmentName(), 'name' => $this->parseFragmentName(),
'directives' => $this->parseDirectives(), 'directives' => $this->parseDirectives(false),
'loc' => $this->loc($start) 'loc' => $this->loc($start)
]); ]);
} }
@ -570,7 +637,7 @@ class Parser
return new InlineFragmentNode([ return new InlineFragmentNode([
'typeCondition' => $typeCondition, 'typeCondition' => $typeCondition,
'directives' => $this->parseDirectives(), 'directives' => $this->parseDirectives(false),
'selectionSet' => $this->parseSelectionSet(), 'selectionSet' => $this->parseSelectionSet(),
'loc' => $this->loc($start) 'loc' => $this->loc($start)
]); ]);
@ -586,13 +653,21 @@ class Parser
$this->expectKeyword('fragment'); $this->expectKeyword('fragment');
$name = $this->parseFragmentName(); $name = $this->parseFragmentName();
// Experimental support for defining variables within fragments changes
// the grammar of FragmentDefinition:
// - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet
$variableDefinitions = null;
if (isset($this->lexer->options['experimentalFragmentVariables'])) {
$variableDefinitions = $this->parseVariableDefinitions();
}
$this->expectKeyword('on'); $this->expectKeyword('on');
$typeCondition = $this->parseNamedType(); $typeCondition = $this->parseNamedType();
return new FragmentDefinitionNode([ return new FragmentDefinitionNode([
'name' => $name, 'name' => $name,
'variableDefinitions' => $variableDefinitions,
'typeCondition' => $typeCondition, 'typeCondition' => $typeCondition,
'directives' => $this->parseDirectives(), 'directives' => $this->parseDirectives(false),
'selectionSet' => $this->parseSelectionSet(), 'selectionSet' => $this->parseSelectionSet(),
'loc' => $this->loc($start) 'loc' => $this->loc($start)
]); ]);
@ -655,11 +730,8 @@ class Parser
'loc' => $this->loc($token) 'loc' => $this->loc($token)
]); ]);
case Token::STRING: case Token::STRING:
$this->lexer->advance(); case Token::BLOCK_STRING:
return new StringValueNode([ return $this->parseStringLiteral();
'value' => $token->value,
'loc' => $this->loc($token)
]);
case Token::NAME: case Token::NAME:
if ($token->value === 'true' || $token->value === 'false') { if ($token->value === 'true' || $token->value === 'false') {
$this->lexer->advance(); $this->lexer->advance();
@ -690,6 +762,20 @@ class Parser
throw $this->unexpected(); throw $this->unexpected();
} }
/**
* @return StringValueNode
*/
function parseStringLiteral() {
$token = $this->lexer->token;
$this->lexer->advance();
return new StringValueNode([
'value' => $token->value,
'block' => $token->kind === Token::BLOCK_STRING,
'loc' => $this->loc($token)
]);
}
/** /**
* @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode|VariableNode * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode|VariableNode
* @throws SyntaxError * @throws SyntaxError
@ -760,28 +846,31 @@ class Parser
// Implements the parsing rules in the Directives section. // Implements the parsing rules in the Directives section.
/** /**
* @param bool $isConst
* @return DirectiveNode[]|NodeList * @return DirectiveNode[]|NodeList
* @throws SyntaxError
*/ */
function parseDirectives() function parseDirectives($isConst)
{ {
$directives = []; $directives = [];
while ($this->peek(Token::AT)) { while ($this->peek(Token::AT)) {
$directives[] = $this->parseDirective(); $directives[] = $this->parseDirective($isConst);
} }
return new NodeList($directives); return new NodeList($directives);
} }
/** /**
* @param bool $isConst
* @return DirectiveNode * @return DirectiveNode
* @throws SyntaxError * @throws SyntaxError
*/ */
function parseDirective() function parseDirective($isConst)
{ {
$start = $this->lexer->token; $start = $this->lexer->token;
$this->expect(Token::AT); $this->expect(Token::AT);
return new DirectiveNode([ return new DirectiveNode([
'name' => $this->parseName(), 'name' => $this->parseName(),
'arguments' => $this->parseArguments(), 'arguments' => $this->parseArguments($isConst),
'loc' => $this->loc($start) 'loc' => $this->loc($start)
]); ]);
} }
@ -834,7 +923,7 @@ class Parser
* TypeSystemDefinition : * TypeSystemDefinition :
* - SchemaDefinition * - SchemaDefinition
* - TypeDefinition * - TypeDefinition
* - TypeExtensionDefinition * - TypeExtension
* - DirectiveDefinition * - DirectiveDefinition
* *
* TypeDefinition : * TypeDefinition :
@ -850,8 +939,13 @@ class Parser
*/ */
function parseTypeSystemDefinition() function parseTypeSystemDefinition()
{ {
if ($this->peek(Token::NAME)) { // Many definitions begin with a description and require a lookahead.
switch ($this->lexer->token->value) { $keywordToken = $this->peekDescription()
? $this->lexer->lookahead()
: $this->lexer->token;
if ($keywordToken->kind === Token::NAME) {
switch ($keywordToken->value) {
case 'schema': return $this->parseSchemaDefinition(); case 'schema': return $this->parseSchemaDefinition();
case 'scalar': return $this->parseScalarTypeDefinition(); case 'scalar': return $this->parseScalarTypeDefinition();
case 'type': return $this->parseObjectTypeDefinition(); case 'type': return $this->parseObjectTypeDefinition();
@ -859,12 +953,28 @@ class Parser
case 'union': return $this->parseUnionTypeDefinition(); case 'union': return $this->parseUnionTypeDefinition();
case 'enum': return $this->parseEnumTypeDefinition(); case 'enum': return $this->parseEnumTypeDefinition();
case 'input': return $this->parseInputObjectTypeDefinition(); case 'input': return $this->parseInputObjectTypeDefinition();
case 'extend': return $this->parseTypeExtensionDefinition(); case 'extend': return $this->parseTypeExtension();
case 'directive': return $this->parseDirectiveDefinition(); case 'directive': return $this->parseDirectiveDefinition();
} }
} }
throw $this->unexpected(); throw $this->unexpected($keywordToken);
}
/**
* @return bool
*/
function peekDescription() {
return $this->peek(Token::STRING) || $this->peek(Token::BLOCK_STRING);
}
/**
* @return StringValueNode|null
*/
function parseDescription() {
if ($this->peekDescription()) {
return $this->parseStringLiteral();
}
} }
/** /**
@ -875,7 +985,7 @@ class Parser
{ {
$start = $this->lexer->token; $start = $this->lexer->token;
$this->expectKeyword('schema'); $this->expectKeyword('schema');
$directives = $this->parseDirectives(); $directives = $this->parseDirectives(true);
$operationTypes = $this->many( $operationTypes = $this->many(
Token::BRACE_L, Token::BRACE_L,
@ -892,6 +1002,7 @@ class Parser
/** /**
* @return OperationTypeDefinitionNode * @return OperationTypeDefinitionNode
* @throws SyntaxError
*/ */
function parseOperationTypeDefinition() function parseOperationTypeDefinition()
{ {
@ -914,11 +1025,10 @@ class Parser
function parseScalarTypeDefinition() function parseScalarTypeDefinition()
{ {
$start = $this->lexer->token; $start = $this->lexer->token;
$description = $this->parseDescription();
$this->expectKeyword('scalar'); $this->expectKeyword('scalar');
$name = $this->parseName(); $name = $this->parseName();
$directives = $this->parseDirectives(); $directives = $this->parseDirectives(true);
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
return new ScalarTypeDefinitionNode([ return new ScalarTypeDefinitionNode([
'name' => $name, 'name' => $name,
@ -935,18 +1045,12 @@ class Parser
function parseObjectTypeDefinition() function parseObjectTypeDefinition()
{ {
$start = $this->lexer->token; $start = $this->lexer->token;
$description = $this->parseDescription();
$this->expectKeyword('type'); $this->expectKeyword('type');
$name = $this->parseName(); $name = $this->parseName();
$interfaces = $this->parseImplementsInterfaces(); $interfaces = $this->parseImplementsInterfaces();
$directives = $this->parseDirectives(); $directives = $this->parseDirectives(true);
$fields = $this->parseFieldsDefinition();
$fields = $this->any(
Token::BRACE_L,
[$this, 'parseFieldDefinition'],
Token::BRACE_R
);
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
return new ObjectTypeDefinitionNode([ return new ObjectTypeDefinitionNode([
'name' => $name, 'name' => $name,
@ -973,6 +1077,21 @@ class Parser
return $types; return $types;
} }
/**
* @return FieldDefinitionNode[]|NodeList
* @throws SyntaxError
*/
function parseFieldsDefinition()
{
return $this->peek(Token::BRACE_L)
? $this->many(
Token::BRACE_L,
[$this, 'parseFieldDefinition'],
Token::BRACE_R
)
: new NodeList([]);
}
/** /**
* @return FieldDefinitionNode * @return FieldDefinitionNode
* @throws SyntaxError * @throws SyntaxError
@ -980,13 +1099,12 @@ class Parser
function parseFieldDefinition() function parseFieldDefinition()
{ {
$start = $this->lexer->token; $start = $this->lexer->token;
$description = $this->parseDescription();
$name = $this->parseName(); $name = $this->parseName();
$args = $this->parseArgumentDefs(); $args = $this->parseArgumentDefs();
$this->expect(Token::COLON); $this->expect(Token::COLON);
$type = $this->parseTypeReference(); $type = $this->parseTypeReference();
$directives = $this->parseDirectives(); $directives = $this->parseDirectives(true);
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
return new FieldDefinitionNode([ return new FieldDefinitionNode([
'name' => $name, 'name' => $name,
@ -1000,11 +1118,12 @@ class Parser
/** /**
* @return InputValueDefinitionNode[]|NodeList * @return InputValueDefinitionNode[]|NodeList
* @throws SyntaxError
*/ */
function parseArgumentDefs() function parseArgumentDefs()
{ {
if (!$this->peek(Token::PAREN_L)) { if (!$this->peek(Token::PAREN_L)) {
return []; return new NodeList([]);
} }
return $this->many(Token::PAREN_L, [$this, 'parseInputValueDef'], Token::PAREN_R); return $this->many(Token::PAREN_L, [$this, 'parseInputValueDef'], Token::PAREN_R);
} }
@ -1016,6 +1135,7 @@ class Parser
function parseInputValueDef() function parseInputValueDef()
{ {
$start = $this->lexer->token; $start = $this->lexer->token;
$description = $this->parseDescription();
$name = $this->parseName(); $name = $this->parseName();
$this->expect(Token::COLON); $this->expect(Token::COLON);
$type = $this->parseTypeReference(); $type = $this->parseTypeReference();
@ -1023,8 +1143,7 @@ class Parser
if ($this->skip(Token::EQUALS)) { if ($this->skip(Token::EQUALS)) {
$defaultValue = $this->parseConstValue(); $defaultValue = $this->parseConstValue();
} }
$directives = $this->parseDirectives(); $directives = $this->parseDirectives(true);
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
return new InputValueDefinitionNode([ return new InputValueDefinitionNode([
'name' => $name, 'name' => $name,
'type' => $type, 'type' => $type,
@ -1042,16 +1161,11 @@ class Parser
function parseInterfaceTypeDefinition() function parseInterfaceTypeDefinition()
{ {
$start = $this->lexer->token; $start = $this->lexer->token;
$description = $this->parseDescription();
$this->expectKeyword('interface'); $this->expectKeyword('interface');
$name = $this->parseName(); $name = $this->parseName();
$directives = $this->parseDirectives(); $directives = $this->parseDirectives(true);
$fields = $this->any( $fields = $this->parseFieldsDefinition();
Token::BRACE_L,
[$this, 'parseFieldDefinition'],
Token::BRACE_R
);
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
return new InterfaceTypeDefinitionNode([ return new InterfaceTypeDefinitionNode([
'name' => $name, 'name' => $name,
@ -1069,13 +1183,11 @@ class Parser
function parseUnionTypeDefinition() function parseUnionTypeDefinition()
{ {
$start = $this->lexer->token; $start = $this->lexer->token;
$description = $this->parseDescription();
$this->expectKeyword('union'); $this->expectKeyword('union');
$name = $this->parseName(); $name = $this->parseName();
$directives = $this->parseDirectives(); $directives = $this->parseDirectives(true);
$this->expect(Token::EQUALS); $types = $this->parseMemberTypesDefinition();
$types = $this->parseUnionMembers();
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
return new UnionTypeDefinitionNode([ return new UnionTypeDefinitionNode([
'name' => $name, 'name' => $name,
@ -1087,22 +1199,23 @@ class Parser
} }
/** /**
* UnionMembers : * MemberTypes :
* - `|`? NamedType * - `|`? NamedType
* - UnionMembers | NamedType * - MemberTypes | NamedType
* *
* @return NamedTypeNode[] * @return NamedTypeNode[]
*/ */
function parseUnionMembers() function parseMemberTypesDefinition()
{ {
$types = [];
if ($this->skip(Token::EQUALS)) {
// Optional leading pipe // Optional leading pipe
$this->skip(Token::PIPE); $this->skip(Token::PIPE);
$members = [];
do { do {
$members[] = $this->parseNamedType(); $types[] = $this->parseNamedType();
} while ($this->skip(Token::PIPE)); } while ($this->skip(Token::PIPE));
return $members; }
return $types;
} }
/** /**
@ -1112,16 +1225,11 @@ class Parser
function parseEnumTypeDefinition() function parseEnumTypeDefinition()
{ {
$start = $this->lexer->token; $start = $this->lexer->token;
$description = $this->parseDescription();
$this->expectKeyword('enum'); $this->expectKeyword('enum');
$name = $this->parseName(); $name = $this->parseName();
$directives = $this->parseDirectives(); $directives = $this->parseDirectives(true);
$values = $this->many( $values = $this->parseEnumValuesDefinition();
Token::BRACE_L,
[$this, 'parseEnumValueDefinition'],
Token::BRACE_R
);
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
return new EnumTypeDefinitionNode([ return new EnumTypeDefinitionNode([
'name' => $name, 'name' => $name,
@ -1132,16 +1240,31 @@ class Parser
]); ]);
} }
/**
* @return EnumValueDefinitionNode[]|NodeList
* @throws SyntaxError
*/
function parseEnumValuesDefinition()
{
return $this->peek(Token::BRACE_L)
? $this->many(
Token::BRACE_L,
[$this, 'parseEnumValueDefinition'],
Token::BRACE_R
)
: new NodeList([]);
}
/** /**
* @return EnumValueDefinitionNode * @return EnumValueDefinitionNode
* @throws SyntaxError
*/ */
function parseEnumValueDefinition() function parseEnumValueDefinition()
{ {
$start = $this->lexer->token; $start = $this->lexer->token;
$description = $this->parseDescription();
$name = $this->parseName(); $name = $this->parseName();
$directives = $this->parseDirectives(); $directives = $this->parseDirectives(true);
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
return new EnumValueDefinitionNode([ return new EnumValueDefinitionNode([
'name' => $name, 'name' => $name,
@ -1158,16 +1281,11 @@ class Parser
function parseInputObjectTypeDefinition() function parseInputObjectTypeDefinition()
{ {
$start = $this->lexer->token; $start = $this->lexer->token;
$description = $this->parseDescription();
$this->expectKeyword('input'); $this->expectKeyword('input');
$name = $this->parseName(); $name = $this->parseName();
$directives = $this->parseDirectives(); $directives = $this->parseDirectives(true);
$fields = $this->any( $fields = $this->parseInputFieldsDefinition();
Token::BRACE_L,
[$this, 'parseInputValueDef'],
Token::BRACE_R
);
$description = $this->getDescriptionFromAdjacentCommentTokens($start);
return new InputObjectTypeDefinitionNode([ return new InputObjectTypeDefinitionNode([
'name' => $name, 'name' => $name,
@ -1179,17 +1297,206 @@ class Parser
} }
/** /**
* @return TypeExtensionDefinitionNode * @return InputValueDefinitionNode[]|NodeList
* @throws SyntaxError * @throws SyntaxError
*/ */
function parseTypeExtensionDefinition() function parseInputFieldsDefinition() {
return $this->peek(Token::BRACE_L)
? $this->many(
Token::BRACE_L,
[$this, 'parseInputValueDef'],
Token::BRACE_R
)
: new NodeList([]);
}
/**
* TypeExtension :
* - ScalarTypeExtension
* - ObjectTypeExtension
* - InterfaceTypeExtension
* - UnionTypeExtension
* - EnumTypeExtension
* - InputObjectTypeDefinition
*
* @return TypeExtensionNode
* @throws SyntaxError
*/
function parseTypeExtension()
{ {
$keywordToken = $this->lexer->lookahead();
if ($keywordToken->kind === Token::NAME) {
switch ($keywordToken->value) {
case 'scalar':
return $this->parseScalarTypeExtension();
case 'type':
return $this->parseObjectTypeExtension();
case 'interface':
return $this->parseInterfaceTypeExtension();
case 'union':
return $this->parseUnionTypeExtension();
case 'enum':
return $this->parseEnumTypeExtension();
case 'input':
return $this->parseInputObjectTypeExtension();
}
}
throw $this->unexpected($keywordToken);
}
/**
* @return ScalarTypeExtensionNode
* @throws SyntaxError
*/
function parseScalarTypeExtension() {
$start = $this->lexer->token; $start = $this->lexer->token;
$this->expectKeyword('extend'); $this->expectKeyword('extend');
$definition = $this->parseObjectTypeDefinition(); $this->expectKeyword('scalar');
$name = $this->parseName();
$directives = $this->parseDirectives(true);
if (count($directives) === 0) {
throw $this->unexpected();
}
return new TypeExtensionDefinitionNode([ return new ScalarTypeExtensionNode([
'definition' => $definition, 'name' => $name,
'directives' => $directives,
'loc' => $this->loc($start)
]);
}
/**
* @return ObjectTypeExtensionNode
* @throws SyntaxError
*/
function parseObjectTypeExtension() {
$start = $this->lexer->token;
$this->expectKeyword('extend');
$this->expectKeyword('type');
$name = $this->parseName();
$interfaces = $this->parseImplementsInterfaces();
$directives = $this->parseDirectives(true);
$fields = $this->parseFieldsDefinition();
if (
!$interfaces &&
count($directives) === 0 &&
count($fields) === 0
) {
throw $this->unexpected();
}
return new ObjectTypeExtensionNode([
'name' => $name,
'interfaces' => $interfaces,
'directives' => $directives,
'fields' => $fields,
'loc' => $this->loc($start)
]);
}
/**
* @return InterfaceTypeExtensionNode
* @throws SyntaxError
*/
function parseInterfaceTypeExtension() {
$start = $this->lexer->token;
$this->expectKeyword('extend');
$this->expectKeyword('interface');
$name = $this->parseName();
$directives = $this->parseDirectives(true);
$fields = $this->parseFieldsDefinition();
if (
count($directives) === 0 &&
count($fields) === 0
) {
throw $this->unexpected();
}
return new InterfaceTypeExtensionNode([
'name' => $name,
'directives' => $directives,
'fields' => $fields,
'loc' => $this->loc($start)
]);
}
/**
* @return UnionTypeExtensionNode
* @throws SyntaxError
*/
function parseUnionTypeExtension() {
$start = $this->lexer->token;
$this->expectKeyword('extend');
$this->expectKeyword('union');
$name = $this->parseName();
$directives = $this->parseDirectives(true);
$types = $this->parseMemberTypesDefinition();
if (
count($directives) === 0 &&
!$types
) {
throw $this->unexpected();
}
return new UnionTypeExtensionNode([
'name' => $name,
'directives' => $directives,
'types' => $types,
'loc' => $this->loc($start)
]);
}
/**
* @return EnumTypeExtensionNode
* @throws SyntaxError
*/
function parseEnumTypeExtension() {
$start = $this->lexer->token;
$this->expectKeyword('extend');
$this->expectKeyword('enum');
$name = $this->parseName();
$directives = $this->parseDirectives(true);
$values = $this->parseEnumValuesDefinition();
if (
count($directives) === 0 &&
count($values) === 0
) {
throw $this->unexpected();
}
return new EnumTypeExtensionNode([
'name' => $name,
'directives' => $directives,
'values' => $values,
'loc' => $this->loc($start)
]);
}
/**
* @return InputObjectTypeExtensionNode
* @throws SyntaxError
*/
function parseInputObjectTypeExtension() {
$start = $this->lexer->token;
$this->expectKeyword('extend');
$this->expectKeyword('input');
$name = $this->parseName();
$directives = $this->parseDirectives(true);
$fields = $this->parseInputFieldsDefinition();
if (
count($directives) === 0 &&
count($fields) === 0
) {
throw $this->unexpected();
}
return new InputObjectTypeExtensionNode([
'name' => $name,
'directives' => $directives,
'fields' => $fields,
'loc' => $this->loc($start) 'loc' => $this->loc($start)
]); ]);
} }
@ -1204,6 +1511,7 @@ class Parser
function parseDirectiveDefinition() function parseDirectiveDefinition()
{ {
$start = $this->lexer->token; $start = $this->lexer->token;
$description = $this->parseDescription();
$this->expectKeyword('directive'); $this->expectKeyword('directive');
$this->expect(Token::AT); $this->expect(Token::AT);
$name = $this->parseName(); $name = $this->parseName();
@ -1215,12 +1523,14 @@ class Parser
'name' => $name, 'name' => $name,
'arguments' => $args, 'arguments' => $args,
'locations' => $locations, 'locations' => $locations,
'loc' => $this->loc($start) 'loc' => $this->loc($start),
'description' => $description
]); ]);
} }
/** /**
* @return NameNode[] * @return NameNode[]
* @throws SyntaxError
*/ */
function parseDirectiveLocations() function parseDirectiveLocations()
{ {
@ -1228,32 +1538,23 @@ class Parser
$this->skip(Token::PIPE); $this->skip(Token::PIPE);
$locations = []; $locations = [];
do { do {
$locations[] = $this->parseName(); $locations[] = $this->parseDirectiveLocation();
} while ($this->skip(Token::PIPE)); } while ($this->skip(Token::PIPE));
return $locations; return $locations;
} }
/** /**
* @param Token $nameToken * @return NameNode
* @return null|string * @throws SyntaxError
*/ */
private function getDescriptionFromAdjacentCommentTokens(Token $nameToken) function parseDirectiveLocation()
{ {
$description = null; $start = $this->lexer->token;
$name = $this->parseName();
$currentToken = $nameToken; if (DirectiveLocation::has($name->value)) {
$previousToken = $currentToken->prev; return $name;
while ($previousToken->kind == Token::COMMENT
&& ($previousToken->line + 1) == $currentToken->line
) {
$description = $previousToken->value . $description;
// walk the tokens backwards until no longer adjacent comments
$currentToken = $previousToken;
$previousToken = $currentToken->prev;
} }
return $description; throw $this->unexpected($start);
} }
} }

View File

@ -4,11 +4,14 @@ namespace GraphQL\Language;
use GraphQL\Language\AST\ArgumentNode; use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\DirectiveDefinitionNode; use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\EnumTypeDefinitionNode; use GraphQL\Language\AST\EnumTypeDefinitionNode;
use GraphQL\Language\AST\EnumTypeExtensionNode;
use GraphQL\Language\AST\EnumValueDefinitionNode; use GraphQL\Language\AST\EnumValueDefinitionNode;
use GraphQL\Language\AST\FieldDefinitionNode; use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode; use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeExtensionNode;
use GraphQL\Language\AST\InputValueDefinitionNode; use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode; use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
use GraphQL\Language\AST\ListValueNode; use GraphQL\Language\AST\ListValueNode;
use GraphQL\Language\AST\BooleanValueNode; use GraphQL\Language\AST\BooleanValueNode;
use GraphQL\Language\AST\DirectiveNode; use GraphQL\Language\AST\DirectiveNode;
@ -32,11 +35,13 @@ use GraphQL\Language\AST\ObjectValueNode;
use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\OperationTypeDefinitionNode; use GraphQL\Language\AST\OperationTypeDefinitionNode;
use GraphQL\Language\AST\ScalarTypeDefinitionNode; use GraphQL\Language\AST\ScalarTypeDefinitionNode;
use GraphQL\Language\AST\ScalarTypeExtensionNode;
use GraphQL\Language\AST\SchemaDefinitionNode; use GraphQL\Language\AST\SchemaDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\AST\StringValueNode; use GraphQL\Language\AST\StringValueNode;
use GraphQL\Language\AST\TypeExtensionDefinitionNode; use GraphQL\Language\AST\ObjectTypeExtensionNode;
use GraphQL\Language\AST\UnionTypeDefinitionNode; use GraphQL\Language\AST\UnionTypeDefinitionNode;
use GraphQL\Language\AST\UnionTypeExtensionNode;
use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
@ -126,7 +131,11 @@ class Printer
], ' '); ], ' ');
}, },
NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) { NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) {
return "fragment {$node->name} on {$node->typeCondition} " // Note: fragment variable definitions are experimental and may be changed
// or removed in the future.
return "fragment {$node->name}"
. $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')')
. " on {$node->typeCondition} "
. $this->wrap('', $this->join($node->directives, ' '), ' ') . $this->wrap('', $this->join($node->directives, ' '), ' ')
. $node->selectionSet; . $node->selectionSet;
}, },
@ -138,7 +147,10 @@ class Printer
NodeKind::FLOAT => function(FloatValueNode $node) { NodeKind::FLOAT => function(FloatValueNode $node) {
return $node->value; return $node->value;
}, },
NodeKind::STRING => function(StringValueNode $node) { NodeKind::STRING => function(StringValueNode $node, $key) {
if ($node->block) {
return $this->printBlockString($node->value, $key === 'description');
}
return json_encode($node->value); return json_encode($node->value);
}, },
NodeKind::BOOLEAN => function(BooleanValueNode $node) { NodeKind::BOOLEAN => function(BooleanValueNode $node) {
@ -189,74 +201,150 @@ class Printer
}, },
NodeKind::SCALAR_TYPE_DEFINITION => function(ScalarTypeDefinitionNode $def) { NodeKind::SCALAR_TYPE_DEFINITION => function(ScalarTypeDefinitionNode $def) {
return $this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' '); return $this->join([
$def->description,
$this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' ')
], "\n");
}, },
NodeKind::OBJECT_TYPE_DEFINITION => function(ObjectTypeDefinitionNode $def) { NodeKind::OBJECT_TYPE_DEFINITION => function(ObjectTypeDefinitionNode $def) {
return $this->join([ return $this->join([
$def->description,
$this->join([
'type', 'type',
$def->name, $def->name,
$this->wrap('implements ', $this->join($def->interfaces, ', ')), $this->wrap('implements ', $this->join($def->interfaces, ', ')),
$this->join($def->directives, ' '), $this->join($def->directives, ' '),
$this->block($def->fields) $this->block($def->fields)
], ' '); ], ' ')
], "\n");
}, },
NodeKind::FIELD_DEFINITION => function(FieldDefinitionNode $def) { NodeKind::FIELD_DEFINITION => function(FieldDefinitionNode $def) {
return $def->name return $this->join([
$def->description,
$def->name
. $this->wrap('(', $this->join($def->arguments, ', '), ')') . $this->wrap('(', $this->join($def->arguments, ', '), ')')
. ': ' . $def->type . ': ' . $def->type
. $this->wrap(' ', $this->join($def->directives, ' ')); . $this->wrap(' ', $this->join($def->directives, ' '))
], "\n");
}, },
NodeKind::INPUT_VALUE_DEFINITION => function(InputValueDefinitionNode $def) { NodeKind::INPUT_VALUE_DEFINITION => function(InputValueDefinitionNode $def) {
return $this->join([ return $this->join([
$def->description,
$this->join([
$def->name . ': ' . $def->type, $def->name . ': ' . $def->type,
$this->wrap('= ', $def->defaultValue), $this->wrap('= ', $def->defaultValue),
$this->join($def->directives, ' ') $this->join($def->directives, ' ')
], ' '); ], ' ')
], "\n");
}, },
NodeKind::INTERFACE_TYPE_DEFINITION => function(InterfaceTypeDefinitionNode $def) { NodeKind::INTERFACE_TYPE_DEFINITION => function(InterfaceTypeDefinitionNode $def) {
return $this->join([ return $this->join([
$def->description,
$this->join([
'interface', 'interface',
$def->name, $def->name,
$this->join($def->directives, ' '), $this->join($def->directives, ' '),
$this->block($def->fields) $this->block($def->fields)
], ' '); ], ' ')
], "\n");
}, },
NodeKind::UNION_TYPE_DEFINITION => function(UnionTypeDefinitionNode $def) { NodeKind::UNION_TYPE_DEFINITION => function(UnionTypeDefinitionNode $def) {
return $this->join([ return $this->join([
$def->description,
$this->join([
'union', 'union',
$def->name, $def->name,
$this->join($def->directives, ' '), $this->join($def->directives, ' '),
'= ' . $this->join($def->types, ' | ') $def->types
], ' '); ? '= ' . $this->join($def->types, ' | ')
: ''
], ' ')
], "\n");
}, },
NodeKind::ENUM_TYPE_DEFINITION => function(EnumTypeDefinitionNode $def) { NodeKind::ENUM_TYPE_DEFINITION => function(EnumTypeDefinitionNode $def) {
return $this->join([ return $this->join([
$def->description,
$this->join([
'enum', 'enum',
$def->name, $def->name,
$this->join($def->directives, ' '), $this->join($def->directives, ' '),
$this->block($def->values) $this->block($def->values)
], ' '); ], ' ')
], "\n");
}, },
NodeKind::ENUM_VALUE_DEFINITION => function(EnumValueDefinitionNode $def) { NodeKind::ENUM_VALUE_DEFINITION => function(EnumValueDefinitionNode $def) {
return $this->join([ return $this->join([
$def->name, $def->description,
$this->join($def->directives, ' ') $this->join([$def->name, $this->join($def->directives, ' ')], ' ')
], ' '); ], "\n");
}, },
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => function(InputObjectTypeDefinitionNode $def) { NodeKind::INPUT_OBJECT_TYPE_DEFINITION => function(InputObjectTypeDefinitionNode $def) {
return $this->join([ return $this->join([
$def->description,
$this->join([
'input', 'input',
$def->name, $def->name,
$this->join($def->directives, ' '), $this->join($def->directives, ' '),
$this->block($def->fields) $this->block($def->fields)
], ' ')
], "\n");
},
NodeKind::SCALAR_TYPE_EXTENSION => function(ScalarTypeExtensionNode $def) {
return $this->join([
'extend scalar',
$def->name,
$this->join($def->directives, ' '),
], ' '); ], ' ');
}, },
NodeKind::TYPE_EXTENSION_DEFINITION => function(TypeExtensionDefinitionNode $def) { NodeKind::OBJECT_TYPE_EXTENSION => function(ObjectTypeExtensionNode $def) {
return "extend {$def->definition}"; return $this->join([
'extend type',
$def->name,
$this->wrap('implements ', $this->join($def->interfaces, ', ')),
$this->join($def->directives, ' '),
$this->block($def->fields),
], ' ');
},
NodeKind::INTERFACE_TYPE_EXTENSION => function(InterfaceTypeExtensionNode $def) {
return $this->join([
'extend interface',
$def->name,
$this->join($def->directives, ' '),
$this->block($def->fields),
], ' ');
},
NodeKind::UNION_TYPE_EXTENSION => function(UnionTypeExtensionNode $def) {
return $this->join([
'extend union',
$def->name,
$this->join($def->directives, ' '),
$def->types
? '= ' . $this->join($def->types, ' | ')
: ''
], ' ');
},
NodeKind::ENUM_TYPE_EXTENSION => function(EnumTypeExtensionNode $def) {
return $this->join([
'extend enum',
$def->name,
$this->join($def->directives, ' '),
$this->block($def->values),
], ' ');
},
NodeKind::INPUT_OBJECT_TYPE_EXTENSION => function(InputObjectTypeExtensionNode $def) {
return $this->join([
'extend input',
$def->name,
$this->join($def->directives, ' '),
$this->block($def->fields),
], ' ');
}, },
NodeKind::DIRECTIVE_DEFINITION => function(DirectiveDefinitionNode $def) { NodeKind::DIRECTIVE_DEFINITION => function(DirectiveDefinitionNode $def) {
return 'directive @' . $def->name . $this->wrap('(', $this->join($def->arguments, ', '), ')') return $this->join([
. ' on ' . $this->join($def->locations, ' | '); $def->description,
'directive @' . $def->name . $this->wrap('(', $this->join($def->arguments, ', '), ')')
. ' on ' . $this->join($def->locations, ' | ')
], "\n");
} }
] ]
]); ]);
@ -277,12 +365,14 @@ class Printer
*/ */
public function block($array) public function block($array)
{ {
return $array && $this->length($array) ? $this->indent("{\n" . $this->join($array, "\n")) . "\n}" : '{}'; return ($array && $this->length($array))
? "{\n" . $this->indent($this->join($array, "\n")) . "\n}"
: '';
} }
public function indent($maybeString) public function indent($maybeString)
{ {
return $maybeString ? str_replace("\n", "\n ", $maybeString) : ''; return $maybeString ? ' ' . str_replace("\n", "\n ", $maybeString) : '';
} }
public function manyList($start, $list, $separator, $end) public function manyList($start, $list, $separator, $end)
@ -307,4 +397,16 @@ class Printer
) )
: ''; : '';
} }
/**
* Print a block string in the indented block form by adding a leading and
* trailing blank line. However, if a block string starts with whitespace and is
* a single-line, adding a leading blank line would strip that whitespace.
*/
private function printBlockString($value, $isDescription) {
$escaped = str_replace('"""', '\\"""', $value);
return (($value[0] === ' ' || $value[0] === "\t") && strpos($value, "\n") === false)
? ('"""' . preg_replace('/"$/', "\"\n", $escaped) . '"""')
: ("\"\"\"\n" . ($isDescription ? $escaped : $this->indent($escaped)) . "\n\"\"\"");
}
} }

View File

@ -39,8 +39,8 @@ class Source
* be "Foo.graphql" and location to be `{ line: 40, column: 0 }`. * be "Foo.graphql" and location to be `{ line: 40, column: 0 }`.
* line and column in locationOffset are 1-indexed * line and column in locationOffset are 1-indexed
* *
* @param $body * @param string $body
* @param null $name * @param string|null $name
* @param SourceLocation|null $location * @param SourceLocation|null $location
*/ */
public function __construct($body, $name = null, SourceLocation $location = null) public function __construct($body, $name = null, SourceLocation $location = null)
@ -52,7 +52,7 @@ class Source
$this->body = $body; $this->body = $body;
$this->length = mb_strlen($body, 'UTF-8'); $this->length = mb_strlen($body, 'UTF-8');
$this->name = $name ?: 'GraphQL'; $this->name = $name ?: 'GraphQL request';
$this->locationOffset = $location ?: new SourceLocation(1, 1); $this->locationOffset = $location ?: new SourceLocation(1, 1);
Utils::invariant( Utils::invariant(

View File

@ -27,6 +27,7 @@ class Token
const INT = 'Int'; const INT = 'Int';
const FLOAT = 'Float'; const FLOAT = 'Float';
const STRING = 'String'; const STRING = 'String';
const BLOCK_STRING = 'BlockString';
const COMMENT = 'Comment'; const COMMENT = 'Comment';
/** /**
@ -57,6 +58,7 @@ class Token
$description[self::INT] = 'Int'; $description[self::INT] = 'Int';
$description[self::FLOAT] = 'Float'; $description[self::FLOAT] = 'Float';
$description[self::STRING] = 'String'; $description[self::STRING] = 'String';
$description[self::BLOCK_STRING] = 'BlockString';
$description[self::COMMENT] = 'Comment'; $description[self::COMMENT] = 'Comment';
return $description[$kind]; return $description[$kind];

View File

@ -115,7 +115,15 @@ class Visitor
NodeKind::ARGUMENT => ['name', 'value'], NodeKind::ARGUMENT => ['name', 'value'],
NodeKind::FRAGMENT_SPREAD => ['name', 'directives'], NodeKind::FRAGMENT_SPREAD => ['name', 'directives'],
NodeKind::INLINE_FRAGMENT => ['typeCondition', 'directives', 'selectionSet'], NodeKind::INLINE_FRAGMENT => ['typeCondition', 'directives', 'selectionSet'],
NodeKind::FRAGMENT_DEFINITION => ['name', 'typeCondition', 'directives', 'selectionSet'], NodeKind::FRAGMENT_DEFINITION => [
'name',
// Note: fragment variable definitions are experimental and may be changed
// or removed in the future.
'variableDefinitions',
'typeCondition',
'directives',
'selectionSet'
],
NodeKind::INT => [], NodeKind::INT => [],
NodeKind::FLOAT => [], NodeKind::FLOAT => [],
@ -133,17 +141,24 @@ class Visitor
NodeKind::SCHEMA_DEFINITION => ['directives', 'operationTypes'], NodeKind::SCHEMA_DEFINITION => ['directives', 'operationTypes'],
NodeKind::OPERATION_TYPE_DEFINITION => ['type'], NodeKind::OPERATION_TYPE_DEFINITION => ['type'],
NodeKind::SCALAR_TYPE_DEFINITION => ['name', 'directives'], NodeKind::SCALAR_TYPE_DEFINITION => ['description', 'name', 'directives'],
NodeKind::OBJECT_TYPE_DEFINITION => ['name', 'interfaces', 'directives', 'fields'], NodeKind::OBJECT_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'],
NodeKind::FIELD_DEFINITION => ['name', 'arguments', 'type', 'directives'], NodeKind::FIELD_DEFINITION => ['description', 'name', 'arguments', 'type', 'directives'],
NodeKind::INPUT_VALUE_DEFINITION => ['name', 'type', 'defaultValue', 'directives'], NodeKind::INPUT_VALUE_DEFINITION => ['description', 'name', 'type', 'defaultValue', 'directives'],
NodeKind::INTERFACE_TYPE_DEFINITION => [ 'name', 'directives', 'fields' ], NodeKind::INTERFACE_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'],
NodeKind::UNION_TYPE_DEFINITION => [ 'name', 'directives', 'types' ], NodeKind::UNION_TYPE_DEFINITION => ['description', 'name', 'directives', 'types'],
NodeKind::ENUM_TYPE_DEFINITION => [ 'name', 'directives', 'values' ], NodeKind::ENUM_TYPE_DEFINITION => ['description', 'name', 'directives', 'values'],
NodeKind::ENUM_VALUE_DEFINITION => [ 'name', 'directives' ], NodeKind::ENUM_VALUE_DEFINITION => ['description', 'name', 'directives'],
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => [ 'name', 'directives', 'fields' ], NodeKind::INPUT_OBJECT_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'],
NodeKind::TYPE_EXTENSION_DEFINITION => [ 'definition' ],
NodeKind::DIRECTIVE_DEFINITION => [ 'name', 'arguments', 'locations' ] NodeKind::SCALAR_TYPE_EXTENSION => ['name', 'directives'],
NodeKind::OBJECT_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'],
NodeKind::INTERFACE_TYPE_EXTENSION => ['name', 'directives', 'fields'],
NodeKind::UNION_TYPE_EXTENSION => ['name', 'directives', 'types'],
NodeKind::ENUM_TYPE_EXTENSION => ['name', 'directives', 'values'],
NodeKind::INPUT_OBJECT_TYPE_EXTENSION => ['name', 'directives', 'fields'],
NodeKind::DIRECTIVE_DEFINITION => ['description', 'name', 'arguments', 'locations']
]; ];
/** /**
@ -180,7 +195,7 @@ class Visitor
$isEdited = $isLeaving && count($edits) !== 0; $isEdited = $isLeaving && count($edits) !== 0;
if ($isLeaving) { if ($isLeaving) {
$key = count($ancestors) === 0 ? $UNDEFINED : array_pop($path); $key = !$ancestors ? $UNDEFINED : $path[count($path) - 1];
$node = $parent; $node = $parent;
$parent = array_pop($ancestors); $parent = array_pop($ancestors);
@ -277,7 +292,9 @@ class Visitor
$edits[] = [$key, $node]; $edits[] = [$key, $node];
} }
if (!$isLeaving) { if ($isLeaving) {
array_pop($path);
} else {
$stack = [ $stack = [
'inArray' => $inArray, 'inArray' => $inArray,
'index' => $index, 'index' => $index,

View File

@ -472,6 +472,7 @@ class Server
{ {
try { try {
$schema = $this->getSchema(); $schema = $this->getSchema();
$schema->assertValid();
} catch (InvariantViolation $e) { } catch (InvariantViolation $e) {
throw new InvariantViolation("Cannot validate, schema contains errors: {$e->getMessage()}", null, $e); throw new InvariantViolation("Cannot validate, schema contains errors: {$e->getMessage()}", null, $e);
} }

View File

@ -2,6 +2,7 @@
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Language\AST\BooleanValueNode; use GraphQL\Language\AST\BooleanValueNode;
use GraphQL\Utils\Utils;
/** /**
* Class BooleanType * Class BooleanType
@ -34,18 +35,19 @@ class BooleanType extends ScalarType
*/ */
public function parseValue($value) public function parseValue($value)
{ {
return is_bool($value) ? $value : null; return is_bool($value) ? $value : Utils::undefined();
} }
/** /**
* @param $ast * @param $valueNode
* @param array|null $variables
* @return bool|null * @return bool|null
*/ */
public function parseLiteral($ast) public function parseLiteral($valueNode, array $variables = null)
{ {
if ($ast instanceof BooleanValueNode) { if ($valueNode instanceof BooleanValueNode) {
return (bool) $ast->value; return (bool) $valueNode->value;
} }
return null; return Utils::undefined();
} }
} }

View File

@ -1,6 +1,7 @@
<?php <?php
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Utils\AST;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
/** /**
@ -24,23 +25,28 @@ class CustomScalarType extends ScalarType
*/ */
public function parseValue($value) public function parseValue($value)
{ {
if (Utils::isInvalid($value)) {
return Utils::undefined();
}
if (isset($this->config['parseValue'])) { if (isset($this->config['parseValue'])) {
return call_user_func($this->config['parseValue'], $value); return call_user_func($this->config['parseValue'], $value);
} else { } else {
return null; return $value;
} }
} }
/** /**
* @param $valueNode * @param $valueNode
* @param array|null $variables
* @return mixed * @return mixed
*/ */
public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode) public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode, array $variables = null)
{ {
if (isset($this->config['parseLiteral'])) { if (isset($this->config['parseLiteral'])) {
return call_user_func($this->config['parseLiteral'], $valueNode); return call_user_func($this->config['parseLiteral'], $valueNode, $variables);
} else { } else {
return null; return AST::valueFromASTUntyped($valueNode, $variables);
} }
} }

View File

@ -2,6 +2,8 @@
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Language\AST\DirectiveDefinitionNode; use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\DirectiveLocation;
use GraphQL\Utils\Utils;
/** /**
* Class Directive * Class Directive
@ -18,35 +20,6 @@ class Directive
// Schema Definitions // Schema Definitions
/**
* @var array
* @deprecated as of 8.0 (use DirectiveLocation constants directly)
*/
public static $directiveLocations = [
// Operations:
DirectiveLocation::QUERY => DirectiveLocation::QUERY,
DirectiveLocation::MUTATION => DirectiveLocation::MUTATION,
DirectiveLocation::SUBSCRIPTION => DirectiveLocation::SUBSCRIPTION,
DirectiveLocation::FIELD => DirectiveLocation::FIELD,
DirectiveLocation::FRAGMENT_DEFINITION => DirectiveLocation::FRAGMENT_DEFINITION,
DirectiveLocation::FRAGMENT_SPREAD => DirectiveLocation::FRAGMENT_SPREAD,
DirectiveLocation::INLINE_FRAGMENT => DirectiveLocation::INLINE_FRAGMENT,
// Schema Definitions
DirectiveLocation::SCHEMA => DirectiveLocation::SCHEMA,
DirectiveLocation::SCALAR => DirectiveLocation::SCALAR,
DirectiveLocation::OBJECT => DirectiveLocation::OBJECT,
DirectiveLocation::FIELD_DEFINITION => DirectiveLocation::FIELD_DEFINITION,
DirectiveLocation::ARGUMENT_DEFINITION => DirectiveLocation::ARGUMENT_DEFINITION,
DirectiveLocation::IFACE => DirectiveLocation::IFACE,
DirectiveLocation::UNION => DirectiveLocation::UNION,
DirectiveLocation::ENUM => DirectiveLocation::ENUM,
DirectiveLocation::ENUM_VALUE => DirectiveLocation::ENUM_VALUE,
DirectiveLocation::INPUT_OBJECT => DirectiveLocation::INPUT_OBJECT,
DirectiveLocation::INPUT_FIELD_DEFINITION => DirectiveLocation::INPUT_FIELD_DEFINITION
];
/** /**
* @return Directive * @return Directive
*/ */
@ -74,6 +47,15 @@ class Directive
return $internal['deprecated']; return $internal['deprecated'];
} }
/**
* @param Directive $directive
* @return bool
*/
public static function isSpecifiedDirective(Directive $directive)
{
return in_array($directive->name, array_keys(self::getInternalDirectives()));
}
/** /**
* @return array * @return array
*/ */
@ -178,6 +160,9 @@ class Directive
foreach ($config as $key => $value) { foreach ($config as $key => $value) {
$this->{$key} = $value; $this->{$key} = $value;
} }
Utils::invariant($this->name, 'Directive must be named.');
Utils::invariant(is_array($this->locations), 'Must provide locations for directive.');
$this->config = $config; $this->config = $config;
} }
} }

View File

@ -1,27 +1,17 @@
<?php <?php
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Language\DirectiveLocation as NewDirectiveLocation;
trigger_error(
'GraphQL\Type\Definition\DirectiveLocation was moved to GraphQL\Language\DirectiveLocation and will be deleted on next release',
E_USER_DEPRECATED
);
/** /**
* List of available directive locations * @deprecated moved to GraphQL\Language\DirectiveLocation
*/ */
class DirectiveLocation class DirectiveLocation extends NewDirectiveLocation
{ {
const IFACE = 'INTERFACE';
const SUBSCRIPTION = 'SUBSCRIPTION';
const FRAGMENT_SPREAD = 'FRAGMENT_SPREAD';
const QUERY = 'QUERY';
const MUTATION = 'MUTATION';
const FRAGMENT_DEFINITION = 'FRAGMENT_DEFINITION';
const INPUT_OBJECT = 'INPUT_OBJECT';
const INLINE_FRAGMENT = 'INLINE_FRAGMENT';
const UNION = 'UNION';
const SCALAR = 'SCALAR';
const FIELD_DEFINITION = 'FIELD_DEFINITION';
const ARGUMENT_DEFINITION = 'ARGUMENT_DEFINITION';
const ENUM = 'ENUM';
const OBJECT = 'OBJECT';
const ENUM_VALUE = 'ENUM_VALUE';
const FIELD = 'FIELD';
const SCHEMA = 'SCHEMA';
const INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION';
} }

View File

@ -11,7 +11,7 @@ use GraphQL\Utils\Utils;
* Class EnumType * Class EnumType
* @package GraphQL\Type\Definition * @package GraphQL\Type\Definition
*/ */
class EnumType extends Type implements InputType, OutputType, LeafType class EnumType extends Type implements InputType, OutputType, LeafType, NamedType
{ {
/** /**
* @var EnumTypeDefinitionNode|null * @var EnumTypeDefinitionNode|null
@ -39,7 +39,7 @@ class EnumType extends Type implements InputType, OutputType, LeafType
$config['name'] = $this->tryInferName(); $config['name'] = $this->tryInferName();
} }
Utils::assertValidName($config['name'], !empty($config['isIntrospection'])); Utils::invariant(is_string($config['name']), 'Must provide name.');
Config::validate($config, [ Config::validate($config, [
'name' => Config::NAME | Config::REQUIRED, 'name' => Config::NAME | Config::REQUIRED,
@ -108,25 +108,11 @@ class EnumType extends Type implements InputType, OutputType, LeafType
public function serialize($value) public function serialize($value)
{ {
$lookup = $this->getValueLookup(); $lookup = $this->getValueLookup();
return isset($lookup[$value]) ? $lookup[$value]->name : null; if (isset($lookup[$value])) {
return $lookup[$value]->name;
} }
/** return Utils::undefined();
* @param string $value
* @return bool
*/
public function isValidValue($value)
{
return is_string($value) && $this->getNameLookup()->offsetExists($value);
}
/**
* @param $valueNode
* @return bool
*/
public function isValidLiteral($valueNode)
{
return $valueNode instanceof EnumValueNode && $this->getNameLookup()->offsetExists($valueNode->value);
} }
/** /**
@ -136,14 +122,15 @@ class EnumType extends Type implements InputType, OutputType, LeafType
public function parseValue($value) public function parseValue($value)
{ {
$lookup = $this->getNameLookup(); $lookup = $this->getNameLookup();
return isset($lookup[$value]) ? $lookup[$value]->value : null; return isset($lookup[$value]) ? $lookup[$value]->value : Utils::undefined();
} }
/** /**
* @param $value * @param $value
* @param array|null $variables
* @return null * @return null
*/ */
public function parseLiteral($value) public function parseLiteral($value, array $variables = null)
{ {
if ($value instanceof EnumValueNode) { if ($value instanceof EnumValueNode) {
$lookup = $this->getNameLookup(); $lookup = $this->getNameLookup();
@ -201,24 +188,7 @@ class EnumType extends Type implements InputType, OutputType, LeafType
); );
$values = $this->getValues(); $values = $this->getValues();
Utils::invariant(
!empty($values),
"{$this->name} values must be not empty."
);
foreach ($values as $value) { foreach ($values as $value) {
try {
Utils::assertValidName($value->name);
} catch (InvariantViolation $e) {
throw new InvariantViolation(
"{$this->name} has value with invalid name: " .
Utils::printSafe($value->name) . " ({$e->getMessage()})"
);
}
Utils::invariant(
!in_array($value->name, ['true', 'false', 'null']),
"{$this->name}: \"{$value->name}\" can not be used as an Enum value."
);
Utils::invariant( Utils::invariant(
!isset($value->config['isDeprecated']), !isset($value->config['isDeprecated']),
"{$this->name}.{$value->name} should provide \"deprecationReason\" instead of \"isDeprecated\"." "{$this->name}.{$value->name} should provide \"deprecationReason\" instead of \"isDeprecated\"."

View File

@ -2,7 +2,6 @@
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\FloatValueNode; use GraphQL\Language\AST\FloatValueNode;
use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\IntValueNode;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
@ -29,39 +28,50 @@ values as specified by
/** /**
* @param mixed $value * @param mixed $value
* @return float|null * @return float|null
* @throws Error
*/ */
public function serialize($value) public function serialize($value)
{ {
if (is_numeric($value) || $value === true || $value === false) { return $this->coerceFloat($value);
return (float) $value;
}
if ($value === '') {
$err = 'Float cannot represent non numeric value: (empty string)';
} else {
$err = sprintf('Float cannot represent non numeric value: %s', Utils::printSafe($value));
}
throw new InvariantViolation($err);
} }
/** /**
* @param mixed $value * @param mixed $value
* @return float|null * @return float|null
* @throws Error
*/ */
public function parseValue($value) public function parseValue($value)
{ {
return (is_numeric($value) && !is_string($value)) ? (float) $value : null; return $this->coerceFloat($value);
} }
/** /**
* @param $ast * @param $valueNode
* @param array|null $variables
* @return float|null * @return float|null
*/ */
public function parseLiteral($ast) public function parseLiteral($valueNode, array $variables = null)
{ {
if ($ast instanceof FloatValueNode || $ast instanceof IntValueNode) { if ($valueNode instanceof FloatValueNode || $valueNode instanceof IntValueNode) {
return (float) $ast->value; return (float) $valueNode->value;
} }
return null; return Utils::undefined();
}
private function coerceFloat($value) {
if ($value === '') {
throw new Error(
'Float cannot represent non numeric value: (empty string)'
);
}
if (!is_numeric($value) && $value !== true && $value !== false) {
throw new Error(
'Float cannot represent non numeric value: ' .
Utils::printSafe($value)
);
}
return (float) $value;
} }
} }

View File

@ -2,7 +2,6 @@
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\IntValueNode;
use GraphQL\Language\AST\StringValueNode; use GraphQL\Language\AST\StringValueNode;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
@ -44,7 +43,7 @@ When expected as an input type, any string (such as `"4"`) or integer
return 'null'; return 'null';
} }
if (!is_scalar($value) && (!is_object($value) || !method_exists($value, '__toString'))) { if (!is_scalar($value) && (!is_object($value) || !method_exists($value, '__toString'))) {
throw new InvariantViolation("ID type cannot represent non scalar value: " . Utils::printSafe($value)); throw new Error("ID type cannot represent non scalar value: " . Utils::printSafe($value));
} }
return (string) $value; return (string) $value;
} }
@ -55,18 +54,19 @@ When expected as an input type, any string (such as `"4"`) or integer
*/ */
public function parseValue($value) public function parseValue($value)
{ {
return (is_string($value) || is_int($value)) ? (string) $value : null; return (is_string($value) || is_int($value)) ? (string) $value : Utils::undefined();
} }
/** /**
* @param $ast * @param $ast
* @param array|null $variables
* @return null|string * @return null|string
*/ */
public function parseLiteral($ast) public function parseLiteral($valueNode, array $variables = null)
{ {
if ($ast instanceof StringValueNode || $ast instanceof IntValueNode) { if ($valueNode instanceof StringValueNode || $valueNode instanceof IntValueNode) {
return $ast->value; return $valueNode->value;
} }
return null; return Utils::undefined();
} }
} }

View File

@ -9,7 +9,7 @@ use GraphQL\Utils\Utils;
* Class InputObjectType * Class InputObjectType
* @package GraphQL\Type\Definition * @package GraphQL\Type\Definition
*/ */
class InputObjectType extends Type implements InputType class InputObjectType extends Type implements InputType, NamedType
{ {
/** /**
* @var InputObjectField[] * @var InputObjectField[]
@ -31,7 +31,7 @@ class InputObjectType extends Type implements InputType
$config['name'] = $this->tryInferName(); $config['name'] = $this->tryInferName();
} }
Utils::assertValidName($config['name']); Utils::invariant(is_string($config['name']), 'Must provide name.');
Config::validate($config, [ Config::validate($config, [
'name' => Config::NAME | Config::REQUIRED, 'name' => Config::NAME | Config::REQUIRED,
@ -91,41 +91,4 @@ class InputObjectType extends Type implements InputType
Utils::invariant(isset($this->fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name); Utils::invariant(isset($this->fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name);
return $this->fields[$name]; return $this->fields[$name];
} }
/**
* @throws InvariantViolation
*/
public function assertValid()
{
parent::assertValid();
$fields = $this->getFields();
Utils::invariant(
!empty($fields),
"{$this->name} fields must not be empty"
);
foreach ($fields as $field) {
try {
Utils::assertValidName($field->name);
} catch (InvariantViolation $e) {
throw new InvariantViolation("{$this->name}.{$field->name}: {$e->getMessage()}");
}
$fieldType = $field->type;
if ($fieldType instanceof WrappingType) {
$fieldType = $fieldType->getWrappedType(true);
}
Utils::invariant(
$fieldType instanceof InputType,
"{$this->name}.{$field->name} field type must be Input Type but got: %s.",
Utils::printSafe($field->type)
);
Utils::invariant(
!isset($field->config['resolve']),
"{$this->name}.{$field->name} field type has a resolve property, but Input Types cannot define resolvers."
);
}
}
} }

View File

@ -3,11 +3,16 @@ namespace GraphQL\Type\Definition;
/* /*
export type GraphQLInputType = export type GraphQLInputType =
GraphQLScalarType | | GraphQLScalarType
GraphQLEnumType | | GraphQLEnumType
GraphQLInputObjectType | | GraphQLInputObjectType
GraphQLList | | GraphQLList<GraphQLInputType>
GraphQLNonNull; | GraphQLNonNull<
| GraphQLScalarType
| GraphQLEnumType
| GraphQLInputObjectType
| GraphQLList<GraphQLInputType>,
>;
*/ */
interface InputType interface InputType
{ {

View File

@ -2,7 +2,6 @@
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\IntValueNode;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
@ -35,63 +34,60 @@ values. Int can represent values between -(2^31) and 2^31 - 1. ';
/** /**
* @param mixed $value * @param mixed $value
* @return int|null * @return int|null
* @throws Error
*/ */
public function serialize($value) public function serialize($value)
{ {
if ($value === '') { return $this->coerceInt($value);
throw new InvariantViolation('Int cannot represent non 32-bit signed integer value: (empty string)');
}
if (false === $value || true === $value) {
return (int) $value;
}
if (!is_numeric($value) || $value > self::MAX_INT || $value < self::MIN_INT) {
throw new InvariantViolation(sprintf(
'Int cannot represent non 32-bit signed integer value: %s',
Utils::printSafe($value)
));
}
$num = (float) $value;
// The GraphQL specification does not allow serializing non-integer values
// as Int to avoid accidental data loss.
// Examples: 1.0 == 1; 1.1 != 1, etc
if ($num != (int) $value) {
// Additionally account for scientific notation (i.e. 1e3), because (float)'1e3' is 1000, but (int)'1e3' is 1
$trimmed = floor($num);
if ($trimmed !== $num) {
throw new InvariantViolation(sprintf(
'Int cannot represent non-integer value: %s',
Utils::printSafe($value)
));
}
}
return (int) $value;
} }
/** /**
* @param mixed $value * @param mixed $value
* @return int|null * @return int|null
* @throws Error
*/ */
public function parseValue($value) public function parseValue($value)
{ {
// Below is a fix against PHP bug where (in some combinations of OSs and versions) return $this->coerceInt($value);
// boundary values are treated as "double" vs "integer" and failing is_int() check
$isInt = is_int($value) || $value === self::MIN_INT || $value === self::MAX_INT;
return $isInt && $value <= self::MAX_INT && $value >= self::MIN_INT ? $value : null;
} }
/** /**
* @param $ast * @param $valueNode
* @param array|null $variables
* @return int|null * @return int|null
*/ */
public function parseLiteral($ast) public function parseLiteral($valueNode, array $variables = null)
{ {
if ($ast instanceof IntValueNode) { if ($valueNode instanceof IntValueNode) {
$val = (int) $ast->value; $val = (int) $valueNode->value;
if ($ast->value === (string) $val && self::MIN_INT <= $val && $val <= self::MAX_INT) { if ($valueNode->value === (string) $val && self::MIN_INT <= $val && $val <= self::MAX_INT) {
return $val; return $val;
} }
} }
return null; return Utils::undefined();
}
private function coerceInt($value) {
if ($value === '') {
throw new Error(
'Int cannot represent non 32-bit signed integer value: (empty string)'
);
}
$num = floatval($value);
if (!is_numeric($value) && !is_bool($value) || $num > self::MAX_INT || $num < self::MIN_INT) {
throw new Error(
'Int cannot represent non 32-bit signed integer value: ' .
Utils::printSafe($value)
);
}
$int = intval($num);
if ($int != $num) {
throw new Error(
'Int cannot represent non-integer value: ' .
Utils::printSafe($value)
);
}
return $int;
} }
} }

View File

@ -3,14 +3,29 @@ namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode; use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
/** /**
* Class InterfaceType * Class InterfaceType
* @package GraphQL\Type\Definition * @package GraphQL\Type\Definition
*/ */
class InterfaceType extends Type implements AbstractType, OutputType, CompositeType class InterfaceType extends Type implements AbstractType, OutputType, CompositeType, NamedType
{ {
/**
* @param mixed $type
* @return self
*/
public static function assertInterfaceType($type)
{
Utils::invariant(
$type instanceof self,
'Expected ' . Utils::printSafe($type) . ' to be a GraphQL Interface type.'
);
return $type;
}
/** /**
* @var FieldDefinition[] * @var FieldDefinition[]
*/ */
@ -21,6 +36,11 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
*/ */
public $astNode; public $astNode;
/**
* @var InterfaceTypeExtensionNode[]
*/
public $extensionASTNodes;
/** /**
* InterfaceType constructor. * InterfaceType constructor.
* @param array $config * @param array $config
@ -31,7 +51,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
$config['name'] = $this->tryInferName(); $config['name'] = $this->tryInferName();
} }
Utils::assertValidName($config['name']); Utils::invariant(is_string($config['name']), 'Must provide name.');
Config::validate($config, [ Config::validate($config, [
'name' => Config::NAME, 'name' => Config::NAME,
@ -46,6 +66,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
$this->name = $config['name']; $this->name = $config['name'];
$this->description = isset($config['description']) ? $config['description'] : null; $this->description = isset($config['description']) ? $config['description'] : null;
$this->astNode = isset($config['astNode']) ? $config['astNode'] : null; $this->astNode = isset($config['astNode']) ? $config['astNode'] : null;
$this->extensionASTNodes = isset($config['extensionASTNodes']) ? $config['extensionASTNodes'] : null;
$this->config = $config; $this->config = $config;
} }
@ -99,23 +120,9 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
{ {
parent::assertValid(); parent::assertValid();
$fields = $this->getFields();
Utils::invariant( Utils::invariant(
!isset($this->config['resolveType']) || is_callable($this->config['resolveType']), !isset($this->config['resolveType']) || is_callable($this->config['resolveType']),
"{$this->name} must provide \"resolveType\" as a function." "{$this->name} must provide \"resolveType\" as a function."
); );
Utils::invariant(
!empty($fields),
"{$this->name} fields must not be empty"
);
foreach ($fields as $field) {
$field->assertValid($this);
foreach ($field->args as $arg) {
$arg->assertValid($field, $this);
}
}
} }
} }

View File

@ -1,6 +1,8 @@
<?php <?php
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use \GraphQL\Language\AST\Node;
/* /*
export type GraphQLLeafType = export type GraphQLLeafType =
GraphQLScalarType | GraphQLScalarType |
@ -19,6 +21,8 @@ interface LeafType
/** /**
* Parses an externally provided value (query variable) to use as an input * Parses an externally provided value (query variable) to use as an input
* *
* In the case of an invalid value this method must return Utils::undefined()
*
* @param mixed $value * @param mixed $value
* @return mixed * @return mixed
*/ */
@ -27,20 +31,11 @@ interface LeafType
/** /**
* Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input * Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input
* *
* @param \GraphQL\Language\AST\Node $valueNode * In the case of an invalid value this method must return Utils::undefined()
*
* @param Node $valueNode
* @param array|null $variables
* @return mixed * @return mixed
*/ */
public function parseLiteral($valueNode); public function parseLiteral($valueNode, array $variables = null);
/**
* @param string $value
* @return bool
*/
public function isValidValue($value);
/**
* @param \GraphQL\Language\AST\Node $valueNode
* @return mixed
*/
public function isValidLiteral($valueNode);
} }

View File

@ -20,12 +20,7 @@ class ListOfType extends Type implements WrappingType, OutputType, InputType
*/ */
public function __construct($type) public function __construct($type)
{ {
if (!$type instanceof Type && !is_callable($type)) { $this->ofType = Type::assertType($type);
throw new InvariantViolation(
'Can only create List of a GraphQLType but got: ' . Utils::printSafe($type)
);
}
$this->ofType = $type;
} }
/** /**

View File

@ -0,0 +1,15 @@
<?php
namespace GraphQL\Type\Definition;
/*
export type GraphQLNamedType =
| GraphQLScalarType
| GraphQLObjectType
| GraphQLInterfaceType
| GraphQLUnionType
| GraphQLEnumType
| GraphQLInputObjectType;
*/
interface NamedType
{
}

View File

@ -11,7 +11,35 @@ use GraphQL\Utils\Utils;
class NonNull extends Type implements WrappingType, OutputType, InputType class NonNull extends Type implements WrappingType, OutputType, InputType
{ {
/** /**
* @var ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType * @param mixed $type
* @return self
*/
public static function assertNullType($type)
{
Utils::invariant(
$type instanceof self,
'Expected ' . Utils::printSafe($type) . ' to be a GraphQL Non-Null type.'
);
return $type;
}
/**
* @param mixed $type
* @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType
*/
public static function assertNullableType($type)
{
Utils::invariant(
Type::isType($type) && !$type instanceof self,
'Expected ' . Utils::printSafe($type) . ' to be a GraphQL nullable type.'
);
return $type;
}
/**
* @var ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType
*/ */
private $ofType; private $ofType;
@ -21,37 +49,17 @@ class NonNull extends Type implements WrappingType, OutputType, InputType
*/ */
public function __construct($type) public function __construct($type)
{ {
if (!$type instanceof Type && !is_callable($type)) { $this->ofType = self::assertNullableType($type);
throw new InvariantViolation(
'Can only create NonNull of a Nullable GraphQLType but got: ' . Utils::printSafe($type)
);
}
if ($type instanceof NonNull) {
throw new InvariantViolation(
'Can only create NonNull of a Nullable GraphQLType but got: ' . Utils::printSafe($type)
);
}
Utils::invariant(
!($type instanceof NonNull),
'Cannot nest NonNull inside NonNull'
);
$this->ofType = $type;
} }
/** /**
* @param bool $recurse * @param bool $recurse
* @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType
* @throws InvariantViolation * @throws InvariantViolation
*/ */
public function getWrappedType($recurse = false) public function getWrappedType($recurse = false)
{ {
$type = $this->ofType; $type = $this->ofType;
Utils::invariant(
!($type instanceof NonNull),
'Cannot nest NonNull inside NonNull'
);
return ($recurse && $type instanceof WrappingType) ? $type->getWrappedType($recurse) : $type; return ($recurse && $type instanceof WrappingType) ? $type->getWrappedType($recurse) : $type;
} }

View File

@ -3,7 +3,7 @@ namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\ObjectTypeDefinitionNode; use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\AST\TypeExtensionDefinitionNode; use GraphQL\Language\AST\ObjectTypeExtensionNode;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
@ -47,8 +47,22 @@ use GraphQL\Utils\Utils;
* ]); * ]);
* *
*/ */
class ObjectType extends Type implements OutputType, CompositeType class ObjectType extends Type implements OutputType, CompositeType, NamedType
{ {
/**
* @param mixed $type
* @return self
*/
public static function assertObjectType($type)
{
Utils::invariant(
$type instanceof self,
'Expected ' . Utils::printSafe($type) . ' to be a GraphQL Object type.'
);
return $type;
}
/** /**
* @var FieldDefinition[] * @var FieldDefinition[]
*/ */
@ -70,7 +84,7 @@ class ObjectType extends Type implements OutputType, CompositeType
public $astNode; public $astNode;
/** /**
* @var TypeExtensionDefinitionNode[] * @var ObjectTypeExtensionNode[]
*/ */
public $extensionASTNodes; public $extensionASTNodes;
@ -89,7 +103,7 @@ class ObjectType extends Type implements OutputType, CompositeType
$config['name'] = $this->tryInferName(); $config['name'] = $this->tryInferName();
} }
Utils::assertValidName($config['name'], !empty($config['isIntrospection'])); Utils::invariant(is_string($config['name']), 'Must provide name.');
// Note: this validation is disabled by default, because it is resource-consuming // Note: this validation is disabled by default, because it is resource-consuming
// TODO: add bin/validate script to check if schema is valid during development // TODO: add bin/validate script to check if schema is valid during development
@ -152,13 +166,13 @@ class ObjectType extends Type implements OutputType, CompositeType
$interfaces = isset($this->config['interfaces']) ? $this->config['interfaces'] : []; $interfaces = isset($this->config['interfaces']) ? $this->config['interfaces'] : [];
$interfaces = is_callable($interfaces) ? call_user_func($interfaces) : $interfaces; $interfaces = is_callable($interfaces) ? call_user_func($interfaces) : $interfaces;
if (!is_array($interfaces)) { if ($interfaces && !is_array($interfaces)) {
throw new InvariantViolation( throw new InvariantViolation(
"{$this->name} interfaces must be an Array or a callable which returns an Array." "{$this->name} interfaces must be an Array or a callable which returns an Array."
); );
} }
$this->interfaces = $interfaces; $this->interfaces = $interfaces ?: [];
} }
return $this->interfaces; return $this->interfaces;
} }
@ -214,41 +228,5 @@ class ObjectType extends Type implements OutputType, CompositeType
!isset($this->config['isTypeOf']) || is_callable($this->config['isTypeOf']), !isset($this->config['isTypeOf']) || is_callable($this->config['isTypeOf']),
"{$this->name} must provide 'isTypeOf' as a function" "{$this->name} must provide 'isTypeOf' as a function"
); );
// getFields() and getInterfaceMap() will do structural validation
$fields = $this->getFields();
Utils::invariant(
!empty($fields),
"{$this->name} fields must not be empty"
);
foreach ($fields as $field) {
$field->assertValid($this);
foreach ($field->args as $arg) {
$arg->assertValid($field, $this);
}
}
$implemented = [];
foreach ($this->getInterfaces() as $iface) {
Utils::invariant(
$iface instanceof InterfaceType,
"{$this->name} may only implement Interface types, it cannot implement %s.",
Utils::printSafe($iface)
);
Utils::invariant(
!isset($implemented[$iface->name]),
"{$this->name} may declare it implements {$iface->name} only once."
);
$implemented[$iface->name] = true;
if (!isset($iface->config['resolveType'])) {
Utils::invariant(
isset($this->config['isTypeOf']),
"Interface Type {$iface->name} does not provide a \"resolveType\" " .
"function and implementing Type {$this->name} does not provide a " .
'"isTypeOf" function. There is no way to resolve this implementing ' .
'type during execution.'
);
}
}
} }
} }

View File

@ -22,7 +22,7 @@ use GraphQL\Utils\Utils;
* } * }
* } * }
*/ */
abstract class ScalarType extends Type implements OutputType, InputType, LeafType abstract class ScalarType extends Type implements OutputType, InputType, LeafType, NamedType
{ {
/** /**
* @var ScalarTypeDefinitionNode|null * @var ScalarTypeDefinitionNode|null
@ -36,30 +36,6 @@ abstract class ScalarType extends Type implements OutputType, InputType, LeafTyp
$this->astNode = isset($config['astNode']) ? $config['astNode'] : null; $this->astNode = isset($config['astNode']) ? $config['astNode'] : null;
$this->config = $config; $this->config = $config;
Utils::assertValidName($this->name); Utils::invariant(is_string($this->name), 'Must provide name.');
}
/**
* Determines if an internal value is valid for this type.
* Equivalent to checking for if the parsedValue is nullish.
*
* @param $value
* @return bool
*/
public function isValidValue($value)
{
return null !== $this->parseValue($value);
}
/**
* Determines if an internal value is valid for this type.
* Equivalent to checking for if the parsedLiteral is nullish.
*
* @param $valueNode
* @return bool
*/
public function isValidLiteral($valueNode)
{
return null !== $this->parseLiteral($valueNode);
} }
} }

View File

@ -2,7 +2,6 @@
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\StringValueNode; use GraphQL\Language\AST\StringValueNode;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
@ -28,6 +27,7 @@ represent free-form human-readable text.';
/** /**
* @param mixed $value * @param mixed $value
* @return mixed|string * @return mixed|string
* @throws Error
*/ */
public function serialize($value) public function serialize($value)
{ {
@ -41,29 +41,42 @@ represent free-form human-readable text.';
return 'null'; return 'null';
} }
if (!is_scalar($value)) { if (!is_scalar($value)) {
throw new InvariantViolation("String cannot represent non scalar value: " . Utils::printSafe($value)); throw new Error("String cannot represent non scalar value: " . Utils::printSafe($value));
} }
return (string) $value; return $this->coerceString($value);
} }
/** /**
* @param mixed $value * @param mixed $value
* @return string * @return string
* @throws Error
*/ */
public function parseValue($value) public function parseValue($value)
{ {
return is_string($value) ? $value : null; return $this->coerceString($value);
} }
/** /**
* @param $ast * @param $valueNode
* @param array|null $variables
* @return null|string * @return null|string
*/ */
public function parseLiteral($ast) public function parseLiteral($valueNode, array $variables = null)
{ {
if ($ast instanceof StringValueNode) { if ($valueNode instanceof StringValueNode) {
return $ast->value; return $valueNode->value;
} }
return null; return Utils::undefined();
}
private function coerceString($value) {
if (is_array($value)) {
throw new Error(
'String cannot represent an array value: ' .
Utils::printSafe($value)
);
}
return (string) $value;
} }
} }

View File

@ -2,7 +2,12 @@
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\ListType;
use GraphQL\Language\AST\NamedType;
use GraphQL\Language\AST\NonNullType;
use GraphQL\Language\AST\TypeDefinitionNode; use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Type\Introspection;
use GraphQL\Utils\Utils;
/** /**
* Registry of standard GraphQL types * Registry of standard GraphQL types
@ -23,6 +28,11 @@ abstract class Type implements \JsonSerializable
*/ */
private static $internalTypes; private static $internalTypes;
/**
* @var array
*/
private static $builtInTypes;
/** /**
* @api * @api
* @return IDType * @return IDType
@ -70,7 +80,7 @@ abstract class Type implements \JsonSerializable
/** /**
* @api * @api
* @param ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType * @param Type|ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType
* @return ListOfType * @return ListOfType
*/ */
public static function listOf($wrappedType) public static function listOf($wrappedType)
@ -107,6 +117,8 @@ abstract class Type implements \JsonSerializable
} }
/** /**
* Returns all builtin scalar types
*
* @return Type[] * @return Type[]
*/ */
public static function getInternalTypes() public static function getInternalTypes()
@ -114,6 +126,34 @@ abstract class Type implements \JsonSerializable
return self::getInternalType(); return self::getInternalType();
} }
/**
* Returns all builtin in types including base scalar and
* introspection types
*
* @return Type[]
*/
public static function getAllBuiltInTypes()
{
if (null === self::$builtInTypes) {
self::$builtInTypes = array_merge(
Introspection::getTypes(),
self::getInternalTypes()
);
}
return self::$builtInTypes;
}
/**
* Checks if the type is a builtin type
*
* @param Type $type
* @return bool
*/
public static function isBuiltInType(Type $type)
{
return in_array($type->name, array_keys(self::getAllBuiltInTypes()));
}
/** /**
* @api * @api
* @param Type $type * @param Type $type
@ -121,8 +161,11 @@ abstract class Type implements \JsonSerializable
*/ */
public static function isInputType($type) public static function isInputType($type)
{ {
$nakedType = self::getNamedType($type); return $type instanceof InputType &&
return $nakedType instanceof InputType; (
!$type instanceof WrappingType ||
self::getNamedType($type) instanceof InputType
);
} }
/** /**
@ -132,8 +175,11 @@ abstract class Type implements \JsonSerializable
*/ */
public static function isOutputType($type) public static function isOutputType($type)
{ {
$nakedType = self::getNamedType($type); return $type instanceof OutputType &&
return $nakedType instanceof OutputType; (
!$type instanceof WrappingType ||
self::getNamedType($type) instanceof OutputType
);
} }
/** /**
@ -166,6 +212,39 @@ abstract class Type implements \JsonSerializable
return $type instanceof AbstractType; return $type instanceof AbstractType;
} }
/**
* @api
* @param Type $type
* @return bool
*/
public static function isType($type)
{
return (
$type instanceof ScalarType ||
$type instanceof ObjectType ||
$type instanceof InterfaceType ||
$type instanceof UnionType ||
$type instanceof EnumType ||
$type instanceof InputObjectType ||
$type instanceof ListOfType ||
$type instanceof NonNull
);
}
/**
* @param mixed $type
* @return mixed
*/
public static function assertType($type)
{
Utils::invariant(
self::isType($type),
'Expected ' . Utils::printSafe($type) . ' to be a GraphQL type.'
);
return $type;
}
/** /**
* @api * @api
* @param Type $type * @param Type $type
@ -238,6 +317,7 @@ abstract class Type implements \JsonSerializable
*/ */
public function assertValid() public function assertValid()
{ {
Utils::assertValidName($this->name);
} }
/** /**

View File

@ -9,7 +9,7 @@ use GraphQL\Utils\Utils;
* Class UnionType * Class UnionType
* @package GraphQL\Type\Definition * @package GraphQL\Type\Definition
*/ */
class UnionType extends Type implements AbstractType, OutputType, CompositeType class UnionType extends Type implements AbstractType, OutputType, CompositeType, NamedType
{ {
/** /**
* @var UnionTypeDefinitionNode * @var UnionTypeDefinitionNode
@ -36,7 +36,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
$config['name'] = $this->tryInferName(); $config['name'] = $this->tryInferName();
} }
Utils::assertValidName($config['name']); Utils::invariant(is_string($config['name']), 'Must provide name.');
Config::validate($config, [ Config::validate($config, [
'name' => Config::NAME | Config::REQUIRED, 'name' => Config::NAME | Config::REQUIRED,
@ -81,7 +81,8 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
if (!is_array($types)) { if (!is_array($types)) {
throw new InvariantViolation( throw new InvariantViolation(
"{$this->name} types must be an Array or a callable which returns an Array." "Must provide Array of types or a callable which returns " .
"such an array for Union {$this->name}"
); );
} }
@ -133,40 +134,11 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
{ {
parent::assertValid(); parent::assertValid();
$types = $this->getTypes();
Utils::invariant(
!empty($types),
"{$this->name} types must not be empty"
);
if (isset($this->config['resolveType'])) { if (isset($this->config['resolveType'])) {
Utils::invariant( Utils::invariant(
is_callable($this->config['resolveType']), is_callable($this->config['resolveType']),
"{$this->name} must provide \"resolveType\" as a function." "{$this->name} must provide \"resolveType\" as a function."
); );
} }
$includedTypeNames = [];
foreach ($types as $objType) {
Utils::invariant(
$objType instanceof ObjectType,
"{$this->name} may only contain Object types, it cannot contain: %s.",
Utils::printSafe($objType)
);
Utils::invariant(
!isset($includedTypeNames[$objType->name]),
"{$this->name} can include {$objType->name} type only once."
);
$includedTypeNames[$objType->name] = true;
if (!isset($this->config['resolveType'])) {
Utils::invariant(
isset($objType->config['isTypeOf']) && is_callable($objType->config['isTypeOf']),
"Union type \"{$this->name}\" does not provide a \"resolveType\" " .
"function and possible type \"{$objType->name}\" does not provide an " .
'"isTypeOf" function. There is no way to resolve this possible type ' .
'during execution.'
);
}
}
} }
} }

View File

@ -1,10 +1,9 @@
<?php <?php
namespace GraphQL\Type; namespace GraphQL\Type;
use GraphQL\Language\Printer; use GraphQL\Language\Printer;
use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\DirectiveLocation; use GraphQL\Language\DirectiveLocation;
use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\FieldDefinition;
@ -38,11 +37,25 @@ class Introspection
private static $map = []; private static $map = [];
/** /**
* Options:
* - descriptions
* Whether to include descriptions in the introspection result.
* Default: true
*
* @param array $options
* @return string * @return string
*/ */
public static function getIntrospectionQuery($includeDescription = true) public static function getIntrospectionQuery($options = [])
{ {
$withDescription = <<<'EOD' if (is_bool($options)) {
trigger_error('Calling Introspection::getIntrospectionQuery(boolean) is deprecated. Please use Introspection::getIntrospectionQuery(["descriptions" => boolean]).', E_USER_DEPRECATED);
$descriptions = $options;
} else {
$descriptions = !array_key_exists('descriptions', $options) || $options['descriptions'] === true;
}
$descriptionField = $descriptions ? 'description' : '';
return <<<EOD
query IntrospectionQuery { query IntrospectionQuery {
__schema { __schema {
queryType { name } queryType { name }
@ -53,7 +66,7 @@ class Introspection
} }
directives { directives {
name name
description {$descriptionField}
locations locations
args { args {
...InputValue ...InputValue
@ -65,10 +78,10 @@ class Introspection
fragment FullType on __Type { fragment FullType on __Type {
kind kind
name name
description {$descriptionField}
fields(includeDeprecated: true) { fields(includeDeprecated: true) {
name name
description {$descriptionField}
args { args {
...InputValue ...InputValue
} }
@ -86,7 +99,7 @@ class Introspection
} }
enumValues(includeDeprecated: true) { enumValues(includeDeprecated: true) {
name name
description {$descriptionField}
isDeprecated isDeprecated
deprecationReason deprecationReason
} }
@ -97,7 +110,7 @@ class Introspection
fragment InputValue on __InputValue { fragment InputValue on __InputValue {
name name
description {$descriptionField}
type { ...TypeRef } type { ...TypeRef }
defaultValue defaultValue
} }
@ -135,95 +148,6 @@ class Introspection
} }
} }
EOD; EOD;
$withoutDescription = <<<'EOD'
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
fields(includeDeprecated: true) {
name
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
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
}
}
}
}
}
}
}
}
EOD;
return $includeDescription ? $withDescription : $withoutDescription;
} }
public static function getTypes() public static function getTypes()
@ -240,6 +164,15 @@ EOD;
]; ];
} }
/**
* @param Type $type
* @return bool
*/
public static function isIntrospectionType($type)
{
return in_array($type->name, array_keys(self::getTypes()));
}
public static function _schema() public static function _schema()
{ {
if (!isset(self::$map['__Schema'])) { if (!isset(self::$map['__Schema'])) {
@ -593,7 +526,7 @@ EOD;
], ],
'type' => [ 'type' => [
'type' => Type::nonNull(self::_type()), 'type' => Type::nonNull(self::_type()),
'resolve' => function ($field) { 'resolve' => function (FieldDefinition $field) {
return $field->getType(); return $field->getType();
} }
], ],

View File

@ -1,18 +1,16 @@
<?php <?php
namespace GraphQL\Type; namespace GraphQL\Type;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\GraphQL; use GraphQL\GraphQL;
use GraphQL\Language\AST\SchemaDefinitionNode; use GraphQL\Language\AST\SchemaDefinitionNode;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Definition\UnionType;
use GraphQL\Utils\TypeComparators;
use GraphQL\Utils\TypeInfo; use GraphQL\Utils\TypeInfo;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
@ -64,6 +62,11 @@ class Schema
*/ */
private $fullyLoaded = false; private $fullyLoaded = false;
/**
* @var InvariantViolation[]|null
*/
private $validationErrors;
/** /**
* Schema constructor. * Schema constructor.
* *
@ -86,10 +89,18 @@ class Schema
'subscription' => $subscriptionType 'subscription' => $subscriptionType
]; ];
} }
if (is_array($config)) { if (is_array($config)) {
$config = SchemaConfig::create($config); $config = SchemaConfig::create($config);
} }
// If this schema was built from a source known to be valid, then it may be
// marked with assumeValid to avoid an additional type system validation.
if ($config->getAssumeValid()) {
$this->validationErrors = [];
} else {
// Otherwise check for common mistakes during construction to produce
// clear and early error messages.
Utils::invariant( Utils::invariant(
$config instanceof SchemaConfig, $config instanceof SchemaConfig,
'Schema constructor expects instance of GraphQL\Type\SchemaConfig or an array with keys: %s; but got: %s', 'Schema constructor expects instance of GraphQL\Type\SchemaConfig or an array with keys: %s; but got: %s',
@ -103,15 +114,20 @@ class Schema
]), ]),
Utils::getVariableType($config) Utils::getVariableType($config)
); );
Utils::invariant( Utils::invariant(
$config->query instanceof ObjectType, !$config->types || is_array($config->types) || is_callable($config->types),
"Schema query must be Object Type but got: " . Utils::getVariableType($config->query) "\"types\" must be array or callable if provided but got: " . Utils::getVariableType($config->types)
); );
Utils::invariant(
!$config->directives || is_array($config->directives),
"\"directives\" must be Array if provided but got: " . Utils::getVariableType($config->directives)
);
}
$this->config = $config; $this->config = $config;
if ($config->query) {
$this->resolvedTypes[$config->query->name] = $config->query; $this->resolvedTypes[$config->query->name] = $config->query;
}
if ($config->mutation) { if ($config->mutation) {
$this->resolvedTypes[$config->mutation->name] = $config->mutation; $this->resolvedTypes[$config->mutation->name] = $config->mutation;
} }
@ -208,7 +224,11 @@ class Schema
public function getType($name) public function getType($name)
{ {
if (!isset($this->resolvedTypes[$name])) { if (!isset($this->resolvedTypes[$name])) {
$this->resolvedTypes[$name] = $this->loadType($name); $type = $this->loadType($name);
if (!$type) {
return null;
}
$this->resolvedTypes[$name] = $type;
} }
return $this->resolvedTypes[$name]; return $this->resolvedTypes[$name];
} }
@ -393,6 +413,32 @@ class Schema
return isset($typeMap[$typeName]) ? $typeMap[$typeName] : null; return isset($typeMap[$typeName]) ? $typeMap[$typeName] : null;
} }
/**
* Validates schema.
*
* This operation requires full schema scan. Do not use in production environment.
*
* @api
* @return InvariantViolation[]|Error[]
*/
public function validate() {
// If this Schema has already been validated, return the previous results.
if ($this->validationErrors !== null) {
return $this->validationErrors;
}
// Validate the schema, producing a list of errors.
$context = new SchemaValidationContext($this);
$context->validateRootTypes();
$context->validateDirectives();
$context->validateTypes();
// Persist the results of validation before returning to ensure validation
// does not run multiple times for this schema.
$this->validationErrors = $context->getErrors();
return $this->validationErrors;
}
/** /**
* Validates schema. * Validates schema.
* *
@ -403,18 +449,13 @@ class Schema
*/ */
public function assertValid() public function assertValid()
{ {
foreach ($this->config->getDirectives() as $index => $directive) { $errors = $this->validate();
Utils::invariant(
$directive instanceof Directive, if ($errors) {
"Each entry of \"directives\" option of Schema config must be an instance of %s but entry at position %d is %s.", throw new InvariantViolation(implode("\n\n", $this->validationErrors));
Directive::class,
$index,
Utils::printSafe($directive)
);
} }
$internalTypes = Type::getInternalTypes() + Introspection::getTypes(); $internalTypes = Type::getInternalTypes() + Introspection::getTypes();
foreach ($this->getTypeMap() as $name => $type) { foreach ($this->getTypeMap() as $name => $type) {
if (isset($internalTypes[$name])) { if (isset($internalTypes[$name])) {
continue ; continue ;
@ -422,22 +463,6 @@ class Schema
$type->assertValid(); $type->assertValid();
if ($type instanceof AbstractType) {
$possibleTypes = $this->getPossibleTypes($type);
Utils::invariant(
!empty($possibleTypes),
"Could not find possible implementing types for {$type->name} " .
'in schema. Check that schema.types is defined and is an array of ' .
'all possible types in the schema.'
);
} else if ($type instanceof ObjectType) {
foreach ($type->getInterfaces() as $iface) {
$this->assertImplementsIntarface($type, $iface);
}
}
// Make sure type loader returns the same instance as registered in other places of schema // Make sure type loader returns the same instance as registered in other places of schema
if ($this->config->typeLoader) { if ($this->config->typeLoader) {
Utils::invariant( Utils::invariant(
@ -448,74 +473,4 @@ class Schema
} }
} }
} }
private function assertImplementsIntarface(ObjectType $object, InterfaceType $iface)
{
$objectFieldMap = $object->getFields();
$ifaceFieldMap = $iface->getFields();
// Assert each interface field is implemented.
foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
// Assert interface field exists on object.
Utils::invariant(
isset($objectFieldMap[$fieldName]),
"{$iface->name} expects field \"{$fieldName}\" but {$object->name} does not provide it"
);
$objectField = $objectFieldMap[$fieldName];
// Assert interface field type is satisfied by object field type, by being
// a valid subtype. (covariant)
Utils::invariant(
TypeComparators::isTypeSubTypeOf($this, $objectField->getType(), $ifaceField->getType()),
"{$iface->name}.{$fieldName} expects type \"{$ifaceField->getType()}\" " .
"but " .
"{$object->name}.${fieldName} provides type \"{$objectField->getType()}\""
);
// Assert each interface field arg is implemented.
foreach ($ifaceField->args as $ifaceArg) {
$argName = $ifaceArg->name;
/** @var FieldArgument $objectArg */
$objectArg = Utils::find($objectField->args, function(FieldArgument $arg) use ($argName) {
return $arg->name === $argName;
});
// Assert interface field arg exists on object field.
Utils::invariant(
$objectArg,
"{$iface->name}.{$fieldName} expects argument \"{$argName}\" but ".
"{$object->name}.{$fieldName} does not provide it."
);
// Assert interface field arg type matches object field arg type.
// (invariant)
Utils::invariant(
TypeComparators::isEqualType($ifaceArg->getType(), $objectArg->getType()),
"{$iface->name}.{$fieldName}({$argName}:) expects type " .
"\"{$ifaceArg->getType()->name}\" but " .
"{$object->name}.{$fieldName}({$argName}:) provides type " .
"\"{$objectArg->getType()->name}\"."
);
// Assert additional arguments must not be required.
foreach ($objectField->args as $objectArg) {
$argName = $objectArg->name;
$ifaceArg = Utils::find($ifaceField->args, function(FieldArgument $arg) use ($argName) {
return $arg->name === $argName;
});
if (!$ifaceArg) {
Utils::invariant(
!($objectArg->getType() instanceof NonNull),
"{$object->name}.{$fieldName}({$argName}:) is of required type " .
"\"{$objectArg->getType()}\" but is not also provided by the " .
"interface {$iface->name}.{$fieldName}."
);
}
}
}
}
}
} }

View File

@ -58,6 +58,11 @@ class SchemaConfig
*/ */
public $astNode; public $astNode;
/**
* @var bool
*/
public $assumeValid;
/** /**
* Converts an array of options to instance of SchemaConfig * Converts an array of options to instance of SchemaConfig
* (or just returns empty config when array is not passed). * (or just returns empty config when array is not passed).
@ -72,47 +77,22 @@ class SchemaConfig
if (!empty($options)) { if (!empty($options)) {
if (isset($options['query'])) { if (isset($options['query'])) {
Utils::invariant(
$options['query'] instanceof ObjectType,
'Schema query must be Object Type if provided but got: %s',
Utils::printSafe($options['query'])
);
$config->setQuery($options['query']); $config->setQuery($options['query']);
} }
if (isset($options['mutation'])) { if (isset($options['mutation'])) {
Utils::invariant(
$options['mutation'] instanceof ObjectType,
'Schema mutation must be Object Type if provided but got: %s',
Utils::printSafe($options['mutation'])
);
$config->setMutation($options['mutation']); $config->setMutation($options['mutation']);
} }
if (isset($options['subscription'])) { if (isset($options['subscription'])) {
Utils::invariant(
$options['subscription'] instanceof ObjectType,
'Schema subscription must be Object Type if provided but got: %s',
Utils::printSafe($options['subscription'])
);
$config->setSubscription($options['subscription']); $config->setSubscription($options['subscription']);
} }
if (isset($options['types'])) { if (isset($options['types'])) {
Utils::invariant(
is_array($options['types']) || is_callable($options['types']),
'Schema types must be array or callable if provided but got: %s',
Utils::printSafe($options['types'])
);
$config->setTypes($options['types']); $config->setTypes($options['types']);
} }
if (isset($options['directives'])) { if (isset($options['directives'])) {
Utils::invariant(
is_array($options['directives']),
'Schema directives must be array if provided but got: %s',
Utils::printSafe($options['directives'])
);
$config->setDirectives($options['directives']); $config->setDirectives($options['directives']);
} }
@ -140,13 +120,12 @@ class SchemaConfig
} }
if (isset($options['astNode'])) { if (isset($options['astNode'])) {
Utils::invariant(
$options['astNode'] instanceof SchemaDefinitionNode,
'Schema astNode must be an instance of SchemaDefinitionNode but got: %s',
Utils::printSafe($options['typeLoader'])
);
$config->setAstNode($options['astNode']); $config->setAstNode($options['astNode']);
} }
if (isset($options['assumeValid'])) {
$config->setAssumeValid((bool) $options['assumeValid']);
}
} }
return $config; return $config;
@ -175,7 +154,7 @@ class SchemaConfig
* @param ObjectType $query * @param ObjectType $query
* @return SchemaConfig * @return SchemaConfig
*/ */
public function setQuery(ObjectType $query) public function setQuery($query)
{ {
$this->query = $query; $this->query = $query;
return $this; return $this;
@ -186,7 +165,7 @@ class SchemaConfig
* @param ObjectType $mutation * @param ObjectType $mutation
* @return SchemaConfig * @return SchemaConfig
*/ */
public function setMutation(ObjectType $mutation) public function setMutation($mutation)
{ {
$this->mutation = $mutation; $this->mutation = $mutation;
return $this; return $this;
@ -197,7 +176,7 @@ class SchemaConfig
* @param ObjectType $subscription * @param ObjectType $subscription
* @return SchemaConfig * @return SchemaConfig
*/ */
public function setSubscription(ObjectType $subscription) public function setSubscription($subscription)
{ {
$this->subscription = $subscription; $this->subscription = $subscription;
return $this; return $this;
@ -236,6 +215,16 @@ class SchemaConfig
return $this; return $this;
} }
/**
* @param bool $assumeValid
* @return SchemaConfig
*/
public function setAssumeValid($assumeValid)
{
$this->assumeValid = $assumeValid;
return $this;
}
/** /**
* @api * @api
* @return ObjectType * @return ObjectType
@ -289,4 +278,12 @@ class SchemaConfig
{ {
return $this->typeLoader; return $this->typeLoader;
} }
/**
* @return bool
*/
public function getAssumeValid()
{
return $this->assumeValid;
}
} }

View File

@ -0,0 +1,728 @@
<?php
namespace GraphQL\Type;
use GraphQL\Error\Error;
use GraphQL\Language\AST\EnumValueDefinitionNode;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\AST\ObjectTypeExtensionNode;
use GraphQL\Language\AST\SchemaDefinitionNode;
use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Language\AST\TypeNode;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\EnumValueDefinition;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputObjectField;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\NamedType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Utils\TypeComparators;
use GraphQL\Utils\Utils;
class SchemaValidationContext
{
/**
* @var Error[]
*/
private $errors = [];
/**
* @var Schema
*/
private $schema;
public function __construct(Schema $schema)
{
$this->schema = $schema;
}
/**
* @return Error[]
*/
public function getErrors() {
return $this->errors;
}
public function validateRootTypes() {
$queryType = $this->schema->getQueryType();
if (!$queryType) {
$this->reportError(
'Query root type must be provided.',
$this->schema->getAstNode()
);
} else if (!$queryType instanceof ObjectType) {
$this->reportError(
'Query root type must be Object type, it cannot be ' . Utils::printSafe($queryType) . '.',
$this->getOperationTypeNode($queryType, 'query')
);
}
$mutationType = $this->schema->getMutationType();
if ($mutationType && !$mutationType instanceof ObjectType) {
$this->reportError(
'Mutation root type must be Object type if provided, it cannot be ' . Utils::printSafe($mutationType) . '.',
$this->getOperationTypeNode($mutationType, 'mutation')
);
}
$subscriptionType = $this->schema->getSubscriptionType();
if ($subscriptionType && !$subscriptionType instanceof ObjectType) {
$this->reportError(
'Subscription root type must be Object type if provided, it cannot be ' . Utils::printSafe($subscriptionType) . '.',
$this->getOperationTypeNode($subscriptionType, 'subscription')
);
}
}
/**
* @param Type $type
* @param string $operation
*
* @return TypeNode|TypeDefinitionNode
*/
private function getOperationTypeNode($type, $operation)
{
$astNode = $this->schema->getAstNode();
$operationTypeNode = null;
if ($astNode instanceof SchemaDefinitionNode) {
$operationTypeNode = null;
foreach($astNode->operationTypes as $operationType) {
if ($operationType->operation === $operation) {
$operationTypeNode = $operationType;
break;
}
}
}
return $operationTypeNode ? $operationTypeNode->type : ($type ? $type->astNode : null);
}
public function validateDirectives()
{
$directives = $this->schema->getDirectives();
foreach($directives as $directive) {
// Ensure all directives are in fact GraphQL directives.
if (!$directive instanceof Directive) {
$this->reportError(
"Expected directive but got: " . Utils::printSafe($directive) . '.',
is_object($directive) ? $directive->astNode : null
);
continue;
}
// Ensure they are named correctly.
$this->validateName($directive);
// TODO: Ensure proper locations.
$argNames = [];
foreach ($directive->args as $arg) {
$argName = $arg->name;
// Ensure they are named correctly.
$this->validateName($directive);
if (isset($argNames[$argName])) {
$this->reportError(
"Argument @{$directive->name}({$argName}:) can only be defined once.",
$this->getAllDirectiveArgNodes($directive, $argName)
);
continue;
}
$argNames[$argName] = true;
// Ensure the type is an input type.
if (!Type::isInputType($arg->getType())) {
$this->reportError(
"The type of @{$directive->name}({$argName}:) must be Input Type " .
'but got: ' . Utils::printSafe($arg->getType()) . '.',
$this->getDirectiveArgTypeNode($directive, $argName)
);
}
}
}
}
/**
* @param Type|Directive|FieldDefinition|EnumValueDefinition|InputObjectField $node
*/
private function validateName($node)
{
// Ensure names are valid, however introspection types opt out.
$error = Utils::isValidNameError($node->name, $node->astNode);
if ($error && !Introspection::isIntrospectionType($node)) {
$this->addError($error);
}
}
public function validateTypes()
{
$typeMap = $this->schema->getTypeMap();
foreach($typeMap as $typeName => $type) {
// Ensure all provided types are in fact GraphQL type.
if (!$type instanceof NamedType) {
$this->reportError(
"Expected GraphQL named type but got: " . Utils::printSafe($type) . '.',
is_object($type) ? $type->astNode : null
);
continue;
}
$this->validateName($type);
if ($type instanceof ObjectType) {
// Ensure fields are valid
$this->validateFields($type);
// Ensure objects implement the interfaces they claim to.
$this->validateObjectInterfaces($type);
} else if ($type instanceof InterfaceType) {
// Ensure fields are valid.
$this->validateFields($type);
} else if ($type instanceof UnionType) {
// Ensure Unions include valid member types.
$this->validateUnionMembers($type);
} else if ($type instanceof EnumType) {
// Ensure Enums have valid values.
$this->validateEnumValues($type);
} else if ($type instanceof InputObjectType) {
// Ensure Input Object fields are valid.
$this->validateInputFields($type);
}
}
}
/**
* @param ObjectType|InterfaceType $type
*/
private function validateFields($type) {
$fieldMap = $type->getFields();
// Objects and Interfaces both must define one or more fields.
if (!$fieldMap) {
$this->reportError(
"Type {$type->name} must define one or more fields.",
$this->getAllObjectOrInterfaceNodes($type)
);
}
foreach ($fieldMap as $fieldName => $field) {
// Ensure they are named correctly.
$this->validateName($field);
// Ensure they were defined at most once.
$fieldNodes = $this->getAllFieldNodes($type, $fieldName);
if ($fieldNodes && count($fieldNodes) > 1) {
$this->reportError(
"Field {$type->name}.{$fieldName} can only be defined once.",
$fieldNodes
);
continue;
}
// Ensure the type is an output type
if (!Type::isOutputType($field->getType())) {
$this->reportError(
"The type of {$type->name}.{$fieldName} must be Output Type " .
'but got: ' . Utils::printSafe($field->getType()) . '.',
$this->getFieldTypeNode($type, $fieldName)
);
}
// Ensure the arguments are valid
$argNames = [];
foreach($field->args as $arg) {
$argName = $arg->name;
// Ensure they are named correctly.
$this->validateName($arg);
if (isset($argNames[$argName])) {
$this->reportError(
"Field argument {$type->name}.{$fieldName}({$argName}:) can only " .
'be defined once.',
$this->getAllFieldArgNodes($type, $fieldName, $argName)
);
}
$argNames[$argName] = true;
// Ensure the type is an input type
if (!Type::isInputType($arg->getType())) {
$this->reportError(
"The type of {$type->name}.{$fieldName}({$argName}:) must be Input " .
'Type but got: '. Utils::printSafe($arg->getType()) . '.',
$this->getFieldArgTypeNode($type, $fieldName, $argName)
);
}
}
}
}
private function validateObjectInterfaces(ObjectType $object) {
$implementedTypeNames = [];
foreach($object->getInterfaces() as $iface) {
if (isset($implementedTypeNames[$iface->name])) {
$this->reportError(
"Type {$object->name} can only implement {$iface->name} once.",
$this->getAllImplementsInterfaceNodes($object, $iface)
);
continue;
}
$implementedTypeNames[$iface->name] = true;
$this->validateObjectImplementsInterface($object, $iface);
}
}
/**
* @param ObjectType $object
* @param InterfaceType $iface
*/
private function validateObjectImplementsInterface(ObjectType $object, $iface)
{
if (!$iface instanceof InterfaceType) {
$this->reportError(
"Type {$object->name} must only implement Interface types, " .
"it cannot implement ". Utils::printSafe($iface) . ".",
$this->getImplementsInterfaceNode($object, $iface)
);
return;
}
$objectFieldMap = $object->getFields();
$ifaceFieldMap = $iface->getFields();
// Assert each interface field is implemented.
foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
$objectField = array_key_exists($fieldName, $objectFieldMap)
? $objectFieldMap[$fieldName]
: null;
// Assert interface field exists on object.
if (!$objectField) {
$this->reportError(
"Interface field {$iface->name}.{$fieldName} expected but " .
"{$object->name} does not provide it.",
[$this->getFieldNode($iface, $fieldName), $object->astNode]
);
continue;
}
// Assert interface field type is satisfied by object field type, by being
// a valid subtype. (covariant)
if (
!TypeComparators::isTypeSubTypeOf(
$this->schema,
$objectField->getType(),
$ifaceField->getType()
)
) {
$this->reportError(
"Interface field {$iface->name}.{$fieldName} expects type ".
"{$ifaceField->getType()} but {$object->name}.{$fieldName} " .
"is type " . Utils::printSafe($objectField->getType()) . ".",
[
$this->getFieldTypeNode($iface, $fieldName),
$this->getFieldTypeNode($object, $fieldName),
]
);
}
// Assert each interface field arg is implemented.
foreach($ifaceField->args as $ifaceArg) {
$argName = $ifaceArg->name;
$objectArg = null;
foreach($objectField->args as $arg) {
if ($arg->name === $argName) {
$objectArg = $arg;
break;
}
}
// Assert interface field arg exists on object field.
if (!$objectArg) {
$this->reportError(
"Interface field argument {$iface->name}.{$fieldName}({$argName}:) " .
"expected but {$object->name}.{$fieldName} does not provide it.",
[
$this->getFieldArgNode($iface, $fieldName, $argName),
$this->getFieldNode($object, $fieldName),
]
);
continue;
}
// Assert interface field arg type matches object field arg type.
// (invariant)
// TODO: change to contravariant?
if (!TypeComparators::isEqualType($ifaceArg->getType(), $objectArg->getType())) {
$this->reportError(
"Interface field argument {$iface->name}.{$fieldName}({$argName}:) ".
"expects type " . Utils::printSafe($ifaceArg->getType()) . " but " .
"{$object->name}.{$fieldName}({$argName}:) is type " .
Utils::printSafe($objectArg->getType()) . ".",
[
$this->getFieldArgTypeNode($iface, $fieldName, $argName),
$this->getFieldArgTypeNode($object, $fieldName, $argName),
]
);
}
// TODO: validate default values?
}
// Assert additional arguments must not be required.
foreach($objectField->args as $objectArg) {
$argName = $objectArg->name;
$ifaceArg = null;
foreach($ifaceField->args as $arg) {
if ($arg->name === $argName) {
$ifaceArg = $arg;
break;
}
}
if (!$ifaceArg && $objectArg->getType() instanceof NonNull) {
$this->reportError(
"Object field argument {$object->name}.{$fieldName}({$argName}:) " .
"is of required type " . Utils::printSafe($objectArg->getType()) . " but is not also " .
"provided by the Interface field {$iface->name}.{$fieldName}.",
[
$this->getFieldArgTypeNode($object, $fieldName, $argName),
$this->getFieldNode($iface, $fieldName),
]
);
}
}
}
}
private function validateUnionMembers(UnionType $union)
{
$memberTypes = $union->getTypes();
if (!$memberTypes) {
$this->reportError(
"Union type {$union->name} must define one or more member types.",
$union->astNode
);
}
$includedTypeNames = [];
foreach($memberTypes as $memberType) {
if (isset($includedTypeNames[$memberType->name])) {
$this->reportError(
"Union type {$union->name} can only include type ".
"{$memberType->name} once.",
$this->getUnionMemberTypeNodes($union, $memberType->name)
);
continue;
}
$includedTypeNames[$memberType->name] = true;
if (!$memberType instanceof ObjectType) {
$this->reportError(
"Union type {$union->name} can only include Object types, ".
"it cannot include " . Utils::printSafe($memberType) . ".",
$this->getUnionMemberTypeNodes($union, Utils::printSafe($memberType))
);
}
}
}
private function validateEnumValues(EnumType $enumType)
{
$enumValues = $enumType->getValues();
if (!$enumValues) {
$this->reportError(
"Enum type {$enumType->name} must define one or more values.",
$enumType->astNode
);
}
foreach($enumValues as $enumValue) {
$valueName = $enumValue->name;
// Ensure no duplicates
$allNodes = $this->getEnumValueNodes($enumType, $valueName);
if ($allNodes && count($allNodes) > 1) {
$this->reportError(
"Enum type {$enumType->name} can include value {$valueName} only once.",
$allNodes
);
}
// Ensure valid name.
$this->validateName($enumValue);
if ($valueName === 'true' || $valueName === 'false' || $valueName === 'null') {
$this->reportError(
"Enum type {$enumType->name} cannot include value: {$valueName}.",
$enumValue->astNode
);
}
}
}
private function validateInputFields(InputObjectType $inputObj)
{
$fieldMap = $inputObj->getFields();
if (!$fieldMap) {
$this->reportError(
"Input Object type {$inputObj->name} must define one or more fields.",
$inputObj->astNode
);
}
// Ensure the arguments are valid
foreach ($fieldMap as $fieldName => $field) {
// Ensure they are named correctly.
$this->validateName($field);
// TODO: Ensure they are unique per field.
// Ensure the type is an input type
if (!Type::isInputType($field->getType())) {
$this->reportError(
"The type of {$inputObj->name}.{$fieldName} must be Input Type " .
"but got: " . Utils::printSafe($field->getType()) . ".",
$field->astNode ? $field->astNode->type : null
);
}
}
}
/**
* @param ObjectType|InterfaceType $type
* @return ObjectTypeDefinitionNode[]|ObjectTypeExtensionNode[]|InterfaceTypeDefinitionNode[]|InterfaceTypeExtensionNode[]
*/
private function getAllObjectOrInterfaceNodes($type)
{
return $type->astNode
? ($type->extensionASTNodes
? array_merge([$type->astNode], $type->extensionASTNodes)
: [$type->astNode])
: ($type->extensionASTNodes ?: []);
}
/**
* @param ObjectType $type
* @param InterfaceType $iface
* @return NamedTypeNode|null
*/
private function getImplementsInterfaceNode(ObjectType $type, $iface)
{
$nodes = $this->getAllImplementsInterfaceNodes($type, $iface);
return $nodes && isset($nodes[0]) ? $nodes[0] : null;
}
/**
* @param ObjectType $type
* @param InterfaceType $iface
* @return NamedTypeNode[]
*/
private function getAllImplementsInterfaceNodes(ObjectType $type, $iface)
{
$implementsNodes = [];
$astNodes = $this->getAllObjectOrInterfaceNodes($type);
foreach($astNodes as $astNode) {
if ($astNode && $astNode->interfaces) {
foreach($astNode->interfaces as $node) {
if ($node->name->value === $iface->name) {
$implementsNodes[] = $node;
}
}
}
}
return $implementsNodes;
}
/**
* @param ObjectType|InterfaceType $type
* @param string $fieldName
* @return FieldDefinitionNode|null
*/
private function getFieldNode($type, $fieldName)
{
$nodes = $this->getAllFieldNodes($type, $fieldName);
return $nodes && isset($nodes[0]) ? $nodes[0] : null;
}
/**
* @param ObjectType|InterfaceType $type
* @param string $fieldName
* @return FieldDefinitionNode[]
*/
private function getAllFieldNodes($type, $fieldName)
{
$fieldNodes = [];
$astNodes = $this->getAllObjectOrInterfaceNodes($type);
foreach($astNodes as $astNode) {
if ($astNode && $astNode->fields) {
foreach($astNode->fields as $node) {
if ($node->name->value === $fieldName) {
$fieldNodes[] = $node;
}
}
}
}
return $fieldNodes;
}
/**
* @param ObjectType|InterfaceType $type
* @param string $fieldName
* @return TypeNode|null
*/
private function getFieldTypeNode($type, $fieldName)
{
$fieldNode = $this->getFieldNode($type, $fieldName);
return $fieldNode ? $fieldNode->type : null;
}
/**
* @param ObjectType|InterfaceType $type
* @param string $fieldName
* @param string $argName
* @return InputValueDefinitionNode|null
*/
private function getFieldArgNode($type, $fieldName, $argName)
{
$nodes = $this->getAllFieldArgNodes($type, $fieldName, $argName);
return $nodes && isset($nodes[0]) ? $nodes[0] : null;
}
/**
* @param ObjectType|InterfaceType $type
* @param string $fieldName
* @param string $argName
* @return InputValueDefinitionNode[]
*/
private function getAllFieldArgNodes($type, $fieldName, $argName)
{
$argNodes = [];
$fieldNode = $this->getFieldNode($type, $fieldName);
if ($fieldNode && $fieldNode->arguments) {
foreach ($fieldNode->arguments as $node) {
if ($node->name->value === $argName) {
$argNodes[] = $node;
}
}
}
return $argNodes;
}
/**
* @param ObjectType|InterfaceType $type
* @param string $fieldName
* @param string $argName
* @return TypeNode|null
*/
private function getFieldArgTypeNode($type, $fieldName, $argName)
{
$fieldArgNode = $this->getFieldArgNode($type, $fieldName, $argName);
return $fieldArgNode ? $fieldArgNode->type : null;
}
/**
* @param Directive $directive
* @param string $argName
* @return InputValueDefinitionNode[]
*/
private function getAllDirectiveArgNodes(Directive $directive, $argName)
{
$argNodes = [];
$directiveNode = $directive->astNode;
if ($directiveNode && $directiveNode->arguments) {
foreach($directiveNode->arguments as $node) {
if ($node->name->value === $argName) {
$argNodes[] = $node;
}
}
}
return $argNodes;
}
/**
* @param Directive $directive
* @param string $argName
* @return TypeNode|null
*/
private function getDirectiveArgTypeNode(Directive $directive, $argName)
{
$argNode = $this->getAllDirectiveArgNodes($directive, $argName)[0];
return $argNode ? $argNode->type : null;
}
/**
* @param UnionType $union
* @param string $typeName
* @return NamedTypeNode[]
*/
private function getUnionMemberTypeNodes(UnionType $union, $typeName)
{
if ($union->astNode && $union->astNode->types) {
return array_filter(
$union->astNode->types,
function (NamedTypeNode $value) use ($typeName) {
return $value->name->value === $typeName;
}
);
}
return $union->astNode ?
$union->astNode->types : null;
}
/**
* @param EnumType $enum
* @param string $valueName
* @return EnumValueDefinitionNode[]
*/
private function getEnumValueNodes(EnumType $enum, $valueName)
{
if ($enum->astNode && $enum->astNode->values) {
return array_filter(
iterator_to_array($enum->astNode->values),
function (EnumValueDefinitionNode $value) use ($valueName) {
return $value->name->value === $valueName;
}
);
}
return $enum->astNode ?
$enum->astNode->values : null;
}
/**
* @param string $message
* @param array|Node|TypeNode|TypeDefinitionNode $nodes
*/
private function reportError($message, $nodes = null) {
$nodes = array_filter($nodes && is_array($nodes) ? $nodes : [$nodes]);
$this->addError(new Error($message, $nodes));
}
/**
* @param Error $error
*/
private function addError($error) {
$this->errors[] = $error;
}
}

View File

@ -6,6 +6,9 @@ trigger_error(
E_USER_DEPRECATED E_USER_DEPRECATED
); );
/**
* @deprecated Use GraphQL\Utils\Utils
*/
class Utils extends \GraphQL\Utils\Utils class Utils extends \GraphQL\Utils\Utils
{ {
} }

View File

@ -1,6 +1,7 @@
<?php <?php
namespace GraphQL\Utils; namespace GraphQL\Utils;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\BooleanValueNode; use GraphQL\Language\AST\BooleanValueNode;
use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\DocumentNode;
@ -30,9 +31,9 @@ use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\LeafType; use GraphQL\Type\Definition\LeafType;
use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema; use GraphQL\Type\Schema;
use GraphQL\Utils\Utils;
/** /**
* Various utilities dealing with AST * Various utilities dealing with AST
@ -205,15 +206,11 @@ class AST
return new ObjectValueNode(['fields' => $fieldNodes]); return new ObjectValueNode(['fields' => $fieldNodes]);
} }
if ($type instanceof ScalarType || $type instanceof EnumType) {
// Since value is an internally represented value, it must be serialized // Since value is an internally represented value, it must be serialized
// to an externally represented value before converting into an AST. // to an externally represented value before converting into an AST.
if ($type instanceof LeafType) {
$serialized = $type->serialize($value); $serialized = $type->serialize($value);
} else { if (null === $serialized || Utils::isInvalid($serialized)) {
throw new InvariantViolation("Must provide Input Type, cannot use: " . Utils::printSafe($type));
}
if (null === $serialized) {
return null; return null;
} }
@ -252,6 +249,9 @@ class AST
throw new InvariantViolation('Cannot convert value to AST: ' . Utils::printSafe($serialized)); throw new InvariantViolation('Cannot convert value to AST: ' . Utils::printSafe($serialized));
} }
throw new Error('Unknown type: ' . Utils::printSafe($type) . '.');
}
/** /**
* Produces a PHP value given a GraphQL Value AST. * Produces a PHP value given a GraphQL Value AST.
* *
@ -383,19 +383,100 @@ class AST
return $coercedObj; return $coercedObj;
} }
if ($type instanceof LeafType) { if ($type instanceof EnumType) {
$parsed = $type->parseLiteral($valueNode); if (!$valueNode instanceof EnumValueNode) {
return $undefined;
if (null === $parsed && !$type->isValidLiteral($valueNode)) { }
// Invalid values represent a failure to parse correctly, in which case $enumValue = $type->getValue($valueNode->value);
// no value is returned. if (!$enumValue) {
return $undefined; return $undefined;
} }
return $parsed; return $enumValue->value;
} }
throw new InvariantViolation('Must be input type'); if ($type instanceof ScalarType) {
// Scalars fulfill parsing a literal value via parseLiteral().
// Invalid values represent a failure to parse correctly, in which case
// no value is returned.
try {
$result = $type->parseLiteral($valueNode, $variables);
} catch (\Exception $error) {
return $undefined;
} catch (\Throwable $error) {
return $undefined;
}
if (Utils::isInvalid($result)) {
return $undefined;
}
return $result;
}
throw new Error('Unknown type: ' . Utils::printSafe($type) . '.');
}
/**
* Produces a PHP value given a GraphQL Value AST.
*
* Unlike `valueFromAST()`, no type is provided. The resulting JavaScript value
* will reflect the provided GraphQL value AST.
*
* | GraphQL Value | PHP Value |
* | -------------------- | ------------- |
* | Input Object | Assoc Array |
* | List | Array |
* | Boolean | Boolean |
* | String | String |
* | Int / Float | Int / Float |
* | Enum | Mixed |
* | Null | null |
*
* @api
* @param Node $valueNode
* @param array|null $variables
* @return mixed
* @throws \Exception
*/
public static function valueFromASTUntyped($valueNode, array $variables = null) {
switch (true) {
case $valueNode instanceof NullValueNode:
return null;
case $valueNode instanceof IntValueNode:
return intval($valueNode->value, 10);
case $valueNode instanceof FloatValueNode:
return floatval($valueNode->value);
case $valueNode instanceof StringValueNode:
case $valueNode instanceof EnumValueNode:
case $valueNode instanceof BooleanValueNode:
return $valueNode->value;
case $valueNode instanceof ListValueNode:
return array_map(
function($node) use ($variables) {
return self::valueFromASTUntyped($node, $variables);
},
iterator_to_array($valueNode->values)
);
case $valueNode instanceof ObjectValueNode:
return array_combine(
array_map(
function($field) { return $field->name->value; },
iterator_to_array($valueNode->fields)
),
array_map(
function($field) use ($variables) { return self::valueFromASTUntyped($field->value, $variables); },
iterator_to_array($valueNode->fields)
)
);
case $valueNode instanceof VariableNode:
$variableName = $valueNode->name->value;
return ($variables && isset($variables[$variableName]) && !Utils::isInvalid($variables[$variableName]))
? $variables[$variableName]
: null;
}
throw new Error('Unexpected value kind: ' . $valueNode->kind . '.');
} }
/** /**
@ -405,7 +486,7 @@ class AST
* @param Schema $schema * @param Schema $schema
* @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode * @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
* @return Type * @return Type
* @throws InvariantViolation * @throws \Exception
*/ */
public static function typeFromAST(Schema $schema, $inputTypeNode) public static function typeFromAST(Schema $schema, $inputTypeNode)
{ {
@ -417,11 +498,13 @@ class AST
$innerType = self::typeFromAST($schema, $inputTypeNode->type); $innerType = self::typeFromAST($schema, $inputTypeNode->type);
return $innerType ? new NonNull($innerType) : null; return $innerType ? new NonNull($innerType) : null;
} }
if ($inputTypeNode instanceof NamedTypeNode) {
Utils::invariant($inputTypeNode && $inputTypeNode instanceof NamedTypeNode, 'Must be a named type');
return $schema->getType($inputTypeNode->name->value); return $schema->getType($inputTypeNode->name->value);
} }
throw new Error('Unexpected type kind: ' . $inputTypeNode->kind . '.');
}
/** /**
* Returns true if the provided valueNode is a variable which is not defined * Returns true if the provided valueNode is a variable which is not defined
* in the set of variables. * in the set of variables.

View File

@ -0,0 +1,455 @@
<?php
namespace GraphQL\Utils;
use GraphQL\Error\Error;
use GraphQL\Executor\Values;
use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\EnumTypeDefinitionNode;
use GraphQL\Language\AST\EnumValueDefinitionNode;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\ListTypeNode;
use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NonNullTypeNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
use GraphQL\Language\AST\TypeNode;
use GraphQL\Language\AST\UnionTypeDefinitionNode;
use GraphQL\Language\Token;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
class ASTDefinitionBuilder
{
/**
* @var array
*/
private $typeDefintionsMap;
/**
* @var callable
*/
private $typeConfigDecorator;
/**
* @var array
*/
private $options;
/**
* @var callable
*/
private $resolveType;
/**
* @var array
*/
private $cache;
public function __construct(array $typeDefintionsMap, $options, callable $resolveType, callable $typeConfigDecorator = null)
{
$this->typeDefintionsMap = $typeDefintionsMap;
$this->typeConfigDecorator = $typeConfigDecorator;
$this->options = $options;
$this->resolveType = $resolveType;
$this->cache = Type::getAllBuiltInTypes();
}
/**
* @param Type $innerType
* @param TypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
* @return Type
*/
private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode)
{
if ($inputTypeNode->kind == NodeKind::LIST_TYPE) {
return Type::listOf($this->buildWrappedType($innerType, $inputTypeNode->type));
}
if ($inputTypeNode->kind == NodeKind::NON_NULL_TYPE) {
$wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type);
return Type::nonNull(NonNull::assertNullableType($wrappedType));
}
return $innerType;
}
/**
* @param TypeNode|ListTypeNode|NonNullTypeNode $typeNode
* @return TypeNode
*/
private function getNamedTypeNode(TypeNode $typeNode)
{
$namedType = $typeNode;
while ($namedType->kind === NodeKind::LIST_TYPE || $namedType->kind === NodeKind::NON_NULL_TYPE) {
$namedType = $namedType->type;
}
return $namedType;
}
/**
* @param string $typeName
* @param NamedTypeNode|null $typeNode
* @return Type
* @throws Error
*/
private function internalBuildType($typeName, $typeNode = null) {
if (!isset($this->cache[$typeName])) {
if (isset($this->typeDefintionsMap[$typeName])) {
$type = $this->makeSchemaDef($this->typeDefintionsMap[$typeName]);
if ($this->typeConfigDecorator) {
$fn = $this->typeConfigDecorator;
try {
$config = $fn($type->config, $this->typeDefintionsMap[$typeName], $this->typeDefintionsMap);
} catch (\Exception $e) {
throw new Error(
"Type config decorator passed to " . (static::class) . " threw an error " .
"when building $typeName type: {$e->getMessage()}",
null,
null,
null,
null,
$e
);
} catch (\Throwable $e) {
throw new Error(
"Type config decorator passed to " . (static::class) . " threw an error " .
"when building $typeName type: {$e->getMessage()}",
null,
null,
null,
null,
$e
);
}
if (!is_array($config) || isset($config[0])) {
throw new Error(
"Type config decorator passed to " . (static::class) . " is expected to return an array, but got " .
Utils::getVariableType($config)
);
}
$type = $this->makeSchemaDefFromConfig($this->typeDefintionsMap[$typeName], $config);
}
$this->cache[$typeName] = $type;
} else {
$fn = $this->resolveType;
$this->cache[$typeName] = $fn($typeName, $typeNode);
}
}
return $this->cache[$typeName];
}
/**
* @param string|NamedTypeNode $ref
* @return Type
* @throws Error
*/
public function buildType($ref)
{
if (is_string($ref)) {
return $this->internalBuildType($ref);
}
return $this->internalBuildType($ref->name->value, $ref);
}
/**
* @param TypeNode $typeNode
* @return Type|InputType
* @throws Error
*/
private function internalBuildWrappedType(TypeNode $typeNode)
{
$typeDef = $this->buildType($this->getNamedTypeNode($typeNode));
return $this->buildWrappedType($typeDef, $typeNode);
}
public function buildDirective(DirectiveDefinitionNode $directiveNode)
{
return new Directive([
'name' => $directiveNode->name->value,
'description' => $this->getDescription($directiveNode),
'locations' => Utils::map($directiveNode->locations, function ($node) {
return $node->value;
}),
'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null,
'astNode' => $directiveNode,
]);
}
public function buildField(FieldDefinitionNode $field)
{
return [
// Note: While this could make assertions to get the correctly typed
// value, that would throw immediately while type system validation
// with validateSchema() will produce more actionable results.
'type' => $this->internalBuildWrappedType($field->type),
'description' => $this->getDescription($field),
'args' => $field->arguments ? $this->makeInputValues($field->arguments) : null,
'deprecationReason' => $this->getDeprecationReason($field),
'astNode' => $field,
];
}
private function makeSchemaDef($def)
{
if (!$def) {
throw new Error('def must be defined.');
}
switch ($def->kind) {
case NodeKind::OBJECT_TYPE_DEFINITION:
return $this->makeTypeDef($def);
case NodeKind::INTERFACE_TYPE_DEFINITION:
return $this->makeInterfaceDef($def);
case NodeKind::ENUM_TYPE_DEFINITION:
return $this->makeEnumDef($def);
case NodeKind::UNION_TYPE_DEFINITION:
return $this->makeUnionDef($def);
case NodeKind::SCALAR_TYPE_DEFINITION:
return $this->makeScalarDef($def);
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
return $this->makeInputObjectDef($def);
default:
throw new Error("Type kind of {$def->kind} not supported.");
}
}
private function makeSchemaDefFromConfig($def, array $config)
{
if (!$def) {
throw new Error('def must be defined.');
}
switch ($def->kind) {
case NodeKind::OBJECT_TYPE_DEFINITION:
return new ObjectType($config);
case NodeKind::INTERFACE_TYPE_DEFINITION:
return new InterfaceType($config);
case NodeKind::ENUM_TYPE_DEFINITION:
return new EnumType($config);
case NodeKind::UNION_TYPE_DEFINITION:
return new UnionType($config);
case NodeKind::SCALAR_TYPE_DEFINITION:
return new CustomScalarType($config);
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
return new InputObjectType($config);
default:
throw new Error("Type kind of {$def->kind} not supported.");
}
}
private function makeTypeDef(ObjectTypeDefinitionNode $def)
{
$typeName = $def->name->value;
return new ObjectType([
'name' => $typeName,
'description' => $this->getDescription($def),
'fields' => function () use ($def) {
return $this->makeFieldDefMap($def);
},
'interfaces' => function () use ($def) {
return $this->makeImplementedInterfaces($def);
},
'astNode' => $def
]);
}
private function makeFieldDefMap($def)
{
return $def->fields
? Utils::keyValMap(
$def->fields,
function ($field) {
return $field->name->value;
},
function ($field) {
return $this->buildField($field);
}
)
: [];
}
private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def)
{
if ($def->interfaces) {
// Note: While this could make early assertions to get the correctly
// typed values, that would throw immediately while type system
// validation with validateSchema() will produce more actionable results.
return Utils::map($def->interfaces, function ($iface) {
return $this->buildType($iface);
});
}
return null;
}
private function makeInputValues($values)
{
return Utils::keyValMap(
$values,
function ($value) {
return $value->name->value;
},
function ($value) {
// Note: While this could make assertions to get the correctly typed
// value, that would throw immediately while type system validation
// with validateSchema() will produce more actionable results.
$type = $this->internalBuildWrappedType($value->type);
$config = [
'name' => $value->name->value,
'type' => $type,
'description' => $this->getDescription($value),
'astNode' => $value
];
if (isset($value->defaultValue)) {
$config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type);
}
return $config;
}
);
}
private function makeInterfaceDef(InterfaceTypeDefinitionNode $def)
{
$typeName = $def->name->value;
return new InterfaceType([
'name' => $typeName,
'description' => $this->getDescription($def),
'fields' => function () use ($def) {
return $this->makeFieldDefMap($def);
},
'astNode' => $def
]);
}
private function makeEnumDef(EnumTypeDefinitionNode $def)
{
return new EnumType([
'name' => $def->name->value,
'description' => $this->getDescription($def),
'values' => $def->values
? Utils::keyValMap(
$def->values,
function ($enumValue) {
return $enumValue->name->value;
},
function ($enumValue) {
return [
'description' => $this->getDescription($enumValue),
'deprecationReason' => $this->getDeprecationReason($enumValue),
'astNode' => $enumValue
];
}
)
: [],
'astNode' => $def,
]);
}
private function makeUnionDef(UnionTypeDefinitionNode $def)
{
return new UnionType([
'name' => $def->name->value,
'description' => $this->getDescription($def),
// Note: While this could make assertions to get the correctly typed
// values below, that would throw immediately while type system
// validation with validateSchema() will produce more actionable results.
'types' => $def->types
? Utils::map($def->types, function ($typeNode) {
return $this->buildType($typeNode);
}):
[],
'astNode' => $def,
]);
}
private function makeScalarDef(ScalarTypeDefinitionNode $def)
{
return new CustomScalarType([
'name' => $def->name->value,
'description' => $this->getDescription($def),
'astNode' => $def,
'serialize' => function($value) {
return $value;
},
]);
}
private function makeInputObjectDef(InputObjectTypeDefinitionNode $def)
{
return new InputObjectType([
'name' => $def->name->value,
'description' => $this->getDescription($def),
'fields' => function () use ($def) {
return $def->fields
? $this->makeInputValues($def->fields)
: [];
},
'astNode' => $def,
]);
}
/**
* Given a collection of directives, returns the string value for the
* deprecation reason.
*
* @param EnumValueDefinitionNode | FieldDefinitionNode $node
* @return string
*/
private function getDeprecationReason($node)
{
$deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node);
return isset($deprecated['reason']) ? $deprecated['reason'] : null;
}
/**
* Given an ast node, returns its string description.
*/
private function getDescription($node)
{
if ($node->description) {
return $node->description->value;
}
if (isset($this->options['commentDescriptions'])) {
$rawValue = $this->getLeadingCommentBlock($node);
if ($rawValue !== null) {
return BlockString::value("\n" . $rawValue);
}
}
return null;
}
private function getLeadingCommentBlock($node)
{
$loc = $node->loc;
if (!$loc || !$loc->startToken) {
return null;
}
$comments = [];
$token = $loc->startToken->prev;
while (
$token &&
$token->kind === Token::COMMENT &&
$token->next && $token->prev &&
$token->line + 1 === $token->next->line &&
$token->line !== $token->prev->line
) {
$value = $token->value;
$comments[] = $value;
$token = $token->prev;
}
return implode("\n", array_reverse($comments));
}
}

61
src/Utils/BlockString.php Normal file
View File

@ -0,0 +1,61 @@
<?php
namespace GraphQL\Utils;
class BlockString {
/**
* Produces the value of a block string from its parsed raw value, similar to
* Coffeescript's block string, Python's docstring trim or Ruby's strip_heredoc.
*
* This implements the GraphQL spec's BlockStringValue() static algorithm.
*/
public static function value($rawString) {
// Expand a block string's raw value into independent lines.
$lines = preg_split("/\\r\\n|[\\n\\r]/", $rawString);
// Remove common indentation from all lines but first.
$commonIndent = null;
$linesLength = count($lines);
for ($i = 1; $i < $linesLength; $i++) {
$line = $lines[$i];
$indent = self::leadingWhitespace($line);
if (
$indent < mb_strlen($line) &&
($commonIndent === null || $indent < $commonIndent)
) {
$commonIndent = $indent;
if ($commonIndent === 0) {
break;
}
}
}
if ($commonIndent) {
for ($i = 1; $i < $linesLength; $i++) {
$line = $lines[$i];
$lines[$i] = mb_substr($line, $commonIndent);
}
}
// Remove leading and trailing blank lines.
while (count($lines) > 0 && trim($lines[0], " \t") === '') {
array_shift($lines);
}
while (count($lines) > 0 && trim($lines[count($lines) - 1], " \t") === '') {
array_pop($lines);
}
// Return a string of the lines joined with U+000A.
return implode("\n", $lines);
}
private static function leadingWhitespace($str) {
$i = 0;
while ($i < mb_strlen($str) && ($str[$i] === ' ' || $str[$i] === '\t')) {
$i++;
}
return $i;
}
}

View File

@ -2,35 +2,13 @@
namespace GraphQL\Utils; namespace GraphQL\Utils;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Executor\Values;
use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\EnumTypeDefinitionNode;
use GraphQL\Language\AST\EnumValueDefinitionNode;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\ObjectTypeDefinitionNode; use GraphQL\Language\AST\SchemaDefinitionNode;
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Language\AST\TypeNode;
use GraphQL\Language\AST\UnionTypeDefinitionNode;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Language\Source; use GraphQL\Language\Source;
use GraphQL\Language\Token;
use GraphQL\Type\Schema; use GraphQL\Type\Schema;
use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Introspection;
/** /**
* Build instance of `GraphQL\Type\Schema` out of type language definition (string or parsed AST) * Build instance of `GraphQL\Type\Schema` out of type language definition (string or parsed AST)
@ -38,33 +16,6 @@ use GraphQL\Type\Introspection;
*/ */
class BuildSchema class BuildSchema
{ {
/**
* @param Type $innerType
* @param TypeNode $inputTypeNode
* @return Type
*/
private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode)
{
if ($inputTypeNode->kind == NodeKind::LIST_TYPE) {
return Type::listOf($this->buildWrappedType($innerType, $inputTypeNode->type));
}
if ($inputTypeNode->kind == NodeKind::NON_NULL_TYPE) {
$wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type);
Utils::invariant(!($wrappedType instanceof NonNull), 'No nesting nonnull.');
return Type::nonNull($wrappedType);
}
return $innerType;
}
private function getNamedTypeNode(TypeNode $typeNode)
{
$namedType = $typeNode;
while ($namedType->kind === NodeKind::LIST_TYPE || $namedType->kind === NodeKind::NON_NULL_TYPE) {
$namedType = $namedType->type;
}
return $namedType;
}
/** /**
* This takes the ast of a schema document produced by the parse function in * This takes the ast of a schema document produced by the parse function in
* GraphQL\Language\Parser. * GraphQL\Language\Parser.
@ -75,33 +26,40 @@ class BuildSchema
* Given that AST it constructs a GraphQL\Type\Schema. The resulting schema * Given that AST it constructs a GraphQL\Type\Schema. The resulting schema
* has no resolve methods, so execution will use default resolvers. * has no resolve methods, so execution will use default resolvers.
* *
* Accepts options as a third argument:
*
* - commentDescriptions:
* Provide true to use preceding comments as the description.
*
*
* @api * @api
* @param DocumentNode $ast * @param DocumentNode $ast
* @param callable $typeConfigDecorator * @param callable $typeConfigDecorator
* @param array $options
* @return Schema * @return Schema
* @throws Error * @throws Error
*/ */
public static function buildAST(DocumentNode $ast, callable $typeConfigDecorator = null) public static function buildAST(DocumentNode $ast, callable $typeConfigDecorator = null, array $options = [])
{ {
$builder = new self($ast, $typeConfigDecorator); $builder = new self($ast, $typeConfigDecorator, $options);
return $builder->buildSchema(); return $builder->buildSchema();
} }
private $ast; private $ast;
private $innerTypeMap;
private $nodeMap; private $nodeMap;
private $typeConfigDecorator; private $typeConfigDecorator;
private $loadedTypeDefs; private $options;
public function __construct(DocumentNode $ast, callable $typeConfigDecorator = null) public function __construct(DocumentNode $ast, callable $typeConfigDecorator = null, array $options = [])
{ {
$this->ast = $ast; $this->ast = $ast;
$this->typeConfigDecorator = $typeConfigDecorator; $this->typeConfigDecorator = $typeConfigDecorator;
$this->loadedTypeDefs = []; $this->options = $options;
} }
public function buildSchema() public function buildSchema()
{ {
/** @var SchemaDefinitionNode $schemaDef */
$schemaDef = null; $schemaDef = null;
$typeDefs = []; $typeDefs = [];
$this->nodeMap = []; $this->nodeMap = [];
@ -133,121 +91,70 @@ class BuildSchema
} }
} }
$queryTypeName = null; $operationTypes = $schemaDef
$mutationTypeName = null; ? $this->getOperationTypes($schemaDef)
$subscriptionTypeName = null; : [
if ($schemaDef) { 'query' => isset($this->nodeMap['Query']) ? 'Query' : null,
foreach ($schemaDef->operationTypes as $operationType) { 'mutation' => isset($this->nodeMap['Mutation']) ? 'Mutation' : null,
$typeName = $operationType->type->name->value; 'subscription' => isset($this->nodeMap['Subscription']) ? 'Subscription' : null,
if ($operationType->operation === 'query') {
if ($queryTypeName) {
throw new Error('Must provide only one query type in schema.');
}
if (!isset($this->nodeMap[$typeName])) {
throw new Error(
'Specified query type "' . $typeName . '" not found in document.'
);
}
$queryTypeName = $typeName;
} else if ($operationType->operation === 'mutation') {
if ($mutationTypeName) {
throw new Error('Must provide only one mutation type in schema.');
}
if (!isset($this->nodeMap[$typeName])) {
throw new Error(
'Specified mutation type "' . $typeName . '" not found in document.'
);
}
$mutationTypeName = $typeName;
} else if ($operationType->operation === 'subscription') {
if ($subscriptionTypeName) {
throw new Error('Must provide only one subscription type in schema.');
}
if (!isset($this->nodeMap[$typeName])) {
throw new Error(
'Specified subscription type "' . $typeName . '" not found in document.'
);
}
$subscriptionTypeName = $typeName;
}
}
} else {
if (isset($this->nodeMap['Query'])) {
$queryTypeName = 'Query';
}
if (isset($this->nodeMap['Mutation'])) {
$mutationTypeName = 'Mutation';
}
if (isset($this->nodeMap['Subscription'])) {
$subscriptionTypeName = 'Subscription';
}
}
if (!$queryTypeName) {
throw new Error(
'Must provide schema definition with query type or a type named Query.'
);
}
$this->innerTypeMap = [
'String' => Type::string(),
'Int' => Type::int(),
'Float' => Type::float(),
'Boolean' => Type::boolean(),
'ID' => Type::id(),
'__Schema' => Introspection::_schema(),
'__Directive' => Introspection::_directive(),
'__DirectiveLocation' => Introspection::_directiveLocation(),
'__Type' => Introspection::_type(),
'__Field' => Introspection::_field(),
'__InputValue' => Introspection::_inputValue(),
'__EnumValue' => Introspection::_enumValue(),
'__TypeKind' => Introspection::_typeKind(),
]; ];
$directives = array_map([$this, 'getDirective'], $directiveDefs); $defintionBuilder = new ASTDefinitionBuilder(
$this->nodeMap,
$this->options,
function($typeName) { throw new Error('Type "'. $typeName . '" not found in document.'); },
$this->typeConfigDecorator
);
$directives = array_map(function($def) use ($defintionBuilder) {
return $defintionBuilder->buildDirective($def);
}, $directiveDefs);
// If specified directives were not explicitly declared, add them. // If specified directives were not explicitly declared, add them.
$skip = array_reduce($directives, function($hasSkip, $directive) { $skip = array_reduce($directives, function ($hasSkip, $directive) {
return $hasSkip || $directive->name == 'skip'; return $hasSkip || $directive->name == 'skip';
}); });
if (!$skip) { if (!$skip) {
$directives[] = Directive::skipDirective(); $directives[] = Directive::skipDirective();
} }
$include = array_reduce($directives, function($hasInclude, $directive) { $include = array_reduce($directives, function ($hasInclude, $directive) {
return $hasInclude || $directive->name == 'include'; return $hasInclude || $directive->name == 'include';
}); });
if (!$include) { if (!$include) {
$directives[] = Directive::includeDirective(); $directives[] = Directive::includeDirective();
} }
$deprecated = array_reduce($directives, function($hasDeprecated, $directive) { $deprecated = array_reduce($directives, function ($hasDeprecated, $directive) {
return $hasDeprecated || $directive->name == 'deprecated'; return $hasDeprecated || $directive->name == 'deprecated';
}); });
if (!$deprecated) { if (!$deprecated) {
$directives[] = Directive::deprecatedDirective(); $directives[] = Directive::deprecatedDirective();
} }
// Note: While this could make early assertions to get the correctly
// typed values below, that would throw immediately while type system
// validation with validateSchema() will produce more actionable results.
$schema = new Schema([ $schema = new Schema([
'query' => $this->getObjectType($this->nodeMap[$queryTypeName]), 'query' => isset($operationTypes['query'])
'mutation' => $mutationTypeName ? ? $defintionBuilder->buildType($operationTypes['query'])
$this->getObjectType($this->nodeMap[$mutationTypeName]) : : null,
null, 'mutation' => isset($operationTypes['mutation'])
'subscription' => $subscriptionTypeName ? ? $defintionBuilder->buildType($operationTypes['mutation'])
$this->getObjectType($this->nodeMap[$subscriptionTypeName]) : : null,
null, 'subscription' => isset($operationTypes['subscription'])
'typeLoader' => function($name) { ? $defintionBuilder->buildType($operationTypes['subscription'])
return $this->typeDefNamed($name); : null,
'typeLoader' => function ($name) use ($defintionBuilder) {
return $defintionBuilder->buildType($name);
}, },
'directives' => $directives, 'directives' => $directives,
'astNode' => $schemaDef, 'astNode' => $schemaDef,
'types' => function() { 'types' => function () use ($defintionBuilder) {
$types = []; $types = [];
foreach ($this->nodeMap as $name => $def) { foreach ($this->nodeMap as $name => $def) {
if (!isset($this->loadedTypeDefs[$name])) { $types[] = $defintionBuilder->buildType($def->name->value);
$types[] = $this->typeDefNamed($def->name->value);
}
} }
return $types; return $types;
} }
@ -256,364 +163,31 @@ class BuildSchema
return $schema; return $schema;
} }
private function getDirective(DirectiveDefinitionNode $directiveNode) /**
* @param SchemaDefinitionNode $schemaDef
* @return array
* @throws Error
*/
private function getOperationTypes($schemaDef)
{ {
return new Directive([ $opTypes = [];
'name' => $directiveNode->name->value,
'description' => $this->getDescription($directiveNode),
'locations' => Utils::map($directiveNode->locations, function($node) {
return $node->value;
}),
'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null,
'astNode' => $directiveNode
]);
}
private function getObjectType(TypeDefinitionNode $typeNode) foreach ($schemaDef->operationTypes as $operationType) {
{ $typeName = $operationType->type->name->value;
$type = $this->typeDefNamed($typeNode->name->value); $operation = $operationType->operation;
Utils::invariant(
$type instanceof ObjectType,
'AST must provide object type.'
);
return $type;
}
private function produceType(TypeNode $typeNode) if (isset($opTypes[$operation])) {
{ throw new Error("Must provide only one $operation type in schema.");
$typeName = $this->getNamedTypeNode($typeNode)->name->value;
$typeDef = $this->typeDefNamed($typeName);
return $this->buildWrappedType($typeDef, $typeNode);
}
private function produceInputType(TypeNode $typeNode)
{
$type = $this->produceType($typeNode);
Utils::invariant(Type::isInputType($type), 'Expected Input type.');
return $type;
}
private function produceOutputType(TypeNode $typeNode)
{
$type = $this->produceType($typeNode);
Utils::invariant(Type::isOutputType($type), 'Expected Input type.');
return $type;
}
private function produceObjectType(TypeNode $typeNode)
{
$type = $this->produceType($typeNode);
Utils::invariant($type instanceof ObjectType, 'Expected Object type.');
return $type;
}
private function produceInterfaceType(TypeNode $typeNode)
{
$type = $this->produceType($typeNode);
Utils::invariant($type instanceof InterfaceType, 'Expected Interface type.');
return $type;
}
private function typeDefNamed($typeName)
{
if (isset($this->innerTypeMap[$typeName])) {
return $this->innerTypeMap[$typeName];
} }
if (!isset($this->nodeMap[$typeName])) { if (!isset($this->nodeMap[$typeName])) {
throw new Error('Type "' . $typeName . '" not found in document.'); throw new Error("Specified $operation type \"$typeName\" not found in document.");
} }
$this->loadedTypeDefs[$typeName] = true; $opTypes[$operation] = $typeName;
$config = $this->makeSchemaDefConfig($this->nodeMap[$typeName]);
if ($this->typeConfigDecorator) {
$fn = $this->typeConfigDecorator;
try {
$config = $fn($config, $this->nodeMap[$typeName], $this->nodeMap);
} catch (\Exception $e) {
throw new Error(
"Type config decorator passed to " . (static::class) . " threw an error ".
"when building $typeName type: {$e->getMessage()}",
null,
null,
null,
null,
$e
);
} catch (\Throwable $e) {
throw new Error(
"Type config decorator passed to " . (static::class) . " threw an error ".
"when building $typeName type: {$e->getMessage()}",
null,
null,
null,
null,
$e
);
}
if (!is_array($config) || isset($config[0])) {
throw new Error(
"Type config decorator passed to " . (static::class) . " is expected to return an array, but got ".
Utils::getVariableType($config)
);
}
} }
$innerTypeDef = $this->makeSchemaDef($this->nodeMap[$typeName], $config); return $opTypes;
if (!$innerTypeDef) {
throw new Error("Nothing constructed for $typeName.");
}
$this->innerTypeMap[$typeName] = $innerTypeDef;
return $innerTypeDef;
}
private function makeSchemaDefConfig($def)
{
if (!$def) {
throw new Error('def must be defined.');
}
switch ($def->kind) {
case NodeKind::OBJECT_TYPE_DEFINITION:
return $this->makeTypeDefConfig($def);
case NodeKind::INTERFACE_TYPE_DEFINITION:
return $this->makeInterfaceDefConfig($def);
case NodeKind::ENUM_TYPE_DEFINITION:
return $this->makeEnumDefConfig($def);
case NodeKind::UNION_TYPE_DEFINITION:
return $this->makeUnionDefConfig($def);
case NodeKind::SCALAR_TYPE_DEFINITION:
return $this->makeScalarDefConfig($def);
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
return $this->makeInputObjectDefConfig($def);
default:
throw new Error("Type kind of {$def->kind} not supported.");
}
}
private function makeSchemaDef($def, array $config = null)
{
if (!$def) {
throw new Error('def must be defined.');
}
$config = $config ?: $this->makeSchemaDefConfig($def);
switch ($def->kind) {
case NodeKind::OBJECT_TYPE_DEFINITION:
return new ObjectType($config);
case NodeKind::INTERFACE_TYPE_DEFINITION:
return new InterfaceType($config);
case NodeKind::ENUM_TYPE_DEFINITION:
return new EnumType($config);
case NodeKind::UNION_TYPE_DEFINITION:
return new UnionType($config);
case NodeKind::SCALAR_TYPE_DEFINITION:
return new CustomScalarType($config);
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
return new InputObjectType($config);
default:
throw new Error("Type kind of {$def->kind} not supported.");
}
}
private function makeTypeDefConfig(ObjectTypeDefinitionNode $def)
{
$typeName = $def->name->value;
return [
'name' => $typeName,
'description' => $this->getDescription($def),
'fields' => function() use ($def) {
return $this->makeFieldDefMap($def);
},
'interfaces' => function() use ($def) {
return $this->makeImplementedInterfaces($def);
},
'astNode' => $def
];
}
private function makeFieldDefMap($def)
{
return Utils::keyValMap(
$def->fields,
function ($field) {
return $field->name->value;
},
function($field) {
return [
'type' => $this->produceOutputType($field->type),
'description' => $this->getDescription($field),
'args' => $this->makeInputValues($field->arguments),
'deprecationReason' => $this->getDeprecationReason($field),
'astNode' => $field
];
}
);
}
private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def)
{
if (isset($def->interfaces)) {
return Utils::map($def->interfaces, function ($iface) {
return $this->produceInterfaceType($iface);
});
}
return null;
}
private function makeInputValues($values)
{
return Utils::keyValMap(
$values,
function ($value) {
return $value->name->value;
},
function($value) {
$type = $this->produceInputType($value->type);
$config = [
'name' => $value->name->value,
'type' => $type,
'description' => $this->getDescription($value),
'astNode' => $value
];
if (isset($value->defaultValue)) {
$config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type);
}
return $config;
}
);
}
private function makeInterfaceDefConfig(InterfaceTypeDefinitionNode $def)
{
$typeName = $def->name->value;
return [
'name' => $typeName,
'description' => $this->getDescription($def),
'fields' => function() use ($def) {
return $this->makeFieldDefMap($def);
},
'astNode' => $def,
'resolveType' => function() {
$this->cannotExecuteSchema();
}
];
}
private function makeEnumDefConfig(EnumTypeDefinitionNode $def)
{
return [
'name' => $def->name->value,
'description' => $this->getDescription($def),
'astNode' => $def,
'values' => Utils::keyValMap(
$def->values,
function($enumValue) {
return $enumValue->name->value;
},
function($enumValue) {
return [
'description' => $this->getDescription($enumValue),
'deprecationReason' => $this->getDeprecationReason($enumValue),
'astNode' => $enumValue
];
}
)
];
}
private function makeUnionDefConfig(UnionTypeDefinitionNode $def)
{
return [
'name' => $def->name->value,
'description' => $this->getDescription($def),
'types' => Utils::map($def->types, function($typeNode) {
return $this->produceObjectType($typeNode);
}),
'astNode' => $def,
'resolveType' => [$this, 'cannotExecuteSchema']
];
}
private function makeScalarDefConfig(ScalarTypeDefinitionNode $def)
{
return [
'name' => $def->name->value,
'description' => $this->getDescription($def),
'astNode' => $def,
'serialize' => function() {
return false;
},
// Note: validation calls the parse functions to determine if a
// literal value is correct. Returning null would cause use of custom
// scalars to always fail validation. Returning false causes them to
// always pass validation.
'parseValue' => function() {
return false;
},
'parseLiteral' => function() {
return false;
}
];
}
private function makeInputObjectDefConfig(InputObjectTypeDefinitionNode $def)
{
return [
'name' => $def->name->value,
'description' => $this->getDescription($def),
'fields' => function() use ($def) { return $this->makeInputValues($def->fields); },
'astNode' => $def,
];
}
/**
* Given a collection of directives, returns the string value for the
* deprecation reason.
*
* @param EnumValueDefinitionNode | FieldDefinitionNode $node
* @return string
*/
private function getDeprecationReason($node)
{
$deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node);
return isset($deprecated['reason']) ? $deprecated['reason'] : null;
}
/**
* Given an ast node, returns its string description based on a contiguous
* block full-line of comments preceding it.
*/
public function getDescription($node)
{
$loc = $node->loc;
if (!$loc || !$loc->startToken) {
return ;
}
$comments = [];
$minSpaces = null;
$token = $loc->startToken->prev;
while (
$token &&
$token->kind === Token::COMMENT &&
$token->next && $token->prev &&
$token->line + 1 === $token->next->line &&
$token->line !== $token->prev->line
) {
$value = $token->value;
$spaces = $this->leadingSpaces($value);
if ($minSpaces === null || $spaces < $minSpaces) {
$minSpaces = $spaces;
}
$comments[] = $value;
$token = $token->prev;
}
return implode("\n", array_map(function($comment) use ($minSpaces) {
return mb_substr(str_replace("\n", '', $comment), $minSpaces);
}, array_reverse($comments)));
} }
/** /**
@ -623,25 +197,12 @@ class BuildSchema
* @api * @api
* @param DocumentNode|Source|string $source * @param DocumentNode|Source|string $source
* @param callable $typeConfigDecorator * @param callable $typeConfigDecorator
* @param array $options
* @return Schema * @return Schema
*/ */
public static function build($source, callable $typeConfigDecorator = null) public static function build($source, callable $typeConfigDecorator = null, array $options = [])
{ {
$doc = $source instanceof DocumentNode ? $source : Parser::parse($source); $doc = $source instanceof DocumentNode ? $source : Parser::parse($source);
return self::buildAST($doc, $typeConfigDecorator); return self::buildAST($doc, $typeConfigDecorator, $options);
} }
// Count the number of spaces on the starting side of a string.
private function leadingSpaces($str)
{
return strlen($str) - strlen(ltrim($str));
}
public function cannotExecuteSchema()
{
throw new Error(
'Generated Schema cannot use Interface or Union types for execution.'
);
}
} }

View File

@ -5,10 +5,12 @@
namespace GraphQL\Utils; namespace GraphQL\Utils;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NamedType;
use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\ScalarType;
@ -19,35 +21,28 @@ use GraphQL\Type\Schema;
class FindBreakingChanges class FindBreakingChanges
{ {
const BREAKING_CHANGE_FIELD_CHANGED = 'FIELD_CHANGED_KIND'; const BREAKING_CHANGE_FIELD_CHANGED_KIND = 'FIELD_CHANGED_KIND';
const BREAKING_CHANGE_FIELD_REMOVED = 'FIELD_REMOVED'; const BREAKING_CHANGE_FIELD_REMOVED = 'FIELD_REMOVED';
const BREAKING_CHANGE_TYPE_CHANGED = 'TYPE_CHANGED_KIND'; const BREAKING_CHANGE_TYPE_CHANGED_KIND = 'TYPE_CHANGED_KIND';
const BREAKING_CHANGE_TYPE_REMOVED = 'TYPE_REMOVED'; const BREAKING_CHANGE_TYPE_REMOVED = 'TYPE_REMOVED';
const BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION = 'TYPE_REMOVED_FROM_UNION'; const BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION = 'TYPE_REMOVED_FROM_UNION';
const BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM = 'VALUE_REMOVED_FROM_ENUM'; const BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM = 'VALUE_REMOVED_FROM_ENUM';
const BREAKING_CHANGE_ARG_REMOVED = 'ARG_REMOVED'; const BREAKING_CHANGE_ARG_REMOVED = 'ARG_REMOVED';
const BREAKING_CHANGE_ARG_CHANGED = 'ARG_CHANGED_KIND'; const BREAKING_CHANGE_ARG_CHANGED_KIND = 'ARG_CHANGED_KIND';
const BREAKING_CHANGE_NON_NULL_ARG_ADDED = 'NON_NULL_ARG_ADDED'; const BREAKING_CHANGE_NON_NULL_ARG_ADDED = 'NON_NULL_ARG_ADDED';
const BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED = 'NON_NULL_INPUT_FIELD_ADDED'; const BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED = 'NON_NULL_INPUT_FIELD_ADDED';
const BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT = 'INTERFACE_REMOVED_FROM_OBJECT'; const BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT = 'INTERFACE_REMOVED_FROM_OBJECT';
const BREAKING_CHANGE_DIRECTIVE_REMOVED = 'DIRECTIVE_REMOVED';
const BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED = 'DIRECTIVE_ARG_REMOVED';
const BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED = 'DIRECTIVE_LOCATION_REMOVED';
const BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED = 'NON_NULL_DIRECTIVE_ARG_ADDED';
const DANGEROUS_CHANGE_ARG_DEFAULT_VALUE = 'ARG_DEFAULT_VALUE_CHANGE'; const DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED = 'ARG_DEFAULT_VALUE_CHANGE';
const DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM = 'VALUE_ADDED_TO_ENUM'; const DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM = 'VALUE_ADDED_TO_ENUM';
const DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT = 'INTERFACE_ADDED_TO_OBJECT';
const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION = 'TYPE_ADDED_TO_UNION'; const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION = 'TYPE_ADDED_TO_UNION';
const DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED = 'NULLABLE_INPUT_FIELD_ADDED';
/** const DANGEROUS_CHANGE_NULLABLE_ARG_ADDED = 'NULLABLE_ARG_ADDED';
* Given two schemas, returns an Array containing descriptions of all the types
* of potentially dangerous changes covered by the other functions down below.
*
* @return array
*/
public static function findDangerousChanges(Schema $oldSchema, Schema $newSchema)
{
return array_merge(self::findArgChanges($oldSchema, $newSchema)['dangerousChanges'],
self::findValuesAddedToEnums($oldSchema, $newSchema),
self::findTypesAddedToUnions($oldSchema, $newSchema)
);
}
/** /**
* Given two schemas, returns an Array containing descriptions of all the types * Given two schemas, returns an Array containing descriptions of all the types
@ -60,11 +55,33 @@ class FindBreakingChanges
return array_merge( return array_merge(
self::findRemovedTypes($oldSchema, $newSchema), self::findRemovedTypes($oldSchema, $newSchema),
self::findTypesThatChangedKind($oldSchema, $newSchema), self::findTypesThatChangedKind($oldSchema, $newSchema),
self::findFieldsThatChangedType($oldSchema, $newSchema), self::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema),
self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges'],
self::findTypesRemovedFromUnions($oldSchema, $newSchema), self::findTypesRemovedFromUnions($oldSchema, $newSchema),
self::findValuesRemovedFromEnums($oldSchema, $newSchema), self::findValuesRemovedFromEnums($oldSchema, $newSchema),
self::findArgChanges($oldSchema, $newSchema)['breakingChanges'], self::findArgChanges($oldSchema, $newSchema)['breakingChanges'],
self::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema) self::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema),
self::findRemovedDirectives($oldSchema, $newSchema),
self::findRemovedDirectiveArgs($oldSchema, $newSchema),
self::findAddedNonNullDirectiveArgs($oldSchema, $newSchema),
self::findRemovedDirectiveLocations($oldSchema, $newSchema)
);
}
/**
* Given two schemas, returns an Array containing descriptions of all the types
* of potentially dangerous changes covered by the other functions down below.
*
* @return array
*/
public static function findDangerousChanges(Schema $oldSchema, Schema $newSchema)
{
return array_merge(
self::findArgChanges($oldSchema, $newSchema)['dangerousChanges'],
self::findValuesAddedToEnums($oldSchema, $newSchema),
self::findInterfacesAddedToObjectTypes($oldSchema, $newSchema),
self::findTypesAddedToUnions($oldSchema, $newSchema),
self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['dangerousChanges']
); );
} }
@ -75,20 +92,21 @@ class FindBreakingChanges
* @return array * @return array
*/ */
public static function findRemovedTypes( public static function findRemovedTypes(
Schema $oldSchema, Schema $newSchema Schema $oldSchema,
) Schema $newSchema
{ ) {
$oldTypeMap = $oldSchema->getTypeMap(); $oldTypeMap = $oldSchema->getTypeMap();
$newTypeMap = $newSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap();
$breakingChanges = []; $breakingChanges = [];
foreach ($oldTypeMap as $typeName => $typeDefinition) { foreach (array_keys($oldTypeMap) as $typeName) {
if (!isset($newTypeMap[$typeName])) { if (!isset($newTypeMap[$typeName])) {
$breakingChanges[] = $breakingChanges[] = [
['type' => self::BREAKING_CHANGE_TYPE_REMOVED, 'description' => "${typeName} was removed."]; 'type' => self::BREAKING_CHANGE_TYPE_REMOVED,
'description' => "${typeName} was removed."
];
} }
} }
return $breakingChanges; return $breakingChanges;
} }
@ -99,28 +117,27 @@ class FindBreakingChanges
* @return array * @return array
*/ */
public static function findTypesThatChangedKind( public static function findTypesThatChangedKind(
Schema $oldSchema, Schema $newSchema Schema $oldSchema,
) Schema $newSchema
{ ) {
$oldTypeMap = $oldSchema->getTypeMap(); $oldTypeMap = $oldSchema->getTypeMap();
$newTypeMap = $newSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap();
$breakingChanges = []; $breakingChanges = [];
foreach ($oldTypeMap as $typeName => $typeDefinition) { foreach ($oldTypeMap as $typeName => $oldType) {
if (!isset($newTypeMap[$typeName])) { if (!isset($newTypeMap[$typeName])) {
continue; continue;
} }
$newTypeDefinition = $newTypeMap[$typeName]; $newType = $newTypeMap[$typeName];
if (!($typeDefinition instanceof $newTypeDefinition)) { if (!($oldType instanceof $newType)) {
$oldTypeKindName = self::typeKindName($typeDefinition); $oldTypeKindName = self::typeKindName($oldType);
$newTypeKindName = self::typeKindName($newTypeDefinition); $newTypeKindName = self::typeKindName($newType);
$breakingChanges[] = [ $breakingChanges[] = [
'type' => self::BREAKING_CHANGE_TYPE_CHANGED, 'type' => self::BREAKING_CHANGE_TYPE_CHANGED_KIND,
'description' => "${typeName} changed from ${oldTypeKindName} to ${newTypeKindName}." 'description' => "${typeName} changed from ${oldTypeKindName} to ${newTypeKindName}."
]; ];
} }
} }
return $breakingChanges; return $breakingChanges;
} }
@ -133,56 +150,63 @@ class FindBreakingChanges
* @return array * @return array
*/ */
public static function findArgChanges( public static function findArgChanges(
Schema $oldSchema, Schema $newSchema Schema $oldSchema,
) Schema $newSchema
{ ) {
$oldTypeMap = $oldSchema->getTypeMap(); $oldTypeMap = $oldSchema->getTypeMap();
$newTypeMap = $newSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap();
$breakingChanges = []; $breakingChanges = [];
$dangerousChanges = []; $dangerousChanges = [];
foreach ($oldTypeMap as $oldTypeName => $oldTypeDefinition) {
$newTypeDefinition = isset($newTypeMap[$oldTypeName]) ? $newTypeMap[$oldTypeName] : null; foreach ($oldTypeMap as $typeName => $oldType) {
if (!($oldTypeDefinition instanceof ObjectType || $oldTypeDefinition instanceof InterfaceType) || $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
!($newTypeDefinition instanceof $oldTypeDefinition)) { if (
!($oldType instanceof ObjectType || $oldType instanceof InterfaceType) ||
!($newType instanceof ObjectType || $newType instanceof InterfaceType) ||
!($newType instanceof $oldType)
) {
continue; continue;
} }
$oldTypeFields = $oldTypeDefinition->getFields(); $oldTypeFields = $oldType->getFields();
$newTypeFields = $newTypeDefinition->getFields(); $newTypeFields = $newType->getFields();
foreach ($oldTypeFields as $fieldName => $fieldDefinition) { foreach ($oldTypeFields as $fieldName => $oldField) {
if (!isset($newTypeFields[$fieldName])) { if (!isset($newTypeFields[$fieldName])) {
continue; continue;
} }
foreach ($fieldDefinition->args as $oldArgDef) { foreach ($oldField->args as $oldArgDef) {
$newArgs = $newTypeFields[$fieldName]->args; $newArgs = $newTypeFields[$fieldName]->args;
$newArgDef = Utils::find( $newArgDef = Utils::find(
$newArgs, function ($arg) use ($oldArgDef) { $newArgs,
function ($arg) use ($oldArgDef) {
return $arg->name === $oldArgDef->name; return $arg->name === $oldArgDef->name;
} }
); );
if (!$newArgDef) { if (!$newArgDef) {
$argName = $oldArgDef->name;
$breakingChanges[] = [ $breakingChanges[] = [
'type' => self::BREAKING_CHANGE_ARG_REMOVED, 'type' => self::BREAKING_CHANGE_ARG_REMOVED,
'description' => "${oldTypeName}->${fieldName} arg ${argName} was removed" 'description' => "${typeName}.${fieldName} arg {$oldArgDef->name} was removed"
]; ];
} else { } else {
$isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg($oldArgDef->getType(), $newArgDef->getType()); $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg(
$oldArgDef->getType(),
$newArgDef->getType()
);
$oldArgType = $oldArgDef->getType(); $oldArgType = $oldArgDef->getType();
$oldArgName = $oldArgDef->name; $oldArgName = $oldArgDef->name;
if (!$isSafe) { if (!$isSafe) {
$newArgType = $newArgDef->getType(); $newArgType = $newArgDef->getType();
$breakingChanges[] = [ $breakingChanges[] = [
'type' => self::BREAKING_CHANGE_ARG_CHANGED, 'type' => self::BREAKING_CHANGE_ARG_CHANGED_KIND,
'description' => "${oldTypeName}->${fieldName} arg ${oldArgName} has changed type from ${oldArgType} to ${newArgType}." 'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed type from ${oldArgType} to ${newArgType}"
]; ];
} elseif ($oldArgDef->defaultValueExists() && $oldArgDef->defaultValue !== $newArgDef->defaultValue) { } elseif ($oldArgDef->defaultValueExists() && $oldArgDef->defaultValue !== $newArgDef->defaultValue) {
$dangerousChanges[] = [ $dangerousChanges[] = [
'type' => FindBreakingChanges::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE, 'type' => FindBreakingChanges::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED,
'description' => "${oldTypeName}->${fieldName} arg ${oldArgName} has changed defaultValue" 'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed defaultValue"
]; ];
} }
} }
@ -190,25 +214,36 @@ class FindBreakingChanges
foreach ($newTypeFields[$fieldName]->args as $newArgDef) { foreach ($newTypeFields[$fieldName]->args as $newArgDef) {
$oldArgs = $oldTypeFields[$fieldName]->args; $oldArgs = $oldTypeFields[$fieldName]->args;
$oldArgDef = Utils::find( $oldArgDef = Utils::find(
$oldArgs, function ($arg) use ($newArgDef) { $oldArgs,
function ($arg) use ($newArgDef) {
return $arg->name === $newArgDef->name; return $arg->name === $newArgDef->name;
} }
); );
if (!$oldArgDef && $newArgDef->getType() instanceof NonNull) { if (!$oldArgDef) {
$newTypeName = $newTypeDefinition->name; $newTypeName = $newType->name;
$newArgName = $newArgDef->name; $newArgName = $newArgDef->name;
if ($newArgDef->getType() instanceof NonNull) {
$breakingChanges[] = [ $breakingChanges[] = [
'type' => self::BREAKING_CHANGE_NON_NULL_ARG_ADDED, 'type' => self::BREAKING_CHANGE_NON_NULL_ARG_ADDED,
'description' => "A non-null arg ${newArgName} on ${newTypeName}->${fieldName} was added." 'description' => "A non-null arg ${newArgName} on ${newTypeName}.${fieldName} was added"
];
} else {
$dangerousChanges[] = [
'type' => self::DANGEROUS_CHANGE_NULLABLE_ARG_ADDED,
'description' => "A nullable arg ${newArgName} on ${newTypeName}.${fieldName} was added"
]; ];
} }
} }
} }
} }
} }
}
return ['breakingChanges' => $breakingChanges, 'dangerousChanges' => $dangerousChanges]; return [
'breakingChanges' => $breakingChanges,
'dangerousChanges' => $dangerousChanges,
];
} }
/** /**
@ -236,155 +271,188 @@ class FindBreakingChanges
throw new \TypeError('unknown type ' . $type->name); throw new \TypeError('unknown type ' . $type->name);
} }
/** public static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(
* Given two schemas, returns an Array containing descriptions of any breaking Schema $oldSchema,
* changes in the newSchema related to the fields on a type. This includes if Schema $newSchema
* a field has been removed from a type, if a field has changed type, or if ) {
* a non-null field is added to an input type.
*
* @return array
*/
public static function findFieldsThatChangedType(
Schema $oldSchema, Schema $newSchema
)
{
return array_merge(
self::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema),
self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)
);
}
/**
* @param Schema $oldSchema
* @param Schema $newSchema
*
* @return array
*/
private static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(Schema $oldSchema, Schema $newSchema)
{
$oldTypeMap = $oldSchema->getTypeMap(); $oldTypeMap = $oldSchema->getTypeMap();
$newTypeMap = $newSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap();
$breakingFieldChanges = []; $breakingChanges = [];
foreach ($oldTypeMap as $typeName => $oldType) { foreach ($oldTypeMap as $typeName => $oldType) {
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
if (!($oldType instanceof ObjectType || $oldType instanceof InterfaceType) || !($newType instanceof $oldType)) { if (
!($oldType instanceof ObjectType || $oldType instanceof InterfaceType) ||
!($newType instanceof ObjectType || $newType instanceof InterfaceType) ||
!($newType instanceof $oldType)
) {
continue; continue;
} }
$oldTypeFieldsDef = $oldType->getFields(); $oldTypeFieldsDef = $oldType->getFields();
$newTypeFieldsDef = $newType->getFields(); $newTypeFieldsDef = $newType->getFields();
foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) { foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) {
// Check if the field is missing on the type in the new schema.
if (!isset($newTypeFieldsDef[$fieldName])) { if (!isset($newTypeFieldsDef[$fieldName])) {
$breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_REMOVED, 'description' => "${typeName}->${fieldName} was removed."]; $breakingChanges[] = [
'type' => self::BREAKING_CHANGE_FIELD_REMOVED,
'description' => "${typeName}.${fieldName} was removed."
];
} else { } else {
$oldFieldType = $oldTypeFieldsDef[$fieldName]->getType(); $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
$newfieldType = $newTypeFieldsDef[$fieldName]->getType(); $newFieldType = $newTypeFieldsDef[$fieldName]->getType();
$isSafe = self::isChangeSafeForObjectOrInterfaceField($oldFieldType, $newfieldType); $isSafe = self::isChangeSafeForObjectOrInterfaceField(
$oldFieldType,
$newFieldType
);
if (!$isSafe) { if (!$isSafe) {
$oldFieldTypeString = $oldFieldType instanceof NamedType
$oldFieldTypeString = self::isNamedType($oldFieldType) ? $oldFieldType->name : $oldFieldType; ? $oldFieldType->name
$newFieldTypeString = self::isNamedType($newfieldType) ? $newfieldType->name : $newfieldType; : $oldFieldType;
$breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_CHANGED, 'description' => "${typeName}->${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."]; $newFieldTypeString = $newFieldType instanceof NamedType
? $newFieldType->name
: $newFieldType;
$breakingChanges[] = [
'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."
];
} }
} }
} }
} }
return $breakingFieldChanges; return $breakingChanges;
} }
/**
* @param Schema $oldSchema
* @param Schema $newSchema
*
* @return array
*/
public static function findFieldsThatChangedTypeOnInputObjectTypes( public static function findFieldsThatChangedTypeOnInputObjectTypes(
Schema $oldSchema, Schema $newSchema Schema $oldSchema,
) Schema $newSchema
{ ) {
$oldTypeMap = $oldSchema->getTypeMap(); $oldTypeMap = $oldSchema->getTypeMap();
$newTypeMap = $newSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap();
$breakingFieldChanges = []; $breakingChanges = [];
$dangerousChanges = [];
foreach ($oldTypeMap as $typeName => $oldType) { foreach ($oldTypeMap as $typeName => $oldType) {
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
if (!($oldType instanceof InputObjectType) || !($newType instanceof InputObjectType)) { if (!($oldType instanceof InputObjectType) || !($newType instanceof InputObjectType)) {
continue; continue;
} }
$oldTypeFieldsDef = $oldType->getFields(); $oldTypeFieldsDef = $oldType->getFields();
$newTypeFieldsDef = $newType->getFields(); $newTypeFieldsDef = $newType->getFields();
foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) { foreach (array_keys($oldTypeFieldsDef) as $fieldName) {
if (!isset($newTypeFieldsDef[$fieldName])) { if (!isset($newTypeFieldsDef[$fieldName])) {
$breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_REMOVED, 'description' => "${typeName}->${fieldName} was removed."]; $breakingChanges[] = [
'type' => self::BREAKING_CHANGE_FIELD_REMOVED,
'description' => "${typeName}.${fieldName} was removed."
];
} else { } else {
$oldFieldType = $oldTypeFieldsDef[$fieldName]->getType(); $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
$newfieldType = $newTypeFieldsDef[$fieldName]->getType(); $newFieldType = $newTypeFieldsDef[$fieldName]->getType();
$isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg($oldFieldType, $newfieldType);
$isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg(
$oldFieldType,
$newFieldType
);
if (!$isSafe) { if (!$isSafe) {
$oldFieldTypeString = self::isNamedType($oldFieldType) ? $oldFieldType->name : $oldFieldType; $oldFieldTypeString = $oldFieldType instanceof NamedType
$newFieldTypeString = self::isNamedType($newfieldType) ? $newfieldType->name : $newfieldType; ? $oldFieldType->name
$breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_CHANGED, 'description' => "${typeName}->${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."]; : $oldFieldType;
$newFieldTypeString = $newFieldType instanceof NamedType
? $newFieldType->name
: $newFieldType;
$breakingChanges[] = [
'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."];
} }
} }
} }
// Check if a field was added to the input object type
foreach ($newTypeFieldsDef as $fieldName => $fieldDef) { foreach ($newTypeFieldsDef as $fieldName => $fieldDef) {
if (!isset($oldTypeFieldsDef[$fieldName]) && $fieldDef->getType() instanceof NonNull) { if (!isset($oldTypeFieldsDef[$fieldName])) {
$newTypeName = $newType->name; $newTypeName = $newType->name;
$breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED, 'description' => "A non-null field ${fieldName} on input type ${newTypeName} was added."]; if ($fieldDef->getType() instanceof NonNull) {
$breakingChanges[] = [
'type' => self::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED,
'description' => "A non-null field ${fieldName} on input type ${newTypeName} was added."
];
} else {
$dangerousChanges[] = [
'type' => self::DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED,
'description' => "A nullable field ${fieldName} on input type ${newTypeName} was added."
];
} }
} }
} }
return $breakingFieldChanges; }
return [
'breakingChanges' => $breakingChanges,
'dangerousChanges' => $dangerousChanges,
];
} }
private static function isChangeSafeForObjectOrInterfaceField( private static function isChangeSafeForObjectOrInterfaceField(
Type $oldType, Type $newType Type $oldType,
) Type $newType
{ ) {
if (self::isNamedType($oldType)) { if ($oldType instanceof NamedType) {
return (
// if they're both named types, see if their names are equivalent // if they're both named types, see if their names are equivalent
return (self::isNamedType($newType) && $oldType->name === $newType->name) ($newType instanceof NamedType && $oldType->name === $newType->name) ||
// moving from nullable to non-null of the same underlying type is safe // moving from nullable to non-null of the same underlying type is safe
|| ($newType instanceof NonNull ($newType instanceof NonNull &&
&& self::isChangeSafeForObjectOrInterfaceField( self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType())
$oldType, $newType->getWrappedType() )
)); );
} elseif ($oldType instanceof ListOfType) { } elseif ($oldType instanceof ListOfType) {
return (
// if they're both lists, make sure the underlying types are compatible // if they're both lists, make sure the underlying types are compatible
return ($newType instanceof ListOfType && ($newType instanceof ListOfType &&
self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType())) || self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType())) ||
// moving from nullable to non-null of the same underlying type is safe // moving from nullable to non-null of the same underlying type is safe
($newType instanceof NonNull && ($newType instanceof NonNull &&
self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType())); self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType()))
);
} elseif ($oldType instanceof NonNull) { } elseif ($oldType instanceof NonNull) {
// if they're both non-null, make sure the underlying types are compatible // if they're both non-null, make sure the underlying types are compatible
return $newType instanceof NonNull && return (
self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType()); $newType instanceof NonNull &&
self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType())
);
} }
return false; return false;
} }
/** /**
* @param Type $oldType * @param Type $oldType
* @param Schema $newSchema * @param Type $newType
* *
* @return bool * @return bool
*/ */
private static function isChangeSafeForInputObjectFieldOrFieldArg( private static function isChangeSafeForInputObjectFieldOrFieldArg(
Type $oldType, Type $newType Type $oldType,
) Type $newType
{ ) {
if (self::isNamedType($oldType)) { if ($oldType instanceof NamedType) {
return self::isNamedType($newType) && $oldType->name === $newType->name; // if they're both named types, see if their names are equivalent
return $newType instanceof NamedType && $oldType->name === $newType->name;
} elseif ($oldType instanceof ListOfType) { } elseif ($oldType instanceof ListOfType) {
// if they're both lists, make sure the underlying types are compatible
return $newType instanceof ListOfType && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType->getWrappedType()); return $newType instanceof ListOfType && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType->getWrappedType());
} elseif ($oldType instanceof NonNull) { } elseif ($oldType instanceof NonNull) {
return ( return (
$newType instanceof NonNull && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType->getWrappedType()) // if they're both non-null, make sure the underlying types are
) || ( // compatible
!($newType instanceof NonNull) && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType) ($newType instanceof NonNull &&
self::isChangeSafeForInputObjectFieldOrFieldArg(
$oldType->getWrappedType(),
$newType->getWrappedType()
)) ||
// moving from non-null to nullable of the same underlying type is safe
(!($newType instanceof NonNull) &&
self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType))
); );
} }
return false; return false;
@ -397,9 +465,9 @@ class FindBreakingChanges
* @return array * @return array
*/ */
public static function findTypesRemovedFromUnions( public static function findTypesRemovedFromUnions(
Schema $oldSchema, Schema $newSchema Schema $oldSchema,
) Schema $newSchema
{ ) {
$oldTypeMap = $oldSchema->getTypeMap(); $oldTypeMap = $oldSchema->getTypeMap();
$newTypeMap = $newSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap();
@ -415,8 +483,10 @@ class FindBreakingChanges
} }
foreach ($oldType->getTypes() as $type) { foreach ($oldType->getTypes() as $type) {
if (!isset($typeNamesInNewUnion[$type->name])) { if (!isset($typeNamesInNewUnion[$type->name])) {
$missingTypeName = $type->name; $typesRemovedFromUnion[] = [
$typesRemovedFromUnion[] = ['type' => self::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION, 'description' => "${missingTypeName} was removed from union type ${typeName}."]; 'type' => self::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION,
'description' => "{$type->name} was removed from union type ${typeName}.",
];
} }
} }
} }
@ -430,14 +500,13 @@ class FindBreakingChanges
* @return array * @return array
*/ */
public static function findTypesAddedToUnions( public static function findTypesAddedToUnions(
Schema $oldSchema, Schema $newSchema Schema $oldSchema,
) Schema $newSchema
{ ) {
$oldTypeMap = $oldSchema->getTypeMap(); $oldTypeMap = $oldSchema->getTypeMap();
$newTypeMap = $newSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap();
$typesAddedToUnion = []; $typesAddedToUnion = [];
foreach ($newTypeMap as $typeName => $newType) { foreach ($newTypeMap as $typeName => $newType) {
$oldType = isset($oldTypeMap[$typeName]) ? $oldTypeMap[$typeName] : null; $oldType = isset($oldTypeMap[$typeName]) ? $oldTypeMap[$typeName] : null;
if (!($oldType instanceof UnionType) || !($newType instanceof UnionType)) { if (!($oldType instanceof UnionType) || !($newType instanceof UnionType)) {
@ -450,12 +519,13 @@ class FindBreakingChanges
} }
foreach ($newType->getTypes() as $type) { foreach ($newType->getTypes() as $type) {
if (!isset($typeNamesInOldUnion[$type->name])) { if (!isset($typeNamesInOldUnion[$type->name])) {
$addedTypeName = $type->name; $typesAddedToUnion[] = [
$typesAddedToUnion[] = ['type' => self::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION, 'description' => "${addedTypeName} was added to union type ${typeName}"]; 'type' => self::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION,
'description' => "{$type->name} was added to union type ${typeName}.",
];
} }
} }
} }
return $typesAddedToUnion; return $typesAddedToUnion;
} }
@ -466,14 +536,13 @@ class FindBreakingChanges
* @return array * @return array
*/ */
public static function findValuesRemovedFromEnums( public static function findValuesRemovedFromEnums(
Schema $oldSchema, Schema $newSchema Schema $oldSchema,
) Schema $newSchema
{ ) {
$oldTypeMap = $oldSchema->getTypeMap(); $oldTypeMap = $oldSchema->getTypeMap();
$newTypeMap = $newSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap();
$valuesRemovedFromEnums = []; $valuesRemovedFromEnums = [];
foreach ($oldTypeMap as $typeName => $oldType) { foreach ($oldTypeMap as $typeName => $oldType) {
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
if (!($oldType instanceof EnumType) || !($newType instanceof EnumType)) { if (!($oldType instanceof EnumType) || !($newType instanceof EnumType)) {
@ -485,12 +554,13 @@ class FindBreakingChanges
} }
foreach ($oldType->getValues() as $value) { foreach ($oldType->getValues() as $value) {
if (!isset($valuesInNewEnum[$value->name])) { if (!isset($valuesInNewEnum[$value->name])) {
$valueName = $value->name; $valuesRemovedFromEnums[] = [
$valuesRemovedFromEnums[] = ['type' => self::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM, 'description' => "${valueName} was removed from enum type ${typeName}."]; 'type' => self::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM,
'description' => "{$value->name} was removed from enum type ${typeName}.",
];
} }
} }
} }
return $valuesRemovedFromEnums; return $valuesRemovedFromEnums;
} }
@ -501,9 +571,9 @@ class FindBreakingChanges
* @return array * @return array
*/ */
public static function findValuesAddedToEnums( public static function findValuesAddedToEnums(
Schema $oldSchema, Schema $newSchema Schema $oldSchema,
) Schema $newSchema
{ ) {
$oldTypeMap = $oldSchema->getTypeMap(); $oldTypeMap = $oldSchema->getTypeMap();
$newTypeMap = $newSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap();
@ -519,12 +589,13 @@ class FindBreakingChanges
} }
foreach ($newType->getValues() as $value) { foreach ($newType->getValues() as $value) {
if (!isset($valuesInOldEnum[$value->name])) { if (!isset($valuesInOldEnum[$value->name])) {
$valueName = $value->name; $valuesAddedToEnums[] = [
$valuesAddedToEnums[] = ['type' => self::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM, 'description' => "${valueName} was added to enum type ${typeName}"]; 'type' => self::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM,
'description' => "{$value->name} was added to enum type ${typeName}.",
];
} }
} }
} }
return $valuesAddedToEnums; return $valuesAddedToEnums;
} }
@ -535,13 +606,13 @@ class FindBreakingChanges
* @return array * @return array
*/ */
public static function findInterfacesRemovedFromObjectTypes( public static function findInterfacesRemovedFromObjectTypes(
Schema $oldSchema, Schema $newSchema Schema $oldSchema,
) Schema $newSchema
{ ) {
$oldTypeMap = $oldSchema->getTypeMap(); $oldTypeMap = $oldSchema->getTypeMap();
$newTypeMap = $newSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap();
$breakingChanges = []; $breakingChanges = [];
foreach ($oldTypeMap as $typeName => $oldType) { foreach ($oldTypeMap as $typeName => $oldType) {
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
if (!($oldType instanceof ObjectType) || !($newType instanceof ObjectType)) { if (!($oldType instanceof ObjectType) || !($newType instanceof ObjectType)) {
@ -554,9 +625,9 @@ class FindBreakingChanges
if (!Utils::find($newInterfaces, function (InterfaceType $interface) use ($oldInterface) { if (!Utils::find($newInterfaces, function (InterfaceType $interface) use ($oldInterface) {
return $interface->name === $oldInterface->name; return $interface->name === $oldInterface->name;
})) { })) {
$oldInterfaceName = $oldInterface->name; $breakingChanges[] = [
$breakingChanges[] = ['type' => self::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT, 'type' => self::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT,
'description' => "${typeName} no longer implements interface ${oldInterfaceName}." 'description' => "${typeName} no longer implements interface {$oldInterface->name}."
]; ];
} }
} }
@ -565,19 +636,170 @@ class FindBreakingChanges
} }
/** /**
* @param Type $type * @param Schema $oldSchema
* @param Schema $newSchema
* *
* @return bool * @return array
*/ */
private static function isNamedType(Type $type) public static function findInterfacesAddedToObjectTypes(
Schema $oldSchema,
Schema $newSchema
) {
$oldTypeMap = $oldSchema->getTypeMap();
$newTypeMap = $newSchema->getTypeMap();
$interfacesAddedToObjectTypes = [];
foreach ($newTypeMap as $typeName => $newType) {
$oldType = isset($oldTypeMap[$typeName]) ? $oldTypeMap[$typeName] : null;
if (!($oldType instanceof ObjectType) || !($newType instanceof ObjectType)) {
continue;
}
$oldInterfaces = $oldType->getInterfaces();
$newInterfaces = $newType->getInterfaces();
foreach ($newInterfaces as $newInterface) {
if (!Utils::find($oldInterfaces, function (InterfaceType $interface) use ($newInterface) {
return $interface->name === $newInterface->name;
})) {
$interfacesAddedToObjectTypes[] = [
'type' => self::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT,
'description' => "{$newInterface->name} added to interfaces implemented by {$typeName}.",
];
}
}
}
return $interfacesAddedToObjectTypes;
}
public static function findRemovedDirectives(Schema $oldSchema, Schema $newSchema)
{ {
return ( $removedDirectives = [];
$type instanceof ScalarType ||
$type instanceof ObjectType || $newSchemaDirectiveMap = self::getDirectiveMapForSchema($newSchema);
$type instanceof InterfaceType || foreach($oldSchema->getDirectives() as $directive) {
$type instanceof UnionType || if (!isset($newSchemaDirectiveMap[$directive->name])) {
$type instanceof EnumType || $removedDirectives[] = [
$type instanceof InputObjectType 'type' => self::BREAKING_CHANGE_DIRECTIVE_REMOVED,
); 'description' => "{$directive->name} was removed",
];
}
}
return $removedDirectives;
}
public static function findRemovedArgsForDirectives(Directive $oldDirective, Directive $newDirective)
{
$removedArgs = [];
$newArgMap = self::getArgumentMapForDirective($newDirective);
foreach((array) $oldDirective->args as $arg) {
if (!isset($newArgMap[$arg->name])) {
$removedArgs[] = $arg;
}
}
return $removedArgs;
}
public static function findRemovedDirectiveArgs(Schema $oldSchema, Schema $newSchema)
{
$removedDirectiveArgs = [];
$oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
foreach($newSchema->getDirectives() as $newDirective) {
if (!isset($oldSchemaDirectiveMap[$newDirective->name])) {
continue;
}
foreach(self::findRemovedArgsForDirectives($oldSchemaDirectiveMap[$newDirective->name], $newDirective) as $arg) {
$removedDirectiveArgs[] = [
'type' => self::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED,
'description' => "{$arg->name} was removed from {$newDirective->name}",
];
}
}
return $removedDirectiveArgs;
}
public static function findAddedArgsForDirective(Directive $oldDirective, Directive $newDirective)
{
$addedArgs = [];
$oldArgMap = self::getArgumentMapForDirective($oldDirective);
foreach((array) $newDirective->args as $arg) {
if (!isset($oldArgMap[$arg->name])) {
$addedArgs[] = $arg;
}
}
return $addedArgs;
}
public static function findAddedNonNullDirectiveArgs(Schema $oldSchema, Schema $newSchema)
{
$addedNonNullableArgs = [];
$oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
foreach($newSchema->getDirectives() as $newDirective) {
if (!isset($oldSchemaDirectiveMap[$newDirective->name])) {
continue;
}
foreach(self::findAddedArgsForDirective($oldSchemaDirectiveMap[$newDirective->name], $newDirective) as $arg) {
if (!$arg->getType() instanceof NonNull) {
continue;
}
$addedNonNullableArgs[] = [
'type' => self::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED,
'description' => "A non-null arg {$arg->name} on directive {$newDirective->name} was added",
];
}
}
return $addedNonNullableArgs;
}
public static function findRemovedLocationsForDirective(Directive $oldDirective, Directive $newDirective)
{
$removedLocations = [];
$newLocationSet = array_flip($newDirective->locations);
foreach($oldDirective->locations as $oldLocation) {
if (!array_key_exists($oldLocation, $newLocationSet)) {
$removedLocations[] = $oldLocation;
}
}
return $removedLocations;
}
public static function findRemovedDirectiveLocations(Schema $oldSchema, Schema $newSchema)
{
$removedLocations = [];
$oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
foreach($newSchema->getDirectives() as $newDirective) {
if (!isset($oldSchemaDirectiveMap[$newDirective->name])) {
continue;
}
foreach(self::findRemovedLocationsForDirective($oldSchemaDirectiveMap[$newDirective->name], $newDirective) as $location) {
$removedLocations[] = [
'type' => self::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED,
'description' => "{$location} was removed from {$newDirective->name}",
];
}
}
return $removedLocations;
}
private static function getDirectiveMapForSchema(Schema $schema)
{
return Utils::keyMap($schema->getDirectives(), function ($dir) { return $dir->name; });
}
private static function getArgumentMapForDirective(Directive $directive)
{
return Utils::keyMap($directive->args ?: [], function ($arg) { return $arg->name; });
} }
} }

View File

@ -2,86 +2,65 @@
namespace GraphQL\Utils; namespace GraphQL\Utils;
/** /**
* Class PairSet * A way to keep track of pairs of things when the ordering of the pair does
* @package GraphQL\Utils * not matter. We do this by maintaining a sort of double adjacency sets.
*/ */
class PairSet class PairSet
{ {
/**
* @var \SplObjectStorage<any, Set<any>>
*/
private $data;
/** /**
* @var array * @var array
*/ */
private $wrappers = []; private $data;
/** /**
* PairSet constructor. * PairSet constructor.
*/ */
public function __construct() public function __construct()
{ {
$this->data = new \SplObjectStorage(); // SplObject hash instead? $this->data = [];
} }
/** /**
* @param $a * @param string $a
* @param $b * @param string $b
* @return null|object * @param bool $areMutuallyExclusive
* @return bool
*/ */
public function has($a, $b) public function has($a, $b, $areMutuallyExclusive)
{ {
$a = $this->toObj($a);
$b = $this->toObj($b);
/** @var \SplObjectStorage $first */
$first = isset($this->data[$a]) ? $this->data[$a] : null; $first = isset($this->data[$a]) ? $this->data[$a] : null;
return isset($first, $first[$b]) ? $first[$b] : null; $result = ($first && isset($first[$b])) ? $first[$b] : null;
if ($result === null) {
return false;
}
// areMutuallyExclusive being false is a superset of being true,
// hence if we want to know if this PairSet "has" these two with no
// exclusivity, we have to ensure it was added as such.
if ($areMutuallyExclusive === false) {
return $result === false;
}
return true;
} }
/** /**
* @param $a * @param string $a
* @param $b * @param string $b
* @param bool $areMutuallyExclusive
*/ */
public function add($a, $b) public function add($a, $b, $areMutuallyExclusive)
{ {
$this->pairSetAdd($a, $b); $this->pairSetAdd($a, $b, $areMutuallyExclusive);
$this->pairSetAdd($b, $a); $this->pairSetAdd($b, $a, $areMutuallyExclusive);
} }
/** /**
* @param $var * @param string $a
* @return mixed * @param string $b
* @param bool $areMutuallyExclusive
*/ */
private function toObj($var) private function pairSetAdd($a, $b, $areMutuallyExclusive)
{ {
// SplObjectStorage expects objects, so wrapping non-objects to objects $this->data[$a] = isset($this->data[$a]) ? $this->data[$a] : [];
if (is_object($var)) { $this->data[$a][$b] = $areMutuallyExclusive;
return $var;
}
if (!isset($this->wrappers[$var])) {
$tmp = new \stdClass();
$tmp->_internal = $var;
$this->wrappers[$var] = $tmp;
}
return $this->wrappers[$var];
}
/**
* @param $a
* @param $b
*/
private function pairSetAdd($a, $b)
{
$a = $this->toObj($a);
$b = $this->toObj($b);
$set = isset($this->data[$a]) ? $this->data[$a] : null;
if (!isset($set)) {
$set = new \SplObjectStorage();
$this->data[$a] = $set;
}
$set[$b] = true;
} }
} }

View File

@ -1,9 +1,10 @@
<?php <?php
namespace GraphQL\Utils; namespace GraphQL\Utils;
use GraphQL\Error\Error;
use GraphQL\Language\Printer; use GraphQL\Language\Printer;
use GraphQL\Type\Introspection;
use GraphQL\Type\Schema; use GraphQL\Type\Schema;
use GraphQL\Type\Definition\CompositeType;
use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\InterfaceType;
@ -19,15 +20,26 @@ use GraphQL\Type\Definition\Directive;
class SchemaPrinter class SchemaPrinter
{ {
/** /**
* Accepts options as a second argument:
*
* - commentDescriptions:
* Provide true to use preceding comments as the description.
* @api * @api
* @param Schema $schema * @param Schema $schema
* @return string * @return string
*/ */
public static function doPrint(Schema $schema) public static function doPrint(Schema $schema, array $options = [])
{ {
return self::printFilteredSchema($schema, function($n) { return self::printFilteredSchema(
return !self::isSpecDirective($n); $schema,
}, 'self::isDefinedType'); function($type) {
return !Directive::isSpecifiedDirective($type);
},
function ($type) {
return !Type::isBuiltInType($type);
},
$options
);
} }
/** /**
@ -35,55 +47,29 @@ class SchemaPrinter
* @param Schema $schema * @param Schema $schema
* @return string * @return string
*/ */
public static function printIntrosepctionSchema(Schema $schema) public static function printIntrosepctionSchema(Schema $schema, array $options = [])
{ {
return self::printFilteredSchema($schema, [__CLASS__, 'isSpecDirective'], [__CLASS__, 'isIntrospectionType']); return self::printFilteredSchema(
} $schema,
[Directive::class, 'isSpecifiedDirective'],
private static function isSpecDirective($directiveName) [Introspection::class, 'isIntrospectionType'],
{ $options
return (
$directiveName === 'skip' ||
$directiveName === 'include' ||
$directiveName === 'deprecated'
); );
} }
private static function isDefinedType($typename) private static function printFilteredSchema(Schema $schema, $directiveFilter, $typeFilter, $options)
{
return !self::isIntrospectionType($typename) && !self::isBuiltInScalar($typename);
}
private static function isIntrospectionType($typename)
{
return strpos($typename, '__') === 0;
}
private static function isBuiltInScalar($typename)
{
return (
$typename === Type::STRING ||
$typename === Type::BOOLEAN ||
$typename === Type::INT ||
$typename === Type::FLOAT ||
$typename === Type::ID
);
}
private static function printFilteredSchema(Schema $schema, $directiveFilter, $typeFilter)
{ {
$directives = array_filter($schema->getDirectives(), function($directive) use ($directiveFilter) { $directives = array_filter($schema->getDirectives(), function($directive) use ($directiveFilter) {
return $directiveFilter($directive->name); return $directiveFilter($directive);
}); });
$typeMap = $schema->getTypeMap(); $types = $schema->getTypeMap();
$types = array_filter(array_keys($typeMap), $typeFilter); ksort($types);
sort($types); $types = array_filter($types, $typeFilter);
$types = array_map(function($typeName) use ($typeMap) { return $typeMap[$typeName]; }, $types);
return implode("\n\n", array_filter(array_merge( return implode("\n\n", array_filter(array_merge(
[self::printSchemaDefinition($schema)], [self::printSchemaDefinition($schema)],
array_map('self::printDirective', $directives), array_map(function($directive) use ($options) { return self::printDirective($directive, $options); }, $directives),
array_map('self::printType', $types) array_map(function($type) use ($options) { return self::printType($type, $options); }, $types)
))) . "\n"; ))) . "\n";
} }
@ -145,95 +131,97 @@ class SchemaPrinter
return true; return true;
} }
public static function printType(Type $type) public static function printType(Type $type, array $options = [])
{ {
if ($type instanceof ScalarType) { if ($type instanceof ScalarType) {
return self::printScalar($type); return self::printScalar($type, $options);
} else if ($type instanceof ObjectType) { } else if ($type instanceof ObjectType) {
return self::printObject($type); return self::printObject($type, $options);
} else if ($type instanceof InterfaceType) { } else if ($type instanceof InterfaceType) {
return self::printInterface($type); return self::printInterface($type, $options);
} else if ($type instanceof UnionType) { } else if ($type instanceof UnionType) {
return self::printUnion($type); return self::printUnion($type, $options);
} else if ($type instanceof EnumType) { } else if ($type instanceof EnumType) {
return self::printEnum($type); return self::printEnum($type, $options);
} } else if ($type instanceof InputObjectType) {
Utils::invariant($type instanceof InputObjectType); return self::printInputObject($type, $options);
return self::printInputObject($type);
} }
private static function printScalar(ScalarType $type) throw new Error('Unknown type: ' . Utils::printSafe($type) . '.');
}
private static function printScalar(ScalarType $type, array $options)
{ {
return self::printDescription($type) . "scalar {$type->name}"; return self::printDescription($options, $type) . "scalar {$type->name}";
} }
private static function printObject(ObjectType $type) private static function printObject(ObjectType $type, array $options)
{ {
$interfaces = $type->getInterfaces(); $interfaces = $type->getInterfaces();
$implementedInterfaces = !empty($interfaces) ? $implementedInterfaces = !empty($interfaces) ?
' implements ' . implode(', ', array_map(function($i) { ' implements ' . implode(', ', array_map(function($i) {
return $i->name; return $i->name;
}, $interfaces)) : ''; }, $interfaces)) : '';
return self::printDescription($type) . return self::printDescription($options, $type) .
"type {$type->name}$implementedInterfaces {\n" . "type {$type->name}$implementedInterfaces {\n" .
self::printFields($type) . "\n" . self::printFields($options, $type) . "\n" .
"}"; "}";
} }
private static function printInterface(InterfaceType $type) private static function printInterface(InterfaceType $type, array $options)
{ {
return self::printDescription($type) . return self::printDescription($options, $type) .
"interface {$type->name} {\n" . "interface {$type->name} {\n" .
self::printFields($type) . "\n" . self::printFields($options, $type) . "\n" .
"}"; "}";
} }
private static function printUnion(UnionType $type) private static function printUnion(UnionType $type, array $options)
{ {
return self::printDescription($type) . return self::printDescription($options, $type) .
"union {$type->name} = " . implode(" | ", $type->getTypes()); "union {$type->name} = " . implode(" | ", $type->getTypes());
} }
private static function printEnum(EnumType $type) private static function printEnum(EnumType $type, array $options)
{ {
return self::printDescription($type) . return self::printDescription($options, $type) .
"enum {$type->name} {\n" . "enum {$type->name} {\n" .
self::printEnumValues($type->getValues()) . "\n" . self::printEnumValues($type->getValues(), $options) . "\n" .
"}"; "}";
} }
private static function printEnumValues($values) private static function printEnumValues($values, $options)
{ {
return implode("\n", array_map(function($value, $i) { return implode("\n", array_map(function($value, $i) use ($options) {
return self::printDescription($value, ' ', !$i) . ' ' . return self::printDescription($options, $value, ' ', !$i) . ' ' .
$value->name . self::printDeprecated($value); $value->name . self::printDeprecated($value);
}, $values, array_keys($values))); }, $values, array_keys($values)));
} }
private static function printInputObject(InputObjectType $type) private static function printInputObject(InputObjectType $type, array $options)
{ {
$fields = array_values($type->getFields()); $fields = array_values($type->getFields());
return self::printDescription($type) . return self::printDescription($options, $type) .
"input {$type->name} {\n" . "input {$type->name} {\n" .
implode("\n", array_map(function($f, $i) { implode("\n", array_map(function($f, $i) use ($options) {
return self::printDescription($f, ' ', !$i) . ' ' . self::printInputValue($f); return self::printDescription($options, $f, ' ', !$i) . ' ' . self::printInputValue($f);
}, $fields, array_keys($fields))) . "\n" . }, $fields, array_keys($fields))) . "\n" .
"}"; "}";
} }
private static function printFields($type) private static function printFields($options, $type)
{ {
$fields = array_values($type->getFields()); $fields = array_values($type->getFields());
return implode("\n", array_map(function($f, $i) { return implode("\n", array_map(function($f, $i) use ($options) {
return self::printDescription($f, ' ', !$i) . ' ' . return self::printDescription($options, $f, ' ', !$i) . ' ' .
$f->name . self::printArgs($f->args, ' ') . ': ' . $f->name . self::printArgs($options, $f->args, ' ') . ': ' .
(string) $f->getType() . self::printDeprecated($f); (string) $f->getType() . self::printDeprecated($f);
}, $fields, array_keys($fields))); }, $fields, array_keys($fields)));
} }
private static function printArgs($args, $indentation = '') private static function printArgs($options, $args, $indentation = '')
{ {
if (count($args) === 0) { if (!$args) {
return ''; return '';
} }
@ -242,8 +230,8 @@ class SchemaPrinter
return '(' . implode(', ', array_map('self::printInputValue', $args)) . ')'; return '(' . implode(', ', array_map('self::printInputValue', $args)) . ')';
} }
return "(\n" . implode("\n", array_map(function($arg, $i) use ($indentation) { return "(\n" . implode("\n", array_map(function($arg, $i) use ($indentation, $options) {
return self::printDescription($arg, ' ' . $indentation, !$i) . ' ' . $indentation . return self::printDescription($options, $arg, ' ' . $indentation, !$i) . ' ' . $indentation .
self::printInputValue($arg); self::printInputValue($arg);
}, $args, array_keys($args))) . "\n" . $indentation . ')'; }, $args, array_keys($args))) . "\n" . $indentation . ')';
} }
@ -257,10 +245,10 @@ class SchemaPrinter
return $argDecl; return $argDecl;
} }
private static function printDirective($directive) private static function printDirective($directive, $options)
{ {
return self::printDescription($directive) . return self::printDescription($options, $directive) .
'directive @' . $directive->name . self::printArgs($directive->args) . 'directive @' . $directive->name . self::printArgs($options, $directive->args) .
' on ' . implode(' | ', $directive->locations); ' on ' . implode(' | ', $directive->locations);
} }
@ -277,34 +265,74 @@ class SchemaPrinter
Printer::doPrint(AST::astFromValue($reason, Type::string())) . ')'; Printer::doPrint(AST::astFromValue($reason, Type::string())) . ')';
} }
private static function printDescription($def, $indentation = '', $firstInBlock = true) private static function printDescription($options, $def, $indentation = '', $firstInBlock = true)
{ {
if (!$def->description) { if (!$def->description) {
return ''; return '';
} }
$lines = explode("\n", $def->description); $lines = self::descriptionLines($def->description, 120 - strlen($indentation));
if (isset($options['commentDescriptions'])) {
return self::printDescriptionWithComments($lines, $indentation, $firstInBlock);
}
$description = ($indentation && !$firstInBlock) ? "\n" : '';
if (count($lines) === 1 && mb_strlen($lines[0]) < 70) {
$description .= $indentation . '"""' . self::escapeQuote($lines[0]) . "\"\"\"\n";
return $description;
}
$description .= $indentation . "\"\"\"\n";
foreach ($lines as $line) {
$description .= $indentation . self::escapeQuote($line) . "\n";
}
$description .= $indentation . "\"\"\"\n";
return $description;
}
private static function escapeQuote($line)
{
return str_replace('"""', '\\"""', $line);
}
private static function printDescriptionWithComments($lines, $indentation, $firstInBlock)
{
$description = $indentation && !$firstInBlock ? "\n" : ''; $description = $indentation && !$firstInBlock ? "\n" : '';
foreach ($lines as $line) { foreach ($lines as $line) {
if ($line === '') { if ($line === '') {
$description .= $indentation . "#\n"; $description .= $indentation . "#\n";
} else { } else {
// For > 120 character long lines, cut at space boundaries into sublines $description .= $indentation . '# ' . $line . "\n";
// of ~80 chars.
$sublines = self::breakLine($line, 120 - strlen($indentation));
foreach ($sublines as $subline) {
$description .= $indentation . '# ' . $subline . "\n";
}
} }
} }
return $description; return $description;
} }
private static function breakLine($line, $len) private static function descriptionLines($description, $maxLen) {
$lines = [];
$rawLines = explode("\n", $description);
foreach($rawLines as $line) {
if ($line === '') {
$lines[] = $line;
} else {
// For > 120 character long lines, cut at space boundaries into sublines
// of ~80 chars.
$sublines = self::breakLine($line, $maxLen);
foreach ($sublines as $subline) {
$lines[] = $subline;
}
}
}
return $lines;
}
private static function breakLine($line, $maxLen)
{ {
if (strlen($line) < $len + 5) { if (strlen($line) < $maxLen + 5) {
return [$line]; return [$line];
} }
preg_match_all("/((?: |^).{15," . ($len - 40) . "}(?= |$))/", $line, $parts); preg_match_all("/((?: |^).{15," . ($maxLen - 40) . "}(?= |$))/", $line, $parts);
$parts = $parts[0]; $parts = $parts[0];
return array_map(function($part) { return array_map(function($part) {
return trim($part); return trim($part);

View File

@ -48,7 +48,7 @@ class TypeComparators
* @param Type $superType * @param Type $superType
* @return bool * @return bool
*/ */
static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType) static function isTypeSubTypeOf(Schema $schema, $maybeSubType, $superType)
{ {
// Equivalent type is a valid subtype // Equivalent type is a valid subtype
if ($maybeSubType === $superType) { if ($maybeSubType === $superType) {

View File

@ -19,7 +19,6 @@ use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Definition\UnionType;
@ -121,7 +120,7 @@ class TypeInfo
if ($type instanceof ObjectType) { if ($type instanceof ObjectType) {
$nestedTypes = array_merge($nestedTypes, $type->getInterfaces()); $nestedTypes = array_merge($nestedTypes, $type->getInterfaces());
} }
if ($type instanceof ObjectType || $type instanceof InterfaceType || $type instanceof InputObjectType) { if ($type instanceof ObjectType || $type instanceof InterfaceType) {
foreach ((array) $type->getFields() as $fieldName => $field) { foreach ((array) $type->getFields() as $fieldName => $field) {
if (!empty($field->args)) { if (!empty($field->args)) {
$fieldArgTypes = array_map(function(FieldArgument $arg) { return $arg->getType(); }, $field->args); $fieldArgTypes = array_map(function(FieldArgument $arg) { return $arg->getType(); }, $field->args);
@ -130,6 +129,11 @@ class TypeInfo
$nestedTypes[] = $field->getType(); $nestedTypes[] = $field->getType();
} }
} }
if ($type instanceof InputObjectType) {
foreach ((array) $type->getFields() as $fieldName => $field) {
$nestedTypes[] = $field->getType();
}
}
foreach ($nestedTypes as $type) { foreach ($nestedTypes as $type) {
$typeMap = self::extractTypes($type, $typeMap); $typeMap = self::extractTypes($type, $typeMap);
} }
@ -211,14 +215,26 @@ class TypeInfo
/** /**
* TypeInfo constructor. * TypeInfo constructor.
* @param Schema $schema * @param Schema $schema
* @param Type|null $initialType
*/ */
public function __construct(Schema $schema) public function __construct(Schema $schema, $initialType = null)
{ {
$this->schema = $schema; $this->schema = $schema;
$this->typeStack = []; $this->typeStack = [];
$this->parentTypeStack = []; $this->parentTypeStack = [];
$this->inputTypeStack = []; $this->inputTypeStack = [];
$this->fieldDefStack = []; $this->fieldDefStack = [];
if ($initialType) {
if (Type::isInputType($initialType)) {
$this->inputTypeStack[] = $initialType;
}
if (Type::isCompositeType($initialType)) {
$this->parentTypeStack[] = $initialType;
}
if (Type::isOutputType($initialType)) {
$this->typeStack[] = $initialType;
}
}
} }
/** /**
@ -233,7 +249,7 @@ class TypeInfo
} }
/** /**
* @return Type * @return CompositeType
*/ */
function getParentType() function getParentType()
{ {
@ -254,6 +270,17 @@ class TypeInfo
return null; return null;
} }
/**
* @return InputType|null
*/
public function getParentInputType()
{
$inputTypeStackLength = count($this->inputTypeStack);
if ($inputTypeStackLength > 1) {
return $this->inputTypeStack[$inputTypeStackLength - 2];
}
}
/** /**
* @return FieldDefinition * @return FieldDefinition
*/ */
@ -296,6 +323,10 @@ class TypeInfo
{ {
$schema = $this->schema; $schema = $this->schema;
// Note: many of the types below are explicitly typed as "mixed" to drop
// any assumptions of a valid schema to ensure runtime types are properly
// checked before continuing since TypeInfo is used as part of validation
// which occurs before guarantees of schema and document validity.
switch ($node->kind) { switch ($node->kind) {
case NodeKind::SELECTION_SET: case NodeKind::SELECTION_SET:
$namedType = Type::getNamedType($this->getType()); $namedType = Type::getNamedType($this->getType());
@ -308,8 +339,12 @@ class TypeInfo
if ($parentType) { if ($parentType) {
$fieldDef = self::getFieldDefinition($schema, $parentType, $node); $fieldDef = self::getFieldDefinition($schema, $parentType, $node);
} }
$this->fieldDefStack[] = $fieldDef; // push $fieldType = null;
$this->typeStack[] = $fieldDef ? $fieldDef->getType() : null; // push if ($fieldDef) {
$fieldType = $fieldDef->getType();
}
$this->fieldDefStack[] = $fieldDef;
$this->typeStack[] = Type::isOutputType($fieldType) ? $fieldType : null;
break; break;
case NodeKind::DIRECTIVE: case NodeKind::DIRECTIVE:
@ -325,14 +360,14 @@ class TypeInfo
} else if ($node->operation === 'subscription') { } else if ($node->operation === 'subscription') {
$type = $schema->getSubscriptionType(); $type = $schema->getSubscriptionType();
} }
$this->typeStack[] = $type; // push $this->typeStack[] = Type::isOutputType($type) ? $type : null;
break; break;
case NodeKind::INLINE_FRAGMENT: case NodeKind::INLINE_FRAGMENT:
case NodeKind::FRAGMENT_DEFINITION: case NodeKind::FRAGMENT_DEFINITION:
$typeConditionNode = $node->typeCondition; $typeConditionNode = $node->typeCondition;
$outputType = $typeConditionNode ? self::typeFromAST($schema, $typeConditionNode) : $this->getType(); $outputType = $typeConditionNode ? self::typeFromAST($schema, $typeConditionNode) : Type::getNamedType($this->getType());
$this->typeStack[] = Type::isOutputType($outputType) ? $outputType : null; // push $this->typeStack[] = Type::isOutputType($outputType) ? $outputType : null;
break; break;
case NodeKind::VARIABLE_DEFINITION: case NodeKind::VARIABLE_DEFINITION:
@ -350,23 +385,27 @@ class TypeInfo
} }
} }
$this->argument = $argDef; $this->argument = $argDef;
$this->inputTypeStack[] = $argType; // push $this->inputTypeStack[] = Type::isInputType($argType) ? $argType : null;
break; break;
case NodeKind::LST: case NodeKind::LST:
$listType = Type::getNullableType($this->getInputType()); $listType = Type::getNullableType($this->getInputType());
$this->inputTypeStack[] = ($listType instanceof ListOfType ? $listType->getWrappedType() : null); // push $itemType = $listType instanceof ListOfType
? $listType->getWrappedType()
: $listType;
$this->inputTypeStack[] = Type::isInputType($itemType) ? $itemType : null;
break; break;
case NodeKind::OBJECT_FIELD: case NodeKind::OBJECT_FIELD:
$objectType = Type::getNamedType($this->getInputType()); $objectType = Type::getNamedType($this->getInputType());
$fieldType = null; $fieldType = null;
$inputFieldType = null;
if ($objectType instanceof InputObjectType) { if ($objectType instanceof InputObjectType) {
$tmp = $objectType->getFields(); $tmp = $objectType->getFields();
$inputField = isset($tmp[$node->name->value]) ? $tmp[$node->name->value] : null; $inputField = isset($tmp[$node->name->value]) ? $tmp[$node->name->value] : null;
$fieldType = $inputField ? $inputField->getType() : null; $inputFieldType = $inputField ? $inputField->getType() : null;
} }
$this->inputTypeStack[] = $fieldType; $this->inputTypeStack[] = Type::isInputType($inputFieldType) ? $inputFieldType : null;
break; break;
case NodeKind::ENUM: case NodeKind::ENUM:

View File

@ -1,8 +1,10 @@
<?php <?php
namespace GraphQL\Utils; namespace GraphQL\Utils;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Error\Warning; use GraphQL\Error\Warning;
use GraphQL\Language\AST\Node;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\WrappingType; use GraphQL\Type\Definition\WrappingType;
use \Traversable, \InvalidArgumentException; use \Traversable, \InvalidArgumentException;
@ -15,12 +17,23 @@ class Utils
return $undefined ?: $undefined = new \stdClass(); return $undefined ?: $undefined = new \stdClass();
} }
/**
* Check if the value is invalid
*
* @param mixed $value
* @return bool
*/
public static function isInvalid($value)
{
return self::undefined() === $value;
}
/** /**
* @param object $obj * @param object $obj
* @param array $vars * @param array $vars
* @param array $requiredKeys * @param array $requiredKeys
* *
* @return array * @return object
*/ */
public static function assign($obj, array $vars, array $requiredKeys = []) public static function assign($obj, array $vars, array $requiredKeys = [])
{ {
@ -218,7 +231,7 @@ class Utils
* @param string $message * @param string $message
* @param mixed $sprintfParam1 * @param mixed $sprintfParam1
* @param mixed $sprintfParam2 ... * @param mixed $sprintfParam2 ...
* @throws InvariantViolation * @throws Error
*/ */
public static function invariant($test, $message = '') public static function invariant($test, $message = '')
{ {
@ -228,6 +241,7 @@ class Utils
array_shift($args); array_shift($args);
$message = call_user_func_array('sprintf', $args); $message = call_user_func_array('sprintf', $args);
} }
// TODO switch to Error here
throw new InvariantViolation($message); throw new InvariantViolation($message);
} }
} }
@ -258,22 +272,7 @@ class Utils
$var = (array) $var; $var = (array) $var;
} }
if (is_array($var)) { if (is_array($var)) {
$count = count($var); return json_encode($var);
if (!isset($var[0]) && $count > 0) {
$keys = [];
$keyCount = 0;
foreach ($var as $key => $value) {
$keys[] = '"' . $key . '"';
if ($keyCount++ > 4) {
break;
}
}
$keysLabel = $keyCount === 1 ? 'key' : 'keys';
$msg = "object with first $keysLabel: " . implode(', ', $keys);
} else {
$msg = "array($count)";
}
return $msg;
} }
if ('' === $var) { if ('' === $var) {
return '(empty string)'; return '(empty string)';
@ -285,7 +284,7 @@ class Utils
return 'false'; return 'false';
} }
if (true === $var) { if (true === $var) {
return 'false'; return 'true';
} }
if (is_string($var)) { if (is_string($var)) {
return "\"$var\""; return "\"$var\"";
@ -306,25 +305,14 @@ class Utils
return $var->toString(); return $var->toString();
} }
if (is_object($var)) { if (is_object($var)) {
if (method_exists($var, '__toString')) {
return (string) $var;
} else {
return 'instance of ' . get_class($var); return 'instance of ' . get_class($var);
} }
}
if (is_array($var)) { if (is_array($var)) {
$count = count($var); return json_encode($var);
if (!isset($var[0]) && $count > 0) {
$keys = [];
$keyCount = 0;
foreach ($var as $key => $value) {
$keys[] = '"' . $key . '"';
if ($keyCount++ > 4) {
break;
}
}
$keysLabel = $keyCount === 1 ? 'key' : 'keys';
$msg = "associative array($count) with first $keysLabel: " . implode(', ', $keys);
} else {
$msg = "array($count)";
}
return $msg;
} }
if ('' === $var) { if ('' === $var) {
return '(empty string)'; return '(empty string)';
@ -339,7 +327,7 @@ class Utils
return 'true'; return 'true';
} }
if (is_string($var)) { if (is_string($var)) {
return "\"$var\""; return $var;
} }
if (is_scalar($var)) { if (is_scalar($var)) {
return (string) $var; return (string) $var;
@ -418,34 +406,46 @@ class Utils
} }
/** /**
* Upholds the spec rules about naming.
*
* @param $name * @param $name
* @param bool $isIntrospection * @throws Error
* @throws InvariantViolation
*/ */
public static function assertValidName($name, $isIntrospection = false) public static function assertValidName($name)
{ {
$regex = '/^[_a-zA-Z][_a-zA-Z0-9]*$/'; $error = self::isValidNameError($name);
if ($error) {
throw $error;
}
}
if (!$name || !is_string($name)) { /**
throw new InvariantViolation( * Returns an Error if a name is invalid.
"Must be named. Unexpected name: " . self::printSafe($name) *
* @param string $name
* @param Node|null $node
* @return Error|null
*/
public static function isValidNameError($name, $node = null)
{
Utils::invariant(is_string($name), 'Expected string');
if (isset($name[1]) && $name[0] === '_' && $name[1] === '_') {
return new Error(
"Name \"{$name}\" must not begin with \"__\", which is reserved by " .
"GraphQL introspection.",
$node
); );
} }
if (!$isIntrospection && isset($name[1]) && $name[0] === '_' && $name[1] === '_') { if (!preg_match('/^[_a-zA-Z][_a-zA-Z0-9]*$/', $name)) {
Warning::warnOnce( return new Error(
'Name "'.$name.'" must not begin with "__", which is reserved by ' . "Names must match /^[_a-zA-Z][_a-zA-Z0-9]*\$/ but \"{$name}\" does not.",
'GraphQL introspection. In a future release of graphql this will ' . $node
'become an exception',
Warning::WARNING_NAME
); );
} }
if (!preg_match($regex, $name)) { return null;
throw new InvariantViolation(
'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "'.$name.'" does not.'
);
}
} }
/** /**
@ -471,4 +471,72 @@ class Utils
} }
}; };
} }
/**
* @param string[] $items
* @return string
*/
public static function quotedOrList(array $items)
{
$items = array_map(function($item) { return "\"$item\""; }, $items);
return self::orList($items);
}
public static function orList(array $items)
{
if (!$items) {
throw new \LogicException('items must not need to be empty.');
}
$selected = array_slice($items, 0, 5);
$selectedLength = count($selected);
$firstSelected = $selected[0];
if ($selectedLength === 1) {
return $firstSelected;
}
return array_reduce(
range(1, $selectedLength - 1),
function ($list, $index) use ($selected, $selectedLength) {
return $list.
($selectedLength > 2 ? ', ' : ' ') .
($index === $selectedLength - 1 ? 'or ' : '') .
$selected[$index];
},
$firstSelected
);
}
/**
* Given an invalid input string and a list of valid options, returns a filtered
* list of valid options sorted based on their similarity with the input.
*
* Includes a custom alteration from Damerau-Levenshtein to treat case changes
* as a single edit which helps identify mis-cased values with an edit distance
* of 1
* @param string $input
* @param array $options
* @return string[]
*/
public static function suggestionList($input, array $options)
{
$optionsByDistance = [];
$inputThreshold = mb_strlen($input) / 2;
foreach ($options as $option) {
$distance = $input === $option
? 0
: (strtolower($input) === strtolower($option)
? 1
: levenshtein($input, $option));
$threshold = max($inputThreshold, mb_strlen($option) / 2, 1);
if ($distance <= $threshold) {
$optionsByDistance[$option] = $distance;
}
}
asort($optionsByDistance);
return array_keys($optionsByDistance);
}
} }

261
src/Utils/Value.php Normal file
View File

@ -0,0 +1,261 @@
<?php
namespace GraphQL\Utils;
use GraphQL\Error\Error;
use GraphQL\Language\AST\Node;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ScalarType;
/**
* Coerces a PHP value given a GraphQL Type.
*
* Returns either a value which is valid for the provided type or a list of
* encountered coercion errors.
*/
class Value
{
/**
* Given a type and any value, return a runtime value coerced to match the type.
*/
public static function coerceValue($value, InputType $type, $blameNode = null, array $path = null)
{
if ($type instanceof NonNull) {
if ($value === null) {
return self::ofErrors([
self::coercionError(
"Expected non-nullable type $type not to be null",
$blameNode,
$path
),
]);
}
return self::coerceValue($value, $type->getWrappedType(), $blameNode, $path);
}
if (null === $value) {
// Explicitly return the value null.
return self::ofValue(null);
}
if ($type instanceof ScalarType) {
// Scalars determine if a value is valid via parseValue(), which can
// throw to indicate failure. If it throws, maintain a reference to
// the original error.
try {
$parseResult = $type->parseValue($value);
if (Utils::isInvalid($parseResult)) {
return self::ofErrors([
self::coercionError("Expected type {$type->name}", $blameNode, $path),
]);
}
return self::ofValue($parseResult);
} catch (\Exception $error) {
return self::ofErrors([
self::coercionError(
"Expected type {$type->name}",
$blameNode,
$path,
$error->getMessage(),
$error
),
]);
} catch (\Throwable $error) {
return self::ofErrors([
self::coercionError(
"Expected type {$type->name}",
$blameNode,
$path,
$error->getMessage(),
$error
),
]);
}
}
if ($type instanceof EnumType) {
if (is_string($value)) {
$enumValue = $type->getValue($value);
if ($enumValue) {
return self::ofValue($enumValue->value);
}
}
$suggestions = Utils::suggestionList(
Utils::printSafe($value),
array_map(function($enumValue) { return $enumValue->name; }, $type->getValues())
);
$didYouMean = $suggestions
? "did you mean " . Utils::orList($suggestions) . "?"
: null;
return self::ofErrors([
self::coercionError(
"Expected type {$type->name}",
$blameNode,
$path,
$didYouMean
),
]);
}
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if (is_array($value) || $value instanceof \Traversable) {
$errors = [];
$coercedValue = [];
foreach ($value as $index => $itemValue) {
$coercedItem = self::coerceValue(
$itemValue,
$itemType,
$blameNode,
self::atPath($path, $index)
);
if ($coercedItem['errors']) {
$errors = self::add($errors, $coercedItem['errors']);
} else {
$coercedValue[] = $coercedItem['value'];
}
}
return $errors ? self::ofErrors($errors) : self::ofValue($coercedValue);
}
// Lists accept a non-list value as a list of one.
$coercedItem = self::coerceValue($value, $itemType, $blameNode);
return $coercedItem['errors'] ? $coercedItem : self::ofValue([$coercedItem['value']]);
}
if ($type instanceof InputObjectType) {
if (!is_object($value) && !is_array($value) && !$value instanceof \Traversable) {
return self::ofErrors([
self::coercionError(
"Expected type {$type->name} to be an object",
$blameNode,
$path
),
]);
}
$errors = [];
$coercedValue = [];
$fields = $type->getFields();
foreach ($fields as $fieldName => $field) {
if (!array_key_exists($fieldName, $value)) {
if ($field->defaultValueExists()) {
$coercedValue[$fieldName] = $field->defaultValue;
} else if ($field->getType() instanceof NonNull) {
$fieldPath = self::printPath(self::atPath($path, $fieldName));
$errors = self::add(
$errors,
self::coercionError(
"Field {$fieldPath} of required " .
"type {$field->type} was not provided",
$blameNode
)
);
}
} else {
$fieldValue = $value[$fieldName];
$coercedField = self::coerceValue(
$fieldValue,
$field->getType(),
$blameNode,
self::atPath($path, $fieldName)
);
if ($coercedField['errors']) {
$errors = self::add($errors, $coercedField['errors']);
} else {
$coercedValue[$fieldName] = $coercedField['value'];
}
}
}
// Ensure every provided field is defined.
foreach ($value as $fieldName => $field) {
if (!array_key_exists($fieldName, $fields)) {
$suggestions = Utils::suggestionList(
$fieldName,
array_keys($fields)
);
$didYouMean = $suggestions
? "did you mean " . Utils::orList($suggestions) . "?"
: null;
$errors = self::add(
$errors,
self::coercionError(
"Field \"{$fieldName}\" is not defined by type {$type->name}",
$blameNode,
$path,
$didYouMean
)
);
}
}
return $errors ? self::ofErrors($errors) : self::ofValue($coercedValue);
}
throw new Error("Unexpected type {$type}");
}
private static function ofValue($value) {
return ['errors' => null, 'value' => $value];
}
private static function ofErrors($errors) {
return ['errors' => $errors, 'value' => Utils::undefined()];
}
private static function add($errors, $moreErrors) {
return array_merge($errors, is_array($moreErrors) ? $moreErrors : [$moreErrors]);
}
private static function atPath($prev, $key) {
return ['prev' => $prev, 'key' => $key];
}
/**
* @param string $message
* @param Node $blameNode
* @param array|null $path
* @param string $subMessage
* @param \Exception|\Throwable|null $originalError
* @return Error
*/
private static function coercionError($message, $blameNode, array $path = null, $subMessage = null, $originalError = null) {
$pathStr = self::printPath($path);
// Return a GraphQLError instance
return new Error(
$message .
($pathStr ? ' at ' . $pathStr : '') .
($subMessage ? '; ' . $subMessage : '.'),
$blameNode,
null,
null,
null,
$originalError
);
}
/**
* Build a string describing the path into the value where the error was found
*
* @param $path
* @return string
*/
private static function printPath(array $path = null) {
$pathStr = '';
$currentPath = $path;
while($currentPath) {
$pathStr =
(is_string($currentPath['key'])
? '.' . $currentPath['key']
: '[' . $currentPath['key'] . ']') . $pathStr;
$currentPath = $currentPath['prev'];
}
return $pathStr ? 'value' . $pathStr : '';
}
}

View File

@ -2,26 +2,15 @@
namespace GraphQL\Validator; namespace GraphQL\Validator;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\ListValueNode;
use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NullValueNode;
use GraphQL\Language\AST\VariableNode;
use GraphQL\Language\Printer;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Type\Schema; use GraphQL\Type\Schema;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\LeafType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Utils\Utils;
use GraphQL\Utils\TypeInfo; use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\Rules\AbstractValidationRule; use GraphQL\Validator\Rules\AbstractValidationRule;
use GraphQL\Validator\Rules\ArgumentsOfCorrectType; use GraphQL\Validator\Rules\ValuesOfCorrectType;
use GraphQL\Validator\Rules\DefaultValuesOfCorrectType;
use GraphQL\Validator\Rules\DisableIntrospection; use GraphQL\Validator\Rules\DisableIntrospection;
use GraphQL\Validator\Rules\ExecutableDefinitions;
use GraphQL\Validator\Rules\FieldsOnCorrectType; use GraphQL\Validator\Rules\FieldsOnCorrectType;
use GraphQL\Validator\Rules\FragmentsOnCompositeTypes; use GraphQL\Validator\Rules\FragmentsOnCompositeTypes;
use GraphQL\Validator\Rules\KnownArgumentNames; use GraphQL\Validator\Rules\KnownArgumentNames;
@ -46,6 +35,7 @@ use GraphQL\Validator\Rules\UniqueInputFieldNames;
use GraphQL\Validator\Rules\UniqueOperationNames; use GraphQL\Validator\Rules\UniqueOperationNames;
use GraphQL\Validator\Rules\UniqueVariableNames; use GraphQL\Validator\Rules\UniqueVariableNames;
use GraphQL\Validator\Rules\VariablesAreInputTypes; use GraphQL\Validator\Rules\VariablesAreInputTypes;
use GraphQL\Validator\Rules\VariablesDefaultValueAllowed;
use GraphQL\Validator\Rules\VariablesInAllowedPosition; use GraphQL\Validator\Rules\VariablesInAllowedPosition;
/** /**
@ -122,6 +112,7 @@ class DocumentValidator
{ {
if (null === self::$defaultRules) { if (null === self::$defaultRules) {
self::$defaultRules = [ self::$defaultRules = [
ExecutableDefinitions::class => new ExecutableDefinitions(),
UniqueOperationNames::class => new UniqueOperationNames(), UniqueOperationNames::class => new UniqueOperationNames(),
LoneAnonymousOperation::class => new LoneAnonymousOperation(), LoneAnonymousOperation::class => new LoneAnonymousOperation(),
KnownTypeNames::class => new KnownTypeNames(), KnownTypeNames::class => new KnownTypeNames(),
@ -141,9 +132,9 @@ class DocumentValidator
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(), UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
KnownArgumentNames::class => new KnownArgumentNames(), KnownArgumentNames::class => new KnownArgumentNames(),
UniqueArgumentNames::class => new UniqueArgumentNames(), UniqueArgumentNames::class => new UniqueArgumentNames(),
ArgumentsOfCorrectType::class => new ArgumentsOfCorrectType(), ValuesOfCorrectType::class => new ValuesOfCorrectType(),
ProvidedNonNullArguments::class => new ProvidedNonNullArguments(), ProvidedNonNullArguments::class => new ProvidedNonNullArguments(),
DefaultValuesOfCorrectType::class => new DefaultValuesOfCorrectType(), VariablesDefaultValueAllowed::class => new VariablesDefaultValueAllowed(),
VariablesInAllowedPosition::class => new VariablesInAllowedPosition(), VariablesInAllowedPosition::class => new VariablesInAllowedPosition(),
OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(), OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(),
UniqueInputFieldNames::class => new UniqueInputFieldNames(), UniqueInputFieldNames::class => new UniqueInputFieldNames(),
@ -223,100 +214,23 @@ class DocumentValidator
} }
/** /**
* Utility for validators which determines if a value literal AST is valid given * Utility which determines if a value literal node is valid for an input type.
* an input type.
* *
* Note that this only validates literal values, variables are assumed to * Deprecated. Rely on validation for documents containing literal values.
* provide values of the correct type.
* *
* @return array * @deprecated
* @return Error[]
*/ */
public static function isValidLiteralValue(Type $type, $valueNode) public static function isValidLiteralValue(Type $type, $valueNode)
{ {
// A value must be provided if the type is non-null. $emptySchema = new Schema([]);
if ($type instanceof NonNull) { $emptyDoc = new DocumentNode(['definitions' => []]);
if (!$valueNode || $valueNode instanceof NullValueNode) { $typeInfo = new TypeInfo($emptySchema, $type);
return [ 'Expected "' . Utils::printSafe($type) . '", found null.' ]; $context = new ValidationContext($emptySchema, $emptyDoc, $typeInfo);
} $validator = new ValuesOfCorrectType();
return static::isValidLiteralValue($type->getWrappedType(), $valueNode); $visitor = $validator->getVisitor($context);
} Visitor::visit($valueNode, Visitor::visitWithTypeInfo($typeInfo, $visitor));
return $context->getErrors();
if (!$valueNode || $valueNode instanceof NullValueNode) {
return [];
}
// This function only tests literals, and assumes variables will provide
// values of the correct type.
if ($valueNode instanceof VariableNode) {
return [];
}
// Lists accept a non-list value as a list of one.
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if ($valueNode instanceof ListValueNode) {
$errors = [];
foreach($valueNode->values as $index => $itemNode) {
$tmp = static::isValidLiteralValue($itemType, $itemNode);
if ($tmp) {
$errors = array_merge($errors, Utils::map($tmp, function($error) use ($index) {
return "In element #$index: $error";
}));
}
}
return $errors;
} else {
return static::isValidLiteralValue($itemType, $valueNode);
}
}
// Input objects check each defined field and look for undefined fields.
if ($type instanceof InputObjectType) {
if ($valueNode->kind !== NodeKind::OBJECT) {
return [ "Expected \"{$type->name}\", found not an object." ];
}
$fields = $type->getFields();
$errors = [];
// Ensure every provided field is defined.
$fieldNodes = $valueNode->fields;
foreach ($fieldNodes as $providedFieldNode) {
if (empty($fields[$providedFieldNode->name->value])) {
$errors[] = "In field \"{$providedFieldNode->name->value}\": Unknown field.";
}
}
// Ensure every defined field is valid.
$fieldNodeMap = Utils::keyMap($fieldNodes, function($fieldNode) {return $fieldNode->name->value;});
foreach ($fields as $fieldName => $field) {
$result = static::isValidLiteralValue(
$field->getType(),
isset($fieldNodeMap[$fieldName]) ? $fieldNodeMap[$fieldName]->value : null
);
if ($result) {
$errors = array_merge($errors, Utils::map($result, function($error) use ($fieldName) {
return "In field \"$fieldName\": $error";
}));
}
}
return $errors;
}
if ($type instanceof LeafType) {
// Scalars must parse to a non-null value
if (!$type->isValidLiteral($valueNode)) {
$printed = Printer::doPrint($valueNode);
return [ "Expected type \"{$type->name}\", found $printed." ];
}
return [];
}
throw new InvariantViolation('Must be input type');
} }
/** /**

View File

@ -1,39 +0,0 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Printer;
use GraphQL\Language\Visitor;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\ValidationContext;
class ArgumentsOfCorrectType extends AbstractValidationRule
{
static function badValueMessage($argName, $type, $value, $verboseErrors = [])
{
$message = $verboseErrors ? ("\n" . implode("\n", $verboseErrors)) : '';
return "Argument \"$argName\" has invalid value $value.$message";
}
public function getVisitor(ValidationContext $context)
{
return [
NodeKind::ARGUMENT => function(ArgumentNode $argNode) use ($context) {
$argDef = $context->getArgument();
if ($argDef) {
$errors = DocumentValidator::isValidLiteralValue($argDef->getType(), $argNode->value);
if (!empty($errors)) {
$context->reportError(new Error(
self::badValueMessage($argNode->name->value, $argDef->getType(), Printer::doPrint($argNode->value), $errors),
[$argNode->value]
));
}
}
return Visitor::skipNode();
}
];
}
}

View File

@ -1,59 +0,0 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Language\Printer;
use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\ValidationContext;
class DefaultValuesOfCorrectType extends AbstractValidationRule
{
static function badValueForDefaultArgMessage($varName, $type, $value, $verboseErrors = null)
{
$message = $verboseErrors ? ("\n" . implode("\n", $verboseErrors)) : '';
return "Variable \$$varName has invalid default value: $value.$message";
}
static function defaultForNonNullArgMessage($varName, $type, $guessType)
{
return "Variable \$$varName of type $type " .
"is required and will never use the default value. " .
"Perhaps you meant to use type $guessType.";
}
public function getVisitor(ValidationContext $context)
{
return [
NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $varDefNode) use ($context) {
$name = $varDefNode->variable->name->value;
$defaultValue = $varDefNode->defaultValue;
$type = $context->getInputType();
if ($type instanceof NonNull && $defaultValue) {
$context->reportError(new Error(
static::defaultForNonNullArgMessage($name, $type, $type->getWrappedType()),
[$defaultValue]
));
}
if ($type && $defaultValue) {
$errors = DocumentValidator::isValidLiteralValue($type, $defaultValue);
if (!empty($errors)) {
$context->reportError(new Error(
static::badValueForDefaultArgMessage($name, $type, Printer::doPrint($defaultValue), $errors),
[$defaultValue]
));
}
}
return Visitor::skipNode();
},
NodeKind::SELECTION_SET => function() {return Visitor::skipNode();},
NodeKind::FRAGMENT_DEFINITION => function() {return Visitor::skipNode();}
];
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\Visitor;
use GraphQL\Validator\ValidationContext;
/**
* Executable definitions
*
* A GraphQL document is only valid for execution if all definitions are either
* operation or fragment definitions.
*/
class ExecutableDefinitions extends AbstractValidationRule
{
static function nonExecutableDefinitionMessage($defName)
{
return "The \"$defName\" definition is not executable.";
}
public function getVisitor(ValidationContext $context)
{
return [
NodeKind::DOCUMENT => function (DocumentNode $node) use ($context) {
/** @var Node $definition */
foreach ($node->definitions as $definition) {
if (
!$definition instanceof OperationDefinitionNode &&
!$definition instanceof FragmentDefinitionNode
) {
$context->reportError(new Error(
self::nonExecutableDefinitionMessage($definition->name->value),
[$definition->name]
));
}
}
return Visitor::skipNode();
}
];
}
}

View File

@ -4,27 +4,27 @@ namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Schema;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
class FieldsOnCorrectType extends AbstractValidationRule class FieldsOnCorrectType extends AbstractValidationRule
{ {
static function undefinedFieldMessage($field, $type, array $suggestedTypes = []) static function undefinedFieldMessage($fieldName, $type, array $suggestedTypeNames, array $suggestedFieldNames)
{ {
$message = 'Cannot query field "' . $field . '" on type "' . $type.'".'; $message = 'Cannot query field "' . $fieldName . '" on type "' . $type.'".';
$maxLength = 5; if ($suggestedTypeNames) {
$count = count($suggestedTypes); $suggestions = Utils::quotedOrList($suggestedTypeNames);
if ($count > 0) { $message .= " Did you mean to use an inline fragment on $suggestions?";
$suggestions = array_slice($suggestedTypes, 0, $maxLength); } else if ($suggestedFieldNames) {
$suggestions = Utils::map($suggestions, function($t) { return "\"$t\""; }); $suggestions = Utils::quotedOrList($suggestedFieldNames);
$suggestions = implode(', ', $suggestions); $message .= " Did you mean {$suggestions}?";
if ($count > $maxLength) {
$suggestions .= ', and ' . ($count - $maxLength) . ' other types';
}
$message .= " However, this field exists on $suggestions.";
$message .= ' Perhaps you meant to use an inline fragment?';
} }
return $message; return $message;
} }
@ -37,8 +37,32 @@ class FieldsOnCorrectType extends AbstractValidationRule
if ($type) { if ($type) {
$fieldDef = $context->getFieldDef(); $fieldDef = $context->getFieldDef();
if (!$fieldDef) { if (!$fieldDef) {
// This isn't valid. Let's find suggestions, if any.
$schema = $context->getSchema();
$fieldName = $node->name->value;
// First determine if there are any suggested types to condition on.
$suggestedTypeNames = $this->getSuggestedTypeNames(
$schema,
$type,
$fieldName
);
// If there are no suggested types, then perhaps this was a typo?
$suggestedFieldNames = $suggestedTypeNames
? []
: $this->getSuggestedFieldNames(
$schema,
$type,
$fieldName
);
// Report an error, including helpful suggestions.
$context->reportError(new Error( $context->reportError(new Error(
static::undefinedFieldMessage($node->name->value, $type->name), static::undefinedFieldMessage(
$node->name->value,
$type->name,
$suggestedTypeNames,
$suggestedFieldNames
),
[$node] [$node]
)); ));
} }
@ -46,4 +70,72 @@ class FieldsOnCorrectType extends AbstractValidationRule
} }
]; ];
} }
/**
* Go through all of the implementations of type, as well as the interfaces
* that they implement. If any of those types include the provided field,
* suggest them, sorted by how often the type is referenced, starting
* with Interfaces.
*
* @param Schema $schema
* @param $type
* @param string $fieldName
* @return array
*/
private function getSuggestedTypeNames(Schema $schema, $type, $fieldName)
{
if (Type::isAbstractType($type)) {
$suggestedObjectTypes = [];
$interfaceUsageCount = [];
foreach($schema->getPossibleTypes($type) as $possibleType) {
$fields = $possibleType->getFields();
if (!isset($fields[$fieldName])) {
continue;
}
// This object type defines this field.
$suggestedObjectTypes[] = $possibleType->name;
foreach($possibleType->getInterfaces() as $possibleInterface) {
$fields = $possibleInterface->getFields();
if (!isset($fields[$fieldName])) {
continue;
}
// This interface type defines this field.
$interfaceUsageCount[$possibleInterface->name] =
!isset($interfaceUsageCount[$possibleInterface->name])
? 0
: $interfaceUsageCount[$possibleInterface->name] + 1;
}
}
// Suggest interface types based on how common they are.
arsort($interfaceUsageCount);
$suggestedInterfaceTypes = array_keys($interfaceUsageCount);
// Suggest both interface and object types.
return array_merge($suggestedInterfaceTypes, $suggestedObjectTypes);
}
// Otherwise, must be an Object type, which does not have possible fields.
return [];
}
/**
* For the field name provided, determine if there are any similar field names
* that may be the result of a typo.
*
* @param Schema $schema
* @param $type
* @param string $fieldName
* @return array|string[]
*/
private function getSuggestedFieldNames(Schema $schema, $type, $fieldName)
{
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
$possibleFieldNames = array_keys($type->getFields());
return Utils::suggestionList($fieldName, $possibleFieldNames);
}
// Otherwise, must be a Union type, which does not define fields.
return [];
}
} }

View File

@ -7,56 +7,68 @@ use GraphQL\Language\AST\NodeKind;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
/**
* Known argument names
*
* A GraphQL field is only valid if all supplied arguments are defined by
* that field.
*/
class KnownArgumentNames extends AbstractValidationRule class KnownArgumentNames extends AbstractValidationRule
{ {
public static function unknownArgMessage($argName, $fieldName, $type) public static function unknownArgMessage($argName, $fieldName, $typeName, array $suggestedArgs)
{ {
return "Unknown argument \"$argName\" on field \"$fieldName\" of type \"$type\"."; $message = "Unknown argument \"$argName\" on field \"$fieldName\" of type \"$typeName\".";
if ($suggestedArgs) {
$message .= ' Did you mean ' . Utils::quotedOrList($suggestedArgs) . '?';
}
return $message;
} }
public static function unknownDirectiveArgMessage($argName, $directiveName) public static function unknownDirectiveArgMessage($argName, $directiveName, array $suggestedArgs)
{ {
return "Unknown argument \"$argName\" on directive \"@$directiveName\"."; $message = "Unknown argument \"$argName\" on directive \"@$directiveName\".";
if ($suggestedArgs) {
$message .= ' Did you mean ' . Utils::quotedOrList($suggestedArgs) . '?';
}
return $message;
} }
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
return [ return [
NodeKind::ARGUMENT => function(ArgumentNode $node, $key, $parent, $path, $ancestors) use ($context) { NodeKind::ARGUMENT => function(ArgumentNode $node, $key, $parent, $path, $ancestors) use ($context) {
$argDef = $context->getArgument();
if (!$argDef) {
$argumentOf = $ancestors[count($ancestors) - 1]; $argumentOf = $ancestors[count($ancestors) - 1];
if ($argumentOf->kind === NodeKind::FIELD) { if ($argumentOf->kind === NodeKind::FIELD) {
$fieldDef = $context->getFieldDef(); $fieldDef = $context->getFieldDef();
if ($fieldDef) {
$fieldArgDef = null;
foreach ($fieldDef->args as $arg) {
if ($arg->name === $node->name->value) {
$fieldArgDef = $arg;
break;
}
}
if (!$fieldArgDef) {
$parentType = $context->getParentType(); $parentType = $context->getParentType();
Utils::invariant($parentType); if ($fieldDef && $parentType) {
$context->reportError(new Error( $context->reportError(new Error(
self::unknownArgMessage($node->name->value, $fieldDef->name, $parentType->name), self::unknownArgMessage(
$node->name->value,
$fieldDef->name,
$parentType->name,
Utils::suggestionList(
$node->name->value,
array_map(function ($arg) { return $arg->name; }, $fieldDef->args)
)
),
[$node] [$node]
)); ));
} }
}
} else if ($argumentOf->kind === NodeKind::DIRECTIVE) { } else if ($argumentOf->kind === NodeKind::DIRECTIVE) {
$directive = $context->getDirective(); $directive = $context->getDirective();
if ($directive) { if ($directive) {
$directiveArgDef = null;
foreach ($directive->args as $arg) {
if ($arg->name === $node->name->value) {
$directiveArgDef = $arg;
break;
}
}
if (!$directiveArgDef) {
$context->reportError(new Error( $context->reportError(new Error(
self::unknownDirectiveArgMessage($node->name->value, $directive->name), self::unknownDirectiveArgMessage(
$node->name->value,
$directive->name,
Utils::suggestionList(
$node->name->value,
array_map(function ($arg) { return $arg->name; }, $directive->args)
)
),
[$node] [$node]
)); ));
} }

View File

@ -1,13 +1,12 @@
<?php <?php
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\DirectiveNode; use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\Node; use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\DirectiveLocation;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
use GraphQL\Type\Definition\DirectiveLocation;
class KnownDirectives extends AbstractValidationRule class KnownDirectives extends AbstractValidationRule
{ {
@ -38,10 +37,9 @@ class KnownDirectives extends AbstractValidationRule
self::unknownDirectiveMessage($node->name->value), self::unknownDirectiveMessage($node->name->value),
[$node] [$node]
)); ));
return ; return;
} }
$appliedTo = $ancestors[count($ancestors) - 1]; $candidateLocation = $this->getDirectiveLocationForASTPath($ancestors);
$candidateLocation = $this->getLocationForAppliedNode($appliedTo);
if (!$candidateLocation) { if (!$candidateLocation) {
$context->reportError(new Error( $context->reportError(new Error(
@ -58,8 +56,9 @@ class KnownDirectives extends AbstractValidationRule
]; ];
} }
private function getLocationForAppliedNode(Node $appliedTo) private function getDirectiveLocationForASTPath(array $ancestors)
{ {
$appliedTo = $ancestors[count($ancestors) - 1];
switch ($appliedTo->kind) { switch ($appliedTo->kind) {
case NodeKind::OPERATION_DEFINITION: case NodeKind::OPERATION_DEFINITION:
switch ($appliedTo->operation) { switch ($appliedTo->operation) {
@ -68,10 +67,43 @@ class KnownDirectives extends AbstractValidationRule
case 'subscription': return DirectiveLocation::SUBSCRIPTION; case 'subscription': return DirectiveLocation::SUBSCRIPTION;
} }
break; break;
case NodeKind::FIELD: return DirectiveLocation::FIELD; case NodeKind::FIELD:
case NodeKind::FRAGMENT_SPREAD: return DirectiveLocation::FRAGMENT_SPREAD; return DirectiveLocation::FIELD;
case NodeKind::INLINE_FRAGMENT: return DirectiveLocation::INLINE_FRAGMENT; case NodeKind::FRAGMENT_SPREAD:
case NodeKind::FRAGMENT_DEFINITION: return DirectiveLocation::FRAGMENT_DEFINITION; return DirectiveLocation::FRAGMENT_SPREAD;
case NodeKind::INLINE_FRAGMENT:
return DirectiveLocation::INLINE_FRAGMENT;
case NodeKind::FRAGMENT_DEFINITION:
return DirectiveLocation::FRAGMENT_DEFINITION;
case NodeKind::SCHEMA_DEFINITION:
return DirectiveLocation::SCHEMA;
case NodeKind::SCALAR_TYPE_DEFINITION:
case NodeKind::SCALAR_TYPE_EXTENSION:
return DirectiveLocation::SCALAR;
case NodeKind::OBJECT_TYPE_DEFINITION:
case NodeKind::OBJECT_TYPE_EXTENSION:
return DirectiveLocation::OBJECT;
case NodeKind::FIELD_DEFINITION:
return DirectiveLocation::FIELD_DEFINITION;
case NodeKind::INTERFACE_TYPE_DEFINITION:
case NodeKind::INTERFACE_TYPE_EXTENSION:
return DirectiveLocation::IFACE;
case NodeKind::UNION_TYPE_DEFINITION:
case NodeKind::UNION_TYPE_EXTENSION:
return DirectiveLocation::UNION;
case NodeKind::ENUM_TYPE_DEFINITION:
case NodeKind::ENUM_TYPE_EXTENSION:
return DirectiveLocation::ENUM;
case NodeKind::ENUM_VALUE_DEFINITION:
return DirectiveLocation::ENUM_VALUE;
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
case NodeKind::INPUT_OBJECT_TYPE_EXTENSION:
return DirectiveLocation::INPUT_OBJECT;
case NodeKind::INPUT_VALUE_DEFINITION:
$parentNode = $ancestors[count($ancestors) - 3];
return $parentNode instanceof InputObjectTypeDefinitionNode
? DirectiveLocation::INPUT_FIELD_DEFINITION
: DirectiveLocation::ARGUMENT_DEFINITION;
} }
} }
} }

View File

@ -1,35 +1,55 @@
<?php <?php
namespace GraphQL\Validator\Rules; namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\NamedTypeNode; use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
/**
* Known type names
*
* A GraphQL document is only valid if referenced types (specifically
* variable definitions and fragment conditions) are defined by the type schema.
*/
class KnownTypeNames extends AbstractValidationRule class KnownTypeNames extends AbstractValidationRule
{ {
static function unknownTypeMessage($type) static function unknownTypeMessage($type, array $suggestedTypes)
{ {
return "Unknown type \"$type\"."; $message = "Unknown type \"$type\".";
if ($suggestedTypes) {
$suggestions = Utils::quotedOrList($suggestedTypes);
$message .= " Did you mean $suggestions?";
}
return $message;
} }
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
$skip = function() {return Visitor::skipNode();}; $skip = function() { return Visitor::skipNode(); };
return [ return [
// TODO: when validating IDL, re-enable these. Experimental version does not
// add unreferenced types, resulting in false-positive errors. Squelched
// errors for now.
NodeKind::OBJECT_TYPE_DEFINITION => $skip, NodeKind::OBJECT_TYPE_DEFINITION => $skip,
NodeKind::INTERFACE_TYPE_DEFINITION => $skip, NodeKind::INTERFACE_TYPE_DEFINITION => $skip,
NodeKind::UNION_TYPE_DEFINITION => $skip, NodeKind::UNION_TYPE_DEFINITION => $skip,
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $skip, NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $skip,
NodeKind::NAMED_TYPE => function(NamedTypeNode $node) use ($context) {
NodeKind::NAMED_TYPE => function(NamedTypeNode $node, $key) use ($context) { $schema = $context->getSchema();
$typeName = $node->name->value; $typeName = $node->name->value;
$type = $context->getSchema()->getType($typeName); $type = $schema->getType($typeName);
if (!$type) { if (!$type) {
$context->reportError(new Error(self::unknownTypeMessage($typeName), [$node])); $context->reportError(new Error(
self::unknownTypeMessage(
$typeName,
Utils::suggestionList($typeName, array_keys($schema->getTypeMap()))
), [$node])
);
} }
} }
]; ];

File diff suppressed because it is too large Load Diff

View File

@ -61,7 +61,13 @@ class PossibleFragmentSpreads extends AbstractValidationRule
private function getFragmentType(ValidationContext $context, $name) private function getFragmentType(ValidationContext $context, $name)
{ {
$frag = $context->getFragment($name); $frag = $context->getFragment($name);
return $frag ? TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition) : null; if ($frag) {
$type = TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition);
if ($type instanceof CompositeType) {
return $type;
}
}
return null;
} }
private function doTypesOverlap(Schema $schema, CompositeType $fragType, CompositeType $parentType) private function doTypesOverlap(Schema $schema, CompositeType $fragType, CompositeType $parentType)

View File

@ -203,11 +203,21 @@ class QueryComplexity extends AbstractQuerySecurity
$args = []; $args = [];
if ($fieldDef instanceof FieldDefinition) { if ($fieldDef instanceof FieldDefinition) {
$variableValues = Values::getVariableValues( $variableValuesResult = Values::getVariableValues(
$this->context->getSchema(), $this->context->getSchema(),
$this->variableDefs, $this->variableDefs,
$rawVariableValues $rawVariableValues
); );
if ($variableValuesResult['errors']) {
throw new Error(implode("\n\n", array_map(
function ($error) {
return $error->getMessage();
}
, $variableValuesResult['errors'])));
}
$variableValues = $variableValuesResult['coerced'];
$args = Values::getArgumentValues($fieldDef, $node, $variableValues); $args = Values::getArgumentValues($fieldDef, $node, $variableValues);
} }
@ -220,12 +230,21 @@ class QueryComplexity extends AbstractQuerySecurity
return false; return false;
} }
$variableValues = Values::getVariableValues( $variableValuesResult = Values::getVariableValues(
$this->context->getSchema(), $this->context->getSchema(),
$this->variableDefs, $this->variableDefs,
$this->getRawVariableValues() $this->getRawVariableValues()
); );
if ($variableValuesResult['errors']) {
throw new Error(implode("\n\n", array_map(
function ($error) {
return $error->getMessage();
}
, $variableValuesResult['errors'])));
}
$variableValues = $variableValuesResult['coerced'];
if ($directiveNode->name->value === 'include') { if ($directiveNode->name->value === 'include') {
$directive = Directive::includeDirective(); $directive = Directive::includeDirective();
$directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues); $directiveArgs = Values::getArgumentValues($directive, $directiveNode, $variableValues);

View File

@ -0,0 +1,238 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\BooleanValueNode;
use GraphQL\Language\AST\EnumValueNode;
use GraphQL\Language\AST\FloatValueNode;
use GraphQL\Language\AST\IntValueNode;
use GraphQL\Language\AST\ListValueNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NullValueNode;
use GraphQL\Language\AST\ObjectFieldNode;
use GraphQL\Language\AST\ObjectValueNode;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Language\Printer;
use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\EnumValueDefinition;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\Utils;
use GraphQL\Validator\ValidationContext;
/**
* Value literals of correct type
*
* A GraphQL document is only valid if all value literals are of the type
* expected at their position.
*/
class ValuesOfCorrectType extends AbstractValidationRule
{
static function badValueMessage($typeName, $valueName, $message = null)
{
return "Expected type {$typeName}, found {$valueName}" .
($message ? "; ${message}" : '.');
}
static function requiredFieldMessage($typeName, $fieldName, $fieldTypeName)
{
return "Field {$typeName}.{$fieldName} of required type " .
"{$fieldTypeName} was not provided.";
}
static function unknownFieldMessage($typeName, $fieldName, $message = null)
{
return (
"Field \"{$fieldName}\" is not defined by type {$typeName}" .
($message ? "; {$message}" : '.')
);
}
public function getVisitor(ValidationContext $context)
{
return [
NodeKind::NULL => function(NullValueNode $node) use ($context) {
$type = $context->getInputType();
if ($type instanceof NonNull) {
$context->reportError(
new Error(
self::badValueMessage((string) $type, Printer::doPrint($node)),
$node
)
);
}
},
NodeKind::LST => function(ListValueNode $node) use ($context) {
// Note: TypeInfo will traverse into a list's item type, so look to the
// parent input type to check if it is a list.
$type = Type::getNullableType($context->getParentInputType());
if (!$type instanceof ListOfType) {
$this->isValidScalar($context, $node);
return Visitor::skipNode();
}
},
NodeKind::OBJECT => function(ObjectValueNode $node) use ($context) {
// Note: TypeInfo will traverse into a list's item type, so look to the
// parent input type to check if it is a list.
$type = Type::getNamedType($context->getInputType());
if (!$type instanceof InputObjectType) {
$this->isValidScalar($context, $node);
return Visitor::skipNode();
}
// Ensure every required field exists.
$inputFields = $type->getFields();
$nodeFields = iterator_to_array($node->fields);
$fieldNodeMap = array_combine(
array_map(function ($field) { return $field->name->value; }, $nodeFields),
array_values($nodeFields)
);
foreach ($inputFields as $fieldName => $fieldDef) {
$fieldType = $fieldDef->getType();
if (!isset($fieldNodeMap[$fieldName]) && $fieldType instanceof NonNull) {
$context->reportError(
new Error(
self::requiredFieldMessage($type->name, $fieldName, (string) $fieldType),
$node
)
);
}
}
},
NodeKind::OBJECT_FIELD => function(ObjectFieldNode $node) use ($context) {
$parentType = Type::getNamedType($context->getParentInputType());
$fieldType = $context->getInputType();
if (!$fieldType && $parentType instanceof InputObjectType) {
$suggestions = Utils::suggestionList(
$node->name->value,
array_keys($parentType->getFields())
);
$didYouMean = $suggestions
? "Did you mean " . Utils::orList($suggestions) . "?"
: null;
$context->reportError(
new Error(
self::unknownFieldMessage($parentType->name, $node->name->value, $didYouMean),
$node
)
);
}
},
NodeKind::ENUM => function(EnumValueNode $node) use ($context) {
$type = Type::getNamedType($context->getInputType());
if (!$type instanceof EnumType) {
$this->isValidScalar($context, $node);
} else if (!$type->getValue($node->value)) {
$context->reportError(
new Error(
self::badValueMessage(
$type->name,
Printer::doPrint($node),
$this->enumTypeSuggestion($type, $node)
),
$node
)
);
}
},
NodeKind::INT => function (IntValueNode $node) use ($context) { $this->isValidScalar($context, $node); },
NodeKind::FLOAT => function (FloatValueNode $node) use ($context) { $this->isValidScalar($context, $node); },
NodeKind::STRING => function (StringValueNode $node) use ($context) { $this->isValidScalar($context, $node); },
NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context) { $this->isValidScalar($context, $node); },
];
}
private function isValidScalar(ValidationContext $context, ValueNode $node)
{
// Report any error at the full type expected by the location.
$locationType = $context->getInputType();
if (!$locationType) {
return;
}
$type = Type::getNamedType($locationType);
if (!$type instanceof ScalarType) {
$context->reportError(
new Error(
self::badValueMessage(
(string) $locationType,
Printer::doPrint($node),
$this->enumTypeSuggestion($type, $node)
),
$node
)
);
return;
}
// Scalars determine if a literal value is valid via parseLiteral() which
// may throw or return an invalid value to indicate failure.
try {
$parseResult = $type->parseLiteral($node);
if (Utils::isInvalid($parseResult)) {
$context->reportError(
new Error(
self::badValueMessage(
(string) $locationType,
Printer::doPrint($node)
),
$node
)
);
}
} catch (\Exception $error) {
// Ensure a reference to the original error is maintained.
$context->reportError(
new Error(
self::badValueMessage(
(string) $locationType,
Printer::doPrint($node),
$error->getMessage()
),
$node,
null,
null,
null,
$error
)
);
} catch (\Throwable $error) {
// Ensure a reference to the original error is maintained.
$context->reportError(
new Error(
self::badValueMessage(
(string) $locationType,
Printer::doPrint($node),
$error->getMessage()
),
$node,
null,
null,
null,
$error
)
);
}
}
private function enumTypeSuggestion($type, ValueNode $node)
{
if ($type instanceof EnumType) {
$suggestions = Utils::suggestionList(
Printer::doPrint($node),
array_map(function (EnumValueDefinition $value) {
return $value->name;
}, $type->getValues())
);
return $suggestions ? 'Did you mean the enum value ' . Utils::orList($suggestions) . '?' : null;
}
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Error\Error;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Validator\ValidationContext;
/**
* Variable's default value is allowed
*
* A GraphQL document is only valid if all variable default values are allowed
* due to a variable not being required.
*/
class VariablesDefaultValueAllowed extends AbstractValidationRule
{
static function defaultForRequiredVarMessage($varName, $type, $guessType)
{
return (
"Variable \"\${$varName}\" of type \"{$type}\" is required and " .
'will not use the default value. ' .
"Perhaps you meant to use type \"{$guessType}\"."
);
}
public function getVisitor(ValidationContext $context)
{
return [
NodeKind::VARIABLE_DEFINITION => function(VariableDefinitionNode $node) use ($context) {
$name = $node->variable->name->value;
$defaultValue = $node->defaultValue;
$type = $context->getInputType();
if ($type instanceof NonNull && $defaultValue) {
$context->reportError(
new Error(
self::defaultForRequiredVarMessage(
$name,
$type,
$type->getWrappedType()
),
[$defaultValue]
)
);
}
return Visitor::skipNode();
},
NodeKind::SELECTION_SET => function(SelectionSetNode $node) use ($context) {
return Visitor::skipNode();
},
NodeKind::FRAGMENT_DEFINITION => function(FragmentDefinitionNode $node) use ($context) {
return Visitor::skipNode();
},
];
}
}

View File

@ -12,11 +12,9 @@ use GraphQL\Error\Error;
use GraphQL\Type\Schema; use GraphQL\Type\Schema;
use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\FragmentDefinitionNode; use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\Node;
use GraphQL\Type\Definition\CompositeType; use GraphQL\Type\Definition\CompositeType;
use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\OutputType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Utils\TypeInfo; use GraphQL\Utils\TypeInfo;
@ -124,7 +122,7 @@ class ValidationContext
} }
/** /**
* @param $name * @param string $name
* @return FragmentDefinitionNode|null * @return FragmentDefinitionNode|null
*/ */
function getFragment($name) function getFragment($name)
@ -275,6 +273,14 @@ class ValidationContext
return $this->typeInfo->getInputType(); return $this->typeInfo->getInputType();
} }
/**
* @return InputType
*/
function getParentInputType()
{
return $this->typeInfo->getParentInputType();
}
/** /**
* @return FieldDefinition * @return FieldDefinition
*/ */

View File

@ -1,5 +1,5 @@
<?php <?php
namespace GraphQL\Tests; namespace GraphQL\Tests\Error;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
@ -37,6 +37,24 @@ class ErrorTest extends \PHPUnit_Framework_TestCase
$this->assertEquals([new SourceLocation(2, 7)], $e->getLocations()); $this->assertEquals([new SourceLocation(2, 7)], $e->getLocations());
} }
/**
* @it converts single node to positions and locations
*/
public function testConvertSingleNodeToPositionsAndLocations()
{
$source = new Source('{
field
}');
$ast = Parser::parse($source);
$fieldNode = $ast->definitions[0]->selectionSet->selections[0];
$e = new Error('msg', $fieldNode); // Non-array value.
$this->assertEquals([$fieldNode], $e->nodes);
$this->assertEquals($source, $e->getSource());
$this->assertEquals([8], $e->getPositions());
$this->assertEquals([new SourceLocation(2, 7)], $e->getLocations());
}
/** /**
* @it converts node with loc.start === 0 to positions and locations * @it converts node with loc.start === 0 to positions and locations
*/ */
@ -110,4 +128,23 @@ class ErrorTest extends \PHPUnit_Framework_TestCase
$this->assertEquals([ 'path', 3, 'to', 'field' ], $e->path); $this->assertEquals([ 'path', 3, 'to', 'field' ], $e->path);
$this->assertEquals(['message' => 'msg', 'path' => [ 'path', 3, 'to', 'field' ]], $e->toSerializableArray()); $this->assertEquals(['message' => 'msg', 'path' => [ 'path', 3, 'to', 'field' ]], $e->toSerializableArray());
} }
/**
* @it default error formatter includes extension fields
*/
public function testDefaultErrorFormatterIncludesExtensionFields()
{
$e = new Error(
'msg',
null,
null,
null,
null,
null,
['foo' => 'bar']
);
$this->assertEquals(['foo' => 'bar'], $e->getExtensions());
$this->assertEquals(['message' => 'msg', 'foo' => 'bar'], $e->toSerializableArray());
}
} }

View File

@ -0,0 +1,61 @@
<?php
namespace GraphQL\Tests\Error;
use GraphQL\Error\Error;
use GraphQL\Error\FormattedError;
use GraphQL\Language\Parser;
use GraphQL\Language\Source;
class PrintErrorTest extends \PHPUnit_Framework_TestCase
{
// Describe printError
/**
* @it prints an error with nodes from different sources
*/
public function testPrintsAnErrorWithNodesFromDifferentSources()
{
$sourceA = Parser::parse(new Source('type Foo {
field: String
}',
'SourceA'
));
$fieldTypeA = $sourceA->definitions[0]->fields[0]->type;
$sourceB = Parser::parse(new Source('type Foo {
field: Int
}',
'SourceB'
));
$fieldTypeB = $sourceB->definitions[0]->fields[0]->type;
$error = new Error(
'Example error with two nodes',
[
$fieldTypeA,
$fieldTypeB,
]
);
$this->assertEquals(
'Example error with two nodes
SourceA (2:10)
1: type Foo {
2: field: String
^
3: }
SourceB (2:10)
1: type Foo {
2: field: Int
^
3: }
',
FormattedError::printError($error)
);
}
}

View File

@ -87,9 +87,7 @@ class AbstractPromiseTest extends \PHPUnit_Framework_TestCase
} }
}'; }';
Warning::suppress(Warning::WARNING_FULL_SCHEMA_SCAN);
$result = GraphQL::execute($schema, $query); $result = GraphQL::execute($schema, $query);
Warning::enable(Warning::WARNING_FULL_SCHEMA_SCAN);
$expected = [ $expected = [
'data' => [ 'data' => [
@ -174,9 +172,7 @@ class AbstractPromiseTest extends \PHPUnit_Framework_TestCase
} }
}'; }';
Warning::suppress(Warning::WARNING_FULL_SCHEMA_SCAN);
$result = GraphQL::execute($schema, $query); $result = GraphQL::execute($schema, $query);
Warning::enable(Warning::WARNING_FULL_SCHEMA_SCAN);
$expected = [ $expected = [
'data' => [ 'data' => [

View File

@ -965,14 +965,14 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
} }
/** /**
* @it fails to execute a query containing a type definition * @it executes ignoring invalid non-executable definitions
*/ */
public function testFailsToExecuteQueryContainingTypeDefinition() public function testExecutesIgnoringInvalidNonExecutableDefinitions()
{ {
$query = Parser::parse(' $query = Parser::parse('
{ foo } { foo }
type Query { foo: String } type Query { bar: String }
'); ');
$schema = new Schema([ $schema = new Schema([
@ -988,12 +988,9 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
$result = Executor::execute($schema, $query); $result = Executor::execute($schema, $query);
$expected = [ $expected = [
'errors' => [ 'data' => [
[ 'foo' => null,
'message' => 'GraphQL cannot execute a request containing a ObjectTypeDefinition.', ],
'locations' => [['line' => 4, 'column' => 7]],
]
]
]; ];
$this->assertArraySubset($expected, $result->toArray()); $this->assertArraySubset($expected, $result->toArray());

Some files were not shown because too many files have changed in this diff Show More