From 00d547dc06d00d370696f7e4c36d1d2ca4b38564 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Sun, 26 Aug 2018 11:43:50 +0200 Subject: [PATCH] Fix CS in src/Type --- src/Executor/Values.php | 54 +- src/Language/Printer.php | 2 +- src/Type/Definition/AbstractType.php | 9 +- src/Type/Definition/BooleanType.php | 23 +- src/Type/Definition/CompositeType.php | 4 + src/Type/Definition/CustomScalarType.php | 41 +- src/Type/Definition/Directive.php | 240 +++-- src/Type/Definition/EnumType.php | 168 ++-- src/Type/Definition/EnumValueDefinition.php | 45 +- src/Type/Definition/FieldArgument.php | 99 ++- src/Type/Definition/FieldDefinition.php | 166 ++-- src/Type/Definition/FloatType.php | 57 +- src/Type/Definition/IDType.php | 35 +- src/Type/Definition/InputObjectField.php | 56 +- src/Type/Definition/InputObjectType.php | 103 ++- src/Type/Definition/InputType.php | 4 + src/Type/Definition/IntType.php | 85 +- src/Type/Definition/InterfaceType.php | 103 +-- src/Type/Definition/LeafType.php | 12 +- src/Type/Definition/ListOfType.php | 15 +- src/Type/Definition/NamedType.php | 4 + src/Type/Definition/NonNull.php | 65 +- src/Type/Definition/ObjectType.php | 206 ++--- src/Type/Definition/OutputType.php | 5 +- src/Type/Definition/ResolveInfo.php | 48 +- src/Type/Definition/ScalarType.php | 21 +- src/Type/Definition/StringType.php | 53 +- src/Type/Definition/Type.php | 242 +++--- src/Type/Definition/UnionType.php | 111 +-- src/Type/Definition/UnmodifiedType.php | 4 + src/Type/Definition/WrappingType.php | 3 + src/Type/EagerResolution.php | 90 +- src/Type/Introspection.php | 717 +++++++-------- src/Type/LazyResolution.php | 95 +- src/Type/Resolution.php | 5 +- src/Type/Schema.php | 377 ++++---- src/Type/SchemaConfig.php | 198 ++--- src/Type/SchemaValidationContext.php | 913 +++++++++++--------- src/Type/TypeKind.php | 17 + 39 files changed, 2373 insertions(+), 2122 deletions(-) create mode 100644 src/Type/TypeKind.php diff --git a/src/Executor/Values.php b/src/Executor/Values.php index 01eeb24..53e66cb 100644 --- a/src/Executor/Values.php +++ b/src/Executor/Values.php @@ -52,35 +52,15 @@ class Values /** @var InputType|Type $varType */ $varType = TypeInfo::typeFromAST($schema, $varDefNode->type); - if (! Type::isInputType($varType)) { - $errors[] = new Error( - sprintf( - 'Variable "$%s" expected value of type "%s" which cannot be used as an input type.', - $varName, - Printer::doPrint($varDefNode->type) - ), - [$varDefNode->type] - ); - } else { - if (! array_key_exists($varName, $inputs)) { - if ($varType instanceof NonNull) { - $errors[] = new Error( - sprintf( - 'Variable "$%s" of required type "%s" was not provided.', - $varName, - $varType - ), - [$varDefNode] - ); - } elseif ($varDefNode->defaultValue) { - $coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType); - } - } else { + if (Type::isInputType($varType)) { + if (array_key_exists($varName, $inputs)) { $value = $inputs[$varName]; $coerced = Value::coerceValue($value, $varType, $varDefNode); /** @var Error[] $coercionErrors */ $coercionErrors = $coerced['errors']; - if (! empty($coercionErrors)) { + if (empty($coercionErrors)) { + $coercedValues[$varName] = $coerced['value']; + } else { $messagePrelude = sprintf( 'Variable "$%s" got invalid value %s; ', $varName, @@ -98,10 +78,30 @@ class Values $error->getExtensions() ); } - } else { - $coercedValues[$varName] = $coerced['value']; + } + } else { + if ($varType instanceof NonNull) { + $errors[] = new Error( + sprintf( + 'Variable "$%s" of required type "%s" was not provided.', + $varName, + $varType + ), + [$varDefNode] + ); + } elseif ($varDefNode->defaultValue) { + $coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType); } } + } else { + $errors[] = new Error( + sprintf( + 'Variable "$%s" expected value of type "%s" which cannot be used as an input type.', + $varName, + Printer::doPrint($varDefNode->type) + ), + [$varDefNode->type] + ); } } diff --git a/src/Language/Printer.php b/src/Language/Printer.php index 69a3ca9..f5110f1 100644 --- a/src/Language/Printer.php +++ b/src/Language/Printer.php @@ -470,7 +470,7 @@ class Printer Utils::filter( $maybeArray, function ($x) { - return ! ! $x; + return (bool) $x; } ) ) diff --git a/src/Type/Definition/AbstractType.php b/src/Type/Definition/AbstractType.php index dd27677..f573610 100644 --- a/src/Type/Definition/AbstractType.php +++ b/src/Type/Definition/AbstractType.php @@ -1,4 +1,7 @@ value; diff --git a/src/Type/Definition/CompositeType.php b/src/Type/Definition/CompositeType.php index dd8bdf8..7223ebe 100644 --- a/src/Type/Definition/CompositeType.php +++ b/src/Type/Definition/CompositeType.php @@ -1,4 +1,7 @@ config['parseValue'])) { return call_user_func($this->config['parseValue'], $value); - } else { - return $value; } + + return $value; } /** - * @param Node $valueNode - * @param array|null $variables + * @param Node $valueNode + * @param mixed[]|null $variables * @return mixed * @throws \Exception */ - public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode, array $variables = null) - { + public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ + $valueNode, + ?array $variables = null + ) { if (isset($this->config['parseLiteral'])) { return call_user_func($this->config['parseLiteral'], $valueNode, $variables); - } else { - return AST::valueFromASTUntyped($valueNode, $variables); } + + return AST::valueFromASTUntyped($valueNode, $variables); } public function assertValid() @@ -54,16 +61,18 @@ class CustomScalarType extends ScalarType Utils::invariant( isset($this->config['serialize']) && is_callable($this->config['serialize']), - "{$this->name} must provide \"serialize\" function. If this custom Scalar " . + sprintf('%s must provide "serialize" function. If this custom Scalar ', $this->name) . 'is also used as an input type, ensure "parseValue" and "parseLiteral" ' . 'functions are also provided.' ); - if (isset($this->config['parseValue']) || isset($this->config['parseLiteral'])) { - Utils::invariant( - isset($this->config['parseValue']) && isset($this->config['parseLiteral']) && - is_callable($this->config['parseValue']) && is_callable($this->config['parseLiteral']), - "{$this->name} must provide both \"parseValue\" and \"parseLiteral\" functions." - ); + if (! isset($this->config['parseValue']) && ! isset($this->config['parseLiteral'])) { + return; } + + Utils::invariant( + isset($this->config['parseValue']) && isset($this->config['parseLiteral']) && + is_callable($this->config['parseValue']) && is_callable($this->config['parseLiteral']), + sprintf('%s must provide both "parseValue" and "parseLiteral" functions.', $this->name) + ); } } diff --git a/src/Type/Definition/Directive.php b/src/Type/Definition/Directive.php index b031365..f837db7 100644 --- a/src/Type/Definition/Directive.php +++ b/src/Type/Definition/Directive.php @@ -1,159 +1,48 @@ name, array_keys(self::getInternalDirectives())); - } - - /** - * @return array - */ - public static function getInternalDirectives() - { - if (!self::$internalDirectives) { - self::$internalDirectives = [ - 'include' => new self([ - 'name' => 'include', - 'description' => 'Directs the executor to include this field or fragment only when the `if` argument is true.', - 'locations' => [ - DirectiveLocation::FIELD, - DirectiveLocation::FRAGMENT_SPREAD, - DirectiveLocation::INLINE_FRAGMENT, - ], - 'args' => [ - new FieldArgument([ - 'name' => 'if', - 'type' => Type::nonNull(Type::boolean()), - 'description' => 'Included when true.' - ]) - ], - ]), - 'skip' => new self([ - 'name' => 'skip', - 'description' => 'Directs the executor to skip this field or fragment when the `if` argument is true.', - 'locations' => [ - DirectiveLocation::FIELD, - DirectiveLocation::FRAGMENT_SPREAD, - DirectiveLocation::INLINE_FRAGMENT - ], - 'args' => [ - new FieldArgument([ - 'name' => 'if', - 'type' => Type::nonNull(Type::boolean()), - 'description' => 'Skipped when true.' - ]) - ] - ]), - 'deprecated' => new self([ - 'name' => 'deprecated', - 'description' => 'Marks an element of a GraphQL schema as no longer supported.', - 'locations' => [ - DirectiveLocation::FIELD_DEFINITION, - DirectiveLocation::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 - ]) - ] - ]) - ]; - } - return self::$internalDirectives; - } - - /** - * @var string - */ + /** @var string */ public $name; - /** - * @var string|null - */ + /** @var string|null */ public $description; - /** - * Values from self::$locationMap - * - * @var array - */ + /** @var string[] */ public $locations; - /** - * @var FieldArgument[] - */ + /** @var FieldArgument[] */ public $args; - /** - * @var DirectiveDefinitionNode|null - */ + /** @var DirectiveDefinitionNode|null */ public $astNode; - /** - * @var array - */ + /** @var mixed[] */ public $config; /** - * Directive constructor. - * @param array $config + * + * @param mixed[] $config */ public function __construct(array $config) { @@ -177,4 +66,103 @@ class Directive Utils::invariant(is_array($this->locations), 'Must provide locations for directive.'); $this->config = $config; } + + /** + * @return Directive + */ + public static function includeDirective() + { + $internal = self::getInternalDirectives(); + + return $internal['include']; + } + + /** + * @return Directive[] + */ + public static function getInternalDirectives() + { + if (! self::$internalDirectives) { + self::$internalDirectives = [ + 'include' => new self([ + 'name' => 'include', + 'description' => 'Directs the executor to include this field or fragment only when the `if` argument is true.', + 'locations' => [ + DirectiveLocation::FIELD, + DirectiveLocation::FRAGMENT_SPREAD, + DirectiveLocation::INLINE_FRAGMENT, + ], + 'args' => [new FieldArgument([ + 'name' => 'if', + 'type' => Type::nonNull(Type::boolean()), + 'description' => 'Included when true.', + ]), + ], + ]), + 'skip' => new self([ + 'name' => 'skip', + 'description' => 'Directs the executor to skip this field or fragment when the `if` argument is true.', + 'locations' => [ + DirectiveLocation::FIELD, + DirectiveLocation::FRAGMENT_SPREAD, + DirectiveLocation::INLINE_FRAGMENT, + ], + 'args' => [new FieldArgument([ + 'name' => 'if', + 'type' => Type::nonNull(Type::boolean()), + 'description' => 'Skipped when true.', + ]), + ], + ]), + 'deprecated' => new self([ + 'name' => 'deprecated', + 'description' => 'Marks an element of a GraphQL schema as no longer supported.', + 'locations' => [ + DirectiveLocation::FIELD_DEFINITION, + DirectiveLocation::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, + ]), + ], + ]), + ]; + } + + return self::$internalDirectives; + } + + /** + * @return Directive + */ + public static function skipDirective() + { + $internal = self::getInternalDirectives(); + + return $internal['skip']; + } + + /** + * @return Directive + */ + public static function deprecatedDirective() + { + $internal = self::getInternalDirectives(); + + return $internal['deprecated']; + } + + /** + * @return bool + */ + public static function isSpecifiedDirective(Directive $directive) + { + return array_key_exists($directive->name, self::getInternalDirectives()); + } } diff --git a/src/Type/Definition/EnumType.php b/src/Type/Definition/EnumType.php index 9918414..86e6c94 100644 --- a/src/Type/Definition/EnumType.php +++ b/src/Type/Definition/EnumType.php @@ -1,51 +1,81 @@ - */ + /** @var MixedStore */ private $valueLookup; - /** - * @var \ArrayObject - */ + /** @var \ArrayObject */ private $nameLookup; public function __construct($config) { - if (!isset($config['name'])) { + if (! isset($config['name'])) { $config['name'] = $this->tryInferName(); } Utils::invariant(is_string($config['name']), 'Must provide name.'); - $this->name = $config['name']; - $this->description = isset($config['description']) ? $config['description'] : null; - $this->astNode = isset($config['astNode']) ? $config['astNode'] : null; - $this->config = $config; + $this->name = $config['name']; + $this->description = $config['description'] ?? null; + $this->astNode = $config['astNode'] ?? null; + $this->config = $config; + } + + /** + * @param string|mixed[] $name + * @return EnumValueDefinition|null + */ + public function getValue($name) + { + $lookup = $this->getNameLookup(); + + if (! is_string($name)) { + return null; + } + + return $lookup[$name] ?? null; + } + + /** + * @return \ArrayObject + */ + private function getNameLookup() + { + if (! $this->nameLookup) { + $lookup = new \ArrayObject(); + foreach ($this->getValues() as $value) { + $lookup[$value->name] = $value; + } + $this->nameLookup = $lookup; + } + + return $this->nameLookup; } /** @@ -55,23 +85,28 @@ class EnumType extends Type implements InputType, OutputType, LeafType, NamedTyp { if ($this->values === null) { $this->values = []; - $config = $this->config; + $config = $this->config; if (isset($config['values'])) { - if (!is_array($config['values'])) { - throw new InvariantViolation("{$this->name} values must be an array"); + if (! is_array($config['values'])) { + throw new InvariantViolation(sprintf('%s values must be an array', $this->name)); } foreach ($config['values'] as $name => $value) { if (is_string($name)) { - if (!is_array($value)) { - $value = ['name' => $name, 'value' => $value]; - } else { + if (is_array($value)) { $value += ['name' => $name, 'value' => $name]; + } else { + $value = ['name' => $name, 'value' => $value]; } - } else if (is_int($name) && is_string($value)) { + } elseif (is_int($name) && is_string($value)) { $value = ['name' => $value, 'value' => $value]; } else { - throw new InvariantViolation("{$this->name} values must be an array with value names as keys."); + throw new InvariantViolation( + sprintf( + '%s values must be an array with value names as keys.', + $this->name + ) + ); } $this->values[] = new EnumValueDefinition($value); } @@ -82,17 +117,7 @@ class EnumType extends Type implements InputType, OutputType, LeafType, NamedTyp } /** - * @param $name - * @return EnumValueDefinition|null - */ - public function getValue($name) - { - $lookup = $this->getNameLookup(); - return is_scalar($name) && isset($lookup[$name]) ? $lookup[$name] : null; - } - - /** - * @param $value + * @param mixed $value * @return mixed * @throws Error */ @@ -103,11 +128,27 @@ class EnumType extends Type implements InputType, OutputType, LeafType, NamedTyp return $lookup[$value]->name; } - throw new Error("Cannot serialize value as enum: " . Utils::printSafe($value)); + throw new Error('Cannot serialize value as enum: ' . Utils::printSafe($value)); } /** - * @param $value + * @return MixedStore + */ + private function getValueLookup() + { + if ($this->valueLookup === null) { + $this->valueLookup = new MixedStore(); + + foreach ($this->getValues() as $valueName => $value) { + $this->valueLookup->offsetSet($value->value, $value); + } + } + + return $this->valueLookup; + } + + /** + * @param mixed $value * @return mixed * @throws Error */ @@ -118,16 +159,16 @@ class EnumType extends Type implements InputType, OutputType, LeafType, NamedTyp return $lookup[$value]->value; } - throw new Error("Cannot represent value as enum: " . Utils::printSafe($value)); + throw new Error('Cannot represent value as enum: ' . Utils::printSafe($value)); } /** - * @param $valueNode - * @param array|null $variables + * @param Node $valueNode + * @param mixed[]|null $variables * @return null * @throws \Exception */ - public function parseLiteral($valueNode, array $variables = null) + public function parseLiteral($valueNode, ?array $variables = null) { if ($valueNode instanceof EnumValueNode) { $lookup = $this->getNameLookup(); @@ -143,37 +184,6 @@ class EnumType extends Type implements InputType, OutputType, LeafType, NamedTyp throw new \Exception(); } - /** - * @return MixedStore - */ - private function getValueLookup() - { - if (null === $this->valueLookup) { - $this->valueLookup = new MixedStore(); - - foreach ($this->getValues() as $valueName => $value) { - $this->valueLookup->offsetSet($value->value, $value); - } - } - - return $this->valueLookup; - } - - /** - * @return \ArrayObject - */ - private function getNameLookup() - { - if (!$this->nameLookup) { - $lookup = new \ArrayObject(); - foreach ($this->getValues() as $value) { - $lookup[$value->name] = $value; - } - $this->nameLookup = $lookup; - } - return $this->nameLookup; - } - /** * @throws InvariantViolation */ @@ -183,14 +193,18 @@ class EnumType extends Type implements InputType, OutputType, LeafType, NamedTyp Utils::invariant( isset($this->config['values']), - "{$this->name} values must be an array." + sprintf('%s values must be an array.', $this->name) ); $values = $this->getValues(); foreach ($values as $value) { Utils::invariant( - !isset($value->config['isDeprecated']), - "{$this->name}.{$value->name} should provide \"deprecationReason\" instead of \"isDeprecated\"." + ! isset($value->config['isDeprecated']), + sprintf( + '%s.%s should provide "deprecationReason" instead of "isDeprecated".', + $this->name, + $value->name + ) ); } } diff --git a/src/Type/Definition/EnumValueDefinition.php b/src/Type/Definition/EnumValueDefinition.php index cd94bab..33494be 100644 --- a/src/Type/Definition/EnumValueDefinition.php +++ b/src/Type/Definition/EnumValueDefinition.php @@ -1,51 +1,44 @@ name = isset($config['name']) ? $config['name'] : null; - $this->value = isset($config['value']) ? $config['value'] : null; - $this->deprecationReason = isset($config['deprecationReason']) ? $config['deprecationReason'] : null; - $this->description = isset($config['description']) ? $config['description'] : null; - $this->astNode = isset($config['astNode']) ? $config['astNode'] : null; + $this->name = $config['name'] ?? null; + $this->value = $config['value'] ?? null; + $this->deprecationReason = $config['deprecationReason'] ?? null; + $this->description = $config['description'] ?? null; + $this->astNode = $config['astNode'] ?? null; $this->config = $config; } @@ -55,6 +48,6 @@ class EnumValueDefinition */ public function isDeprecated() { - return !!$this->deprecationReason; + return (bool) $this->deprecationReason; } } diff --git a/src/Type/Definition/FieldArgument.php b/src/Type/Definition/FieldArgument.php index 67a567f..610a60e 100644 --- a/src/Type/Definition/FieldArgument.php +++ b/src/Type/Definition/FieldArgument.php @@ -1,73 +1,45 @@ $argConfig) { - if (!is_array($argConfig)) { - $argConfig = ['type' => $argConfig]; - } - $map[] = new self($argConfig + ['name' => $name]); - } - return $map; - } - - /** - * FieldArgument constructor. - * @param array $def + * + * @param mixed[] $def */ public function __construct(array $def) { @@ -80,7 +52,7 @@ class FieldArgument $this->name = $value; break; case 'defaultValue': - $this->defaultValue = $value; + $this->defaultValue = $value; $this->defaultValueExists = true; break; case 'description': @@ -94,6 +66,23 @@ class FieldArgument $this->config = $def; } + /** + * @param mixed[] $config + * @return FieldArgument[] + */ + public static function createMap(array $config) + { + $map = []; + foreach ($config as $name => $argConfig) { + if (! is_array($argConfig)) { + $argConfig = ['type' => $argConfig]; + } + $map[] = new self($argConfig + ['name' => $name]); + } + + return $map; + } + /** * @return InputType */ @@ -116,8 +105,8 @@ class FieldArgument Utils::assertValidName($this->name); } catch (InvariantViolation $e) { throw new InvariantViolation( - "{$parentType->name}.{$parentField->name}({$this->name}:) {$e->getMessage()}") - ; + sprintf('%s.%s(%s:) %s', $parentType->name, $parentField->name, $this->name, $e->getMessage()) + ); } $type = $this->type; if ($type instanceof WrappingType) { @@ -125,13 +114,23 @@ class FieldArgument } Utils::invariant( $type instanceof InputType, - "{$parentType->name}.{$parentField->name}({$this->name}): argument type must be " . - "Input Type but got: " . Utils::printSafe($this->type) + sprintf( + '%s.%s(%s): argument type must be Input Type but got: %s', + $parentType->name, + $parentField->name, + $this->name, + Utils::printSafe($this->type) + ) ); Utils::invariant( $this->description === null || is_string($this->description), - "{$parentType->name}.{$parentField->name}({$this->name}): argument description type must be " . - "string but got: " . Utils::printSafe($this->description) + sprintf( + '%s.%s(%s): argument description type must be string but got: %s', + $parentType->name, + $parentField->name, + $this->name, + Utils::printSafe($this->description) + ) ); } } diff --git a/src/Type/Definition/FieldDefinition.php b/src/Type/Definition/FieldDefinition.php index 840dd54..f8a1155 100644 --- a/src/Type/Definition/FieldDefinition.php +++ b/src/Type/Definition/FieldDefinition.php @@ -1,28 +1,29 @@ name = $config['name']; + $this->type = $config['type']; + $this->resolveFn = $config['resolve'] ?? null; + $this->mapFn = $config['map'] ?? null; + $this->args = isset($config['args']) ? FieldArgument::createMap($config['args']) : []; + + $this->description = $config['description'] ?? null; + $this->deprecationReason = $config['deprecationReason'] ?? null; + $this->astNode = $config['astNode'] ?? null; + + $this->config = $config; + + $this->complexityFn = $config['complexity'] ?? static::DEFAULT_COMPLEXITY_FN; + } public static function defineFieldMap(Type $type, $fields) { if (is_callable($fields)) { $fields = $fields(); } - if (!is_array($fields)) { + if (! is_array($fields)) { throw new InvariantViolation( - "{$type->name} fields must be an array or a callable which returns such an array." + sprintf('%s fields must be an array or a callable which returns such an array.', $type->name) ); } $map = []; foreach ($fields as $name => $field) { if (is_array($field)) { - if (!isset($field['name'])) { - if (is_string($name)) { - $field['name'] = $name; - } else { + if (! isset($field['name'])) { + if (! is_string($name)) { throw new InvariantViolation( - "{$type->name} fields must be an associative array with field names as keys or a " . - "function which returns such an array." + sprintf( + '%s fields must be an associative array with field names as keys or a function which returns such an array.', + $type->name + ) ); } + + $field['name'] = $name; } - if (isset($field['args']) && !is_array($field['args'])) { + if (isset($field['args']) && ! is_array($field['args'])) { throw new InvariantViolation( - "{$type->name}.{$name} args must be an array." + sprintf('%s.%s args must be an array.', $type->name, $name) ); } $fieldDef = self::create($field); - } else if ($field instanceof FieldDefinition) { + } elseif ($field instanceof self) { $fieldDef = $field; } else { - if (is_string($name) && $field) { - $fieldDef = self::create(['name' => $name, 'type' => $field]); - } else { + if (! is_string($name) || ! $field) { throw new InvariantViolation( - "{$type->name}.$name field config must be an array, but got: " . Utils::printSafe($field) + sprintf( + '%s.%s field config must be an array, but got: %s', + $type->name, + $name, + Utils::printSafe($field) + ) ); } + + $fieldDef = self::create(['name' => $name, 'type' => $field]); } $map[$fieldDef->name] = $fieldDef; } + return $map; } /** - * @param array|Config $field - * @param string $typeName + * @param mixed[] $field * @return FieldDefinition */ - public static function create($field, $typeName = null) + public static function create($field) { return new self($field); } /** - * FieldDefinition constructor. - * @param array $config + * @param int $childrenComplexity + * @return mixed */ - protected function __construct(array $config) + public static function defaultComplexity($childrenComplexity) { - $this->name = $config['name']; - $this->type = $config['type']; - $this->resolveFn = isset($config['resolve']) ? $config['resolve'] : null; - $this->mapFn = isset($config['map']) ? $config['map'] : null; - $this->args = isset($config['args']) ? FieldArgument::createMap($config['args']) : []; - - $this->description = isset($config['description']) ? $config['description'] : null; - $this->deprecationReason = isset($config['deprecationReason']) ? $config['deprecationReason'] : null; - $this->astNode = isset($config['astNode']) ? $config['astNode'] : null; - - $this->config = $config; - - $this->complexityFn = isset($config['complexity']) ? $config['complexity'] : static::DEFAULT_COMPLEXITY_FN; + return $childrenComplexity + 1; } /** - * @param $name + * @param string $name * @return FieldArgument|null */ public function getArg($name) @@ -158,6 +168,7 @@ class FieldDefinition return $arg; } } + return null; } @@ -174,7 +185,7 @@ class FieldDefinition */ public function isDeprecated() { - return !!$this->deprecationReason; + return (bool) $this->deprecationReason; } /** @@ -186,7 +197,6 @@ class FieldDefinition } /** - * @param Type $parentType * @throws InvariantViolation */ public function assertValid(Type $parentType) @@ -194,11 +204,15 @@ class FieldDefinition try { Utils::assertValidName($this->name); } catch (Error $e) { - throw new InvariantViolation("{$parentType->name}.{$this->name}: {$e->getMessage()}"); + throw new InvariantViolation(sprintf('%s.%s: %s', $parentType->name, $this->name, $e->getMessage())); } Utils::invariant( - !isset($this->config['isDeprecated']), - "{$parentType->name}.{$this->name} should provide \"deprecationReason\" instead of \"isDeprecated\"." + ! isset($this->config['isDeprecated']), + sprintf( + '%s.%s should provide "deprecationReason" instead of "isDeprecated".', + $parentType->name, + $this->name + ) ); $type = $this->type; @@ -207,21 +221,21 @@ class FieldDefinition } Utils::invariant( $type instanceof OutputType, - "{$parentType->name}.{$this->name} field type must be Output Type but got: " . Utils::printSafe($this->type) + sprintf( + '%s.%s field type must be Output Type but got: %s', + $parentType->name, + $this->name, + Utils::printSafe($this->type) + ) ); Utils::invariant( $this->resolveFn === null || is_callable($this->resolveFn), - "{$parentType->name}.{$this->name} field resolver must be a function if provided, but got: %s", - Utils::printSafe($this->resolveFn) + sprintf( + '%s.%s field resolver must be a function if provided, but got: %s', + $parentType->name, + $this->name, + Utils::printSafe($this->resolveFn) + ) ); } - - /** - * @param $childrenComplexity - * @return mixed - */ - public static function defaultComplexity($childrenComplexity) - { - return $childrenComplexity + 1; - } } diff --git a/src/Type/Definition/FloatType.php b/src/Type/Definition/FloatType.php index e04cd5a..12df7cb 100644 --- a/src/Type/Definition/FloatType.php +++ b/src/Type/Definition/FloatType.php @@ -1,27 +1,27 @@ coerceFloat($value); } + 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; + } + /** * @param mixed $value * @return float|null @@ -46,12 +64,12 @@ values as specified by } /** - * @param $valueNode - * @param array|null $variables + * @param Node $valueNode + * @param mixed[]|null $variables * @return float|null * @throws \Exception */ - public function parseLiteral($valueNode, array $variables = null) + public function parseLiteral($valueNode, ?array $variables = null) { if ($valueNode instanceof FloatValueNode || $valueNode instanceof IntValueNode) { return (float) $valueNode->value; @@ -60,21 +78,4 @@ values as specified by // Intentionally without message, as all information already in wrapped Exception throw new \Exception(); } - - 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; - } } diff --git a/src/Type/Definition/IDType.php b/src/Type/Definition/IDType.php index a35cf09..456f105 100644 --- a/src/Type/Definition/IDType.php +++ b/src/Type/Definition/IDType.php @@ -1,4 +1,7 @@ value; diff --git a/src/Type/Definition/InputObjectField.php b/src/Type/Definition/InputObjectField.php index c1f8bcb..793994b 100644 --- a/src/Type/Definition/InputObjectField.php +++ b/src/Type/Definition/InputObjectField.php @@ -1,44 +1,33 @@ $v) { switch ($k) { case 'defaultValue': - $this->defaultValue = $v; + $this->defaultValue = $v; $this->defaultValueExists = true; break; case 'defaultValueExists': @@ -86,7 +75,6 @@ class InputObjectField } /** - * @param Type $parentType * @throws InvariantViolation */ public function assertValid(Type $parentType) @@ -94,7 +82,7 @@ class InputObjectField try { Utils::assertValidName($this->name); } catch (Error $e) { - throw new InvariantViolation("{$parentType->name}.{$this->name}: {$e->getMessage()}"); + throw new InvariantViolation(sprintf('%s.%s: %s', $parentType->name, $this->name, $e->getMessage())); } $type = $this->type; if ($type instanceof WrappingType) { @@ -102,12 +90,20 @@ class InputObjectField } Utils::invariant( $type instanceof InputType, - "{$parentType->name}.{$this->name} field type must be Input Type but got: " . Utils::printSafe($this->type) + sprintf( + '%s.%s field type must be Input Type but got: %s', + $parentType->name, + $this->name, + Utils::printSafe($this->type) + ) ); Utils::invariant( empty($this->config['resolve']), - "{$parentType->name}.{$this->name} field type has a resolve property, " . - 'but Input Types cannot define resolvers.' + sprintf( + '%s.%s field type has a resolve property, but Input Types cannot define resolvers.', + $parentType->name, + $this->name + ) ); } } diff --git a/src/Type/Definition/InputObjectType.php b/src/Type/Definition/InputObjectType.php index c3bd1cb..0e39c05 100644 --- a/src/Type/Definition/InputObjectType.php +++ b/src/Type/Definition/InputObjectType.php @@ -1,70 +1,46 @@ tryInferName(); } Utils::invariant(is_string($config['name']), 'Must provide name.'); - $this->config = $config; - $this->name = $config['name']; - $this->astNode = isset($config['astNode']) ? $config['astNode'] : null; - $this->description = isset($config['description']) ? $config['description'] : null; - } - - /** - * @return InputObjectField[] - */ - public function getFields() - { - if (null === $this->fields) { - $this->fields = []; - $fields = isset($this->config['fields']) ? $this->config['fields'] : []; - $fields = is_callable($fields) ? call_user_func($fields) : $fields; - - if (!is_array($fields)) { - throw new InvariantViolation( - "{$this->name} fields must be an array or a callable which returns such an array." - ); - } - - foreach ($fields as $name => $field) { - if ($field instanceof Type) { - $field = ['type' => $field]; - } - $field = new InputObjectField($field + ['name' => $name]); - $this->fields[$field->name] = $field; - } - } - - return $this->fields; + $this->config = $config; + $this->name = $config['name']; + $this->astNode = $config['astNode'] ?? null; + $this->description = $config['description'] ?? null; } /** @@ -74,13 +50,42 @@ class InputObjectType extends Type implements InputType, NamedType */ public function getField($name) { - if (null === $this->fields) { + if ($this->fields === null) { $this->getFields(); } Utils::invariant(isset($this->fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name); + return $this->fields[$name]; } + /** + * @return InputObjectField[] + */ + public function getFields() + { + if ($this->fields === null) { + $this->fields = []; + $fields = $this->config['fields'] ?? []; + $fields = is_callable($fields) ? call_user_func($fields) : $fields; + + if (! is_array($fields)) { + throw new InvariantViolation( + spritnf('%s fields must be an array or a callable which returns such an array.', $this->name) + ); + } + + foreach ($fields as $name => $field) { + if ($field instanceof Type) { + $field = ['type' => $field]; + } + $field = new InputObjectField($field + ['name' => $name]); + $this->fields[$field->name] = $field; + } + } + + return $this->fields; + } + /** * Validates type config and throws if one of type options is invalid. * Note: this method is shallow, it won't validate object fields and their arguments. @@ -92,9 +97,11 @@ class InputObjectType extends Type implements InputType, NamedType parent::assertValid(); Utils::invariant( - !empty($this->getFields()), - "{$this->name} fields must be an associative array with field names as keys or a " . - "callable which returns such an array." + ! empty($this->getFields()), + sprintf( + '%s fields must be an associative array with field names as keys or a callable which returns such an array.', + $this->name + ) ); foreach ($this->getFields() as $field) { diff --git a/src/Type/Definition/InputType.php b/src/Type/Definition/InputType.php index b2c3830..0437657 100644 --- a/src/Type/Definition/InputType.php +++ b/src/Type/Definition/InputType.php @@ -1,4 +1,7 @@ , >; */ + interface InputType { } diff --git a/src/Type/Definition/IntType.php b/src/Type/Definition/IntType.php index 3133aa2..6c0a8f3 100644 --- a/src/Type/Definition/IntType.php +++ b/src/Type/Definition/IntType.php @@ -1,13 +1,20 @@ coerceInt($value); } + /** + * @param mixed $value + * @return int + */ + 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); + // int cast with == used for performance reasons + // @codingStandardsIgnoreLine + if ($int != $num) { + throw new Error( + 'Int cannot represent non-integer value: ' . + Utils::printSafe($value) + ); + } + + return $int; + } + /** * @param mixed $value * @return int|null @@ -52,12 +87,12 @@ values. Int can represent values between -(2^31) and 2^31 - 1. '; } /** - * @param $valueNode - * @param array|null $variables + * @param Node $valueNode + * @param mixed[]|null $variables * @return int|null * @throws \Exception */ - public function parseLiteral($valueNode, array $variables = null) + public function parseLiteral($valueNode, ?array $variables = null) { if ($valueNode instanceof IntValueNode) { $val = (int) $valueNode->value; @@ -69,28 +104,4 @@ values. Int can represent values between -(2^31) and 2^31 - 1. '; // Intentionally without message, as all information already in wrapped Exception throw new \Exception(); } - - 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; - } } diff --git a/src/Type/Definition/InterfaceType.php b/src/Type/Definition/InterfaceType.php index 63b909f..297ad40 100644 --- a/src/Type/Definition/InterfaceType.php +++ b/src/Type/Definition/InterfaceType.php @@ -1,17 +1,50 @@ tryInferName(); + } + + Utils::invariant(is_string($config['name']), 'Must provide name.'); + + $this->name = $config['name']; + $this->description = $config['description'] ?? null; + $this->astNode = $config['astNode'] ?? null; + $this->extensionASTNodes = $config['extensionASTNodes'] ?? null; + $this->config = $config; + } + /** * @param mixed $type * @return self @@ -27,37 +60,17 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT } /** - * @var FieldDefinition[] + * @param string $name + * @return FieldDefinition */ - private $fields; - - /** - * @var InterfaceTypeDefinitionNode|null - */ - public $astNode; - - /** - * @var InterfaceTypeExtensionNode[] - */ - public $extensionASTNodes; - - /** - * InterfaceType constructor. - * @param array $config - */ - public function __construct(array $config) + public function getField($name) { - if (!isset($config['name'])) { - $config['name'] = $this->tryInferName(); + if ($this->fields === null) { + $this->getFields(); } + Utils::invariant(isset($this->fields[$name]), 'Field "%s" is not defined for type "%s"', $name, $this->name); - Utils::invariant(is_string($config['name']), 'Must provide name.'); - - $this->name = $config['name']; - $this->description = isset($config['description']) ? $config['description'] : null; - $this->astNode = isset($config['astNode']) ? $config['astNode'] : null; - $this->extensionASTNodes = isset($config['extensionASTNodes']) ? $config['extensionASTNodes'] : null; - $this->config = $config; + return $this->fields[$name]; } /** @@ -65,41 +78,29 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT */ public function getFields() { - if (null === $this->fields) { - $fields = isset($this->config['fields']) ? $this->config['fields'] : []; + if ($this->fields === null) { + $fields = $this->config['fields'] ?? []; $this->fields = FieldDefinition::defineFieldMap($this, $fields); } - return $this->fields; - } - /** - * @param $name - * @return FieldDefinition - * @throws \Exception - */ - public function getField($name) - { - if (null === $this->fields) { - $this->getFields(); - } - Utils::invariant(isset($this->fields[$name]), 'Field "%s" is not defined for type "%s"', $name, $this->name); - return $this->fields[$name]; + return $this->fields; } /** * Resolves concrete ObjectType for given object value * - * @param $objectValue - * @param $context - * @param ResolveInfo $info + * @param object $objectValue + * @param mixed[] $context * @return callable|null */ public function resolveType($objectValue, $context, ResolveInfo $info) { if (isset($this->config['resolveType'])) { $fn = $this->config['resolveType']; + return $fn($objectValue, $context, $info); } + return null; } @@ -113,8 +114,12 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT $resolveType = $this->config['resolveType'] ?? null; Utils::invariant( - !isset($resolveType) || is_callable($resolveType), - "{$this->name} must provide \"resolveType\" as a function, but got: " . Utils::printSafe($resolveType) + ! isset($resolveType) || is_callable($resolveType), + sprintf( + '%s must provide "resolveType" as a function, but got: %s', + $this->name, + Utils::printSafe($resolveType) + ) ); } } diff --git a/src/Type/Definition/LeafType.php b/src/Type/Definition/LeafType.php index 14ded04..ff7ed97 100644 --- a/src/Type/Definition/LeafType.php +++ b/src/Type/Definition/LeafType.php @@ -1,14 +1,18 @@ ofType; - $str = $type instanceof Type ? $type->toString() : (string) $type; + $str = $type instanceof Type ? $type->toString() : (string) $type; + return '[' . $str . ']'; } @@ -40,6 +38,7 @@ class ListOfType extends Type implements WrappingType, OutputType, InputType public function getWrappedType($recurse = false) { $type = $this->ofType; + return ($recurse && $type instanceof WrappingType) ? $type->getWrappedType($recurse) : $type; } } diff --git a/src/Type/Definition/NamedType.php b/src/Type/Definition/NamedType.php index be9681a..372bcc5 100644 --- a/src/Type/Definition/NamedType.php +++ b/src/Type/Definition/NamedType.php @@ -1,4 +1,7 @@ ofType = self::assertNullableType($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; + } + /** * @param mixed $type * @return self @@ -25,31 +53,11 @@ class NonNull extends Type implements WrappingType, OutputType, InputType } /** - * @param mixed $type - * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType + * @return string */ - public static function assertNullableType($type) + public function toString() { - 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; - - /** - * @param callable|Type $type - * @throws \Exception - */ - public function __construct($type) - { - $this->ofType = self::assertNullableType($type); + return $this->getWrappedType()->toString() . '!'; } /** @@ -60,14 +68,7 @@ class NonNull extends Type implements WrappingType, OutputType, InputType public function getWrappedType($recurse = false) { $type = $this->ofType; + return ($recurse && $type instanceof WrappingType) ? $type->getWrappedType($recurse) : $type; } - - /** - * @return string - */ - public function toString() - { - return $this->getWrappedType()->toString() . '!'; - } } diff --git a/src/Type/Definition/ObjectType.php b/src/Type/Definition/ObjectType.php index f850ad2..43263ce 100644 --- a/src/Type/Definition/ObjectType.php +++ b/src/Type/Definition/ObjectType.php @@ -1,11 +1,18 @@ tryInferName(); + } + + Utils::invariant(is_string($config['name']), 'Must provide name.'); + + $this->name = $config['name']; + $this->description = $config['description'] ?? null; + $this->resolveFieldFn = $config['resolveField'] ?? null; + $this->astNode = $config['astNode'] ?? null; + $this->extensionASTNodes = $config['extensionASTNodes'] ?? []; + $this->config = $config; + } + /** * @param mixed $type * @return self @@ -64,53 +109,18 @@ class ObjectType extends Type implements OutputType, CompositeType, NamedType } /** - * @var FieldDefinition[] + * @param string $name + * @return FieldDefinition + * @throws \Exception */ - private $fields; - - /** - * @var InterfaceType[] - */ - private $interfaces; - - /** - * @var array - */ - private $interfaceMap; - - /** - * @var ObjectTypeDefinitionNode|null - */ - public $astNode; - - /** - * @var ObjectTypeExtensionNode[] - */ - public $extensionASTNodes; - - /** - * @var callable - */ - public $resolveFieldFn; - - /** - * ObjectType constructor. - * @param array $config - */ - public function __construct(array $config) + public function getField($name) { - if (!isset($config['name'])) { - $config['name'] = $this->tryInferName(); + if ($this->fields === null) { + $this->getFields(); } + Utils::invariant(isset($this->fields[$name]), 'Field "%s" is not defined for type "%s"', $name, $this->name); - Utils::invariant(is_string($config['name']), 'Must provide name.'); - - $this->name = $config['name']; - $this->description = isset($config['description']) ? $config['description'] : null; - $this->resolveFieldFn = isset($config['resolveField']) ? $config['resolveField'] : null; - $this->astNode = isset($config['astNode']) ? $config['astNode'] : null; - $this->extensionASTNodes = isset($config['extensionASTNodes']) ? $config['extensionASTNodes'] : []; - $this->config = $config; + return $this->fields[$name]; } /** @@ -119,58 +129,14 @@ class ObjectType extends Type implements OutputType, CompositeType, NamedType */ public function getFields() { - if (null === $this->fields) { - $fields = isset($this->config['fields']) ? $this->config['fields'] : []; + if ($this->fields === null) { + $fields = $this->config['fields'] ?? []; $this->fields = FieldDefinition::defineFieldMap($this, $fields); } + return $this->fields; } - /** - * @param string $name - * @return FieldDefinition - * @throws \Exception - */ - public function getField($name) - { - if (null === $this->fields) { - $this->getFields(); - } - Utils::invariant(isset($this->fields[$name]), 'Field "%s" is not defined for type "%s"', $name, $this->name); - return $this->fields[$name]; - } - - /** - * @return InterfaceType[] - */ - public function getInterfaces() - { - if (null === $this->interfaces) { - $interfaces = isset($this->config['interfaces']) ? $this->config['interfaces'] : []; - $interfaces = is_callable($interfaces) ? call_user_func($interfaces) : $interfaces; - - if ($interfaces && !is_array($interfaces)) { - throw new InvariantViolation( - "{$this->name} interfaces must be an Array or a callable which returns an Array." - ); - } - - $this->interfaces = $interfaces ?: []; - } - return $this->interfaces; - } - - private function getInterfaceMap() - { - if (!$this->interfaceMap) { - $this->interfaceMap = []; - foreach ($this->getInterfaces() as $interface) { - $this->interfaceMap[$interface->name] = $interface; - } - } - return $this->interfaceMap; - } - /** * @param InterfaceType $iface * @return bool @@ -178,18 +144,56 @@ class ObjectType extends Type implements OutputType, CompositeType, NamedType public function implementsInterface($iface) { $map = $this->getInterfaceMap(); + return isset($map[$iface->name]); } + private function getInterfaceMap() + { + if (! $this->interfaceMap) { + $this->interfaceMap = []; + foreach ($this->getInterfaces() as $interface) { + $this->interfaceMap[$interface->name] = $interface; + } + } + + return $this->interfaceMap; + } + /** - * @param $value - * @param $context - * @param ResolveInfo $info + * @return InterfaceType[] + */ + public function getInterfaces() + { + if ($this->interfaces === null) { + $interfaces = $this->config['interfaces'] ?? []; + $interfaces = is_callable($interfaces) ? call_user_func($interfaces) : $interfaces; + + if ($interfaces && ! is_array($interfaces)) { + throw new InvariantViolation( + sprintf('%s interfaces must be an Array or a callable which returns an Array.', $this->name) + ); + } + + $this->interfaces = $interfaces ?: []; + } + + return $this->interfaces; + } + + /** + * @param mixed[] $value + * @param mixed[]|null $context * @return bool|null */ public function isTypeOf($value, $context, ResolveInfo $info) { - return isset($this->config['isTypeOf']) ? call_user_func($this->config['isTypeOf'], $value, $context, $info) : null; + return isset($this->config['isTypeOf']) ? call_user_func( + $this->config['isTypeOf'], + $value, + $context, + $info + ) : null; } /** @@ -203,15 +207,19 @@ class ObjectType extends Type implements OutputType, CompositeType, NamedType parent::assertValid(); Utils::invariant( - null === $this->description || is_string($this->description), - "{$this->name} description must be string if set, but it is: " . Utils::printSafe($this->description) + $this->description === null || is_string($this->description), + sprintf( + '%s description must be string if set, but it is: %s', + $this->name, + Utils::printSafe($this->description) + ) ); $isTypeOf = $this->config['isTypeOf'] ?? null; Utils::invariant( - !isset($isTypeOf) || is_callable($isTypeOf), - "{$this->name} must provide \"isTypeOf\" as a function, but got: " . Utils::printSafe($isTypeOf) + $isTypeOf === null || is_callable($isTypeOf), + sprintf('%s must provide "isTypeOf" as a function, but got: %s', $this->name, Utils::printSafe($isTypeOf)) ); foreach ($this->getFields() as $field) { diff --git a/src/Type/Definition/OutputType.php b/src/Type/Definition/OutputType.php index 4765616..7565a25 100644 --- a/src/Type/Definition/OutputType.php +++ b/src/Type/Definition/OutputType.php @@ -1,6 +1,8 @@ selections as $selectionNode) { if ($selectionNode instanceof FieldNode) { - $fields[$selectionNode->name->value] = $descend > 0 && !empty($selectionNode->selectionSet) + $fields[$selectionNode->name->value] = $descend > 0 && ! empty($selectionNode->selectionSet) ? $this->foldSelectionSet($selectionNode->selectionSet, $descend - 1) : true; - } else if ($selectionNode instanceof FragmentSpreadNode) { + } elseif ($selectionNode instanceof FragmentSpreadNode) { $spreadName = $selectionNode->name->value; if (isset($this->fragments[$spreadName])) { /** @var FragmentDefinitionNode $fragment */ $fragment = $this->fragments[$spreadName]; - $fields = array_merge_recursive($this->foldSelectionSet($fragment->selectionSet, $descend), $fields); + $fields = array_merge_recursive( + $this->foldSelectionSet($fragment->selectionSet, $descend), + $fields + ); } - } else if ($selectionNode instanceof InlineFragmentNode) { - $fields = array_merge_recursive($this->foldSelectionSet($selectionNode->selectionSet, $descend), $fields); + } elseif ($selectionNode instanceof InlineFragmentNode) { + $fields = array_merge_recursive( + $this->foldSelectionSet($selectionNode->selectionSet, $descend), + $fields + ); } } diff --git a/src/Type/Definition/ScalarType.php b/src/Type/Definition/ScalarType.php index eee6bb0..d93b2e7 100644 --- a/src/Type/Definition/ScalarType.php +++ b/src/Type/Definition/ScalarType.php @@ -1,8 +1,12 @@ name = isset($config['name']) ? $config['name'] : $this->tryInferName(); - $this->description = isset($config['description']) ? $config['description'] : $this->description; - $this->astNode = isset($config['astNode']) ? $config['astNode'] : null; - $this->config = $config; + $this->name = $config['name'] ?? $this->tryInferName(); + $this->description = $config['description'] ?? $this->description; + $this->astNode = $config['astNode'] ?? null; + $this->config = $config; Utils::invariant(is_string($this->name), 'Must provide name.'); } diff --git a/src/Type/Definition/StringType.php b/src/Type/Definition/StringType.php index cabd344..61dbd39 100644 --- a/src/Type/Definition/StringType.php +++ b/src/Type/Definition/StringType.php @@ -1,26 +1,29 @@ coerceString($value); } + private function coerceString($value) + { + if (is_array($value)) { + throw new Error( + 'String cannot represent an array value: ' . + Utils::printSafe($value) + ); + } + + return (string) $value; + } + /** * @param mixed $value * @return string @@ -60,12 +76,12 @@ represent free-form human-readable text.'; } /** - * @param $valueNode - * @param array|null $variables + * @param Node $valueNode + * @param mixed[]|null $variables * @return null|string * @throws \Exception */ - public function parseLiteral($valueNode, array $variables = null) + public function parseLiteral($valueNode, ?array $variables = null) { if ($valueNode instanceof StringValueNode) { return $valueNode->value; @@ -74,15 +90,4 @@ represent free-form human-readable text.'; // Intentionally without message, as all information already in wrapped Exception throw new \Exception(); } - - private function coerceString($value) { - if (is_array($value)) { - throw new Error( - 'String cannot represent an array value: ' . - Utils::printSafe($value) - ); - } - - return (string) $value; - } } diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index 0dd5ef4..0fab77d 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -1,35 +1,48 @@ new IDType(), + self::STRING => new StringType(), + self::FLOAT => new FloatType(), + self::INT => new IntType(), + self::BOOLEAN => new BooleanType(), + ]; + } + + return $name ? self::$internalTypes[$name] : self::$internalTypes; + } + /** * @api * @return StringType @@ -96,21 +128,31 @@ abstract class Type implements \JsonSerializable } /** - * @param $name - * @return array|IDType|StringType|FloatType|IntType|BooleanType + * Checks if the type is a builtin type + * + * @return bool */ - private static function getInternalType($name = null) + public static function isBuiltInType(Type $type) { - if (null === self::$internalTypes) { - self::$internalTypes = [ - self::ID => new IDType(), - self::STRING => new StringType(), - self::FLOAT => new FloatType(), - self::INT => new IntType(), - self::BOOLEAN => new BooleanType() - ]; + return in_array($type->name, array_keys(self::getAllBuiltInTypes())); + } + + /** + * Returns all builtin in types including base scalar and + * introspection types + * + * @return Type[] + */ + public static function getAllBuiltInTypes() + { + if (self::$builtInTypes === null) { + self::$builtInTypes = array_merge( + Introspection::getTypes(), + self::getInternalTypes() + ); } - return $name ? self::$internalTypes[$name] : self::$internalTypes; + + return self::$builtInTypes; } /** @@ -123,34 +165,6 @@ abstract class Type implements \JsonSerializable 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 * @param Type $type @@ -160,11 +174,28 @@ abstract class Type implements \JsonSerializable { return $type instanceof InputType && ( - !$type instanceof WrappingType || + ! $type instanceof WrappingType || self::getNamedType($type) instanceof InputType ); } + /** + * @api + * @param Type $type + * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType + */ + public static function getNamedType($type) + { + if ($type === null) { + return null; + } + while ($type instanceof WrappingType) { + $type = $type->getWrappedType(); + } + + return $type; + } + /** * @api * @param Type $type @@ -174,14 +205,14 @@ abstract class Type implements \JsonSerializable { return $type instanceof OutputType && ( - !$type instanceof WrappingType || + ! $type instanceof WrappingType || self::getNamedType($type) instanceof OutputType ); } /** * @api - * @param $type + * @param Type $type * @return bool */ public static function isLeafType($type) @@ -209,16 +240,6 @@ abstract class Type implements \JsonSerializable return $type instanceof AbstractType; } - /** - * @api - * @param Type $type - * @return bool - */ - public static function isType($type) - { - return $type instanceof Type; - } - /** * @param mixed $type * @return mixed @@ -236,68 +257,21 @@ abstract class Type implements \JsonSerializable /** * @api * @param Type $type - * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType + * @return bool */ - public static function getNullableType($type) + public static function isType($type) { - return $type instanceof NonNull ? $type->getWrappedType() : $type; + return $type instanceof Type; } /** * @api * @param Type $type - * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType + * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType */ - public static function getNamedType($type) + public static function getNullableType($type) { - if (null === $type) { - return null; - } - while ($type instanceof WrappingType) { - $type = $type->getWrappedType(); - } - return $type; - } - - /** - * @var string - */ - public $name; - - /** - * @var string|null - */ - public $description; - - /** - * @var TypeDefinitionNode|null - */ - public $astNode; - - /** - * @var array - */ - public $config; - - /** - * @return null|string - */ - protected function tryInferName() - { - if ($this->name) { - return $this->name; - } - - // If class is extended - infer name from className - // QueryType -> Type - // SomeOtherType -> SomeOther - $tmp = new \ReflectionClass($this); - $name = $tmp->getShortName(); - - if ($tmp->getNamespaceName() !== __NAMESPACE__) { - return preg_replace('~Type$~', '', $name); - } - return null; + return $type instanceof NonNull ? $type->getWrappedType() : $type; } /** @@ -311,17 +285,17 @@ abstract class Type implements \JsonSerializable /** * @return string */ - public function toString() + public function jsonSerialize() { - return $this->name; + return $this->toString(); } /** * @return string */ - public function jsonSerialize() + public function toString() { - return $this->toString(); + return $this->name; } /** @@ -337,4 +311,26 @@ abstract class Type implements \JsonSerializable echo $e; } } + + /** + * @return null|string + */ + protected function tryInferName() + { + if ($this->name) { + return $this->name; + } + + // If class is extended - infer name from className + // QueryType -> Type + // SomeOtherType -> SomeOther + $tmp = new \ReflectionClass($this); + $name = $tmp->getShortName(); + + if ($tmp->getNamespaceName() !== __NAMESPACE__) { + return preg_replace('~Type$~', '', $name); + } + + return null; + } } diff --git a/src/Type/Definition/UnionType.php b/src/Type/Definition/UnionType.php index d26efac..26ba005 100644 --- a/src/Type/Definition/UnionType.php +++ b/src/Type/Definition/UnionType.php @@ -1,38 +1,35 @@ tryInferName(); } @@ -43,10 +40,29 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType, * the default implemenation will call `isTypeOf` on each implementing * Object type. */ - $this->name = $config['name']; - $this->description = isset($config['description']) ? $config['description'] : null; - $this->astNode = isset($config['astNode']) ? $config['astNode'] : null; - $this->config = $config; + $this->name = $config['name']; + $this->description = $config['description'] ?? null; + $this->astNode = $config['astNode'] ?? null; + $this->config = $config; + } + + /** + * @return mixed + */ + public function isPossibleType(Type $type) + { + if (! $type instanceof ObjectType) { + return false; + } + + if ($this->possibleTypeNames === null) { + $this->possibleTypeNames = []; + foreach ($this->getTypes() as $possibleType) { + $this->possibleTypeNames[$possibleType->name] = true; + } + } + + return isset($this->possibleTypeNames[$type->name]); } /** @@ -54,60 +70,45 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType, */ public function getTypes() { - if (null === $this->types) { - if (!isset($this->config['types'])) { + if ($this->types === null) { + if (! isset($this->config['types'])) { $types = null; - } else if (is_callable($this->config['types'])) { + } elseif (is_callable($this->config['types'])) { $types = call_user_func($this->config['types']); } else { $types = $this->config['types']; } - if (!is_array($types)) { + if (! is_array($types)) { throw new InvariantViolation( - "Must provide Array of types or a callable which returns " . - "such an array for Union {$this->name}" + sprintf( + 'Must provide Array of types or a callable which returns such an array for Union %s', + $this->name + ) ); } $this->types = $types; } + return $this->types; } - /** - * @param Type $type - * @return mixed - */ - public function isPossibleType(Type $type) - { - if (!$type instanceof ObjectType) { - return false; - } - - if (null === $this->possibleTypeNames) { - $this->possibleTypeNames = []; - foreach ($this->getTypes() as $possibleType) { - $this->possibleTypeNames[$possibleType->name] = true; - } - } - return isset($this->possibleTypeNames[$type->name]); - } - /** * Resolves concrete ObjectType for given object value * - * @param $objectValue - * @param $context - * @param ResolveInfo $info + * @param object $objectValue + * @param mixed $context * @return callable|null */ public function resolveType($objectValue, $context, ResolveInfo $info) { if (isset($this->config['resolveType'])) { $fn = $this->config['resolveType']; + return $fn($objectValue, $context, $info); } + return null; } @@ -118,11 +119,17 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType, { parent::assertValid(); - if (isset($this->config['resolveType'])) { - Utils::invariant( - is_callable($this->config['resolveType']), - "{$this->name} must provide \"resolveType\" as a function, but got: " . Utils::printSafe($this->config['resolveType']) - ); + if (! isset($this->config['resolveType'])) { + return; } + + Utils::invariant( + is_callable($this->config['resolveType']), + sprintf( + '%s must provide "resolveType" as a function, but got: %s', + $this->name, + Utils::printSafe($this->config['resolveType']) + ) + ); } } diff --git a/src/Type/Definition/UnmodifiedType.php b/src/Type/Definition/UnmodifiedType.php index ef01c10..7a31c47 100644 --- a/src/Type/Definition/UnmodifiedType.php +++ b/src/Type/Definition/UnmodifiedType.php @@ -1,4 +1,7 @@ - */ + /** @var array */ private $implementations = []; /** - * EagerResolution constructor. + * * @param Type[] $initialTypes */ public function __construct(array $initialTypes) @@ -42,10 +38,12 @@ class EagerResolution implements Resolution // Keep track of all possible types for abstract types foreach ($this->typeMap as $typeName => $type) { - if ($type instanceof ObjectType) { - foreach ($type->getInterfaces() as $iface) { - $this->implementations[$iface->name][] = $type; - } + if (! ($type instanceof ObjectType)) { + continue; + } + + foreach ($type->getInterfaces() as $iface) { + $this->implementations[$iface->name][] = $type; } } } @@ -55,7 +53,7 @@ class EagerResolution implements Resolution */ public function resolveType($name) { - return isset($this->typeMap[$name]) ? $this->typeMap[$name] : null; + return $this->typeMap[$name] ?? null; } /** @@ -63,7 +61,7 @@ class EagerResolution implements Resolution */ public function resolvePossibleTypes(AbstractType $abstractType) { - if (!isset($this->typeMap[$abstractType->name])) { + if (! isset($this->typeMap[$abstractType->name])) { return []; } @@ -73,7 +71,37 @@ class EagerResolution implements Resolution /** @var InterfaceType $abstractType */ Utils::invariant($abstractType instanceof InterfaceType); - return isset($this->implementations[$abstractType->name]) ? $this->implementations[$abstractType->name] : []; + + return $this->implementations[$abstractType->name] ?? []; + } + + /** + * Returns serializable schema representation suitable for GraphQL\Type\LazyResolution + * + * @return mixed[] + */ + public function getDescriptor() + { + $typeMap = []; + $possibleTypesMap = []; + foreach ($this->getTypeMap() as $type) { + if ($type instanceof UnionType) { + foreach ($type->getTypes() as $innerType) { + $possibleTypesMap[$type->name][$innerType->name] = 1; + } + } elseif ($type instanceof InterfaceType) { + foreach ($this->implementations[$type->name] as $obj) { + $possibleTypesMap[$type->name][$obj->name] = 1; + } + } + $typeMap[$type->name] = 1; + } + + return [ + 'version' => '1.0', + 'typeMap' => $typeMap, + 'possibleTypeMap' => $possibleTypesMap, + ]; } /** @@ -83,32 +111,4 @@ class EagerResolution implements Resolution { return $this->typeMap; } - - /** - * Returns serializable schema representation suitable for GraphQL\Type\LazyResolution - * - * @return array - */ - public function getDescriptor() - { - $typeMap = []; - $possibleTypesMap = []; - foreach ($this->getTypeMap() as $type) { - if ($type instanceof UnionType) { - foreach ($type->getTypes() as $innerType) { - $possibleTypesMap[$type->name][$innerType->name] = 1; - } - } else if ($type instanceof InterfaceType) { - foreach ($this->implementations[$type->name] as $obj) { - $possibleTypesMap[$type->name][$obj->name] = 1; - } - } - $typeMap[$type->name] = 1; - } - return [ - 'version' => '1.0', - 'typeMap' => $typeMap, - 'possibleTypeMap' => $possibleTypesMap - ]; - } } diff --git a/src/Type/Introspection.php b/src/Type/Introspection.php index 911b171..cab3a62 100644 --- a/src/Type/Introspection.php +++ b/src/Type/Introspection.php @@ -1,9 +1,12 @@ boolean]).', E_USER_DEPRECATED ); $descriptions = $options; } else { - $descriptions = !array_key_exists('descriptions', $options) || $options['descriptions'] === true; + $descriptions = ! array_key_exists('descriptions', $options) || $options['descriptions'] === true; } $descriptionField = $descriptions ? 'description' : ''; @@ -154,63 +155,63 @@ class Introspection EOD; } - public static function getTypes() - { - return [ - '__Schema' => self::_schema(), - '__Type' => self::_type(), - '__Directive' => self::_directive(), - '__Field' => self::_field(), - '__InputValue' => self::_inputValue(), - '__EnumValue' => self::_enumValue(), - '__TypeKind' => self::_typeKind(), - '__DirectiveLocation' => self::_directiveLocation(), - ]; - } - /** * @param Type $type * @return bool */ public static function isIntrospectionType($type) { - return in_array($type->name, array_keys(self::getTypes())); + return array_key_exists($type->name, self::getTypes()); + } + + public static function getTypes() + { + return [ + '__Schema' => self::_schema(), + '__Type' => self::_type(), + '__Directive' => self::_directive(), + '__Field' => self::_field(), + '__InputValue' => self::_inputValue(), + '__EnumValue' => self::_enumValue(), + '__TypeKind' => self::_typeKind(), + '__DirectiveLocation' => self::_directiveLocation(), + ]; } public static function _schema() { - if (!isset(self::$map['__Schema'])) { + if (! isset(self::$map['__Schema'])) { self::$map['__Schema'] = new ObjectType([ - 'name' => '__Schema', + 'name' => '__Schema', 'isIntrospection' => true, - 'description' => + 'description' => 'A GraphQL Schema defines the capabilities of a GraphQL ' . 'server. It exposes all available types and directives on ' . 'the server, as well as the entry points for query, mutation, and ' . 'subscription operations.', - 'fields' => [ - 'types' => [ + 'fields' => [ + 'types' => [ 'description' => 'A list of all types supported by this server.', - 'type' => new NonNull(new ListOfType(new NonNull(self::_type()))), - 'resolve' => function (Schema $schema) { + 'type' => new NonNull(new ListOfType(new NonNull(self::_type()))), + 'resolve' => function (Schema $schema) { return array_values($schema->getTypeMap()); - } + }, ], - 'queryType' => [ + 'queryType' => [ 'description' => 'The type that query operations will be rooted at.', - 'type' => new NonNull(self::_type()), - 'resolve' => function (Schema $schema) { + 'type' => new NonNull(self::_type()), + 'resolve' => function (Schema $schema) { return $schema->getQueryType(); - } + }, ], - 'mutationType' => [ + 'mutationType' => [ 'description' => 'If this server supports mutation, the type that ' . 'mutation operations will be rooted at.', - 'type' => self::_type(), - 'resolve' => function (Schema $schema) { + 'type' => self::_type(), + 'resolve' => function (Schema $schema) { return $schema->getMutationType(); - } + }, ], 'subscriptionType' => [ 'description' => 'If this server support subscription, the type that subscription operations will be rooted at.', @@ -219,188 +220,40 @@ EOD; return $schema->getSubscriptionType(); }, ], - 'directives' => [ + 'directives' => [ 'description' => 'A list of all directives supported by this server.', - 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_directive()))), - 'resolve' => function(Schema $schema) { + 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_directive()))), + 'resolve' => function (Schema $schema) { return $schema->getDirectives(); - } - ] - ] + }, + ], + ], ]); } + return self::$map['__Schema']; } - public static function _directive() - { - if (!isset(self::$map['__Directive'])) { - self::$map['__Directive'] = new ObjectType([ - 'name' => '__Directive', - 'isIntrospection' => true, - 'description' => 'A Directive provides a way to describe alternate runtime execution and ' . - 'type validation behavior in a GraphQL document.' . - "\n\nIn some cases, you need to provide options to alter GraphQL's " . - 'execution behavior in ways field arguments will not suffice, such as ' . - 'conditionally including or skipping a field. Directives provide this by ' . - 'describing additional information to the executor.', - 'fields' => [ - 'name' => ['type' => Type::nonNull(Type::string())], - 'description' => ['type' => Type::string()], - 'locations' => [ - 'type' => Type::nonNull(Type::listOf(Type::nonNull( - self::_directiveLocation() - ))) - ], - 'args' => [ - 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))), - 'resolve' => function (Directive $directive) { - return $directive->args ?: []; - } - ], - - // NOTE: the following three fields are deprecated and are no longer part - // of the GraphQL specification. - 'onOperation' => [ - 'deprecationReason' => 'Use `locations`.', - 'type' => Type::nonNull(Type::boolean()), - 'resolve' => function($d) { - return in_array(DirectiveLocation::QUERY, $d->locations) || - in_array(DirectiveLocation::MUTATION, $d->locations) || - in_array(DirectiveLocation::SUBSCRIPTION, $d->locations); - } - ], - 'onFragment' => [ - 'deprecationReason' => 'Use `locations`.', - 'type' => Type::nonNull(Type::boolean()), - 'resolve' => function($d) { - return in_array(DirectiveLocation::FRAGMENT_SPREAD, $d->locations) || - in_array(DirectiveLocation::INLINE_FRAGMENT, $d->locations) || - in_array(DirectiveLocation::FRAGMENT_DEFINITION, $d->locations); - } - ], - 'onField' => [ - 'deprecationReason' => 'Use `locations`.', - 'type' => Type::nonNull(Type::boolean()), - 'resolve' => function($d) { - return in_array(DirectiveLocation::FIELD, $d->locations); - } - ] - ] - ]); - } - return self::$map['__Directive']; - } - - public static function _directiveLocation() - { - if (!isset(self::$map['__DirectiveLocation'])) { - self::$map['__DirectiveLocation'] = new EnumType([ - 'name' => '__DirectiveLocation', - 'isIntrospection' => true, - 'description' => - 'A Directive can be adjacent to many parts of the GraphQL language, a ' . - '__DirectiveLocation describes one such possible adjacencies.', - 'values' => [ - 'QUERY' => [ - 'value' => DirectiveLocation::QUERY, - 'description' => 'Location adjacent to a query operation.' - ], - 'MUTATION' => [ - 'value' => DirectiveLocation::MUTATION, - 'description' => 'Location adjacent to a mutation operation.' - ], - 'SUBSCRIPTION' => [ - 'value' => DirectiveLocation::SUBSCRIPTION, - 'description' => 'Location adjacent to a subscription operation.' - ], - 'FIELD' => [ - 'value' => DirectiveLocation::FIELD, - 'description' => 'Location adjacent to a field.' - ], - 'FRAGMENT_DEFINITION' => [ - 'value' => DirectiveLocation::FRAGMENT_DEFINITION, - 'description' => 'Location adjacent to a fragment definition.' - ], - 'FRAGMENT_SPREAD' => [ - 'value' => DirectiveLocation::FRAGMENT_SPREAD, - 'description' => 'Location adjacent to a fragment spread.' - ], - 'INLINE_FRAGMENT' => [ - 'value' => DirectiveLocation::INLINE_FRAGMENT, - 'description' => 'Location adjacent to an inline fragment.' - ], - 'SCHEMA' => [ - 'value' => DirectiveLocation::SCHEMA, - 'description' => 'Location adjacent to a schema definition.' - ], - 'SCALAR' => [ - 'value' => DirectiveLocation::SCALAR, - 'description' => 'Location adjacent to a scalar definition.' - ], - 'OBJECT' => [ - 'value' => DirectiveLocation::OBJECT, - 'description' => 'Location adjacent to an object type definition.' - ], - 'FIELD_DEFINITION' => [ - 'value' => DirectiveLocation::FIELD_DEFINITION, - 'description' => 'Location adjacent to a field definition.' - ], - 'ARGUMENT_DEFINITION' => [ - 'value' => DirectiveLocation::ARGUMENT_DEFINITION, - 'description' => 'Location adjacent to an argument definition.' - ], - 'INTERFACE' => [ - 'value' => DirectiveLocation::IFACE, - 'description' => 'Location adjacent to an interface definition.' - ], - 'UNION' => [ - 'value' => DirectiveLocation::UNION, - 'description' => 'Location adjacent to a union definition.' - ], - 'ENUM' => [ - 'value' => DirectiveLocation::ENUM, - 'description' => 'Location adjacent to an enum definition.' - ], - 'ENUM_VALUE' => [ - 'value' => DirectiveLocation::ENUM_VALUE, - 'description' => 'Location adjacent to an enum value definition.' - ], - 'INPUT_OBJECT' => [ - 'value' => DirectiveLocation::INPUT_OBJECT, - 'description' => 'Location adjacent to an input object type definition.' - ], - 'INPUT_FIELD_DEFINITION' => [ - 'value' => DirectiveLocation::INPUT_FIELD_DEFINITION, - 'description' => 'Location adjacent to an input object field definition.' - ] - - ] - ]); - } - return self::$map['__DirectiveLocation']; - } - public static function _type() { - if (!isset(self::$map['__Type'])) { + if (! isset(self::$map['__Type'])) { self::$map['__Type'] = new ObjectType([ - 'name' => '__Type', + 'name' => '__Type', 'isIntrospection' => true, - 'description' => + 'description' => 'The fundamental unit of any GraphQL Schema is the type. There are ' . 'many kinds of types in GraphQL as represented by the `__TypeKind` enum.' . - "\n\n". + "\n\n" . 'Depending on the kind of a type, certain fields describe ' . 'information about that type. Scalar types provide no information ' . 'beyond a name and description, while Enum types provide their values. ' . 'Object and Interface types provide the fields they describe. Abstract ' . 'types, Union and Interface, provide the Object types possible ' . 'at runtime. List and NonNull types compose other types.', - 'fields' => function() { + 'fields' => function () { return [ - 'kind' => [ - 'type' => Type::nonNull(self::_typeKind()), + 'kind' => [ + 'type' => Type::nonNull(self::_typeKind()), 'resolve' => function (Type $type) { switch (true) { case $type instanceof ListOfType: @@ -420,305 +273,479 @@ EOD; case $type instanceof UnionType: return TypeKind::UNION; default: - throw new \Exception("Unknown kind of type: " . Utils::printSafe($type)); + throw new \Exception('Unknown kind of type: ' . Utils::printSafe($type)); } - } + }, ], - 'name' => ['type' => Type::string()], - 'description' => ['type' => Type::string()], - 'fields' => [ - 'type' => Type::listOf(Type::nonNull(self::_field())), - 'args' => [ - 'includeDeprecated' => ['type' => Type::boolean(), 'defaultValue' => false] + 'name' => ['type' => Type::string()], + 'description' => ['type' => Type::string()], + 'fields' => [ + 'type' => Type::listOf(Type::nonNull(self::_field())), + 'args' => [ + 'includeDeprecated' => ['type' => Type::boolean(), 'defaultValue' => false], ], 'resolve' => function (Type $type, $args) { if ($type instanceof ObjectType || $type instanceof InterfaceType) { $fields = $type->getFields(); if (empty($args['includeDeprecated'])) { - $fields = array_filter($fields, function (FieldDefinition $field) { - return !$field->deprecationReason; - }); + $fields = array_filter( + $fields, + function (FieldDefinition $field) { + return ! $field->deprecationReason; + } + ); } + return array_values($fields); } + return null; - } + }, ], - 'interfaces' => [ - 'type' => Type::listOf(Type::nonNull(self::_type())), + 'interfaces' => [ + 'type' => Type::listOf(Type::nonNull(self::_type())), 'resolve' => function ($type) { if ($type instanceof ObjectType) { return $type->getInterfaces(); } + return null; - } + }, ], 'possibleTypes' => [ - 'type' => Type::listOf(Type::nonNull(self::_type())), + 'type' => Type::listOf(Type::nonNull(self::_type())), 'resolve' => function ($type, $args, $context, ResolveInfo $info) { if ($type instanceof InterfaceType || $type instanceof UnionType) { return $info->schema->getPossibleTypes($type); } + return null; - } + }, ], - 'enumValues' => [ - 'type' => Type::listOf(Type::nonNull(self::_enumValue())), - 'args' => [ - 'includeDeprecated' => ['type' => Type::boolean(), 'defaultValue' => false] + 'enumValues' => [ + 'type' => Type::listOf(Type::nonNull(self::_enumValue())), + 'args' => [ + 'includeDeprecated' => ['type' => Type::boolean(), 'defaultValue' => false], ], 'resolve' => function ($type, $args) { if ($type instanceof EnumType) { $values = array_values($type->getValues()); if (empty($args['includeDeprecated'])) { - $values = array_filter($values, function ($value) { - return !$value->deprecationReason; - }); + $values = array_filter( + $values, + function ($value) { + return ! $value->deprecationReason; + } + ); } return $values; } + return null; - } + }, ], - 'inputFields' => [ - 'type' => Type::listOf(Type::nonNull(self::_inputValue())), + 'inputFields' => [ + 'type' => Type::listOf(Type::nonNull(self::_inputValue())), 'resolve' => function ($type) { if ($type instanceof InputObjectType) { return array_values($type->getFields()); } + return null; - } + }, ], - 'ofType' => [ - 'type' => self::_type(), + 'ofType' => [ + 'type' => self::_type(), 'resolve' => function ($type) { if ($type instanceof WrappingType) { return $type->getWrappedType(); } + return null; - } - ] + }, + ], ]; - } + }, ]); } + return self::$map['__Type']; } + public static function _typeKind() + { + if (! isset(self::$map['__TypeKind'])) { + self::$map['__TypeKind'] = new EnumType([ + 'name' => '__TypeKind', + 'isIntrospection' => true, + 'description' => 'An enum describing what kind of type a given `__Type` is.', + 'values' => [ + 'SCALAR' => [ + 'value' => TypeKind::SCALAR, + 'description' => 'Indicates this type is a scalar.', + ], + 'OBJECT' => [ + 'value' => TypeKind::OBJECT, + 'description' => 'Indicates this type is an object. `fields` and `interfaces` are valid fields.', + ], + 'INTERFACE' => [ + 'value' => TypeKind::INTERFACE_KIND, + 'description' => 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.', + ], + 'UNION' => [ + 'value' => TypeKind::UNION, + 'description' => 'Indicates this type is a union. `possibleTypes` is a valid field.', + ], + 'ENUM' => [ + 'value' => TypeKind::ENUM, + 'description' => 'Indicates this type is an enum. `enumValues` is a valid field.', + ], + 'INPUT_OBJECT' => [ + 'value' => TypeKind::INPUT_OBJECT, + 'description' => 'Indicates this type is an input object. `inputFields` is a valid field.', + ], + 'LIST' => [ + 'value' => TypeKind::LIST_KIND, + 'description' => 'Indicates this type is a list. `ofType` is a valid field.', + ], + 'NON_NULL' => [ + 'value' => TypeKind::NON_NULL, + 'description' => 'Indicates this type is a non-null. `ofType` is a valid field.', + ], + ], + ]); + } + + return self::$map['__TypeKind']; + } + public static function _field() { - if (!isset(self::$map['__Field'])) { - + if (! isset(self::$map['__Field'])) { self::$map['__Field'] = new ObjectType([ - 'name' => '__Field', + 'name' => '__Field', 'isIntrospection' => true, - 'description' => + 'description' => 'Object and Interface types are described by a list of Fields, each of ' . 'which has a name, potentially a list of arguments, and a return type.', - 'fields' => function() { + 'fields' => function () { return [ - 'name' => ['type' => Type::nonNull(Type::string())], - 'description' => ['type' => Type::string()], - 'args' => [ - 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))), + 'name' => ['type' => Type::nonNull(Type::string())], + 'description' => ['type' => Type::string()], + 'args' => [ + 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))), 'resolve' => function (FieldDefinition $field) { return empty($field->args) ? [] : $field->args; - } + }, ], - 'type' => [ - 'type' => Type::nonNull(self::_type()), + 'type' => [ + 'type' => Type::nonNull(self::_type()), 'resolve' => function (FieldDefinition $field) { return $field->getType(); - } + }, ], - 'isDeprecated' => [ - 'type' => Type::nonNull(Type::boolean()), + 'isDeprecated' => [ + 'type' => Type::nonNull(Type::boolean()), 'resolve' => function (FieldDefinition $field) { - return !!$field->deprecationReason; - } + return (bool) $field->deprecationReason; + }, ], 'deprecationReason' => [ - 'type' => Type::string() - ] + 'type' => Type::string(), + ], ]; - } + }, ]); } + return self::$map['__Field']; } public static function _inputValue() { - if (!isset(self::$map['__InputValue'])) { + if (! isset(self::$map['__InputValue'])) { self::$map['__InputValue'] = new ObjectType([ - 'name' => '__InputValue', + 'name' => '__InputValue', 'isIntrospection' => true, - 'description' => + 'description' => 'Arguments provided to Fields or Directives and the input fields of an ' . 'InputObject are represented as Input Values which describe their type ' . 'and optionally a default value.', - 'fields' => function() { + 'fields' => function () { return [ - 'name' => ['type' => Type::nonNull(Type::string())], - 'description' => ['type' => Type::string()], - 'type' => [ - 'type' => Type::nonNull(self::_type()), + 'name' => ['type' => Type::nonNull(Type::string())], + 'description' => ['type' => Type::string()], + 'type' => [ + 'type' => Type::nonNull(self::_type()), 'resolve' => function ($value) { return method_exists($value, 'getType') ? $value->getType() : $value->type; - } + }, ], 'defaultValue' => [ - 'type' => Type::string(), + 'type' => Type::string(), 'description' => 'A GraphQL-formatted string representing the default value for this input value.', - 'resolve' => function ($inputValue) { + 'resolve' => function ($inputValue) { /** @var FieldArgument|InputObjectField $inputValue */ - return !$inputValue->defaultValueExists() + return ! $inputValue->defaultValueExists() ? null - : Printer::doPrint(AST::astFromValue($inputValue->defaultValue, $inputValue->getType())); - } - ] + : Printer::doPrint(AST::astFromValue( + $inputValue->defaultValue, + $inputValue->getType() + )); + }, + ], ]; - } + }, ]); } + return self::$map['__InputValue']; } public static function _enumValue() { - if (!isset(self::$map['__EnumValue'])) { + if (! isset(self::$map['__EnumValue'])) { self::$map['__EnumValue'] = new ObjectType([ - 'name' => '__EnumValue', + 'name' => '__EnumValue', 'isIntrospection' => true, - 'description' => + 'description' => 'One possible value for a given Enum. Enum values are unique values, not ' . 'a placeholder for a string or numeric value. However an Enum value is ' . 'returned in a JSON response as a string.', - 'fields' => [ - 'name' => ['type' => Type::nonNull(Type::string())], - 'description' => ['type' => Type::string()], - 'isDeprecated' => [ - 'type' => Type::nonNull(Type::boolean()), + 'fields' => [ + 'name' => ['type' => Type::nonNull(Type::string())], + 'description' => ['type' => Type::string()], + 'isDeprecated' => [ + 'type' => Type::nonNull(Type::boolean()), 'resolve' => function ($enumValue) { - return !!$enumValue->deprecationReason; - } + return (bool) $enumValue->deprecationReason; + }, ], 'deprecationReason' => [ - 'type' => Type::string() - ] - ] + 'type' => Type::string(), + ], + ], ]); } + return self::$map['__EnumValue']; } - public static function _typeKind() + public static function _directive() { - if (!isset(self::$map['__TypeKind'])) { - self::$map['__TypeKind'] = new EnumType([ - 'name' => '__TypeKind', + if (! isset(self::$map['__Directive'])) { + self::$map['__Directive'] = new ObjectType([ + 'name' => '__Directive', 'isIntrospection' => true, - 'description' => 'An enum describing what kind of type a given `__Type` is.', - 'values' => [ - 'SCALAR' => [ - 'value' => TypeKind::SCALAR, - 'description' => 'Indicates this type is a scalar.' + 'description' => 'A Directive provides a way to describe alternate runtime execution and ' . + 'type validation behavior in a GraphQL document.' . + "\n\nIn some cases, you need to provide options to alter GraphQL's " . + 'execution behavior in ways field arguments will not suffice, such as ' . + 'conditionally including or skipping a field. Directives provide this by ' . + 'describing additional information to the executor.', + 'fields' => [ + 'name' => ['type' => Type::nonNull(Type::string())], + 'description' => ['type' => Type::string()], + 'locations' => [ + 'type' => Type::nonNull(Type::listOf(Type::nonNull( + self::_directiveLocation() + ))), ], - 'OBJECT' => [ - 'value' => TypeKind::OBJECT, - 'description' => 'Indicates this type is an object. `fields` and `interfaces` are valid fields.' + 'args' => [ + 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))), + 'resolve' => function (Directive $directive) { + return $directive->args ?: []; + }, ], - 'INTERFACE' => [ - 'value' => TypeKind::INTERFACE_KIND, - 'description' => 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.' + + // NOTE: the following three fields are deprecated and are no longer part + // of the GraphQL specification. + 'onOperation' => [ + 'deprecationReason' => 'Use `locations`.', + 'type' => Type::nonNull(Type::boolean()), + 'resolve' => function ($d) { + return in_array(DirectiveLocation::QUERY, $d->locations) || + in_array(DirectiveLocation::MUTATION, $d->locations) || + in_array(DirectiveLocation::SUBSCRIPTION, $d->locations); + }, ], - 'UNION' => [ - 'value' => TypeKind::UNION, - 'description' => 'Indicates this type is a union. `possibleTypes` is a valid field.' + 'onFragment' => [ + 'deprecationReason' => 'Use `locations`.', + 'type' => Type::nonNull(Type::boolean()), + 'resolve' => function ($d) { + return in_array(DirectiveLocation::FRAGMENT_SPREAD, $d->locations) || + in_array(DirectiveLocation::INLINE_FRAGMENT, $d->locations) || + in_array(DirectiveLocation::FRAGMENT_DEFINITION, $d->locations); + }, ], - 'ENUM' => [ - 'value' => TypeKind::ENUM, - 'description' => 'Indicates this type is an enum. `enumValues` is a valid field.' + 'onField' => [ + 'deprecationReason' => 'Use `locations`.', + 'type' => Type::nonNull(Type::boolean()), + 'resolve' => function ($d) { + return in_array(DirectiveLocation::FIELD, $d->locations); + }, ], - 'INPUT_OBJECT' => [ - 'value' => TypeKind::INPUT_OBJECT, - 'description' => 'Indicates this type is an input object. `inputFields` is a valid field.' - ], - 'LIST' => [ - 'value' => TypeKind::LIST_KIND, - 'description' => 'Indicates this type is a list. `ofType` is a valid field.' - ], - 'NON_NULL' => [ - 'value' => TypeKind::NON_NULL, - 'description' => 'Indicates this type is a non-null. `ofType` is a valid field.' - ] - ] + ], ]); } - return self::$map['__TypeKind']; + + return self::$map['__Directive']; + } + + public static function _directiveLocation() + { + if (! isset(self::$map['__DirectiveLocation'])) { + self::$map['__DirectiveLocation'] = new EnumType([ + 'name' => '__DirectiveLocation', + 'isIntrospection' => true, + 'description' => + 'A Directive can be adjacent to many parts of the GraphQL language, a ' . + '__DirectiveLocation describes one such possible adjacencies.', + 'values' => [ + 'QUERY' => [ + 'value' => DirectiveLocation::QUERY, + 'description' => 'Location adjacent to a query operation.', + ], + 'MUTATION' => [ + 'value' => DirectiveLocation::MUTATION, + 'description' => 'Location adjacent to a mutation operation.', + ], + 'SUBSCRIPTION' => [ + 'value' => DirectiveLocation::SUBSCRIPTION, + 'description' => 'Location adjacent to a subscription operation.', + ], + 'FIELD' => [ + 'value' => DirectiveLocation::FIELD, + 'description' => 'Location adjacent to a field.', + ], + 'FRAGMENT_DEFINITION' => [ + 'value' => DirectiveLocation::FRAGMENT_DEFINITION, + 'description' => 'Location adjacent to a fragment definition.', + ], + 'FRAGMENT_SPREAD' => [ + 'value' => DirectiveLocation::FRAGMENT_SPREAD, + 'description' => 'Location adjacent to a fragment spread.', + ], + 'INLINE_FRAGMENT' => [ + 'value' => DirectiveLocation::INLINE_FRAGMENT, + 'description' => 'Location adjacent to an inline fragment.', + ], + 'SCHEMA' => [ + 'value' => DirectiveLocation::SCHEMA, + 'description' => 'Location adjacent to a schema definition.', + ], + 'SCALAR' => [ + 'value' => DirectiveLocation::SCALAR, + 'description' => 'Location adjacent to a scalar definition.', + ], + 'OBJECT' => [ + 'value' => DirectiveLocation::OBJECT, + 'description' => 'Location adjacent to an object type definition.', + ], + 'FIELD_DEFINITION' => [ + 'value' => DirectiveLocation::FIELD_DEFINITION, + 'description' => 'Location adjacent to a field definition.', + ], + 'ARGUMENT_DEFINITION' => [ + 'value' => DirectiveLocation::ARGUMENT_DEFINITION, + 'description' => 'Location adjacent to an argument definition.', + ], + 'INTERFACE' => [ + 'value' => DirectiveLocation::IFACE, + 'description' => 'Location adjacent to an interface definition.', + ], + 'UNION' => [ + 'value' => DirectiveLocation::UNION, + 'description' => 'Location adjacent to a union definition.', + ], + 'ENUM' => [ + 'value' => DirectiveLocation::ENUM, + 'description' => 'Location adjacent to an enum definition.', + ], + 'ENUM_VALUE' => [ + 'value' => DirectiveLocation::ENUM_VALUE, + 'description' => 'Location adjacent to an enum value definition.', + ], + 'INPUT_OBJECT' => [ + 'value' => DirectiveLocation::INPUT_OBJECT, + 'description' => 'Location adjacent to an input object type definition.', + ], + 'INPUT_FIELD_DEFINITION' => [ + 'value' => DirectiveLocation::INPUT_FIELD_DEFINITION, + 'description' => 'Location adjacent to an input object field definition.', + ], + + ], + ]); + } + + return self::$map['__DirectiveLocation']; } public static function schemaMetaFieldDef() { - if (!isset(self::$map['__schema'])) { + if (! isset(self::$map['__schema'])) { self::$map['__schema'] = FieldDefinition::create([ - 'name' => '__schema', - 'type' => Type::nonNull(self::_schema()), + 'name' => '__schema', + 'type' => Type::nonNull(self::_schema()), 'description' => 'Access the current type schema of this server.', - 'args' => [], - 'resolve' => function ( + 'args' => [], + 'resolve' => function ( $source, $args, $context, ResolveInfo $info ) { return $info->schema; - } + }, ]); } + return self::$map['__schema']; } public static function typeMetaFieldDef() { - if (!isset(self::$map['__type'])) { + if (! isset(self::$map['__type'])) { self::$map['__type'] = FieldDefinition::create([ - 'name' => '__type', - 'type' => self::_type(), + 'name' => '__type', + 'type' => self::_type(), 'description' => 'Request the type information of a single type.', - 'args' => [ - ['name' => 'name', 'type' => Type::nonNull(Type::string())] + 'args' => [ + ['name' => 'name', 'type' => Type::nonNull(Type::string())], ], - 'resolve' => function ($source, $args, $context, ResolveInfo $info) { + 'resolve' => function ($source, $args, $context, ResolveInfo $info) { return $info->schema->getType($args['name']); - } + }, ]); } + return self::$map['__type']; } public static function typeNameMetaFieldDef() { - if (!isset(self::$map['__typename'])) { + if (! isset(self::$map['__typename'])) { self::$map['__typename'] = FieldDefinition::create([ - 'name' => '__typename', - 'type' => Type::nonNull(Type::string()), + 'name' => '__typename', + 'type' => Type::nonNull(Type::string()), 'description' => 'The name of the current Object type at runtime.', - 'args' => [], - 'resolve' => function ( + 'args' => [], + 'resolve' => function ( $source, $args, $context, ResolveInfo $info ) { return $info->parentType->name; - } + }, ]); } + return self::$map['__typename']; } } diff --git a/src/Type/LazyResolution.php b/src/Type/LazyResolution.php index 6efc06c..08f5028 100644 --- a/src/Type/LazyResolution.php +++ b/src/Type/LazyResolution.php @@ -1,4 +1,7 @@ $objectType[] * - * @var array + * @var Type[][] */ private $loadedPossibleTypes; /** - * LazyResolution constructor. - * @param array $descriptor - * @param callable $typeLoader + * + * @param mixed[] $descriptor */ public function __construct(array $descriptor, callable $typeLoader) { @@ -59,57 +54,63 @@ class LazyResolution implements Resolution $descriptor['version'] === '1.0' ); - $this->typeLoader = $typeLoader; - $this->typeMap = $descriptor['typeMap'] + Type::getInternalTypes(); - $this->possibleTypeMap = $descriptor['possibleTypeMap']; - $this->loadedTypes = Type::getInternalTypes(); + $this->typeLoader = $typeLoader; + $this->typeMap = $descriptor['typeMap'] + Type::getInternalTypes(); + $this->possibleTypeMap = $descriptor['possibleTypeMap']; + $this->loadedTypes = Type::getInternalTypes(); $this->loadedPossibleTypes = []; } - /** - * @inheritdoc - */ - public function resolveType($name) - { - if (!isset($this->typeMap[$name])) { - return null; - } - if (!isset($this->loadedTypes[$name])) { - $type = call_user_func($this->typeLoader, $name); - if (!$type instanceof Type && null !== $type) { - throw new InvariantViolation( - "Lazy Type Resolution Error: Expecting GraphQL Type instance, but got " . - Utils::getVariableType($type) - ); - } - - $this->loadedTypes[$name] = $type; - } - return $this->loadedTypes[$name]; - } - /** * @inheritdoc */ public function resolvePossibleTypes(AbstractType $type) { - if (!isset($this->possibleTypeMap[$type->name])) { + if (! isset($this->possibleTypeMap[$type->name])) { return []; } - if (!isset($this->loadedPossibleTypes[$type->name])) { + if (! isset($this->loadedPossibleTypes[$type->name])) { $tmp = []; foreach ($this->possibleTypeMap[$type->name] as $typeName => $true) { $obj = $this->resolveType($typeName); - if (!$obj instanceof ObjectType) { + if (! $obj instanceof ObjectType) { throw new InvariantViolation( - "Lazy Type Resolution Error: Implementation {$typeName} of interface {$type->name} " . - "is expected to be instance of ObjectType, but got " . Utils::getVariableType($obj) + sprintf( + 'Lazy Type Resolution Error: Implementation %s of interface %s is expected to be instance of ObjectType, but got %s', + $typeName, + $type->name, + Utils::getVariableType($obj) + ) ); } $tmp[] = $obj; } $this->loadedPossibleTypes[$type->name] = $tmp; } + return $this->loadedPossibleTypes[$type->name]; } + + /** + * @inheritdoc + */ + public function resolveType($name) + { + if (! isset($this->typeMap[$name])) { + return null; + } + if (! isset($this->loadedTypes[$name])) { + $type = call_user_func($this->typeLoader, $name); + if (! $type instanceof Type && $type !== null) { + throw new InvariantViolation( + 'Lazy Type Resolution Error: Expecting GraphQL Type instance, but got ' . + Utils::getVariableType($type) + ); + } + + $this->loadedTypes[$name] = $type; + } + + return $this->loadedTypes[$name]; + } } diff --git a/src/Type/Resolution.php b/src/Type/Resolution.php index bd00b8f..177e50e 100644 --- a/src/Type/Resolution.php +++ b/src/Type/Resolution.php @@ -1,4 +1,7 @@ setMutation($MyAppMutationRootType); * * $schema = new GraphQL\Type\Schema($config); - * - * @package GraphQL */ class Schema { - /** - * @var SchemaConfig - */ + /** @var SchemaConfig */ private $config; /** @@ -50,9 +54,7 @@ class Schema */ private $resolvedTypes = []; - /** - * @var array - */ + /** @var Type[][]|null */ private $possibleTypeMap; /** @@ -62,16 +64,12 @@ class Schema */ private $fullyLoaded = false; - /** - * @var InvariantViolation[]|null - */ + /** @var InvariantViolation[]|null */ private $validationErrors; /** - * Schema constructor. - * * @api - * @param array|SchemaConfig $config + * @param mixed[]|SchemaConfig $config */ public function __construct($config) { @@ -89,23 +87,26 @@ class Schema Utils::invariant( $config instanceof SchemaConfig, 'Schema constructor expects instance of GraphQL\Type\SchemaConfig or an array with keys: %s; but got: %s', - implode(', ', [ - 'query', - 'mutation', - 'subscription', - 'types', - 'directives', - 'typeLoader' - ]), + implode( + ', ', + [ + 'query', + 'mutation', + 'subscription', + 'types', + 'directives', + 'typeLoader', + ] + ), Utils::getVariableType($config) ); Utils::invariant( - !$config->types || is_array($config->types) || is_callable($config->types), - "\"types\" must be array or callable if provided but got: " . Utils::getVariableType($config->types) + ! $config->types || is_array($config->types) || is_callable($config->types), + '"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) + ! $config->directives || is_array($config->directives), + '"directives" must be Array if provided but got: ' . Utils::getVariableType($config->directives) ); } @@ -124,8 +125,10 @@ class Schema if (isset($this->resolvedTypes[$type->name])) { Utils::invariant( $type === $this->resolvedTypes[$type->name], - "Schema must contain unique named types but contains multiple types named \"$type\" ". - "(see http://webonyx.github.io/graphql-php/type-system/#type-registry)." + sprintf( + 'Schema must contain unique named types but contains multiple types named "%s" (see http://webonyx.github.io/graphql-php/type-system/#type-registry).', + $type + ) ); } $this->resolvedTypes[$type->name] = $type; @@ -133,10 +136,98 @@ class Schema } $this->resolvedTypes += Type::getInternalTypes() + Introspection::getTypes(); - if (!$this->config->typeLoader) { - // Perform full scan of the schema - $this->getTypeMap(); + if ($this->config->typeLoader) { + return; } + + // Perform full scan of the schema + $this->getTypeMap(); + } + + /** + * @return \Generator + */ + private function resolveAdditionalTypes() + { + $types = $this->config->types ?: []; + + if (is_callable($types)) { + $types = $types(); + } + + if (! is_array($types) && ! $types instanceof \Traversable) { + throw new InvariantViolation(sprintf( + 'Schema types callable must return array or instance of Traversable but got: %s', + Utils::getVariableType($types) + )); + } + + foreach ($types as $index => $type) { + if (! $type instanceof Type) { + throw new InvariantViolation(sprintf( + 'Each entry of schema types must be instance of GraphQL\Type\Definition\Type but entry at %s is %s', + $index, + Utils::printSafe($type) + )); + } + yield $type; + } + } + + /** + * Returns array of all types in this schema. Keys of this array represent type names, values are instances + * of corresponding type definitions + * + * This operation requires full schema scan. Do not use in production environment. + * + * @api + * @return Type[] + */ + public function getTypeMap() + { + if (! $this->fullyLoaded) { + $this->resolvedTypes = $this->collectAllTypes(); + $this->fullyLoaded = true; + } + + return $this->resolvedTypes; + } + + /** + * @return Type[] + */ + private function collectAllTypes() + { + $typeMap = []; + foreach ($this->resolvedTypes as $type) { + $typeMap = TypeInfo::extractTypes($type, $typeMap); + } + foreach ($this->getDirectives() as $directive) { + if (! ($directive instanceof Directive)) { + continue; + } + + $typeMap = TypeInfo::extractTypesFromDirectives($directive, $typeMap); + } + // When types are set as array they are resolved in constructor + if (is_callable($this->config->types)) { + foreach ($this->resolveAdditionalTypes() as $type) { + $typeMap = TypeInfo::extractTypes($type, $typeMap); + } + } + + return $typeMap; + } + + /** + * Returns a list of directives supported by this schema + * + * @api + * @return Directive[] + */ + public function getDirectives() + { + return $this->config->directives ?: GraphQL::getStandardDirectives(); } /** @@ -181,24 +272,6 @@ class Schema return $this->config; } - /** - * Returns array of all types in this schema. Keys of this array represent type names, values are instances - * of corresponding type definitions - * - * This operation requires full schema scan. Do not use in production environment. - * - * @api - * @return Type[] - */ - public function getTypeMap() - { - if (!$this->fullyLoaded) { - $this->resolvedTypes = $this->collectAllTypes(); - $this->fullyLoaded = true; - } - return $this->resolvedTypes; - } - /** * Returns type by it's name * @@ -208,67 +281,59 @@ class Schema */ public function getType($name) { - if (!isset($this->resolvedTypes[$name])) { + if (! isset($this->resolvedTypes[$name])) { $type = $this->loadType($name); - if (!$type) { + if (! $type) { return null; } $this->resolvedTypes[$name] = $type; } + return $this->resolvedTypes[$name]; } /** - * @return array + * @param string $typeName + * @return Type */ - private function collectAllTypes() + private function loadType($typeName) { - $typeMap = []; - foreach ($this->resolvedTypes as $type) { - $typeMap = TypeInfo::extractTypes($type, $typeMap); + $typeLoader = $this->config->typeLoader; + + if (! $typeLoader) { + return $this->defaultTypeLoader($typeName); } - foreach ($this->getDirectives() as $directive) { - if ($directive instanceof Directive) { - $typeMap = TypeInfo::extractTypesFromDirectives($directive, $typeMap); - } + + $type = $typeLoader($typeName); + + if (! $type instanceof Type) { + throw new InvariantViolation( + sprintf( + 'Type loader is expected to return valid type "%s", but it returned %s', + $typeName, + Utils::printSafe($type) + ) + ); } - // When types are set as array they are resolved in constructor - if (is_callable($this->config->types)) { - foreach ($this->resolveAdditionalTypes() as $type) { - $typeMap = TypeInfo::extractTypes($type, $typeMap); - } + if ($type->name !== $typeName) { + throw new InvariantViolation( + sprintf('Type loader is expected to return type "%s", but it returned "%s"', $typeName, $type->name) + ); } - return $typeMap; + + return $type; } /** - * @return \Generator + * @param string $typeName + * @return Type */ - private function resolveAdditionalTypes() + private function defaultTypeLoader($typeName) { - $types = $this->config->types ?: []; + // Default type loader simply fallbacks to collecting all types + $typeMap = $this->getTypeMap(); - if (is_callable($types)) { - $types = $types(); - } - - if (!is_array($types) && !$types instanceof \Traversable) { - throw new InvariantViolation(sprintf( - 'Schema types callable must return array or instance of Traversable but got: %s', - Utils::getVariableType($types) - )); - } - - foreach ($types as $index => $type) { - if (!$type instanceof Type) { - throw new InvariantViolation(sprintf( - 'Each entry of schema types must be instance of GraphQL\Type\Definition\Type but entry at %s is %s', - $index, - Utils::printSafe($type) - )); - } - yield $type; - } + return $typeMap[$typeName] ?? null; } /** @@ -278,17 +343,17 @@ class Schema * This operation requires full schema scan. Do not use in production environment. * * @api - * @param AbstractType $abstractType * @return ObjectType[] */ public function getPossibleTypes(AbstractType $abstractType) { $possibleTypeMap = $this->getPossibleTypeMap(); + return isset($possibleTypeMap[$abstractType->name]) ? array_values($possibleTypeMap[$abstractType->name]) : []; } /** - * @return array + * @return Type[][] */ private function getPossibleTypeMap() { @@ -297,55 +362,28 @@ class Schema foreach ($this->getTypeMap() as $type) { if ($type instanceof ObjectType) { foreach ($type->getInterfaces() as $interface) { - if ($interface instanceof InterfaceType) { - $this->possibleTypeMap[$interface->name][$type->name] = $type; + if (! ($interface instanceof InterfaceType)) { + continue; } + + $this->possibleTypeMap[$interface->name][$type->name] = $type; } - } else if ($type instanceof UnionType) { + } elseif ($type instanceof UnionType) { foreach ($type->getTypes() as $innerType) { $this->possibleTypeMap[$type->name][$innerType->name] = $innerType; } } } } + return $this->possibleTypeMap; } - /** - * @param $typeName - * @return Type - */ - private function loadType($typeName) - { - $typeLoader = $this->config->typeLoader; - - if (!$typeLoader) { - return $this->defaultTypeLoader($typeName); - } - - $type = $typeLoader($typeName); - - if (!$type instanceof Type) { - throw new InvariantViolation( - "Type loader is expected to return valid type \"$typeName\", but it returned " . Utils::printSafe($type) - ); - } - if ($type->name !== $typeName) { - throw new InvariantViolation( - "Type loader is expected to return type \"$typeName\", but it returned \"{$type->name}\"" - ); - } - - return $type; - } - /** * Returns true if object type is concrete type of given abstract type * (implementation for interfaces and members of union type for unions) * * @api - * @param AbstractType $abstractType - * @param ObjectType $possibleType * @return bool */ public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType) @@ -358,22 +396,11 @@ class Schema return $abstractType->isPossibleType($possibleType); } - /** - * Returns a list of directives supported by this schema - * - * @api - * @return Directive[] - */ - public function getDirectives() - { - return $this->config->directives ?: GraphQL::getStandardDirectives(); - } - /** * Returns instance of directive by name * * @api - * @param $name + * @param string $name * @return Directive */ public function getDirective($name) @@ -383,6 +410,7 @@ class Schema return $directive; } } + return null; } @@ -395,14 +423,42 @@ class Schema } /** - * @param $typeName - * @return Type + * Validates schema. + * + * This operation requires full schema scan. Do not use in production environment. + * + * @api + * @throws InvariantViolation */ - private function defaultTypeLoader($typeName) + public function assertValid() { - // Default type loader simply fallbacks to collecting all types - $typeMap = $this->getTypeMap(); - return isset($typeMap[$typeName]) ? $typeMap[$typeName] : null; + $errors = $this->validate(); + + if ($errors) { + throw new InvariantViolation(implode("\n\n", $this->validationErrors)); + } + + $internalTypes = Type::getInternalTypes() + Introspection::getTypes(); + foreach ($this->getTypeMap() as $name => $type) { + if (isset($internalTypes[$name])) { + continue; + } + + $type->assertValid(); + + // Make sure type loader returns the same instance as registered in other places of schema + if (! $this->config->typeLoader) { + continue; + } + + Utils::invariant( + $this->loadType($name) === $type, + sprintf( + 'Type loader returns different instance for %s than field/argument definitions. Make sure you always return the same instance for the same type name.', + $name + ) + ); + } } /** @@ -431,39 +487,4 @@ class Schema return $this->validationErrors; } - - /** - * Validates schema. - * - * This operation requires full schema scan. Do not use in production environment. - * - * @api - * @throws InvariantViolation - */ - public function assertValid() - { - $errors = $this->validate(); - - if ($errors) { - throw new InvariantViolation(implode("\n\n", $this->validationErrors)); - } - - $internalTypes = Type::getInternalTypes() + Introspection::getTypes(); - foreach ($this->getTypeMap() as $name => $type) { - if (isset($internalTypes[$name])) { - continue ; - } - - $type->assertValid(); - - // Make sure type loader returns the same instance as registered in other places of schema - if ($this->config->typeLoader) { - Utils::invariant( - $this->loadType($name) === $type, - "Type loader returns different instance for {$name} than field/argument definitions. ". - 'Make sure you always return the same instance for the same type name.' - ); - } - } - } } diff --git a/src/Type/SchemaConfig.php b/src/Type/SchemaConfig.php index ca8bc7f..3be1c98 100644 --- a/src/Type/SchemaConfig.php +++ b/src/Type/SchemaConfig.php @@ -1,4 +1,7 @@ setQuery($options['query']); } @@ -126,88 +114,12 @@ class SchemaConfig } /** - * @param SchemaDefinitionNode $astNode * @return SchemaConfig */ public function setAstNode(SchemaDefinitionNode $astNode) { $this->astNode = $astNode; - return $this; - } - /** - * @api - * @param ObjectType $query - * @return SchemaConfig - */ - public function setQuery($query) - { - $this->query = $query; - return $this; - } - - /** - * @api - * @param ObjectType $mutation - * @return SchemaConfig - */ - public function setMutation($mutation) - { - $this->mutation = $mutation; - return $this; - } - - /** - * @api - * @param ObjectType $subscription - * @return SchemaConfig - */ - public function setSubscription($subscription) - { - $this->subscription = $subscription; - return $this; - } - - /** - * @api - * @param Type[]|callable $types - * @return SchemaConfig - */ - public function setTypes($types) - { - $this->types = $types; - return $this; - } - - /** - * @api - * @param Directive[] $directives - * @return SchemaConfig - */ - public function setDirectives(array $directives) - { - $this->directives = $directives; - return $this; - } - - /** - * @api - * @param callable $typeLoader - * @return SchemaConfig - */ - public function setTypeLoader(callable $typeLoader) - { - $this->typeLoader = $typeLoader; - return $this; - } - - /** - * @param bool $assumeValid - * @return SchemaConfig - */ - public function setAssumeValid($assumeValid) - { - $this->assumeValid = $assumeValid; return $this; } @@ -220,6 +132,18 @@ class SchemaConfig return $this->query; } + /** + * @api + * @param ObjectType $query + * @return SchemaConfig + */ + public function setQuery($query) + { + $this->query = $query; + + return $this; + } + /** * @api * @return ObjectType @@ -229,6 +153,18 @@ class SchemaConfig return $this->mutation; } + /** + * @api + * @param ObjectType $mutation + * @return SchemaConfig + */ + public function setMutation($mutation) + { + $this->mutation = $mutation; + + return $this; + } + /** * @api * @return ObjectType @@ -238,6 +174,18 @@ class SchemaConfig return $this->subscription; } + /** + * @api + * @param ObjectType $subscription + * @return SchemaConfig + */ + public function setSubscription($subscription) + { + $this->subscription = $subscription; + + return $this; + } + /** * @api * @return Type[] @@ -247,6 +195,18 @@ class SchemaConfig return $this->types ?: []; } + /** + * @api + * @param Type[]|callable $types + * @return SchemaConfig + */ + public function setTypes($types) + { + $this->types = $types; + + return $this; + } + /** * @api * @return Directive[] @@ -256,6 +216,18 @@ class SchemaConfig return $this->directives ?: []; } + /** + * @api + * @param Directive[] $directives + * @return SchemaConfig + */ + public function setDirectives(array $directives) + { + $this->directives = $directives; + + return $this; + } + /** * @api * @return callable @@ -265,6 +237,17 @@ class SchemaConfig return $this->typeLoader; } + /** + * @api + * @return SchemaConfig + */ + public function setTypeLoader(callable $typeLoader) + { + $this->typeLoader = $typeLoader; + + return $this; + } + /** * @return bool */ @@ -272,4 +255,15 @@ class SchemaConfig { return $this->assumeValid; } + + /** + * @param bool $assumeValid + * @return SchemaConfig + */ + public function setAssumeValid($assumeValid) + { + $this->assumeValid = $assumeValid; + + return $this; + } } diff --git a/src/Type/SchemaValidationContext.php b/src/Type/SchemaValidationContext.php index d1d587d..53b7020 100644 --- a/src/Type/SchemaValidationContext.php +++ b/src/Type/SchemaValidationContext.php @@ -1,4 +1,7 @@ schema->getQueryType(); - if (!$queryType) { + if (! $queryType) { $this->reportError( 'Query root type must be provided.', $this->schema->getAstNode() ); - } else if (!$queryType instanceof ObjectType) { + } elseif (! $queryType instanceof ObjectType) { $this->reportError( 'Query root type must be Object type, it cannot be ' . Utils::printSafe($queryType) . '.', $this->getOperationTypeNode($queryType, 'query') @@ -70,7 +77,7 @@ class SchemaValidationContext } $mutationType = $this->schema->getMutationType(); - if ($mutationType && !$mutationType instanceof ObjectType) { + 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') @@ -78,16 +85,36 @@ class SchemaValidationContext } $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') - ); + if (! $subscriptionType || $subscriptionType instanceof ObjectType) { + return; } + + $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 $message + * @param Node[]|Node|TypeNode|TypeDefinitionNode|null $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; + } + + /** + * @param Type $type * @param string $operation * * @return TypeNode|TypeDefinitionNode @@ -100,7 +127,7 @@ class SchemaValidationContext if ($astNode instanceof SchemaDefinitionNode) { $operationTypeNode = null; - foreach($astNode->operationTypes as $operationType) { + foreach ($astNode->operationTypes as $operationType) { if ($operationType->operation === $operation) { $operationTypeNode = $operationType; break; @@ -114,11 +141,11 @@ class SchemaValidationContext public function validateDirectives() { $directives = $this->schema->getDirectives(); - foreach($directives as $directive) { + foreach ($directives as $directive) { // Ensure all directives are in fact GraphQL directives. - if (!$directive instanceof Directive) { + if (! $directive instanceof Directive) { $this->reportError( - "Expected directive but got: " . Utils::printSafe($directive) . '.', + 'Expected directive but got: ' . Utils::printSafe($directive) . '.', is_object($directive) ? $directive->astNode : null ); continue; @@ -138,7 +165,7 @@ class SchemaValidationContext if (isset($argNames[$argName])) { $this->reportError( - "Argument @{$directive->name}({$argName}:) can only be defined once.", + sprintf('Argument @%s(%s:) can only be defined once.', $directive->name, $argName), $this->getAllDirectiveArgNodes($directive, $argName) ); continue; @@ -147,13 +174,19 @@ class SchemaValidationContext $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) - ); + if (Type::isInputType($arg->getType())) { + continue; } + + $this->reportError( + sprintf( + 'The type of @%s(%s:) must be Input Type but got: %s.', + $directive->name, + $argName, + Utils::printSafe($arg->getType()) + ), + $this->getDirectiveArgTypeNode($directive, $argName) + ); } } } @@ -165,19 +198,53 @@ class SchemaValidationContext { // Ensure names are valid, however introspection types opt out. $error = Utils::isValidNameError($node->name, $node->astNode); - if ($error && !Introspection::isIntrospectionType($node)) { - $this->addError($error); + if (! $error || Introspection::isIntrospectionType($node)) { + return; } + + $this->addError($error); + } + + /** + * @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) { + continue; + } + + $argNodes[] = $node; + } + } + + return $argNodes; + } + + /** + * @param string $argName + * @return TypeNode|null + */ + private function getDirectiveArgTypeNode(Directive $directive, $argName) + { + $argNode = $this->getAllDirectiveArgNodes($directive, $argName)[0]; + + return $argNode ? $argNode->type : null; } public function validateTypes() { $typeMap = $this->schema->getTypeMap(); - foreach($typeMap as $typeName => $type) { + foreach ($typeMap as $typeName => $type) { // Ensure all provided types are in fact GraphQL type. - if (!$type instanceof NamedType) { + if (! $type instanceof NamedType) { $this->reportError( - "Expected GraphQL named type but got: " . Utils::printSafe($type) . '.', + 'Expected GraphQL named type but got: ' . Utils::printSafe($type) . '.', is_object($type) ? $type->astNode : null ); continue; @@ -191,16 +258,16 @@ class SchemaValidationContext // Ensure objects implement the interfaces they claim to. $this->validateObjectInterfaces($type); - } else if ($type instanceof InterfaceType) { + } elseif ($type instanceof InterfaceType) { // Ensure fields are valid. $this->validateFields($type); - } else if ($type instanceof UnionType) { + } elseif ($type instanceof UnionType) { // Ensure Unions include valid member types. $this->validateUnionMembers($type); - } else if ($type instanceof EnumType) { + } elseif ($type instanceof EnumType) { // Ensure Enums have valid values. $this->validateEnumValues($type); - } else if ($type instanceof InputObjectType) { + } elseif ($type instanceof InputObjectType) { // Ensure Input Object fields are valid. $this->validateInputFields($type); } @@ -215,9 +282,9 @@ class SchemaValidationContext $fieldMap = $type->getFields(); // Objects and Interfaces both must define one or more fields. - if (!$fieldMap) { + if (! $fieldMap) { $this->reportError( - "Type {$type->name} must define one or more fields.", + sprintf('Type %s must define one or more fields.', $type->name), $this->getAllObjectOrInterfaceNodes($type) ); } @@ -230,24 +297,28 @@ class SchemaValidationContext $fieldNodes = $this->getAllFieldNodes($type, $fieldName); if ($fieldNodes && count($fieldNodes) > 1) { $this->reportError( - "Field {$type->name}.{$fieldName} can only be defined once.", + sprintf('Field %s.%s can only be defined once.', $type->name, $fieldName), $fieldNodes ); continue; } // Ensure the type is an output type - if (!Type::isOutputType($field->getType())) { + if (! Type::isOutputType($field->getType())) { $this->reportError( - "The type of {$type->name}.{$fieldName} must be Output Type " . - 'but got: ' . Utils::printSafe($field->getType()) . '.', + sprintf( + 'The type of %s.%s must be Output Type but got: %s.', + $type->name, + $fieldName, + Utils::printSafe($field->getType()) + ), $this->getFieldTypeNode($type, $fieldName) ); } // Ensure the arguments are valid $argNames = []; - foreach($field->args as $arg) { + foreach ($field->args as $arg) { $argName = $arg->name; // Ensure they are named correctly. @@ -255,260 +326,31 @@ class SchemaValidationContext if (isset($argNames[$argName])) { $this->reportError( - "Field argument {$type->name}.{$fieldName}({$argName}:) can only " . - 'be defined once.', + sprintf( + 'Field argument %s.%s(%s:) can only be defined once.', + $type->name, + $fieldName, + $argName + ), $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 (! $iface instanceof InterfaceType) { - $this->reportError( - sprintf( - 'Type %s must only implement Interface types, ' . - 'it cannot implement %s.', - $object->name, - Utils::printSafe($iface) - ), - $this->getImplementsInterfaceNode($object, $iface) - ); - continue; - } - 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) - { - $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), - ] - ); + if (Type::isInputType($arg->getType())) { 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 + sprintf( + 'The type of %s.%s(%s:) must be Input Type but got: %s.', + $type->name, + $fieldName, + $argName, + Utils::printSafe($arg->getType()) + ), + $this->getFieldArgTypeNode($type, $fieldName, $argName) ); } } @@ -528,33 +370,162 @@ class SchemaValidationContext } /** - * @param ObjectType $type + * @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) { + continue; + } + + foreach ($astNode->fields as $node) { + if ($node->name->value !== $fieldName) { + continue; + } + + $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 + * @return FieldDefinitionNode|null + */ + private function getFieldNode($type, $fieldName) + { + $nodes = $this->getAllFieldNodes($type, $fieldName); + + return $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) { + continue; + } + + $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 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[0] ?? null; + } + + private function validateObjectInterfaces(ObjectType $object) + { + $implementedTypeNames = []; + foreach ($object->getInterfaces() as $iface) { + if (! $iface instanceof InterfaceType) { + $this->reportError( + sprintf( + 'Type %s must only implement Interface types, it cannot implement %s.', + $object->name, + Utils::printSafe($iface) + ), + $this->getImplementsInterfaceNode($object, $iface) + ); + continue; + } + if (isset($implementedTypeNames[$iface->name])) { + $this->reportError( + sprintf('Type %s can only implement %s once.', $object->name, $iface->name), + $this->getAllImplementsInterfaceNodes($object, $iface) + ); + continue; + } + $implementedTypeNames[$iface->name] = true; + $this->validateObjectImplementsInterface($object, $iface); + } + } + + /** * @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; + + return $nodes[0] ?? null; } /** - * @param ObjectType $type * @param InterfaceType $iface * @return NamedTypeNode[] */ private function getAllImplementsInterfaceNodes(ObjectType $type, $iface) { $implementsNodes = []; - $astNodes = $this->getAllObjectOrInterfaceNodes($type); + $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; - } + foreach ($astNodes as $astNode) { + if (! $astNode || ! $astNode->interfaces) { + continue; + } + + foreach ($astNode->interfaces as $node) { + if ($node->name->value !== $iface->name) { + continue; } + + $implementsNodes[] = $node; } } @@ -562,125 +533,187 @@ class SchemaValidationContext } /** - * @param ObjectType|InterfaceType $type - * @param string $fieldName - * @return FieldDefinitionNode|null + * @param InterfaceType $iface */ - private function getFieldNode($type, $fieldName) + private function validateObjectImplementsInterface(ObjectType $object, $iface) { - $nodes = $this->getAllFieldNodes($type, $fieldName); - return $nodes && isset($nodes[0]) ? $nodes[0] : null; - } + $objectFieldMap = $object->getFields(); + $ifaceFieldMap = $iface->getFields(); - /** - * @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; + // 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( + sprintf( + 'Interface field %s.%s expected but %s does not provide it.', + $iface->name, + $fieldName, + $object->name + ), + [$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( + sprintf( + 'Interface field %s.%s expects type %s but %s.%s is type %s.', + $iface->name, + $fieldName, + $ifaceField->getType(), + $object->name, + $fieldName, + 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; } } - } - } - 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; + // Assert interface field arg exists on object field. + if (! $objectArg) { + $this->reportError( + sprintf( + 'Interface field argument %s.%s(%s:) expected but %s.%s does not provide it.', + $iface->name, + $fieldName, + $argName, + $object->name, + $fieldName + ), + [ + $this->getFieldArgNode($iface, $fieldName, $argName), + $this->getFieldNode($object, $fieldName), + ] + ); + continue; } - } - } - 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; + // Assert interface field arg type matches object field arg type. + // (invariant) + // TODO: change to contravariant? + if (! TypeComparators::isEqualType($ifaceArg->getType(), $objectArg->getType())) { + $this->reportError( + sprintf( + 'Interface field argument %s.%s(%s:) expects type %s but %s.%s(%s:) is type %s.', + $iface->name, + $fieldName, + $argName, + Utils::printSafe($ifaceArg->getType()), + $object->name, + $fieldName, + $argName, + 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)) { + continue; + } + + $this->reportError( + sprintf( + 'Object field argument %s.%s(%s:) is of required type %s but is not also provided by the Interface field %s.%s.', + $object->name, + $fieldName, + $argName, + Utils::printSafe($objectArg->getType()), + $iface->name, + $fieldName + ), + [ + $this->getFieldArgTypeNode($object, $fieldName, $argName), + $this->getFieldNode($iface, $fieldName), + ] + ); } } - - return $argNodes; } - /** - * @param Directive $directive - * @param string $argName - * @return TypeNode|null - */ - private function getDirectiveArgTypeNode(Directive $directive, $argName) + private function validateUnionMembers(UnionType $union) { - $argNode = $this->getAllDirectiveArgNodes($directive, $argName)[0]; - return $argNode ? $argNode->type : null; + $memberTypes = $union->getTypes(); + + if (! $memberTypes) { + $this->reportError( + sprintf('Union type %s must define one or more member types.', $union->name), + $union->astNode + ); + } + + $includedTypeNames = []; + + foreach ($memberTypes as $memberType) { + if (isset($includedTypeNames[$memberType->name])) { + $this->reportError( + sprintf('Union type %s can only include type %s once.', $union->name, $memberType->name), + $this->getUnionMemberTypeNodes($union, $memberType->name) + ); + continue; + } + $includedTypeNames[$memberType->name] = true; + if ($memberType instanceof ObjectType) { + continue; + } + + $this->reportError( + sprintf( + 'Union type %s can only include Object types, it cannot include %s.', + $union->name, + Utils::printSafe($memberType) + ), + $this->getUnionMemberTypeNodes($union, Utils::printSafe($memberType)) + ); + } } /** - * @param UnionType $union * @param string $typeName * @return NamedTypeNode[] */ @@ -694,12 +727,48 @@ class SchemaValidationContext } ); } + return $union->astNode ? $union->astNode->types : null; } + private function validateEnumValues(EnumType $enumType) + { + $enumValues = $enumType->getValues(); + + if (! $enumValues) { + $this->reportError( + sprintf('Enum type %s must define one or more values.', $enumType->name), + $enumType->astNode + ); + } + + foreach ($enumValues as $enumValue) { + $valueName = $enumValue->name; + + // Ensure no duplicates + $allNodes = $this->getEnumValueNodes($enumType, $valueName); + if ($allNodes && count($allNodes) > 1) { + $this->reportError( + sprintf('Enum type %s can include value %s only once.', $enumType->name, $valueName), + $allNodes + ); + } + + // Ensure valid name. + $this->validateName($enumValue); + if ($valueName !== 'true' && $valueName !== 'false' && $valueName !== 'null') { + continue; + } + + $this->reportError( + sprintf('Enum type %s cannot include value: %s.', $enumType->name, $valueName), + $enumValue->astNode + ); + } + } + /** - * @param EnumType $enum * @param string $valueName * @return EnumValueDefinitionNode[] */ @@ -713,25 +782,43 @@ class SchemaValidationContext } ); } + return $enum->astNode ? $enum->astNode->values : null; } - /** - * @param string $message - * @param array|Node|TypeNode|TypeDefinitionNode $nodes - */ - private function reportError($message, $nodes = null) + private function validateInputFields(InputObjectType $inputObj) { - $nodes = array_filter($nodes && is_array($nodes) ? $nodes : [$nodes]); - $this->addError(new Error($message, $nodes)); - } + $fieldMap = $inputObj->getFields(); - /** - * @param Error $error - */ - private function addError($error) - { - $this->errors[] = $error; + if (! $fieldMap) { + $this->reportError( + sprintf('Input Object type %s must define one or more fields.', $inputObj->name), + $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())) { + continue; + } + + $this->reportError( + sprintf( + 'The type of %s.%s must be Input Type but got: %s.', + $inputObj->name, + $fieldName, + Utils::printSafe($field->getType()) + ), + $field->astNode ? $field->astNode->type : null + ); + } } } diff --git a/src/Type/TypeKind.php b/src/Type/TypeKind.php new file mode 100644 index 0000000..7cd38ff --- /dev/null +++ b/src/Type/TypeKind.php @@ -0,0 +1,17 @@ +