Version 4.5.0

This commit is contained in:
Neo committed 2025-12-19 05:51:58 -08:00
1 parent 1a0c7fd3c2
commit f79dcf067a
3791 files changed
+248032 -76372

No files matched your search

+459
View File
@@ -0,0 +1,459 @@
<?php
namespace GraphQL\Utils;
use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\BooleanValueNode;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\EnumValueNode;
use GraphQL\Language\AST\FloatValueNode;
use GraphQL\Language\AST\IntValueNode;
use GraphQL\Language\AST\ListTypeNode;
use GraphQL\Language\AST\ListValueNode;
use GraphQL\Language\AST\Location;
use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\NonNullTypeNode;
use GraphQL\Language\AST\NullValueNode;
use GraphQL\Language\AST\ObjectFieldNode;
use GraphQL\Language\AST\ObjectValueNode;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Language\AST\VariableNode;
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\Type;
use GraphQL\Type\Schema;
use GraphQL\Utils\Utils;
/**
* Various utilities dealing with AST
*/
class AST
{
/**
* Convert representation of AST as an associative array to instance of GraphQL\Language\AST\Node.
*
* For example:
*
* ```php
* AST::fromArray([
* 'kind' => 'ListValue',
* 'values' => [
* ['kind' => 'StringValue', 'value' => 'my str'],
* ['kind' => 'StringValue', 'value' => 'my other str']
* ],
* 'loc' => ['start' => 21, 'end' => 25]
* ]);
* ```
*
* Will produce instance of `ListValueNode` where `values` prop is a lazily-evaluated `NodeList`
* returning instances of `StringValueNode` on access.
*
* This is a reverse operation for AST::toArray($node)
*
* @api
* @param array $node
* @return Node
*/
public static function fromArray(array $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];
$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 (is_array($value)) {
if (isset($value[0]) || empty($value)) {
$value = new NodeList($value);
} else {
$value = self::fromArray($value);
}
}
$instance->{$key} = $value;
}
return $instance;
}
/**
* Convert AST node to serializable array
*
* @api
* @param Node $node
* @return array
*/
public static function toArray(Node $node)
{
return $node->toArray(true);
}
/**
* Produces a GraphQL Value AST given a PHP value.
*
* Optionally, a GraphQL type may be provided, which will be used to
* disambiguate between value primitives.
*
* | PHP Value | GraphQL Value |
* | ------------- | -------------------- |
* | Object | Input Object |
* | Assoc Array | Input Object |
* | Array | List |
* | Boolean | Boolean |
* | String | String / Enum Value |
* | Int | Int |
* | Float | Int / Float |
* | Mixed | Enum Value |
* | null | NullValue |
*
* @api
* @param $value
* @param InputType $type
* @return ObjectValueNode|ListValueNode|BooleanValueNode|IntValueNode|FloatValueNode|EnumValueNode|StringValueNode|NullValueNode
*/
static function astFromValue($value, InputType $type)
{
if ($type instanceof NonNull) {
$astValue = self::astFromValue($value, $type->getWrappedType());
if ($astValue instanceof NullValueNode) {
return null;
}
return $astValue;
}
if ($value === null) {
return new NullValueNode([]);
}
// Convert PHP array to GraphQL list. If the GraphQLType is a list, but
// 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)) {
$valuesNodes = [];
foreach ($value as $item) {
$itemNode = self::astFromValue($item, $itemType);
if ($itemNode) {
$valuesNodes[] = $itemNode;
}
}
return new ListValueNode(['values' => $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))) {
return null;
}
$fields = $type->getFields();
$fieldNodes = [];
foreach ($fields as $fieldName => $field) {
if ($isArrayLike) {
$fieldValue = isset($value[$fieldName]) ? $value[$fieldName] : null;
} else {
$fieldValue = isset($value->{$fieldName}) ? $value->{$fieldName} : null;
}
// Have to check additionally if key exists, since we differentiate between
// "no key" and "value is null":
if (null !== $fieldValue) {
$fieldExists = true;
} else if ($isArray) {
$fieldExists = array_key_exists($fieldName, $value);
} else if ($isArrayLike) {
/** @var \ArrayAccess $value */
$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
]);
}
}
}
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));
}
if (null === $serialized) {
return null;
}
// 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]);
}
// 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)
]);
}
throw new InvariantViolation('Cannot convert value to AST: ' . Utils::printSafe($serialized));
}
/**
* Produces a PHP value given a GraphQL Value AST.
*
* A GraphQL type must be provided, which will be used to interpret different
* GraphQL Value literals.
*
* Returns `null` when the value could not be validly coerced according to
* the provided type.
*
* | GraphQL Value | PHP Value |
* | -------------------- | ------------- |
* | Input Object | Assoc Array |
* | List | Array |
* | Boolean | Boolean |
* | String | String |
* | Int / Float | Int / Float |
* | Enum Value | Mixed |
* | Null Value | null |
*
* @api
* @param $valueNode
* @param InputType $type
* @param null $variables
* @return array|null|\stdClass
* @throws \Exception
*/
public static function valueFromAST($valueNode, InputType $type, $variables = null)
{
$undefined = Utils::undefined();
if (!$valueNode) {
// When there is no AST, then there is also no value.
// Importantly, this is different from returning the GraphQL null value.
return $undefined;
}
if ($type instanceof NonNull) {
if ($valueNode instanceof NullValueNode) {
// Invalid: intentionally return no value.
return $undefined;
}
return self::valueFromAST($valueNode, $type->getWrappedType(), $variables);
}
if ($valueNode instanceof NullValueNode) {
// This is explicitly returning the value null.
return null;
}
if ($valueNode instanceof VariableNode) {
$variableName = $valueNode->name->value;
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.
return $variables[$variableName];
}
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if ($valueNode instanceof ListValueNode) {
$coercedValues = [];
$itemNodes = $valueNode->values;
foreach ($itemNodes as $itemNode) {
if (self::isMissingVariable($itemNode, $variables)) {
// If an array contains a missing variable, it is either coerced to
// null or if the item type is non-null, it considered invalid.
if ($itemType instanceof NonNull) {
// Invalid: intentionally return no value.
return $undefined;
}
$coercedValues[] = null;
} else {
$itemValue = self::valueFromAST($itemNode, $itemType, $variables);
if ($undefined === $itemValue) {
// Invalid: intentionally return no value.
return $undefined;
}
$coercedValues[] = $itemValue;
}
}
return $coercedValues;
}
$coercedValue = self::valueFromAST($valueNode, $itemType, $variables);
if ($undefined === $coercedValue) {
// Invalid: intentionally return no value.
return $undefined;
}
return [$coercedValue];
}
if ($type instanceof InputObjectType) {
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;});
foreach ($fields as $field) {
/** @var ValueNode $fieldNode */
$fieldName = $field->name;
$fieldNode = isset($fieldNodes[$fieldName]) ? $fieldNodes[$fieldName] : null;
if (!$fieldNode || self::isMissingVariable($fieldNode->value, $variables)) {
if ($field->defaultValueExists()) {
$coercedObj[$fieldName] = $field->defaultValue;
} else if ($field->getType() instanceof NonNull) {
// Invalid: intentionally return no value.
return $undefined;
}
continue ;
}
$fieldValue = self::valueFromAST($fieldNode ? $fieldNode->value : null, $field->getType(), $variables);
if ($undefined === $fieldValue) {
// Invalid: intentionally return no value.
return $undefined;
}
$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.
return $undefined;
}
return $parsed;
}
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;
}
Utils::invariant($inputTypeNode && $inputTypeNode instanceof NamedTypeNode, 'Must be a named type');
return $schema->getType($inputTypeNode->name->value);
}
/**
* Returns true if the provided valueNode is a variable which is not defined
* in the set of variables.
* @param $valueNode
* @param $variables
* @return bool
*/
private static function isMissingVariable($valueNode, $variables)
{
return $valueNode instanceof VariableNode &&
(!$variables || !array_key_exists($valueNode->name->value, $variables));
}
/**
* Returns operation type ("query", "mutation" or "subscription") given a document and operation name
*
* @api
* @param DocumentNode $document
* @param string $operationName
* @return bool
*/
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;
}
}
}
}
return false;
}
}
+647
View File
@@ -0,0 +1,647 @@
<?php
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\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;
/**
* Build instance of `GraphQL\Type\Schema` out of type language definition (string or parsed AST)
* See [section in docs](type-system/type-language.md) for details.
*/
class BuildSchema
{
/**
* @param Type $innerType
* @param TypeNode $inputTypeNode
* @return Type
*/
private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode)
{
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;
}
private function getNamedTypeNode(TypeNode $typeNode)
{
$namedType = $typeNode;
while ($namedType->kind === NodeKind::LIST_TYPE || $namedType->kind === NodeKind::NON_NULL_TYPE) {
$namedType = $namedType->type;
}
return $namedType;
}
/**
* This takes the ast of a schema document produced by the parse function in
* GraphQL\Language\Parser.
*
* If no schema definition is provided, then it will look for types named Query
* and Mutation.
*
* 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
* @return Schema
* @throws Error
*/
public static function buildAST(DocumentNode $ast, callable $typeConfigDecorator = null)
{
$builder = new self($ast, $typeConfigDecorator);
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 = [];
$this->nodeMap = [];
$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;
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.");
}
$typeDefs[] = $d;
$this->nodeMap[$typeName] = $d;
break;
case NodeKind::DIRECTIVE_DEFINITION:
$directiveDefs[] = $d;
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';
}
}
if (!$queryTypeName) {
throw new Error(
'Must provide schema definition with query type or a type named Query.'
);
}
$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);
// If specified directives were not explicitly declared, add them.
$skip = array_reduce($directives, function($hasSkip, $directive) {
return $hasSkip || $directive->name == 'skip';
});
if (!$skip) {
$directives[] = Directive::skipDirective();
}
$include = array_reduce($directives, function($hasInclude, $directive) {
return $hasInclude || $directive->name == 'include';
});
if (!$include) {
$directives[] = Directive::includeDirective();
}
$deprecated = array_reduce($directives, function($hasDeprecated, $directive) {
return $hasDeprecated || $directive->name == 'deprecated';
});
if (!$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);
},
'directives' => $directives,
'astNode' => $schemaDef,
'types' => function() {
$types = [];
foreach ($this->nodeMap as $name => $def) {
if (!isset($this->loadedTypeDefs[$name])) {
$types[] = $this->typeDefNamed($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 EnumValueDefinitionNode | FieldDefinitionNode $node
* @return string
*/
private function getDeprecationReason($node)
{
$deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node);
return isset($deprecated['reason']) ? $deprecated['reason'] : null;
}
/**
* 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;
}
$comments[] = $value;
$token = $token->prev;
}
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);
}
// 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.'
);
}
}
@@ -0,0 +1,583 @@
<?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
);
}
}
+254
View File
@@ -0,0 +1,254 @@
<?php
namespace GraphQL\Utils;
/**
* 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
*/
class MixedStore implements \ArrayAccess
{
/**
* @var array
*/
private $standardStore;
/**
* @var array
*/
private $floatStore;
/**
* @var \SplObjectStorage
*/
private $objectStore;
/**
* @var array
*/
private $arrayKeys;
/**
* @var array
*/
private $arrayValues;
/**
* @var array
*/
private $lastArrayKey;
/**
* @var mixed
*/
private $lastArrayValue;
/**
* @var mixed
*/
private $nullValue;
/**
* @var bool
*/
private $nullValueIsSet;
/**
* @var mixed
*/
private $trueValue;
/**
* @var bool
*/
private $trueValueIsSet;
/**
* @var mixed
*/
private $falseValue;
/**
* @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->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.
* </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) {
return $this->falseValueIsSet;
}
if (true === $offset) {
return $this->trueValueIsSet;
}
if (is_int($offset) || is_string($offset)) {
return array_key_exists($offset, $this->standardStore);
}
if (is_float($offset)) {
return array_key_exists((string) $offset, $this->floatStore);
}
if (is_object($offset)) {
return $this->objectStore->offsetExists($offset);
}
if (is_array($offset)) {
foreach ($this->arrayKeys as $index => $entry) {
if ($entry === $offset) {
$this->lastArrayKey = $offset;
$this->lastArrayValue = $this->arrayValues[$index];
return true;
}
}
}
if (null === $offset) {
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) {
return $this->trueValue;
}
if (false === $offset) {
return $this->falseValue;
}
if (is_int($offset) || is_string($offset)) {
return $this->standardStore[$offset];
}
if (is_float($offset)) {
return $this->floatStore[(string)$offset];
}
if (is_object($offset)) {
return $this->objectStore->offsetGet($offset);
}
if (is_array($offset)) {
// offsetGet is often called directly after offsetExists, so optimize to avoid second loop:
if ($this->lastArrayKey === $offset) {
return $this->lastArrayValue;
}
foreach ($this->arrayKeys as $index => $entry) {
if ($entry === $offset) {
return $this->arrayValues[$index];
}
}
}
if (null === $offset) {
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>
* @return void
* @since 5.0.0
*/
public function offsetSet($offset, $value)
{
if (false === $offset) {
$this->falseValue = $value;
$this->falseValueIsSet = true;
} else if (true === $offset) {
$this->trueValue = $value;
$this->trueValueIsSet = true;
} else if (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)) {
$this->objectStore[$offset] = $value;
} else if (is_array($offset)) {
$this->arrayKeys[] = $offset;
$this->arrayValues[] = $value;
} else if (null === $offset) {
$this->nullValue = $value;
$this->nullValueIsSet = true;
} else {
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;
$this->trueValueIsSet = false;
} else if (false === $offset) {
$this->falseValue = null;
$this->falseValueIsSet = false;
} else if (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)) {
$this->objectStore->offsetUnset($offset);
} else if (is_array($offset)) {
$index = array_search($offset, $this->arrayKeys, true);
if (false !== $index) {
array_splice($this->arrayKeys, $index, 1);
array_splice($this->arrayValues, $index, 1);
}
} else if (null === $offset) {
$this->nullValue = null;
$this->nullValueIsSet = false;
}
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
namespace GraphQL\Utils;
/**
* Class PairSet
* @package GraphQL\Utils
*/
class PairSet
{
/**
* @var \SplObjectStorage<any, Set<any>>
*/
private $data;
/**
* @var array
*/
private $wrappers = [];
/**
* PairSet constructor.
*/
public function __construct()
{
$this->data = new \SplObjectStorage(); // SplObject hash instead?
}
/**
* @param $a
* @param $b
* @return null|object
*/
public function has($a, $b)
{
$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;
}
if (!isset($this->wrappers[$var])) {
$tmp = new \stdClass();
$tmp->_internal = $var;
$this->wrappers[$var] = $tmp;
}
return $this->wrappers[$var];
}
/**
* @param $a
* @param $b
*/
private function pairSetAdd($a, $b)
{
$a = $this->toObj($a);
$b = $this->toObj($b);
$set = isset($this->data[$a]) ? $this->data[$a] : null;
if (!isset($set)) {
$set = new \SplObjectStorage();
$this->data[$a] = $set;
}
$set[$b] = true;
}
}
+313
View File
@@ -0,0 +1,313 @@
<?php
namespace GraphQL\Utils;
use GraphQL\Language\Printer;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\CompositeType;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
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;
/**
* Given an instance of Schema, prints it in GraphQL type language.
*/
class SchemaPrinter
{
/**
* @api
* @param Schema $schema
* @return string
*/
public static function doPrint(Schema $schema)
{
return self::printFilteredSchema($schema, function($n) {
return !self::isSpecDirective($n);
}, 'self::isDefinedType');
}
/**
* @api
* @param Schema $schema
* @return string
*/
public static function printIntrosepctionSchema(Schema $schema)
{
return self::printFilteredSchema($schema, [__CLASS__, 'isSpecDirective'], [__CLASS__, 'isIntrospectionType']);
}
private static function isSpecDirective($directiveName)
{
return (
$directiveName === 'skip' ||
$directiveName === 'include' ||
$directiveName === 'deprecated'
);
}
private static function isDefinedType($typename)
{
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;
}
$operationTypes = [];
$queryType = $schema->getQueryType();
if ($queryType) {
$operationTypes[] = " query: {$queryType->name}";
}
$mutationType = $schema->getMutationType();
if ($mutationType) {
$operationTypes[] = " mutation: {$mutationType->name}";
}
$subscriptionType = $schema->getSubscriptionType();
if ($subscriptionType) {
$operationTypes[] = " subscription: {$subscriptionType->name}";
}
return "schema {\n" . implode("\n", $operationTypes) . "\n}";
}
/**
* 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
* a common naming convention:
*
* schema {
* query: Query
* mutation: Mutation
* }
*
* When using this naming convention, the schema description can be omitted.
*/
private static function isSchemaOfCommonNames(Schema $schema)
{
$queryType = $schema->getQueryType();
if ($queryType && $queryType->name !== 'Query') {
return false;
}
$mutationType = $schema->getMutationType();
if ($mutationType && $mutationType->name !== 'Mutation') {
return false;
}
$subscriptionType = $schema->getSubscriptionType();
if ($subscriptionType && $subscriptionType->name !== 'Subscription') {
return false;
}
return true;
}
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);
}
Utils::invariant($type instanceof InputObjectType);
return self::printInputObject($type);
}
private static function printScalar(ScalarType $type)
{
return self::printDescription($type) . "scalar {$type->name}";
}
private static function printObject(ObjectType $type)
{
$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" .
"}";
}
private static function printInterface(InterfaceType $type)
{
return self::printDescription($type) .
"interface {$type->name} {\n" .
self::printFields($type) . "\n" .
"}";
}
private static function printUnion(UnionType $type)
{
return self::printDescription($type) .
"union {$type->name} = " . implode(" | ", $type->getTypes());
}
private static function printEnum(EnumType $type)
{
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) {
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)) . ')';
}
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 . ')';
}
private static function printInputValue($arg)
{
$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)
{
return self::printDescription($directive) .
'directive @' . $directive->name . self::printArgs($directive->args) .
' on ' . implode(' | ', $directive->locations);
}
private static function printDeprecated($fieldOrEnumVal)
{
$reason = $fieldOrEnumVal->deprecationReason;
if (empty($reason)) {
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)
{
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;
}
private static function breakLine($line, $len)
{
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);
}
}
+137
View File
@@ -0,0 +1,137 @@
<?php
namespace GraphQL\Utils;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\CompositeType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
class TypeComparators
{
/**
* Provided two types, return true if the types are equal (invariant).
*
* @param Type $typeA
* @param Type $typeB
* @return bool
*/
public static function isEqualType(Type $typeA, Type $typeB)
{
// Equivalent types are equal.
if ($typeA === $typeB) {
return true;
}
// If either type is non-null, the other must also be non-null.
if ($typeA instanceof NonNull && $typeB instanceof NonNull) {
return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType());
}
// If either type is a list, the other must also be a list.
if ($typeA instanceof ListOfType && $typeB instanceof ListOfType) {
return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType());
}
// Otherwise the types are not equal.
return false;
}
/**
* Provided a type and a super type, return true if the first type is either
* equal or a subset of the second super type (covariant).
*
* @param Schema $schema
* @param Type $maybeSubType
* @param Type $superType
* @return bool
*/
static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType)
{
// Equivalent type is a valid subtype
if ($maybeSubType === $superType) {
return true;
}
// If superType is non-null, maybeSubType must also be nullable.
if ($superType instanceof NonNull) {
if ($maybeSubType instanceof NonNull) {
return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType());
}
return false;
} else if ($maybeSubType instanceof NonNull) {
// If superType is nullable, maybeSubType may be non-null.
return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType);
}
// If superType type is a list, maybeSubType type must also be a list.
if ($superType instanceof ListOfType) {
if ($maybeSubType instanceof ListOfType) {
return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType());
}
return false;
} else if ($maybeSubType instanceof ListOfType) {
// If superType is not a list, maybeSubType must also be not a list.
return false;
}
// If superType type is an abstract type, maybeSubType type may be a currently
// possible object type.
if (Type::isAbstractType($superType) && $maybeSubType instanceof ObjectType && $schema->isPossibleType($superType, $maybeSubType)) {
return true;
}
// Otherwise, the child type is not a valid subtype of the parent type.
return false;
}
/**
* Provided two composite types, determine if they "overlap". Two composite
* types overlap when the Sets of possible concrete types for each intersect.
*
* This is often used to determine if a fragment of a given type could possibly
* be visited in a context of another type.
*
* This function is commutative.
*
* @param Schema $schema
* @param CompositeType $typeA
* @param CompositeType $typeB
* @return bool
*/
static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB)
{
// Equivalent types overlap
if ($typeA === $typeB) {
return true;
}
if ($typeA instanceof AbstractType) {
if ($typeB instanceof AbstractType) {
// If both types are abstract, then determine if there is any intersection
// between possible concrete types of each.
foreach ($schema->getPossibleTypes($typeA) as $type) {
if ($schema->isPossibleType($typeB, $type)) {
return true;
}
}
return false;
}
/** @var $typeB ObjectType */
// Determine if the latter type is a possible concrete type of the former.
return $schema->isPossibleType($typeA, $typeB);
}
if ($typeB instanceof AbstractType) {
/** @var $typeA ObjectType */
// Determine if the former type is a possible concrete type of the latter.
return $schema->isPossibleType($typeB, $typeA);
}
// Otherwise the types do not overlap.
return false;
}
}
+423
View File
@@ -0,0 +1,423 @@
<?php
namespace GraphQL\Utils;
use GraphQL\Error\InvariantViolation;
use GraphQL\Error\Warning;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\ListTypeNode;
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\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\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\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Definition\WrappingType;
use GraphQL\Type\Introspection;
/**
* Class TypeInfo
* @package GraphQL\Utils
*/
class TypeInfo
{
/**
* @deprecated moved to GraphQL\Utils\TypeComparators
*/
public static function isEqualType(Type $typeA, Type $typeB)
{
return TypeComparators::isEqualType($typeA, $typeB);
}
/**
* @deprecated moved to GraphQL\Utils\TypeComparators
*/
static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType)
{
return TypeComparators::isTypeSubTypeOf($schema, $maybeSubType, $superType);
}
/**
* @deprecated moved to GraphQL\Utils\TypeComparators
*/
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.
*
* Example output:
* [
* 'String' => $instanceOfStringType,
* 'MyType' => $instanceOfMyType,
* ...
* ]
*
* @param Type $type
* @param array|null $typeMap
* @return array
*/
public static function extractTypes($type, array $typeMap = null)
{
if (!$typeMap) {
$typeMap = [];
}
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
);
return $typeMap;
}
if (!empty($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)."
);
return $typeMap;
}
$typeMap[$type->name] = $type;
$nestedTypes = [];
if ($type instanceof UnionType) {
$nestedTypes = $type->getTypes();
}
if ($type instanceof ObjectType) {
$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);
$nestedTypes = array_merge($nestedTypes, $fieldArgTypes);
}
$nestedTypes[] = $field->getType();
}
}
foreach ($nestedTypes as $type) {
$typeMap = self::extractTypes($type, $typeMap);
}
return $typeMap;
}
/**
* 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)
{
$name = $fieldNode->name->value;
$schemaMeta = Introspection::schemaMetaFieldDef();
if ($name === $schemaMeta->name && $schema->getQueryType() === $parentType) {
return $schemaMeta;
}
$typeMeta = Introspection::typeMetaFieldDef();
if ($name === $typeMeta->name && $schema->getQueryType() === $parentType) {
return $typeMeta;
}
$typeNameMeta = Introspection::typeNameMetaFieldDef();
if ($name === $typeNameMeta->name && $parentType instanceof CompositeType) {
return $typeNameMeta;
}
if ($parentType instanceof ObjectType ||
$parentType instanceof InterfaceType) {
$fields = $parentType->getFields();
return isset($fields[$name]) ? $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
*/
function getParentType()
{
if (!empty($this->parentTypeStack)) {
return $this->parentTypeStack[count($this->parentTypeStack) - 1];
}
return null;
}
/**
* @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()
{
return $this->directive;
}
/**
* @return FieldArgument|null
*/
function getArgument()
{
return $this->argument;
}
/**
* @return mixed
*/
function getEnumValue()
{
return $this->enumValue;
}
/**
* @param Node $node
*/
function enter(Node $node)
{
$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;
}
}
/**
* @param Node $node
*/
function leave(Node $node)
{
switch ($node->kind) {
case NodeKind::SELECTION_SET:
array_pop($this->parentTypeStack);
break;
case NodeKind::FIELD:
array_pop($this->fieldDefStack);
array_pop($this->typeStack);
break;
case NodeKind::DIRECTIVE:
$this->directive = null;
break;
case NodeKind::OPERATION_DEFINITION:
case NodeKind::INLINE_FRAGMENT:
case NodeKind::FRAGMENT_DEFINITION:
array_pop($this->typeStack);
break;
case NodeKind::VARIABLE_DEFINITION:
array_pop($this->inputTypeStack);
break;
case NodeKind::ARGUMENT:
$this->argument = null;
array_pop($this->inputTypeStack);
break;
case NodeKind::LST:
case NodeKind::OBJECT_FIELD:
array_pop($this->inputTypeStack);
break;
case NodeKind::ENUM:
$this->enumValue = null;
break;
}
}
}
+474
View File
@@ -0,0 +1,474 @@
<?php
namespace GraphQL\Utils;
use GraphQL\Error\InvariantViolation;
use GraphQL\Error\Warning;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\WrappingType;
use \Traversable, \InvalidArgumentException;
class Utils
{
public static function undefined()
{
static $undefined;
return $undefined ?: $undefined = new \stdClass();
}
/**
* @param object $obj
* @param array $vars
* @param array $requiredKeys
*
* @return array
*/
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");
}
}
foreach ($vars as $key => $value) {
if (!property_exists($obj, $key)) {
$cls = get_class($obj);
Warning::warn(
"Trying to set non-existing property '$key' on class '$cls'",
Warning::WARNING_ASSIGN
);
}
$obj->{$key} = $value;
}
return $obj;
}
/**
* @param array|Traversable $traversable
* @param callable $predicate
* @return null
*/
public static function find($traversable, callable $predicate)
{
self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable');
foreach ($traversable as $key => $value) {
if ($predicate($value, $key)) {
return $value;
}
}
return null;
}
/**
* @param $traversable
* @param callable $predicate
* @return array
* @throws \Exception
*/
public static function filter($traversable, callable $predicate)
{
self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable');
$result = [];
$assoc = false;
foreach ($traversable as $key => $value) {
if (!$assoc && !is_int($key)) {
$assoc = true;
}
if ($predicate($value, $key)) {
$result[$key] = $value;
}
}
return $assoc ? $result : array_values($result);
}
/**
* @param array|\Traversable $traversable
* @param callable $fn function($value, $key) => $newValue
* @return array
* @throws \Exception
*/
public static function map($traversable, callable $fn)
{
self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable');
$map = [];
foreach ($traversable as $key => $value) {
$map[$key] = $fn($value, $key);
}
return $map;
}
/**
* @param $traversable
* @param callable $fn
* @return array
* @throws \Exception
*/
public static function mapKeyValue($traversable, callable $fn)
{
self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable');
$map = [];
foreach ($traversable as $key => $value) {
list($newKey, $newValue) = $fn($value, $key);
$map[$newKey] = $newValue;
}
return $map;
}
/**
* @param $traversable
* @param callable $keyFn function($value, $key) => $newKey
* @return array
* @throws \Exception
*/
public static function keyMap($traversable, callable $keyFn)
{
self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable');
$map = [];
foreach ($traversable as $key => $value) {
$newKey = $keyFn($value, $key);
if (is_scalar($newKey)) {
$map[$newKey] = $value;
}
}
return $map;
}
/**
* @param $traversable
* @param callable $fn
*/
public static function each($traversable, callable $fn)
{
self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable');
foreach ($traversable as $key => $item) {
$fn($item, $key);
}
}
/**
* Splits original traversable 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:
* [
* 1 => [1, 4],
* 2 => [2, 5],
* 0 => [3],
* ]
*
* $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
*/
public static function groupBy($traversable, callable $keyFn)
{
self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable');
$grouped = [];
foreach ($traversable as $key => $value) {
$newKeys = (array) $keyFn($value, $key);
foreach ($newKeys as $key) {
$grouped[$key][] = $value;
}
}
return $grouped;
}
/**
* @param array|Traversable $traversable
* @param callable $keyFn
* @param callable $valFn
* @return array
*/
public static function keyValMap($traversable, callable $keyFn, callable $valFn)
{
$map = [];
foreach ($traversable as $item) {
$map[$keyFn($item)] = $valFn($item);
}
return $map;
}
/**
* @param $traversable
* @param callable $predicate
* @return bool
*/
public static function every($traversable, callable $predicate)
{
foreach ($traversable as $key => $value) {
if (!$predicate($value, $key)) {
return false;
}
}
return true;
}
/**
* @param $test
* @param string $message
* @param mixed $sprintfParam1
* @param mixed $sprintfParam2 ...
* @throws InvariantViolation
*/
public static function invariant($test, $message = '')
{
if (!$test) {
if (func_num_args() > 2) {
$args = func_get_args();
array_shift($args);
$message = call_user_func_array('sprintf', $args);
}
throw new InvariantViolation($message);
}
}
/**
* @param $var
* @return string
*/
public static function getVariableType($var)
{
if ($var instanceof Type) {
// FIXME: Replace with schema printer call
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) {
$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;
}
if ('' === $var) {
return '(empty string)';
}
if (null === $var) {
return 'null';
}
if (false === $var) {
return 'false';
}
if (true === $var) {
return 'false';
}
if (is_string($var)) {
return "\"$var\"";
}
if (is_scalar($var)) {
return (string) $var;
}
return gettype($var);
}
/**
* @param $var
* @return string
*/
public static function printSafe($var)
{
if ($var instanceof Type) {
return $var->toString();
}
if (is_object($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;
}
if ('' === $var) {
return '(empty string)';
}
if (null === $var) {
return 'null';
}
if (false === $var) {
return 'false';
}
if (true === $var) {
return 'false';
}
if (is_string($var)) {
return "\"$var\"";
}
if (is_scalar($var)) {
return (string) $var;
}
return gettype($var);
}
/**
* UTF-8 compatible chr()
*
* @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');
}
}
/**
* UTF-8 compatible ord()
*
* @param string $char
* @param string $encoding
* @return mixed
*/
public static function ord($char, $encoding = 'UTF-8')
{
if (!$char && '0' !== $char) {
return 0;
}
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;
}
/**
* Returns UTF-8 char code at given $positing of the $string
*
* @param $string
* @param $position
* @return mixed
*/
public static function charCodeAt($string, $position)
{
$char = mb_substr($string, $position, 1, 'UTF-8');
return self::ord($char);
}
/**
* @param $code
* @return string
*/
public static function printCharCode($code)
{
if (null === $code) {
return '<EOF>';
}
return $code < 0x007F
// Trust JSON for ASCII.
? json_encode(Utils::chr($code))
// Otherwise print the escaped form.
: '"\\u' . dechex($code) . '"';
}
/**
* @param $name
* @param bool $isIntrospection
* @throws InvariantViolation
*/
public static function assertValidName($name, $isIntrospection = false)
{
$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.'
);
}
}
/**
* 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.
*
* @param callable $fn
* @param \ErrorException[] $errors
* @return \Closure
*/
public static function withErrorHandling(callable $fn, array &$errors)
{
return 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);
});
try {
return $fn();
} finally {
restore_error_handler();
}
};
}
}