Version 4.6.0
This commit is contained in:
1 parent
f79dcf067a
commit
517a5e1f70
2036 files changed
+110041
-26162
No files matched your search
+299
-147
@@ -1,6 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use ArrayAccess;
|
||||
use Exception;
|
||||
use GraphQL\Error\DebugFlag;
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Language\AST\BooleanValueNode;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
@@ -27,12 +34,28 @@ use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\IDType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
use GraphQL\Type\Definition\LeafType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Utils\Utils;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
use Traversable;
|
||||
use function array_combine;
|
||||
use function array_key_exists;
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function floatval;
|
||||
use function intval;
|
||||
use function is_array;
|
||||
use function is_bool;
|
||||
use function is_float;
|
||||
use function is_int;
|
||||
use function is_object;
|
||||
use function is_string;
|
||||
use function iterator_to_array;
|
||||
use function property_exists;
|
||||
|
||||
/**
|
||||
* Various utilities dealing with AST
|
||||
@@ -60,31 +83,30 @@ class AST
|
||||
*
|
||||
* This is a reverse operation for AST::toArray($node)
|
||||
*
|
||||
* @param mixed[] $node
|
||||
*
|
||||
* @api
|
||||
* @param array $node
|
||||
* @return Node
|
||||
*/
|
||||
public static function fromArray(array $node)
|
||||
public static function fromArray(array $node) : Node
|
||||
{
|
||||
if (!isset($node['kind']) || !isset(NodeKind::$classMap[$node['kind']])) {
|
||||
throw new InvariantViolation("Unexpected node structure: " . Utils::printSafeJson($node));
|
||||
if (! isset($node['kind']) || ! isset(NodeKind::$classMap[$node['kind']])) {
|
||||
throw new InvariantViolation('Unexpected node structure: ' . Utils::printSafeJson($node));
|
||||
}
|
||||
|
||||
$kind = isset($node['kind']) ? $node['kind'] : null;
|
||||
$class = NodeKind::$classMap[$kind];
|
||||
$kind = $node['kind'] ?? null;
|
||||
$class = NodeKind::$classMap[$kind];
|
||||
$instance = new $class([]);
|
||||
|
||||
if (isset($node['loc'], $node['loc']['start'], $node['loc']['end'])) {
|
||||
$instance->loc = Location::create($node['loc']['start'], $node['loc']['end']);
|
||||
}
|
||||
|
||||
|
||||
foreach ($node as $key => $value) {
|
||||
if ('loc' === $key || 'kind' === $key) {
|
||||
continue ;
|
||||
if ($key === 'loc' || $key === 'kind') {
|
||||
continue;
|
||||
}
|
||||
if (is_array($value)) {
|
||||
if (isset($value[0]) || empty($value)) {
|
||||
if (isset($value[0]) || count($value) === 0) {
|
||||
$value = new NodeList($value);
|
||||
} else {
|
||||
$value = self::fromArray($value);
|
||||
@@ -92,17 +114,18 @@ class AST
|
||||
}
|
||||
$instance->{$key} = $value;
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert AST node to serializable array
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @api
|
||||
* @param Node $node
|
||||
* @return array
|
||||
*/
|
||||
public static function toArray(Node $node)
|
||||
public static function toArray(Node $node) : array
|
||||
{
|
||||
return $node->toArray(true);
|
||||
}
|
||||
@@ -125,18 +148,20 @@ class AST
|
||||
* | Mixed | Enum Value |
|
||||
* | null | NullValue |
|
||||
*
|
||||
* @param Type|mixed|null $value
|
||||
*
|
||||
* @return ObjectValueNode|ListValueNode|BooleanValueNode|IntValueNode|FloatValueNode|EnumValueNode|StringValueNode|NullValueNode|null
|
||||
*
|
||||
* @api
|
||||
* @param $value
|
||||
* @param InputType $type
|
||||
* @return ObjectValueNode|ListValueNode|BooleanValueNode|IntValueNode|FloatValueNode|EnumValueNode|StringValueNode|NullValueNode
|
||||
*/
|
||||
static function astFromValue($value, InputType $type)
|
||||
public static function astFromValue($value, InputType $type)
|
||||
{
|
||||
if ($type instanceof NonNull) {
|
||||
$astValue = self::astFromValue($value, $type->getWrappedType());
|
||||
if ($astValue instanceof NullValueNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $astValue;
|
||||
}
|
||||
|
||||
@@ -148,108 +173,120 @@ class AST
|
||||
// the value is not an array, convert the value using the list's item type.
|
||||
if ($type instanceof ListOfType) {
|
||||
$itemType = $type->getWrappedType();
|
||||
if (is_array($value) || ($value instanceof \Traversable)) {
|
||||
if (is_array($value) || ($value instanceof Traversable)) {
|
||||
$valuesNodes = [];
|
||||
foreach ($value as $item) {
|
||||
$itemNode = self::astFromValue($item, $itemType);
|
||||
if ($itemNode) {
|
||||
$valuesNodes[] = $itemNode;
|
||||
if (! $itemNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$valuesNodes[] = $itemNode;
|
||||
}
|
||||
return new ListValueNode(['values' => $valuesNodes]);
|
||||
|
||||
return new ListValueNode(['values' => new NodeList($valuesNodes)]);
|
||||
}
|
||||
|
||||
return self::astFromValue($value, $itemType);
|
||||
}
|
||||
|
||||
// Populate the fields of the input object by creating ASTs from each value
|
||||
// in the PHP object according to the fields in the input type.
|
||||
if ($type instanceof InputObjectType) {
|
||||
$isArray = is_array($value);
|
||||
$isArrayLike = $isArray || $value instanceof \ArrayAccess;
|
||||
if ($value === null || (!$isArrayLike && !is_object($value))) {
|
||||
$isArray = is_array($value);
|
||||
$isArrayLike = $isArray || $value instanceof ArrayAccess;
|
||||
if ($value === null || (! $isArrayLike && ! is_object($value))) {
|
||||
return null;
|
||||
}
|
||||
$fields = $type->getFields();
|
||||
$fields = $type->getFields();
|
||||
$fieldNodes = [];
|
||||
foreach ($fields as $fieldName => $field) {
|
||||
if ($isArrayLike) {
|
||||
$fieldValue = isset($value[$fieldName]) ? $value[$fieldName] : null;
|
||||
$fieldValue = $value[$fieldName] ?? null;
|
||||
} else {
|
||||
$fieldValue = isset($value->{$fieldName}) ? $value->{$fieldName} : null;
|
||||
$fieldValue = $value->{$fieldName} ?? null;
|
||||
}
|
||||
|
||||
// Have to check additionally if key exists, since we differentiate between
|
||||
// "no key" and "value is null":
|
||||
if (null !== $fieldValue) {
|
||||
if ($fieldValue !== null) {
|
||||
$fieldExists = true;
|
||||
} else if ($isArray) {
|
||||
} elseif ($isArray) {
|
||||
$fieldExists = array_key_exists($fieldName, $value);
|
||||
} else if ($isArrayLike) {
|
||||
/** @var \ArrayAccess $value */
|
||||
} elseif ($isArrayLike) {
|
||||
$fieldExists = $value->offsetExists($fieldName);
|
||||
} else {
|
||||
$fieldExists = property_exists($value, $fieldName);
|
||||
}
|
||||
|
||||
if ($fieldExists) {
|
||||
$fieldNode = self::astFromValue($fieldValue, $field->getType());
|
||||
|
||||
if ($fieldNode) {
|
||||
$fieldNodes[] = new ObjectFieldNode([
|
||||
'name' => new NameNode(['value' => $fieldName]),
|
||||
'value' => $fieldNode
|
||||
]);
|
||||
}
|
||||
if (! $fieldExists) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return new ObjectValueNode(['fields' => $fieldNodes]);
|
||||
}
|
||||
|
||||
// Since value is an internally represented value, it must be serialized
|
||||
// to an externally represented value before converting into an AST.
|
||||
if ($type instanceof LeafType) {
|
||||
$serialized = $type->serialize($value);
|
||||
} else {
|
||||
throw new InvariantViolation("Must provide Input Type, cannot use: " . Utils::printSafe($type));
|
||||
}
|
||||
$fieldNode = self::astFromValue($fieldValue, $field->getType());
|
||||
|
||||
if (null === $serialized) {
|
||||
return null;
|
||||
}
|
||||
if (! $fieldNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Others serialize based on their corresponding PHP scalar types.
|
||||
if (is_bool($serialized)) {
|
||||
return new BooleanValueNode(['value' => $serialized]);
|
||||
}
|
||||
if (is_int($serialized)) {
|
||||
return new IntValueNode(['value' => $serialized]);
|
||||
}
|
||||
if (is_float($serialized)) {
|
||||
if ((int) $serialized == $serialized) {
|
||||
return new IntValueNode(['value' => $serialized]);
|
||||
}
|
||||
return new FloatValueNode(['value' => $serialized]);
|
||||
}
|
||||
if (is_string($serialized)) {
|
||||
// Enum types use Enum literals.
|
||||
if ($type instanceof EnumType) {
|
||||
return new EnumValueNode(['value' => $serialized]);
|
||||
$fieldNodes[] = new ObjectFieldNode([
|
||||
'name' => new NameNode(['value' => $fieldName]),
|
||||
'value' => $fieldNode,
|
||||
]);
|
||||
}
|
||||
|
||||
// ID types can use Int literals.
|
||||
$asInt = (int) $serialized;
|
||||
if ($type instanceof IDType && (string) $asInt === $serialized) {
|
||||
return new IntValueNode(['value' => $serialized]);
|
||||
}
|
||||
|
||||
// Use json_encode, which uses the same string encoding as GraphQL,
|
||||
// then remove the quotes.
|
||||
return new StringValueNode([
|
||||
'value' => substr(json_encode($serialized), 1, -1)
|
||||
]);
|
||||
return new ObjectValueNode(['fields' => new NodeList($fieldNodes)]);
|
||||
}
|
||||
|
||||
throw new InvariantViolation('Cannot convert value to AST: ' . Utils::printSafe($serialized));
|
||||
if ($type instanceof ScalarType || $type instanceof EnumType) {
|
||||
// Since value is an internally represented value, it must be serialized
|
||||
// to an externally represented value before converting into an AST.
|
||||
try {
|
||||
$serialized = $type->serialize($value);
|
||||
} catch (Throwable $error) {
|
||||
if ($error instanceof Error && $type instanceof EnumType) {
|
||||
return null;
|
||||
}
|
||||
throw $error;
|
||||
}
|
||||
|
||||
// Others serialize based on their corresponding PHP scalar types.
|
||||
if (is_bool($serialized)) {
|
||||
return new BooleanValueNode(['value' => $serialized]);
|
||||
}
|
||||
if (is_int($serialized)) {
|
||||
return new IntValueNode(['value' => (string) $serialized]);
|
||||
}
|
||||
if (is_float($serialized)) {
|
||||
// int cast with == used for performance reasons
|
||||
// phpcs:ignore
|
||||
if ((int) $serialized == $serialized) {
|
||||
return new IntValueNode(['value' => (string) $serialized]);
|
||||
}
|
||||
|
||||
return new FloatValueNode(['value' => (string) $serialized]);
|
||||
}
|
||||
if (is_string($serialized)) {
|
||||
// Enum types use Enum literals.
|
||||
if ($type instanceof EnumType) {
|
||||
return new EnumValueNode(['value' => $serialized]);
|
||||
}
|
||||
|
||||
// ID types can use Int literals.
|
||||
$asInt = (int) $serialized;
|
||||
if ($type instanceof IDType && (string) $asInt === $serialized) {
|
||||
return new IntValueNode(['value' => $serialized]);
|
||||
}
|
||||
|
||||
// Use json_encode, which uses the same string encoding as GraphQL,
|
||||
// then remove the quotes.
|
||||
return new StringValueNode(['value' => $serialized]);
|
||||
}
|
||||
|
||||
throw new InvariantViolation('Cannot convert value to AST: ' . Utils::printSafe($serialized));
|
||||
}
|
||||
|
||||
throw new Error('Unknown type: ' . Utils::printSafe($type) . '.');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,18 +308,20 @@ class AST
|
||||
* | Enum Value | Mixed |
|
||||
* | Null Value | null |
|
||||
*
|
||||
* @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null $valueNode
|
||||
* @param mixed[]|null $variables
|
||||
*
|
||||
* @return mixed[]|stdClass|null
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @api
|
||||
* @param $valueNode
|
||||
* @param InputType $type
|
||||
* @param null $variables
|
||||
* @return array|null|\stdClass
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function valueFromAST($valueNode, InputType $type, $variables = null)
|
||||
public static function valueFromAST(?ValueNode $valueNode, Type $type, ?array $variables = null)
|
||||
{
|
||||
$undefined = Utils::undefined();
|
||||
|
||||
if (!$valueNode) {
|
||||
if ($valueNode === null) {
|
||||
// When there is no AST, then there is also no value.
|
||||
// Importantly, this is different from returning the GraphQL null value.
|
||||
return $undefined;
|
||||
@@ -293,6 +332,7 @@ class AST
|
||||
// Invalid: intentionally return no value.
|
||||
return $undefined;
|
||||
}
|
||||
|
||||
return self::valueFromAST($valueNode, $type->getWrappedType(), $variables);
|
||||
}
|
||||
|
||||
@@ -304,13 +344,19 @@ class AST
|
||||
if ($valueNode instanceof VariableNode) {
|
||||
$variableName = $valueNode->name->value;
|
||||
|
||||
if (!$variables || !array_key_exists($variableName, $variables)) {
|
||||
if (! $variables || ! array_key_exists($variableName, $variables)) {
|
||||
// No valid return value.
|
||||
return $undefined;
|
||||
}
|
||||
// Note: we're not doing any checking that this variable is correct. We're
|
||||
// assuming that this query has been validated and the variable usage here
|
||||
// is of the correct type.
|
||||
|
||||
$variableValue = $variables[$variableName] ?? null;
|
||||
if ($variableValue === null && $type instanceof NonNull) {
|
||||
return $undefined; // Invalid: intentionally return no value.
|
||||
}
|
||||
|
||||
// Note: This does no further checking that this variable is correct.
|
||||
// This assumes that this query has been validated and the variable
|
||||
// usage here is of the correct type.
|
||||
return $variables[$variableName];
|
||||
}
|
||||
|
||||
@@ -319,7 +365,7 @@ class AST
|
||||
|
||||
if ($valueNode instanceof ListValueNode) {
|
||||
$coercedValues = [];
|
||||
$itemNodes = $valueNode->values;
|
||||
$itemNodes = $valueNode->values;
|
||||
foreach ($itemNodes as $itemNode) {
|
||||
if (self::isMissingVariable($itemNode, $variables)) {
|
||||
// If an array contains a missing variable, it is either coerced to
|
||||
@@ -338,6 +384,7 @@ class AST
|
||||
$coercedValues[] = $itemValue;
|
||||
}
|
||||
}
|
||||
|
||||
return $coercedValues;
|
||||
}
|
||||
$coercedValue = self::valueFromAST($valueNode, $itemType, $variables);
|
||||
@@ -345,34 +392,44 @@ class AST
|
||||
// Invalid: intentionally return no value.
|
||||
return $undefined;
|
||||
}
|
||||
|
||||
return [$coercedValue];
|
||||
}
|
||||
|
||||
if ($type instanceof InputObjectType) {
|
||||
if (!$valueNode instanceof ObjectValueNode) {
|
||||
if (! $valueNode instanceof ObjectValueNode) {
|
||||
// Invalid: intentionally return no value.
|
||||
return $undefined;
|
||||
}
|
||||
|
||||
$coercedObj = [];
|
||||
$fields = $type->getFields();
|
||||
$fieldNodes = Utils::keyMap($valueNode->fields, function($field) {return $field->name->value;});
|
||||
$fields = $type->getFields();
|
||||
$fieldNodes = Utils::keyMap(
|
||||
$valueNode->fields,
|
||||
static function ($field) {
|
||||
return $field->name->value;
|
||||
}
|
||||
);
|
||||
foreach ($fields as $field) {
|
||||
/** @var ValueNode $fieldNode */
|
||||
$fieldName = $field->name;
|
||||
$fieldNode = isset($fieldNodes[$fieldName]) ? $fieldNodes[$fieldName] : null;
|
||||
/** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $fieldNode */
|
||||
$fieldNode = $fieldNodes[$fieldName] ?? null;
|
||||
|
||||
if (!$fieldNode || self::isMissingVariable($fieldNode->value, $variables)) {
|
||||
if ($fieldNode === null || self::isMissingVariable($fieldNode->value, $variables)) {
|
||||
if ($field->defaultValueExists()) {
|
||||
$coercedObj[$fieldName] = $field->defaultValue;
|
||||
} else if ($field->getType() instanceof NonNull) {
|
||||
} elseif ($field->getType() instanceof NonNull) {
|
||||
// Invalid: intentionally return no value.
|
||||
return $undefined;
|
||||
}
|
||||
continue ;
|
||||
continue;
|
||||
}
|
||||
|
||||
$fieldValue = self::valueFromAST($fieldNode ? $fieldNode->value : null, $field->getType(), $variables);
|
||||
$fieldValue = self::valueFromAST(
|
||||
$fieldNode !== null ? $fieldNode->value : null,
|
||||
$field->getType(),
|
||||
$variables
|
||||
);
|
||||
|
||||
if ($undefined === $fieldValue) {
|
||||
// Invalid: intentionally return no value.
|
||||
@@ -380,80 +437,175 @@ class AST
|
||||
}
|
||||
$coercedObj[$fieldName] = $fieldValue;
|
||||
}
|
||||
|
||||
return $coercedObj;
|
||||
}
|
||||
|
||||
if ($type instanceof LeafType) {
|
||||
$parsed = $type->parseLiteral($valueNode);
|
||||
|
||||
if (null === $parsed && !$type->isValidLiteral($valueNode)) {
|
||||
// Invalid values represent a failure to parse correctly, in which case
|
||||
// no value is returned.
|
||||
if ($type instanceof EnumType) {
|
||||
if (! $valueNode instanceof EnumValueNode) {
|
||||
return $undefined;
|
||||
}
|
||||
$enumValue = $type->getValue($valueNode->value);
|
||||
if (! $enumValue) {
|
||||
return $undefined;
|
||||
}
|
||||
|
||||
return $parsed;
|
||||
return $enumValue->value;
|
||||
}
|
||||
|
||||
throw new InvariantViolation('Must be input type');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns type definition for given AST Type node
|
||||
*
|
||||
* @api
|
||||
* @param Schema $schema
|
||||
* @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
|
||||
* @return Type
|
||||
* @throws InvariantViolation
|
||||
*/
|
||||
public static function typeFromAST(Schema $schema, $inputTypeNode)
|
||||
{
|
||||
if ($inputTypeNode instanceof ListTypeNode) {
|
||||
$innerType = self::typeFromAST($schema, $inputTypeNode->type);
|
||||
return $innerType ? new ListOfType($innerType) : null;
|
||||
}
|
||||
if ($inputTypeNode instanceof NonNullTypeNode) {
|
||||
$innerType = self::typeFromAST($schema, $inputTypeNode->type);
|
||||
return $innerType ? new NonNull($innerType) : null;
|
||||
if ($type instanceof ScalarType) {
|
||||
// Scalars fulfill parsing a literal value via parseLiteral().
|
||||
// Invalid values represent a failure to parse correctly, in which case
|
||||
// no value is returned.
|
||||
try {
|
||||
return $type->parseLiteral($valueNode, $variables);
|
||||
} catch (Throwable $error) {
|
||||
return $undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Utils::invariant($inputTypeNode && $inputTypeNode instanceof NamedTypeNode, 'Must be a named type');
|
||||
return $schema->getType($inputTypeNode->name->value);
|
||||
throw new Error('Unknown type: ' . Utils::printSafe($type) . '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the provided valueNode is a variable which is not defined
|
||||
* in the set of variables.
|
||||
* @param $valueNode
|
||||
* @param $variables
|
||||
*
|
||||
* @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $valueNode
|
||||
* @param mixed[] $variables
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function isMissingVariable($valueNode, $variables)
|
||||
private static function isMissingVariable(ValueNode $valueNode, $variables)
|
||||
{
|
||||
return $valueNode instanceof VariableNode &&
|
||||
(!$variables || !array_key_exists($valueNode->name->value, $variables));
|
||||
(count($variables) === 0 || ! array_key_exists($valueNode->name->value, $variables));
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a PHP value given a GraphQL Value AST.
|
||||
*
|
||||
* Unlike `valueFromAST()`, no type is provided. The resulting PHP value
|
||||
* will reflect the provided GraphQL value AST.
|
||||
*
|
||||
* | GraphQL Value | PHP Value |
|
||||
* | -------------------- | ------------- |
|
||||
* | Input Object | Assoc Array |
|
||||
* | List | Array |
|
||||
* | Boolean | Boolean |
|
||||
* | String | String |
|
||||
* | Int / Float | Int / Float |
|
||||
* | Enum | Mixed |
|
||||
* | Null | null |
|
||||
*
|
||||
* @param Node $valueNode
|
||||
* @param mixed[]|null $variables
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function valueFromASTUntyped($valueNode, ?array $variables = null)
|
||||
{
|
||||
switch (true) {
|
||||
case $valueNode instanceof NullValueNode:
|
||||
return null;
|
||||
case $valueNode instanceof IntValueNode:
|
||||
return (int) $valueNode->value;
|
||||
case $valueNode instanceof FloatValueNode:
|
||||
return (float) $valueNode->value;
|
||||
case $valueNode instanceof StringValueNode:
|
||||
case $valueNode instanceof EnumValueNode:
|
||||
case $valueNode instanceof BooleanValueNode:
|
||||
return $valueNode->value;
|
||||
case $valueNode instanceof ListValueNode:
|
||||
return array_map(
|
||||
static function ($node) use ($variables) {
|
||||
return self::valueFromASTUntyped($node, $variables);
|
||||
},
|
||||
iterator_to_array($valueNode->values)
|
||||
);
|
||||
case $valueNode instanceof ObjectValueNode:
|
||||
return array_combine(
|
||||
array_map(
|
||||
static function ($field) : string {
|
||||
return $field->name->value;
|
||||
},
|
||||
iterator_to_array($valueNode->fields)
|
||||
),
|
||||
array_map(
|
||||
static function ($field) use ($variables) {
|
||||
return self::valueFromASTUntyped($field->value, $variables);
|
||||
},
|
||||
iterator_to_array($valueNode->fields)
|
||||
)
|
||||
);
|
||||
case $valueNode instanceof VariableNode:
|
||||
$variableName = $valueNode->name->value;
|
||||
|
||||
return $variables && isset($variables[$variableName])
|
||||
? $variables[$variableName]
|
||||
: null;
|
||||
}
|
||||
|
||||
throw new Error('Unexpected value kind: ' . $valueNode->kind . '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns type definition for given AST Type node
|
||||
*
|
||||
* @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
|
||||
*
|
||||
* @return Type|null
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function typeFromAST(Schema $schema, $inputTypeNode)
|
||||
{
|
||||
if ($inputTypeNode instanceof ListTypeNode) {
|
||||
$innerType = self::typeFromAST($schema, $inputTypeNode->type);
|
||||
|
||||
return $innerType ? new ListOfType($innerType) : null;
|
||||
}
|
||||
if ($inputTypeNode instanceof NonNullTypeNode) {
|
||||
$innerType = self::typeFromAST($schema, $inputTypeNode->type);
|
||||
|
||||
return $innerType ? new NonNull($innerType) : null;
|
||||
}
|
||||
if ($inputTypeNode instanceof NamedTypeNode) {
|
||||
return $schema->getType($inputTypeNode->name->value);
|
||||
}
|
||||
|
||||
throw new Error('Unexpected type kind: ' . $inputTypeNode->kind . '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns operation type ("query", "mutation" or "subscription") given a document and operation name
|
||||
*
|
||||
* @api
|
||||
* @param DocumentNode $document
|
||||
* @param string $operationName
|
||||
* @return bool
|
||||
*
|
||||
* @return bool|string
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function getOperation(DocumentNode $document, $operationName = null)
|
||||
{
|
||||
if ($document->definitions) {
|
||||
foreach ($document->definitions as $def) {
|
||||
if ($def instanceof OperationDefinitionNode) {
|
||||
if (!$operationName || (isset($def->name->value) && $def->name->value === $operationName)) {
|
||||
return $def->operation;
|
||||
}
|
||||
if (! ($def instanceof OperationDefinitionNode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $operationName || (isset($def->name->value) && $def->name->value === $operationName)) {
|
||||
return $def->operation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Executor\Values;
|
||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||
use GraphQL\Language\AST\EnumTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\InputValueDefinitionNode;
|
||||
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\ListTypeNode;
|
||||
use GraphQL\Language\AST\NamedTypeNode;
|
||||
use GraphQL\Language\AST\NameNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeList;
|
||||
use GraphQL\Language\AST\NonNullTypeNode;
|
||||
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\TypeDefinitionNode;
|
||||
use GraphQL\Language\AST\TypeNode;
|
||||
use GraphQL\Language\AST\UnionTypeDefinitionNode;
|
||||
use GraphQL\Language\Token;
|
||||
use GraphQL\Type\Definition\CustomScalarType;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use Throwable;
|
||||
use function array_reverse;
|
||||
use function implode;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use function sprintf;
|
||||
|
||||
class ASTDefinitionBuilder
|
||||
{
|
||||
/** @var array<string, Node&TypeDefinitionNode> */
|
||||
private $typeDefinitionsMap;
|
||||
|
||||
/** @var callable */
|
||||
private $typeConfigDecorator;
|
||||
|
||||
/** @var array<string, bool> */
|
||||
private $options;
|
||||
|
||||
/** @var callable */
|
||||
private $resolveType;
|
||||
|
||||
/** @var array<string, Type> */
|
||||
private $cache;
|
||||
|
||||
/**
|
||||
* code sniffer doesn't understand this syntax. Pr with a fix here: waiting on https://github.com/squizlabs/PHP_CodeSniffer/pull/2919
|
||||
* phpcs:disable Squiz.Commenting.FunctionComment.SpacingAfterParamType
|
||||
* @param array<string, Node&TypeDefinitionNode> $typeDefinitionsMap
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
public function __construct(
|
||||
array $typeDefinitionsMap,
|
||||
array $options,
|
||||
callable $resolveType,
|
||||
?callable $typeConfigDecorator = null
|
||||
) {
|
||||
$this->typeDefinitionsMap = $typeDefinitionsMap;
|
||||
$this->typeConfigDecorator = $typeConfigDecorator;
|
||||
$this->options = $options;
|
||||
$this->resolveType = $resolveType;
|
||||
|
||||
$this->cache = Type::getAllBuiltInTypes();
|
||||
}
|
||||
|
||||
public function buildDirective(DirectiveDefinitionNode $directiveNode) : Directive
|
||||
{
|
||||
return new Directive([
|
||||
'name' => $directiveNode->name->value,
|
||||
'description' => $this->getDescription($directiveNode),
|
||||
'args' => FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)),
|
||||
'isRepeatable' => $directiveNode->repeatable,
|
||||
'locations' => Utils::map(
|
||||
$directiveNode->locations,
|
||||
static function (NameNode $node) : string {
|
||||
return $node->value;
|
||||
}
|
||||
),
|
||||
'astNode' => $directiveNode,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an ast node, returns its string description.
|
||||
*/
|
||||
private function getDescription(Node $node) : ?string
|
||||
{
|
||||
if (isset($node->description)) {
|
||||
return $node->description->value;
|
||||
}
|
||||
|
||||
if (isset($this->options['commentDescriptions'])) {
|
||||
$rawValue = $this->getLeadingCommentBlock($node);
|
||||
if ($rawValue !== null) {
|
||||
return BlockString::value("\n" . $rawValue);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getLeadingCommentBlock(Node $node) : ?string
|
||||
{
|
||||
$loc = $node->loc;
|
||||
if ($loc === null || $loc->startToken === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$comments = [];
|
||||
$token = $loc->startToken->prev;
|
||||
while ($token !== null
|
||||
&& $token->kind === Token::COMMENT
|
||||
&& $token->next !== null
|
||||
&& $token->prev !== null
|
||||
&& $token->line + 1 === $token->next->line
|
||||
&& $token->line !== $token->prev->line
|
||||
) {
|
||||
$value = $token->value;
|
||||
$comments[] = $value;
|
||||
$token = $token->prev;
|
||||
}
|
||||
|
||||
return implode("\n", array_reverse($comments));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function makeInputValues(NodeList $values) : array
|
||||
{
|
||||
return Utils::keyValMap(
|
||||
$values,
|
||||
static function (InputValueDefinitionNode $value) : string {
|
||||
return $value->name->value;
|
||||
},
|
||||
function (InputValueDefinitionNode $value) : array {
|
||||
// Note: While this could make assertions to get the correctly typed
|
||||
// value, that would throw immediately while type system validation
|
||||
// with validateSchema() will produce more actionable results.
|
||||
$type = $this->buildWrappedType($value->type);
|
||||
|
||||
$config = [
|
||||
'name' => $value->name->value,
|
||||
'type' => $type,
|
||||
'description' => $this->getDescription($value),
|
||||
'astNode' => $value,
|
||||
];
|
||||
if (isset($value->defaultValue)) {
|
||||
$config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private function buildWrappedType(TypeNode $typeNode) : Type
|
||||
{
|
||||
if ($typeNode instanceof ListTypeNode) {
|
||||
return Type::listOf($this->buildWrappedType($typeNode->type));
|
||||
}
|
||||
|
||||
if ($typeNode instanceof NonNullTypeNode) {
|
||||
return Type::nonNull($this->buildWrappedType($typeNode->type));
|
||||
}
|
||||
|
||||
return $this->buildType($typeNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|(Node &NamedTypeNode)|(Node&TypeDefinitionNode) $ref
|
||||
*/
|
||||
public function buildType($ref) : Type
|
||||
{
|
||||
if (is_string($ref)) {
|
||||
return $this->internalBuildType($ref);
|
||||
}
|
||||
|
||||
return $this->internalBuildType($ref->name->value, $ref);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param (Node &NamedTypeNode)|(Node&TypeDefinitionNode)|null $typeNode
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
private function internalBuildType(string $typeName, ?Node $typeNode = null) : Type
|
||||
{
|
||||
if (! isset($this->cache[$typeName])) {
|
||||
if (isset($this->typeDefinitionsMap[$typeName])) {
|
||||
$type = $this->makeSchemaDef($this->typeDefinitionsMap[$typeName]);
|
||||
if ($this->typeConfigDecorator) {
|
||||
$fn = $this->typeConfigDecorator;
|
||||
try {
|
||||
$config = $fn($type->config, $this->typeDefinitionsMap[$typeName], $this->typeDefinitionsMap);
|
||||
} catch (Throwable $e) {
|
||||
throw new Error(
|
||||
sprintf('Type config decorator passed to %s threw an error ', static::class) .
|
||||
sprintf('when building %s type: %s', $typeName, $e->getMessage()),
|
||||
null,
|
||||
null,
|
||||
[],
|
||||
null,
|
||||
$e
|
||||
);
|
||||
}
|
||||
if (! is_array($config) || isset($config[0])) {
|
||||
throw new Error(
|
||||
sprintf(
|
||||
'Type config decorator passed to %s is expected to return an array, but got %s',
|
||||
static::class,
|
||||
Utils::getVariableType($config)
|
||||
)
|
||||
);
|
||||
}
|
||||
$type = $this->makeSchemaDefFromConfig($this->typeDefinitionsMap[$typeName], $config);
|
||||
}
|
||||
$this->cache[$typeName] = $type;
|
||||
} else {
|
||||
$fn = $this->resolveType;
|
||||
$this->cache[$typeName] = $fn($typeName, $typeNode);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->cache[$typeName];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|EnumTypeDefinitionNode|ScalarTypeDefinitionNode|InputObjectTypeDefinitionNode|UnionTypeDefinitionNode $def
|
||||
*
|
||||
* @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
private function makeSchemaDef(Node $def) : Type
|
||||
{
|
||||
switch (true) {
|
||||
case $def instanceof ObjectTypeDefinitionNode:
|
||||
return $this->makeTypeDef($def);
|
||||
case $def instanceof InterfaceTypeDefinitionNode:
|
||||
return $this->makeInterfaceDef($def);
|
||||
case $def instanceof EnumTypeDefinitionNode:
|
||||
return $this->makeEnumDef($def);
|
||||
case $def instanceof UnionTypeDefinitionNode:
|
||||
return $this->makeUnionDef($def);
|
||||
case $def instanceof ScalarTypeDefinitionNode:
|
||||
return $this->makeScalarDef($def);
|
||||
case $def instanceof InputObjectTypeDefinitionNode:
|
||||
return $this->makeInputObjectDef($def);
|
||||
default:
|
||||
throw new Error(sprintf('Type kind of %s not supported.', $def->kind));
|
||||
}
|
||||
}
|
||||
|
||||
private function makeTypeDef(ObjectTypeDefinitionNode $def) : ObjectType
|
||||
{
|
||||
return new ObjectType([
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'fields' => function () use ($def) : array {
|
||||
return $this->makeFieldDefMap($def);
|
||||
},
|
||||
'interfaces' => function () use ($def) : array {
|
||||
return $this->makeImplementedInterfaces($def);
|
||||
},
|
||||
'astNode' => $def,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode $def
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function makeFieldDefMap(Node $def) : array
|
||||
{
|
||||
return Utils::keyValMap(
|
||||
$def->fields,
|
||||
static function (FieldDefinitionNode $field) : string {
|
||||
return $field->name->value;
|
||||
},
|
||||
function (FieldDefinitionNode $field) : array {
|
||||
return $this->buildField($field);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function buildField(FieldDefinitionNode $field) : array
|
||||
{
|
||||
return [
|
||||
// Note: While this could make assertions to get the correctly typed
|
||||
// value, that would throw immediately while type system validation
|
||||
// with validateSchema() will produce more actionable results.
|
||||
'type' => $this->buildWrappedType($field->type),
|
||||
'description' => $this->getDescription($field),
|
||||
'args' => $this->makeInputValues($field->arguments),
|
||||
'deprecationReason' => $this->getDeprecationReason($field),
|
||||
'astNode' => $field,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a collection of directives, returns the string value for the
|
||||
* deprecation reason.
|
||||
*
|
||||
* @param EnumValueDefinitionNode|FieldDefinitionNode $node
|
||||
*/
|
||||
private function getDeprecationReason(Node $node) : ?string
|
||||
{
|
||||
$deprecated = Values::getDirectiveValues(
|
||||
Directive::deprecatedDirective(),
|
||||
$node
|
||||
);
|
||||
|
||||
return $deprecated['reason'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode $def
|
||||
*
|
||||
* @return array<int, Type>
|
||||
*/
|
||||
private function makeImplementedInterfaces($def) : array
|
||||
{
|
||||
// Note: While this could make early assertions to get the correctly
|
||||
// typed values, that would throw immediately while type system
|
||||
// validation with validateSchema() will produce more actionable results.
|
||||
return Utils::map(
|
||||
$def->interfaces,
|
||||
function (NamedTypeNode $iface) : Type {
|
||||
return $this->buildType($iface);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private function makeInterfaceDef(InterfaceTypeDefinitionNode $def) : InterfaceType
|
||||
{
|
||||
return new InterfaceType([
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'fields' => function () use ($def) : array {
|
||||
return $this->makeFieldDefMap($def);
|
||||
},
|
||||
'interfaces' => function () use ($def) : array {
|
||||
return $this->makeImplementedInterfaces($def);
|
||||
},
|
||||
'astNode' => $def,
|
||||
]);
|
||||
}
|
||||
|
||||
private function makeEnumDef(EnumTypeDefinitionNode $def) : EnumType
|
||||
{
|
||||
return new EnumType([
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'values' => Utils::keyValMap(
|
||||
$def->values,
|
||||
static function ($enumValue) {
|
||||
return $enumValue->name->value;
|
||||
},
|
||||
function ($enumValue) : array {
|
||||
return [
|
||||
'description' => $this->getDescription($enumValue),
|
||||
'deprecationReason' => $this->getDeprecationReason($enumValue),
|
||||
'astNode' => $enumValue,
|
||||
];
|
||||
}
|
||||
),
|
||||
'astNode' => $def,
|
||||
]);
|
||||
}
|
||||
|
||||
private function makeUnionDef(UnionTypeDefinitionNode $def) : UnionType
|
||||
{
|
||||
return new UnionType([
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
// Note: While this could make assertions to get the correctly typed
|
||||
// values below, that would throw immediately while type system
|
||||
// validation with validateSchema() will produce more actionable results.
|
||||
'types' => function () use ($def) : array {
|
||||
return Utils::map(
|
||||
$def->types,
|
||||
function ($typeNode) : Type {
|
||||
return $this->buildType($typeNode);
|
||||
}
|
||||
);
|
||||
},
|
||||
'astNode' => $def,
|
||||
]);
|
||||
}
|
||||
|
||||
private function makeScalarDef(ScalarTypeDefinitionNode $def) : CustomScalarType
|
||||
{
|
||||
return new CustomScalarType([
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'astNode' => $def,
|
||||
'serialize' => static function ($value) {
|
||||
return $value;
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
private function makeInputObjectDef(InputObjectTypeDefinitionNode $def) : InputObjectType
|
||||
{
|
||||
return new InputObjectType([
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'fields' => function () use ($def) : array {
|
||||
return $this->makeInputValues($def->fields);
|
||||
},
|
||||
'astNode' => $def,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
*
|
||||
* @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
private function makeSchemaDefFromConfig(Node $def, array $config) : Type
|
||||
{
|
||||
switch (true) {
|
||||
case $def instanceof ObjectTypeDefinitionNode:
|
||||
return new ObjectType($config);
|
||||
case $def instanceof InterfaceTypeDefinitionNode:
|
||||
return new InterfaceType($config);
|
||||
case $def instanceof EnumTypeDefinitionNode:
|
||||
return new EnumType($config);
|
||||
case $def instanceof UnionTypeDefinitionNode:
|
||||
return new UnionType($config);
|
||||
case $def instanceof ScalarTypeDefinitionNode:
|
||||
return new CustomScalarType($config);
|
||||
case $def instanceof InputObjectTypeDefinitionNode:
|
||||
return new InputObjectType($config);
|
||||
default:
|
||||
throw new Error(sprintf('Type kind of %s not supported.', $def->kind));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function buildInputField(InputValueDefinitionNode $value) : array
|
||||
{
|
||||
$type = $this->buildWrappedType($value->type);
|
||||
|
||||
$config = [
|
||||
'name' => $value->name->value,
|
||||
'type' => $type,
|
||||
'description' => $this->getDescription($value),
|
||||
'astNode' => $value,
|
||||
];
|
||||
|
||||
if ($value->defaultValue !== null) {
|
||||
$config['defaultValue'] = $value->defaultValue;
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function buildEnumValue(EnumValueDefinitionNode $value) : array
|
||||
{
|
||||
return [
|
||||
'description' => $this->getDescription($value),
|
||||
'deprecationReason' => $this->getDeprecationReason($value),
|
||||
'astNode' => $value,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use function array_pop;
|
||||
use function array_shift;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function mb_strlen;
|
||||
use function mb_substr;
|
||||
use function preg_split;
|
||||
use function trim;
|
||||
|
||||
class BlockString
|
||||
{
|
||||
/**
|
||||
* Produces the value of a block string from its parsed raw value, similar to
|
||||
* Coffeescript's block string, Python's docstring trim or Ruby's strip_heredoc.
|
||||
*
|
||||
* This implements the GraphQL spec's BlockStringValue() static algorithm.
|
||||
*/
|
||||
public static function value($rawString)
|
||||
{
|
||||
// Expand a block string's raw value into independent lines.
|
||||
$lines = preg_split("/\\r\\n|[\\n\\r]/", $rawString);
|
||||
|
||||
// Remove common indentation from all lines but first.
|
||||
$commonIndent = null;
|
||||
$linesLength = count($lines);
|
||||
|
||||
for ($i = 1; $i < $linesLength; $i++) {
|
||||
$line = $lines[$i];
|
||||
$indent = self::leadingWhitespace($line);
|
||||
|
||||
if ($indent >= mb_strlen($line) ||
|
||||
($commonIndent !== null && $indent >= $commonIndent)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$commonIndent = $indent;
|
||||
if ($commonIndent === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($commonIndent) {
|
||||
for ($i = 1; $i < $linesLength; $i++) {
|
||||
$line = $lines[$i];
|
||||
$lines[$i] = mb_substr($line, $commonIndent);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove leading and trailing blank lines.
|
||||
while (count($lines) > 0 && trim($lines[0], " \t") === '') {
|
||||
array_shift($lines);
|
||||
}
|
||||
while (count($lines) > 0 && trim($lines[count($lines) - 1], " \t") === '') {
|
||||
array_pop($lines);
|
||||
}
|
||||
|
||||
// Return a string of the lines joined with U+000A.
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
private static function leadingWhitespace($str)
|
||||
{
|
||||
$i = 0;
|
||||
while ($i < mb_strlen($str) && ($str[$i] === ' ' || $str[$i] === '\t')) {
|
||||
$i++;
|
||||
}
|
||||
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,938 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Utility for finding breaking/dangerous changes between two schemas.
|
||||
*/
|
||||
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
use GraphQL\Type\Definition\ImplementingType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NamedType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Schema;
|
||||
use TypeError;
|
||||
use function array_flip;
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function class_alias;
|
||||
use function sprintf;
|
||||
|
||||
class BreakingChangesFinder
|
||||
{
|
||||
public const BREAKING_CHANGE_FIELD_CHANGED_KIND = 'FIELD_CHANGED_KIND';
|
||||
public const BREAKING_CHANGE_FIELD_REMOVED = 'FIELD_REMOVED';
|
||||
public const BREAKING_CHANGE_TYPE_CHANGED_KIND = 'TYPE_CHANGED_KIND';
|
||||
public const BREAKING_CHANGE_TYPE_REMOVED = 'TYPE_REMOVED';
|
||||
public const BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION = 'TYPE_REMOVED_FROM_UNION';
|
||||
public const BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM = 'VALUE_REMOVED_FROM_ENUM';
|
||||
public const BREAKING_CHANGE_ARG_REMOVED = 'ARG_REMOVED';
|
||||
public const BREAKING_CHANGE_ARG_CHANGED_KIND = 'ARG_CHANGED_KIND';
|
||||
public const BREAKING_CHANGE_REQUIRED_ARG_ADDED = 'REQUIRED_ARG_ADDED';
|
||||
public const BREAKING_CHANGE_REQUIRED_INPUT_FIELD_ADDED = 'REQUIRED_INPUT_FIELD_ADDED';
|
||||
public const BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED = 'IMPLEMENTED_INTERFACE_REMOVED';
|
||||
public const BREAKING_CHANGE_DIRECTIVE_REMOVED = 'DIRECTIVE_REMOVED';
|
||||
public const BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED = 'DIRECTIVE_ARG_REMOVED';
|
||||
public const BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED = 'DIRECTIVE_LOCATION_REMOVED';
|
||||
public const BREAKING_CHANGE_REQUIRED_DIRECTIVE_ARG_ADDED = 'REQUIRED_DIRECTIVE_ARG_ADDED';
|
||||
public const DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED = 'ARG_DEFAULT_VALUE_CHANGE';
|
||||
public const DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM = 'VALUE_ADDED_TO_ENUM';
|
||||
public const DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED = 'IMPLEMENTED_INTERFACE_ADDED';
|
||||
public const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION = 'TYPE_ADDED_TO_UNION';
|
||||
public const DANGEROUS_CHANGE_OPTIONAL_INPUT_FIELD_ADDED = 'OPTIONAL_INPUT_FIELD_ADDED';
|
||||
public const DANGEROUS_CHANGE_OPTIONAL_ARG_ADDED = 'OPTIONAL_ARG_ADDED';
|
||||
/** @deprecated use BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED instead, will be removed in v15.0.0. */
|
||||
public const BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT = 'IMPLEMENTED_INTERFACE_REMOVED';
|
||||
/** @deprecated use DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED instead, will be removed in v15.0.0. */
|
||||
public const DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT = 'IMPLEMENTED_INTERFACE_ADDED';
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of all the types
|
||||
* of breaking changes covered by the other functions down below.
|
||||
*
|
||||
* @return string[][]
|
||||
*/
|
||||
public static function findBreakingChanges(Schema $oldSchema, Schema $newSchema)
|
||||
{
|
||||
return array_merge(
|
||||
self::findRemovedTypes($oldSchema, $newSchema),
|
||||
self::findTypesThatChangedKind($oldSchema, $newSchema),
|
||||
self::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema),
|
||||
self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges'],
|
||||
self::findTypesRemovedFromUnions($oldSchema, $newSchema),
|
||||
self::findValuesRemovedFromEnums($oldSchema, $newSchema),
|
||||
self::findArgChanges($oldSchema, $newSchema)['breakingChanges'],
|
||||
self::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema),
|
||||
self::findRemovedDirectives($oldSchema, $newSchema),
|
||||
self::findRemovedDirectiveArgs($oldSchema, $newSchema),
|
||||
self::findAddedNonNullDirectiveArgs($oldSchema, $newSchema),
|
||||
self::findRemovedDirectiveLocations($oldSchema, $newSchema)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of any breaking
|
||||
* changes in the newSchema related to removing an entire type.
|
||||
*
|
||||
* @return string[][]
|
||||
*/
|
||||
public static function findRemovedTypes(
|
||||
Schema $oldSchema,
|
||||
Schema $newSchema
|
||||
) {
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$breakingChanges = [];
|
||||
foreach (array_keys($oldTypeMap) as $typeName) {
|
||||
if (isset($newTypeMap[$typeName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$breakingChanges[] = [
|
||||
'type' => self::BREAKING_CHANGE_TYPE_REMOVED,
|
||||
'description' => "${typeName} was removed.",
|
||||
];
|
||||
}
|
||||
|
||||
return $breakingChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of any breaking
|
||||
* changes in the newSchema related to changing the type of a type.
|
||||
*
|
||||
* @return string[][]
|
||||
*/
|
||||
public static function findTypesThatChangedKind(
|
||||
Schema $schemaA,
|
||||
Schema $schemaB
|
||||
) : iterable {
|
||||
$schemaATypeMap = $schemaA->getTypeMap();
|
||||
$schemaBTypeMap = $schemaB->getTypeMap();
|
||||
|
||||
$breakingChanges = [];
|
||||
foreach ($schemaATypeMap as $typeName => $schemaAType) {
|
||||
if (! isset($schemaBTypeMap[$typeName])) {
|
||||
continue;
|
||||
}
|
||||
$schemaBType = $schemaBTypeMap[$typeName];
|
||||
if ($schemaAType instanceof $schemaBType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($schemaBType instanceof $schemaAType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$schemaATypeKindName = self::typeKindName($schemaAType);
|
||||
$schemaBTypeKindName = self::typeKindName($schemaBType);
|
||||
$breakingChanges[] = [
|
||||
'type' => self::BREAKING_CHANGE_TYPE_CHANGED_KIND,
|
||||
'description' => "${typeName} changed from ${schemaATypeKindName} to ${schemaBTypeKindName}.",
|
||||
];
|
||||
}
|
||||
|
||||
return $breakingChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* @throws TypeError
|
||||
*/
|
||||
private static function typeKindName(Type $type)
|
||||
{
|
||||
if ($type instanceof ScalarType) {
|
||||
return 'a Scalar type';
|
||||
}
|
||||
|
||||
if ($type instanceof ObjectType) {
|
||||
return 'an Object type';
|
||||
}
|
||||
|
||||
if ($type instanceof InterfaceType) {
|
||||
return 'an Interface type';
|
||||
}
|
||||
|
||||
if ($type instanceof UnionType) {
|
||||
return 'a Union type';
|
||||
}
|
||||
|
||||
if ($type instanceof EnumType) {
|
||||
return 'an Enum type';
|
||||
}
|
||||
|
||||
if ($type instanceof InputObjectType) {
|
||||
return 'an Input type';
|
||||
}
|
||||
|
||||
throw new TypeError('unknown type ' . $type->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[][]
|
||||
*/
|
||||
public static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(
|
||||
Schema $oldSchema,
|
||||
Schema $newSchema
|
||||
) {
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$breakingChanges = [];
|
||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||
$newType = $newTypeMap[$typeName] ?? null;
|
||||
if (! ($oldType instanceof ObjectType || $oldType instanceof InterfaceType) ||
|
||||
! ($newType instanceof ObjectType || $newType instanceof InterfaceType) ||
|
||||
! ($newType instanceof $oldType)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldTypeFieldsDef = $oldType->getFields();
|
||||
$newTypeFieldsDef = $newType->getFields();
|
||||
foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) {
|
||||
// Check if the field is missing on the type in the new schema.
|
||||
if (! isset($newTypeFieldsDef[$fieldName])) {
|
||||
$breakingChanges[] = [
|
||||
'type' => self::BREAKING_CHANGE_FIELD_REMOVED,
|
||||
'description' => "${typeName}.${fieldName} was removed.",
|
||||
];
|
||||
} else {
|
||||
$oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
|
||||
$newFieldType = $newTypeFieldsDef[$fieldName]->getType();
|
||||
$isSafe = self::isChangeSafeForObjectOrInterfaceField(
|
||||
$oldFieldType,
|
||||
$newFieldType
|
||||
);
|
||||
if (! $isSafe) {
|
||||
$oldFieldTypeString = $oldFieldType instanceof NamedType && $oldFieldType instanceof Type
|
||||
? $oldFieldType->name
|
||||
: $oldFieldType;
|
||||
$newFieldTypeString = $newFieldType instanceof NamedType && $newFieldType instanceof Type
|
||||
? $newFieldType->name
|
||||
: $newFieldType;
|
||||
$breakingChanges[] = [
|
||||
'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
|
||||
'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}.",
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $breakingChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
private static function isChangeSafeForObjectOrInterfaceField(
|
||||
Type $oldType,
|
||||
Type $newType
|
||||
) {
|
||||
if ($oldType instanceof NamedType) {
|
||||
return // if they're both named types, see if their names are equivalent
|
||||
($newType instanceof NamedType && $oldType->name === $newType->name) ||
|
||||
// moving from nullable to non-null of the same underlying type is safe
|
||||
($newType instanceof NonNull &&
|
||||
self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType())
|
||||
);
|
||||
}
|
||||
|
||||
if ($oldType instanceof ListOfType) {
|
||||
return // if they're both lists, make sure the underlying types are compatible
|
||||
($newType instanceof ListOfType &&
|
||||
self::isChangeSafeForObjectOrInterfaceField(
|
||||
$oldType->getWrappedType(),
|
||||
$newType->getWrappedType()
|
||||
)) ||
|
||||
// moving from nullable to non-null of the same underlying type is safe
|
||||
($newType instanceof NonNull &&
|
||||
self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType()));
|
||||
}
|
||||
|
||||
if ($oldType instanceof NonNull) {
|
||||
// if they're both non-null, make sure the underlying types are compatible
|
||||
return $newType instanceof NonNull &&
|
||||
self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, array<string, string>>>
|
||||
*/
|
||||
public static function findFieldsThatChangedTypeOnInputObjectTypes(
|
||||
Schema $oldSchema,
|
||||
Schema $newSchema
|
||||
) {
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$breakingChanges = [];
|
||||
$dangerousChanges = [];
|
||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||
$newType = $newTypeMap[$typeName] ?? null;
|
||||
if (! ($oldType instanceof InputObjectType) || ! ($newType instanceof InputObjectType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldTypeFieldsDef = $oldType->getFields();
|
||||
$newTypeFieldsDef = $newType->getFields();
|
||||
foreach (array_keys($oldTypeFieldsDef) as $fieldName) {
|
||||
if (! isset($newTypeFieldsDef[$fieldName])) {
|
||||
$breakingChanges[] = [
|
||||
'type' => self::BREAKING_CHANGE_FIELD_REMOVED,
|
||||
'description' => "${typeName}.${fieldName} was removed.",
|
||||
];
|
||||
} else {
|
||||
$oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
|
||||
$newFieldType = $newTypeFieldsDef[$fieldName]->getType();
|
||||
|
||||
$isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg(
|
||||
$oldFieldType,
|
||||
$newFieldType
|
||||
);
|
||||
if (! $isSafe) {
|
||||
if ($oldFieldType instanceof NamedType) {
|
||||
$oldFieldTypeString = $oldFieldType->name;
|
||||
} else {
|
||||
$oldFieldTypeString = $oldFieldType;
|
||||
}
|
||||
if ($newFieldType instanceof NamedType) {
|
||||
$newFieldTypeString = $newFieldType->name;
|
||||
} else {
|
||||
$newFieldTypeString = $newFieldType;
|
||||
}
|
||||
$breakingChanges[] = [
|
||||
'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
|
||||
'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}.",
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if a field was added to the input object type
|
||||
foreach ($newTypeFieldsDef as $fieldName => $fieldDef) {
|
||||
if (isset($oldTypeFieldsDef[$fieldName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$newTypeName = $newType->name;
|
||||
if ($fieldDef->isRequired()) {
|
||||
$breakingChanges[] = [
|
||||
'type' => self::BREAKING_CHANGE_REQUIRED_INPUT_FIELD_ADDED,
|
||||
'description' => "A required field ${fieldName} on input type ${newTypeName} was added.",
|
||||
];
|
||||
} else {
|
||||
$dangerousChanges[] = [
|
||||
'type' => self::DANGEROUS_CHANGE_OPTIONAL_INPUT_FIELD_ADDED,
|
||||
'description' => "An optional field ${fieldName} on input type ${newTypeName} was added.",
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'breakingChanges' => $breakingChanges,
|
||||
'dangerousChanges' => $dangerousChanges,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
private static function isChangeSafeForInputObjectFieldOrFieldArg(
|
||||
Type $oldType,
|
||||
Type $newType
|
||||
) {
|
||||
if ($oldType instanceof NamedType) {
|
||||
if (! $newType instanceof NamedType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if they're both named types, see if their names are equivalent
|
||||
return $oldType->name === $newType->name;
|
||||
}
|
||||
|
||||
if ($oldType instanceof ListOfType) {
|
||||
// if they're both lists, make sure the underlying types are compatible
|
||||
return $newType instanceof ListOfType &&
|
||||
self::isChangeSafeForInputObjectFieldOrFieldArg(
|
||||
$oldType->getWrappedType(),
|
||||
$newType->getWrappedType()
|
||||
);
|
||||
}
|
||||
|
||||
if ($oldType instanceof NonNull) {
|
||||
return // if they're both non-null, make sure the underlying types are
|
||||
// compatible
|
||||
($newType instanceof NonNull &&
|
||||
self::isChangeSafeForInputObjectFieldOrFieldArg(
|
||||
$oldType->getWrappedType(),
|
||||
$newType->getWrappedType()
|
||||
)) ||
|
||||
// moving from non-null to nullable of the same underlying type is safe
|
||||
! ($newType instanceof NonNull) &&
|
||||
self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of any breaking
|
||||
* changes in the newSchema related to removing types from a union type.
|
||||
*
|
||||
* @return string[][]
|
||||
*/
|
||||
public static function findTypesRemovedFromUnions(
|
||||
Schema $oldSchema,
|
||||
Schema $newSchema
|
||||
) {
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$typesRemovedFromUnion = [];
|
||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||
$newType = $newTypeMap[$typeName] ?? null;
|
||||
if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) {
|
||||
continue;
|
||||
}
|
||||
$typeNamesInNewUnion = [];
|
||||
foreach ($newType->getTypes() as $type) {
|
||||
$typeNamesInNewUnion[$type->name] = true;
|
||||
}
|
||||
foreach ($oldType->getTypes() as $type) {
|
||||
if (isset($typeNamesInNewUnion[$type->name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$typesRemovedFromUnion[] = [
|
||||
'type' => self::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION,
|
||||
'description' => sprintf('%s was removed from union type %s.', $type->name, $typeName),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $typesRemovedFromUnion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of any breaking
|
||||
* changes in the newSchema related to removing values from an enum type.
|
||||
*
|
||||
* @return string[][]
|
||||
*/
|
||||
public static function findValuesRemovedFromEnums(
|
||||
Schema $oldSchema,
|
||||
Schema $newSchema
|
||||
) {
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$valuesRemovedFromEnums = [];
|
||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||
$newType = $newTypeMap[$typeName] ?? null;
|
||||
if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) {
|
||||
continue;
|
||||
}
|
||||
$valuesInNewEnum = [];
|
||||
foreach ($newType->getValues() as $value) {
|
||||
$valuesInNewEnum[$value->name] = true;
|
||||
}
|
||||
foreach ($oldType->getValues() as $value) {
|
||||
if (isset($valuesInNewEnum[$value->name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$valuesRemovedFromEnums[] = [
|
||||
'type' => self::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM,
|
||||
'description' => sprintf('%s was removed from enum type %s.', $value->name, $typeName),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $valuesRemovedFromEnums;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of any
|
||||
* breaking or dangerous changes in the newSchema related to arguments
|
||||
* (such as removal or change of type of an argument, or a change in an
|
||||
* argument's default value).
|
||||
*
|
||||
* @return array<string, array<int,array<string, string>>>
|
||||
*/
|
||||
public static function findArgChanges(
|
||||
Schema $oldSchema,
|
||||
Schema $newSchema
|
||||
) {
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$breakingChanges = [];
|
||||
$dangerousChanges = [];
|
||||
|
||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||
$newType = $newTypeMap[$typeName] ?? null;
|
||||
if (! ($oldType instanceof ObjectType || $oldType instanceof InterfaceType) ||
|
||||
! ($newType instanceof ObjectType || $newType instanceof InterfaceType) ||
|
||||
! ($newType instanceof $oldType)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldTypeFields = $oldType->getFields();
|
||||
$newTypeFields = $newType->getFields();
|
||||
|
||||
foreach ($oldTypeFields as $fieldName => $oldField) {
|
||||
if (! isset($newTypeFields[$fieldName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($oldField->args as $oldArgDef) {
|
||||
$newArgs = $newTypeFields[$fieldName]->args;
|
||||
$newArgDef = Utils::find(
|
||||
$newArgs,
|
||||
static function ($arg) use ($oldArgDef) : bool {
|
||||
return $arg->name === $oldArgDef->name;
|
||||
}
|
||||
);
|
||||
if ($newArgDef !== null) {
|
||||
$isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg(
|
||||
$oldArgDef->getType(),
|
||||
$newArgDef->getType()
|
||||
);
|
||||
/** @var ScalarType|EnumType|InputObjectType|ListOfType|NonNull $oldArgType */
|
||||
$oldArgType = $oldArgDef->getType();
|
||||
$oldArgName = $oldArgDef->name;
|
||||
if (! $isSafe) {
|
||||
$newArgType = $newArgDef->getType();
|
||||
$breakingChanges[] = [
|
||||
'type' => self::BREAKING_CHANGE_ARG_CHANGED_KIND,
|
||||
'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed type from ${oldArgType} to ${newArgType}",
|
||||
];
|
||||
} elseif ($oldArgDef->defaultValueExists() && $oldArgDef->defaultValue !== $newArgDef->defaultValue) {
|
||||
$dangerousChanges[] = [
|
||||
'type' => self::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED,
|
||||
'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed defaultValue",
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$breakingChanges[] = [
|
||||
'type' => self::BREAKING_CHANGE_ARG_REMOVED,
|
||||
'description' => sprintf(
|
||||
'%s.%s arg %s was removed',
|
||||
$typeName,
|
||||
$fieldName,
|
||||
$oldArgDef->name
|
||||
),
|
||||
];
|
||||
}
|
||||
// Check if arg was added to the field
|
||||
foreach ($newTypeFields[$fieldName]->args as $newTypeFieldArgDef) {
|
||||
$oldArgs = $oldTypeFields[$fieldName]->args;
|
||||
$oldArgDef = Utils::find(
|
||||
$oldArgs,
|
||||
static function ($arg) use ($newTypeFieldArgDef) : bool {
|
||||
return $arg->name === $newTypeFieldArgDef->name;
|
||||
}
|
||||
);
|
||||
|
||||
if ($oldArgDef !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$newTypeName = $newType->name;
|
||||
$newArgName = $newTypeFieldArgDef->name;
|
||||
if ($newTypeFieldArgDef->isRequired()) {
|
||||
$breakingChanges[] = [
|
||||
'type' => self::BREAKING_CHANGE_REQUIRED_ARG_ADDED,
|
||||
'description' => "A required arg ${newArgName} on ${newTypeName}.${fieldName} was added",
|
||||
];
|
||||
} else {
|
||||
$dangerousChanges[] = [
|
||||
'type' => self::DANGEROUS_CHANGE_OPTIONAL_ARG_ADDED,
|
||||
'description' => "An optional arg ${newArgName} on ${newTypeName}.${fieldName} was added",
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'breakingChanges' => $breakingChanges,
|
||||
'dangerousChanges' => $dangerousChanges,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[][]
|
||||
*/
|
||||
public static function findInterfacesRemovedFromObjectTypes(
|
||||
Schema $oldSchema,
|
||||
Schema $newSchema
|
||||
) {
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
$breakingChanges = [];
|
||||
|
||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||
$newType = $newTypeMap[$typeName] ?? null;
|
||||
if (! ($oldType instanceof ImplementingType) || ! ($newType instanceof ImplementingType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldInterfaces = $oldType->getInterfaces();
|
||||
$newInterfaces = $newType->getInterfaces();
|
||||
foreach ($oldInterfaces as $oldInterface) {
|
||||
$interface = Utils::find(
|
||||
$newInterfaces,
|
||||
static function (InterfaceType $interface) use ($oldInterface) : bool {
|
||||
return $interface->name === $oldInterface->name;
|
||||
}
|
||||
);
|
||||
if ($interface !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$breakingChanges[] = [
|
||||
'type' => self::BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED,
|
||||
'description' => sprintf('%s no longer implements interface %s.', $typeName, $oldInterface->name),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $breakingChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[][]
|
||||
*/
|
||||
public static function findRemovedDirectives(Schema $oldSchema, Schema $newSchema)
|
||||
{
|
||||
$removedDirectives = [];
|
||||
|
||||
$newSchemaDirectiveMap = self::getDirectiveMapForSchema($newSchema);
|
||||
foreach ($oldSchema->getDirectives() as $directive) {
|
||||
if (isset($newSchemaDirectiveMap[$directive->name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$removedDirectives[] = [
|
||||
'type' => self::BREAKING_CHANGE_DIRECTIVE_REMOVED,
|
||||
'description' => sprintf('%s was removed', $directive->name),
|
||||
];
|
||||
}
|
||||
|
||||
return $removedDirectives;
|
||||
}
|
||||
|
||||
private static function getDirectiveMapForSchema(Schema $schema)
|
||||
{
|
||||
return Utils::keyMap(
|
||||
$schema->getDirectives(),
|
||||
static function ($dir) {
|
||||
return $dir->name;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static function findRemovedDirectiveArgs(Schema $oldSchema, Schema $newSchema)
|
||||
{
|
||||
$removedDirectiveArgs = [];
|
||||
$oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
|
||||
|
||||
foreach ($newSchema->getDirectives() as $newDirective) {
|
||||
if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (self::findRemovedArgsForDirectives(
|
||||
$oldSchemaDirectiveMap[$newDirective->name],
|
||||
$newDirective
|
||||
) as $arg) {
|
||||
$removedDirectiveArgs[] = [
|
||||
'type' => self::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED,
|
||||
'description' => sprintf('%s was removed from %s', $arg->name, $newDirective->name),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $removedDirectiveArgs;
|
||||
}
|
||||
|
||||
public static function findRemovedArgsForDirectives(Directive $oldDirective, Directive $newDirective)
|
||||
{
|
||||
$removedArgs = [];
|
||||
$newArgMap = self::getArgumentMapForDirective($newDirective);
|
||||
foreach ($oldDirective->args as $arg) {
|
||||
if (isset($newArgMap[$arg->name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$removedArgs[] = $arg;
|
||||
}
|
||||
|
||||
return $removedArgs;
|
||||
}
|
||||
|
||||
private static function getArgumentMapForDirective(Directive $directive)
|
||||
{
|
||||
return Utils::keyMap(
|
||||
$directive->args ?? [],
|
||||
static function ($arg) {
|
||||
return $arg->name;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static function findAddedNonNullDirectiveArgs(Schema $oldSchema, Schema $newSchema)
|
||||
{
|
||||
$addedNonNullableArgs = [];
|
||||
$oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
|
||||
|
||||
foreach ($newSchema->getDirectives() as $newDirective) {
|
||||
if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (self::findAddedArgsForDirective(
|
||||
$oldSchemaDirectiveMap[$newDirective->name],
|
||||
$newDirective
|
||||
) as $arg) {
|
||||
if (! $arg->isRequired()) {
|
||||
continue;
|
||||
}
|
||||
$addedNonNullableArgs[] = [
|
||||
'type' => self::BREAKING_CHANGE_REQUIRED_DIRECTIVE_ARG_ADDED,
|
||||
'description' => sprintf(
|
||||
'A required arg %s on directive %s was added',
|
||||
$arg->name,
|
||||
$newDirective->name
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $addedNonNullableArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FieldArgument[]
|
||||
*/
|
||||
public static function findAddedArgsForDirective(Directive $oldDirective, Directive $newDirective)
|
||||
{
|
||||
$addedArgs = [];
|
||||
$oldArgMap = self::getArgumentMapForDirective($oldDirective);
|
||||
foreach ($newDirective->args as $arg) {
|
||||
if (isset($oldArgMap[$arg->name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$addedArgs[] = $arg;
|
||||
}
|
||||
|
||||
return $addedArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[][]
|
||||
*/
|
||||
public static function findRemovedDirectiveLocations(Schema $oldSchema, Schema $newSchema)
|
||||
{
|
||||
$removedLocations = [];
|
||||
$oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
|
||||
|
||||
foreach ($newSchema->getDirectives() as $newDirective) {
|
||||
if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (self::findRemovedLocationsForDirective(
|
||||
$oldSchemaDirectiveMap[$newDirective->name],
|
||||
$newDirective
|
||||
) as $location) {
|
||||
$removedLocations[] = [
|
||||
'type' => self::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED,
|
||||
'description' => sprintf('%s was removed from %s', $location, $newDirective->name),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $removedLocations;
|
||||
}
|
||||
|
||||
public static function findRemovedLocationsForDirective(Directive $oldDirective, Directive $newDirective)
|
||||
{
|
||||
$removedLocations = [];
|
||||
$newLocationSet = array_flip($newDirective->locations);
|
||||
foreach ($oldDirective->locations as $oldLocation) {
|
||||
if (array_key_exists($oldLocation, $newLocationSet)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$removedLocations[] = $oldLocation;
|
||||
}
|
||||
|
||||
return $removedLocations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of all the types
|
||||
* of potentially dangerous changes covered by the other functions down below.
|
||||
*
|
||||
* @return string[][]
|
||||
*/
|
||||
public static function findDangerousChanges(Schema $oldSchema, Schema $newSchema)
|
||||
{
|
||||
return array_merge(
|
||||
self::findArgChanges($oldSchema, $newSchema)['dangerousChanges'],
|
||||
self::findValuesAddedToEnums($oldSchema, $newSchema),
|
||||
self::findInterfacesAddedToObjectTypes($oldSchema, $newSchema),
|
||||
self::findTypesAddedToUnions($oldSchema, $newSchema),
|
||||
self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['dangerousChanges']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of any dangerous
|
||||
* changes in the newSchema related to adding values to an enum type.
|
||||
*
|
||||
* @return string[][]
|
||||
*/
|
||||
public static function findValuesAddedToEnums(
|
||||
Schema $oldSchema,
|
||||
Schema $newSchema
|
||||
) {
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$valuesAddedToEnums = [];
|
||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||
$newType = $newTypeMap[$typeName] ?? null;
|
||||
if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) {
|
||||
continue;
|
||||
}
|
||||
$valuesInOldEnum = [];
|
||||
foreach ($oldType->getValues() as $value) {
|
||||
$valuesInOldEnum[$value->name] = true;
|
||||
}
|
||||
foreach ($newType->getValues() as $value) {
|
||||
if (isset($valuesInOldEnum[$value->name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$valuesAddedToEnums[] = [
|
||||
'type' => self::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM,
|
||||
'description' => sprintf('%s was added to enum type %s.', $value->name, $typeName),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $valuesAddedToEnums;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[][]
|
||||
*/
|
||||
public static function findInterfacesAddedToObjectTypes(
|
||||
Schema $oldSchema,
|
||||
Schema $newSchema
|
||||
) {
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
$interfacesAddedToObjectTypes = [];
|
||||
|
||||
foreach ($newTypeMap as $typeName => $newType) {
|
||||
$oldType = $oldTypeMap[$typeName] ?? null;
|
||||
if (! ($oldType instanceof ObjectType || $oldType instanceof InterfaceType)
|
||||
|| ! ($newType instanceof ObjectType || $newType instanceof InterfaceType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldInterfaces = $oldType->getInterfaces();
|
||||
$newInterfaces = $newType->getInterfaces();
|
||||
foreach ($newInterfaces as $newInterface) {
|
||||
$interface = Utils::find(
|
||||
$oldInterfaces,
|
||||
static function (InterfaceType $interface) use ($newInterface) : bool {
|
||||
return $interface->name === $newInterface->name;
|
||||
}
|
||||
);
|
||||
|
||||
if ($interface !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$interfacesAddedToObjectTypes[] = [
|
||||
'type' => self::DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED,
|
||||
'description' => sprintf(
|
||||
'%s added to interfaces implemented by %s.',
|
||||
$newInterface->name,
|
||||
$typeName
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $interfacesAddedToObjectTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of any dangerous
|
||||
* changes in the newSchema related to adding types to a union type.
|
||||
*
|
||||
* @return string[][]
|
||||
*/
|
||||
public static function findTypesAddedToUnions(
|
||||
Schema $oldSchema,
|
||||
Schema $newSchema
|
||||
) {
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$typesAddedToUnion = [];
|
||||
foreach ($newTypeMap as $typeName => $newType) {
|
||||
$oldType = $oldTypeMap[$typeName] ?? null;
|
||||
if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$typeNamesInOldUnion = [];
|
||||
foreach ($oldType->getTypes() as $type) {
|
||||
$typeNamesInOldUnion[$type->name] = true;
|
||||
}
|
||||
foreach ($newType->getTypes() as $type) {
|
||||
if (isset($typeNamesInOldUnion[$type->name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$typesAddedToUnion[] = [
|
||||
'type' => self::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION,
|
||||
'description' => sprintf('%s was added to union type %s.', $type->name, $typeName),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $typesAddedToUnion;
|
||||
}
|
||||
}
|
||||
|
||||
class_alias(BreakingChangesFinder::class, 'GraphQL\Utils\FindBreakingChanges');
|
||||
@@ -0,0 +1,496 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Type\Definition\CustomScalarType;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NamedType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\NullableType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\OutputType;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Introspection;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Type\SchemaConfig;
|
||||
use GraphQL\Type\TypeKind;
|
||||
use function array_key_exists;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function json_encode;
|
||||
|
||||
class BuildClientSchema
|
||||
{
|
||||
/** @var array<string, mixed[]> */
|
||||
private $introspection;
|
||||
|
||||
/** @var array<string, bool> */
|
||||
private $options;
|
||||
|
||||
/** @var array<string, NamedType&Type> */
|
||||
private $typeMap;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed[]> $introspectionQuery
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
public function __construct(array $introspectionQuery, array $options = [])
|
||||
{
|
||||
$this->introspection = $introspectionQuery;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a schema for use by client tools.
|
||||
*
|
||||
* Given the result of a client running the introspection query, creates and
|
||||
* returns a \GraphQL\Type\Schema instance which can be then used with all graphql-php
|
||||
* tools, but cannot be used to execute a query, as introspection does not
|
||||
* represent the "resolver", "parse" or "serialize" functions or any other
|
||||
* server-internal mechanisms.
|
||||
*
|
||||
* This function expects a complete introspection result. Don't forget to check
|
||||
* the "errors" field of a server response before calling this function.
|
||||
*
|
||||
* Accepts options as a third argument:
|
||||
*
|
||||
* - assumeValid:
|
||||
* When building a schema from a GraphQL service's introspection result, it
|
||||
* might be safe to assume the schema is valid. Set to true to assume the
|
||||
* produced schema is valid.
|
||||
*
|
||||
* Default: false
|
||||
*
|
||||
* @param array<string, mixed[]> $introspectionQuery
|
||||
* @param array<string, bool> $options
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function build(array $introspectionQuery, array $options = []) : Schema
|
||||
{
|
||||
$builder = new self($introspectionQuery, $options);
|
||||
|
||||
return $builder->buildSchema();
|
||||
}
|
||||
|
||||
public function buildSchema() : Schema
|
||||
{
|
||||
if (! array_key_exists('__schema', $this->introspection)) {
|
||||
throw new InvariantViolation('Invalid or incomplete introspection result. Ensure that you are passing "data" property of introspection response and no "errors" was returned alongside: ' . json_encode($this->introspection) . '.');
|
||||
}
|
||||
|
||||
$schemaIntrospection = $this->introspection['__schema'];
|
||||
|
||||
$this->typeMap = Utils::keyValMap(
|
||||
$schemaIntrospection['types'],
|
||||
static function (array $typeIntrospection) {
|
||||
return $typeIntrospection['name'];
|
||||
},
|
||||
function (array $typeIntrospection) : NamedType {
|
||||
return $this->buildType($typeIntrospection);
|
||||
}
|
||||
);
|
||||
|
||||
$builtInTypes = array_merge(
|
||||
Type::getStandardTypes(),
|
||||
Introspection::getTypes()
|
||||
);
|
||||
foreach ($builtInTypes as $name => $type) {
|
||||
if (! isset($this->typeMap[$name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->typeMap[$name] = $type;
|
||||
}
|
||||
|
||||
$queryType = isset($schemaIntrospection['queryType'])
|
||||
? $this->getObjectType($schemaIntrospection['queryType'])
|
||||
: null;
|
||||
|
||||
$mutationType = isset($schemaIntrospection['mutationType'])
|
||||
? $this->getObjectType($schemaIntrospection['mutationType'])
|
||||
: null;
|
||||
|
||||
$subscriptionType = isset($schemaIntrospection['subscriptionType'])
|
||||
? $this->getObjectType($schemaIntrospection['subscriptionType'])
|
||||
: null;
|
||||
|
||||
$directives = isset($schemaIntrospection['directives'])
|
||||
? array_map(
|
||||
[$this, 'buildDirective'],
|
||||
$schemaIntrospection['directives']
|
||||
)
|
||||
: [];
|
||||
|
||||
$schemaConfig = new SchemaConfig();
|
||||
$schemaConfig->setQuery($queryType)
|
||||
->setMutation($mutationType)
|
||||
->setSubscription($subscriptionType)
|
||||
->setTypes($this->typeMap)
|
||||
->setDirectives($directives)
|
||||
->setAssumeValid(
|
||||
isset($this->options)
|
||||
&& isset($this->options['assumeValid'])
|
||||
&& $this->options['assumeValid']
|
||||
);
|
||||
|
||||
return new Schema($schemaConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $typeRef
|
||||
*/
|
||||
private function getType(array $typeRef) : Type
|
||||
{
|
||||
if (isset($typeRef['kind'])) {
|
||||
if ($typeRef['kind'] === TypeKind::LIST) {
|
||||
if (! isset($typeRef['ofType'])) {
|
||||
throw new InvariantViolation('Decorated type deeper than introspection query.');
|
||||
}
|
||||
|
||||
return new ListOfType($this->getType($typeRef['ofType']));
|
||||
}
|
||||
|
||||
if ($typeRef['kind'] === TypeKind::NON_NULL) {
|
||||
if (! isset($typeRef['ofType'])) {
|
||||
throw new InvariantViolation('Decorated type deeper than introspection query.');
|
||||
}
|
||||
/** @var NullableType $nullableType */
|
||||
$nullableType = $this->getType($typeRef['ofType']);
|
||||
|
||||
return new NonNull($nullableType);
|
||||
}
|
||||
}
|
||||
|
||||
if (! isset($typeRef['name'])) {
|
||||
throw new InvariantViolation('Unknown type reference: ' . json_encode($typeRef) . '.');
|
||||
}
|
||||
|
||||
return $this->getNamedType($typeRef['name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return NamedType&Type
|
||||
*/
|
||||
private function getNamedType(string $typeName) : NamedType
|
||||
{
|
||||
if (! isset($this->typeMap[$typeName])) {
|
||||
throw new InvariantViolation(
|
||||
"Invalid or incomplete schema, unknown type: ${typeName}. Ensure that a full introspection query is used in order to build a client schema."
|
||||
);
|
||||
}
|
||||
|
||||
return $this->typeMap[$typeName];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $typeRef
|
||||
*/
|
||||
private function getInputType(array $typeRef) : InputType
|
||||
{
|
||||
$type = $this->getType($typeRef);
|
||||
|
||||
if ($type instanceof InputType) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
throw new InvariantViolation('Introspection must provide input type for arguments, but received: ' . json_encode($type) . '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $typeRef
|
||||
*/
|
||||
private function getOutputType(array $typeRef) : OutputType
|
||||
{
|
||||
$type = $this->getType($typeRef);
|
||||
|
||||
if ($type instanceof OutputType) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
throw new InvariantViolation('Introspection must provide output type for fields, but received: ' . json_encode($type) . '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $typeRef
|
||||
*/
|
||||
private function getObjectType(array $typeRef) : ObjectType
|
||||
{
|
||||
$type = $this->getType($typeRef);
|
||||
|
||||
return ObjectType::assertObjectType($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $typeRef
|
||||
*/
|
||||
public function getInterfaceType(array $typeRef) : InterfaceType
|
||||
{
|
||||
$type = $this->getType($typeRef);
|
||||
|
||||
return InterfaceType::assertInterfaceType($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $type
|
||||
*/
|
||||
private function buildType(array $type) : NamedType
|
||||
{
|
||||
if (array_key_exists('name', $type) && array_key_exists('kind', $type)) {
|
||||
switch ($type['kind']) {
|
||||
case TypeKind::SCALAR:
|
||||
return $this->buildScalarDef($type);
|
||||
case TypeKind::OBJECT:
|
||||
return $this->buildObjectDef($type);
|
||||
case TypeKind::INTERFACE:
|
||||
return $this->buildInterfaceDef($type);
|
||||
case TypeKind::UNION:
|
||||
return $this->buildUnionDef($type);
|
||||
case TypeKind::ENUM:
|
||||
return $this->buildEnumDef($type);
|
||||
case TypeKind::INPUT_OBJECT:
|
||||
return $this->buildInputObjectDef($type);
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvariantViolation(
|
||||
'Invalid or incomplete introspection result. Ensure that a full introspection query is used in order to build a client schema: ' . json_encode($type) . '.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $scalar
|
||||
*/
|
||||
private function buildScalarDef(array $scalar) : ScalarType
|
||||
{
|
||||
return new CustomScalarType([
|
||||
'name' => $scalar['name'],
|
||||
'description' => $scalar['description'],
|
||||
'serialize' => static function ($value) : string {
|
||||
return (string) $value;
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $implementingIntrospection
|
||||
*
|
||||
* @return array<int, InterfaceType>
|
||||
*/
|
||||
private function buildImplementationsList(array $implementingIntrospection) : array
|
||||
{
|
||||
// TODO: Temporary workaround until GraphQL ecosystem will fully support 'interfaces' on interface types.
|
||||
if (array_key_exists('interfaces', $implementingIntrospection) &&
|
||||
$implementingIntrospection['interfaces'] === null &&
|
||||
$implementingIntrospection['kind'] === TypeKind::INTERFACE) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (! array_key_exists('interfaces', $implementingIntrospection)) {
|
||||
throw new InvariantViolation('Introspection result missing interfaces: ' . json_encode($implementingIntrospection) . '.');
|
||||
}
|
||||
|
||||
return array_map([$this, 'getInterfaceType'], $implementingIntrospection['interfaces']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $object
|
||||
*/
|
||||
private function buildObjectDef(array $object) : ObjectType
|
||||
{
|
||||
return new ObjectType([
|
||||
'name' => $object['name'],
|
||||
'description' => $object['description'],
|
||||
'interfaces' => function () use ($object) : array {
|
||||
return $this->buildImplementationsList($object);
|
||||
},
|
||||
'fields' => function () use ($object) {
|
||||
return $this->buildFieldDefMap($object);
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $interface
|
||||
*/
|
||||
private function buildInterfaceDef(array $interface) : InterfaceType
|
||||
{
|
||||
return new InterfaceType([
|
||||
'name' => $interface['name'],
|
||||
'description' => $interface['description'],
|
||||
'fields' => function () use ($interface) {
|
||||
return $this->buildFieldDefMap($interface);
|
||||
},
|
||||
'interfaces' => function () use ($interface) : array {
|
||||
return $this->buildImplementationsList($interface);
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|array<string>> $union
|
||||
*/
|
||||
private function buildUnionDef(array $union) : UnionType
|
||||
{
|
||||
if (! array_key_exists('possibleTypes', $union)) {
|
||||
throw new InvariantViolation('Introspection result missing possibleTypes: ' . json_encode($union) . '.');
|
||||
}
|
||||
|
||||
return new UnionType([
|
||||
'name' => $union['name'],
|
||||
'description' => $union['description'],
|
||||
'types' => function () use ($union) : array {
|
||||
return array_map(
|
||||
[$this, 'getObjectType'],
|
||||
$union['possibleTypes']
|
||||
);
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|array<string, string>> $enum
|
||||
*/
|
||||
private function buildEnumDef(array $enum) : EnumType
|
||||
{
|
||||
if (! array_key_exists('enumValues', $enum)) {
|
||||
throw new InvariantViolation('Introspection result missing enumValues: ' . json_encode($enum) . '.');
|
||||
}
|
||||
|
||||
return new EnumType([
|
||||
'name' => $enum['name'],
|
||||
'description' => $enum['description'],
|
||||
'values' => Utils::keyValMap(
|
||||
$enum['enumValues'],
|
||||
static function (array $enumValue) : string {
|
||||
return $enumValue['name'];
|
||||
},
|
||||
static function (array $enumValue) : array {
|
||||
return [
|
||||
'description' => $enumValue['description'],
|
||||
'deprecationReason' => $enumValue['deprecationReason'],
|
||||
];
|
||||
}
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $inputObject
|
||||
*/
|
||||
private function buildInputObjectDef(array $inputObject) : InputObjectType
|
||||
{
|
||||
if (! array_key_exists('inputFields', $inputObject)) {
|
||||
throw new InvariantViolation('Introspection result missing inputFields: ' . json_encode($inputObject) . '.');
|
||||
}
|
||||
|
||||
return new InputObjectType([
|
||||
'name' => $inputObject['name'],
|
||||
'description' => $inputObject['description'],
|
||||
'fields' => function () use ($inputObject) : array {
|
||||
return $this->buildInputValueDefMap($inputObject['inputFields']);
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $typeIntrospection
|
||||
*/
|
||||
private function buildFieldDefMap(array $typeIntrospection)
|
||||
{
|
||||
if (! array_key_exists('fields', $typeIntrospection)) {
|
||||
throw new InvariantViolation('Introspection result missing fields: ' . json_encode($typeIntrospection) . '.');
|
||||
}
|
||||
|
||||
return Utils::keyValMap(
|
||||
$typeIntrospection['fields'],
|
||||
static function (array $fieldIntrospection) : string {
|
||||
return $fieldIntrospection['name'];
|
||||
},
|
||||
function (array $fieldIntrospection) : array {
|
||||
if (! array_key_exists('args', $fieldIntrospection)) {
|
||||
throw new InvariantViolation('Introspection result missing field args: ' . json_encode($fieldIntrospection) . '.');
|
||||
}
|
||||
|
||||
return [
|
||||
'description' => $fieldIntrospection['description'],
|
||||
'deprecationReason' => $fieldIntrospection['deprecationReason'],
|
||||
'type' => $this->getOutputType($fieldIntrospection['type']),
|
||||
'args' => $this->buildInputValueDefMap($fieldIntrospection['args']),
|
||||
];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $inputValueIntrospections
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function buildInputValueDefMap(array $inputValueIntrospections) : array
|
||||
{
|
||||
return Utils::keyValMap(
|
||||
$inputValueIntrospections,
|
||||
static function (array $inputValue) : string {
|
||||
return $inputValue['name'];
|
||||
},
|
||||
[$this, 'buildInputValue']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $inputValueIntrospection
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function buildInputValue(array $inputValueIntrospection) : array
|
||||
{
|
||||
$type = $this->getInputType($inputValueIntrospection['type']);
|
||||
|
||||
$inputValue = [
|
||||
'description' => $inputValueIntrospection['description'],
|
||||
'type' => $type,
|
||||
];
|
||||
|
||||
if (isset($inputValueIntrospection['defaultValue'])) {
|
||||
$inputValue['defaultValue'] = AST::valueFromAST(
|
||||
Parser::parseValue($inputValueIntrospection['defaultValue']),
|
||||
$type
|
||||
);
|
||||
}
|
||||
|
||||
return $inputValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $directive
|
||||
*/
|
||||
public function buildDirective(array $directive) : Directive
|
||||
{
|
||||
if (! array_key_exists('args', $directive)) {
|
||||
throw new InvariantViolation('Introspection result missing directive args: ' . json_encode($directive) . '.');
|
||||
}
|
||||
if (! array_key_exists('locations', $directive)) {
|
||||
throw new InvariantViolation('Introspection result missing directive locations: ' . json_encode($directive) . '.');
|
||||
}
|
||||
|
||||
return new Directive([
|
||||
'name' => $directive['name'],
|
||||
'description' => $directive['description'],
|
||||
'args' => $this->buildInputValueDefMap($directive['args']),
|
||||
'isRepeatable' => $directive['isRepeatable'],
|
||||
'locations' => $directive['locations'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
+149
-559
@@ -1,36 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Executor\Values;
|
||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\AST\EnumTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\SchemaDefinitionNode;
|
||||
use GraphQL\Language\AST\TypeDefinitionNode;
|
||||
use GraphQL\Language\AST\TypeNode;
|
||||
use GraphQL\Language\AST\UnionTypeDefinitionNode;
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Language\Source;
|
||||
use GraphQL\Language\Token;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\CustomScalarType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Introspection;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
use function array_map;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Build instance of `GraphQL\Type\Schema` out of type language definition (string or parsed AST)
|
||||
@@ -38,31 +30,46 @@ use GraphQL\Type\Introspection;
|
||||
*/
|
||||
class BuildSchema
|
||||
{
|
||||
/** @var DocumentNode */
|
||||
private $ast;
|
||||
|
||||
/** @var array<string, TypeDefinitionNode> */
|
||||
private $nodeMap;
|
||||
|
||||
/** @var callable|null */
|
||||
private $typeConfigDecorator;
|
||||
|
||||
/** @var array<string, bool> */
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* @param Type $innerType
|
||||
* @param TypeNode $inputTypeNode
|
||||
* @return Type
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode)
|
||||
public function __construct(DocumentNode $ast, ?callable $typeConfigDecorator = null, array $options = [])
|
||||
{
|
||||
if ($inputTypeNode->kind == NodeKind::LIST_TYPE) {
|
||||
return Type::listOf($this->buildWrappedType($innerType, $inputTypeNode->type));
|
||||
}
|
||||
if ($inputTypeNode->kind == NodeKind::NON_NULL_TYPE) {
|
||||
$wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type);
|
||||
Utils::invariant(!($wrappedType instanceof NonNull), 'No nesting nonnull.');
|
||||
return Type::nonNull($wrappedType);
|
||||
}
|
||||
return $innerType;
|
||||
$this->ast = $ast;
|
||||
$this->typeConfigDecorator = $typeConfigDecorator;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
private function getNamedTypeNode(TypeNode $typeNode)
|
||||
/**
|
||||
* A helper function to build a GraphQLSchema directly from a source
|
||||
* document.
|
||||
*
|
||||
* @param DocumentNode|Source|string $source
|
||||
* @param array<string, bool> $options
|
||||
*
|
||||
* @return Schema
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function build($source, ?callable $typeConfigDecorator = null, array $options = [])
|
||||
{
|
||||
$namedType = $typeNode;
|
||||
while ($namedType->kind === NodeKind::LIST_TYPE || $namedType->kind === NodeKind::NON_NULL_TYPE) {
|
||||
$namedType = $namedType->type;
|
||||
}
|
||||
return $namedType;
|
||||
$doc = $source instanceof DocumentNode
|
||||
? $source
|
||||
: Parser::parse($source);
|
||||
|
||||
return self::buildAST($doc, $typeConfigDecorator, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,573 +82,156 @@ class BuildSchema
|
||||
* Given that AST it constructs a GraphQL\Type\Schema. The resulting schema
|
||||
* has no resolve methods, so execution will use default resolvers.
|
||||
*
|
||||
* @api
|
||||
* @param DocumentNode $ast
|
||||
* @param callable $typeConfigDecorator
|
||||
* Accepts options as a third argument:
|
||||
*
|
||||
* - commentDescriptions:
|
||||
* Provide true to use preceding comments as the description.
|
||||
* This option is provided to ease adoption and will be removed in v16.
|
||||
*
|
||||
* @param array<string, bool> $options
|
||||
*
|
||||
* @return Schema
|
||||
*
|
||||
* @throws Error
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function buildAST(DocumentNode $ast, callable $typeConfigDecorator = null)
|
||||
public static function buildAST(DocumentNode $ast, ?callable $typeConfigDecorator = null, array $options = [])
|
||||
{
|
||||
$builder = new self($ast, $typeConfigDecorator);
|
||||
$builder = new self($ast, $typeConfigDecorator, $options);
|
||||
|
||||
return $builder->buildSchema();
|
||||
}
|
||||
|
||||
private $ast;
|
||||
private $innerTypeMap;
|
||||
private $nodeMap;
|
||||
private $typeConfigDecorator;
|
||||
private $loadedTypeDefs;
|
||||
|
||||
public function __construct(DocumentNode $ast, callable $typeConfigDecorator = null)
|
||||
{
|
||||
$this->ast = $ast;
|
||||
$this->typeConfigDecorator = $typeConfigDecorator;
|
||||
$this->loadedTypeDefs = [];
|
||||
}
|
||||
|
||||
public function buildSchema()
|
||||
{
|
||||
$schemaDef = null;
|
||||
$typeDefs = [];
|
||||
$options = $this->options;
|
||||
if (! ($options['assumeValid'] ?? false) && ! ($options['assumeValidSDL'] ?? false)) {
|
||||
DocumentValidator::assertValidSDL($this->ast);
|
||||
}
|
||||
|
||||
$schemaDef = null;
|
||||
$typeDefs = [];
|
||||
$this->nodeMap = [];
|
||||
/** @var array<int, DirectiveDefinitionNode> $directiveDefs */
|
||||
$directiveDefs = [];
|
||||
foreach ($this->ast->definitions as $d) {
|
||||
switch ($d->kind) {
|
||||
case NodeKind::SCHEMA_DEFINITION:
|
||||
if ($schemaDef) {
|
||||
throw new Error('Must provide only one schema definition.');
|
||||
}
|
||||
$schemaDef = $d;
|
||||
foreach ($this->ast->definitions as $definition) {
|
||||
switch (true) {
|
||||
case $definition instanceof SchemaDefinitionNode:
|
||||
$schemaDef = $definition;
|
||||
break;
|
||||
case NodeKind::SCALAR_TYPE_DEFINITION:
|
||||
case NodeKind::OBJECT_TYPE_DEFINITION:
|
||||
case NodeKind::INTERFACE_TYPE_DEFINITION:
|
||||
case NodeKind::ENUM_TYPE_DEFINITION:
|
||||
case NodeKind::UNION_TYPE_DEFINITION:
|
||||
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
|
||||
$typeName = $d->name->value;
|
||||
if (!empty($this->nodeMap[$typeName])) {
|
||||
throw new Error("Type \"$typeName\" was defined more than once.");
|
||||
case $definition instanceof TypeDefinitionNode:
|
||||
$typeName = $definition->name->value;
|
||||
if (isset($this->nodeMap[$typeName])) {
|
||||
throw new Error(sprintf('Type "%s" was defined more than once.', $typeName));
|
||||
}
|
||||
$typeDefs[] = $d;
|
||||
$this->nodeMap[$typeName] = $d;
|
||||
$typeDefs[] = $definition;
|
||||
$this->nodeMap[$typeName] = $definition;
|
||||
break;
|
||||
case NodeKind::DIRECTIVE_DEFINITION:
|
||||
$directiveDefs[] = $d;
|
||||
case $definition instanceof DirectiveDefinitionNode:
|
||||
$directiveDefs[] = $definition;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$queryTypeName = null;
|
||||
$mutationTypeName = null;
|
||||
$subscriptionTypeName = null;
|
||||
if ($schemaDef) {
|
||||
foreach ($schemaDef->operationTypes as $operationType) {
|
||||
$typeName = $operationType->type->name->value;
|
||||
if ($operationType->operation === 'query') {
|
||||
if ($queryTypeName) {
|
||||
throw new Error('Must provide only one query type in schema.');
|
||||
}
|
||||
if (!isset($this->nodeMap[$typeName])) {
|
||||
throw new Error(
|
||||
'Specified query type "' . $typeName . '" not found in document.'
|
||||
);
|
||||
}
|
||||
$queryTypeName = $typeName;
|
||||
} else if ($operationType->operation === 'mutation') {
|
||||
if ($mutationTypeName) {
|
||||
throw new Error('Must provide only one mutation type in schema.');
|
||||
}
|
||||
if (!isset($this->nodeMap[$typeName])) {
|
||||
throw new Error(
|
||||
'Specified mutation type "' . $typeName . '" not found in document.'
|
||||
);
|
||||
}
|
||||
$mutationTypeName = $typeName;
|
||||
} else if ($operationType->operation === 'subscription') {
|
||||
if ($subscriptionTypeName) {
|
||||
throw new Error('Must provide only one subscription type in schema.');
|
||||
}
|
||||
if (!isset($this->nodeMap[$typeName])) {
|
||||
throw new Error(
|
||||
'Specified subscription type "' . $typeName . '" not found in document.'
|
||||
);
|
||||
}
|
||||
$subscriptionTypeName = $typeName;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isset($this->nodeMap['Query'])) {
|
||||
$queryTypeName = 'Query';
|
||||
}
|
||||
if (isset($this->nodeMap['Mutation'])) {
|
||||
$mutationTypeName = 'Mutation';
|
||||
}
|
||||
if (isset($this->nodeMap['Subscription'])) {
|
||||
$subscriptionTypeName = 'Subscription';
|
||||
}
|
||||
}
|
||||
$operationTypes = $schemaDef !== null
|
||||
? $this->getOperationTypes($schemaDef)
|
||||
: [
|
||||
'query' => isset($this->nodeMap['Query']) ? 'Query' : null,
|
||||
'mutation' => isset($this->nodeMap['Mutation']) ? 'Mutation' : null,
|
||||
'subscription' => isset($this->nodeMap['Subscription']) ? 'Subscription' : null,
|
||||
];
|
||||
|
||||
if (!$queryTypeName) {
|
||||
throw new Error(
|
||||
'Must provide schema definition with query type or a type named Query.'
|
||||
);
|
||||
}
|
||||
$DefinitionBuilder = new ASTDefinitionBuilder(
|
||||
$this->nodeMap,
|
||||
$this->options,
|
||||
static function ($typeName) : void {
|
||||
throw new Error('Type "' . $typeName . '" not found in document.');
|
||||
},
|
||||
$this->typeConfigDecorator
|
||||
);
|
||||
|
||||
$this->innerTypeMap = [
|
||||
'String' => Type::string(),
|
||||
'Int' => Type::int(),
|
||||
'Float' => Type::float(),
|
||||
'Boolean' => Type::boolean(),
|
||||
'ID' => Type::id(),
|
||||
'__Schema' => Introspection::_schema(),
|
||||
'__Directive' => Introspection::_directive(),
|
||||
'__DirectiveLocation' => Introspection::_directiveLocation(),
|
||||
'__Type' => Introspection::_type(),
|
||||
'__Field' => Introspection::_field(),
|
||||
'__InputValue' => Introspection::_inputValue(),
|
||||
'__EnumValue' => Introspection::_enumValue(),
|
||||
'__TypeKind' => Introspection::_typeKind(),
|
||||
];
|
||||
|
||||
$directives = array_map([$this, 'getDirective'], $directiveDefs);
|
||||
$directives = array_map(
|
||||
static function (DirectiveDefinitionNode $def) use ($DefinitionBuilder) : Directive {
|
||||
return $DefinitionBuilder->buildDirective($def);
|
||||
},
|
||||
$directiveDefs
|
||||
);
|
||||
|
||||
// If specified directives were not explicitly declared, add them.
|
||||
$skip = array_reduce($directives, function($hasSkip, $directive) {
|
||||
return $hasSkip || $directive->name == 'skip';
|
||||
});
|
||||
if (!$skip) {
|
||||
$directivesByName = Utils::groupBy(
|
||||
$directives,
|
||||
static function (Directive $directive) : string {
|
||||
return $directive->name;
|
||||
}
|
||||
);
|
||||
if (! isset($directivesByName['skip'])) {
|
||||
$directives[] = Directive::skipDirective();
|
||||
}
|
||||
|
||||
$include = array_reduce($directives, function($hasInclude, $directive) {
|
||||
return $hasInclude || $directive->name == 'include';
|
||||
});
|
||||
if (!$include) {
|
||||
if (! isset($directivesByName['include'])) {
|
||||
$directives[] = Directive::includeDirective();
|
||||
}
|
||||
|
||||
$deprecated = array_reduce($directives, function($hasDeprecated, $directive) {
|
||||
return $hasDeprecated || $directive->name == 'deprecated';
|
||||
});
|
||||
if (!$deprecated) {
|
||||
if (! isset($directivesByName['deprecated'])) {
|
||||
$directives[] = Directive::deprecatedDirective();
|
||||
}
|
||||
|
||||
$schema = new Schema([
|
||||
'query' => $this->getObjectType($this->nodeMap[$queryTypeName]),
|
||||
'mutation' => $mutationTypeName ?
|
||||
$this->getObjectType($this->nodeMap[$mutationTypeName]) :
|
||||
null,
|
||||
'subscription' => $subscriptionTypeName ?
|
||||
$this->getObjectType($this->nodeMap[$subscriptionTypeName]) :
|
||||
null,
|
||||
'typeLoader' => function($name) {
|
||||
return $this->typeDefNamed($name);
|
||||
// Note: While this could make early assertions to get the correctly
|
||||
// typed values below, that would throw immediately while type system
|
||||
// validation with validateSchema() will produce more actionable results.
|
||||
|
||||
return new Schema([
|
||||
'query' => isset($operationTypes['query'])
|
||||
? $DefinitionBuilder->buildType($operationTypes['query'])
|
||||
: null,
|
||||
'mutation' => isset($operationTypes['mutation'])
|
||||
? $DefinitionBuilder->buildType($operationTypes['mutation'])
|
||||
: null,
|
||||
'subscription' => isset($operationTypes['subscription'])
|
||||
? $DefinitionBuilder->buildType($operationTypes['subscription'])
|
||||
: null,
|
||||
'typeLoader' => static function ($name) use ($DefinitionBuilder) : Type {
|
||||
return $DefinitionBuilder->buildType($name);
|
||||
},
|
||||
'directives' => $directives,
|
||||
'astNode' => $schemaDef,
|
||||
'types' => function() {
|
||||
'directives' => $directives,
|
||||
'astNode' => $schemaDef,
|
||||
'types' => function () use ($DefinitionBuilder) : array {
|
||||
$types = [];
|
||||
/** @var ScalarTypeDefinitionNode|ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|UnionTypeDefinitionNode|EnumTypeDefinitionNode|InputObjectTypeDefinitionNode $def */
|
||||
foreach ($this->nodeMap as $name => $def) {
|
||||
if (!isset($this->loadedTypeDefs[$name])) {
|
||||
$types[] = $this->typeDefNamed($def->name->value);
|
||||
}
|
||||
$types[] = $DefinitionBuilder->buildType($def->name->value);
|
||||
}
|
||||
|
||||
return $types;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
private function getDirective(DirectiveDefinitionNode $directiveNode)
|
||||
{
|
||||
return new Directive([
|
||||
'name' => $directiveNode->name->value,
|
||||
'description' => $this->getDescription($directiveNode),
|
||||
'locations' => Utils::map($directiveNode->locations, function($node) {
|
||||
return $node->value;
|
||||
}),
|
||||
'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null,
|
||||
'astNode' => $directiveNode
|
||||
]);
|
||||
}
|
||||
|
||||
private function getObjectType(TypeDefinitionNode $typeNode)
|
||||
{
|
||||
$type = $this->typeDefNamed($typeNode->name->value);
|
||||
Utils::invariant(
|
||||
$type instanceof ObjectType,
|
||||
'AST must provide object type.'
|
||||
);
|
||||
return $type;
|
||||
}
|
||||
|
||||
private function produceType(TypeNode $typeNode)
|
||||
{
|
||||
$typeName = $this->getNamedTypeNode($typeNode)->name->value;
|
||||
$typeDef = $this->typeDefNamed($typeName);
|
||||
return $this->buildWrappedType($typeDef, $typeNode);
|
||||
}
|
||||
|
||||
private function produceInputType(TypeNode $typeNode)
|
||||
{
|
||||
$type = $this->produceType($typeNode);
|
||||
Utils::invariant(Type::isInputType($type), 'Expected Input type.');
|
||||
return $type;
|
||||
}
|
||||
|
||||
private function produceOutputType(TypeNode $typeNode)
|
||||
{
|
||||
$type = $this->produceType($typeNode);
|
||||
Utils::invariant(Type::isOutputType($type), 'Expected Input type.');
|
||||
return $type;
|
||||
}
|
||||
|
||||
private function produceObjectType(TypeNode $typeNode)
|
||||
{
|
||||
$type = $this->produceType($typeNode);
|
||||
Utils::invariant($type instanceof ObjectType, 'Expected Object type.');
|
||||
return $type;
|
||||
}
|
||||
|
||||
private function produceInterfaceType(TypeNode $typeNode)
|
||||
{
|
||||
$type = $this->produceType($typeNode);
|
||||
Utils::invariant($type instanceof InterfaceType, 'Expected Interface type.');
|
||||
return $type;
|
||||
}
|
||||
|
||||
private function typeDefNamed($typeName)
|
||||
{
|
||||
if (isset($this->innerTypeMap[$typeName])) {
|
||||
return $this->innerTypeMap[$typeName];
|
||||
}
|
||||
|
||||
if (!isset($this->nodeMap[$typeName])) {
|
||||
throw new Error('Type "' . $typeName . '" not found in document.');
|
||||
}
|
||||
|
||||
$this->loadedTypeDefs[$typeName] = true;
|
||||
|
||||
$config = $this->makeSchemaDefConfig($this->nodeMap[$typeName]);
|
||||
|
||||
if ($this->typeConfigDecorator) {
|
||||
$fn = $this->typeConfigDecorator;
|
||||
try {
|
||||
$config = $fn($config, $this->nodeMap[$typeName], $this->nodeMap);
|
||||
} catch (\Exception $e) {
|
||||
throw new Error(
|
||||
"Type config decorator passed to " . (static::class) . " threw an error ".
|
||||
"when building $typeName type: {$e->getMessage()}",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
$e
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
throw new Error(
|
||||
"Type config decorator passed to " . (static::class) . " threw an error ".
|
||||
"when building $typeName type: {$e->getMessage()}",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
$e
|
||||
);
|
||||
}
|
||||
if (!is_array($config) || isset($config[0])) {
|
||||
throw new Error(
|
||||
"Type config decorator passed to " . (static::class) . " is expected to return an array, but got ".
|
||||
Utils::getVariableType($config)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$innerTypeDef = $this->makeSchemaDef($this->nodeMap[$typeName], $config);
|
||||
|
||||
if (!$innerTypeDef) {
|
||||
throw new Error("Nothing constructed for $typeName.");
|
||||
}
|
||||
$this->innerTypeMap[$typeName] = $innerTypeDef;
|
||||
return $innerTypeDef;
|
||||
}
|
||||
|
||||
private function makeSchemaDefConfig($def)
|
||||
{
|
||||
if (!$def) {
|
||||
throw new Error('def must be defined.');
|
||||
}
|
||||
switch ($def->kind) {
|
||||
case NodeKind::OBJECT_TYPE_DEFINITION:
|
||||
return $this->makeTypeDefConfig($def);
|
||||
case NodeKind::INTERFACE_TYPE_DEFINITION:
|
||||
return $this->makeInterfaceDefConfig($def);
|
||||
case NodeKind::ENUM_TYPE_DEFINITION:
|
||||
return $this->makeEnumDefConfig($def);
|
||||
case NodeKind::UNION_TYPE_DEFINITION:
|
||||
return $this->makeUnionDefConfig($def);
|
||||
case NodeKind::SCALAR_TYPE_DEFINITION:
|
||||
return $this->makeScalarDefConfig($def);
|
||||
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
|
||||
return $this->makeInputObjectDefConfig($def);
|
||||
default:
|
||||
throw new Error("Type kind of {$def->kind} not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
private function makeSchemaDef($def, array $config = null)
|
||||
{
|
||||
if (!$def) {
|
||||
throw new Error('def must be defined.');
|
||||
}
|
||||
|
||||
$config = $config ?: $this->makeSchemaDefConfig($def);
|
||||
|
||||
switch ($def->kind) {
|
||||
case NodeKind::OBJECT_TYPE_DEFINITION:
|
||||
return new ObjectType($config);
|
||||
case NodeKind::INTERFACE_TYPE_DEFINITION:
|
||||
return new InterfaceType($config);
|
||||
case NodeKind::ENUM_TYPE_DEFINITION:
|
||||
return new EnumType($config);
|
||||
case NodeKind::UNION_TYPE_DEFINITION:
|
||||
return new UnionType($config);
|
||||
case NodeKind::SCALAR_TYPE_DEFINITION:
|
||||
return new CustomScalarType($config);
|
||||
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
|
||||
return new InputObjectType($config);
|
||||
default:
|
||||
throw new Error("Type kind of {$def->kind} not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
private function makeTypeDefConfig(ObjectTypeDefinitionNode $def)
|
||||
{
|
||||
$typeName = $def->name->value;
|
||||
return [
|
||||
'name' => $typeName,
|
||||
'description' => $this->getDescription($def),
|
||||
'fields' => function() use ($def) {
|
||||
return $this->makeFieldDefMap($def);
|
||||
},
|
||||
'interfaces' => function() use ($def) {
|
||||
return $this->makeImplementedInterfaces($def);
|
||||
},
|
||||
'astNode' => $def
|
||||
];
|
||||
}
|
||||
|
||||
private function makeFieldDefMap($def)
|
||||
{
|
||||
return Utils::keyValMap(
|
||||
$def->fields,
|
||||
function ($field) {
|
||||
return $field->name->value;
|
||||
},
|
||||
function($field) {
|
||||
return [
|
||||
'type' => $this->produceOutputType($field->type),
|
||||
'description' => $this->getDescription($field),
|
||||
'args' => $this->makeInputValues($field->arguments),
|
||||
'deprecationReason' => $this->getDeprecationReason($field),
|
||||
'astNode' => $field
|
||||
];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def)
|
||||
{
|
||||
if (isset($def->interfaces)) {
|
||||
return Utils::map($def->interfaces, function ($iface) {
|
||||
return $this->produceInterfaceType($iface);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function makeInputValues($values)
|
||||
{
|
||||
return Utils::keyValMap(
|
||||
$values,
|
||||
function ($value) {
|
||||
return $value->name->value;
|
||||
},
|
||||
function($value) {
|
||||
$type = $this->produceInputType($value->type);
|
||||
$config = [
|
||||
'name' => $value->name->value,
|
||||
'type' => $type,
|
||||
'description' => $this->getDescription($value),
|
||||
'astNode' => $value
|
||||
];
|
||||
if (isset($value->defaultValue)) {
|
||||
$config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type);
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private function makeInterfaceDefConfig(InterfaceTypeDefinitionNode $def)
|
||||
{
|
||||
$typeName = $def->name->value;
|
||||
return [
|
||||
'name' => $typeName,
|
||||
'description' => $this->getDescription($def),
|
||||
'fields' => function() use ($def) {
|
||||
return $this->makeFieldDefMap($def);
|
||||
},
|
||||
'astNode' => $def,
|
||||
'resolveType' => function() {
|
||||
$this->cannotExecuteSchema();
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private function makeEnumDefConfig(EnumTypeDefinitionNode $def)
|
||||
{
|
||||
return [
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'astNode' => $def,
|
||||
'values' => Utils::keyValMap(
|
||||
$def->values,
|
||||
function($enumValue) {
|
||||
return $enumValue->name->value;
|
||||
},
|
||||
function($enumValue) {
|
||||
return [
|
||||
'description' => $this->getDescription($enumValue),
|
||||
'deprecationReason' => $this->getDeprecationReason($enumValue),
|
||||
'astNode' => $enumValue
|
||||
];
|
||||
}
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
private function makeUnionDefConfig(UnionTypeDefinitionNode $def)
|
||||
{
|
||||
return [
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'types' => Utils::map($def->types, function($typeNode) {
|
||||
return $this->produceObjectType($typeNode);
|
||||
}),
|
||||
'astNode' => $def,
|
||||
'resolveType' => [$this, 'cannotExecuteSchema']
|
||||
];
|
||||
}
|
||||
|
||||
private function makeScalarDefConfig(ScalarTypeDefinitionNode $def)
|
||||
{
|
||||
return [
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'astNode' => $def,
|
||||
'serialize' => function() {
|
||||
return false;
|
||||
},
|
||||
// Note: validation calls the parse functions to determine if a
|
||||
// literal value is correct. Returning null would cause use of custom
|
||||
// scalars to always fail validation. Returning false causes them to
|
||||
// always pass validation.
|
||||
'parseValue' => function() {
|
||||
return false;
|
||||
},
|
||||
'parseLiteral' => function() {
|
||||
return false;
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private function makeInputObjectDefConfig(InputObjectTypeDefinitionNode $def)
|
||||
{
|
||||
return [
|
||||
'name' => $def->name->value,
|
||||
'description' => $this->getDescription($def),
|
||||
'fields' => function() use ($def) { return $this->makeInputValues($def->fields); },
|
||||
'astNode' => $def,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a collection of directives, returns the string value for the
|
||||
* deprecation reason.
|
||||
* @param SchemaDefinitionNode $schemaDef
|
||||
*
|
||||
* @param EnumValueDefinitionNode | FieldDefinitionNode $node
|
||||
* @return string
|
||||
* @return string[]
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
private function getDeprecationReason($node)
|
||||
private function getOperationTypes($schemaDef)
|
||||
{
|
||||
$deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node);
|
||||
return isset($deprecated['reason']) ? $deprecated['reason'] : null;
|
||||
}
|
||||
$opTypes = [];
|
||||
|
||||
/**
|
||||
* Given an ast node, returns its string description based on a contiguous
|
||||
* block full-line of comments preceding it.
|
||||
*/
|
||||
public function getDescription($node)
|
||||
{
|
||||
$loc = $node->loc;
|
||||
if (!$loc || !$loc->startToken) {
|
||||
return ;
|
||||
}
|
||||
$comments = [];
|
||||
$minSpaces = null;
|
||||
$token = $loc->startToken->prev;
|
||||
while (
|
||||
$token &&
|
||||
$token->kind === Token::COMMENT &&
|
||||
$token->next && $token->prev &&
|
||||
$token->line + 1 === $token->next->line &&
|
||||
$token->line !== $token->prev->line
|
||||
) {
|
||||
$value = $token->value;
|
||||
$spaces = $this->leadingSpaces($value);
|
||||
if ($minSpaces === null || $spaces < $minSpaces) {
|
||||
$minSpaces = $spaces;
|
||||
foreach ($schemaDef->operationTypes as $operationType) {
|
||||
$typeName = $operationType->type->name->value;
|
||||
$operation = $operationType->operation;
|
||||
|
||||
if (isset($opTypes[$operation])) {
|
||||
throw new Error(sprintf('Must provide only one %s type in schema.', $operation));
|
||||
}
|
||||
$comments[] = $value;
|
||||
$token = $token->prev;
|
||||
|
||||
if (! isset($this->nodeMap[$typeName])) {
|
||||
throw new Error(sprintf('Specified %s type "%s" not found in document.', $operation, $typeName));
|
||||
}
|
||||
|
||||
$opTypes[$operation] = $typeName;
|
||||
}
|
||||
return implode("\n", array_map(function($comment) use ($minSpaces) {
|
||||
return mb_substr(str_replace("\n", '', $comment), $minSpaces);
|
||||
}, array_reverse($comments)));
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to build a GraphQLSchema directly from a source
|
||||
* document.
|
||||
*
|
||||
* @api
|
||||
* @param DocumentNode|Source|string $source
|
||||
* @param callable $typeConfigDecorator
|
||||
* @return Schema
|
||||
*/
|
||||
public static function build($source, callable $typeConfigDecorator = null)
|
||||
{
|
||||
$doc = $source instanceof DocumentNode ? $source : Parser::parse($source);
|
||||
return self::buildAST($doc, $typeConfigDecorator);
|
||||
return $opTypes;
|
||||
}
|
||||
|
||||
// Count the number of spaces on the starting side of a string.
|
||||
private function leadingSpaces($str)
|
||||
{
|
||||
return strlen($str) - strlen(ltrim($str));
|
||||
}
|
||||
|
||||
public function cannotExecuteSchema()
|
||||
{
|
||||
throw new Error(
|
||||
'Generated Schema cannot use Interface or Union types for execution.'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,583 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Utility for finding breaking/dangerous changes between two schemas.
|
||||
*/
|
||||
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Schema;
|
||||
|
||||
class FindBreakingChanges
|
||||
{
|
||||
|
||||
const BREAKING_CHANGE_FIELD_CHANGED = 'FIELD_CHANGED_KIND';
|
||||
const BREAKING_CHANGE_FIELD_REMOVED = 'FIELD_REMOVED';
|
||||
const BREAKING_CHANGE_TYPE_CHANGED = 'TYPE_CHANGED_KIND';
|
||||
const BREAKING_CHANGE_TYPE_REMOVED = 'TYPE_REMOVED';
|
||||
const BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION = 'TYPE_REMOVED_FROM_UNION';
|
||||
const BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM = 'VALUE_REMOVED_FROM_ENUM';
|
||||
const BREAKING_CHANGE_ARG_REMOVED = 'ARG_REMOVED';
|
||||
const BREAKING_CHANGE_ARG_CHANGED = 'ARG_CHANGED_KIND';
|
||||
const BREAKING_CHANGE_NON_NULL_ARG_ADDED = 'NON_NULL_ARG_ADDED';
|
||||
const BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED = 'NON_NULL_INPUT_FIELD_ADDED';
|
||||
const BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT = 'INTERFACE_REMOVED_FROM_OBJECT';
|
||||
|
||||
const DANGEROUS_CHANGE_ARG_DEFAULT_VALUE = 'ARG_DEFAULT_VALUE_CHANGE';
|
||||
const DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM = 'VALUE_ADDED_TO_ENUM';
|
||||
const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION = 'TYPE_ADDED_TO_UNION';
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of all the types
|
||||
* of potentially dangerous changes covered by the other functions down below.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function findDangerousChanges(Schema $oldSchema, Schema $newSchema)
|
||||
{
|
||||
return array_merge(self::findArgChanges($oldSchema, $newSchema)['dangerousChanges'],
|
||||
self::findValuesAddedToEnums($oldSchema, $newSchema),
|
||||
self::findTypesAddedToUnions($oldSchema, $newSchema)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of all the types
|
||||
* of breaking changes covered by the other functions down below.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function findBreakingChanges(Schema $oldSchema, Schema $newSchema)
|
||||
{
|
||||
return array_merge(
|
||||
self::findRemovedTypes($oldSchema, $newSchema),
|
||||
self::findTypesThatChangedKind($oldSchema, $newSchema),
|
||||
self::findFieldsThatChangedType($oldSchema, $newSchema),
|
||||
self::findTypesRemovedFromUnions($oldSchema, $newSchema),
|
||||
self::findValuesRemovedFromEnums($oldSchema, $newSchema),
|
||||
self::findArgChanges($oldSchema, $newSchema)['breakingChanges'],
|
||||
self::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of any breaking
|
||||
* changes in the newSchema related to removing an entire type.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function findRemovedTypes(
|
||||
Schema $oldSchema, Schema $newSchema
|
||||
)
|
||||
{
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$breakingChanges = [];
|
||||
foreach ($oldTypeMap as $typeName => $typeDefinition) {
|
||||
if (!isset($newTypeMap[$typeName])) {
|
||||
$breakingChanges[] =
|
||||
['type' => self::BREAKING_CHANGE_TYPE_REMOVED, 'description' => "${typeName} was removed."];
|
||||
}
|
||||
}
|
||||
|
||||
return $breakingChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of any breaking
|
||||
* changes in the newSchema related to changing the type of a type.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function findTypesThatChangedKind(
|
||||
Schema $oldSchema, Schema $newSchema
|
||||
)
|
||||
{
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$breakingChanges = [];
|
||||
foreach ($oldTypeMap as $typeName => $typeDefinition) {
|
||||
if (!isset($newTypeMap[$typeName])) {
|
||||
continue;
|
||||
}
|
||||
$newTypeDefinition = $newTypeMap[$typeName];
|
||||
if (!($typeDefinition instanceof $newTypeDefinition)) {
|
||||
$oldTypeKindName = self::typeKindName($typeDefinition);
|
||||
$newTypeKindName = self::typeKindName($newTypeDefinition);
|
||||
$breakingChanges[] = [
|
||||
'type' => self::BREAKING_CHANGE_TYPE_CHANGED,
|
||||
'description' => "${typeName} changed from ${oldTypeKindName} to ${newTypeKindName}."
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $breakingChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of any
|
||||
* breaking or dangerous changes in the newSchema related to arguments
|
||||
* (such as removal or change of type of an argument, or a change in an
|
||||
* argument's default value).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function findArgChanges(
|
||||
Schema $oldSchema, Schema $newSchema
|
||||
)
|
||||
{
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$breakingChanges = [];
|
||||
$dangerousChanges = [];
|
||||
foreach ($oldTypeMap as $oldTypeName => $oldTypeDefinition) {
|
||||
$newTypeDefinition = isset($newTypeMap[$oldTypeName]) ? $newTypeMap[$oldTypeName] : null;
|
||||
if (!($oldTypeDefinition instanceof ObjectType || $oldTypeDefinition instanceof InterfaceType) ||
|
||||
!($newTypeDefinition instanceof $oldTypeDefinition)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldTypeFields = $oldTypeDefinition->getFields();
|
||||
$newTypeFields = $newTypeDefinition->getFields();
|
||||
|
||||
foreach ($oldTypeFields as $fieldName => $fieldDefinition) {
|
||||
if (!isset($newTypeFields[$fieldName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($fieldDefinition->args as $oldArgDef) {
|
||||
$newArgs = $newTypeFields[$fieldName]->args;
|
||||
$newArgDef = Utils::find(
|
||||
$newArgs, function ($arg) use ($oldArgDef) {
|
||||
return $arg->name === $oldArgDef->name;
|
||||
}
|
||||
);
|
||||
if (!$newArgDef) {
|
||||
$argName = $oldArgDef->name;
|
||||
$breakingChanges[] = [
|
||||
'type' => self::BREAKING_CHANGE_ARG_REMOVED,
|
||||
'description' => "${oldTypeName}->${fieldName} arg ${argName} was removed"
|
||||
];
|
||||
} else {
|
||||
$isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg($oldArgDef->getType(), $newArgDef->getType());
|
||||
$oldArgType = $oldArgDef->getType();
|
||||
$oldArgName = $oldArgDef->name;
|
||||
if (!$isSafe) {
|
||||
$newArgType = $newArgDef->getType();
|
||||
$breakingChanges[] = [
|
||||
'type' => self::BREAKING_CHANGE_ARG_CHANGED,
|
||||
'description' => "${oldTypeName}->${fieldName} arg ${oldArgName} has changed type from ${oldArgType} to ${newArgType}."
|
||||
];
|
||||
} elseif ($oldArgDef->defaultValueExists() && $oldArgDef->defaultValue !== $newArgDef->defaultValue) {
|
||||
$dangerousChanges[] = [
|
||||
'type' => FindBreakingChanges::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE,
|
||||
'description' => "${oldTypeName}->${fieldName} arg ${oldArgName} has changed defaultValue"
|
||||
];
|
||||
}
|
||||
}
|
||||
// Check if a non-null arg was added to the field
|
||||
foreach ($newTypeFields[$fieldName]->args as $newArgDef) {
|
||||
$oldArgs = $oldTypeFields[$fieldName]->args;
|
||||
$oldArgDef = Utils::find(
|
||||
$oldArgs, function ($arg) use ($newArgDef) {
|
||||
return $arg->name === $newArgDef->name;
|
||||
}
|
||||
);
|
||||
|
||||
if (!$oldArgDef && $newArgDef->getType() instanceof NonNull) {
|
||||
$newTypeName = $newTypeDefinition->name;
|
||||
$newArgName = $newArgDef->name;
|
||||
$breakingChanges[] = [
|
||||
'type' => self::BREAKING_CHANGE_NON_NULL_ARG_ADDED,
|
||||
'description' => "A non-null arg ${newArgName} on ${newTypeName}->${fieldName} was added."
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['breakingChanges' => $breakingChanges, 'dangerousChanges' => $dangerousChanges];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Type $type
|
||||
* @return string
|
||||
*
|
||||
* @throws \TypeError
|
||||
*/
|
||||
private static function typeKindName(Type $type)
|
||||
{
|
||||
if ($type instanceof ScalarType) {
|
||||
return 'a Scalar type';
|
||||
} elseif ($type instanceof ObjectType) {
|
||||
return 'an Object type';
|
||||
} elseif ($type instanceof InterfaceType) {
|
||||
return 'an Interface type';
|
||||
} elseif ($type instanceof UnionType) {
|
||||
return 'a Union type';
|
||||
} elseif ($type instanceof EnumType) {
|
||||
return 'an Enum type';
|
||||
} elseif ($type instanceof InputObjectType) {
|
||||
return 'an Input type';
|
||||
}
|
||||
|
||||
throw new \TypeError('unknown type ' . $type->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of any breaking
|
||||
* changes in the newSchema related to the fields on a type. This includes if
|
||||
* a field has been removed from a type, if a field has changed type, or if
|
||||
* a non-null field is added to an input type.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function findFieldsThatChangedType(
|
||||
Schema $oldSchema, Schema $newSchema
|
||||
)
|
||||
{
|
||||
return array_merge(
|
||||
self::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema),
|
||||
self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $oldSchema
|
||||
* @param Schema $newSchema
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(Schema $oldSchema, Schema $newSchema)
|
||||
{
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$breakingFieldChanges = [];
|
||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
|
||||
if (!($oldType instanceof ObjectType || $oldType instanceof InterfaceType) || !($newType instanceof $oldType)) {
|
||||
continue;
|
||||
}
|
||||
$oldTypeFieldsDef = $oldType->getFields();
|
||||
$newTypeFieldsDef = $newType->getFields();
|
||||
foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) {
|
||||
if (!isset($newTypeFieldsDef[$fieldName])) {
|
||||
$breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_REMOVED, 'description' => "${typeName}->${fieldName} was removed."];
|
||||
} else {
|
||||
$oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
|
||||
$newfieldType = $newTypeFieldsDef[$fieldName]->getType();
|
||||
$isSafe = self::isChangeSafeForObjectOrInterfaceField($oldFieldType, $newfieldType);
|
||||
if (!$isSafe) {
|
||||
|
||||
$oldFieldTypeString = self::isNamedType($oldFieldType) ? $oldFieldType->name : $oldFieldType;
|
||||
$newFieldTypeString = self::isNamedType($newfieldType) ? $newfieldType->name : $newfieldType;
|
||||
$breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_CHANGED, 'description' => "${typeName}->${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $breakingFieldChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $oldSchema
|
||||
* @param Schema $newSchema
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function findFieldsThatChangedTypeOnInputObjectTypes(
|
||||
Schema $oldSchema, Schema $newSchema
|
||||
)
|
||||
{
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$breakingFieldChanges = [];
|
||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
|
||||
if (!($oldType instanceof InputObjectType) || !($newType instanceof InputObjectType)) {
|
||||
continue;
|
||||
}
|
||||
$oldTypeFieldsDef = $oldType->getFields();
|
||||
$newTypeFieldsDef = $newType->getFields();
|
||||
foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) {
|
||||
if (!isset($newTypeFieldsDef[$fieldName])) {
|
||||
$breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_REMOVED, 'description' => "${typeName}->${fieldName} was removed."];
|
||||
} else {
|
||||
$oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
|
||||
$newfieldType = $newTypeFieldsDef[$fieldName]->getType();
|
||||
$isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg($oldFieldType, $newfieldType);
|
||||
if (!$isSafe) {
|
||||
$oldFieldTypeString = self::isNamedType($oldFieldType) ? $oldFieldType->name : $oldFieldType;
|
||||
$newFieldTypeString = self::isNamedType($newfieldType) ? $newfieldType->name : $newfieldType;
|
||||
$breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_FIELD_CHANGED, 'description' => "${typeName}->${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."];
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($newTypeFieldsDef as $fieldName => $fieldDef) {
|
||||
if (!isset($oldTypeFieldsDef[$fieldName]) && $fieldDef->getType() instanceof NonNull) {
|
||||
$newTypeName = $newType->name;
|
||||
$breakingFieldChanges[] = ['type' => self::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED, 'description' => "A non-null field ${fieldName} on input type ${newTypeName} was added."];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $breakingFieldChanges;
|
||||
|
||||
}
|
||||
|
||||
private static function isChangeSafeForObjectOrInterfaceField(
|
||||
Type $oldType, Type $newType
|
||||
)
|
||||
{
|
||||
if (self::isNamedType($oldType)) {
|
||||
// if they're both named types, see if their names are equivalent
|
||||
return (self::isNamedType($newType) && $oldType->name === $newType->name)
|
||||
// moving from nullable to non-null of the same underlying type is safe
|
||||
|| ($newType instanceof NonNull
|
||||
&& self::isChangeSafeForObjectOrInterfaceField(
|
||||
$oldType, $newType->getWrappedType()
|
||||
));
|
||||
} elseif ($oldType instanceof ListOfType) {
|
||||
// if they're both lists, make sure the underlying types are compatible
|
||||
return ($newType instanceof ListOfType &&
|
||||
self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType())) ||
|
||||
// moving from nullable to non-null of the same underlying type is safe
|
||||
($newType instanceof NonNull &&
|
||||
self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType()));
|
||||
} elseif ($oldType instanceof NonNull) {
|
||||
// if they're both non-null, make sure the underlying types are compatible
|
||||
return $newType instanceof NonNull &&
|
||||
self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Type $oldType
|
||||
* @param Schema $newSchema
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function isChangeSafeForInputObjectFieldOrFieldArg(
|
||||
Type $oldType, Type $newType
|
||||
)
|
||||
{
|
||||
if (self::isNamedType($oldType)) {
|
||||
return self::isNamedType($newType) && $oldType->name === $newType->name;
|
||||
} elseif ($oldType instanceof ListOfType) {
|
||||
return $newType instanceof ListOfType && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType->getWrappedType());
|
||||
} elseif ($oldType instanceof NonNull) {
|
||||
return (
|
||||
$newType instanceof NonNull && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType->getWrappedType())
|
||||
) || (
|
||||
!($newType instanceof NonNull) && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of any breaking
|
||||
* changes in the newSchema related to removing types from a union type.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function findTypesRemovedFromUnions(
|
||||
Schema $oldSchema, Schema $newSchema
|
||||
)
|
||||
{
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$typesRemovedFromUnion = [];
|
||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
|
||||
if (!($oldType instanceof UnionType) || !($newType instanceof UnionType)) {
|
||||
continue;
|
||||
}
|
||||
$typeNamesInNewUnion = [];
|
||||
foreach ($newType->getTypes() as $type) {
|
||||
$typeNamesInNewUnion[$type->name] = true;
|
||||
}
|
||||
foreach ($oldType->getTypes() as $type) {
|
||||
if (!isset($typeNamesInNewUnion[$type->name])) {
|
||||
$missingTypeName = $type->name;
|
||||
$typesRemovedFromUnion[] = ['type' => self::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION, 'description' => "${missingTypeName} was removed from union type ${typeName}."];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $typesRemovedFromUnion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of any dangerous
|
||||
* changes in the newSchema related to adding types to a union type.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function findTypesAddedToUnions(
|
||||
Schema $oldSchema, Schema $newSchema
|
||||
)
|
||||
{
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$typesAddedToUnion = [];
|
||||
|
||||
foreach ($newTypeMap as $typeName => $newType) {
|
||||
$oldType = isset($oldTypeMap[$typeName]) ? $oldTypeMap[$typeName] : null;
|
||||
if (!($oldType instanceof UnionType) || !($newType instanceof UnionType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$typeNamesInOldUnion = [];
|
||||
foreach ($oldType->getTypes() as $type) {
|
||||
$typeNamesInOldUnion[$type->name] = true;
|
||||
}
|
||||
foreach ($newType->getTypes() as $type) {
|
||||
if (!isset($typeNamesInOldUnion[$type->name])) {
|
||||
$addedTypeName = $type->name;
|
||||
$typesAddedToUnion[] = ['type' => self::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION, 'description' => "${addedTypeName} was added to union type ${typeName}"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $typesAddedToUnion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of any breaking
|
||||
* changes in the newSchema related to removing values from an enum type.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function findValuesRemovedFromEnums(
|
||||
Schema $oldSchema, Schema $newSchema
|
||||
)
|
||||
{
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$valuesRemovedFromEnums = [];
|
||||
|
||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
|
||||
if (!($oldType instanceof EnumType) || !($newType instanceof EnumType)) {
|
||||
continue;
|
||||
}
|
||||
$valuesInNewEnum = [];
|
||||
foreach ($newType->getValues() as $value) {
|
||||
$valuesInNewEnum[$value->name] = true;
|
||||
}
|
||||
foreach ($oldType->getValues() as $value) {
|
||||
if (!isset($valuesInNewEnum[$value->name])) {
|
||||
$valueName = $value->name;
|
||||
$valuesRemovedFromEnums[] = ['type' => self::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM, 'description' => "${valueName} was removed from enum type ${typeName}."];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $valuesRemovedFromEnums;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two schemas, returns an Array containing descriptions of any dangerous
|
||||
* changes in the newSchema related to adding values to an enum type.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function findValuesAddedToEnums(
|
||||
Schema $oldSchema, Schema $newSchema
|
||||
)
|
||||
{
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$valuesAddedToEnums = [];
|
||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
|
||||
if (!($oldType instanceof EnumType) || !($newType instanceof EnumType)) {
|
||||
continue;
|
||||
}
|
||||
$valuesInOldEnum = [];
|
||||
foreach ($oldType->getValues() as $value) {
|
||||
$valuesInOldEnum[$value->name] = true;
|
||||
}
|
||||
foreach ($newType->getValues() as $value) {
|
||||
if (!isset($valuesInOldEnum[$value->name])) {
|
||||
$valueName = $value->name;
|
||||
$valuesAddedToEnums[] = ['type' => self::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM, 'description' => "${valueName} was added to enum type ${typeName}"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $valuesAddedToEnums;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $oldSchema
|
||||
* @param Schema $newSchema
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function findInterfacesRemovedFromObjectTypes(
|
||||
Schema $oldSchema, Schema $newSchema
|
||||
)
|
||||
{
|
||||
$oldTypeMap = $oldSchema->getTypeMap();
|
||||
$newTypeMap = $newSchema->getTypeMap();
|
||||
|
||||
$breakingChanges = [];
|
||||
foreach ($oldTypeMap as $typeName => $oldType) {
|
||||
$newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null;
|
||||
if (!($oldType instanceof ObjectType) || !($newType instanceof ObjectType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldInterfaces = $oldType->getInterfaces();
|
||||
$newInterfaces = $newType->getInterfaces();
|
||||
foreach ($oldInterfaces as $oldInterface) {
|
||||
if (!Utils::find($newInterfaces, function (InterfaceType $interface) use ($oldInterface) {
|
||||
return $interface->name === $oldInterface->name;
|
||||
})) {
|
||||
$oldInterfaceName = $oldInterface->name;
|
||||
$breakingChanges[] = ['type' => self::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT,
|
||||
'description' => "${typeName} no longer implements interface ${oldInterfaceName}."
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $breakingChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Type $type
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function isNamedType(Type $type)
|
||||
{
|
||||
return (
|
||||
$type instanceof ScalarType ||
|
||||
$type instanceof ObjectType ||
|
||||
$type instanceof InterfaceType ||
|
||||
$type instanceof UnionType ||
|
||||
$type instanceof EnumType ||
|
||||
$type instanceof InputObjectType
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
|
||||
/**
|
||||
* A way to track interface implementations.
|
||||
*
|
||||
* Distinguishes between implementations by ObjectTypes and InterfaceTypes.
|
||||
*/
|
||||
class InterfaceImplementations
|
||||
{
|
||||
/** @var array<int, ObjectType> */
|
||||
private $objects;
|
||||
|
||||
/** @var array<int, InterfaceType> */
|
||||
private $interfaces;
|
||||
|
||||
/**
|
||||
* @param array<int, ObjectType> $objects
|
||||
* @param array<int, InterfaceType> $interfaces
|
||||
*/
|
||||
public function __construct(array $objects, array $interfaces)
|
||||
{
|
||||
$this->objects = $objects;
|
||||
$this->interfaces = $interfaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, ObjectType>
|
||||
*/
|
||||
public function objects() : array
|
||||
{
|
||||
return $this->objects;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, InterfaceType>
|
||||
*/
|
||||
public function interfaces() : array
|
||||
{
|
||||
return $this->interfaces;
|
||||
}
|
||||
}
|
||||
+90
-95
@@ -1,115 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use ArrayAccess;
|
||||
use GraphQL\Type\Definition\EnumValueDefinition;
|
||||
use InvalidArgumentException;
|
||||
use SplObjectStorage;
|
||||
use function array_key_exists;
|
||||
use function array_search;
|
||||
use function array_splice;
|
||||
use function is_array;
|
||||
use function is_float;
|
||||
use function is_int;
|
||||
use function is_object;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Similar to PHP array, but allows any type of data to act as key (including arrays, objects, scalars)
|
||||
*
|
||||
* Note: unfortunately when storing array as key - access and modification is O(N)
|
||||
* (yet this should be really rare case and should be avoided when possible)
|
||||
*
|
||||
* Class MixedStore
|
||||
* @package GraphQL\Utils
|
||||
* (yet this should rarely be the case and should be avoided when possible)
|
||||
*/
|
||||
class MixedStore implements \ArrayAccess
|
||||
class MixedStore implements ArrayAccess
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
/** @var EnumValueDefinition[] */
|
||||
private $standardStore;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
/** @var mixed[] */
|
||||
private $floatStore;
|
||||
|
||||
/**
|
||||
* @var \SplObjectStorage
|
||||
*/
|
||||
/** @var SplObjectStorage */
|
||||
private $objectStore;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
/** @var callable[] */
|
||||
private $arrayKeys;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
/** @var EnumValueDefinition[] */
|
||||
private $arrayValues;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
/** @var callable[] */
|
||||
private $lastArrayKey;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
/** @var mixed */
|
||||
private $lastArrayValue;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
/** @var mixed */
|
||||
private $nullValue;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
/** @var bool */
|
||||
private $nullValueIsSet;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
/** @var mixed */
|
||||
private $trueValue;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
/** @var bool */
|
||||
private $trueValueIsSet;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
/** @var mixed */
|
||||
private $falseValue;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
/** @var bool */
|
||||
private $falseValueIsSet;
|
||||
|
||||
/**
|
||||
* MixedStore constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->standardStore = [];
|
||||
$this->floatStore = [];
|
||||
$this->objectStore = new \SplObjectStorage();
|
||||
$this->arrayKeys = [];
|
||||
$this->arrayValues = [];
|
||||
$this->nullValueIsSet = false;
|
||||
$this->trueValueIsSet = false;
|
||||
$this->standardStore = [];
|
||||
$this->floatStore = [];
|
||||
$this->objectStore = new SplObjectStorage();
|
||||
$this->arrayKeys = [];
|
||||
$this->arrayValues = [];
|
||||
$this->nullValueIsSet = false;
|
||||
$this->trueValueIsSet = false;
|
||||
$this->falseValueIsSet = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a offset exists
|
||||
*
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
|
||||
*
|
||||
* @param mixed $offset <p>
|
||||
* An offset to check for.
|
||||
* </p>
|
||||
* @return boolean true on success or false on failure.
|
||||
*
|
||||
* @return bool true on success or false on failure.
|
||||
* </p>
|
||||
* <p>
|
||||
* The return value will be casted to boolean if non-boolean was returned.
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
if (false === $offset) {
|
||||
if ($offset === false) {
|
||||
return $this->falseValueIsSet;
|
||||
}
|
||||
if (true === $offset) {
|
||||
if ($offset === true) {
|
||||
return $this->trueValueIsSet;
|
||||
}
|
||||
if (is_int($offset) || is_string($offset)) {
|
||||
@@ -124,40 +110,44 @@ class MixedStore implements \ArrayAccess
|
||||
if (is_array($offset)) {
|
||||
foreach ($this->arrayKeys as $index => $entry) {
|
||||
if ($entry === $offset) {
|
||||
$this->lastArrayKey = $offset;
|
||||
$this->lastArrayKey = $offset;
|
||||
$this->lastArrayValue = $this->arrayValues[$index];
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (null === $offset) {
|
||||
if ($offset === null) {
|
||||
return $this->nullValueIsSet;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to retrieve
|
||||
*
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetget.php
|
||||
*
|
||||
* @param mixed $offset <p>
|
||||
* The offset to retrieve.
|
||||
* </p>
|
||||
*
|
||||
* @return mixed Can return all value types.
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
if (true === $offset) {
|
||||
if ($offset === true) {
|
||||
return $this->trueValue;
|
||||
}
|
||||
if (false === $offset) {
|
||||
if ($offset === false) {
|
||||
return $this->falseValue;
|
||||
}
|
||||
if (is_int($offset) || is_string($offset)) {
|
||||
return $this->standardStore[$offset];
|
||||
}
|
||||
if (is_float($offset)) {
|
||||
return $this->floatStore[(string)$offset];
|
||||
return $this->floatStore[(string) $offset];
|
||||
}
|
||||
if (is_object($offset)) {
|
||||
return $this->objectStore->offsetGet($offset);
|
||||
@@ -173,81 +163,86 @@ class MixedStore implements \ArrayAccess
|
||||
}
|
||||
}
|
||||
}
|
||||
if (null === $offset) {
|
||||
if ($offset === null) {
|
||||
return $this->nullValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to set
|
||||
*
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetset.php
|
||||
*
|
||||
* @param mixed $offset <p>
|
||||
* The offset to assign the value to.
|
||||
* </p>
|
||||
* @param mixed $value <p>
|
||||
* The value to set.
|
||||
* </p>
|
||||
* @param mixed $value <p>
|
||||
* The value to set.
|
||||
* </p>
|
||||
*
|
||||
* @return void
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
if (false === $offset) {
|
||||
$this->falseValue = $value;
|
||||
if ($offset === false) {
|
||||
$this->falseValue = $value;
|
||||
$this->falseValueIsSet = true;
|
||||
} else if (true === $offset) {
|
||||
$this->trueValue = $value;
|
||||
} elseif ($offset === true) {
|
||||
$this->trueValue = $value;
|
||||
$this->trueValueIsSet = true;
|
||||
} else if (is_int($offset) || is_string($offset)) {
|
||||
} elseif (is_int($offset) || is_string($offset)) {
|
||||
$this->standardStore[$offset] = $value;
|
||||
} else if (is_float($offset)) {
|
||||
$this->floatStore[(string)$offset] = $value;
|
||||
} else if (is_object($offset)) {
|
||||
} elseif (is_float($offset)) {
|
||||
$this->floatStore[(string) $offset] = $value;
|
||||
} elseif (is_object($offset)) {
|
||||
$this->objectStore[$offset] = $value;
|
||||
} else if (is_array($offset)) {
|
||||
$this->arrayKeys[] = $offset;
|
||||
} elseif (is_array($offset)) {
|
||||
$this->arrayKeys[] = $offset;
|
||||
$this->arrayValues[] = $value;
|
||||
} else if (null === $offset) {
|
||||
$this->nullValue = $value;
|
||||
} elseif ($offset === null) {
|
||||
$this->nullValue = $value;
|
||||
$this->nullValueIsSet = true;
|
||||
} else {
|
||||
throw new \InvalidArgumentException("Unexpected offset type: " . Utils::printSafe($offset));
|
||||
throw new InvalidArgumentException('Unexpected offset type: ' . Utils::printSafe($offset));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset to unset
|
||||
*
|
||||
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
|
||||
*
|
||||
* @param mixed $offset <p>
|
||||
* The offset to unset.
|
||||
* </p>
|
||||
*
|
||||
* @return void
|
||||
* @since 5.0.0
|
||||
*/
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
if (true === $offset) {
|
||||
$this->trueValue = null;
|
||||
if ($offset === true) {
|
||||
$this->trueValue = null;
|
||||
$this->trueValueIsSet = false;
|
||||
} else if (false === $offset) {
|
||||
$this->falseValue = null;
|
||||
} elseif ($offset === false) {
|
||||
$this->falseValue = null;
|
||||
$this->falseValueIsSet = false;
|
||||
} else if (is_int($offset) || is_string($offset)) {
|
||||
} elseif (is_int($offset) || is_string($offset)) {
|
||||
unset($this->standardStore[$offset]);
|
||||
} else if (is_float($offset)) {
|
||||
unset($this->floatStore[(string)$offset]);
|
||||
} else if (is_object($offset)) {
|
||||
} elseif (is_float($offset)) {
|
||||
unset($this->floatStore[(string) $offset]);
|
||||
} elseif (is_object($offset)) {
|
||||
$this->objectStore->offsetUnset($offset);
|
||||
} else if (is_array($offset)) {
|
||||
} elseif (is_array($offset)) {
|
||||
$index = array_search($offset, $this->arrayKeys, true);
|
||||
|
||||
if (false !== $index) {
|
||||
if ($index !== false) {
|
||||
array_splice($this->arrayKeys, $index, 1);
|
||||
array_splice($this->arrayValues, $index, 1);
|
||||
}
|
||||
} else if (null === $offset) {
|
||||
$this->nullValue = null;
|
||||
} elseif ($offset === null) {
|
||||
$this->nullValue = null;
|
||||
$this->nullValueIsSet = false;
|
||||
}
|
||||
}
|
||||
|
||||
+40
-61
@@ -1,87 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
/**
|
||||
* Class PairSet
|
||||
* @package GraphQL\Utils
|
||||
* A way to keep track of pairs of things when the ordering of the pair does
|
||||
* not matter. We do this by maintaining a sort of double adjacency sets.
|
||||
*/
|
||||
class PairSet
|
||||
{
|
||||
/**
|
||||
* @var \SplObjectStorage<any, Set<any>>
|
||||
*/
|
||||
/** @var bool[][] */
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $wrappers = [];
|
||||
|
||||
/**
|
||||
* PairSet constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->data = new \SplObjectStorage(); // SplObject hash instead?
|
||||
$this->data = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $a
|
||||
* @param $b
|
||||
* @return null|object
|
||||
* @param string $a
|
||||
* @param string $b
|
||||
* @param bool $areMutuallyExclusive
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($a, $b)
|
||||
public function has($a, $b, $areMutuallyExclusive)
|
||||
{
|
||||
$a = $this->toObj($a);
|
||||
$b = $this->toObj($b);
|
||||
|
||||
/** @var \SplObjectStorage $first */
|
||||
$first = isset($this->data[$a]) ? $this->data[$a] : null;
|
||||
return isset($first, $first[$b]) ? $first[$b] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $a
|
||||
* @param $b
|
||||
*/
|
||||
public function add($a, $b)
|
||||
{
|
||||
$this->pairSetAdd($a, $b);
|
||||
$this->pairSetAdd($b, $a);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $var
|
||||
* @return mixed
|
||||
*/
|
||||
private function toObj($var)
|
||||
{
|
||||
// SplObjectStorage expects objects, so wrapping non-objects to objects
|
||||
if (is_object($var)) {
|
||||
return $var;
|
||||
$first = $this->data[$a] ?? null;
|
||||
$result = $first && isset($first[$b]) ? $first[$b] : null;
|
||||
if ($result === null) {
|
||||
return false;
|
||||
}
|
||||
if (!isset($this->wrappers[$var])) {
|
||||
$tmp = new \stdClass();
|
||||
$tmp->_internal = $var;
|
||||
$this->wrappers[$var] = $tmp;
|
||||
// areMutuallyExclusive being false is a superset of being true,
|
||||
// hence if we want to know if this PairSet "has" these two with no
|
||||
// exclusivity, we have to ensure it was added as such.
|
||||
if ($areMutuallyExclusive === false) {
|
||||
return $result === false;
|
||||
}
|
||||
return $this->wrappers[$var];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $a
|
||||
* @param $b
|
||||
* @param string $a
|
||||
* @param string $b
|
||||
* @param bool $areMutuallyExclusive
|
||||
*/
|
||||
private function pairSetAdd($a, $b)
|
||||
public function add($a, $b, $areMutuallyExclusive)
|
||||
{
|
||||
$a = $this->toObj($a);
|
||||
$b = $this->toObj($b);
|
||||
$set = isset($this->data[$a]) ? $this->data[$a] : null;
|
||||
$this->pairSetAdd($a, $b, $areMutuallyExclusive);
|
||||
$this->pairSetAdd($b, $a, $areMutuallyExclusive);
|
||||
}
|
||||
|
||||
if (!isset($set)) {
|
||||
$set = new \SplObjectStorage();
|
||||
$this->data[$a] = $set;
|
||||
}
|
||||
$set[$b] = true;
|
||||
/**
|
||||
* @param string $a
|
||||
* @param string $b
|
||||
* @param bool $areMutuallyExclusive
|
||||
*/
|
||||
private function pairSetAdd($a, $b, $areMutuallyExclusive)
|
||||
{
|
||||
$this->data[$a] = $this->data[$a] ?? [];
|
||||
$this->data[$a][$b] = $areMutuallyExclusive;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,644 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\AST\EnumTypeExtensionNode;
|
||||
use GraphQL\Language\AST\InputObjectTypeExtensionNode;
|
||||
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\ObjectTypeExtensionNode;
|
||||
use GraphQL\Language\AST\SchemaDefinitionNode;
|
||||
use GraphQL\Language\AST\SchemaTypeExtensionNode;
|
||||
use GraphQL\Language\AST\TypeDefinitionNode;
|
||||
use GraphQL\Language\AST\TypeExtensionNode;
|
||||
use GraphQL\Language\AST\UnionTypeExtensionNode;
|
||||
use GraphQL\Type\Definition\CustomScalarType;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\EnumValueDefinition;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
use GraphQL\Type\Definition\ImplementingType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NamedType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Introspection;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function count;
|
||||
|
||||
class SchemaExtender
|
||||
{
|
||||
const SCHEMA_EXTENSION = 'SchemaExtension';
|
||||
|
||||
/** @var Type[] */
|
||||
protected static $extendTypeCache;
|
||||
|
||||
/** @var mixed[] */
|
||||
protected static $typeExtensionsMap;
|
||||
|
||||
/** @var ASTDefinitionBuilder */
|
||||
protected static $astBuilder;
|
||||
|
||||
/**
|
||||
* @return TypeExtensionNode[]|null
|
||||
*/
|
||||
protected static function getExtensionASTNodes(NamedType $type) : ?array
|
||||
{
|
||||
if (! $type instanceof Type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = $type->name;
|
||||
if ($type->extensionASTNodes !== null) {
|
||||
if (isset(static::$typeExtensionsMap[$name])) {
|
||||
return array_merge($type->extensionASTNodes, static::$typeExtensionsMap[$name]);
|
||||
}
|
||||
|
||||
return $type->extensionASTNodes;
|
||||
}
|
||||
|
||||
return static::$typeExtensionsMap[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
protected static function checkExtensionNode(Type $type, Node $node) : void
|
||||
{
|
||||
switch (true) {
|
||||
case $node instanceof ObjectTypeExtensionNode:
|
||||
if (! ($type instanceof ObjectType)) {
|
||||
throw new Error(
|
||||
'Cannot extend non-object type "' . $type->name . '".',
|
||||
[$node]
|
||||
);
|
||||
}
|
||||
break;
|
||||
case $node instanceof InterfaceTypeExtensionNode:
|
||||
if (! ($type instanceof InterfaceType)) {
|
||||
throw new Error(
|
||||
'Cannot extend non-interface type "' . $type->name . '".',
|
||||
[$node]
|
||||
);
|
||||
}
|
||||
break;
|
||||
case $node instanceof EnumTypeExtensionNode:
|
||||
if (! ($type instanceof EnumType)) {
|
||||
throw new Error(
|
||||
'Cannot extend non-enum type "' . $type->name . '".',
|
||||
[$node]
|
||||
);
|
||||
}
|
||||
break;
|
||||
case $node instanceof UnionTypeExtensionNode:
|
||||
if (! ($type instanceof UnionType)) {
|
||||
throw new Error(
|
||||
'Cannot extend non-union type "' . $type->name . '".',
|
||||
[$node]
|
||||
);
|
||||
}
|
||||
break;
|
||||
case $node instanceof InputObjectTypeExtensionNode:
|
||||
if (! ($type instanceof InputObjectType)) {
|
||||
throw new Error(
|
||||
'Cannot extend non-input object type "' . $type->name . '".',
|
||||
[$node]
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected static function extendScalarType(ScalarType $type) : CustomScalarType
|
||||
{
|
||||
return new CustomScalarType([
|
||||
'name' => $type->name,
|
||||
'description' => $type->description,
|
||||
'astNode' => $type->astNode,
|
||||
'serialize' => $type->config['serialize'] ?? null,
|
||||
'parseValue' => $type->config['parseValue'] ?? null,
|
||||
'parseLiteral' => $type->config['parseLiteral'] ?? null,
|
||||
'extensionASTNodes' => static::getExtensionASTNodes($type),
|
||||
]);
|
||||
}
|
||||
|
||||
protected static function extendUnionType(UnionType $type) : UnionType
|
||||
{
|
||||
return new UnionType([
|
||||
'name' => $type->name,
|
||||
'description' => $type->description,
|
||||
'types' => static function () use ($type) : array {
|
||||
return static::extendPossibleTypes($type);
|
||||
},
|
||||
'astNode' => $type->astNode,
|
||||
'resolveType' => $type->config['resolveType'] ?? null,
|
||||
'extensionASTNodes' => static::getExtensionASTNodes($type),
|
||||
]);
|
||||
}
|
||||
|
||||
protected static function extendEnumType(EnumType $type) : EnumType
|
||||
{
|
||||
return new EnumType([
|
||||
'name' => $type->name,
|
||||
'description' => $type->description,
|
||||
'values' => static::extendValueMap($type),
|
||||
'astNode' => $type->astNode,
|
||||
'extensionASTNodes' => static::getExtensionASTNodes($type),
|
||||
]);
|
||||
}
|
||||
|
||||
protected static function extendInputObjectType(InputObjectType $type) : InputObjectType
|
||||
{
|
||||
return new InputObjectType([
|
||||
'name' => $type->name,
|
||||
'description' => $type->description,
|
||||
'fields' => static function () use ($type) : array {
|
||||
return static::extendInputFieldMap($type);
|
||||
},
|
||||
'astNode' => $type->astNode,
|
||||
'extensionASTNodes' => static::getExtensionASTNodes($type),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected static function extendInputFieldMap(InputObjectType $type) : array
|
||||
{
|
||||
$newFieldMap = [];
|
||||
$oldFieldMap = $type->getFields();
|
||||
foreach ($oldFieldMap as $fieldName => $field) {
|
||||
$newFieldMap[$fieldName] = [
|
||||
'description' => $field->description,
|
||||
'type' => static::extendType($field->getType()),
|
||||
'astNode' => $field->astNode,
|
||||
];
|
||||
|
||||
if (! $field->defaultValueExists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$newFieldMap[$fieldName]['defaultValue'] = $field->defaultValue;
|
||||
}
|
||||
|
||||
$extensions = static::$typeExtensionsMap[$type->name] ?? null;
|
||||
if ($extensions !== null) {
|
||||
foreach ($extensions as $extension) {
|
||||
foreach ($extension->fields as $field) {
|
||||
$fieldName = $field->name->value;
|
||||
if (isset($oldFieldMap[$fieldName])) {
|
||||
throw new Error('Field "' . $type->name . '.' . $fieldName . '" already exists in the schema. It cannot also be defined in this type extension.', [$field]);
|
||||
}
|
||||
|
||||
$newFieldMap[$fieldName] = static::$astBuilder->buildInputField($field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $newFieldMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected static function extendValueMap(EnumType $type) : array
|
||||
{
|
||||
$newValueMap = [];
|
||||
/** @var EnumValueDefinition[] $oldValueMap */
|
||||
$oldValueMap = [];
|
||||
foreach ($type->getValues() as $value) {
|
||||
$oldValueMap[$value->name] = $value;
|
||||
}
|
||||
|
||||
foreach ($oldValueMap as $key => $value) {
|
||||
$newValueMap[$key] = [
|
||||
'name' => $value->name,
|
||||
'description' => $value->description,
|
||||
'value' => $value->value,
|
||||
'deprecationReason' => $value->deprecationReason,
|
||||
'astNode' => $value->astNode,
|
||||
];
|
||||
}
|
||||
|
||||
$extensions = static::$typeExtensionsMap[$type->name] ?? null;
|
||||
if ($extensions !== null) {
|
||||
foreach ($extensions as $extension) {
|
||||
foreach ($extension->values as $value) {
|
||||
$valueName = $value->name->value;
|
||||
if (isset($oldValueMap[$valueName])) {
|
||||
throw new Error('Enum value "' . $type->name . '.' . $valueName . '" already exists in the schema. It cannot also be defined in this type extension.', [$value]);
|
||||
}
|
||||
$newValueMap[$valueName] = static::$astBuilder->buildEnumValue($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $newValueMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ObjectType[]
|
||||
*/
|
||||
protected static function extendPossibleTypes(UnionType $type) : array
|
||||
{
|
||||
$possibleTypes = array_map(static function ($type) {
|
||||
return static::extendNamedType($type);
|
||||
}, $type->getTypes());
|
||||
|
||||
$extensions = static::$typeExtensionsMap[$type->name] ?? null;
|
||||
if ($extensions !== null) {
|
||||
foreach ($extensions as $extension) {
|
||||
foreach ($extension->types as $namedType) {
|
||||
$possibleTypes[] = static::$astBuilder->buildType($namedType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $possibleTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType|InterfaceType $type
|
||||
*
|
||||
* @return array<int, InterfaceType>
|
||||
*/
|
||||
protected static function extendImplementedInterfaces(ImplementingType $type) : array
|
||||
{
|
||||
$interfaces = array_map(static function (InterfaceType $interfaceType) {
|
||||
return static::extendNamedType($interfaceType);
|
||||
}, $type->getInterfaces());
|
||||
|
||||
$extensions = static::$typeExtensionsMap[$type->name] ?? null;
|
||||
if ($extensions !== null) {
|
||||
/** @var ObjectTypeExtensionNode|InterfaceTypeExtensionNode $extension */
|
||||
foreach ($extensions as $extension) {
|
||||
foreach ($extension->interfaces as $namedType) {
|
||||
$interfaces[] = static::$astBuilder->buildType($namedType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $interfaces;
|
||||
}
|
||||
|
||||
protected static function extendType($typeDef)
|
||||
{
|
||||
if ($typeDef instanceof ListOfType) {
|
||||
return Type::listOf(static::extendType($typeDef->getOfType()));
|
||||
}
|
||||
|
||||
if ($typeDef instanceof NonNull) {
|
||||
return Type::nonNull(static::extendType($typeDef->getWrappedType()));
|
||||
}
|
||||
|
||||
return static::extendNamedType($typeDef);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FieldArgument[] $args
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected static function extendArgs(array $args) : array
|
||||
{
|
||||
return Utils::keyValMap(
|
||||
$args,
|
||||
static function (FieldArgument $arg) : string {
|
||||
return $arg->name;
|
||||
},
|
||||
static function (FieldArgument $arg) : array {
|
||||
$def = [
|
||||
'type' => static::extendType($arg->getType()),
|
||||
'description' => $arg->description,
|
||||
'astNode' => $arg->astNode,
|
||||
];
|
||||
|
||||
if ($arg->defaultValueExists()) {
|
||||
$def['defaultValue'] = $arg->defaultValue;
|
||||
}
|
||||
|
||||
return $def;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InterfaceType|ObjectType $type
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
protected static function extendFieldMap($type) : array
|
||||
{
|
||||
$newFieldMap = [];
|
||||
$oldFieldMap = $type->getFields();
|
||||
|
||||
foreach (array_keys($oldFieldMap) as $fieldName) {
|
||||
$field = $oldFieldMap[$fieldName];
|
||||
|
||||
$newFieldMap[$fieldName] = [
|
||||
'name' => $fieldName,
|
||||
'description' => $field->description,
|
||||
'deprecationReason' => $field->deprecationReason,
|
||||
'type' => static::extendType($field->getType()),
|
||||
'args' => static::extendArgs($field->args),
|
||||
'astNode' => $field->astNode,
|
||||
'resolve' => $field->resolveFn,
|
||||
];
|
||||
}
|
||||
|
||||
$extensions = static::$typeExtensionsMap[$type->name] ?? null;
|
||||
if ($extensions !== null) {
|
||||
foreach ($extensions as $extension) {
|
||||
foreach ($extension->fields as $field) {
|
||||
$fieldName = $field->name->value;
|
||||
if (isset($oldFieldMap[$fieldName])) {
|
||||
throw new Error('Field "' . $type->name . '.' . $fieldName . '" already exists in the schema. It cannot also be defined in this type extension.', [$field]);
|
||||
}
|
||||
|
||||
$newFieldMap[$fieldName] = static::$astBuilder->buildField($field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $newFieldMap;
|
||||
}
|
||||
|
||||
protected static function extendObjectType(ObjectType $type) : ObjectType
|
||||
{
|
||||
return new ObjectType([
|
||||
'name' => $type->name,
|
||||
'description' => $type->description,
|
||||
'interfaces' => static function () use ($type) : array {
|
||||
return static::extendImplementedInterfaces($type);
|
||||
},
|
||||
'fields' => static function () use ($type) : array {
|
||||
return static::extendFieldMap($type);
|
||||
},
|
||||
'astNode' => $type->astNode,
|
||||
'extensionASTNodes' => static::getExtensionASTNodes($type),
|
||||
'isTypeOf' => $type->config['isTypeOf'] ?? null,
|
||||
'resolveField' => $type->resolveFieldFn ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
protected static function extendInterfaceType(InterfaceType $type) : InterfaceType
|
||||
{
|
||||
return new InterfaceType([
|
||||
'name' => $type->name,
|
||||
'description' => $type->description,
|
||||
'interfaces' => static function () use ($type) : array {
|
||||
return static::extendImplementedInterfaces($type);
|
||||
},
|
||||
'fields' => static function () use ($type) : array {
|
||||
return static::extendFieldMap($type);
|
||||
},
|
||||
'astNode' => $type->astNode,
|
||||
'extensionASTNodes' => static::getExtensionASTNodes($type),
|
||||
'resolveType' => $type->config['resolveType'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
protected static function isSpecifiedScalarType(Type $type) : bool
|
||||
{
|
||||
return $type instanceof NamedType &&
|
||||
(
|
||||
$type->name === Type::STRING ||
|
||||
$type->name === Type::INT ||
|
||||
$type->name === Type::FLOAT ||
|
||||
$type->name === Type::BOOLEAN ||
|
||||
$type->name === Type::ID
|
||||
);
|
||||
}
|
||||
|
||||
protected static function extendNamedType(Type $type)
|
||||
{
|
||||
if (Introspection::isIntrospectionType($type) || static::isSpecifiedScalarType($type)) {
|
||||
return $type;
|
||||
}
|
||||
|
||||
$name = $type->name;
|
||||
if (! isset(static::$extendTypeCache[$name])) {
|
||||
if ($type instanceof ScalarType) {
|
||||
static::$extendTypeCache[$name] = static::extendScalarType($type);
|
||||
} elseif ($type instanceof ObjectType) {
|
||||
static::$extendTypeCache[$name] = static::extendObjectType($type);
|
||||
} elseif ($type instanceof InterfaceType) {
|
||||
static::$extendTypeCache[$name] = static::extendInterfaceType($type);
|
||||
} elseif ($type instanceof UnionType) {
|
||||
static::$extendTypeCache[$name] = static::extendUnionType($type);
|
||||
} elseif ($type instanceof EnumType) {
|
||||
static::$extendTypeCache[$name] = static::extendEnumType($type);
|
||||
} elseif ($type instanceof InputObjectType) {
|
||||
static::$extendTypeCache[$name] = static::extendInputObjectType($type);
|
||||
}
|
||||
}
|
||||
|
||||
return static::$extendTypeCache[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed|null
|
||||
*/
|
||||
protected static function extendMaybeNamedType(?NamedType $type = null)
|
||||
{
|
||||
if ($type !== null) {
|
||||
return static::extendNamedType($type);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DirectiveDefinitionNode[] $directiveDefinitions
|
||||
*
|
||||
* @return Directive[]
|
||||
*/
|
||||
protected static function getMergedDirectives(Schema $schema, array $directiveDefinitions) : array
|
||||
{
|
||||
$existingDirectives = array_map(static function (Directive $directive) : Directive {
|
||||
return static::extendDirective($directive);
|
||||
}, $schema->getDirectives());
|
||||
|
||||
Utils::invariant(count($existingDirectives) > 0, 'schema must have default directives');
|
||||
|
||||
return array_merge(
|
||||
$existingDirectives,
|
||||
array_map(static function (DirectiveDefinitionNode $directive) : Directive {
|
||||
return static::$astBuilder->buildDirective($directive);
|
||||
}, $directiveDefinitions)
|
||||
);
|
||||
}
|
||||
|
||||
protected static function extendDirective(Directive $directive) : Directive
|
||||
{
|
||||
return new Directive([
|
||||
'name' => $directive->name,
|
||||
'description' => $directive->description,
|
||||
'locations' => $directive->locations,
|
||||
'args' => static::extendArgs($directive->args),
|
||||
'astNode' => $directive->astNode,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
public static function extend(Schema $schema, DocumentNode $documentAST, array $options = []) : Schema
|
||||
{
|
||||
if (! (isset($options['assumeValid']) || isset($options['assumeValidSDL']))) {
|
||||
DocumentValidator::assertValidSDLExtension($documentAST, $schema);
|
||||
}
|
||||
|
||||
/** @var array<string, Node&TypeDefinitionNode> $typeDefinitionMap */
|
||||
$typeDefinitionMap = [];
|
||||
static::$typeExtensionsMap = [];
|
||||
$directiveDefinitions = [];
|
||||
/** @var SchemaDefinitionNode|null $schemaDef */
|
||||
$schemaDef = null;
|
||||
/** @var array<int, SchemaTypeExtensionNode> $schemaExtensions */
|
||||
$schemaExtensions = [];
|
||||
|
||||
$definitionsCount = count($documentAST->definitions);
|
||||
for ($i = 0; $i < $definitionsCount; $i++) {
|
||||
|
||||
/** @var Node $def */
|
||||
$def = $documentAST->definitions[$i];
|
||||
|
||||
if ($def instanceof SchemaDefinitionNode) {
|
||||
$schemaDef = $def;
|
||||
} elseif ($def instanceof SchemaTypeExtensionNode) {
|
||||
$schemaExtensions[] = $def;
|
||||
} elseif ($def instanceof TypeDefinitionNode) {
|
||||
$typeName = isset($def->name) ? $def->name->value : null;
|
||||
|
||||
try {
|
||||
$type = $schema->getType($typeName);
|
||||
} catch (Error $error) {
|
||||
$type = null;
|
||||
}
|
||||
|
||||
if ($type) {
|
||||
throw new Error('Type "' . $typeName . '" already exists in the schema. It cannot also be defined in this type definition.', [$def]);
|
||||
}
|
||||
$typeDefinitionMap[$typeName] = $def;
|
||||
} elseif ($def instanceof TypeExtensionNode) {
|
||||
$extendedTypeName = isset($def->name) ? $def->name->value : null;
|
||||
$existingType = $schema->getType($extendedTypeName);
|
||||
if ($existingType === null) {
|
||||
throw new Error('Cannot extend type "' . $extendedTypeName . '" because it does not exist in the existing schema.', [$def]);
|
||||
}
|
||||
|
||||
static::checkExtensionNode($existingType, $def);
|
||||
|
||||
$existingTypeExtensions = static::$typeExtensionsMap[$extendedTypeName] ?? null;
|
||||
static::$typeExtensionsMap[$extendedTypeName] = $existingTypeExtensions !== null ? array_merge($existingTypeExtensions, [$def]) : [$def];
|
||||
} elseif ($def instanceof DirectiveDefinitionNode) {
|
||||
$directiveName = $def->name->value;
|
||||
$existingDirective = $schema->getDirective($directiveName);
|
||||
if ($existingDirective !== null) {
|
||||
throw new Error('Directive "' . $directiveName . '" already exists in the schema. It cannot be redefined.', [$def]);
|
||||
}
|
||||
$directiveDefinitions[] = $def;
|
||||
}
|
||||
}
|
||||
|
||||
if (count(static::$typeExtensionsMap) === 0
|
||||
&& count($typeDefinitionMap) === 0
|
||||
&& count($directiveDefinitions) === 0
|
||||
&& count($schemaExtensions) === 0
|
||||
&& $schemaDef === null
|
||||
) {
|
||||
return $schema;
|
||||
}
|
||||
|
||||
static::$astBuilder = new ASTDefinitionBuilder(
|
||||
$typeDefinitionMap,
|
||||
$options,
|
||||
static function (string $typeName) use ($schema) {
|
||||
/** @var ScalarType|ObjectType|InterfaceType|UnionType|EnumType|InputObjectType $existingType */
|
||||
$existingType = $schema->getType($typeName);
|
||||
if ($existingType !== null) {
|
||||
return static::extendNamedType($existingType);
|
||||
}
|
||||
|
||||
throw new Error('Unknown type: "' . $typeName . '". Ensure that this type exists either in the original schema, or is added in a type definition.', [$typeName]);
|
||||
}
|
||||
);
|
||||
|
||||
static::$extendTypeCache = [];
|
||||
|
||||
$operationTypes = [
|
||||
'query' => static::extendMaybeNamedType($schema->getQueryType()),
|
||||
'mutation' => static::extendMaybeNamedType($schema->getMutationType()),
|
||||
'subscription' => static::extendMaybeNamedType($schema->getSubscriptionType()),
|
||||
];
|
||||
|
||||
if ($schemaDef) {
|
||||
foreach ($schemaDef->operationTypes as $operationType) {
|
||||
$operation = $operationType->operation;
|
||||
$type = $operationType->type;
|
||||
|
||||
if (isset($operationTypes[$operation])) {
|
||||
throw new Error('Must provide only one ' . $operation . ' type in schema.');
|
||||
}
|
||||
|
||||
$operationTypes[$operation] = static::$astBuilder->buildType($type);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($schemaExtensions as $schemaExtension) {
|
||||
if ($schemaExtension->operationTypes === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($schemaExtension->operationTypes as $operationType) {
|
||||
$operation = $operationType->operation;
|
||||
if (isset($operationTypes[$operation])) {
|
||||
throw new Error('Must provide only one ' . $operation . ' type in schema.');
|
||||
}
|
||||
$operationTypes[$operation] = static::$astBuilder->buildType($operationType->type);
|
||||
}
|
||||
}
|
||||
|
||||
$schemaExtensionASTNodes = array_merge($schema->extensionASTNodes, $schemaExtensions);
|
||||
|
||||
$types = array_merge(
|
||||
// Iterate through all types, getting the type definition for each, ensuring
|
||||
// that any type not directly referenced by a field will get created.
|
||||
array_map(static function (Type $type) : Type {
|
||||
return static::extendNamedType($type);
|
||||
}, $schema->getTypeMap()),
|
||||
// Do the same with new types.
|
||||
array_map(static function (TypeDefinitionNode $type) : Type {
|
||||
return static::$astBuilder->buildType($type);
|
||||
}, $typeDefinitionMap)
|
||||
);
|
||||
|
||||
return new Schema([
|
||||
'query' => $operationTypes['query'],
|
||||
'mutation' => $operationTypes['mutation'],
|
||||
'subscription' => $operationTypes['subscription'],
|
||||
'types' => $types,
|
||||
'directives' => static::getMergedDirectives($schema, $directiveDefinitions),
|
||||
'astNode' => $schema->getAstNode(),
|
||||
'extensionASTNodes' => $schemaExtensionASTNodes,
|
||||
]);
|
||||
}
|
||||
}
|
||||
+387
-192
@@ -1,9 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Type\Definition\CompositeType;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
@@ -11,7 +14,23 @@ use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Introspection;
|
||||
use GraphQL\Type\Schema;
|
||||
use function array_filter;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function implode;
|
||||
use function ksort;
|
||||
use function mb_strlen;
|
||||
use function preg_match_all;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* Given an instance of Schema, prints it in GraphQL type language.
|
||||
@@ -19,100 +38,90 @@ use GraphQL\Type\Definition\Directive;
|
||||
class SchemaPrinter
|
||||
{
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
* Available options:
|
||||
* - commentDescriptions:
|
||||
* Provide true to use preceding comments as the description.
|
||||
* This option is provided to ease adoption and will be removed in v16.
|
||||
*
|
||||
* @api
|
||||
* @param Schema $schema
|
||||
* @return string
|
||||
*/
|
||||
public static function doPrint(Schema $schema)
|
||||
public static function doPrint(Schema $schema, array $options = []) : string
|
||||
{
|
||||
return self::printFilteredSchema($schema, function($n) {
|
||||
return !self::isSpecDirective($n);
|
||||
}, 'self::isDefinedType');
|
||||
return static::printFilteredSchema(
|
||||
$schema,
|
||||
static function ($type) : bool {
|
||||
return ! Directive::isSpecifiedDirective($type);
|
||||
},
|
||||
static function ($type) : bool {
|
||||
return ! Type::isBuiltInType($type);
|
||||
},
|
||||
$options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @param Schema $schema
|
||||
* @return string
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
public static function printIntrosepctionSchema(Schema $schema)
|
||||
protected static function printFilteredSchema(Schema $schema, callable $directiveFilter, callable $typeFilter, array $options) : string
|
||||
{
|
||||
return self::printFilteredSchema($schema, [__CLASS__, 'isSpecDirective'], [__CLASS__, 'isIntrospectionType']);
|
||||
}
|
||||
$directives = array_filter($schema->getDirectives(), $directiveFilter);
|
||||
|
||||
private static function isSpecDirective($directiveName)
|
||||
{
|
||||
return (
|
||||
$directiveName === 'skip' ||
|
||||
$directiveName === 'include' ||
|
||||
$directiveName === 'deprecated'
|
||||
$types = $schema->getTypeMap();
|
||||
ksort($types);
|
||||
$types = array_filter($types, $typeFilter);
|
||||
|
||||
return sprintf(
|
||||
"%s\n",
|
||||
implode(
|
||||
"\n\n",
|
||||
array_filter(
|
||||
array_merge(
|
||||
[static::printSchemaDefinition($schema)],
|
||||
array_map(
|
||||
static function (Directive $directive) use ($options) : string {
|
||||
return static::printDirective($directive, $options);
|
||||
},
|
||||
$directives
|
||||
),
|
||||
array_map(
|
||||
static function ($type) use ($options) : string {
|
||||
return static::printType($type, $options);
|
||||
},
|
||||
$types
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static function isDefinedType($typename)
|
||||
protected static function printSchemaDefinition(Schema $schema) : string
|
||||
{
|
||||
return !self::isIntrospectionType($typename) && !self::isBuiltInScalar($typename);
|
||||
}
|
||||
|
||||
private static function isIntrospectionType($typename)
|
||||
{
|
||||
return strpos($typename, '__') === 0;
|
||||
}
|
||||
|
||||
private static function isBuiltInScalar($typename)
|
||||
{
|
||||
return (
|
||||
$typename === Type::STRING ||
|
||||
$typename === Type::BOOLEAN ||
|
||||
$typename === Type::INT ||
|
||||
$typename === Type::FLOAT ||
|
||||
$typename === Type::ID
|
||||
);
|
||||
}
|
||||
|
||||
private static function printFilteredSchema(Schema $schema, $directiveFilter, $typeFilter)
|
||||
{
|
||||
$directives = array_filter($schema->getDirectives(), function($directive) use ($directiveFilter) {
|
||||
return $directiveFilter($directive->name);
|
||||
});
|
||||
$typeMap = $schema->getTypeMap();
|
||||
$types = array_filter(array_keys($typeMap), $typeFilter);
|
||||
sort($types);
|
||||
$types = array_map(function($typeName) use ($typeMap) { return $typeMap[$typeName]; }, $types);
|
||||
|
||||
return implode("\n\n", array_filter(array_merge(
|
||||
[self::printSchemaDefinition($schema)],
|
||||
array_map('self::printDirective', $directives),
|
||||
array_map('self::printType', $types)
|
||||
))) . "\n";
|
||||
}
|
||||
|
||||
private static function printSchemaDefinition(Schema $schema)
|
||||
{
|
||||
if (self::isSchemaOfCommonNames($schema)) {
|
||||
return;
|
||||
if (static::isSchemaOfCommonNames($schema)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$operationTypes = [];
|
||||
|
||||
$queryType = $schema->getQueryType();
|
||||
if ($queryType) {
|
||||
$operationTypes[] = " query: {$queryType->name}";
|
||||
if ($queryType !== null) {
|
||||
$operationTypes[] = sprintf(' query: %s', $queryType->name);
|
||||
}
|
||||
|
||||
$mutationType = $schema->getMutationType();
|
||||
if ($mutationType) {
|
||||
$operationTypes[] = " mutation: {$mutationType->name}";
|
||||
if ($mutationType !== null) {
|
||||
$operationTypes[] = sprintf(' mutation: %s', $mutationType->name);
|
||||
}
|
||||
|
||||
$subscriptionType = $schema->getSubscriptionType();
|
||||
if ($subscriptionType) {
|
||||
$operationTypes[] = " subscription: {$subscriptionType->name}";
|
||||
if ($subscriptionType !== null) {
|
||||
$operationTypes[] = sprintf(' subscription: %s', $subscriptionType->name);
|
||||
}
|
||||
|
||||
return "schema {\n" . implode("\n", $operationTypes) . "\n}";
|
||||
return sprintf("schema {\n%s\n}", implode("\n", $operationTypes));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* GraphQL schema define root types for each type of operation. These types are
|
||||
* the same as any other type and can be named in any manner, however there is
|
||||
@@ -125,189 +134,375 @@ class SchemaPrinter
|
||||
*
|
||||
* When using this naming convention, the schema description can be omitted.
|
||||
*/
|
||||
private static function isSchemaOfCommonNames(Schema $schema)
|
||||
protected static function isSchemaOfCommonNames(Schema $schema) : bool
|
||||
{
|
||||
$queryType = $schema->getQueryType();
|
||||
if ($queryType && $queryType->name !== 'Query') {
|
||||
if ($queryType !== null && $queryType->name !== 'Query') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mutationType = $schema->getMutationType();
|
||||
if ($mutationType && $mutationType->name !== 'Mutation') {
|
||||
if ($mutationType !== null && $mutationType->name !== 'Mutation') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$subscriptionType = $schema->getSubscriptionType();
|
||||
if ($subscriptionType && $subscriptionType->name !== 'Subscription') {
|
||||
return false;
|
||||
|
||||
return $subscriptionType === null || $subscriptionType->name === 'Subscription';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
protected static function printDirective(Directive $directive, array $options) : string
|
||||
{
|
||||
return static::printDescription($options, $directive)
|
||||
. 'directive @' . $directive->name
|
||||
. static::printArgs($options, $directive->args)
|
||||
. ($directive->isRepeatable ? ' repeatable' : '')
|
||||
. ' on ' . implode(' | ', $directive->locations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
protected static function printDescription(array $options, $def, $indentation = '', $firstInBlock = true) : string
|
||||
{
|
||||
if (! $def->description) {
|
||||
return '';
|
||||
}
|
||||
$lines = static::descriptionLines($def->description, 120 - strlen($indentation));
|
||||
if (isset($options['commentDescriptions'])) {
|
||||
return static::printDescriptionWithComments($lines, $indentation, $firstInBlock);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
$description = $indentation && ! $firstInBlock
|
||||
? "\n" . $indentation . '"""'
|
||||
: $indentation . '"""';
|
||||
|
||||
public static function printType(Type $type)
|
||||
{
|
||||
if ($type instanceof ScalarType) {
|
||||
return self::printScalar($type);
|
||||
} else if ($type instanceof ObjectType) {
|
||||
return self::printObject($type);
|
||||
} else if ($type instanceof InterfaceType) {
|
||||
return self::printInterface($type);
|
||||
} else if ($type instanceof UnionType) {
|
||||
return self::printUnion($type);
|
||||
} else if ($type instanceof EnumType) {
|
||||
return self::printEnum($type);
|
||||
// In some circumstances, a single line can be used for the description.
|
||||
if (count($lines) === 1 &&
|
||||
mb_strlen($lines[0]) < 70 &&
|
||||
substr($lines[0], -1) !== '"'
|
||||
) {
|
||||
return $description . static::escapeQuote($lines[0]) . "\"\"\"\n";
|
||||
}
|
||||
Utils::invariant($type instanceof InputObjectType);
|
||||
return self::printInputObject($type);
|
||||
|
||||
// Format a multi-line block quote to account for leading space.
|
||||
$hasLeadingSpace = isset($lines[0]) &&
|
||||
(
|
||||
substr($lines[0], 0, 1) === ' ' ||
|
||||
substr($lines[0], 0, 1) === '\t'
|
||||
);
|
||||
if (! $hasLeadingSpace) {
|
||||
$description .= "\n";
|
||||
}
|
||||
|
||||
$lineLength = count($lines);
|
||||
for ($i = 0; $i < $lineLength; $i++) {
|
||||
if ($i !== 0 || ! $hasLeadingSpace) {
|
||||
$description .= $indentation;
|
||||
}
|
||||
$description .= static::escapeQuote($lines[$i]) . "\n";
|
||||
}
|
||||
$description .= $indentation . "\"\"\"\n";
|
||||
|
||||
return $description;
|
||||
}
|
||||
|
||||
private static function printScalar(ScalarType $type)
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected static function descriptionLines(string $description, int $maxLen) : array
|
||||
{
|
||||
return self::printDescription($type) . "scalar {$type->name}";
|
||||
$lines = [];
|
||||
$rawLines = explode("\n", $description);
|
||||
foreach ($rawLines as $line) {
|
||||
if ($line === '') {
|
||||
$lines[] = $line;
|
||||
} else {
|
||||
// For > 120 character long lines, cut at space boundaries into sublines
|
||||
// of ~80 chars.
|
||||
$sublines = static::breakLine($line, $maxLen);
|
||||
foreach ($sublines as $subline) {
|
||||
$lines[] = $subline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
private static function printObject(ObjectType $type)
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected static function breakLine(string $line, int $maxLen) : array
|
||||
{
|
||||
$interfaces = $type->getInterfaces();
|
||||
$implementedInterfaces = !empty($interfaces) ?
|
||||
' implements ' . implode(', ', array_map(function($i) {
|
||||
return $i->name;
|
||||
}, $interfaces)) : '';
|
||||
return self::printDescription($type) .
|
||||
"type {$type->name}$implementedInterfaces {\n" .
|
||||
self::printFields($type) . "\n" .
|
||||
"}";
|
||||
if (strlen($line) < $maxLen + 5) {
|
||||
return [$line];
|
||||
}
|
||||
preg_match_all('/((?: |^).{15,' . ($maxLen - 40) . '}(?= |$))/', $line, $parts);
|
||||
$parts = $parts[0];
|
||||
|
||||
return array_map('trim', $parts);
|
||||
}
|
||||
|
||||
private static function printInterface(InterfaceType $type)
|
||||
protected static function printDescriptionWithComments($lines, $indentation, $firstInBlock) : string
|
||||
{
|
||||
return self::printDescription($type) .
|
||||
"interface {$type->name} {\n" .
|
||||
self::printFields($type) . "\n" .
|
||||
"}";
|
||||
$description = $indentation && ! $firstInBlock ? "\n" : '';
|
||||
foreach ($lines as $line) {
|
||||
if ($line === '') {
|
||||
$description .= $indentation . "#\n";
|
||||
} else {
|
||||
$description .= $indentation . '# ' . $line . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $description;
|
||||
}
|
||||
|
||||
private static function printUnion(UnionType $type)
|
||||
protected static function escapeQuote($line) : string
|
||||
{
|
||||
return self::printDescription($type) .
|
||||
"union {$type->name} = " . implode(" | ", $type->getTypes());
|
||||
return str_replace('"""', '\\"""', $line);
|
||||
}
|
||||
|
||||
private static function printEnum(EnumType $type)
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
protected static function printArgs(array $options, $args, $indentation = '') : string
|
||||
{
|
||||
return self::printDescription($type) .
|
||||
"enum {$type->name} {\n" .
|
||||
self::printEnumValues($type->getValues()) . "\n" .
|
||||
"}";
|
||||
}
|
||||
|
||||
private static function printEnumValues($values)
|
||||
{
|
||||
return implode("\n", array_map(function($value, $i) {
|
||||
return self::printDescription($value, ' ', !$i) . ' ' .
|
||||
$value->name . self::printDeprecated($value);
|
||||
}, $values, array_keys($values)));
|
||||
}
|
||||
|
||||
private static function printInputObject(InputObjectType $type)
|
||||
{
|
||||
$fields = array_values($type->getFields());
|
||||
return self::printDescription($type) .
|
||||
"input {$type->name} {\n" .
|
||||
implode("\n", array_map(function($f, $i) {
|
||||
return self::printDescription($f, ' ', !$i) . ' ' . self::printInputValue($f);
|
||||
}, $fields, array_keys($fields))) . "\n" .
|
||||
"}";
|
||||
}
|
||||
|
||||
private static function printFields($type)
|
||||
{
|
||||
$fields = array_values($type->getFields());
|
||||
return implode("\n", array_map(function($f, $i) {
|
||||
return self::printDescription($f, ' ', !$i) . ' ' .
|
||||
$f->name . self::printArgs($f->args, ' ') . ': ' .
|
||||
(string) $f->getType() . self::printDeprecated($f);
|
||||
}, $fields, array_keys($fields)));
|
||||
}
|
||||
|
||||
private static function printArgs($args, $indentation = '')
|
||||
{
|
||||
if (count($args) === 0) {
|
||||
if (! $args) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// If every arg does not have a description, print them on one line.
|
||||
if (Utils::every($args, function($arg) { return empty($arg->description); })) {
|
||||
return '(' . implode(', ', array_map('self::printInputValue', $args)) . ')';
|
||||
if (Utils::every(
|
||||
$args,
|
||||
static function ($arg) : bool {
|
||||
return strlen($arg->description ?? '') === 0;
|
||||
}
|
||||
)) {
|
||||
return '(' . implode(', ', array_map('static::printInputValue', $args)) . ')';
|
||||
}
|
||||
|
||||
return "(\n" . implode("\n", array_map(function($arg, $i) use ($indentation) {
|
||||
return self::printDescription($arg, ' ' . $indentation, !$i) . ' ' . $indentation .
|
||||
self::printInputValue($arg);
|
||||
}, $args, array_keys($args))) . "\n" . $indentation . ')';
|
||||
return sprintf(
|
||||
"(\n%s\n%s)",
|
||||
implode(
|
||||
"\n",
|
||||
array_map(
|
||||
static function ($arg, $i) use ($indentation, $options) : string {
|
||||
return static::printDescription($options, $arg, ' ' . $indentation, ! $i) . ' ' . $indentation .
|
||||
static::printInputValue($arg);
|
||||
},
|
||||
$args,
|
||||
array_keys($args)
|
||||
)
|
||||
),
|
||||
$indentation
|
||||
);
|
||||
}
|
||||
|
||||
private static function printInputValue($arg)
|
||||
protected static function printInputValue($arg) : string
|
||||
{
|
||||
$argDecl = $arg->name . ': ' . (string) $arg->getType();
|
||||
if ($arg->defaultValueExists()) {
|
||||
$argDecl .= ' = ' . Printer::doPrint(AST::astFromValue($arg->defaultValue, $arg->getType()));
|
||||
}
|
||||
|
||||
return $argDecl;
|
||||
}
|
||||
|
||||
private static function printDirective($directive)
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
public static function printType(Type $type, array $options = []) : string
|
||||
{
|
||||
return self::printDescription($directive) .
|
||||
'directive @' . $directive->name . self::printArgs($directive->args) .
|
||||
' on ' . implode(' | ', $directive->locations);
|
||||
if ($type instanceof ScalarType) {
|
||||
return static::printScalar($type, $options);
|
||||
}
|
||||
|
||||
if ($type instanceof ObjectType) {
|
||||
return static::printObject($type, $options);
|
||||
}
|
||||
|
||||
if ($type instanceof InterfaceType) {
|
||||
return static::printInterface($type, $options);
|
||||
}
|
||||
|
||||
if ($type instanceof UnionType) {
|
||||
return static::printUnion($type, $options);
|
||||
}
|
||||
|
||||
if ($type instanceof EnumType) {
|
||||
return static::printEnum($type, $options);
|
||||
}
|
||||
|
||||
if ($type instanceof InputObjectType) {
|
||||
return static::printInputObject($type, $options);
|
||||
}
|
||||
|
||||
throw new Error(sprintf('Unknown type: %s.', Utils::printSafe($type)));
|
||||
}
|
||||
|
||||
private static function printDeprecated($fieldOrEnumVal)
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
protected static function printScalar(ScalarType $type, array $options) : string
|
||||
{
|
||||
return sprintf('%sscalar %s', static::printDescription($options, $type), $type->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
protected static function printObject(ObjectType $type, array $options) : string
|
||||
{
|
||||
$interfaces = $type->getInterfaces();
|
||||
$implementedInterfaces = count($interfaces) > 0
|
||||
? ' implements ' . implode(
|
||||
' & ',
|
||||
array_map(
|
||||
static function (InterfaceType $interface) : string {
|
||||
return $interface->name;
|
||||
},
|
||||
$interfaces
|
||||
)
|
||||
)
|
||||
: '';
|
||||
|
||||
return static::printDescription($options, $type) .
|
||||
sprintf("type %s%s {\n%s\n}", $type->name, $implementedInterfaces, static::printFields($options, $type));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
protected static function printFields(array $options, $type) : string
|
||||
{
|
||||
$fields = array_values($type->getFields());
|
||||
|
||||
return implode(
|
||||
"\n",
|
||||
array_map(
|
||||
static function ($f, $i) use ($options) : string {
|
||||
return static::printDescription($options, $f, ' ', ! $i) . ' ' .
|
||||
$f->name . static::printArgs($options, $f->args, ' ') . ': ' .
|
||||
(string) $f->getType() . static::printDeprecated($f);
|
||||
},
|
||||
$fields,
|
||||
array_keys($fields)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected static function printDeprecated($fieldOrEnumVal) : string
|
||||
{
|
||||
$reason = $fieldOrEnumVal->deprecationReason;
|
||||
if (empty($reason)) {
|
||||
if ($reason === null) {
|
||||
return '';
|
||||
}
|
||||
if ($reason === '' || $reason === Directive::DEFAULT_DEPRECATION_REASON) {
|
||||
return ' @deprecated';
|
||||
}
|
||||
|
||||
return ' @deprecated(reason: ' .
|
||||
Printer::doPrint(AST::astFromValue($reason, Type::string())) . ')';
|
||||
}
|
||||
|
||||
private static function printDescription($def, $indentation = '', $firstInBlock = true)
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
protected static function printInterface(InterfaceType $type, array $options) : string
|
||||
{
|
||||
if (!$def->description) {
|
||||
return '';
|
||||
}
|
||||
$lines = explode("\n", $def->description);
|
||||
$description = $indentation && !$firstInBlock ? "\n" : '';
|
||||
foreach ($lines as $line) {
|
||||
if ($line === '') {
|
||||
$description .= $indentation . "#\n";
|
||||
} else {
|
||||
// For > 120 character long lines, cut at space boundaries into sublines
|
||||
// of ~80 chars.
|
||||
$sublines = self::breakLine($line, 120 - strlen($indentation));
|
||||
foreach ($sublines as $subline) {
|
||||
$description .= $indentation . '# ' . $subline . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
return $description;
|
||||
$interfaces = $type->getInterfaces();
|
||||
$implementedInterfaces = count($interfaces) > 0
|
||||
? ' implements ' . implode(
|
||||
' & ',
|
||||
array_map(
|
||||
static function (InterfaceType $interface) : string {
|
||||
return $interface->name;
|
||||
},
|
||||
$interfaces
|
||||
)
|
||||
)
|
||||
: '';
|
||||
|
||||
return static::printDescription($options, $type) .
|
||||
sprintf("interface %s%s {\n%s\n}", $type->name, $implementedInterfaces, static::printFields($options, $type));
|
||||
}
|
||||
|
||||
private static function breakLine($line, $len)
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
protected static function printUnion(UnionType $type, array $options) : string
|
||||
{
|
||||
if (strlen($line) < $len + 5) {
|
||||
return [$line];
|
||||
}
|
||||
preg_match_all("/((?: |^).{15," . ($len - 40) . "}(?= |$))/", $line, $parts);
|
||||
$parts = $parts[0];
|
||||
return array_map(function($part) {
|
||||
return trim($part);
|
||||
}, $parts);
|
||||
return static::printDescription($options, $type) .
|
||||
sprintf('union %s = %s', $type->name, implode(' | ', $type->getTypes()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
protected static function printEnum(EnumType $type, array $options) : string
|
||||
{
|
||||
return static::printDescription($options, $type) .
|
||||
sprintf("enum %s {\n%s\n}", $type->name, static::printEnumValues($type->getValues(), $options));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
protected static function printEnumValues($values, array $options) : string
|
||||
{
|
||||
return implode(
|
||||
"\n",
|
||||
array_map(
|
||||
static function ($value, $i) use ($options) : string {
|
||||
return static::printDescription($options, $value, ' ', ! $i) . ' ' .
|
||||
$value->name . static::printDeprecated($value);
|
||||
},
|
||||
$values,
|
||||
array_keys($values)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
protected static function printInputObject(InputObjectType $type, array $options) : string
|
||||
{
|
||||
$fields = array_values($type->getFields());
|
||||
|
||||
return static::printDescription($options, $type) .
|
||||
sprintf(
|
||||
"input %s {\n%s\n}",
|
||||
$type->name,
|
||||
implode(
|
||||
"\n",
|
||||
array_map(
|
||||
static function ($f, $i) use ($options) : string {
|
||||
return static::printDescription($options, $f, ' ', ! $i) . ' ' . static::printInputValue($f);
|
||||
},
|
||||
$fields,
|
||||
array_keys($fields)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function printIntrospectionSchema(Schema $schema, array $options = []) : string
|
||||
{
|
||||
return static::printFilteredSchema(
|
||||
$schema,
|
||||
[Directive::class, 'isSpecifiedDirective'],
|
||||
[Introspection::class, 'isIntrospectionType'],
|
||||
$options
|
||||
);
|
||||
}
|
||||
}
|
||||
+26
-25
@@ -1,21 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Type\Definition\AbstractType;
|
||||
use GraphQL\Type\Definition\CompositeType;
|
||||
use GraphQL\Type\Definition\ImplementingType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Schema;
|
||||
|
||||
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)
|
||||
@@ -43,12 +45,9 @@ class TypeComparators
|
||||
* 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)
|
||||
public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType)
|
||||
{
|
||||
// Equivalent type is a valid subtype
|
||||
if ($maybeSubType === $superType) {
|
||||
@@ -60,8 +59,11 @@ class TypeComparators
|
||||
if ($maybeSubType instanceof NonNull) {
|
||||
return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType());
|
||||
}
|
||||
|
||||
return false;
|
||||
} else if ($maybeSubType instanceof NonNull) {
|
||||
}
|
||||
|
||||
if ($maybeSubType instanceof NonNull) {
|
||||
// If superType is nullable, maybeSubType may be non-null.
|
||||
return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType);
|
||||
}
|
||||
@@ -71,20 +73,23 @@ class TypeComparators
|
||||
if ($maybeSubType instanceof ListOfType) {
|
||||
return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType());
|
||||
}
|
||||
|
||||
return false;
|
||||
} else if ($maybeSubType instanceof ListOfType) {
|
||||
}
|
||||
|
||||
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;
|
||||
// possible object or interface type.
|
||||
return Type::isAbstractType($superType) &&
|
||||
$maybeSubType instanceof ImplementingType &&
|
||||
$schema->isSubType(
|
||||
$superType,
|
||||
$maybeSubType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,12 +101,9 @@ class TypeComparators
|
||||
*
|
||||
* This function is commutative.
|
||||
*
|
||||
* @param Schema $schema
|
||||
* @param CompositeType $typeA
|
||||
* @param CompositeType $typeB
|
||||
* @return bool
|
||||
*/
|
||||
static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB)
|
||||
public static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB)
|
||||
{
|
||||
// Equivalent types overlap
|
||||
if ($typeA === $typeB) {
|
||||
@@ -113,22 +115,21 @@ class TypeComparators
|
||||
// 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)) {
|
||||
if ($schema->isSubType($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);
|
||||
return $schema->isSubType($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);
|
||||
return $schema->isSubType($typeB, $typeA);
|
||||
}
|
||||
|
||||
// Otherwise the types do not overlap.
|
||||
|
||||
+334
-255
@@ -1,72 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Error\Warning;
|
||||
use GraphQL\Language\AST\ArgumentNode;
|
||||
use GraphQL\Language\AST\DirectiveNode;
|
||||
use GraphQL\Language\AST\EnumValueNode;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\InlineFragmentNode;
|
||||
use GraphQL\Language\AST\ListTypeNode;
|
||||
use GraphQL\Language\AST\ListValueNode;
|
||||
use GraphQL\Language\AST\NamedTypeNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\NonNullTypeNode;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Language\AST\ObjectFieldNode;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||
use GraphQL\Type\Definition\CompositeType;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Type\Definition\ImplementingType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\OutputType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Definition\WrappingType;
|
||||
use GraphQL\Type\Introspection;
|
||||
use GraphQL\Type\Schema;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_pop;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Class TypeInfo
|
||||
* @package GraphQL\Utils
|
||||
*/
|
||||
class TypeInfo
|
||||
{
|
||||
/** @var Schema */
|
||||
private $schema;
|
||||
|
||||
/** @var array<(OutputType&Type)|null> */
|
||||
private $typeStack;
|
||||
|
||||
/** @var array<(CompositeType&Type)|null> */
|
||||
private $parentTypeStack;
|
||||
|
||||
/** @var array<(InputType&Type)|null> */
|
||||
private $inputTypeStack;
|
||||
|
||||
/** @var array<FieldDefinition> */
|
||||
private $fieldDefStack;
|
||||
|
||||
/** @var array<mixed> */
|
||||
private $defaultValueStack;
|
||||
|
||||
/** @var Directive|null */
|
||||
private $directive;
|
||||
|
||||
/** @var FieldArgument|null */
|
||||
private $argument;
|
||||
|
||||
/** @var mixed */
|
||||
private $enumValue;
|
||||
|
||||
/**
|
||||
* @param Type|null $initialType
|
||||
*/
|
||||
public function __construct(Schema $schema, $initialType = null)
|
||||
{
|
||||
$this->schema = $schema;
|
||||
$this->typeStack = [];
|
||||
$this->parentTypeStack = [];
|
||||
$this->inputTypeStack = [];
|
||||
$this->fieldDefStack = [];
|
||||
$this->defaultValueStack = [];
|
||||
|
||||
if ($initialType === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Type::isInputType($initialType)) {
|
||||
$this->inputTypeStack[] = $initialType;
|
||||
}
|
||||
if (Type::isCompositeType($initialType)) {
|
||||
$this->parentTypeStack[] = $initialType;
|
||||
}
|
||||
if (! Type::isOutputType($initialType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->typeStack[] = $initialType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated moved to GraphQL\Utils\TypeComparators
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function isEqualType(Type $typeA, Type $typeB)
|
||||
public static function isEqualType(Type $typeA, Type $typeB) : bool
|
||||
{
|
||||
return TypeComparators::isEqualType($typeA, $typeB);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated moved to GraphQL\Utils\TypeComparators
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType)
|
||||
public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType)
|
||||
{
|
||||
return TypeComparators::isTypeSubTypeOf($schema, $maybeSubType, $superType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated moved to GraphQL\Utils\TypeComparators
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB)
|
||||
public static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB)
|
||||
{
|
||||
return TypeComparators::doTypesOverlap($schema, $typeA, $typeB);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
|
||||
* @return Type
|
||||
* @throws InvariantViolation
|
||||
*/
|
||||
public static function typeFromAST(Schema $schema, $inputTypeNode)
|
||||
{
|
||||
return AST::typeFromAST($schema, $inputTypeNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given root type scans through all fields to find nested types. Returns array where keys are for type name
|
||||
* and value contains corresponding type instance.
|
||||
@@ -78,37 +143,47 @@ class TypeInfo
|
||||
* ...
|
||||
* ]
|
||||
*
|
||||
* @param Type $type
|
||||
* @param array|null $typeMap
|
||||
* @return array
|
||||
* @param Type|null $type
|
||||
* @param Type[]|null $typeMap
|
||||
*
|
||||
* @return Type[]|null
|
||||
*/
|
||||
public static function extractTypes($type, array $typeMap = null)
|
||||
public static function extractTypes($type, ?array $typeMap = null)
|
||||
{
|
||||
if (!$typeMap) {
|
||||
if (! $typeMap) {
|
||||
$typeMap = [];
|
||||
}
|
||||
if (!$type) {
|
||||
if (! $type) {
|
||||
return $typeMap;
|
||||
}
|
||||
|
||||
if ($type instanceof WrappingType) {
|
||||
return self::extractTypes($type->getWrappedType(true), $typeMap);
|
||||
}
|
||||
if (!$type instanceof Type) {
|
||||
Warning::warnOnce(
|
||||
'One of the schema types is not a valid type definition instance. '.
|
||||
'Try running $schema->assertValid() to find out the cause of this warning.',
|
||||
Warning::WARNING_NOT_A_TYPE
|
||||
);
|
||||
|
||||
if (! $type instanceof Type) {
|
||||
// Preserve these invalid types in map (at numeric index) to make them
|
||||
// detectable during $schema->validate()
|
||||
$i = 0;
|
||||
$alreadyInMap = false;
|
||||
while (isset($typeMap[$i])) {
|
||||
$alreadyInMap = $alreadyInMap || $typeMap[$i] === $type;
|
||||
$i++;
|
||||
}
|
||||
if (! $alreadyInMap) {
|
||||
$typeMap[$i] = $type;
|
||||
}
|
||||
|
||||
return $typeMap;
|
||||
}
|
||||
|
||||
if (!empty($typeMap[$type->name])) {
|
||||
if (isset($typeMap[$type->name])) {
|
||||
Utils::invariant(
|
||||
$typeMap[$type->name] === $type,
|
||||
"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" ', $type) .
|
||||
'(see http://webonyx.github.io/graphql-php/type-system/#type-registry).'
|
||||
);
|
||||
|
||||
return $typeMap;
|
||||
}
|
||||
$typeMap[$type->name] = $type;
|
||||
@@ -118,34 +193,214 @@ class TypeInfo
|
||||
if ($type instanceof UnionType) {
|
||||
$nestedTypes = $type->getTypes();
|
||||
}
|
||||
if ($type instanceof ObjectType) {
|
||||
if ($type instanceof ImplementingType) {
|
||||
$nestedTypes = array_merge($nestedTypes, $type->getInterfaces());
|
||||
}
|
||||
if ($type instanceof ObjectType || $type instanceof InterfaceType || $type instanceof InputObjectType) {
|
||||
foreach ((array) $type->getFields() as $fieldName => $field) {
|
||||
if (!empty($field->args)) {
|
||||
$fieldArgTypes = array_map(function(FieldArgument $arg) { return $arg->getType(); }, $field->args);
|
||||
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
|
||||
foreach ($type->getFields() as $fieldName => $field) {
|
||||
if (count($field->args ?? []) > 0) {
|
||||
$fieldArgTypes = array_map(
|
||||
static function (FieldArgument $arg) : Type {
|
||||
return $arg->getType();
|
||||
},
|
||||
$field->args
|
||||
);
|
||||
|
||||
$nestedTypes = array_merge($nestedTypes, $fieldArgTypes);
|
||||
}
|
||||
$nestedTypes[] = $field->getType();
|
||||
}
|
||||
}
|
||||
foreach ($nestedTypes as $type) {
|
||||
$typeMap = self::extractTypes($type, $typeMap);
|
||||
if ($type instanceof InputObjectType) {
|
||||
foreach ($type->getFields() as $fieldName => $field) {
|
||||
$nestedTypes[] = $field->getType();
|
||||
}
|
||||
}
|
||||
foreach ($nestedTypes as $nestedType) {
|
||||
$typeMap = self::extractTypes($nestedType, $typeMap);
|
||||
}
|
||||
|
||||
return $typeMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Type[] $typeMap
|
||||
*
|
||||
* @return Type[]
|
||||
*/
|
||||
public static function extractTypesFromDirectives(Directive $directive, array $typeMap = [])
|
||||
{
|
||||
if (is_array($directive->args)) {
|
||||
foreach ($directive->args as $arg) {
|
||||
$typeMap = self::extractTypes($arg->getType(), $typeMap);
|
||||
}
|
||||
}
|
||||
|
||||
return $typeMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return (Type&InputType)|null
|
||||
*/
|
||||
public function getParentInputType() : ?InputType
|
||||
{
|
||||
return $this->inputTypeStack[count($this->inputTypeStack) - 2] ?? null;
|
||||
}
|
||||
|
||||
public function getArgument() : ?FieldArgument
|
||||
{
|
||||
return $this->argument;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getEnumValue()
|
||||
{
|
||||
return $this->enumValue;
|
||||
}
|
||||
|
||||
public function enter(Node $node)
|
||||
{
|
||||
$schema = $this->schema;
|
||||
|
||||
// Note: many of the types below are explicitly typed as "mixed" to drop
|
||||
// any assumptions of a valid schema to ensure runtime types are properly
|
||||
// checked before continuing since TypeInfo is used as part of validation
|
||||
// which occurs before guarantees of schema and document validity.
|
||||
switch (true) {
|
||||
case $node instanceof SelectionSetNode:
|
||||
$namedType = Type::getNamedType($this->getType());
|
||||
$this->parentTypeStack[] = Type::isCompositeType($namedType) ? $namedType : null;
|
||||
break;
|
||||
|
||||
case $node instanceof FieldNode:
|
||||
$parentType = $this->getParentType();
|
||||
$fieldDef = null;
|
||||
if ($parentType) {
|
||||
$fieldDef = self::getFieldDefinition($schema, $parentType, $node);
|
||||
}
|
||||
$fieldType = null;
|
||||
if ($fieldDef) {
|
||||
$fieldType = $fieldDef->getType();
|
||||
}
|
||||
$this->fieldDefStack[] = $fieldDef;
|
||||
$this->typeStack[] = Type::isOutputType($fieldType) ? $fieldType : null;
|
||||
break;
|
||||
|
||||
case $node instanceof DirectiveNode:
|
||||
$this->directive = $schema->getDirective($node->name->value);
|
||||
break;
|
||||
|
||||
case $node instanceof OperationDefinitionNode:
|
||||
$type = null;
|
||||
if ($node->operation === 'query') {
|
||||
$type = $schema->getQueryType();
|
||||
} elseif ($node->operation === 'mutation') {
|
||||
$type = $schema->getMutationType();
|
||||
} elseif ($node->operation === 'subscription') {
|
||||
$type = $schema->getSubscriptionType();
|
||||
}
|
||||
$this->typeStack[] = Type::isOutputType($type) ? $type : null;
|
||||
break;
|
||||
|
||||
case $node instanceof InlineFragmentNode:
|
||||
case $node instanceof FragmentDefinitionNode:
|
||||
$typeConditionNode = $node->typeCondition;
|
||||
$outputType = $typeConditionNode
|
||||
? self::typeFromAST(
|
||||
$schema,
|
||||
$typeConditionNode
|
||||
)
|
||||
: Type::getNamedType($this->getType());
|
||||
$this->typeStack[] = Type::isOutputType($outputType) ? $outputType : null;
|
||||
break;
|
||||
|
||||
case $node instanceof VariableDefinitionNode:
|
||||
$inputType = self::typeFromAST($schema, $node->type);
|
||||
$this->inputTypeStack[] = Type::isInputType($inputType) ? $inputType : null; // push
|
||||
break;
|
||||
|
||||
case $node instanceof ArgumentNode:
|
||||
$fieldOrDirective = $this->getDirective() ?? $this->getFieldDef();
|
||||
$argDef = $argType = null;
|
||||
if ($fieldOrDirective) {
|
||||
/** @var FieldArgument $argDef */
|
||||
$argDef = Utils::find(
|
||||
$fieldOrDirective->args,
|
||||
static function ($arg) use ($node) : bool {
|
||||
return $arg->name === $node->name->value;
|
||||
}
|
||||
);
|
||||
if ($argDef !== null) {
|
||||
$argType = $argDef->getType();
|
||||
}
|
||||
}
|
||||
$this->argument = $argDef;
|
||||
$this->defaultValueStack[] = $argDef && $argDef->defaultValueExists() ? $argDef->defaultValue : Utils::undefined();
|
||||
$this->inputTypeStack[] = Type::isInputType($argType) ? $argType : null;
|
||||
break;
|
||||
|
||||
case $node instanceof ListValueNode:
|
||||
$type = $this->getInputType();
|
||||
$listType = $type === null ? null : Type::getNullableType($type);
|
||||
$itemType = $listType instanceof ListOfType
|
||||
? $listType->getWrappedType()
|
||||
: $listType;
|
||||
// List positions never have a default value.
|
||||
$this->defaultValueStack[] = Utils::undefined();
|
||||
$this->inputTypeStack[] = Type::isInputType($itemType) ? $itemType : null;
|
||||
break;
|
||||
|
||||
case $node instanceof ObjectFieldNode:
|
||||
$objectType = Type::getNamedType($this->getInputType());
|
||||
$fieldType = null;
|
||||
$inputField = null;
|
||||
$inputFieldType = null;
|
||||
if ($objectType instanceof InputObjectType) {
|
||||
$tmp = $objectType->getFields();
|
||||
$inputField = $tmp[$node->name->value] ?? null;
|
||||
$inputFieldType = $inputField ? $inputField->getType() : null;
|
||||
}
|
||||
$this->defaultValueStack[] = $inputField && $inputField->defaultValueExists() ? $inputField->defaultValue : Utils::undefined();
|
||||
$this->inputTypeStack[] = Type::isInputType($inputFieldType) ? $inputFieldType : null;
|
||||
break;
|
||||
|
||||
case $node instanceof EnumValueNode:
|
||||
$enumType = Type::getNamedType($this->getInputType());
|
||||
$enumValue = null;
|
||||
if ($enumType instanceof EnumType) {
|
||||
$this->enumValue = $enumType->getValue($node->value);
|
||||
}
|
||||
$this->enumValue = $enumValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return (Type & OutputType) | null
|
||||
*/
|
||||
public function getType() : ?OutputType
|
||||
{
|
||||
return $this->typeStack[count($this->typeStack) - 1] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return (CompositeType & Type) | null
|
||||
*/
|
||||
public function getParentType() : ?CompositeType
|
||||
{
|
||||
return $this->parentTypeStack[count($this->parentTypeStack) - 1] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not exactly the same as the executor's definition of getFieldDef, in this
|
||||
* statically evaluated environment we do not always have an Object type,
|
||||
* and need to handle Interface and Union types.
|
||||
*
|
||||
* @return FieldDefinition
|
||||
*/
|
||||
static private function getFieldDefinition(Schema $schema, Type $parentType, FieldNode $fieldNode)
|
||||
private static function getFieldDefinition(Schema $schema, Type $parentType, FieldNode $fieldNode) : ?FieldDefinition
|
||||
{
|
||||
$name = $fieldNode->name->value;
|
||||
$name = $fieldNode->name->value;
|
||||
$schemaMeta = Introspection::schemaMetaFieldDef();
|
||||
if ($name === $schemaMeta->name && $schema->getQueryType() === $parentType) {
|
||||
return $schemaMeta;
|
||||
@@ -162,260 +417,84 @@ class TypeInfo
|
||||
if ($parentType instanceof ObjectType ||
|
||||
$parentType instanceof InterfaceType) {
|
||||
$fields = $parentType->getFields();
|
||||
return isset($fields[$name]) ? $fields[$name] : null;
|
||||
|
||||
return $fields[$name] ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @var Schema
|
||||
*/
|
||||
private $schema;
|
||||
|
||||
/**
|
||||
* @var \SplStack<OutputType>
|
||||
*/
|
||||
private $typeStack;
|
||||
|
||||
/**
|
||||
* @var \SplStack<CompositeType>
|
||||
*/
|
||||
private $parentTypeStack;
|
||||
|
||||
/**
|
||||
* @var \SplStack<InputType>
|
||||
*/
|
||||
private $inputTypeStack;
|
||||
|
||||
/**
|
||||
* @var \SplStack<FieldDefinition>
|
||||
*/
|
||||
private $fieldDefStack;
|
||||
|
||||
/**
|
||||
* @var Directive
|
||||
*/
|
||||
private $directive;
|
||||
|
||||
/**
|
||||
* @var FieldArgument
|
||||
*/
|
||||
private $argument;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
private $enumValue;
|
||||
|
||||
/**
|
||||
* TypeInfo constructor.
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function __construct(Schema $schema)
|
||||
{
|
||||
$this->schema = $schema;
|
||||
$this->typeStack = [];
|
||||
$this->parentTypeStack = [];
|
||||
$this->inputTypeStack = [];
|
||||
$this->fieldDefStack = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Type
|
||||
*/
|
||||
function getType()
|
||||
{
|
||||
if (!empty($this->typeStack)) {
|
||||
return $this->typeStack[count($this->typeStack) - 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Type
|
||||
* @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
|
||||
*
|
||||
* @throws InvariantViolation
|
||||
*/
|
||||
function getParentType()
|
||||
public static function typeFromAST(Schema $schema, $inputTypeNode) : ?Type
|
||||
{
|
||||
if (!empty($this->parentTypeStack)) {
|
||||
return $this->parentTypeStack[count($this->parentTypeStack) - 1];
|
||||
}
|
||||
return null;
|
||||
return AST::typeFromAST($schema, $inputTypeNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InputType
|
||||
*/
|
||||
function getInputType()
|
||||
{
|
||||
if (!empty($this->inputTypeStack)) {
|
||||
return $this->inputTypeStack[count($this->inputTypeStack) - 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FieldDefinition
|
||||
*/
|
||||
function getFieldDef()
|
||||
{
|
||||
if (!empty($this->fieldDefStack)) {
|
||||
return $this->fieldDefStack[count($this->fieldDefStack) - 1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Directive|null
|
||||
*/
|
||||
function getDirective()
|
||||
public function getDirective() : ?Directive
|
||||
{
|
||||
return $this->directive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FieldArgument|null
|
||||
*/
|
||||
function getArgument()
|
||||
public function getFieldDef() : ?FieldDefinition
|
||||
{
|
||||
return $this->argument;
|
||||
return $this->fieldDefStack[count($this->fieldDefStack) - 1] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
* @return mixed|null
|
||||
*/
|
||||
function getEnumValue()
|
||||
public function getDefaultValue()
|
||||
{
|
||||
return $this->enumValue;
|
||||
return $this->defaultValueStack[count($this->defaultValueStack) - 1] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $node
|
||||
* @return (Type & InputType) | null
|
||||
*/
|
||||
function enter(Node $node)
|
||||
public function getInputType() : ?InputType
|
||||
{
|
||||
$schema = $this->schema;
|
||||
|
||||
switch ($node->kind) {
|
||||
case NodeKind::SELECTION_SET:
|
||||
$namedType = Type::getNamedType($this->getType());
|
||||
$this->parentTypeStack[] = Type::isCompositeType($namedType) ? $namedType : null;
|
||||
break;
|
||||
|
||||
case NodeKind::FIELD:
|
||||
$parentType = $this->getParentType();
|
||||
$fieldDef = null;
|
||||
if ($parentType) {
|
||||
$fieldDef = self::getFieldDefinition($schema, $parentType, $node);
|
||||
}
|
||||
$this->fieldDefStack[] = $fieldDef; // push
|
||||
$this->typeStack[] = $fieldDef ? $fieldDef->getType() : null; // push
|
||||
break;
|
||||
|
||||
case NodeKind::DIRECTIVE:
|
||||
$this->directive = $schema->getDirective($node->name->value);
|
||||
break;
|
||||
|
||||
case NodeKind::OPERATION_DEFINITION:
|
||||
$type = null;
|
||||
if ($node->operation === 'query') {
|
||||
$type = $schema->getQueryType();
|
||||
} else if ($node->operation === 'mutation') {
|
||||
$type = $schema->getMutationType();
|
||||
} else if ($node->operation === 'subscription') {
|
||||
$type = $schema->getSubscriptionType();
|
||||
}
|
||||
$this->typeStack[] = $type; // push
|
||||
break;
|
||||
|
||||
case NodeKind::INLINE_FRAGMENT:
|
||||
case NodeKind::FRAGMENT_DEFINITION:
|
||||
$typeConditionNode = $node->typeCondition;
|
||||
$outputType = $typeConditionNode ? self::typeFromAST($schema, $typeConditionNode) : $this->getType();
|
||||
$this->typeStack[] = Type::isOutputType($outputType) ? $outputType : null; // push
|
||||
break;
|
||||
|
||||
case NodeKind::VARIABLE_DEFINITION:
|
||||
$inputType = self::typeFromAST($schema, $node->type);
|
||||
$this->inputTypeStack[] = Type::isInputType($inputType) ? $inputType : null; // push
|
||||
break;
|
||||
|
||||
case NodeKind::ARGUMENT:
|
||||
$fieldOrDirective = $this->getDirective() ?: $this->getFieldDef();
|
||||
$argDef = $argType = null;
|
||||
if ($fieldOrDirective) {
|
||||
$argDef = Utils::find($fieldOrDirective->args, function($arg) use ($node) {return $arg->name === $node->name->value;});
|
||||
if ($argDef) {
|
||||
$argType = $argDef->getType();
|
||||
}
|
||||
}
|
||||
$this->argument = $argDef;
|
||||
$this->inputTypeStack[] = $argType; // push
|
||||
break;
|
||||
|
||||
case NodeKind::LST:
|
||||
$listType = Type::getNullableType($this->getInputType());
|
||||
$this->inputTypeStack[] = ($listType instanceof ListOfType ? $listType->getWrappedType() : null); // push
|
||||
break;
|
||||
|
||||
case NodeKind::OBJECT_FIELD:
|
||||
$objectType = Type::getNamedType($this->getInputType());
|
||||
$fieldType = null;
|
||||
if ($objectType instanceof InputObjectType) {
|
||||
$tmp = $objectType->getFields();
|
||||
$inputField = isset($tmp[$node->name->value]) ? $tmp[$node->name->value] : null;
|
||||
$fieldType = $inputField ? $inputField->getType() : null;
|
||||
}
|
||||
$this->inputTypeStack[] = $fieldType;
|
||||
break;
|
||||
|
||||
case NodeKind::ENUM:
|
||||
$enumType = Type::getNamedType($this->getInputType());
|
||||
$enumValue = null;
|
||||
if ($enumType instanceof EnumType) {
|
||||
$enumValue = $enumType->getValue($node->value);
|
||||
}
|
||||
$this->enumValue = $enumValue;
|
||||
break;
|
||||
}
|
||||
return $this->inputTypeStack[count($this->inputTypeStack) - 1] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $node
|
||||
*/
|
||||
function leave(Node $node)
|
||||
public function leave(Node $node)
|
||||
{
|
||||
switch ($node->kind) {
|
||||
case NodeKind::SELECTION_SET:
|
||||
switch (true) {
|
||||
case $node instanceof SelectionSetNode:
|
||||
array_pop($this->parentTypeStack);
|
||||
break;
|
||||
|
||||
case NodeKind::FIELD:
|
||||
case $node instanceof FieldNode:
|
||||
array_pop($this->fieldDefStack);
|
||||
array_pop($this->typeStack);
|
||||
break;
|
||||
|
||||
case NodeKind::DIRECTIVE:
|
||||
case $node instanceof DirectiveNode:
|
||||
$this->directive = null;
|
||||
break;
|
||||
|
||||
case NodeKind::OPERATION_DEFINITION:
|
||||
case NodeKind::INLINE_FRAGMENT:
|
||||
case NodeKind::FRAGMENT_DEFINITION:
|
||||
case $node instanceof OperationDefinitionNode:
|
||||
case $node instanceof InlineFragmentNode:
|
||||
case $node instanceof FragmentDefinitionNode:
|
||||
array_pop($this->typeStack);
|
||||
break;
|
||||
case NodeKind::VARIABLE_DEFINITION:
|
||||
case $node instanceof VariableDefinitionNode:
|
||||
array_pop($this->inputTypeStack);
|
||||
break;
|
||||
case NodeKind::ARGUMENT:
|
||||
case $node instanceof ArgumentNode:
|
||||
$this->argument = null;
|
||||
array_pop($this->defaultValueStack);
|
||||
array_pop($this->inputTypeStack);
|
||||
break;
|
||||
case NodeKind::LST:
|
||||
case NodeKind::OBJECT_FIELD:
|
||||
case $node instanceof ListValueNode:
|
||||
case $node instanceof ObjectFieldNode:
|
||||
array_pop($this->defaultValueStack);
|
||||
array_pop($this->inputTypeStack);
|
||||
break;
|
||||
case NodeKind::ENUM:
|
||||
case $node instanceof EnumValueNode:
|
||||
$this->enumValue = null;
|
||||
break;
|
||||
}
|
||||
|
||||
+363
-179
@@ -1,159 +1,247 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use ErrorException;
|
||||
use Exception;
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Error\Warning;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\WrappingType;
|
||||
use \Traversable, \InvalidArgumentException;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use stdClass;
|
||||
use Traversable;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_reduce;
|
||||
use function array_shift;
|
||||
use function array_slice;
|
||||
use function array_values;
|
||||
use function asort;
|
||||
use function count;
|
||||
use function dechex;
|
||||
use function func_get_args;
|
||||
use function func_num_args;
|
||||
use function get_class;
|
||||
use function gettype;
|
||||
use function is_array;
|
||||
use function is_int;
|
||||
use function is_object;
|
||||
use function is_scalar;
|
||||
use function is_string;
|
||||
use function json_encode;
|
||||
use function levenshtein;
|
||||
use function max;
|
||||
use function mb_convert_encoding;
|
||||
use function mb_strlen;
|
||||
use function mb_substr;
|
||||
use function method_exists;
|
||||
use function ord;
|
||||
use function pack;
|
||||
use function preg_match;
|
||||
use function property_exists;
|
||||
use function range;
|
||||
use function restore_error_handler;
|
||||
use function set_error_handler;
|
||||
use function sprintf;
|
||||
use function strtolower;
|
||||
use function unpack;
|
||||
|
||||
class Utils
|
||||
{
|
||||
public static function undefined()
|
||||
{
|
||||
static $undefined;
|
||||
return $undefined ?: $undefined = new \stdClass();
|
||||
|
||||
return $undefined ?? $undefined = new stdClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $obj
|
||||
* @param array $vars
|
||||
* @param array $requiredKeys
|
||||
* Check if the value is invalid
|
||||
*
|
||||
* @return array
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInvalid($value)
|
||||
{
|
||||
return self::undefined() === $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $obj
|
||||
* @param mixed[] $vars
|
||||
* @param string[] $requiredKeys
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public static function assign($obj, array $vars, array $requiredKeys = [])
|
||||
{
|
||||
foreach ($requiredKeys as $key) {
|
||||
if (!isset($vars[$key])) {
|
||||
throw new InvalidArgumentException("Key {$key} is expected to be set and not to be null");
|
||||
if (! isset($vars[$key])) {
|
||||
throw new InvalidArgumentException(sprintf('Key %s is expected to be set and not to be null', $key));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($vars as $key => $value) {
|
||||
if (!property_exists($obj, $key)) {
|
||||
if (! property_exists($obj, $key)) {
|
||||
$cls = get_class($obj);
|
||||
Warning::warn(
|
||||
"Trying to set non-existing property '$key' on class '$cls'",
|
||||
sprintf("Trying to set non-existing property '%s' on class '%s'", $key, $cls),
|
||||
Warning::WARNING_ASSIGN
|
||||
);
|
||||
}
|
||||
$obj->{$key} = $value;
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|Traversable $traversable
|
||||
* @param callable $predicate
|
||||
* @return null
|
||||
* @param iterable<mixed> $iterable
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
public static function find($traversable, callable $predicate)
|
||||
public static function find($iterable, callable $predicate)
|
||||
{
|
||||
self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable');
|
||||
self::invariant(
|
||||
is_array($iterable) || $iterable instanceof Traversable,
|
||||
__METHOD__ . ' expects array or Traversable'
|
||||
);
|
||||
|
||||
foreach ($traversable as $key => $value) {
|
||||
foreach ($iterable as $key => $value) {
|
||||
if ($predicate($value, $key)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $traversable
|
||||
* @param callable $predicate
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
* @param iterable<mixed> $iterable
|
||||
*
|
||||
* @return array<mixed>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function filter($traversable, callable $predicate)
|
||||
public static function filter($iterable, callable $predicate) : array
|
||||
{
|
||||
self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable');
|
||||
self::invariant(
|
||||
is_array($iterable) || $iterable instanceof Traversable,
|
||||
__METHOD__ . ' expects array or Traversable'
|
||||
);
|
||||
|
||||
$result = [];
|
||||
$assoc = false;
|
||||
foreach ($traversable as $key => $value) {
|
||||
if (!$assoc && !is_int($key)) {
|
||||
$assoc = false;
|
||||
foreach ($iterable as $key => $value) {
|
||||
if (! $assoc && ! is_int($key)) {
|
||||
$assoc = true;
|
||||
}
|
||||
if ($predicate($value, $key)) {
|
||||
$result[$key] = $value;
|
||||
if (! $predicate($value, $key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[$key] = $value;
|
||||
}
|
||||
|
||||
return $assoc ? $result : array_values($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|\Traversable $traversable
|
||||
* @param callable $fn function($value, $key) => $newValue
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
* @param iterable<mixed> $iterable
|
||||
*
|
||||
* @return array<mixed>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function map($traversable, callable $fn)
|
||||
public static function map($iterable, callable $fn) : array
|
||||
{
|
||||
self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable');
|
||||
self::invariant(
|
||||
is_array($iterable) || $iterable instanceof Traversable,
|
||||
__METHOD__ . ' expects array or Traversable'
|
||||
);
|
||||
|
||||
$map = [];
|
||||
foreach ($traversable as $key => $value) {
|
||||
foreach ($iterable as $key => $value) {
|
||||
$map[$key] = $fn($value, $key);
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $traversable
|
||||
* @param callable $fn
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
* @param iterable<mixed> $iterable
|
||||
*
|
||||
* @return array<mixed>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function mapKeyValue($traversable, callable $fn)
|
||||
public static function mapKeyValue($iterable, callable $fn) : array
|
||||
{
|
||||
self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable');
|
||||
self::invariant(
|
||||
is_array($iterable) || $iterable instanceof Traversable,
|
||||
__METHOD__ . ' expects array or Traversable'
|
||||
);
|
||||
|
||||
$map = [];
|
||||
foreach ($traversable as $key => $value) {
|
||||
list($newKey, $newValue) = $fn($value, $key);
|
||||
$map[$newKey] = $newValue;
|
||||
foreach ($iterable as $key => $value) {
|
||||
[$newKey, $newValue] = $fn($value, $key);
|
||||
$map[$newKey] = $newValue;
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $traversable
|
||||
* @param callable $keyFn function($value, $key) => $newKey
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
* @param iterable<mixed> $iterable
|
||||
*
|
||||
* @return array<mixed>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function keyMap($traversable, callable $keyFn)
|
||||
public static function keyMap($iterable, callable $keyFn) : array
|
||||
{
|
||||
self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable');
|
||||
self::invariant(
|
||||
is_array($iterable) || $iterable instanceof Traversable,
|
||||
__METHOD__ . ' expects array or Traversable'
|
||||
);
|
||||
|
||||
$map = [];
|
||||
foreach ($traversable as $key => $value) {
|
||||
foreach ($iterable as $key => $value) {
|
||||
$newKey = $keyFn($value, $key);
|
||||
if (is_scalar($newKey)) {
|
||||
$map[$newKey] = $value;
|
||||
if (! is_scalar($newKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$map[$newKey] = $value;
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $traversable
|
||||
* @param callable $fn
|
||||
* @param iterable<mixed> $iterable
|
||||
*/
|
||||
public static function each($traversable, callable $fn)
|
||||
public static function each($iterable, callable $fn) : void
|
||||
{
|
||||
self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable');
|
||||
self::invariant(
|
||||
is_array($iterable) || $iterable instanceof Traversable,
|
||||
__METHOD__ . ' expects array or Traversable'
|
||||
);
|
||||
|
||||
foreach ($traversable as $key => $item) {
|
||||
foreach ($iterable as $key => $item) {
|
||||
$fn($item, $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits original traversable to several arrays with keys equal to $keyFn return
|
||||
* Splits original iterable to several arrays with keys equal to $keyFn return
|
||||
*
|
||||
* E.g. Utils::groupBy([1, 2, 3, 4, 5], function($value) {return $value % 3}) will output:
|
||||
* [
|
||||
@@ -164,19 +252,22 @@ class Utils
|
||||
*
|
||||
* $keyFn is also allowed to return array of keys. Then value will be added to all arrays with given keys
|
||||
*
|
||||
* @param $traversable
|
||||
* @param callable $keyFn function($value, $key) => $newKey(s)
|
||||
* @return array
|
||||
* @param iterable<mixed> $iterable
|
||||
*
|
||||
* @return array<array<mixed>>
|
||||
*/
|
||||
public static function groupBy($traversable, callable $keyFn)
|
||||
public static function groupBy($iterable, callable $keyFn) : array
|
||||
{
|
||||
self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable');
|
||||
self::invariant(
|
||||
is_array($iterable) || $iterable instanceof Traversable,
|
||||
__METHOD__ . ' expects array or Traversable'
|
||||
);
|
||||
|
||||
$grouped = [];
|
||||
foreach ($traversable as $key => $value) {
|
||||
foreach ($iterable as $key => $value) {
|
||||
$newKeys = (array) $keyFn($value, $key);
|
||||
foreach ($newKeys as $key) {
|
||||
$grouped[$key][] = $value;
|
||||
foreach ($newKeys as $newKey) {
|
||||
$grouped[$newKey][] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,56 +275,68 @@ class Utils
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|Traversable $traversable
|
||||
* @param callable $keyFn
|
||||
* @param callable $valFn
|
||||
* @return array
|
||||
* @param iterable<mixed> $iterable
|
||||
*
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public static function keyValMap($traversable, callable $keyFn, callable $valFn)
|
||||
public static function keyValMap($iterable, callable $keyFn, callable $valFn) : array
|
||||
{
|
||||
$map = [];
|
||||
foreach ($traversable as $item) {
|
||||
foreach ($iterable as $item) {
|
||||
$map[$keyFn($item)] = $valFn($item);
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $traversable
|
||||
* @param callable $predicate
|
||||
* @return bool
|
||||
* @param iterable<mixed> $iterable
|
||||
*/
|
||||
public static function every($traversable, callable $predicate)
|
||||
public static function every($iterable, callable $predicate) : bool
|
||||
{
|
||||
foreach ($traversable as $key => $value) {
|
||||
if (!$predicate($value, $key)) {
|
||||
foreach ($iterable as $key => $value) {
|
||||
if (! $predicate($value, $key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $test
|
||||
* @param iterable<mixed> $iterable
|
||||
*/
|
||||
public static function some($iterable, callable $predicate) : bool
|
||||
{
|
||||
foreach ($iterable as $key => $value) {
|
||||
if ($predicate($value, $key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $test
|
||||
* @param string $message
|
||||
* @param mixed $sprintfParam1
|
||||
* @param mixed $sprintfParam2 ...
|
||||
* @throws InvariantViolation
|
||||
*/
|
||||
public static function invariant($test, $message = '')
|
||||
{
|
||||
if (!$test) {
|
||||
if (! $test) {
|
||||
if (func_num_args() > 2) {
|
||||
$args = func_get_args();
|
||||
array_shift($args);
|
||||
$message = call_user_func_array('sprintf', $args);
|
||||
$message = sprintf(...$args);
|
||||
}
|
||||
// TODO switch to Error here
|
||||
throw new InvariantViolation($message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $var
|
||||
* @param Type|mixed $var
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getVariableType($var)
|
||||
@@ -243,61 +346,51 @@ class Utils
|
||||
if ($var instanceof WrappingType) {
|
||||
$var = $var->getWrappedType(true);
|
||||
}
|
||||
|
||||
return $var->name;
|
||||
}
|
||||
|
||||
return is_object($var) ? get_class($var) : gettype($var);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $var
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function printSafeJson($var)
|
||||
{
|
||||
if ($var instanceof \stdClass) {
|
||||
if ($var instanceof stdClass) {
|
||||
$var = (array) $var;
|
||||
}
|
||||
if (is_array($var)) {
|
||||
$count = count($var);
|
||||
if (!isset($var[0]) && $count > 0) {
|
||||
$keys = [];
|
||||
$keyCount = 0;
|
||||
foreach ($var as $key => $value) {
|
||||
$keys[] = '"' . $key . '"';
|
||||
if ($keyCount++ > 4) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$keysLabel = $keyCount === 1 ? 'key' : 'keys';
|
||||
$msg = "object with first $keysLabel: " . implode(', ', $keys);
|
||||
} else {
|
||||
$msg = "array($count)";
|
||||
}
|
||||
return $msg;
|
||||
return json_encode($var);
|
||||
}
|
||||
if ('' === $var) {
|
||||
if ($var === '') {
|
||||
return '(empty string)';
|
||||
}
|
||||
if (null === $var) {
|
||||
if ($var === null) {
|
||||
return 'null';
|
||||
}
|
||||
if (false === $var) {
|
||||
if ($var === false) {
|
||||
return 'false';
|
||||
}
|
||||
if (true === $var) {
|
||||
return 'false';
|
||||
if ($var === true) {
|
||||
return 'true';
|
||||
}
|
||||
if (is_string($var)) {
|
||||
return "\"$var\"";
|
||||
return sprintf('"%s"', $var);
|
||||
}
|
||||
if (is_scalar($var)) {
|
||||
return (string) $var;
|
||||
}
|
||||
|
||||
return gettype($var);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $var
|
||||
* @param Type|mixed $var
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function printSafe($var)
|
||||
@@ -306,44 +399,34 @@ class Utils
|
||||
return $var->toString();
|
||||
}
|
||||
if (is_object($var)) {
|
||||
if (method_exists($var, '__toString')) {
|
||||
return (string) $var;
|
||||
}
|
||||
|
||||
return 'instance of ' . get_class($var);
|
||||
}
|
||||
if (is_array($var)) {
|
||||
$count = count($var);
|
||||
if (!isset($var[0]) && $count > 0) {
|
||||
$keys = [];
|
||||
$keyCount = 0;
|
||||
foreach ($var as $key => $value) {
|
||||
$keys[] = '"' . $key . '"';
|
||||
if ($keyCount++ > 4) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$keysLabel = $keyCount === 1 ? 'key' : 'keys';
|
||||
$msg = "associative array($count) with first $keysLabel: " . implode(', ', $keys);
|
||||
} else {
|
||||
$msg = "array($count)";
|
||||
}
|
||||
return $msg;
|
||||
return json_encode($var);
|
||||
}
|
||||
if ('' === $var) {
|
||||
if ($var === '') {
|
||||
return '(empty string)';
|
||||
}
|
||||
if (null === $var) {
|
||||
if ($var === null) {
|
||||
return 'null';
|
||||
}
|
||||
if (false === $var) {
|
||||
if ($var === false) {
|
||||
return 'false';
|
||||
}
|
||||
if (true === $var) {
|
||||
return 'false';
|
||||
if ($var === true) {
|
||||
return 'true';
|
||||
}
|
||||
if (is_string($var)) {
|
||||
return "\"$var\"";
|
||||
return $var;
|
||||
}
|
||||
if (is_scalar($var)) {
|
||||
return (string) $var;
|
||||
}
|
||||
|
||||
return gettype($var);
|
||||
}
|
||||
|
||||
@@ -352,18 +435,16 @@ class Utils
|
||||
*
|
||||
* @param string $ord
|
||||
* @param string $encoding
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function chr($ord, $encoding = 'UTF-8')
|
||||
{
|
||||
if ($ord <= 255) {
|
||||
return chr($ord);
|
||||
}
|
||||
if ($encoding === 'UCS-4BE') {
|
||||
return pack("N", $ord);
|
||||
} else {
|
||||
return mb_convert_encoding(self::chr($ord, 'UCS-4BE'), $encoding, 'UCS-4BE');
|
||||
return pack('N', $ord);
|
||||
}
|
||||
|
||||
return mb_convert_encoding(self::chr($ord, 'UCS-4BE'), $encoding, 'UCS-4BE');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -371,97 +452,116 @@ class Utils
|
||||
*
|
||||
* @param string $char
|
||||
* @param string $encoding
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function ord($char, $encoding = 'UTF-8')
|
||||
{
|
||||
if (!$char && '0' !== $char) {
|
||||
if (! $char && $char !== '0') {
|
||||
return 0;
|
||||
}
|
||||
if (!isset($char[1])) {
|
||||
if (! isset($char[1])) {
|
||||
return ord($char);
|
||||
}
|
||||
if ($encoding !== 'UCS-4BE') {
|
||||
$char = mb_convert_encoding($char, 'UCS-4BE', $encoding);
|
||||
}
|
||||
list(, $ord) = unpack('N', $char);
|
||||
return $ord;
|
||||
|
||||
return unpack('N', $char)[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns UTF-8 char code at given $positing of the $string
|
||||
*
|
||||
* @param $string
|
||||
* @param $position
|
||||
* @param string $string
|
||||
* @param int $position
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function charCodeAt($string, $position)
|
||||
{
|
||||
$char = mb_substr($string, $position, 1, 'UTF-8');
|
||||
|
||||
return self::ord($char);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $code
|
||||
* @param int|null $code
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function printCharCode($code)
|
||||
{
|
||||
if (null === $code) {
|
||||
if ($code === null) {
|
||||
return '<EOF>';
|
||||
}
|
||||
|
||||
return $code < 0x007F
|
||||
// Trust JSON for ASCII.
|
||||
? json_encode(Utils::chr($code))
|
||||
? json_encode(self::chr($code))
|
||||
// Otherwise print the escaped form.
|
||||
: '"\\u' . dechex($code) . '"';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* @param bool $isIntrospection
|
||||
* @throws InvariantViolation
|
||||
* Upholds the spec rules about naming.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public static function assertValidName($name, $isIntrospection = false)
|
||||
public static function assertValidName($name)
|
||||
{
|
||||
$regex = '/^[_a-zA-Z][_a-zA-Z0-9]*$/';
|
||||
|
||||
if (!$name || !is_string($name)) {
|
||||
throw new InvariantViolation(
|
||||
"Must be named. Unexpected name: " . self::printSafe($name)
|
||||
);
|
||||
}
|
||||
|
||||
if (!$isIntrospection && isset($name[1]) && $name[0] === '_' && $name[1] === '_') {
|
||||
Warning::warnOnce(
|
||||
'Name "'.$name.'" must not begin with "__", which is reserved by ' .
|
||||
'GraphQL introspection. In a future release of graphql this will ' .
|
||||
'become an exception',
|
||||
Warning::WARNING_NAME
|
||||
);
|
||||
}
|
||||
|
||||
if (!preg_match($regex, $name)) {
|
||||
throw new InvariantViolation(
|
||||
'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "'.$name.'" does not.'
|
||||
);
|
||||
$error = self::isValidNameError($name);
|
||||
if ($error) {
|
||||
throw $error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps original closure with PHP error handling (using set_error_handler).
|
||||
* Resulting closure will collect all PHP errors that occur during the call in $errors array.
|
||||
* Returns an Error if a name is invalid.
|
||||
*
|
||||
* @param callable $fn
|
||||
* @param \ErrorException[] $errors
|
||||
* @return \Closure
|
||||
* @param string $name
|
||||
* @param Node|null $node
|
||||
*
|
||||
* @return Error|null
|
||||
*/
|
||||
public static function isValidNameError($name, $node = null)
|
||||
{
|
||||
self::invariant(is_string($name), 'Expected string');
|
||||
|
||||
if (isset($name[1]) && $name[0] === '_' && $name[1] === '_') {
|
||||
return new Error(
|
||||
sprintf('Name "%s" must not begin with "__", which is reserved by ', $name) .
|
||||
'GraphQL introspection.',
|
||||
$node
|
||||
);
|
||||
}
|
||||
|
||||
if (! preg_match('/^[_a-zA-Z][_a-zA-Z0-9]*$/', $name)) {
|
||||
return new Error(
|
||||
sprintf('Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "%s" does not.', $name),
|
||||
$node
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps original callable with PHP error handling (using set_error_handler).
|
||||
* Resulting callable will collect all PHP errors that occur during the call in $errors array.
|
||||
*
|
||||
* @param ErrorException[] $errors
|
||||
*
|
||||
* @return callable
|
||||
*/
|
||||
public static function withErrorHandling(callable $fn, array &$errors)
|
||||
{
|
||||
return function() use ($fn, &$errors) {
|
||||
return static function () use ($fn, &$errors) {
|
||||
// Catch custom errors (to report them in query results)
|
||||
set_error_handler(function ($severity, $message, $file, $line) use (&$errors) {
|
||||
$errors[] = new \ErrorException($message, 0, $severity, $file, $line);
|
||||
set_error_handler(static function ($severity, $message, $file, $line) use (&$errors) : void {
|
||||
$errors[] = new ErrorException($message, 0, $severity, $file, $line);
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -471,4 +571,88 @@ class Utils
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $items
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function quotedOrList(array $items)
|
||||
{
|
||||
$items = array_map(
|
||||
static function ($item) : string {
|
||||
return sprintf('"%s"', $item);
|
||||
},
|
||||
$items
|
||||
);
|
||||
|
||||
return self::orList($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $items
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function orList(array $items)
|
||||
{
|
||||
if (count($items) === 0) {
|
||||
throw new LogicException('items must not need to be empty.');
|
||||
}
|
||||
$selected = array_slice($items, 0, 5);
|
||||
$selectedLength = count($selected);
|
||||
$firstSelected = $selected[0];
|
||||
|
||||
if ($selectedLength === 1) {
|
||||
return $firstSelected;
|
||||
}
|
||||
|
||||
return array_reduce(
|
||||
range(1, $selectedLength - 1),
|
||||
static function ($list, $index) use ($selected, $selectedLength) : string {
|
||||
return $list .
|
||||
($selectedLength > 2 ? ', ' : ' ') .
|
||||
($index === $selectedLength - 1 ? 'or ' : '') .
|
||||
$selected[$index];
|
||||
},
|
||||
$firstSelected
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an invalid input string and a list of valid options, returns a filtered
|
||||
* list of valid options sorted based on their similarity with the input.
|
||||
*
|
||||
* Includes a custom alteration from Damerau-Levenshtein to treat case changes
|
||||
* as a single edit which helps identify mis-cased values with an edit distance
|
||||
* of 1
|
||||
*
|
||||
* @param string $input
|
||||
* @param string[] $options
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function suggestionList($input, array $options)
|
||||
{
|
||||
$optionsByDistance = [];
|
||||
$threshold = mb_strlen($input) * 0.4 + 1;
|
||||
foreach ($options as $option) {
|
||||
if ($input === $option) {
|
||||
$distance = 0;
|
||||
} else {
|
||||
$distance = (strtolower($input) === strtolower($option)
|
||||
? 1
|
||||
: levenshtein($input, $option));
|
||||
}
|
||||
if ($distance > $threshold) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$optionsByDistance[$option] = $distance;
|
||||
}
|
||||
|
||||
asort($optionsByDistance);
|
||||
|
||||
return array_keys($optionsByDistance);
|
||||
}
|
||||
}
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use Exception;
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
use Traversable;
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
use function is_string;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Coerces a PHP value given a GraphQL Type.
|
||||
*
|
||||
* Returns either a value which is valid for the provided type or a list of
|
||||
* encountered coercion errors.
|
||||
*/
|
||||
class Value
|
||||
{
|
||||
/**
|
||||
* Given a type and any value, return a runtime value coerced to match the type.
|
||||
*
|
||||
* @param ScalarType|EnumType|InputObjectType|ListOfType|NonNull $type
|
||||
* @param mixed[] $path
|
||||
*/
|
||||
public static function coerceValue($value, InputType $type, $blameNode = null, ?array $path = null)
|
||||
{
|
||||
if ($type instanceof NonNull) {
|
||||
if ($value === null) {
|
||||
return self::ofErrors([
|
||||
self::coercionError(
|
||||
sprintf('Expected non-nullable type %s not to be null', $type),
|
||||
$blameNode,
|
||||
$path
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return self::coerceValue($value, $type->getWrappedType(), $blameNode, $path);
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
// Explicitly return the value null.
|
||||
return self::ofValue(null);
|
||||
}
|
||||
|
||||
if ($type instanceof ScalarType) {
|
||||
// Scalars determine if a value is valid via parseValue(), which can
|
||||
// throw to indicate failure. If it throws, maintain a reference to
|
||||
// the original error.
|
||||
try {
|
||||
return self::ofValue($type->parseValue($value));
|
||||
} catch (Throwable $error) {
|
||||
return self::ofErrors([
|
||||
self::coercionError(
|
||||
sprintf('Expected type %s', $type->name),
|
||||
$blameNode,
|
||||
$path,
|
||||
$error->getMessage(),
|
||||
$error
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($type instanceof EnumType) {
|
||||
if (is_string($value)) {
|
||||
$enumValue = $type->getValue($value);
|
||||
if ($enumValue) {
|
||||
return self::ofValue($enumValue->value);
|
||||
}
|
||||
}
|
||||
|
||||
$suggestions = Utils::suggestionList(
|
||||
Utils::printSafe($value),
|
||||
array_map(
|
||||
static function ($enumValue) : string {
|
||||
return $enumValue->name;
|
||||
},
|
||||
$type->getValues()
|
||||
)
|
||||
);
|
||||
|
||||
$didYouMean = $suggestions
|
||||
? 'did you mean ' . Utils::orList($suggestions) . '?'
|
||||
: null;
|
||||
|
||||
return self::ofErrors([
|
||||
self::coercionError(
|
||||
sprintf('Expected type %s', $type->name),
|
||||
$blameNode,
|
||||
$path,
|
||||
$didYouMean
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($type instanceof ListOfType) {
|
||||
$itemType = $type->getWrappedType();
|
||||
if (is_array($value) || $value instanceof Traversable) {
|
||||
$errors = [];
|
||||
$coercedValue = [];
|
||||
foreach ($value as $index => $itemValue) {
|
||||
$coercedItem = self::coerceValue(
|
||||
$itemValue,
|
||||
$itemType,
|
||||
$blameNode,
|
||||
self::atPath($path, $index)
|
||||
);
|
||||
if ($coercedItem['errors']) {
|
||||
$errors = self::add($errors, $coercedItem['errors']);
|
||||
} else {
|
||||
$coercedValue[] = $coercedItem['value'];
|
||||
}
|
||||
}
|
||||
|
||||
return $errors ? self::ofErrors($errors) : self::ofValue($coercedValue);
|
||||
}
|
||||
// Lists accept a non-list value as a list of one.
|
||||
$coercedItem = self::coerceValue($value, $itemType, $blameNode);
|
||||
|
||||
return $coercedItem['errors'] ? $coercedItem : self::ofValue([$coercedItem['value']]);
|
||||
}
|
||||
|
||||
if ($type instanceof InputObjectType) {
|
||||
if (! is_object($value) && ! is_array($value) && ! $value instanceof Traversable) {
|
||||
return self::ofErrors([
|
||||
self::coercionError(
|
||||
sprintf('Expected type %s to be an object', $type->name),
|
||||
$blameNode,
|
||||
$path
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
// Cast \stdClass to associative array before checking the fields. Note that the coerced value will be an array.
|
||||
if ($value instanceof stdClass) {
|
||||
$value = (array) $value;
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$coercedValue = [];
|
||||
$fields = $type->getFields();
|
||||
foreach ($fields as $fieldName => $field) {
|
||||
if (array_key_exists($fieldName, $value)) {
|
||||
$fieldValue = $value[$fieldName];
|
||||
$coercedField = self::coerceValue(
|
||||
$fieldValue,
|
||||
$field->getType(),
|
||||
$blameNode,
|
||||
self::atPath($path, $fieldName)
|
||||
);
|
||||
if ($coercedField['errors']) {
|
||||
$errors = self::add($errors, $coercedField['errors']);
|
||||
} else {
|
||||
$coercedValue[$fieldName] = $coercedField['value'];
|
||||
}
|
||||
} elseif ($field->defaultValueExists()) {
|
||||
$coercedValue[$fieldName] = $field->defaultValue;
|
||||
} elseif ($field->getType() instanceof NonNull) {
|
||||
$fieldPath = self::printPath(self::atPath($path, $fieldName));
|
||||
$errors = self::add(
|
||||
$errors,
|
||||
self::coercionError(
|
||||
sprintf(
|
||||
'Field %s of required type %s was not provided',
|
||||
$fieldPath,
|
||||
$field->getType()->toString()
|
||||
),
|
||||
$blameNode
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure every provided field is defined.
|
||||
foreach ($value as $fieldName => $field) {
|
||||
if (array_key_exists($fieldName, $fields)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$suggestions = Utils::suggestionList(
|
||||
(string) $fieldName,
|
||||
array_keys($fields)
|
||||
);
|
||||
$didYouMean = $suggestions
|
||||
? 'did you mean ' . Utils::orList($suggestions) . '?'
|
||||
: null;
|
||||
$errors = self::add(
|
||||
$errors,
|
||||
self::coercionError(
|
||||
sprintf('Field "%s" is not defined by type %s', $fieldName, $type->name),
|
||||
$blameNode,
|
||||
$path,
|
||||
$didYouMean
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $errors ? self::ofErrors($errors) : self::ofValue($coercedValue);
|
||||
}
|
||||
|
||||
throw new Error(sprintf('Unexpected type %s', $type->name));
|
||||
}
|
||||
|
||||
private static function ofErrors($errors)
|
||||
{
|
||||
return ['errors' => $errors, 'value' => Utils::undefined()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @param Node $blameNode
|
||||
* @param mixed[]|null $path
|
||||
* @param string $subMessage
|
||||
* @param Exception|Throwable|null $originalError
|
||||
*
|
||||
* @return Error
|
||||
*/
|
||||
private static function coercionError(
|
||||
$message,
|
||||
$blameNode,
|
||||
?array $path = null,
|
||||
$subMessage = null,
|
||||
$originalError = null
|
||||
) {
|
||||
$pathStr = self::printPath($path);
|
||||
|
||||
// Return a GraphQLError instance
|
||||
return new Error(
|
||||
$message .
|
||||
($pathStr ? ' at ' . $pathStr : '') .
|
||||
($subMessage ? '; ' . $subMessage : '.'),
|
||||
$blameNode,
|
||||
null,
|
||||
[],
|
||||
null,
|
||||
$originalError
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a string describing the path into the value where the error was found
|
||||
*
|
||||
* @param mixed[]|null $path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function printPath(?array $path = null)
|
||||
{
|
||||
$pathStr = '';
|
||||
$currentPath = $path;
|
||||
while ($currentPath) {
|
||||
$pathStr =
|
||||
(is_string($currentPath['key'])
|
||||
? '.' . $currentPath['key']
|
||||
: '[' . $currentPath['key'] . ']') . $pathStr;
|
||||
$currentPath = $currentPath['prev'];
|
||||
}
|
||||
|
||||
return $pathStr ? 'value' . $pathStr : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return (mixed|null)[]
|
||||
*/
|
||||
private static function ofValue($value)
|
||||
{
|
||||
return ['errors' => null, 'value' => $value];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed|null $prev
|
||||
* @param mixed|null $key
|
||||
*
|
||||
* @return (mixed|null)[]
|
||||
*/
|
||||
private static function atPath($prev, $key)
|
||||
{
|
||||
return ['prev' => $prev, 'key' => $key];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Error[] $errors
|
||||
* @param Error|Error[] $moreErrors
|
||||
*
|
||||
* @return Error[]
|
||||
*/
|
||||
private static function add($errors, $moreErrors)
|
||||
{
|
||||
return array_merge($errors, is_array($moreErrors) ? $moreErrors : [$moreErrors]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user