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: which is equivalent of:
```php ```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([ $episodeEnum = new EnumType([
'name' => 'Episode', 'name' => 'Episode',
'description' => 'One of the films in the Star Wars Trilogy', 'description' => 'One of the films in the Star Wars Trilogy',

View File

@ -14,6 +14,13 @@ final class Warning
static $warned = []; static $warned = [];
static private $warningHandler;
public static function setWarningHandler(callable $warningHandler = null)
{
self::$warningHandler = $warningHandler;
}
static function suppress($suppress = true) static function suppress($suppress = true)
{ {
if (true === $suppress) { if (true === $suppress) {
@ -40,7 +47,10 @@ final class Warning
static function warnOnce($errorMessage, $warningId) 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; self::$warned[$warningId] = true;
trigger_error($errorMessage, E_USER_WARNING); trigger_error($errorMessage, E_USER_WARNING);
} }
@ -48,7 +58,10 @@ final class Warning
static function warn($errorMessage, $warningId) 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); trigger_error($errorMessage, E_USER_WARNING);
} }
} }

View File

@ -1,6 +1,8 @@
<?php <?php
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Utils\Utils;
/** /**
* Class CustomScalarType * Class CustomScalarType
* @package GraphQL\Type\Definition * @package GraphQL\Type\Definition
@ -38,7 +40,11 @@ class CustomScalarType extends ScalarType
*/ */
public function parseValue($value) public function parseValue($value)
{ {
return call_user_func($this->config['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) public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode)
{ {
return call_user_func($this->config['parseLiteral'], $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 <?php
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\EnumValueNode; use GraphQL\Language\AST\EnumValueNode;
use GraphQL\Utils\MixedStore; use GraphQL\Utils\MixedStore;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
@ -26,6 +27,11 @@ class EnumType extends Type implements InputType, OutputType, LeafType
*/ */
private $nameLookup; private $nameLookup;
/**
* @var array
*/
public $config;
public function __construct($config) public function __construct($config)
{ {
if (!isset($config['name'])) { if (!isset($config['name'])) {
@ -47,21 +53,7 @@ class EnumType extends Type implements InputType, OutputType, LeafType
$this->name = $config['name']; $this->name = $config['name'];
$this->description = isset($config['description']) ? $config['description'] : null; $this->description = isset($config['description']) ? $config['description'] : null;
$this->values = []; $this->config = $config;
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]);
}
}
} }
/** /**
@ -69,6 +61,33 @@ class EnumType extends Type implements InputType, OutputType, LeafType
*/ */
public function getValues() 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; return $this->values;
} }
@ -168,4 +187,42 @@ class EnumType extends Type implements InputType, OutputType, LeafType
} }
return $this->nameLookup; 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; public $description;
/**
* @var array
*/
public $config;
public function __construct(array $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 <?php
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils;
/** /**
* Class FieldArgument * Class FieldArgument
@ -105,4 +108,29 @@ class FieldArgument
{ {
return $this->defaultValueExists; 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
]); ]);
} }
/** public static function defineFieldMap(Type $type, $fields)
* @param array|Config $fields
* @param string $parentTypeName
* @return array
*/
public static function createMap(array $fields, $parentTypeName = null)
{ {
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 = []; $map = [];
foreach ($fields as $name => $field) { foreach ($fields as $name => $field) {
if (is_array($field)) { if (is_array($field)) {
if (!isset($field['name']) && is_string($name)) { if (!isset($field['name']) && is_string($name)) {
$field['name'] = $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) { } else if ($field instanceof FieldDefinition) {
$fieldDef = $field; $fieldDef = $field;
} else { } else {
if (is_string($name)) { if (is_string($name)) {
$fieldDef = self::create(['name' => $name, 'type' => $field], $parentTypeName); $fieldDef = self::create(['name' => $name, 'type' => $field]);
} else { } else {
throw new InvariantViolation( 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; 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 * @param $childrenComplexity
* @return mixed * @return mixed

View File

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

View File

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

View File

@ -1,6 +1,7 @@
<?php <?php
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
/** /**
@ -57,10 +58,8 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
public function getFields() public function getFields()
{ {
if (null === $this->fields) { if (null === $this->fields) {
$this->fields = [];
$fields = isset($this->config['fields']) ? $this->config['fields'] : []; $fields = isset($this->config['fields']) ? $this->config['fields'] : [];
$fields = is_callable($fields) ? call_user_func($fields) : $fields; $this->fields = FieldDefinition::defineFieldMap($this, $fields);
$this->fields = FieldDefinition::createMap($fields, $this->name);
} }
return $this->fields; return $this->fields;
} }
@ -95,4 +94,31 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
} }
return null; 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 <?php
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
/** /**
@ -19,11 +20,11 @@ class ListOfType extends Type implements WrappingType, OutputType, InputType
*/ */
public function __construct($type) public function __construct($type)
{ {
Utils::invariant( if (!$type instanceof Type && !is_callable($type)) {
$type instanceof Type || is_callable($type), throw new InvariantViolation(
'Expecting instance of GraphQL\Type\Definition\Type or callable returning instance of that class' 'Can only create List of a GraphQLType but got: ' . Utils::printSafe($type)
); );
}
$this->ofType = $type; $this->ofType = $type;
} }

View File

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

View File

@ -1,6 +1,6 @@
<?php <?php
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
@ -60,7 +60,7 @@ class ObjectType extends Type implements OutputType, CompositeType
/** /**
* @var array * @var array
*/ */
private $interfaceMap = []; private $interfaceMap;
/** /**
* Keeping reference of config for late bindings and custom app-level metadata * 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[] * @return FieldDefinition[]
* @throws InvariantViolation
*/ */
public function getFields() public function getFields()
{ {
if (null === $this->fields) { if (null === $this->fields) {
$fields = isset($this->config['fields']) ? $this->config['fields'] : []; $fields = isset($this->config['fields']) ? $this->config['fields'] : [];
$fields = is_callable($fields) ? call_user_func($fields) : $fields; $this->fields = FieldDefinition::defineFieldMap($this, $fields);
$this->fields = FieldDefinition::createMap($fields, $this->name);
} }
return $this->fields; return $this->fields;
} }
@ -132,9 +132,7 @@ class ObjectType extends Type implements OutputType, CompositeType
if (null === $this->fields) { if (null === $this->fields) {
$this->getFields(); $this->getFields();
} }
if (!isset($this->fields[$name])) { Utils::invariant(isset($this->fields[$name]), 'Field "%s" is not defined for type "%s"', $name, $this->name);
throw new Error(sprintf("Field '%s' is not defined for type '%s'", $name, $this->name));
}
return $this->fields[$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 = isset($this->config['interfaces']) ? $this->config['interfaces'] : [];
$interfaces = is_callable($interfaces) ? call_user_func($interfaces) : $interfaces; $interfaces = is_callable($interfaces) ? call_user_func($interfaces) : $interfaces;
if (!is_array($interfaces)) {
throw new InvariantViolation(
"{$this->name} interfaces must be an Array or a callable which returns an Array."
);
}
$this->interfaces = []; $this->interfaces = [];
foreach ($interfaces as $iface) { foreach ($interfaces as $iface) {
$iface = Type::resolve($iface); $iface = Type::resolve($iface);
if (!$iface instanceof InterfaceType) { 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? // TODO: return interfaceMap vs interfaces. Possibly breaking change?
$this->interfaces[] = $iface; $this->interfaces[] = $iface;
@ -161,6 +169,17 @@ class ObjectType extends Type implements OutputType, CompositeType
return $this->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 * @param InterfaceType $iface
* @return bool * @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; 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; return null;
} }
/**
* @throws InvariantViolation
*/
public function assertValid()
{
}
/** /**
* @return string * @return string
*/ */

View File

@ -1,6 +1,7 @@
<?php <?php
namespace GraphQL\Type\Definition; namespace GraphQL\Type\Definition;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
/** /**
@ -68,17 +69,19 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
public function getTypes() public function getTypes()
{ {
if (null === $this->types) { 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']); $types = call_user_func($this->config['types']);
} else { } else {
$types = $this->config['types']; $types = $this->config['types'];
} }
Utils::invariant( if (!is_array($types)) {
is_array($types), throw new InvariantViolation(
'Option "types" of union "%s" is expected to return array of types (or closure returning array of types)', "{$this->name} types must be an Array or a callable which returns an Array."
$this->name );
); }
$this->types = []; $this->types = [];
foreach ($types as $type) { foreach ($types as $type) {
@ -123,4 +126,48 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
} }
return null; 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]; 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 <?php
namespace GraphQL\Type; namespace GraphQL\Type;
use GraphQL\Error\InvariantViolation;
use GraphQL\Type\Descriptor; use GraphQL\Type\Descriptor;
use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ObjectType;
@ -203,7 +204,7 @@ class SchemaConfig
*/ */
public function getDirectives() 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\Node;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NonNullTypeNode; use GraphQL\Language\AST\NonNullTypeNode;
use GraphQL\Schema; use GraphQL\Type\Schema;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\CompositeType; use GraphQL\Type\Definition\CompositeType;
use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumType;
@ -32,114 +31,27 @@ use GraphQL\Type\Introspection;
class TypeInfo 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) public static function isEqualType(Type $typeA, Type $typeB)
{ {
// Equivalent types are equal. return TypeComparators::isEqualType($typeA, $typeB);
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 * @deprecated moved to GraphQL\Utils\TypeComparators
* equal or a subset of the second super type (covariant).
*/ */
static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType) static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType)
{ {
// Equivalent type is a valid subtype return TypeComparators::isTypeSubTypeOf($schema, $maybeSubType, $superType);
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 * @deprecated moved to GraphQL\Utils\TypeComparators
* 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.
*/ */
static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB) static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB)
{ {
// Equivalent types overlap return TypeComparators::doTypesOverlap($schema, $typeA, $typeB);
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

@ -218,7 +218,7 @@ class Utils
* @param string $message * @param string $message
* @param mixed $sprintfParam1 * @param mixed $sprintfParam1
* @param mixed $sprintfParam2 ... * @param mixed $sprintfParam2 ...
* @throws \Exception * @throws InvariantViolation
*/ */
public static function invariant($test, $message = '') public static function invariant($test, $message = '')
{ {
@ -420,7 +420,7 @@ class Utils
/** /**
* @param $name * @param $name
* @param bool $isIntrospection * @param bool $isIntrospection
* @throws Error * @throws InvariantViolation
*/ */
public static function assertValidName($name, $isIntrospection = false) 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\Language\AST\VariableDefinitionNode;
use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\NonNull;
use GraphQL\Utils\TypeComparators;
use GraphQL\Utils\TypeInfo; use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\ValidationContext; use GraphQL\Validator\ValidationContext;
@ -45,7 +46,7 @@ class VariablesInAllowedPosition
$schema = $context->getSchema(); $schema = $context->getSchema();
$varType = TypeInfo::typeFromAST($schema, $varDef->type); $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( $context->reportError(new Error(
self::badVarPosMessage($varName, $varType, $type), self::badVarPosMessage($varName, $varType, $type),
[$varDef, $node] [$varDef, $node]

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

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([ $otherUserType = new ObjectType([
'name' => 'User', 'name' => 'User',
'fields' => [] 'fields' => ['a' => Type::string()]
]); ]);
$queryType = new ObjectType([ $queryType = new ObjectType([