Schema validation + tests (#148)

This commit is contained in:
Vladimir Razuvaev 2017-08-13 02:50:34 +07:00
parent d3580e959e
commit 34eae0b891
27 changed files with 4489 additions and 1234 deletions

View File

@ -64,19 +64,6 @@ $episodeEnum = new EnumType([
which is equivalent of:
```php
$episodeEnum = new EnumType([
'name' => 'Episode',
'description' => 'One of the films in the Star Wars Trilogy',
'values' => [
'NEWHOPE' => 'NEWHOPE',
'EMPIRE' => 'EMPIRE',
'JEDI' => 'JEDI'
]
]);
```
which is in turn equivalent of:
```php
$episodeEnum = new EnumType([
'name' => 'Episode',
'description' => 'One of the films in the Star Wars Trilogy',

View File

@ -14,6 +14,13 @@ final class Warning
static $warned = [];
static private $warningHandler;
public static function setWarningHandler(callable $warningHandler = null)
{
self::$warningHandler = $warningHandler;
}
static function suppress($suppress = true)
{
if (true === $suppress) {
@ -40,7 +47,10 @@ final class Warning
static function warnOnce($errorMessage, $warningId)
{
if ((self::$enableWarnings & $warningId) > 0 && !isset(self::$warned[$warningId])) {
if (self::$warningHandler) {
$fn = self::$warningHandler;
$fn($errorMessage, $warningId);
} else if ((self::$enableWarnings & $warningId) > 0 && !isset(self::$warned[$warningId])) {
self::$warned[$warningId] = true;
trigger_error($errorMessage, E_USER_WARNING);
}
@ -48,7 +58,10 @@ final class Warning
static function warn($errorMessage, $warningId)
{
if ((self::$enableWarnings & $warningId) > 0) {
if (self::$warningHandler) {
$fn = self::$warningHandler;
$fn($errorMessage, $warningId);
} else if ((self::$enableWarnings & $warningId) > 0) {
trigger_error($errorMessage, E_USER_WARNING);
}
}

View File

@ -1,6 +1,8 @@
<?php
namespace GraphQL\Type\Definition;
use GraphQL\Utils\Utils;
/**
* Class CustomScalarType
* @package GraphQL\Type\Definition
@ -38,7 +40,11 @@ class CustomScalarType extends ScalarType
*/
public function parseValue($value)
{
if (isset($this->config['parseValue'])) {
return call_user_func($this->config['parseValue'], $value);
} else {
return null;
}
}
/**
@ -47,6 +53,29 @@ class CustomScalarType extends ScalarType
*/
public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode)
{
if (isset($this->config['parseLiteral'])) {
return call_user_func($this->config['parseLiteral'], $valueNode);
} else {
return null;
}
}
public function assertValid()
{
parent::assertValid();
Utils::invariant(
isset($this->config['serialize']) && is_callable($this->config['serialize']),
"{$this->name} must provide \"serialize\" function. If this custom Scalar " .
'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."
);
}
}
}

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\EnumValueNode;
use GraphQL\Utils\MixedStore;
use GraphQL\Utils\Utils;
@ -26,6 +27,11 @@ class EnumType extends Type implements InputType, OutputType, LeafType
*/
private $nameLookup;
/**
* @var array
*/
public $config;
public function __construct($config)
{
if (!isset($config['name'])) {
@ -47,21 +53,7 @@ class EnumType extends Type implements InputType, OutputType, LeafType
$this->name = $config['name'];
$this->description = isset($config['description']) ? $config['description'] : null;
$this->values = [];
if (!empty($config['values'])) {
foreach ($config['values'] as $name => $value) {
if (!is_array($value)) {
if (is_string($name)) {
$value = ['name' => $name, 'value' => $value];
} else if (is_int($name) && is_string($value)) {
$value = ['name' => $value, 'value' => $value];
}
}
// value will be equal to name only if 'value' is not set in definition
$this->values[] = new EnumValueDefinition($value + ['name' => $name, 'value' => $name]);
}
}
$this->config = $config;
}
/**
@ -69,6 +61,33 @@ class EnumType extends Type implements InputType, OutputType, LeafType
*/
public function getValues()
{
if ($this->values === null) {
$this->values = [];
$config = $this->config;
if (isset($config['values'])) {
if (!is_array($config['values'])) {
throw new InvariantViolation("{$this->name} values must be an array");
}
foreach ($config['values'] as $name => $value) {
if (is_string($name)) {
if (!is_array($value)) {
throw new InvariantViolation(
"{$this->name}.$name must refer to an associative array with a " .
'"value" key representing an internal value but got: ' . Utils::printSafe($value)
);
}
$value += ['name' => $name, 'value' => $name];
} else if (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.");
}
$this->values[] = new EnumValueDefinition($value);
}
}
}
return $this->values;
}
@ -168,4 +187,42 @@ class EnumType extends Type implements InputType, OutputType, LeafType
}
return $this->nameLookup;
}
/**
* @throws InvariantViolation
*/
public function assertValid()
{
parent::assertValid();
Utils::invariant(
isset($this->config['values']),
"{$this->name} values must be an array."
);
$values = $this->getValues();
Utils::invariant(
!empty($values),
"{$this->name} values must be not empty."
);
foreach ($values as $value) {
try {
Utils::assertValidName($value->name);
} catch (InvariantViolation $e) {
throw new InvariantViolation(
"{$this->name} has value with invalid name: " .
Utils::printSafe($value->name) . " ({$e->getMessage()})"
);
}
Utils::invariant(
!in_array($value->name, ['true', 'false', 'null']),
"{$this->name}: \"{$value->name}\" can not be used as an Enum value."
);
Utils::invariant(
!isset($value->config['isDeprecated']),
"{$this->name}.{$value->name} should provide \"deprecationReason\" instead of \"isDeprecated\"."
);
}
}
}

View File

@ -28,9 +28,19 @@ class EnumValueDefinition
*/
public $description;
/**
* @var array
*/
public $config;
public function __construct(array $config)
{
Utils::assign($this, $config);
$this->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->config = $config;
}
/**

View File

@ -1,6 +1,9 @@
<?php
namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils;
/**
* Class FieldArgument
@ -105,4 +108,29 @@ class FieldArgument
{
return $this->defaultValueExists;
}
public function assertValid(FieldDefinition $parentField, Type $parentType)
{
try {
Utils::assertValidName($this->name);
} catch (InvariantViolation $e) {
throw new InvariantViolation(
"{$parentType->name}.{$parentField->name}({$this->name}:) {$e->getMessage()}")
;
}
$type = $this->type;
if ($type instanceof WrappingType) {
$type = $type->getWrappedType(true);
}
Utils::invariant(
$type instanceof InputType,
"{$parentType->name}.{$parentField->name}({$this->name}): argument type must be " .
"Input Type but got: " . 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)
);
}
}

View File

@ -89,28 +89,72 @@ class FieldDefinition
]);
}
/**
* @param array|Config $fields
* @param string $parentTypeName
* @return array
*/
public static function createMap(array $fields, $parentTypeName = null)
public static function defineFieldMap(Type $type, $fields)
{
if (is_callable($fields)) {
$fields = $fields();
}
if (!is_array($fields)) {
throw new InvariantViolation(
"{$type->name} fields must be an array or a callable which returns such an array."
);
}
$map = [];
foreach ($fields as $name => $field) {
if (is_array($field)) {
if (!isset($field['name']) && is_string($name)) {
$field['name'] = $name;
}
$fieldDef = self::create($field, $parentTypeName);
if (isset($field['args']) && !is_array($field['args'])) {
throw new InvariantViolation(
"{$type->name}.{$name} args must be an array."
);
}
$fieldDef = self::create($field);
} else if ($field instanceof FieldDefinition) {
$fieldDef = $field;
} else {
if (is_string($name) && $field) {
$fieldDef = self::create(['name' => $name, 'type' => $field]);
} else {
throw new InvariantViolation(
"{$type->name}.$name field config must be an array, but got: " . Utils::printSafe($field)
);
}
}
$map[$fieldDef->name] = $fieldDef;
}
return $map;
}
/**
* @param array|Config $fields
* @param string $parentTypeName
* @deprecated
* @return array
*/
public static function createMap(array $fields, $parentTypeName = null)
{
trigger_error(
__METHOD__ . ' is deprecated, use ' . __CLASS__ . '::defineFieldMap() instead',
E_USER_DEPRECATED
);
$map = [];
foreach ($fields as $name => $field) {
if (is_array($field)) {
if (!isset($field['name']) && is_string($name)) {
$field['name'] = $name;
}
$fieldDef = self::create($field);
} else if ($field instanceof FieldDefinition) {
$fieldDef = $field;
} else {
if (is_string($name)) {
$fieldDef = self::create(['name' => $name, 'type' => $field], $parentTypeName);
$fieldDef = self::create(['name' => $name, 'type' => $field]);
} else {
throw new InvariantViolation(
"Unexpected field definition for type $parentTypeName at key $name: " . Utils::printSafe($field)
"Unexpected field definition for type $parentTypeName at field $name: " . Utils::printSafe($field)
);
}
}
@ -195,6 +239,37 @@ class FieldDefinition
return $this->complexityFn;
}
/**
* @param Type $parentType
* @throws InvariantViolation
*/
public function assertValid(Type $parentType)
{
try {
Utils::assertValidName($this->name);
} catch (InvariantViolation $e) {
throw new InvariantViolation("{$parentType->name}.{$this->name}: {$e->getMessage()}");
}
Utils::invariant(
!isset($this->config['isDeprecated']),
"{$parentType->name}.{$this->name} should provide \"deprecationReason\" instead of \"isDeprecated\"."
);
$type = $this->type;
if ($type instanceof WrappingType) {
$type = $type->getWrappedType(true);
}
Utils::invariant(
$type instanceof OutputType,
"{$parentType->name}.{$this->name} field type must be Output Type but got: " . 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)
);
}
/**
* @param $childrenComplexity
* @return mixed

View File

@ -27,6 +27,11 @@ class InputObjectField
*/
public $type;
/**
* @var array
*/
public $config;
/**
* Helps to differentiate when `defaultValue` is `null` and when it was not even set initially
*
@ -52,6 +57,7 @@ class InputObjectField
$this->{$k} = $v;
}
}
$this->config = $opts;
}
/**

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils;
/**
@ -56,6 +57,13 @@ class InputObjectType extends Type implements InputType
$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];
@ -81,4 +89,41 @@ class InputObjectType extends Type implements InputType
Utils::invariant(isset($this->fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name);
return $this->fields[$name];
}
/**
* @throws InvariantViolation
*/
public function assertValid()
{
parent::assertValid();
$fields = $this->getFields();
Utils::invariant(
!empty($fields),
"{$this->name} fields must not be empty"
);
foreach ($fields as $field) {
try {
Utils::assertValidName($field->name);
} catch (InvariantViolation $e) {
throw new InvariantViolation("{$this->name}.{$field->name}: {$e->getMessage()}");
}
$fieldType = $field->type;
if ($fieldType instanceof WrappingType) {
$fieldType = $fieldType->getWrappedType(true);
}
Utils::invariant(
$fieldType instanceof InputType,
"{$this->name}.{$field->name} field type must be Input Type but got: %s.",
Utils::printSafe($field->type)
);
Utils::invariant(
!isset($field->config['resolve']),
"{$this->name}.{$field->name} field type has a resolve property, but Input Types cannot define resolvers."
);
}
}
}

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils;
/**
@ -57,10 +58,8 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
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;
$this->fields = FieldDefinition::createMap($fields, $this->name);
$this->fields = FieldDefinition::defineFieldMap($this, $fields);
}
return $this->fields;
}
@ -95,4 +94,31 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
}
return null;
}
/**
* @throws InvariantViolation
*/
public function assertValid()
{
parent::assertValid();
$fields = $this->getFields();
Utils::invariant(
!isset($this->config['resolveType']) || is_callable($this->config['resolveType']),
"{$this->name} must provide \"resolveType\" as a function."
);
Utils::invariant(
!empty($fields),
"{$this->name} fields must not be empty"
);
foreach ($fields as $field) {
$field->assertValid($this);
foreach ($field->args as $arg) {
$arg->assertValid($field, $this);
}
}
}
}

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils;
/**
@ -19,11 +20,11 @@ class ListOfType extends Type implements WrappingType, OutputType, InputType
*/
public function __construct($type)
{
Utils::invariant(
$type instanceof Type || is_callable($type),
'Expecting instance of GraphQL\Type\Definition\Type or callable returning instance of that class'
if (!$type instanceof Type && !is_callable($type)) {
throw new InvariantViolation(
'Can only create List of a GraphQLType but got: ' . Utils::printSafe($type)
);
}
$this->ofType = $type;
}

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils;
/**
@ -20,10 +21,16 @@ class NonNull extends Type implements WrappingType, OutputType, InputType
*/
public function __construct($type)
{
Utils::invariant(
$type instanceof Type || is_callable($type),
'Expecting instance of GraphQL\Type\Definition\Type or callable returning instance of that class'
if (!$type instanceof Type && !is_callable($type)) {
throw new InvariantViolation(
'Can only create NonNull of a Nullable GraphQLType but got: ' . Utils::printSafe($type)
);
}
if ($type instanceof NonNull) {
throw new InvariantViolation(
'Can only create NonNull of a Nullable GraphQLType but got: ' . Utils::printSafe($type)
);
}
Utils::invariant(
!($type instanceof NonNull),
'Cannot nest NonNull inside NonNull'

View File

@ -1,6 +1,6 @@
<?php
namespace GraphQL\Type\Definition;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils;
@ -60,7 +60,7 @@ class ObjectType extends Type implements OutputType, CompositeType
/**
* @var array
*/
private $interfaceMap = [];
private $interfaceMap;
/**
* Keeping reference of config for late bindings and custom app-level metadata
@ -111,13 +111,13 @@ class ObjectType extends Type implements OutputType, CompositeType
/**
* @return FieldDefinition[]
* @throws InvariantViolation
*/
public function getFields()
{
if (null === $this->fields) {
$fields = isset($this->config['fields']) ? $this->config['fields'] : [];
$fields = is_callable($fields) ? call_user_func($fields) : $fields;
$this->fields = FieldDefinition::createMap($fields, $this->name);
$this->fields = FieldDefinition::defineFieldMap($this, $fields);
}
return $this->fields;
}
@ -132,9 +132,7 @@ class ObjectType extends Type implements OutputType, CompositeType
if (null === $this->fields) {
$this->getFields();
}
if (!isset($this->fields[$name])) {
throw new Error(sprintf("Field '%s' is not defined for type '%s'", $name, $this->name));
}
Utils::invariant(isset($this->fields[$name]), 'Field "%s" is not defined for type "%s"', $name, $this->name);
return $this->fields[$name];
}
@ -147,11 +145,21 @@ class ObjectType extends Type implements OutputType, CompositeType
$interfaces = isset($this->config['interfaces']) ? $this->config['interfaces'] : [];
$interfaces = is_callable($interfaces) ? call_user_func($interfaces) : $interfaces;
if (!is_array($interfaces)) {
throw new InvariantViolation(
"{$this->name} interfaces must be an Array or a callable which returns an Array."
);
}
$this->interfaces = [];
foreach ($interfaces as $iface) {
$iface = Type::resolve($iface);
if (!$iface instanceof InterfaceType) {
throw new InvariantViolation(sprintf('Expecting interface type, got %s', Utils::printSafe($iface)));
throw new InvariantViolation(sprintf(
'%s may only implement Interface types, it cannot implement %s',
$this->name,
Utils::printSafe($iface)
));
}
// TODO: return interfaceMap vs interfaces. Possibly breaking change?
$this->interfaces[] = $iface;
@ -161,6 +169,17 @@ class ObjectType extends Type implements OutputType, CompositeType
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
@ -182,4 +201,61 @@ class ObjectType extends Type implements OutputType, CompositeType
{
return isset($this->config['isTypeOf']) ? call_user_func($this->config['isTypeOf'], $value, $context, $info) : null;
}
/**
* 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.
*
* @throws InvariantViolation
*/
public function assertValid()
{
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)
);
Utils::invariant(
!isset($this->config['isTypeOf']) || is_callable($this->config['isTypeOf']),
"{$this->name} must provide 'isTypeOf' as a function"
);
// getFields() and getInterfaceMap() will do structural validation
$fields = $this->getFields();
Utils::invariant(
!empty($fields),
"{$this->name} fields must not be empty"
);
foreach ($fields as $field) {
$field->assertValid($this);
foreach ($field->args as $arg) {
$arg->assertValid($field, $this);
}
}
$implemented = [];
foreach ($this->getInterfaces() as $iface) {
Utils::invariant(
$iface instanceof InterfaceType,
"{$this->name} may only implement Interface types, it cannot implement: %s.",
Utils::printSafe($iface)
);
Utils::invariant(
!isset($implemented[$iface->name]),
"{$this->name} may declare it implements {$iface->name} only once."
);
$implemented[$iface->name] = true;
if (!isset($iface->config['resolveType'])) {
Utils::invariant(
isset($this->config['isTypeOf']),
"Interface Type {$iface->name} does not provide a \"resolveType\" " .
"function and implementing Type {$this->name} does not provide a " .
'"isTypeOf" function. There is no way to resolve this implementing ' .
'type during execution.'
);
}
}
}
}

View File

@ -237,6 +237,13 @@ abstract class Type implements \JsonSerializable
return null;
}
/**
* @throws InvariantViolation
*/
public function assertValid()
{
}
/**
* @return string
*/

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils;
/**
@ -68,17 +69,19 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
public function getTypes()
{
if (null === $this->types) {
if ($this->config['types'] instanceof \Closure) {
if (!isset($this->config['types'])) {
$types = null;
} else if (is_callable($this->config['types'])) {
$types = call_user_func($this->config['types']);
} else {
$types = $this->config['types'];
}
Utils::invariant(
is_array($types),
'Option "types" of union "%s" is expected to return array of types (or closure returning array of types)',
$this->name
if (!is_array($types)) {
throw new InvariantViolation(
"{$this->name} types must be an Array or a callable which returns an Array."
);
}
$this->types = [];
foreach ($types as $type) {
@ -123,4 +126,48 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
}
return null;
}
/**
* @throws InvariantViolation
*/
public function assertValid()
{
parent::assertValid();
$types = $this->getTypes();
Utils::invariant(
!empty($types),
"{$this->name} types must not be empty"
);
if (isset($this->config['resolveType'])) {
Utils::invariant(
is_callable($this->config['resolveType']),
"{$this->name} must provide \"resolveType\" as a function."
);
}
$includedTypeNames = [];
foreach ($types as $objType) {
Utils::invariant(
$objType instanceof ObjectType,
"{$this->name} may only contain Object types, it cannot contain: %s.",
Utils::printSafe($objType)
);
Utils::invariant(
!isset($includedTypeNames[$objType->name]),
"{$this->name} can include {$objType->name} type only once."
);
$includedTypeNames[$objType->name] = true;
if (!isset($this->config['resolveType'])) {
Utils::invariant(
isset($objType->config['isTypeOf']) && is_callable($objType->config['isTypeOf']),
"Union type \"{$this->name}\" does not provide a \"resolveType\" " .
"function and possible type \"{$objType->name}\" does not provide an " .
'"isTypeOf" function. There is no way to resolve this possible type ' .
'during execution.'
);
}
}
}
}

View File

@ -390,4 +390,111 @@ class Schema
}
return $this->resolvedTypes[$typeName];
}
/**
* @throws InvariantViolation
*/
public function assertValid()
{
foreach ($this->config->getDirectives() as $index => $directive) {
Utils::invariant(
$directive instanceof Directive,
"Each entry of \"directives\" option of Schema config must be an instance of %s but entry at position %d is %s.",
Directive::class,
$index,
Utils::printSafe($directive)
);
}
foreach ($this->getTypeMap() as $name => $type) {
$type->assertValid();
if ($type instanceof AbstractType) {
$possibleTypes = $this->getPossibleTypes($type);
Utils::invariant(
!empty($possibleTypes),
"Could not find possible implementing types for {$type->name} " .
'in schema. Check that schema.types is defined and is an array of ' .
'all possible types in the schema.'
);
} else if ($type instanceof ObjectType) {
foreach ($type->getInterfaces() as $iface) {
$this->assertImplementsIntarface($type, $iface);
}
}
}
}
private function assertImplementsIntarface(ObjectType $object, InterfaceType $iface)
{
$objectFieldMap = $object->getFields();
$ifaceFieldMap = $iface->getFields();
// Assert each interface field is implemented.
foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
// Assert interface field exists on object.
Utils::invariant(
isset($objectFieldMap[$fieldName]),
"{$iface->name} expects field \"{$fieldName}\" but {$object->name} does not provide it"
);
$objectField = $objectFieldMap[$fieldName];
// Assert interface field type is satisfied by object field type, by being
// a valid subtype. (covariant)
Utils::invariant(
TypeComparators::isTypeSubTypeOf($this, $objectField->getType(), $ifaceField->getType()),
"{$iface->name}.{$fieldName} expects type \"{$ifaceField->getType()}\" " .
"but " .
"{$object->name}.${fieldName} provides type \"{$objectField->getType()}\""
);
// Assert each interface field arg is implemented.
foreach ($ifaceField->args as $ifaceArg) {
$argName = $ifaceArg->name;
/** @var FieldArgument $objectArg */
$objectArg = Utils::find($objectField->args, function(FieldArgument $arg) use ($argName) {
return $arg->name === $argName;
});
// Assert interface field arg exists on object field.
Utils::invariant(
$objectArg,
"{$iface->name}.{$fieldName} expects argument \"{$argName}\" but ".
"{$object->name}.{$fieldName} does not provide it."
);
// Assert interface field arg type matches object field arg type.
// (invariant)
Utils::invariant(
TypeComparators::isEqualType($ifaceArg->getType(), $objectArg->getType()),
"{$iface->name}.{$fieldName}({$argName}:) expects type " .
"\"{$ifaceArg->getType()->name}\" but " .
"{$object->name}.{$fieldName}({$argName}:) provides type " .
"\"{$objectArg->getType()->name}\"."
);
// Assert additional arguments must not be required.
foreach ($objectField->args as $objectArg) {
$argName = $objectArg->name;
$ifaceArg = Utils::find($ifaceField->args, function(FieldArgument $arg) use ($argName) {
return $arg->name === $argName;
});
if (!$ifaceArg) {
Utils::invariant(
!($objectArg->getType() instanceof NonNull),
"{$object->name}.{$fieldName}({$argName}:) is of required type " .
"\"{$objectArg->getType()}\" but is not also provided by the " .
"interface {$iface->name}.{$fieldName}."
);
}
}
}
}
}
}

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Type;
use GraphQL\Error\InvariantViolation;
use GraphQL\Type\Descriptor;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\ObjectType;
@ -203,7 +204,7 @@ class SchemaConfig
*/
public function getDirectives()
{
return $this->directives;
return $this->directives ?: [];
}
/**

View File

@ -0,0 +1,137 @@
<?php
namespace GraphQL\Utils;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\CompositeType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
class TypeComparators
{
/**
* Provided two types, return true if the types are equal (invariant).
*
* @param Type $typeA
* @param Type $typeB
* @return bool
*/
public static function isEqualType(Type $typeA, Type $typeB)
{
// Equivalent types are equal.
if ($typeA === $typeB) {
return true;
}
// If either type is non-null, the other must also be non-null.
if ($typeA instanceof NonNull && $typeB instanceof NonNull) {
return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType());
}
// If either type is a list, the other must also be a list.
if ($typeA instanceof ListOfType && $typeB instanceof ListOfType) {
return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType());
}
// Otherwise the types are not equal.
return false;
}
/**
* Provided a type and a super type, return true if the first type is either
* equal or a subset of the second super type (covariant).
*
* @param Schema $schema
* @param Type $maybeSubType
* @param Type $superType
* @return bool
*/
static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType)
{
// Equivalent type is a valid subtype
if ($maybeSubType === $superType) {
return true;
}
// If superType is non-null, maybeSubType must also be nullable.
if ($superType instanceof NonNull) {
if ($maybeSubType instanceof NonNull) {
return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType());
}
return false;
} else if ($maybeSubType instanceof NonNull) {
// If superType is nullable, maybeSubType may be non-null.
return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType);
}
// If superType type is a list, maybeSubType type must also be a list.
if ($superType instanceof ListOfType) {
if ($maybeSubType instanceof ListOfType) {
return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType());
}
return false;
} else if ($maybeSubType instanceof ListOfType) {
// If superType is not a list, maybeSubType must also be not a list.
return false;
}
// If superType type is an abstract type, maybeSubType type may be a currently
// possible object type.
if (Type::isAbstractType($superType) && $maybeSubType instanceof ObjectType && $schema->isPossibleType($superType, $maybeSubType)) {
return true;
}
// Otherwise, the child type is not a valid subtype of the parent type.
return false;
}
/**
* Provided two composite types, determine if they "overlap". Two composite
* types overlap when the Sets of possible concrete types for each intersect.
*
* This is often used to determine if a fragment of a given type could possibly
* be visited in a context of another type.
*
* This function is commutative.
*
* @param Schema $schema
* @param CompositeType $typeA
* @param CompositeType $typeB
* @return bool
*/
static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB)
{
// Equivalent types overlap
if ($typeA === $typeB) {
return true;
}
if ($typeA instanceof AbstractType) {
if ($typeB instanceof AbstractType) {
// If both types are abstract, then determine if there is any intersection
// between possible concrete types of each.
foreach ($schema->getPossibleTypes($typeA) as $type) {
if ($schema->isPossibleType($typeB, $type)) {
return true;
}
}
return false;
}
/** @var $typeB ObjectType */
// Determine if the latter type is a possible concrete type of the former.
return $schema->isPossibleType($typeA, $typeB);
}
if ($typeB instanceof AbstractType) {
/** @var $typeA ObjectType */
// Determine if the former type is a possible concrete type of the latter.
return $schema->isPossibleType($typeB, $typeA);
}
// Otherwise the types do not overlap.
return false;
}
}

View File

@ -7,8 +7,7 @@ use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NonNullTypeNode;
use GraphQL\Schema;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\CompositeType;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
@ -32,114 +31,27 @@ use GraphQL\Type\Introspection;
class TypeInfo
{
/**
* Provided two types, return true if the types are equal (invariant).
* @deprecated moved to GraphQL\Utils\TypeComparators
*/
public static function isEqualType(Type $typeA, Type $typeB)
{
// Equivalent types are equal.
if ($typeA === $typeB) {
return true;
}
// If either type is non-null, the other must also be non-null.
if ($typeA instanceof NonNull && $typeB instanceof NonNull) {
return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType());
}
// If either type is a list, the other must also be a list.
if ($typeA instanceof ListOfType && $typeB instanceof ListOfType) {
return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType());
}
// Otherwise the types are not equal.
return false;
return TypeComparators::isEqualType($typeA, $typeB);
}
/**
* Provided a type and a super type, return true if the first type is either
* equal or a subset of the second super type (covariant).
* @deprecated moved to GraphQL\Utils\TypeComparators
*/
static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType)
{
// Equivalent type is a valid subtype
if ($maybeSubType === $superType) {
return true;
return TypeComparators::isTypeSubTypeOf($schema, $maybeSubType, $superType);
}
// If superType is non-null, maybeSubType must also be nullable.
if ($superType instanceof NonNull) {
if ($maybeSubType instanceof NonNull) {
return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType());
}
return false;
} else if ($maybeSubType instanceof NonNull) {
// If superType is nullable, maybeSubType may be non-null.
return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType);
}
// If superType type is a list, maybeSubType type must also be a list.
if ($superType instanceof ListOfType) {
if ($maybeSubType instanceof ListOfType) {
return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType());
}
return false;
} else if ($maybeSubType instanceof ListOfType) {
// If superType is not a list, maybeSubType must also be not a list.
return false;
}
// If superType type is an abstract type, maybeSubType type may be a currently
// possible object type.
if (Type::isAbstractType($superType) && $maybeSubType instanceof ObjectType && $schema->isPossibleType($superType, $maybeSubType)) {
return true;
}
// Otherwise, the child type is not a valid subtype of the parent type.
return false;
}
/**
* Provided two composite types, determine if they "overlap". Two composite
* types overlap when the Sets of possible concrete types for each intersect.
*
* This is often used to determine if a fragment of a given type could possibly
* be visited in a context of another type.
*
* This function is commutative.
* @deprecated moved to GraphQL\Utils\TypeComparators
*/
static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB)
{
// Equivalent types overlap
if ($typeA === $typeB) {
return true;
}
if ($typeA instanceof AbstractType) {
if ($typeB instanceof AbstractType) {
// If both types are abstract, then determine if there is any intersection
// between possible concrete types of each.
foreach ($schema->getPossibleTypes($typeA) as $type) {
if ($schema->isPossibleType($typeB, $type)) {
return true;
}
}
return false;
}
/** @var $typeB ObjectType */
// Determine if the latter type is a possible concrete type of the former.
return $schema->isPossibleType($typeA, $typeB);
}
if ($typeB instanceof AbstractType) {
/** @var $typeA ObjectType */
// Determine if the former type is a possible concrete type of the latter.
return $schema->isPossibleType($typeB, $typeA);
}
// Otherwise the types do not overlap.
return false;
return TypeComparators::doTypesOverlap($schema, $typeA, $typeB);
}

View File

@ -218,7 +218,7 @@ class Utils
* @param string $message
* @param mixed $sprintfParam1
* @param mixed $sprintfParam2 ...
* @throws \Exception
* @throws InvariantViolation
*/
public static function invariant($test, $message = '')
{
@ -420,7 +420,7 @@ class Utils
/**
* @param $name
* @param bool $isIntrospection
* @throws Error
* @throws InvariantViolation
*/
public static function assertValidName($name, $isIntrospection = false)
{

View File

@ -7,6 +7,7 @@ use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Utils\TypeComparators;
use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\ValidationContext;
@ -45,7 +46,7 @@ class VariablesInAllowedPosition
$schema = $context->getSchema();
$varType = TypeInfo::typeFromAST($schema, $varDef->type);
if ($varType && !TypeInfo::isTypeSubTypeOf($schema, $this->effectiveType($varType, $varDef), $type)) {
if ($varType && !TypeComparators::isTypeSubTypeOf($schema, $this->effectiveType($varType, $varDef), $type)) {
$context->reportError(new Error(
self::badVarPosMessage($varName, $varType, $type),
[$varDef, $node]

View File

@ -62,12 +62,12 @@ class LazyInterfaceTest extends \PHPUnit_Framework_TestCase
if (!$this->lazyInterface) {
$this->lazyInterface = new InterfaceType([
'name' => 'LazyInterface',
'fields' => [
'a' => Type::string()
],
'resolveType' => function() {
return $this->getTestObjectType();
},
'resolve' => function() {
return [];
}
]);
}

View File

@ -47,7 +47,7 @@ class ServerTest extends \PHPUnit_Framework_TestCase
public function testSchemaDefinition()
{
$mutationType = $queryType = $subscriptionType = new ObjectType(['name' => 'A', 'fields' => []]);
$mutationType = $queryType = $subscriptionType = new ObjectType(['name' => 'A', 'fields' => ['a' => Type::string()]]);
$schema = new Schema([
'query' => $queryType
@ -283,7 +283,7 @@ class ServerTest extends \PHPUnit_Framework_TestCase
public function testValidate()
{
$server = Server::create()
->setQueryType(new ObjectType(['name' => 'Q', 'fields' => []]));
->setQueryType(new ObjectType(['name' => 'Q', 'fields' => ['a' => Type::string()]]));
$ast = $server->parse('{q}');
$errors = $server->validate($ast);

View File

@ -243,11 +243,11 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase
$value = $enumTypeWithDeprecatedValue->getValues()[0];
$this->assertEquals([
$this->assertArraySubset([
'name' => 'foo',
'description' => null,
'deprecationReason' => 'Just because',
'value' => 'foo'
'value' => 'foo',
], (array) $value);
$this->assertEquals(true, $value->isDeprecated());
@ -284,8 +284,8 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase
$actual = $EnumTypeWithNullishValue->getValues();
$this->assertEquals(count($expected), count($actual));
$this->assertEquals($expected[0], (array)$actual[0]);
$this->assertEquals($expected[1], (array)$actual[1]);
$this->assertArraySubset($expected[0], (array)$actual[0]);
$this->assertArraySubset($expected[1], (array)$actual[1]);
}
/**

View File

@ -24,7 +24,7 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
$emptySchema = new Schema([
'query' => new ObjectType([
'name' => 'QueryRoot',
'fields' => []
'fields' => ['a' => Type::string()]
])
]);
@ -42,7 +42,6 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
),
'types' =>
array (
0 =>
array (
'kind' => 'OBJECT',
'name' => 'QueryRoot',
@ -52,9 +51,29 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
),
'enumValues' => NULL,
'possibleTypes' => NULL,
'fields' => Array ()
'fields' => array (
array (
'name' => 'a',
'args' => array(),
'type' => array(
'kind' => 'SCALAR',
'name' => 'String',
'ofType' => null
),
'isDeprecated' => false,
'deprecationReason' => null,
)
)
),
array (
'kind' => 'SCALAR',
'name' => 'String',
'fields' => NULL,
'inputFields' => NULL,
'interfaces' => NULL,
'enumValues' => NULL,
'possibleTypes' => NULL,
),
1 =>
array (
'kind' => 'OBJECT',
'name' => '__Schema',
@ -108,7 +127,6 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
'isDeprecated' => false,
'deprecationReason' => NULL,
),
2 =>
array (
'name' => 'mutationType',
'args' =>
@ -122,7 +140,6 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
'isDeprecated' => false,
'deprecationReason' => NULL,
),
3 =>
array (
'name' => 'subscriptionType',
'args' =>
@ -136,7 +153,6 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
'isDeprecated' => false,
'deprecationReason' => NULL,
),
4 =>
array (
'name' => 'directives',
'args' =>
@ -173,7 +189,6 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
'enumValues' => NULL,
'possibleTypes' => NULL,
),
2 =>
array (
'kind' => 'OBJECT',
'name' => '__Type',
@ -388,7 +403,6 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
'enumValues' => NULL,
'possibleTypes' => NULL,
),
3 =>
array (
'kind' => 'ENUM',
'name' => '__TypeKind',
@ -448,17 +462,6 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
),
'possibleTypes' => NULL,
),
4 =>
array (
'kind' => 'SCALAR',
'name' => 'String',
'fields' => NULL,
'inputFields' => NULL,
'interfaces' => NULL,
'enumValues' => NULL,
'possibleTypes' => NULL,
),
5 =>
array (
'kind' => 'SCALAR',
'name' => 'Boolean',
@ -468,7 +471,6 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
'enumValues' => NULL,
'possibleTypes' => NULL,
),
6 =>
array (
'kind' => 'OBJECT',
'name' => '__Field',
@ -596,7 +598,6 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
'enumValues' => NULL,
'possibleTypes' => NULL,
),
7 =>
array (
'kind' => 'OBJECT',
'name' => '__InputValue',
@ -676,7 +677,6 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
'enumValues' => NULL,
'possibleTypes' => NULL,
),
8 =>
array (
'kind' => 'OBJECT',
'name' => '__EnumValue',
@ -756,7 +756,6 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
'enumValues' => NULL,
'possibleTypes' => NULL,
),
9 =>
array (
'kind' => 'OBJECT',
'name' => '__Directive',
@ -919,7 +918,6 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
'enumValues' => NULL,
'possibleTypes' => NULL,
),
10 =>
array (
'kind' => 'ENUM',
'name' => '__DirectiveLocation',
@ -973,7 +971,7 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
),
'possibleTypes' => NULL,
),
11 => array (
array (
'kind' => 'SCALAR',
'name' => 'ID',
'fields' => NULL,
@ -982,7 +980,7 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
'enumValues' => NULL,
'possibleTypes' => NULL,
),
12 => array (
array (
'kind' => 'SCALAR',
'name' => 'Float',
'fields' => NULL,
@ -991,7 +989,7 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
'enumValues' => NULL,
'possibleTypes' => NULL,
),
13 => array (
array (
'kind' => 'SCALAR',
'name' => 'Int',
'fields' => NULL,
@ -1491,7 +1489,7 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
{
$QueryRoot = new ObjectType([
'name' => 'QueryRoot',
'fields' => []
'fields' => ['a' => Type::string()]
]);
$schema = new Schema(['query' => $QueryRoot]);
@ -1551,7 +1549,7 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
{
$QueryRoot = new ObjectType([
'name' => 'QueryRoot',
'fields' => []
'fields' => ['a' => Type::string()]
]);
$schema = new Schema(['query' => $QueryRoot]);

File diff suppressed because it is too large Load Diff

View File

@ -318,7 +318,7 @@ class ExtractTypesTest extends \PHPUnit_Framework_TestCase
{
$otherUserType = new ObjectType([
'name' => 'User',
'fields' => []
'fields' => ['a' => Type::string()]
]);
$queryType = new ObjectType([