From 236021acf8e9ba01382c76f37c5646966c87a4eb Mon Sep 17 00:00:00 2001 From: vladar Date: Wed, 19 Oct 2016 01:34:46 +0700 Subject: [PATCH] Added deprecated directive; changed custom directives handling in schema; various minor tweaks --- UPGRADE.md | 24 ++++++++- src/Executor/Values.php | 7 ++- src/GraphQL.php | 9 ++++ src/Language/AST/Document.php | 2 +- src/Language/SourceLocation.php | 25 +++++++++- src/Schema.php | 39 +++++++++++++-- src/Type/Definition/Directive.php | 44 +++++++++++++++++ src/Type/Definition/FieldDefinition.php | 1 - src/Type/Definition/InputObjectField.php | 1 - src/Type/Definition/ResolveInfo.php | 5 ++ src/Type/Definition/UnionType.php | 5 ++ src/Utils.php | 25 ++++++++++ tests/Type/DefinitionTest.php | 63 ++++++++++++++++++++++++ tests/Validator/TestCase.php | 5 +- 14 files changed, 238 insertions(+), 17 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 4ba9f44..976a195 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -2,7 +2,27 @@ ## Upgrade v0.7.x > v1.0.x -### 1. Protected property and method naming +### 1. Custom directives handling +When passing custom directives to schema, default directives (like `@skip` and `@include`) +are not added to schema automatically anymore. If you need them - add them explicitly with your other directives + +Before the change: +```php +$schema = new Schema([ + // ... + 'directives' => [$myDirective] +]); +``` + +After the change: +```php +$schema = new Schema([ + // ... + 'directives' => array_merge(GraphQL::getInternalDirectives(), [$myDirective]) +]); +``` + +### 2. Protected property and method naming In order to unify coding style, leading underscores were removed from all private and protected properties and methods. @@ -19,7 +39,7 @@ GraphQL\Schema::$queryType; So if you rely on any protected properties or methods of any GraphQL class, make sure to delete leading underscores. -### 2. Returning closure from field resolver +### 3. Returning closure from field resolver Previously when you returned closure from any resolver, expected signature of this closure was `function($sourceValue)`, new signature is `function($args, $context)` (now mirrors reference graphql-js implementation) diff --git a/src/Executor/Values.php b/src/Executor/Values.php index f4675a2..0300cee 100644 --- a/src/Executor/Values.php +++ b/src/Executor/Values.php @@ -234,11 +234,10 @@ class Values if ($type instanceof ListOfType) { $itemType = $type->getWrappedType(); - // TODO: support iterable input - if (is_array($value)) { - return array_map(function ($item) use ($itemType) { + if (is_array($value) || $value instanceof \Traversable) { + return Utils::map($value, function($item) use ($itemType) { return Values::coerceValue($itemType, $item); - }, $value); + }); } else { return [self::coerceValue($itemType, $value)]; } diff --git a/src/GraphQL.php b/src/GraphQL.php index 4e25181..ea2e291 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -6,6 +6,7 @@ use GraphQL\Executor\Executor; use GraphQL\Language\AST\Document; use GraphQL\Language\Parser; use GraphQL\Language\Source; +use GraphQL\Type\Definition\Directive; use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\Rules\QueryComplexity; @@ -57,4 +58,12 @@ class GraphQL return new ExecutionResult(null, [$e]); } } + + /** + * @return array + */ + public static function getInternalDirectives() + { + return array_values(Directive::getInternalDirectives()); + } } diff --git a/src/Language/AST/Document.php b/src/Language/AST/Document.php index e071555..800f48b 100644 --- a/src/Language/AST/Document.php +++ b/src/Language/AST/Document.php @@ -6,7 +6,7 @@ class Document extends Node public $kind = Node::DOCUMENT; /** - * @var array + * @var Definition[] */ public $definitions; } diff --git a/src/Language/SourceLocation.php b/src/Language/SourceLocation.php index eec2bb3..d09363b 100644 --- a/src/Language/SourceLocation.php +++ b/src/Language/SourceLocation.php @@ -1,7 +1,7 @@ column = $col; } + /** + * @return array + */ public function toArray() { return [ @@ -19,4 +22,24 @@ class SourceLocation 'column' => $this->column ]; } + + /** + * @return array + */ + public function toSerializableArray() + { + return $this->toArray(); + } + + /** + * Specify data which should be serialized to JSON + * @link http://php.net/manual/en/jsonserializable.jsonserialize.php + * @return mixed data which can be serialized by json_encode, + * which is a value of any type other than a resource. + * @since 5.4.0 + */ + function jsonSerialize() + { + return $this->toSerializableArray(); + } } diff --git a/src/Schema.php b/src/Schema.php index 83b05bd..c9996ca 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -12,7 +12,30 @@ use GraphQL\Type\Definition\WrappingType; use GraphQL\Type\Introspection; /** - * Class Schema + * Schema Definition + * + * A Schema is created by supplying the root types of each type of operation: + * query, mutation (optional) and subscription (optional). A schema definition is + * then supplied to the validator and executor. + * + * Example: + * + * $schema = new GraphQL\Schema([ + * 'query' => $MyAppQueryRootType, + * 'mutation' => $MyAppMutationRootType, + * ]); + * + * Note: If an array of `directives` are provided to GraphQL\Schema, that will be + * the exact list of directives represented and allowed. If `directives` is not + * provided then a default set of the specified directives (e.g. @include and + * @skip) will be used. If you wish to provide *additional* directives to these + * specified directives, you must explicitly declare them. Example: + * + * $mySchema = new GraphQL\Schema([ + * ... + * 'directives' => array_merge(GraphQL::getInternalDirectives(), [ $myCustomDirective ]), + * ]) + * * @package GraphQL */ class Schema @@ -119,10 +142,7 @@ class Schema "Schema directives must be Directive[] if provided but got " . Utils::getVariableType($config['directives']) ); - $this->directives = array_merge($config['directives'], [ - Directive::includeDirective(), - Directive::skipDirective() - ]); + $this->directives = $config['directives'] ?: GraphQL::getInternalDirectives(); // Build type map now to detect any errors within this schema. $initialTypes = [ @@ -222,6 +242,15 @@ class Schema foreach ($this->getPossibleTypes($abstractType) as $type) { $tmp[$type->name] = true; } + + Utils::invariant( + !empty($tmp), + 'Could not find possible implementing types for $%s ' . + 'in schema. Check that schema.types is defined and is an array of ' . + 'all possible types in the schema.', + $abstractType->name + ); + $this->possibleTypeMap[$abstractType->name] = $tmp; } diff --git a/src/Type/Definition/Directive.php b/src/Type/Definition/Directive.php index d815dfa..e5a6887 100644 --- a/src/Type/Definition/Directive.php +++ b/src/Type/Definition/Directive.php @@ -7,6 +7,8 @@ namespace GraphQL\Type\Definition; */ class Directive { + const DEFAULT_DEPRECATION_REASON = 'No longer supported'; + /** * @var array */ @@ -16,6 +18,7 @@ class Directive * @var array */ public static $directiveLocations = [ + // Operations: 'QUERY' => 'QUERY', 'MUTATION' => 'MUTATION', 'SUBSCRIPTION' => 'SUBSCRIPTION', @@ -23,6 +26,19 @@ class Directive 'FRAGMENT_DEFINITION' => 'FRAGMENT_DEFINITION', 'FRAGMENT_SPREAD' => 'FRAGMENT_SPREAD', 'INLINE_FRAGMENT' => 'INLINE_FRAGMENT', + + // Schema Definitions + 'SCHEMA' => 'SCHEMA', + 'SCALAR' => 'SCALAR', + 'OBJECT' => 'OBJECT', + 'FIELD_DEFINITION' => 'FIELD_DEFINITION', + 'ARGUMENT_DEFINITION' => 'ARGUMENT_DEFINITION', + 'INTERFACE' => 'INTERFACE', + 'UNION' => 'UNION', + 'ENUM' => 'ENUM', + 'ENUM_VALUE' => 'ENUM_VALUE', + 'INPUT_OBJECT' => 'INPUT_OBJECT', + 'INPUT_FIELD_DEFINITION' => 'INPUT_FIELD_DEFINITION' ]; /** @@ -43,6 +59,15 @@ class Directive return $internal['skip']; } + /** + * @return Directive + */ + public static function deprecatedDirective() + { + $internal = self::getInternalDirectives(); + return $internal['deprecated']; + } + /** * @return array */ @@ -81,6 +106,25 @@ class Directive 'description' => 'Skipped when true' ]) ] + ]), + 'deprecated' => new self([ + 'name' => 'deprecated', + 'description' => 'Marks an element of a GraphQL schema as no longer supported.', + 'locations' => [ + self::$directiveLocations['FIELD_DEFINITION'], + self::$directiveLocations['ENUM_VALUE'] + ], + 'args' => [ + new FieldArgument([ + 'name' => 'reason', + 'type' => Type::string(), + 'description' => + 'Explains why this element was deprecated, usually also including a ' . + 'suggestion for how to access supported similar data. Formatted ' . + 'in [Markdown](https://daringfireball.net/projects/markdown/).', + 'defaultValue' => self::DEFAULT_DEPRECATION_REASON + ]) + ] ]) ]; } diff --git a/src/Type/Definition/FieldDefinition.php b/src/Type/Definition/FieldDefinition.php index 4d2bc50..44c380b 100644 --- a/src/Type/Definition/FieldDefinition.php +++ b/src/Type/Definition/FieldDefinition.php @@ -149,7 +149,6 @@ class FieldDefinition } /** - * @deprecated as of 17.10.2016 in favor of setting 'fields' as closure per ObjectType vs setting on field level * @return Type */ public function getType() diff --git a/src/Type/Definition/InputObjectField.php b/src/Type/Definition/InputObjectField.php index 98aa8ed..aafe4bc 100644 --- a/src/Type/Definition/InputObjectField.php +++ b/src/Type/Definition/InputObjectField.php @@ -39,7 +39,6 @@ class InputObjectField } /** - * @deprecated in favor of defining all object 'fields' as closure vs defining closure per field * @return mixed */ public function getType() diff --git a/src/Type/Definition/ResolveInfo.php b/src/Type/Definition/ResolveInfo.php index e0741c0..6c7c49e 100644 --- a/src/Type/Definition/ResolveInfo.php +++ b/src/Type/Definition/ResolveInfo.php @@ -36,6 +36,11 @@ class ResolveInfo */ public $parentType; + /** + * @var array + */ + public $path; + /** * @var Schema */ diff --git a/src/Type/Definition/UnionType.php b/src/Type/Definition/UnionType.php index 5fe0966..876e3bb 100644 --- a/src/Type/Definition/UnionType.php +++ b/src/Type/Definition/UnionType.php @@ -72,6 +72,11 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType { if ($this->types instanceof \Closure) { $this->types = call_user_func($this->types); + Utils::invariant( + is_array($this->types), + 'Closure for option "types" of union "%s" is expected to return array of types', + $this->name + ); } return $this->types; } diff --git a/src/Utils.php b/src/Utils.php index 968842c..a5089a2 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -221,6 +221,31 @@ class Utils return is_object($var) ? get_class($var) : gettype($var); } + /** + * @param $var + * @return string + */ + public static function printSafe($var) + { + if ($var instanceof Type) { + // FIXME: Replace with schema printer call + if ($var instanceof WrappingType) { + $var = $var->getWrappedType(true); + } + return $var->name; + } + if (is_object($var)) { + return 'instance of ' . get_class($var); + } + if (is_scalar($var)) { + return (string) $var; + } + if (null === $var) { + return 'null'; + } + return gettype($var); + } + /** * UTF-8 compatible chr() * diff --git a/tests/Type/DefinitionTest.php b/tests/Type/DefinitionTest.php index 62f5b5f..5257e88 100644 --- a/tests/Type/DefinitionTest.php +++ b/tests/Type/DefinitionTest.php @@ -225,6 +225,54 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase $this->assertEquals($sub->name, 'articleSubscribe'); } + /** + * @it defines an enum type with deprecated value + */ + public function testDefinesEnumTypeWithDeprecatedValue() + { + $enumTypeWithDeprecatedValue = new EnumType([ + 'name' => 'EnumWithDeprecatedValue', + 'values' => [ + 'foo' => ['deprecationReason' => 'Just because'] + ] + ]); + + $value = $enumTypeWithDeprecatedValue->getValues()[0]; + + $this->assertEquals([ + 'name' => 'foo', + 'description' => null, + 'deprecationReason' => 'Just because', + 'value' => 'foo' + ], (array) $value); + + $this->assertEquals(true, $value->isDeprecated()); + } + + /** + * @it defines an object type with deprecated field + */ + public function testDefinesAnObjectTypeWithDeprecatedField() + { + $TypeWithDeprecatedField = new ObjectType([ + 'name' => 'foo', + 'fields' => [ + 'bar' => [ + 'type' => Type::string(), + 'deprecationReason' => 'A terrible reason' + ] + ] + ]); + + $field = $TypeWithDeprecatedField->getField('bar'); + + $this->assertEquals(Type::string(), $field->getType()); + $this->assertEquals(true, $field->isDeprecated()); + $this->assertEquals('A terrible reason', $field->deprecationReason); + $this->assertEquals('bar', $field->name); + $this->assertEquals([], $field->args); + } + /** * @it includes nested input objects in the map */ @@ -424,6 +472,21 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase Config::disableValidation(); } + /** + * @it allows a thunk for Union\'s types + */ + public function testAllowsThunkForUnionTypes() + { + $union = new UnionType([ + 'name' => 'ThunkUnion', + 'types' => function() {return [$this->objectType]; } + ]); + + $types = $union->getTypes(); + $this->assertEquals(1, count($types)); + $this->assertSame($this->objectType, $types[0]); + } + public function testAllowsRecursiveDefinitions() { // See https://github.com/webonyx/graphql-php/issues/16 diff --git a/tests/Validator/TestCase.php b/tests/Validator/TestCase.php index 23f921b..b726d72 100644 --- a/tests/Validator/TestCase.php +++ b/tests/Validator/TestCase.php @@ -1,6 +1,7 @@ $queryRoot, - 'directives' => [ + 'directives' => array_merge(GraphQL::getInternalDirectives(), [ new Directive([ 'name' => 'operationOnly', 'locations' => [ 'QUERY' ], ]) - ] + ]) ]); return $defaultSchema; }