This commit is contained in:
Vladimir Razuvaev 2019-03-04 19:03:55 +07:00
commit e6e9d9ea22
34 changed files with 426 additions and 213 deletions

View File

@ -20,7 +20,7 @@ build:
tools: tools:
external_code_coverage: external_code_coverage:
timeout: 600 timeout: 900
build_failure_conditions: build_failure_conditions:
- 'elements.rating(<= C).new.exists' # No new classes/methods with a rating of C or worse allowed - 'elements.rating(<= C).new.exists' # No new classes/methods with a rating of C or worse allowed

View File

@ -4,12 +4,16 @@ language: php
php: php:
- 7.1 - 7.1
- 7.2 - 7.2
- 7.3
- 7.4snapshot
- nightly - nightly
env: env:
matrix: matrix:
- EXECUTOR=coroutine - EXECUTOR= DEPENDENCIES=--prefer-lowest
- EXECUTOR=coroutine DEPENDENCIES=--prefer-lowest
- EXECUTOR= - EXECUTOR=
- EXECUTOR=coroutine
cache: cache:
@ -26,13 +30,13 @@ script: ./vendor/bin/phpunit --group default,ReactPromise
jobs: jobs:
allow_failures: allow_failures:
- php: 7.4snapshot
- php: nightly - php: nightly
include: include:
- stage: Test - stage: Test
env: DEPENDENCIES=low
install: install:
- travis_retry composer update --prefer-dist --prefer-lowest - travis_retry composer update --prefer-dist {$DEPENDENCIES}
- stage: Test - stage: Test
env: COVERAGE env: COVERAGE
@ -40,10 +44,12 @@ jobs:
- mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{.disabled,} - mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{.disabled,}
- if [[ ! $(php -m | grep -si xdebug) ]]; then echo "xdebug required for coverage"; exit 1; fi - if [[ ! $(php -m | grep -si xdebug) ]]; then echo "xdebug required for coverage"; exit 1; fi
script: script:
- ./vendor/bin/phpunit --coverage-clover clover.xml - ./vendor/bin/phpunit --coverage-php /tmp/coverage/clover_executor.cov
- EXECUTOR=coroutine ./vendor/bin/phpunit --coverage-php /tmp/coverage/clover_executor-coroutine.cov
after_script: after_script:
- wget https://scrutinizer-ci.com/ocular.phar - ./vendor/bin/phpcov merge /tmp/coverage --clover /tmp/clover.xml
- php ocular.phar code-coverage:upload --format=php-clover clover.xml - wget https://github.com/scrutinizer-ci/ocular/releases/download/1.5.2/ocular.phar
- php ocular.phar code-coverage:upload --format=php-clover /tmp/clover.xml
- stage: Code Quality - stage: Code Quality
php: 7.1 php: 7.1
@ -57,3 +63,4 @@ jobs:
env: STATIC_ANALYSIS env: STATIC_ANALYSIS
install: travis_retry composer install --prefer-dist install: travis_retry composer install --prefer-dist
script: composer static-analysis script: composer static-analysis

View File

@ -14,7 +14,7 @@ composer require webonyx/graphql-php
``` ```
## Documentation ## Documentation
Full documentation is available on the [Documentation site](http://webonyx.github.io/graphql-php/) as well Full documentation is available on the [Documentation site](https://webonyx.github.io/graphql-php/) as well
as in the [docs](docs/) folder of the distribution. as in the [docs](docs/) folder of the distribution.
If you don't know what GraphQL is, visit this [official website](http://graphql.org) If you don't know what GraphQL is, visit this [official website](http://graphql.org)

View File

@ -78,6 +78,9 @@ Parser::parse($source, [ 'allowLegacySDLImplementsInterfaces' => true])
- `AbstractQuerySecurity` renamed to `QuerySecurityRule` (NS `GraphQL\Validator\Rules`) - `AbstractQuerySecurity` renamed to `QuerySecurityRule` (NS `GraphQL\Validator\Rules`)
- `FindBreakingChanges` renamed to `BreakingChangesFinder` (NS `GraphQL\Utils`) - `FindBreakingChanges` renamed to `BreakingChangesFinder` (NS `GraphQL\Utils`)
### Breaking: new constructors
`GraphQL\Type\Definition\ResolveInfo` now takes 10 arguments instead of one array.
## Upgrade v0.11.x > v0.12.x ## Upgrade v0.11.x > v0.12.x

View File

@ -9,7 +9,7 @@
"API" "API"
], ],
"require": { "require": {
"php": "^7.1", "php": "^7.1||^8.0",
"ext-json": "*", "ext-json": "*",
"ext-mbstring": "*" "ext-mbstring": "*"
}, },
@ -19,6 +19,7 @@
"phpstan/phpstan": "0.10.5", "phpstan/phpstan": "0.10.5",
"phpstan/phpstan-phpunit": "0.10.0", "phpstan/phpstan-phpunit": "0.10.0",
"phpstan/phpstan-strict-rules": "0.10.1", "phpstan/phpstan-strict-rules": "0.10.1",
"phpunit/phpcov": "^5.0",
"phpunit/phpunit": "^7.2", "phpunit/phpunit": "^7.2",
"psr/http-message": "^1.0", "psr/http-message": "^1.0",
"react/promise": "2.*" "react/promise": "2.*"

View File

@ -1,22 +1,28 @@
# Integrations # Integrations
* [Integration with Relay](https://github.com/ivome/graphql-relay-php) * [Standard Server](executing-queries.md/#using-server) Out of the box integration with any PSR-7 compatible framework (like [Slim](http://slimframework.com) or [Zend Expressive](http://zendframework.github.io/zend-expressive/)).
* [Integration with Laravel 5](https://github.com/Folkloreatelier/laravel-graphql) + [Relay Helpers for Laravel](https://github.com/nuwave/laravel-graphql-relay) + [Nuwave Lighthouse](https://github.com/nuwave/lighthouse) * [Relay Library for graphql-php](https://github.com/ivome/graphql-relay-php) Helps construct Relay related schema definitions.
* [Symfony Bundle](https://github.com/overblog/GraphQLBundle) by Overblog * Laravel
* Out of the box integration with any PSR-7 compatible framework (like [Slim](http://slimframework.com) or [Zend Expressive](http://zendframework.github.io/zend-expressive/)) via [Standard Server](executing-queries.md/#using-server) - [Laravel GraphQL](https://github.com/Folkloreatelier/laravel-graphql) Integration with Laravel 5
- [laravel-graphql-relay](https://github.com/nuwave/laravel-graphql-relay) Relay Helpers for Laravel
- [Lighthouse](https://github.com/nuwave/lighthouse) GraphQL Server for Laravel
* [OverblogGraphQLBundle](https://github.com/overblog/GraphQLBundle) Bundle for Symfony
* [WP-GraphQL](https://github.com/wp-graphql/wp-graphql) - GraphQL API for WordPress
# GraphQL PHP Tools # GraphQL PHP Tools
* Define types with Doctrine ORM annotations ([for PHP7.1](https://github.com/Ecodev/graphql-doctrine), for [earlier PHP versions](https://github.com/rahuljayaraman/doctrine-graphql)) * [GraphQLite](https://graphqlite.thecodingmachine.io) Define your complete schema with annotations
* [DataLoader PHP](https://github.com/overblog/dataloader-php) - as a ready implementation for [deferred resolvers](data-fetching.md#solving-n1-problem) * [GraphQL Doctrine](https://github.com/Ecodev/graphql-doctrine) Define types with Doctrine ORM annotations
* [PSR 15 compliant middleware](https://github.com/phps-cans/psr7-middleware-graphql) for the Standard Server (experimental) * [DataLoaderPHP](https://github.com/overblog/dataloader-php) as a ready implementation for [deferred resolvers](data-fetching.md#solving-n1-problem)
* [GraphQL Uploads](https://github.com/Ecodev/graphql-upload) for the Standard Server * [GraphQL Uploads](https://github.com/Ecodev/graphql-upload) A PSR-15 middleware to support file uploads in GraphQL.
* [GraphQL Batch Processor](https://github.com/vasily-kartashov/graphql-batch-processing) - Simple library that provides a builder interface for defining collection, querying, filtering, and post-processing logic of batched data fetches. * [GraphQL Batch Processor](https://github.com/vasily-kartashov/graphql-batch-processing) Provides a builder interface for defining collection, querying, filtering, and post-processing logic of batched data fetches.
* [GraphQL Utils](https://github.com/simPod/GraphQL-Utils) Objective schema definition builders (no need for arrays anymore) and `DateTime` scalar
* [PSR 15 compliant middleware](https://github.com/phps-cans/psr7-middleware-graphql) for the Standard Server _(experimental)_
# General GraphQL Tools # General GraphQL Tools
* [GraphiQL](https://github.com/graphql/graphiql) - An in-browser IDE for exploring GraphQL * [GraphQL Playground](https://github.com/prismagraphql/graphql-playground) GraphQL IDE for better development workflows (GraphQL Subscriptions, interactive docs & collaboration).
* [GraphiQL](https://github.com/graphql/graphiql) An in-browser IDE for exploring GraphQL
* [ChromeiQL](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij) * [ChromeiQL](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij)
or [GraphiQL Feen](https://chrome.google.com/webstore/detail/graphiql-feen/mcbfdonlkfpbfdpimkjilhdneikhfklp) - or [GraphiQL Feen](https://chrome.google.com/webstore/detail/graphiql-feen/mcbfdonlkfpbfdpimkjilhdneikhfklp)
GraphiQL as Google Chrome extension GraphiQL as Google Chrome extension
* [GraphQL Playground](https://github.com/prismagraphql/graphql-playground) - GraphQL IDE for better development workflows (GraphQL Subscriptions, interactive docs & collaboration).

View File

@ -7,7 +7,7 @@
# About GraphQL # About GraphQL
GraphQL is a modern way to build HTTP APIs consumed by the web and mobile clients. GraphQL is a modern way to build HTTP APIs consumed by the web and mobile clients.
It is intended to be a replacement for REST and SOAP APIs (even for **existing applications**). It is intended to be an alternative to REST and SOAP APIs (even for **existing applications**).
GraphQL itself is a [specification](https://github.com/facebook/graphql) designed by Facebook GraphQL itself is a [specification](https://github.com/facebook/graphql) designed by Facebook
engineers. Various implementations of this specification were written engineers. Various implementations of this specification were written

View File

@ -11,7 +11,7 @@ use Throwable;
class Deferred class Deferred
{ {
/** @var SplQueue */ /** @var SplQueue|null */
private static $queue; private static $queue;
/** @var callable */ /** @var callable */
@ -20,21 +20,6 @@ class Deferred
/** @var SyncPromise */ /** @var SyncPromise */
public $promise; public $promise;
public static function getQueue()
{
return self::$queue ?: self::$queue = new SplQueue();
}
public static function runQueue()
{
$q = self::$queue;
while ($q && ! $q->isEmpty()) {
/** @var self $dfd */
$dfd = $q->dequeue();
$dfd->run();
}
}
public function __construct(callable $callback) public function __construct(callable $callback)
{ {
$this->callback = $callback; $this->callback = $callback;
@ -42,6 +27,25 @@ class Deferred
self::getQueue()->enqueue($this); self::getQueue()->enqueue($this);
} }
public static function getQueue() : SplQueue
{
if (self::$queue === null) {
self::$queue = new SplQueue();
}
return self::$queue;
}
public static function runQueue() : void
{
$queue = self::getQueue();
while (! $queue->isEmpty()) {
/** @var self $dequeuedNodeValue */
$dequeuedNodeValue = $queue->dequeue();
$dequeuedNodeValue->run();
}
}
public function then($onFulfilled = null, $onRejected = null) public function then($onFulfilled = null, $onRejected = null)
{ {
return $this->promise->then($onFulfilled, $onRejected); return $this->promise->then($onFulfilled, $onRejected);

View File

@ -74,7 +74,7 @@ class Executor
* execution are collected in `$result->errors`. * execution are collected in `$result->errors`.
* *
* @param mixed|null $rootValue * @param mixed|null $rootValue
* @param mixed[]|null $contextValue * @param mixed|null $contextValue
* @param mixed[]|ArrayAccess|null $variableValues * @param mixed[]|ArrayAccess|null $variableValues
* @param string|null $operationName * @param string|null $operationName
* *
@ -119,8 +119,8 @@ class Executor
* *
* Useful for async PHP platforms. * Useful for async PHP platforms.
* *
* @param mixed[]|null $rootValue * @param mixed|null $rootValue
* @param mixed[]|null $contextValue * @param mixed|null $contextValue
* @param mixed[]|null $variableValues * @param mixed[]|null $variableValues
* @param string|null $operationName * @param string|null $operationName
* *
@ -161,9 +161,9 @@ class Executor
* and returns it as the result, or if it's a function, returns the result * and returns it as the result, or if it's a function, returns the result
* of calling that function while passing along args and context. * of calling that function while passing along args and context.
* *
* @param mixed $source * @param mixed $source
* @param mixed[] $args * @param mixed[] $args
* @param mixed[]|null $context * @param mixed|null $context
* *
* @return mixed|null * @return mixed|null
*/ */

View File

@ -115,8 +115,8 @@ class ReferenceExecutor implements ExecutorImplementation
* Constructs an ExecutionContext object from the arguments passed to * Constructs an ExecutionContext object from the arguments passed to
* execute, which we will pass throughout the other execution methods. * execute, which we will pass throughout the other execution methods.
* *
* @param mixed[] $rootValue * @param mixed $rootValue
* @param mixed[] $contextValue * @param mixed $contextValue
* @param mixed[]|Traversable $rawVariableValues * @param mixed[]|Traversable $rawVariableValues
* @param string|null $operationName * @param string|null $operationName
* *
@ -153,7 +153,7 @@ class ReferenceExecutor implements ExecutorImplementation
break; break;
} }
} }
if (! $operation) { if ($operation === null) {
if ($operationName) { if ($operationName) {
$errors[] = new Error(sprintf('Unknown operation named "%s".', $operationName)); $errors[] = new Error(sprintf('Unknown operation named "%s".', $operationName));
} else { } else {
@ -165,7 +165,7 @@ class ReferenceExecutor implements ExecutorImplementation
); );
} }
$variableValues = null; $variableValues = null;
if ($operation) { if ($operation !== null) {
[$coercionErrors, $coercedVariableValues] = Values::getVariableValues( [$coercionErrors, $coercedVariableValues] = Values::getVariableValues(
$schema, $schema,
$operation->variableDefinitions ?: [], $operation->variableDefinitions ?: [],

View File

@ -152,8 +152,8 @@ class CoroutineExecutor implements Runtime, ExecutorImplementation
if (is_array($value)) { if (is_array($value)) {
$array = []; $array = [];
foreach ($value as $item) { foreach ($value as $key => $item) {
$array[] = self::resultToArray($item); $array[$key] = self::resultToArray($item);
} }
return $array; return $array;
} }

View File

@ -40,7 +40,7 @@ abstract class Node
public $loc; public $loc;
/** /**
* @param (string|NameNode|NodeList|SelectionSetNode|Location|null)[] $vars * @param (NameNode|NodeList|SelectionSetNode|Location|string|int|bool|float|null)[] $vars
*/ */
public function __construct(array $vars) public function __construct(array $vars)
{ {

View File

@ -46,6 +46,12 @@ class OperationParams
*/ */
public $variables; public $variables;
/**
* @api
* @var mixed[]|null
*/
public $extensions;
/** @var mixed[] */ /** @var mixed[] */
private $originalInput; private $originalInput;
@ -76,24 +82,38 @@ class OperationParams
'id' => null, // alias to queryid 'id' => null, // alias to queryid
'operationname' => null, 'operationname' => null,
'variables' => null, 'variables' => null,
'extensions' => null,
]; ];
if ($params['variables'] === '') { if ($params['variables'] === '') {
$params['variables'] = null; $params['variables'] = null;
} }
if (is_string($params['variables'])) { // Some parameters could be provided as serialized JSON.
$tmp = json_decode($params['variables'], true); foreach (['extensions', 'variables'] as $param) {
if (! json_last_error()) { if (! is_string($params[$param])) {
$params['variables'] = $tmp; continue;
} }
$tmp = json_decode($params[$param], true);
if (json_last_error()) {
continue;
}
$params[$param] = $tmp;
} }
$instance->query = $params['query']; $instance->query = $params['query'];
$instance->queryId = $params['queryid'] ?: $params['documentid'] ?: $params['id']; $instance->queryId = $params['queryid'] ?: $params['documentid'] ?: $params['id'];
$instance->operation = $params['operationname']; $instance->operation = $params['operationname'];
$instance->variables = $params['variables']; $instance->variables = $params['variables'];
$instance->readOnly = (bool) $readonly; $instance->extensions = $params['extensions'];
$instance->readOnly = (bool) $readonly;
// Apollo server/client compatibility: look for the queryid in extensions
if (isset($instance->extensions['persistedQuery']['sha256Hash']) && empty($instance->query) && empty($instance->queryId)) {
$instance->queryId = $instance->extensions['persistedQuery']['sha256Hash'];
}
return $instance; return $instance;
} }

View File

@ -22,7 +22,7 @@ use function sprintf;
/** /**
* Class EnumType * Class EnumType
*/ */
class EnumType extends Type implements InputType, OutputType, LeafType, NamedType class EnumType extends Type implements InputType, OutputType, LeafType, NullableType, NamedType
{ {
/** @var EnumTypeDefinitionNode|null */ /** @var EnumTypeDefinitionNode|null */
public $astNode; public $astNode;

View File

@ -18,7 +18,7 @@ use function sprintf;
/** /**
* Class InputObjectType * Class InputObjectType
*/ */
class InputObjectType extends Type implements InputType, NamedType class InputObjectType extends Type implements InputType, NullableType, NamedType
{ {
/** @var InputObjectTypeDefinitionNode|null */ /** @var InputObjectTypeDefinitionNode|null */
public $astNode; public $astNode;

View File

@ -15,7 +15,7 @@ use function sprintf;
/** /**
* Class InterfaceType * Class InterfaceType
*/ */
class InterfaceType extends Type implements AbstractType, OutputType, CompositeType, NamedType class InterfaceType extends Type implements AbstractType, OutputType, CompositeType, NullableType, NamedType
{ {
/** @var InterfaceTypeDefinitionNode|null */ /** @var InterfaceTypeDefinitionNode|null */
public $astNode; public $astNode;
@ -107,7 +107,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
* @param object $objectValue * @param object $objectValue
* @param mixed[] $context * @param mixed[] $context
* *
* @return callable|null * @return Type|null
*/ */
public function resolveType($objectValue, $context, ResolveInfo $info) public function resolveType($objectValue, $context, ResolveInfo $info)
{ {

View File

@ -7,7 +7,7 @@ namespace GraphQL\Type\Definition;
/** /**
* Class ListOfType * Class ListOfType
*/ */
class ListOfType extends Type implements WrappingType, OutputType, InputType class ListOfType extends Type implements WrappingType, OutputType, NullableType, InputType
{ {
/** @var ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType */ /** @var ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType */
public $ofType; public $ofType;

View File

@ -4,8 +4,6 @@ declare(strict_types=1);
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use Exception;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
/** /**
@ -13,13 +11,11 @@ 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|ListOfType */ /** @var NullableType */
private $ofType; private $ofType;
/** /**
* @param callable|Type $type * @param NullableType $type
*
* @throws Exception
*/ */
public function __construct($type) public function __construct($type)
{ {
@ -29,7 +25,7 @@ class NonNull extends Type implements WrappingType, OutputType, InputType
/** /**
* @param mixed $type * @param mixed $type
* *
* @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType * @return NullableType
*/ */
public static function assertNullableType($type) public static function assertNullableType($type)
{ {
@ -67,9 +63,7 @@ class NonNull extends Type implements WrappingType, OutputType, InputType
/** /**
* @param bool $recurse * @param bool $recurse
* *
* @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType * @return Type
*
* @throws InvariantViolation
*/ */
public function getWrappedType($recurse = false) public function getWrappedType($recurse = false)
{ {

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace GraphQL\Type\Definition;
/*
export type GraphQLNullableType =
| GraphQLScalarType
| GraphQLObjectType
| GraphQLInterfaceType
| GraphQLUnionType
| GraphQLEnumType
| GraphQLInputObjectType
| GraphQLList<any>;
*/
interface NullableType
{
}

View File

@ -54,7 +54,7 @@ use function sprintf;
* } * }
* ]); * ]);
*/ */
class ObjectType extends Type implements OutputType, CompositeType, NamedType class ObjectType extends Type implements OutputType, CompositeType, NullableType, NamedType
{ {
/** @var ObjectTypeDefinitionNode|null */ /** @var ObjectTypeDefinitionNode|null */
public $astNode; public $astNode;

View File

@ -23,7 +23,7 @@ class ResolveInfo
* The name of the field being resolved * The name of the field being resolved
* *
* @api * @api
* @var string|null * @var string
*/ */
public $fieldName; public $fieldName;
@ -31,9 +31,9 @@ class ResolveInfo
* AST of all nodes referencing this field in the query. * AST of all nodes referencing this field in the query.
* *
* @api * @api
* @var FieldNode[]|null * @var FieldNode[]
*/ */
public $fieldNodes; public $fieldNodes = [];
/** /**
* Expected return type of the field being resolved * Expected return type of the field being resolved
@ -47,7 +47,7 @@ class ResolveInfo
* Parent type of the field being resolved * Parent type of the field being resolved
* *
* @api * @api
* @var ObjectType|null * @var ObjectType
*/ */
public $parentType; public $parentType;
@ -55,7 +55,7 @@ class ResolveInfo
* Path to this field from the very root value * Path to this field from the very root value
* *
* @api * @api
* @var string[] * @var string[][]
*/ */
public $path; public $path;
@ -71,9 +71,9 @@ class ResolveInfo
* AST of all fragments defined in query * AST of all fragments defined in query
* *
* @api * @api
* @var FragmentDefinitionNode[]|null * @var FragmentDefinitionNode[]
*/ */
public $fragments; public $fragments = [];
/** /**
* Root value passed to query execution * Root value passed to query execution
@ -95,21 +95,29 @@ class ResolveInfo
* Array of variables passed to query execution * Array of variables passed to query execution
* *
* @api * @api
* @var mixed[]|null * @var mixed[]
*/ */
public $variableValues; public $variableValues = [];
/**
* @param FieldNode[] $fieldNodes
* @param ScalarType|ObjectType|InterfaceType|UnionType|EnumType|ListOfType|NonNull $returnType
* @param string[][] $path
* @param FragmentDefinitionNode[] $fragments
* @param mixed|null $rootValue
* @param mixed[] $variableValues
*/
public function __construct( public function __construct(
string $fieldName, string $fieldName,
$fieldNodes, $fieldNodes,
$returnType, $returnType,
ObjectType $parentType, ObjectType $parentType,
$path, array $path,
Schema $schema, Schema $schema,
$fragments, array $fragments,
$rootValue, $rootValue,
?OperationDefinitionNode $operation, ?OperationDefinitionNode $operation,
$variableValues array $variableValues
) { ) {
$this->fieldName = $fieldName; $this->fieldName = $fieldName;
$this->fieldNodes = $fieldNodes; $this->fieldNodes = $fieldNodes;

View File

@ -27,7 +27,7 @@ use function is_string;
* } * }
* } * }
*/ */
abstract class ScalarType extends Type implements OutputType, InputType, LeafType, NamedType abstract class ScalarType extends Type implements OutputType, InputType, LeafType, NullableType, NamedType
{ {
/** @var ScalarTypeDefinitionNode|null */ /** @var ScalarTypeDefinitionNode|null */
public $astNode; public $astNode;

View File

@ -137,7 +137,7 @@ abstract class Type implements JsonSerializable
} }
/** /**
* @param ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType $wrappedType * @param NullableType $wrappedType
* *
* @return NonNull * @return NonNull
* *
@ -338,7 +338,7 @@ abstract class Type implements JsonSerializable
/** /**
* @param Type $type * @param Type $type
* *
* @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType * @return NullableType
* *
* @api * @api
*/ */

View File

@ -17,7 +17,7 @@ use function sprintf;
/** /**
* Class UnionType * Class UnionType
*/ */
class UnionType extends Type implements AbstractType, OutputType, CompositeType, NamedType class UnionType extends Type implements AbstractType, OutputType, CompositeType, NullableType, NamedType
{ {
/** @var UnionTypeDefinitionNode */ /** @var UnionTypeDefinitionNode */
public $astNode; public $astNode;

View File

@ -112,27 +112,31 @@ class BreakingChangesFinder
* @return string[][] * @return string[][]
*/ */
public static function findTypesThatChangedKind( public static function findTypesThatChangedKind(
Schema $oldSchema, Schema $schemaA,
Schema $newSchema Schema $schemaB
) { ) : iterable {
$oldTypeMap = $oldSchema->getTypeMap(); $schemaATypeMap = $schemaA->getTypeMap();
$newTypeMap = $newSchema->getTypeMap(); $schemaBTypeMap = $schemaB->getTypeMap();
$breakingChanges = []; $breakingChanges = [];
foreach ($oldTypeMap as $typeName => $oldType) { foreach ($schemaATypeMap as $typeName => $schemaAType) {
if (! isset($newTypeMap[$typeName])) { if (! isset($schemaBTypeMap[$typeName])) {
continue; continue;
} }
$newType = $newTypeMap[$typeName]; $schemaBType = $schemaBTypeMap[$typeName];
if ($oldType instanceof $newType) { if ($schemaAType instanceof $schemaBType) {
continue; continue;
} }
$oldTypeKindName = self::typeKindName($oldType); if ($schemaBType instanceof $schemaAType) {
$newTypeKindName = self::typeKindName($newType); continue;
$breakingChanges[] = [ }
$schemaATypeKindName = self::typeKindName($schemaAType);
$schemaBTypeKindName = self::typeKindName($schemaBType);
$breakingChanges[] = [
'type' => self::BREAKING_CHANGE_TYPE_CHANGED_KIND, 'type' => self::BREAKING_CHANGE_TYPE_CHANGED_KIND,
'description' => "${typeName} changed from ${oldTypeKindName} to ${newTypeKindName}.", 'description' => "${typeName} changed from ${schemaATypeKindName} to ${schemaBTypeKindName}.",
]; ];
} }

View File

@ -513,7 +513,14 @@ class SchemaExtender
$schemaExtensions[] = $def; $schemaExtensions[] = $def;
} elseif ($def instanceof TypeDefinitionNode) { } elseif ($def instanceof TypeDefinitionNode) {
$typeName = isset($def->name) ? $def->name->value : null; $typeName = isset($def->name) ? $def->name->value : null;
if ($schema->getType($typeName)) {
try {
$type = $schema->getType($typeName);
} catch (Error $error) {
$type = null;
}
if ($type) {
throw new Error('Type "' . $typeName . '" already exists in the schema. It cannot also be defined in this type definition.', [$def]); throw new Error('Type "' . $typeName . '" already exists in the schema. It cannot also be defined in this type definition.', [$def]);
} }
$typeDefinitionMap[$typeName] = $def; $typeDefinitionMap[$typeName] = $def;

View File

@ -8,6 +8,7 @@ use Exception;
use GraphQL\Error\Error; use GraphQL\Error\Error;
use GraphQL\Language\AST\BooleanValueNode; use GraphQL\Language\AST\BooleanValueNode;
use GraphQL\Language\AST\EnumValueNode; use GraphQL\Language\AST\EnumValueNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FloatValueNode; use GraphQL\Language\AST\FloatValueNode;
use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\IntValueNode;
use GraphQL\Language\AST\ListValueNode; use GraphQL\Language\AST\ListValueNode;
@ -21,6 +22,7 @@ use GraphQL\Language\Printer;
use GraphQL\Language\Visitor; use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\EnumValueDefinition; use GraphQL\Type\Definition\EnumValueDefinition;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\NonNull;
@ -46,8 +48,14 @@ class ValuesOfCorrectType extends ValidationRule
{ {
public function getVisitor(ValidationContext $context) public function getVisitor(ValidationContext $context)
{ {
$fieldName = '';
return [ return [
NodeKind::NULL => static function (NullValueNode $node) use ($context) { NodeKind::FIELD => [
'enter' => static function (FieldNode $node) use (&$fieldName) {
$fieldName = $node->name->value;
},
],
NodeKind::NULL => static function (NullValueNode $node) use ($context, &$fieldName) {
$type = $context->getInputType(); $type = $context->getInputType();
if (! ($type instanceof NonNull)) { if (! ($type instanceof NonNull)) {
return; return;
@ -55,30 +63,31 @@ class ValuesOfCorrectType extends ValidationRule
$context->reportError( $context->reportError(
new Error( new Error(
self::badValueMessage((string) $type, Printer::doPrint($node)), self::getBadValueMessage((string) $type, Printer::doPrint($node), null, $context, $fieldName),
$node $node
) )
); );
}, },
NodeKind::LST => function (ListValueNode $node) use ($context) { NodeKind::LST => function (ListValueNode $node) use ($context, &$fieldName) {
// Note: TypeInfo will traverse into a list's item type, so look to the // Note: TypeInfo will traverse into a list's item type, so look to the
// parent input type to check if it is a list. // parent input type to check if it is a list.
$type = Type::getNullableType($context->getParentInputType()); $type = Type::getNullableType($context->getParentInputType());
if (! $type instanceof ListOfType) { if (! $type instanceof ListOfType) {
$this->isValidScalar($context, $node); $this->isValidScalar($context, $node, $fieldName);
return Visitor::skipNode(); return Visitor::skipNode();
} }
}, },
NodeKind::OBJECT => function (ObjectValueNode $node) use ($context) { NodeKind::OBJECT => function (ObjectValueNode $node) use ($context, &$fieldName) {
// Note: TypeInfo will traverse into a list's item type, so look to the // Note: TypeInfo will traverse into a list's item type, so look to the
// parent input type to check if it is a list. // parent input type to check if it is a list.
$type = Type::getNamedType($context->getInputType()); $type = Type::getNamedType($context->getInputType());
if (! $type instanceof InputObjectType) { if (! $type instanceof InputObjectType) {
$this->isValidScalar($context, $node); $this->isValidScalar($context, $node, $fieldName);
return Visitor::skipNode(); return Visitor::skipNode();
} }
unset($fieldName);
// Ensure every required field exists. // Ensure every required field exists.
$inputFields = $type->getFields(); $inputFields = $type->getFields();
$nodeFields = iterator_to_array($node->fields); $nodeFields = iterator_to_array($node->fields);
@ -127,34 +136,36 @@ class ValuesOfCorrectType extends ValidationRule
) )
); );
}, },
NodeKind::ENUM => function (EnumValueNode $node) use ($context) { NodeKind::ENUM => function (EnumValueNode $node) use ($context, &$fieldName) {
$type = Type::getNamedType($context->getInputType()); $type = Type::getNamedType($context->getInputType());
if (! $type instanceof EnumType) { if (! $type instanceof EnumType) {
$this->isValidScalar($context, $node); $this->isValidScalar($context, $node, $fieldName);
} elseif (! $type->getValue($node->value)) { } elseif (! $type->getValue($node->value)) {
$context->reportError( $context->reportError(
new Error( new Error(
self::badValueMessage( self::getBadValueMessage(
$type->name, $type->name,
Printer::doPrint($node), Printer::doPrint($node),
$this->enumTypeSuggestion($type, $node) $this->enumTypeSuggestion($type, $node),
$context,
$fieldName
), ),
$node $node
) )
); );
} }
}, },
NodeKind::INT => function (IntValueNode $node) use ($context) { NodeKind::INT => function (IntValueNode $node) use ($context, &$fieldName) {
$this->isValidScalar($context, $node); $this->isValidScalar($context, $node, $fieldName);
}, },
NodeKind::FLOAT => function (FloatValueNode $node) use ($context) { NodeKind::FLOAT => function (FloatValueNode $node) use ($context, &$fieldName) {
$this->isValidScalar($context, $node); $this->isValidScalar($context, $node, $fieldName);
}, },
NodeKind::STRING => function (StringValueNode $node) use ($context) { NodeKind::STRING => function (StringValueNode $node) use ($context, &$fieldName) {
$this->isValidScalar($context, $node); $this->isValidScalar($context, $node, $fieldName);
}, },
NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context) { NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context, &$fieldName) {
$this->isValidScalar($context, $node); $this->isValidScalar($context, $node, $fieldName);
}, },
]; ];
} }
@ -165,7 +176,7 @@ class ValuesOfCorrectType extends ValidationRule
($message ? "; ${message}" : '.'); ($message ? "; ${message}" : '.');
} }
private function isValidScalar(ValidationContext $context, ValueNode $node) private function isValidScalar(ValidationContext $context, ValueNode $node, $fieldName)
{ {
// Report any error at the full type expected by the location. // Report any error at the full type expected by the location.
$locationType = $context->getInputType(); $locationType = $context->getInputType();
@ -179,10 +190,12 @@ class ValuesOfCorrectType extends ValidationRule
if (! $type instanceof ScalarType) { if (! $type instanceof ScalarType) {
$context->reportError( $context->reportError(
new Error( new Error(
self::badValueMessage( self::getBadValueMessage(
(string) $locationType, (string) $locationType,
Printer::doPrint($node), Printer::doPrint($node),
$this->enumTypeSuggestion($type, $node) $this->enumTypeSuggestion($type, $node),
$context,
$fieldName
), ),
$node $node
) )
@ -199,32 +212,28 @@ class ValuesOfCorrectType extends ValidationRule
// Ensure a reference to the original error is maintained. // Ensure a reference to the original error is maintained.
$context->reportError( $context->reportError(
new Error( new Error(
self::badValueMessage( self::getBadValueMessage(
(string) $locationType, (string) $locationType,
Printer::doPrint($node), Printer::doPrint($node),
$error->getMessage() $error->getMessage(),
$context,
$fieldName
), ),
$node, $node
null,
null,
null,
$error
) )
); );
} catch (Throwable $error) { } catch (Throwable $error) {
// Ensure a reference to the original error is maintained. // Ensure a reference to the original error is maintained.
$context->reportError( $context->reportError(
new Error( new Error(
self::badValueMessage( self::getBadValueMessage(
(string) $locationType, (string) $locationType,
Printer::doPrint($node), Printer::doPrint($node),
$error->getMessage() $error->getMessage(),
$context,
$fieldName
), ),
$node, $node
null,
null,
null,
$error
) )
); );
} }
@ -247,6 +256,12 @@ class ValuesOfCorrectType extends ValidationRule
} }
} }
public static function badArgumentValueMessage($typeName, $valueName, $fieldName, $argName, $message = null)
{
return sprintf('Field "%s" argument "%s" requires type %s, found %s', $fieldName, $argName, $typeName, $valueName) .
($message ? sprintf('; %s', $message) : '.');
}
public static function requiredFieldMessage($typeName, $fieldName, $fieldTypeName) public static function requiredFieldMessage($typeName, $fieldName, $fieldTypeName)
{ {
return sprintf('Field %s.%s of required type %s was not provided.', $typeName, $fieldName, $fieldTypeName); return sprintf('Field %s.%s of required type %s was not provided.', $typeName, $fieldName, $fieldTypeName);
@ -257,4 +272,15 @@ class ValuesOfCorrectType extends ValidationRule
return sprintf('Field "%s" is not defined by type %s', $fieldName, $typeName) . return sprintf('Field "%s" is not defined by type %s', $fieldName, $typeName) .
($message ? sprintf('; %s', $message) : '.'); ($message ? sprintf('; %s', $message) : '.');
} }
private static function getBadValueMessage($typeName, $valueName, $message = null, $context = null, $fieldName = null)
{
if ($context) {
$arg = $context->getArgument();
if ($arg) {
return self::badArgumentValueMessage($typeName, $valueName, $fieldName, $arg->name, $message);
}
}
return self::badValueMessage($typeName, $valueName, $message);
}
} }

View File

@ -6,6 +6,7 @@ namespace GraphQL\Tests\Executor;
use GraphQL\Executor\Executor; use GraphQL\Executor\Executor;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema; use GraphQL\Type\Schema;
@ -20,6 +21,13 @@ class ExecutorSchemaTest extends TestCase
*/ */
public function testExecutesUsingASchema() : void public function testExecutesUsingASchema() : void
{ {
$BlogSerializableValueType = new CustomScalarType([
'name' => 'JsonSerializableValueScalar',
'serialize' => static function ($value) {
return $value;
},
]);
$BlogArticle = null; $BlogArticle = null;
$BlogImage = new ObjectType([ $BlogImage = new ObjectType([
'name' => 'Image', 'name' => 'Image',
@ -57,6 +65,7 @@ class ExecutorSchemaTest extends TestCase
'title' => ['type' => Type::string()], 'title' => ['type' => Type::string()],
'body' => ['type' => Type::string()], 'body' => ['type' => Type::string()],
'keywords' => ['type' => Type::listOf(Type::string())], 'keywords' => ['type' => Type::listOf(Type::string())],
'meta' => ['type' => $BlogSerializableValueType],
], ],
]); ]);
@ -113,6 +122,7 @@ class ExecutorSchemaTest extends TestCase
keywords keywords
} }
} }
meta
} }
} }
@ -191,6 +201,7 @@ class ExecutorSchemaTest extends TestCase
'keywords' => ['foo', 'bar', '1', 'true', null], 'keywords' => ['foo', 'bar', '1', 'true', null],
], ],
], ],
'meta' => [ 'title' => 'My Article 1 | My Blog' ],
], ],
], ],
]; ];
@ -210,6 +221,7 @@ class ExecutorSchemaTest extends TestCase
'body' => 'This is a post', 'body' => 'This is a post',
'hidden' => 'This data is not exposed in the schema', 'hidden' => 'This data is not exposed in the schema',
'keywords' => ['foo', 'bar', 1, true, null], 'keywords' => ['foo', 'bar', 1, true, null],
'meta' => ['title' => 'My Article 1 | My Blog'],
]; ];
}; };

View File

@ -25,7 +25,7 @@ class RequestParsingTest extends TestCase
]; ];
foreach ($parsed as $source => $parsedBody) { foreach ($parsed as $source => $parsedBody) {
self::assertValidOperationParams($parsedBody, $query, null, null, null, $source); self::assertValidOperationParams($parsedBody, $query, null, null, null, null, $source);
self::assertFalse($parsedBody->isReadOnly(), $source); self::assertFalse($parsedBody->isReadOnly(), $source);
} }
} }
@ -91,6 +91,7 @@ class RequestParsingTest extends TestCase
$queryId = null, $queryId = null,
$variables = null, $variables = null,
$operation = null, $operation = null,
$extensions = null,
$message = '' $message = ''
) { ) {
self::assertInstanceOf(OperationParams::class, $params, $message); self::assertInstanceOf(OperationParams::class, $params, $message);
@ -99,6 +100,7 @@ class RequestParsingTest extends TestCase
self::assertSame($queryId, $params->queryId, $message); self::assertSame($queryId, $params->queryId, $message);
self::assertSame($variables, $params->variables, $message); self::assertSame($variables, $params->variables, $message);
self::assertSame($operation, $params->operation, $message); self::assertSame($operation, $params->operation, $message);
self::assertSame($extensions, $params->extensions, $message);
} }
public function testParsesUrlencodedRequest() : void public function testParsesUrlencodedRequest() : void
@ -118,7 +120,7 @@ class RequestParsingTest extends TestCase
]; ];
foreach ($parsed as $method => $parsedBody) { foreach ($parsed as $method => $parsedBody) {
self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, $method); self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, null, $method);
self::assertFalse($parsedBody->isReadOnly(), $method); self::assertFalse($parsedBody->isReadOnly(), $method);
} }
} }
@ -175,7 +177,7 @@ class RequestParsingTest extends TestCase
]; ];
foreach ($parsed as $method => $parsedBody) { foreach ($parsed as $method => $parsedBody) {
self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, $method); self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, null, $method);
self::assertTrue($parsedBody->isReadonly(), $method); self::assertTrue($parsedBody->isReadonly(), $method);
} }
} }
@ -230,7 +232,7 @@ class RequestParsingTest extends TestCase
]; ];
foreach ($parsed as $method => $parsedBody) { foreach ($parsed as $method => $parsedBody) {
self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, $method); self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, null, $method);
self::assertFalse($parsedBody->isReadOnly(), $method); self::assertFalse($parsedBody->isReadOnly(), $method);
} }
} }
@ -286,19 +288,21 @@ class RequestParsingTest extends TestCase
'psr' => $this->parsePsrRequest('application/json', json_encode($body)), 'psr' => $this->parsePsrRequest('application/json', json_encode($body)),
]; ];
foreach ($parsed as $method => $parsedBody) { foreach ($parsed as $method => $parsedBody) {
self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, $method); self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, null, $method);
self::assertFalse($parsedBody->isReadOnly(), $method); self::assertFalse($parsedBody->isReadOnly(), $method);
} }
} }
public function testParsesVariablesAsJSON() : void public function testParsesParamsAsJSON() : void
{ {
$query = '{my query}'; $query = '{my query}';
$variables = ['test' => 1, 'test2' => 2]; $variables = ['test1' => 1, 'test2' => 2];
$operation = 'op'; $extensions = ['test3' => 3, 'test4' => 4];
$operation = 'op';
$body = [ $body = [
'query' => $query, 'query' => $query,
'extensions' => json_encode($extensions),
'variables' => json_encode($variables), 'variables' => json_encode($variables),
'operationName' => $operation, 'operationName' => $operation,
]; ];
@ -307,7 +311,7 @@ class RequestParsingTest extends TestCase
'psr' => $this->parsePsrRequest('application/json', json_encode($body)), 'psr' => $this->parsePsrRequest('application/json', json_encode($body)),
]; ];
foreach ($parsed as $method => $parsedBody) { foreach ($parsed as $method => $parsedBody) {
self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, $method); self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, $extensions, $method);
self::assertFalse($parsedBody->isReadOnly(), $method); self::assertFalse($parsedBody->isReadOnly(), $method);
} }
} }
@ -328,7 +332,29 @@ class RequestParsingTest extends TestCase
'psr' => $this->parsePsrRequest('application/json', json_encode($body)), 'psr' => $this->parsePsrRequest('application/json', json_encode($body)),
]; ];
foreach ($parsed as $method => $parsedBody) { foreach ($parsed as $method => $parsedBody) {
self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, $method); self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, null, $method);
self::assertFalse($parsedBody->isReadOnly(), $method);
}
}
public function testParsesApolloPersistedQueryJSONRequest() : void
{
$queryId = 'my-query-id';
$extensions = ['persistedQuery' => ['sha256Hash' => $queryId]];
$variables = ['test' => 1, 'test2' => 2];
$operation = 'op';
$body = [
'extensions' => $extensions,
'variables' => $variables,
'operationName' => $operation,
];
$parsed = [
'raw' => $this->parseRawRequest('application/json', json_encode($body)),
'psr' => $this->parsePsrRequest('application/json', json_encode($body)),
];
foreach ($parsed as $method => $parsedBody) {
self::assertValidOperationParams($parsedBody, null, $queryId, $variables, $operation, $extensions, $method);
self::assertFalse($parsedBody->isReadOnly(), $method); self::assertFalse($parsedBody->isReadOnly(), $method);
} }
} }
@ -360,6 +386,7 @@ class RequestParsingTest extends TestCase
null, null,
$body[0]['variables'], $body[0]['variables'],
$body[0]['operationName'], $body[0]['operationName'],
null,
$method $method
); );
self::assertValidOperationParams( self::assertValidOperationParams(
@ -368,6 +395,7 @@ class RequestParsingTest extends TestCase
$body[1]['queryId'], $body[1]['queryId'],
$body[1]['variables'], $body[1]['variables'],
$body[1]['operationName'], $body[1]['operationName'],
null,
$method $method
); );
} }

View File

@ -231,7 +231,7 @@ class EnumTypeTest extends TestCase
'{ colorEnum(fromEnum: "GREEN") }', '{ colorEnum(fromEnum: "GREEN") }',
null, null,
[ [
'message' => 'Expected type Color, found "GREEN"; Did you mean the enum value GREEN?', 'message' => 'Field "colorEnum" argument "fromEnum" requires type Color, found "GREEN"; Did you mean the enum value GREEN?',
'locations' => [new SourceLocation(1, 23)], 'locations' => [new SourceLocation(1, 23)],
] ]
); );
@ -268,7 +268,7 @@ class EnumTypeTest extends TestCase
'{ colorEnum(fromEnum: GREENISH) }', '{ colorEnum(fromEnum: GREENISH) }',
null, null,
[ [
'message' => 'Expected type Color, found GREENISH; Did you mean the enum value GREEN?', 'message' => 'Field "colorEnum" argument "fromEnum" requires type Color, found GREENISH; Did you mean the enum value GREEN?',
'locations' => [new SourceLocation(1, 23)], 'locations' => [new SourceLocation(1, 23)],
] ]
); );
@ -283,7 +283,7 @@ class EnumTypeTest extends TestCase
'{ colorEnum(fromEnum: green) }', '{ colorEnum(fromEnum: green) }',
null, null,
[ [
'message' => 'Expected type Color, found green; Did you mean the enum value GREEN?', 'message' => 'Field "colorEnum" argument "fromEnum" requires type Color, found green; Did you mean the enum value GREEN?',
'locations' => [new SourceLocation(1, 23)], 'locations' => [new SourceLocation(1, 23)],
] ]
); );
@ -313,7 +313,7 @@ class EnumTypeTest extends TestCase
$this->expectFailure( $this->expectFailure(
'{ colorEnum(fromEnum: 1) }', '{ colorEnum(fromEnum: 1) }',
null, null,
'Expected type Color, found 1.' 'Field "colorEnum" argument "fromEnum" requires type Color, found 1.'
); );
} }
@ -325,7 +325,7 @@ class EnumTypeTest extends TestCase
$this->expectFailure( $this->expectFailure(
'{ colorEnum(fromInt: GREEN) }', '{ colorEnum(fromInt: GREEN) }',
null, null,
'Expected type Int, found GREEN.' 'Field "colorEnum" argument "fromInt" requires type Int, found GREEN.'
); );
} }

View File

@ -125,6 +125,47 @@ class BreakingChangesFinderTest extends TestCase
); );
} }
/**
* We need to compare type of class A (old type) and type of class B (new type)
* Class B extends A but are evaluated as same types (if all properties match).
* The reason is that when constructing schema from remote schema,
* we have no certain way to get information about our classes.
* Thus object types from remote schema are constructed as Object Type
* while their local counterparts are usually a subclass of Object Type.
*
* @see https://github.com/webonyx/graphql-php/pull/431
*/
public function testShouldNotMarkTypesWithInheritedClassesAsChanged() : void
{
$objectTypeConstructedFromRemoteSchema = new ObjectType([
'name' => 'ObjectType',
'fields' => [
'field1' => ['type' => Type::string()],
],
]);
$localObjectType = new class([
'name' => 'ObjectType',
'fields' => [
'field1' => ['type' => Type::string()],
],
]) extends ObjectType{
};
$schemaA = new Schema([
'query' => $this->queryType,
'types' => [$objectTypeConstructedFromRemoteSchema],
]);
$schemaB = new Schema([
'query' => $this->queryType,
'types' => [$localObjectType],
]);
self::assertEmpty(BreakingChangesFinder::findTypesThatChangedKind($schemaA, $schemaB));
self::assertEmpty(BreakingChangesFinder::findTypesThatChangedKind($schemaB, $schemaA));
}
/** /**
* @see it('should detect if a field on a type was deleted or changed type') * @see it('should detect if a field on a type was deleted or changed type')
*/ */

View File

@ -24,6 +24,7 @@ use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Schema; use GraphQL\Type\Schema;
use GraphQL\Utils\BuildSchema;
use GraphQL\Utils\SchemaExtender; use GraphQL\Utils\SchemaExtender;
use GraphQL\Utils\SchemaPrinter; use GraphQL\Utils\SchemaPrinter;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -1962,4 +1963,48 @@ extend type Query {
$result = GraphQL::executeQuery($extendedSchema, $query); $result = GraphQL::executeQuery($extendedSchema, $query);
self::assertSame(['data' => ['hello' => 'Hello World!']], $result->toArray()); self::assertSame(['data' => ['hello' => 'Hello World!']], $result->toArray());
} }
/**
* @see https://github.com/webonyx/graphql-php/issues/180
*/
public function testShouldBeAbleToIntroduceNewTypesThroughExtension()
{
$sdl = '
type Query {
defaultValue: String
}
type Foo {
value: Int
}
';
$documentNode = Parser::parse($sdl);
$schema = BuildSchema::build($documentNode);
$extensionSdl = '
type Bar {
foo: Foo
}
';
$extendedDocumentNode = Parser::parse($extensionSdl);
$extendedSchema = SchemaExtender::extend($schema, $extendedDocumentNode);
$expected = '
type Bar {
foo: Foo
}
type Foo {
value: Int
}
type Query {
defaultValue: String
}
';
static::assertEquals($this->dedent($expected), SchemaPrinter::doPrint($extendedSchema));
}
} }

View File

@ -38,7 +38,7 @@ class ValidationTest extends ValidatorTestCase
'; ';
$expectedError = [ $expectedError = [
'message' => 'Expected type Invalid, found "bad value"; Invalid scalar is always invalid: bad value', 'message' => 'Field "invalidArg" argument "arg" requires type Invalid, found "bad value"; Invalid scalar is always invalid: bad value',
'locations' => [['line' => 3, 'column' => 25]], 'locations' => [['line' => 3, 'column' => 25]],
]; ];

View File

@ -240,7 +240,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('String', '1', 4, 39), $this->badValueWithMessage('Field "stringArgField" argument "stringArg" requires type String, found 1.', 4, 39),
] ]
); );
} }
@ -257,6 +257,11 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
); );
} }
private function badValueWithMessage($message, $line, $column)
{
return FormattedError::create($message, [new SourceLocation($line, $column)]);
}
/** /**
* @see it('Float into String') * @see it('Float into String')
*/ */
@ -272,7 +277,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('String', '1.0', 4, 39), $this->badValueWithMessage('Field "stringArgField" argument "stringArg" requires type String, found 1.0.', 4, 39),
] ]
); );
} }
@ -294,7 +299,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('String', 'true', 4, 39), $this->badValueWithMessage('Field "stringArgField" argument "stringArg" requires type String, found true.', 4, 39),
] ]
); );
} }
@ -314,7 +319,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('String', 'BAR', 4, 39), $this->badValueWithMessage('Field "stringArgField" argument "stringArg" requires type String, found BAR.', 4, 39),
] ]
); );
} }
@ -334,7 +339,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('Int', '"3"', 4, 33), $this->badValueWithMessage('Field "intArgField" argument "intArg" requires type Int, found "3".', 4, 33),
] ]
); );
} }
@ -354,7 +359,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('Int', '829384293849283498239482938', 4, 33), $this->badValueWithMessage('Field "intArgField" argument "intArg" requires type Int, found 829384293849283498239482938.', 4, 33),
] ]
); );
} }
@ -376,7 +381,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('Int', 'FOO', 4, 33), $this->badValueWithMessage('Field "intArgField" argument "intArg" requires type Int, found FOO.', 4, 33),
] ]
); );
} }
@ -396,7 +401,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('Int', '3.0', 4, 33), $this->badValueWithMessage('Field "intArgField" argument "intArg" requires type Int, found 3.0.', 4, 33),
] ]
); );
} }
@ -416,7 +421,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('Int', '3.333', 4, 33), $this->badValueWithMessage('Field "intArgField" argument "intArg" requires type Int, found 3.333.', 4, 33),
] ]
); );
} }
@ -436,7 +441,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('Float', '"3.333"', 4, 37), $this->badValueWithMessage('Field "floatArgField" argument "floatArg" requires type Float, found "3.333".', 4, 37),
] ]
); );
} }
@ -456,7 +461,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('Float', 'true', 4, 37), $this->badValueWithMessage('Field "floatArgField" argument "floatArg" requires type Float, found true.', 4, 37),
] ]
); );
} }
@ -478,7 +483,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('Float', 'FOO', 4, 37), $this->badValueWithMessage('Field "floatArgField" argument "floatArg" requires type Float, found FOO.', 4, 37),
] ]
); );
} }
@ -498,7 +503,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('Boolean', '2', 4, 41), $this->badValueWithMessage('Field "booleanArgField" argument "booleanArg" requires type Boolean, found 2.', 4, 41),
] ]
); );
} }
@ -518,7 +523,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('Boolean', '1.0', 4, 41), $this->badValueWithMessage('Field "booleanArgField" argument "booleanArg" requires type Boolean, found 1.0.', 4, 41),
] ]
); );
} }
@ -540,7 +545,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('Boolean', '"true"', 4, 41), $this->badValueWithMessage('Field "booleanArgField" argument "booleanArg" requires type Boolean, found "true".', 4, 41),
] ]
); );
} }
@ -560,7 +565,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('Boolean', 'TRUE', 4, 41), $this->badValueWithMessage('Field "booleanArgField" argument "booleanArg" requires type Boolean, found TRUE.', 4, 41),
] ]
); );
} }
@ -580,7 +585,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('ID', '1.0', 4, 31), $this->badValueWithMessage('Field "idArgField" argument "idArg" requires type ID, found 1.0.', 4, 31),
] ]
); );
} }
@ -600,7 +605,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('ID', 'true', 4, 31), $this->badValueWithMessage('Field "idArgField" argument "idArg" requires type ID, found true.', 4, 31),
] ]
); );
} }
@ -622,7 +627,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('ID', 'SOMETHING', 4, 31), $this->badValueWithMessage('Field "idArgField" argument "idArg" requires type ID, found SOMETHING.', 4, 31),
] ]
); );
} }
@ -642,7 +647,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('DogCommand', '2', 4, 41), $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found 2.', 4, 41),
] ]
); );
} }
@ -662,7 +667,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('DogCommand', '1.0', 4, 41), $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found 1.0.', 4, 41),
] ]
); );
} }
@ -684,13 +689,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue( $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found "SIT"; Did you mean the enum value SIT?', 4, 41),
'DogCommand',
'"SIT"',
4,
41,
'Did you mean the enum value SIT?'
),
] ]
); );
} }
@ -710,7 +709,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('DogCommand', 'true', 4, 41), $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found true.', 4, 41),
] ]
); );
} }
@ -730,7 +729,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('DogCommand', 'JUGGLE', 4, 41), $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found JUGGLE.', 4, 41),
] ]
); );
} }
@ -750,13 +749,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue( $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found sit; Did you mean the enum value SIT?', 4, 41),
'DogCommand',
'sit',
4,
41,
'Did you mean the enum value SIT?'
),
] ]
); );
} }
@ -846,7 +839,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('String', '2', 4, 55), $this->badValueWithMessage('Field "stringListArgField" argument "stringListArg" requires type String, found 2.', 4, 55),
] ]
); );
} }
@ -866,7 +859,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('[String]', '1', 4, 47), $this->badValueWithMessage('Field "stringListArgField" argument "stringListArg" requires type [String], found 1.', 4, 47),
] ]
); );
} }
@ -1060,8 +1053,8 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('Int!', '"two"', 4, 32), $this->badValueWithMessage('Field "multipleReqs" argument "req2" requires type Int!, found "two".', 4, 32),
$this->badValue('Int!', '"one"', 4, 45), $this->badValueWithMessage('Field "multipleReqs" argument "req1" requires type Int!, found "one".', 4, 45),
] ]
); );
} }
@ -1081,7 +1074,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('Int!', '"one"', 4, 32), $this->badValueWithMessage('Field "multipleReqs" argument "req1" requires type Int!, found "one".', 4, 32),
] ]
); );
} }
@ -1103,7 +1096,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('Int!', 'null', 4, 32), $this->badValueWithMessage('Field "multipleReqs" argument "req1" requires type Int!, found null.', 4, 32),
] ]
); );
} }
@ -1277,7 +1270,7 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('String', '2', 5, 40), $this->badValueWithMessage('Field "complexArgField" argument "complexArg" requires type String, found 2.', 5, 40),
] ]
); );
} }
@ -1340,19 +1333,13 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue( $this->badValueWithMessage('Field "invalidArg" argument "arg" requires type Invalid, found 123; Invalid scalar is always invalid: 123', 3, 27),
'Invalid',
'123',
3,
27,
'Invalid scalar is always invalid: 123'
),
] ]
); );
self::assertEquals( self::assertEquals(
'Invalid scalar is always invalid: 123', 'Field "invalidArg" argument "arg" requires type Invalid, found 123; Invalid scalar is always invalid: 123',
$errors[0]->getPrevious()->getMessage() $errors[0]->getMessage()
); );
} }
@ -1411,8 +1398,8 @@ class ValuesOfCorrectTypeTest extends ValidatorTestCase
} }
', ',
[ [
$this->badValue('Boolean!', '"yes"', 3, 28), $this->badValueWithMessage('Field "dog" argument "if" requires type Boolean!, found "yes".', 3, 28),
$this->badValue('Boolean!', 'ENUM', 4, 28), $this->badValueWithMessage('Field "name" argument "if" requires type Boolean!, found ENUM.', 4, 28),
] ]
); );
} }