mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-22 04:46:04 +03:00
Version 0.1
This commit is contained in:
parent
85bd2efaf7
commit
20c482ce2f
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
### Intellij ###
|
||||
.idea/
|
32
composer.json
Normal file
32
composer.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "webonyx/graphql-php",
|
||||
"description": "A PHP port of GraphQL reference implementation (https://github.com/graphql/graphql-js, ref: 099eeb7b5a49a16fa3d55353b9774291881e959c)",
|
||||
"type": "library",
|
||||
"license": "BSD",
|
||||
"homepage": "https://github.com/webonyx/graphql-php",
|
||||
"keywords": [
|
||||
"graphql",
|
||||
"API"
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.4,<8.0-DEV"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "4.7.6"
|
||||
},
|
||||
"config": {
|
||||
"bin-dir": "bin"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"classmap": [
|
||||
"tests/"
|
||||
],
|
||||
"files": [
|
||||
]
|
||||
}
|
||||
}
|
86
src/Error.php
Normal file
86
src/Error.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
namespace GraphQL;
|
||||
|
||||
use GraphQL\Language\Source;
|
||||
|
||||
// /graphql-js/src/error/index.js
|
||||
|
||||
class Error extends \Exception
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $message;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $stack;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $nodes;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $positions;
|
||||
|
||||
/**
|
||||
* @var array<SourceLocation>
|
||||
*/
|
||||
public $locations;
|
||||
|
||||
/**
|
||||
* @var Source|null
|
||||
*/
|
||||
public $source;
|
||||
|
||||
/**
|
||||
* @param $error
|
||||
* @return FormattedError
|
||||
*/
|
||||
public static function formatError($error)
|
||||
{
|
||||
if (is_array($error)) {
|
||||
$message = isset($error['message']) ? $error['message'] : null;
|
||||
$locations = isset($error['locations']) ? $error['locations'] : null;
|
||||
} else if ($error instanceof Error) {
|
||||
$message = $error->message;
|
||||
$locations = $error->locations;
|
||||
} else {
|
||||
$message = (string) $error;
|
||||
$locations = null;
|
||||
}
|
||||
|
||||
return new FormattedError($message, $locations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @param array|null $nodes
|
||||
* @param null $stack
|
||||
*/
|
||||
public function __construct($message, array $nodes = null, $stack = null)
|
||||
{
|
||||
$this->message = $message;
|
||||
$this->stack = $stack ?: $message;
|
||||
|
||||
if ($nodes) {
|
||||
$this->nodes = $nodes;
|
||||
$positions = array_map(function($node) { return isset($node->loc) ? $node->loc->start : null; }, $nodes);
|
||||
$positions = array_filter($positions);
|
||||
|
||||
if (!empty($positions)) {
|
||||
$this->positions = $positions;
|
||||
$loc = $nodes[0]->loc;
|
||||
$source = $loc ? $loc->source : null;
|
||||
if ($source) {
|
||||
$this->locations = array_map(function($pos) use($source) {return $source->getLocation($pos);}, $positions);
|
||||
$this->source = $source;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
60
src/Executor/ExecutionContext.php
Normal file
60
src/Executor/ExecutionContext.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
namespace GraphQL\Executor;
|
||||
|
||||
use GraphQL\Language\AST\OperationDefinition;
|
||||
use GraphQL\Schema;
|
||||
|
||||
/**
|
||||
* Data that must be available at all points during query execution.
|
||||
*
|
||||
* Namely, schema of the type system that is currently executing,
|
||||
* and the fragments defined in the query document
|
||||
*/
|
||||
class ExecutionContext
|
||||
{
|
||||
/**
|
||||
* @var Schema
|
||||
*/
|
||||
public $schema;
|
||||
|
||||
/**
|
||||
* @var array<string, FragmentDefinition>
|
||||
*/
|
||||
public $fragments;
|
||||
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
public $root;
|
||||
|
||||
/**
|
||||
* @var OperationDefinition
|
||||
*/
|
||||
public $operation;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $variables;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $errors;
|
||||
|
||||
public function __construct($schema, $fragments, $root, $operation, $variables, $errors)
|
||||
{
|
||||
$this->schema = $schema;
|
||||
$this->fragments = $fragments;
|
||||
$this->root = $root;
|
||||
$this->operation = $operation;
|
||||
$this->variables = $variables;
|
||||
$this->errors = $errors ?: [];
|
||||
}
|
||||
|
||||
public function addError($error)
|
||||
{
|
||||
$this->errors[] = $error;
|
||||
return $this;
|
||||
}
|
||||
}
|
540
src/Executor/Executor.php
Normal file
540
src/Executor/Executor.php
Normal file
@ -0,0 +1,540 @@
|
||||
<?php
|
||||
namespace GraphQL\Executor;
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Document;
|
||||
use GraphQL\Language\AST\Field;
|
||||
use GraphQL\Language\AST\FragmentDefinition;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\OperationDefinition;
|
||||
use GraphQL\Language\AST\SelectionSet;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
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\Introspection;
|
||||
use GraphQL\Utils;
|
||||
|
||||
/**
|
||||
* Terminology
|
||||
*
|
||||
* "Definitions" are the generic name for top-level statements in the document.
|
||||
* Examples of this include:
|
||||
* 1) Operations (such as a query)
|
||||
* 2) Fragments
|
||||
*
|
||||
* "Operations" are a generic name for requests in the document.
|
||||
* Examples of this include:
|
||||
* 1) query,
|
||||
* 2) mutation
|
||||
*
|
||||
* "Selections" are the statements that can appear legally and at
|
||||
* single level of the query. These include:
|
||||
* 1) field references e.g "a"
|
||||
* 2) fragment "spreads" e.g. "...c"
|
||||
* 3) inline fragment "spreads" e.g. "...on Type { a }"
|
||||
*/
|
||||
class Executor
|
||||
{
|
||||
private static $UNDEFINED;
|
||||
|
||||
public static function execute(Schema $schema, $root, Document $ast, $operationName = null, array $args = null)
|
||||
{
|
||||
if (!self::$UNDEFINED) {
|
||||
self::$UNDEFINED = new \stdClass();
|
||||
}
|
||||
|
||||
try {
|
||||
$errors = new \ArrayObject();
|
||||
$exeContext = self::buildExecutionContext($schema, $root, $ast, $operationName, $args, $errors);
|
||||
$data = self::executeOperation($exeContext, $root, $exeContext->operation);
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = $e;
|
||||
}
|
||||
|
||||
$result = [
|
||||
'data' => isset($data) ? $data : null
|
||||
];
|
||||
if (count($errors) > 0) {
|
||||
$result['errors'] = array_map(['GraphQL\Error', 'formatError'], $errors->getArrayCopy());
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a ExecutionContext object from the arguments passed to
|
||||
* execute, which we will pass throughout the other execution methods.
|
||||
*/
|
||||
private static function buildExecutionContext(Schema $schema, $root, Document $ast, $operationName = null, array $args = null, &$errors)
|
||||
{
|
||||
$operations = [];
|
||||
$fragments = [];
|
||||
|
||||
foreach ($ast->definitions as $statement) {
|
||||
switch ($statement->kind) {
|
||||
case Node::OPERATION_DEFINITION:
|
||||
$operations[$statement->name ? $statement->name->value : ''] = $statement;
|
||||
break;
|
||||
case Node::FRAGMENT_DEFINITION:
|
||||
$fragments[$statement->name->value] = $statement;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$operationName && count($operations) !== 1) {
|
||||
throw new Error(
|
||||
'Must provide operation name if query contains multiple operations'
|
||||
);
|
||||
}
|
||||
|
||||
$opName = $operationName ?: key($operations);
|
||||
if (!isset($operations[$opName])) {
|
||||
throw new Error('Unknown operation name: ' . $opName);
|
||||
}
|
||||
$operation = $operations[$opName];
|
||||
|
||||
$variables = Values::getVariableValues($schema, $operation->variableDefinitions ?: array(), $args ?: []);
|
||||
$exeContext = new ExecutionContext($schema, $fragments, $root, $operation, $variables, $errors);
|
||||
return $exeContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the "Evaluating operations" section of the spec.
|
||||
*/
|
||||
private static function executeOperation(ExecutionContext $exeContext, $root, OperationDefinition $operation)
|
||||
{
|
||||
$type = self::getOperationRootType($exeContext->schema, $operation);
|
||||
$fields = self::collectFields($exeContext, $type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
|
||||
if ($operation->operation === 'mutation') {
|
||||
return self::executeFieldsSerially($exeContext, $type, $root, $fields->getArrayCopy());
|
||||
}
|
||||
return self::executeFields($exeContext, $type, $root, $fields);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extracts the root type of the operation from the schema.
|
||||
*
|
||||
* @param Schema $schema
|
||||
* @param OperationDefinition $operation
|
||||
* @return ObjectType
|
||||
* @throws Error
|
||||
*/
|
||||
private static function getOperationRootType(Schema $schema, OperationDefinition $operation)
|
||||
{
|
||||
switch ($operation->operation) {
|
||||
case 'query':
|
||||
return $schema->getQueryType();
|
||||
case 'mutation':
|
||||
$mutationType = $schema->getMutationType();
|
||||
if (!$mutationType) {
|
||||
throw new Error(
|
||||
'Schema is not configured for mutations',
|
||||
[$operation]
|
||||
);
|
||||
}
|
||||
return $mutationType;
|
||||
default:
|
||||
throw new Error(
|
||||
'Can only execute queries and mutations',
|
||||
[$operation]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the "Evaluating selection sets" section of the spec
|
||||
* for "write" mode.
|
||||
*/
|
||||
private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $source, $fields)
|
||||
{
|
||||
$results = [];
|
||||
foreach ($fields as $responseName => $fieldASTs) {
|
||||
$result = self::resolveField($exeContext, $parentType, $source, $fieldASTs);
|
||||
|
||||
if ($result !== self::$UNDEFINED) {
|
||||
// Undefined means that field is not defined in schema
|
||||
$results[$responseName] = $result;
|
||||
}
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the "Evaluating selection sets" section of the spec
|
||||
* for "read" mode.
|
||||
*/
|
||||
private static function executeFields(ExecutionContext $exeContext, ObjectType $parentType, $source, $fields)
|
||||
{
|
||||
// Native PHP doesn't support promises.
|
||||
// Custom executor should be built for platforms like ReactPHP
|
||||
return self::executeFieldsSerially($exeContext, $parentType, $source, $fields);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a selectionSet, adds all of the fields in that selection to
|
||||
* the passed in map of fields, and returns it at the end.
|
||||
*
|
||||
* @return \ArrayObject
|
||||
*/
|
||||
private static function collectFields(
|
||||
ExecutionContext $exeContext,
|
||||
ObjectType $type,
|
||||
SelectionSet $selectionSet,
|
||||
$fields,
|
||||
$visitedFragmentNames
|
||||
)
|
||||
{
|
||||
for ($i = 0; $i < count($selectionSet->selections); $i++) {
|
||||
$selection = $selectionSet->selections[$i];
|
||||
switch ($selection->kind) {
|
||||
case Node::FIELD:
|
||||
if (!self::shouldIncludeNode($exeContext, $selection->directives)) {
|
||||
continue;
|
||||
}
|
||||
$name = self::getFieldEntryKey($selection);
|
||||
if (!isset($fields[$name])) {
|
||||
$fields[$name] = new \ArrayObject();
|
||||
}
|
||||
$fields[$name][] = $selection;
|
||||
break;
|
||||
case Node::INLINE_FRAGMENT:
|
||||
if (!self::shouldIncludeNode($exeContext, $selection->directives) ||
|
||||
!self::doesFragmentConditionMatch($exeContext, $selection, $type)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
self::collectFields(
|
||||
$exeContext,
|
||||
$type,
|
||||
$selection->selectionSet,
|
||||
$fields,
|
||||
$visitedFragmentNames
|
||||
);
|
||||
break;
|
||||
case Node::FRAGMENT_SPREAD:
|
||||
$fragName = $selection->name->value;
|
||||
if (!empty($visitedFragmentNames[$fragName]) || !self::shouldIncludeNode($exeContext, $selection->directives)) {
|
||||
continue;
|
||||
}
|
||||
$visitedFragmentNames[$fragName] = true;
|
||||
|
||||
/** @var FragmentDefinition|null $fragment */
|
||||
$fragment = isset($exeContext->fragments[$fragName]) ? $exeContext->fragments[$fragName] : null;
|
||||
if (!$fragment ||
|
||||
!self::shouldIncludeNode($exeContext, $fragment->directives) ||
|
||||
!self::doesFragmentConditionMatch($exeContext, $fragment, $type)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
self::collectFields(
|
||||
$exeContext,
|
||||
$type,
|
||||
$fragment->selectionSet,
|
||||
$fields,
|
||||
$visitedFragmentNames
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a field should be included based on @if and @unless directives.
|
||||
*/
|
||||
private static function shouldIncludeNode(ExecutionContext $exeContext, $directives)
|
||||
{
|
||||
$ifDirective = Values::getDirectiveValue(Directive::ifDirective(), $directives, $exeContext->variables);
|
||||
if ($ifDirective !== null) {
|
||||
return $ifDirective;
|
||||
}
|
||||
|
||||
$unlessDirective = Values::getDirectiveValue(Directive::unlessDirective(), $directives, $exeContext->variables);
|
||||
if ($unlessDirective !== null) {
|
||||
return !$unlessDirective;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a fragment is applicable to the given type.
|
||||
*/
|
||||
private static function doesFragmentConditionMatch(ExecutionContext $exeContext,/* FragmentDefinition | InlineFragment*/ $fragment, ObjectType $type)
|
||||
{
|
||||
$conditionalType = Utils\TypeInfo::typeFromAST($exeContext->schema, $fragment->typeCondition);
|
||||
if ($conditionalType === $type) {
|
||||
return true;
|
||||
}
|
||||
if ($conditionalType instanceof InterfaceType ||
|
||||
$conditionalType instanceof UnionType
|
||||
) {
|
||||
return $conditionalType->isPossibleType($type);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the logic to compute the key of a given field’s entry
|
||||
*/
|
||||
private static function getFieldEntryKey(Field $node)
|
||||
{
|
||||
return $node->alias ? $node->alias->value : $node->name->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper function for resolving the field, that catches the error
|
||||
* and adds it to the context's global if the error is not rethrowable.
|
||||
*/
|
||||
private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $source, $fieldASTs)
|
||||
{
|
||||
$fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldASTs[0]);
|
||||
if (!$fieldDef) {
|
||||
return self::$UNDEFINED;
|
||||
}
|
||||
|
||||
// If the field type is non-nullable, then it is resolved without any
|
||||
// protection from errors.
|
||||
if ($fieldDef->getType() instanceof NonNull) {
|
||||
return self::resolveFieldOrError(
|
||||
$exeContext,
|
||||
$parentType,
|
||||
$source,
|
||||
$fieldASTs,
|
||||
$fieldDef
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, error protection is applied, logging the error and resolving
|
||||
// a null value for this field if one is encountered.
|
||||
try {
|
||||
$result = self::resolveFieldOrError(
|
||||
$exeContext,
|
||||
$parentType,
|
||||
$source,
|
||||
$fieldASTs,
|
||||
$fieldDef
|
||||
);
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $error) {
|
||||
$exeContext->addError($error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the field on the given source object. In particular, this
|
||||
* figures out the object that the field returns using the resolve function,
|
||||
* then calls completeField to coerce scalars or execute the sub
|
||||
* selection set for objects.
|
||||
*/
|
||||
private static function resolveFieldOrError(
|
||||
ExecutionContext $exeContext,
|
||||
ObjectType $parentType,
|
||||
$source,
|
||||
/*array<Field>*/ $fieldASTs,
|
||||
FieldDefinition $fieldDef
|
||||
)
|
||||
{
|
||||
$fieldAST = $fieldASTs[0];
|
||||
$fieldType = $fieldDef->getType();
|
||||
$resolveFn = $fieldDef->resolve ?: [__CLASS__, 'defaultResolveFn'];
|
||||
|
||||
// Build a JS object of arguments from the field.arguments AST, using the
|
||||
// variables scope to fulfill any variable references.
|
||||
// TODO: find a way to memoize, in case this field is within a Array type.
|
||||
$args = Values::getArgumentValues(
|
||||
$fieldDef->args,
|
||||
$fieldAST->arguments,
|
||||
$exeContext->variables
|
||||
);
|
||||
|
||||
try {
|
||||
$result = call_user_func($resolveFn,
|
||||
$source,
|
||||
$args,
|
||||
$exeContext->root,
|
||||
// TODO: provide all fieldASTs, not just the first field
|
||||
$fieldAST,
|
||||
$fieldType,
|
||||
$parentType,
|
||||
$exeContext->schema
|
||||
);
|
||||
} catch (\Exception $error) {
|
||||
throw new Error($error->getMessage(), [$fieldAST], $error->getTrace());
|
||||
}
|
||||
|
||||
return self::completeField(
|
||||
$exeContext,
|
||||
$fieldType,
|
||||
$fieldASTs,
|
||||
$result
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implements the instructions for completeValue as defined in the
|
||||
* "Field entries" section of the spec.
|
||||
*
|
||||
* If the field type is Non-Null, then this recursively completes the value
|
||||
* for the inner type. It throws a field error if that completion returns null,
|
||||
* as per the "Nullability" section of the spec.
|
||||
*
|
||||
* If the field type is a List, then this recursively completes the value
|
||||
* for the inner type on each item in the list.
|
||||
*
|
||||
* If the field type is a Scalar or Enum, ensures the completed value is a legal
|
||||
* value of the type by calling the `coerce` method of GraphQL type definition.
|
||||
*
|
||||
* Otherwise, the field type expects a sub-selection set, and will complete the
|
||||
* value by evaluating all sub-selections.
|
||||
*/
|
||||
private static function completeField(ExecutionContext $exeContext, Type $fieldType,/* Array<Field> */ $fieldASTs, &$result)
|
||||
{
|
||||
// If field type is NonNull, complete for inner type, and throw field error
|
||||
// if result is null.
|
||||
if ($fieldType instanceof NonNull) {
|
||||
$completed = self::completeField(
|
||||
$exeContext,
|
||||
$fieldType->getWrappedType(),
|
||||
$fieldASTs,
|
||||
$result
|
||||
);
|
||||
if ($completed === null) {
|
||||
throw new Error(
|
||||
'Cannot return null for non-nullable type.',
|
||||
$fieldASTs instanceof \ArrayObject ? $fieldASTs->getArrayCopy() : $fieldASTs
|
||||
);
|
||||
}
|
||||
return $completed;
|
||||
}
|
||||
|
||||
// If result is null-like, return null.
|
||||
if (null === $result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If field type is List, complete each item in the list with the inner type
|
||||
if ($fieldType instanceof ListOfType) {
|
||||
$itemType = $fieldType->getWrappedType();
|
||||
Utils::invariant(
|
||||
is_array($result) || $result instanceof \ArrayObject,
|
||||
'User Error: expected iterable, but did not find one.'
|
||||
);
|
||||
|
||||
$tmp = [];
|
||||
foreach ($result as $item) {
|
||||
$tmp[] = self::completeField($exeContext, $itemType, $fieldASTs, $item);
|
||||
}
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
// If field type is Scalar or Enum, coerce to a valid value, returning null
|
||||
// if coercion is not possible.
|
||||
if ($fieldType instanceof ScalarType ||
|
||||
$fieldType instanceof EnumType
|
||||
) {
|
||||
Utils::invariant(method_exists($fieldType, 'coerce'), 'Missing coerce method on type');
|
||||
return $fieldType->coerce($result);
|
||||
}
|
||||
|
||||
// Field type must be Object, Interface or Union and expect sub-selections.
|
||||
|
||||
$objectType =
|
||||
$fieldType instanceof ObjectType ? $fieldType :
|
||||
($fieldType instanceof InterfaceType ||
|
||||
$fieldType instanceof UnionType ? $fieldType->resolveType($result) :
|
||||
null);
|
||||
|
||||
if (!$objectType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Collect sub-fields to execute to complete this value.
|
||||
$subFieldASTs = new \ArrayObject();
|
||||
$visitedFragmentNames = new \ArrayObject();
|
||||
for ($i = 0; $i < count($fieldASTs); $i++) {
|
||||
$selectionSet = $fieldASTs[$i]->selectionSet;
|
||||
if ($selectionSet) {
|
||||
$subFieldASTs = self::collectFields(
|
||||
$exeContext,
|
||||
$objectType,
|
||||
$selectionSet,
|
||||
$subFieldASTs,
|
||||
$visitedFragmentNames
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return self::executeFields($exeContext, $objectType, $result, $subFieldASTs);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If a resolve function is not given, then a default resolve behavior is used
|
||||
* which takes the property of the source object of the same name as the field
|
||||
* and returns it as the result, or if it's a function, returns the result
|
||||
* of calling that function.
|
||||
*/
|
||||
public static function defaultResolveFn($source, $args, $root, $fieldAST)
|
||||
{
|
||||
$property = null;
|
||||
if (is_array($source) || $source instanceof \ArrayAccess) {
|
||||
if (isset($source[$fieldAST->name->value])) {
|
||||
$property = $source[$fieldAST->name->value];
|
||||
}
|
||||
} else if (is_object($source)) {
|
||||
if (property_exists($source, $fieldAST->name->value)) {
|
||||
$e = func_get_args();
|
||||
$property = $source->{$fieldAST->name->value};
|
||||
}
|
||||
}
|
||||
|
||||
return is_callable($property) ? call_user_func($property, $source) : $property;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method looks up the field on the given type defintion.
|
||||
* It has special casing for the two introspection fields, __schema
|
||||
* and __typename. __typename is special because it can always be
|
||||
* queried as a field, even in situations where no other fields
|
||||
* are allowed, like on a Union. __schema could get automatically
|
||||
* added to the query type, but that would require mutating type
|
||||
* definitions, which would cause issues.
|
||||
*
|
||||
* @return FieldDefinition
|
||||
*/
|
||||
private static function getFieldDef(Schema $schema, ObjectType $parentType, Field $fieldAST)
|
||||
{
|
||||
$name = $fieldAST->name->value;
|
||||
$schemaMetaFieldDef = Introspection::schemaMetaFieldDef();
|
||||
$typeMetaFieldDef = Introspection::typeMetaFieldDef();
|
||||
$typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef();
|
||||
|
||||
if ($name === $schemaMetaFieldDef->name &&
|
||||
$schema->getQueryType() === $parentType
|
||||
) {
|
||||
return $schemaMetaFieldDef;
|
||||
} else if ($name === $typeMetaFieldDef->name &&
|
||||
$schema->getQueryType() === $parentType
|
||||
) {
|
||||
return $typeMetaFieldDef;
|
||||
} else if ($name === $typeNameMetaFieldDef->name) {
|
||||
return $typeNameMetaFieldDef;
|
||||
}
|
||||
$tmp = $parentType->getFields();
|
||||
return isset($tmp[$name]) ? $tmp[$name] : null;
|
||||
}
|
||||
}
|
275
src/Executor/Values.php
Normal file
275
src/Executor/Values.php
Normal file
@ -0,0 +1,275 @@
|
||||
<?php
|
||||
namespace GraphQL\Executor;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\VariableDefinition;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Utils;
|
||||
|
||||
class Values
|
||||
{
|
||||
/**
|
||||
* Prepares an object map of variables of the correct type based on the provided
|
||||
* variable definitions and arbitrary input. If the input cannot be coerced
|
||||
* to match the variable definitions, a Error will be thrown.
|
||||
*/
|
||||
public static function getVariableValues(Schema $schema, /* Array<VariableDefinition> */ $definitionASTs, array $inputs)
|
||||
{
|
||||
$values = [];
|
||||
foreach ($definitionASTs as $defAST) {
|
||||
$varName = $defAST->variable->name->value;
|
||||
$values[$varName] = self::getvariableValue($schema, $defAST, isset($inputs[$varName]) ? $inputs[$varName] : null);
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares an object map of argument values given a list of argument
|
||||
* definitions and list of argument AST nodes.
|
||||
*/
|
||||
public static function getArgumentValues(/* Array<GraphQLFieldArgument>*/ $argDefs, /*Array<Argument>*/ $argASTs, $variables)
|
||||
{
|
||||
if (!$argDefs || count($argDefs) === 0) {
|
||||
return null;
|
||||
}
|
||||
$argASTMap = $argASTs ? Utils::keyMap($argASTs, function ($arg) {
|
||||
return $arg->name->value;
|
||||
}) : [];
|
||||
$result = [];
|
||||
foreach ($argDefs as $argDef) {
|
||||
$name = $argDef->name;
|
||||
$valueAST = isset($argASTMap[$name]) ? $argASTMap[$name]->value : null;
|
||||
$result[$name] = self::coerceValueAST($argDef->getType(), $valueAST, $variables);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function getDirectiveValue(Directive $directiveDef, /* Array<Directive> */ $directives, $variables)
|
||||
{
|
||||
$directiveAST = null;
|
||||
if ($directives) {
|
||||
foreach ($directives as $directive) {
|
||||
if ($directive->name->value === $directiveDef->name) {
|
||||
$directiveAST = $directive;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($directiveAST) {
|
||||
if (!$directiveDef->type) {
|
||||
return null;
|
||||
}
|
||||
return self::coerceValueAST($directiveDef->type, $directiveAST->value, $variables);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a variable definition, and any value of input, return a value which
|
||||
* adheres to the variable definition, or throw an error.
|
||||
*/
|
||||
private static function getVariableValue(Schema $schema, VariableDefinition $definitionAST, $input)
|
||||
{
|
||||
$type = Utils\TypeInfo::typeFromAST($schema, $definitionAST->type);
|
||||
if (!$type) {
|
||||
return null;
|
||||
}
|
||||
if (self::isValidValue($type, $input)) {
|
||||
if (null === $input) {
|
||||
$defaultValue = $definitionAST->defaultValue;
|
||||
if ($defaultValue) {
|
||||
return self::coerceValueAST($type, $defaultValue);
|
||||
}
|
||||
}
|
||||
return self::coerceValue($type, $input);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Variable \${$definitionAST->variable->name->value} expected value of type " .
|
||||
Printer::doPrint($definitionAST->type) . " but got: " . json_encode($input) . '.',
|
||||
[$definitionAST]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a type and any value, return true if that value is valid.
|
||||
*/
|
||||
private static function isValidValue(Type $type, $value)
|
||||
{
|
||||
if ($type instanceof NonNull) {
|
||||
if (null === $value) {
|
||||
return false;
|
||||
}
|
||||
return self::isValidValue($type->getWrappedType(), $value);
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($type instanceof ListOfType) {
|
||||
$itemType = $type->getWrappedType();
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $item) {
|
||||
if (!self::isValidValue($itemType, $item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return self::isValidValue($itemType, $value);
|
||||
}
|
||||
}
|
||||
|
||||
if ($type instanceof InputObjectType) {
|
||||
$fields = $type->getFields();
|
||||
foreach ($fields as $fieldName => $field) {
|
||||
/** @var FieldDefinition $field */
|
||||
if (!self::isValidValue($field->getType(), isset($value[$fieldName]) ? $value[$fieldName] : null)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($type instanceof ScalarType ||
|
||||
$type instanceof EnumType
|
||||
) {
|
||||
return null !== $type->coerce($value);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a type and any value, return a runtime value coerced to match the type.
|
||||
*/
|
||||
private static function coerceValue(Type $type, $value)
|
||||
{
|
||||
if ($type instanceof NonNull) {
|
||||
// Note: we're not checking that the result of coerceValue is non-null.
|
||||
// We only call this function after calling isValidValue.
|
||||
return self::coerceValue($type->getWrappedType(), $value);
|
||||
}
|
||||
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type instanceof ListOfType) {
|
||||
$itemType = $type->getWrappedType();
|
||||
// TODO: support iterable input
|
||||
if (is_array($value)) {
|
||||
return array_map(function ($item) use ($itemType) {
|
||||
return Values::coerceValue($itemType, $item);
|
||||
}, $value);
|
||||
} else {
|
||||
return [self::coerceValue($itemType, $value)];
|
||||
}
|
||||
}
|
||||
|
||||
if ($type instanceof InputObjectType) {
|
||||
$fields = $type->getFields();
|
||||
$obj = [];
|
||||
foreach ($fields as $fieldName => $field) {
|
||||
$fieldValue = self::coerceValue($field->getType(), $value[$fieldName]);
|
||||
$obj[$fieldName] = $fieldValue === null ? $field->defaultValue : $fieldValue;
|
||||
}
|
||||
return $obj;
|
||||
|
||||
}
|
||||
|
||||
if ($type instanceof ScalarType ||
|
||||
$type instanceof EnumType
|
||||
) {
|
||||
$coerced = $type->coerce($value);
|
||||
if (null !== $coerced) {
|
||||
return $coerced;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a type and a value AST node known to match this type, build a
|
||||
* runtime value.
|
||||
*/
|
||||
private static function coerceValueAST(Type $type, $valueAST, $variables)
|
||||
{
|
||||
if ($type instanceof NonNull) {
|
||||
// Note: we're not checking that the result of coerceValueAST is non-null.
|
||||
// We're assuming that this query has been validated and the value used
|
||||
// here is of the correct type.
|
||||
return self::coerceValueAST($type->getWrappedType(), $valueAST, $variables);
|
||||
}
|
||||
|
||||
if (!$valueAST) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($valueAST->kind === Node::VARIABLE) {
|
||||
$variableName = $valueAST->name->value;
|
||||
|
||||
if (!isset($variables, $variables[$variableName])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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 ($valueAST->kind === Node::ARR) {
|
||||
$tmp = [];
|
||||
foreach ($valueAST->values as $itemAST) {
|
||||
$tmp[] = self::coerceValueAST($itemType, $itemAST, $variables);
|
||||
}
|
||||
return $tmp;
|
||||
} else {
|
||||
return [self::coerceValueAST($itemType, $valueAST, $variables)];
|
||||
}
|
||||
}
|
||||
|
||||
if ($type instanceof InputObjectType) {
|
||||
$fields = $type->getFields();
|
||||
if ($valueAST->kind !== Node::OBJECT) {
|
||||
return null;
|
||||
}
|
||||
$fieldASTs = Utils::keyMap($valueAST->fields, function ($field) {
|
||||
return $field->name->value;
|
||||
});
|
||||
|
||||
$obj = [];
|
||||
foreach ($fields as $fieldName => $field) {
|
||||
$fieldAST = $fieldASTs[$fieldName];
|
||||
$fieldValue = self::coerceValueAST($field->getType(), $fieldAST ? $fieldAST->value : null, $variables);
|
||||
$obj[$fieldName] = $fieldValue === null ? $field->defaultValue : $fieldValue;
|
||||
}
|
||||
return $obj;
|
||||
}
|
||||
|
||||
if ($type instanceof ScalarType || $type instanceof EnumType) {
|
||||
$coerced = $type->coerceLiteral($valueAST);
|
||||
if (null !== $coerced) {
|
||||
return $coerced;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
18
src/FormattedError.php
Normal file
18
src/FormattedError.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
namespace GraphQL;
|
||||
|
||||
class FormattedError
|
||||
{
|
||||
public $message;
|
||||
|
||||
/**
|
||||
* @var array<Language\SourceLocation>
|
||||
*/
|
||||
public $locations;
|
||||
|
||||
public function __construct($message, $locations = null)
|
||||
{
|
||||
$this->message = $message;
|
||||
$this->locations = $locations;
|
||||
}
|
||||
}
|
35
src/GraphQL.php
Normal file
35
src/GraphQL.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
namespace GraphQL;
|
||||
|
||||
use GraphQL\Executor\Executor;
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Language\Source;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
|
||||
class GraphQL
|
||||
{
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @param $requestString
|
||||
* @param mixed $rootObject
|
||||
* @param array <string, string>|null $variableValues
|
||||
* @param string|null $operationName
|
||||
* @return array
|
||||
*/
|
||||
public static function execute(Schema $schema, $requestString, $rootObject = null, $variableValues = null, $operationName = null)
|
||||
{
|
||||
try {
|
||||
$source = new Source($requestString ?: '', 'GraphQL request');
|
||||
$ast = Parser::parse($source);
|
||||
$validationResult = DocumentValidator::validate($schema, $ast);
|
||||
|
||||
if (empty($validationResult['isValid'])) {
|
||||
return ['errors' => $validationResult['errors']];
|
||||
} else {
|
||||
return Executor::execute($schema, $rootObject, $ast, $operationName, $variableValues);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return ['errors' => Error::formatError($e)];
|
||||
}
|
||||
}
|
||||
}
|
17
src/Language/AST/Argument.php
Normal file
17
src/Language/AST/Argument.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class Argument extends Node
|
||||
{
|
||||
public $kind = Node::ARGUMENT;
|
||||
|
||||
/**
|
||||
* @var Name
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var Value
|
||||
*/
|
||||
public $value;
|
||||
}
|
12
src/Language/AST/ArrayValue.php
Normal file
12
src/Language/AST/ArrayValue.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ArrayValue extends Node implements Value
|
||||
{
|
||||
public $kind = Node::ARR;
|
||||
|
||||
/**
|
||||
* @var array<Value>
|
||||
*/
|
||||
public $values;
|
||||
}
|
13
src/Language/AST/BooleanValue.php
Normal file
13
src/Language/AST/BooleanValue.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
|
||||
class BooleanValue extends Node implements Value
|
||||
{
|
||||
public $kind = Node::BOOLEAN;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $value;
|
||||
}
|
10
src/Language/AST/Definition.php
Normal file
10
src/Language/AST/Definition.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
interface Definition
|
||||
{
|
||||
/**
|
||||
* export type Definition = OperationDefinition
|
||||
* | FragmentDefinition
|
||||
*/
|
||||
}
|
17
src/Language/AST/Directive.php
Normal file
17
src/Language/AST/Directive.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class Directive extends Node
|
||||
{
|
||||
public $kind = Node::DIRECTIVE;
|
||||
|
||||
/**
|
||||
* @var Name
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var Value
|
||||
*/
|
||||
public $value;
|
||||
}
|
12
src/Language/AST/Document.php
Normal file
12
src/Language/AST/Document.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class Document extends Node
|
||||
{
|
||||
public $kind = Node::DOCUMENT;
|
||||
|
||||
/**
|
||||
* @var array<Definition>
|
||||
*/
|
||||
public $definitions;
|
||||
}
|
12
src/Language/AST/EnumValue.php
Normal file
12
src/Language/AST/EnumValue.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class EnumValue extends Node implements Value
|
||||
{
|
||||
public $kind = Node::ENUM;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $value;
|
||||
}
|
32
src/Language/AST/Field.php
Normal file
32
src/Language/AST/Field.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class Field extends Node
|
||||
{
|
||||
public $kind = Node::FIELD;
|
||||
|
||||
/**
|
||||
* @var Name|null
|
||||
*/
|
||||
public $alias;
|
||||
|
||||
/**
|
||||
* @var Name
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var array<Argument>|null
|
||||
*/
|
||||
public $arguments;
|
||||
|
||||
/**
|
||||
* @var array<Directive>|null
|
||||
*/
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* @var SelectionSet|null
|
||||
*/
|
||||
public $selectionSet;
|
||||
}
|
13
src/Language/AST/FloatValue.php
Normal file
13
src/Language/AST/FloatValue.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
|
||||
class FloatValue extends Node implements Value
|
||||
{
|
||||
public $kind = Node::FLOAT;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $value;
|
||||
}
|
28
src/Language/AST/FragmentDefinition.php
Normal file
28
src/Language/AST/FragmentDefinition.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
|
||||
class FragmentDefinition extends Node implements Definition
|
||||
{
|
||||
public $kind = Node::FRAGMENT_DEFINITION;
|
||||
|
||||
/**
|
||||
* @var Name
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var Name
|
||||
*/
|
||||
public $typeCondition;
|
||||
|
||||
/**
|
||||
* @var array<Directive>
|
||||
*/
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* @var SelectionSet
|
||||
*/
|
||||
public $selectionSet;
|
||||
}
|
17
src/Language/AST/FragmentSpread.php
Normal file
17
src/Language/AST/FragmentSpread.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class FragmentSpread extends Node
|
||||
{
|
||||
public $kind = Node::FRAGMENT_SPREAD;
|
||||
|
||||
/**
|
||||
* @var Name
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var array<Directive>
|
||||
*/
|
||||
public $directives;
|
||||
}
|
22
src/Language/AST/InlineFragment.php
Normal file
22
src/Language/AST/InlineFragment.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class InlineFragment extends Node
|
||||
{
|
||||
public $kind = Node::INLINE_FRAGMENT;
|
||||
|
||||
/**
|
||||
* @var Name
|
||||
*/
|
||||
public $typeCondition;
|
||||
|
||||
/**
|
||||
* @var array<Directive>|null
|
||||
*/
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* @var SelectionSet
|
||||
*/
|
||||
public $selectionSet;
|
||||
}
|
13
src/Language/AST/IntValue.php
Normal file
13
src/Language/AST/IntValue.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
|
||||
class IntValue extends Node implements Value
|
||||
{
|
||||
public $kind = Node::INT;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $value;
|
||||
}
|
12
src/Language/AST/ListType.php
Normal file
12
src/Language/AST/ListType.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ListType extends Node implements Type
|
||||
{
|
||||
public $kind = Node::LIST_TYPE;
|
||||
|
||||
/**
|
||||
* @var Node
|
||||
*/
|
||||
public $type;
|
||||
}
|
29
src/Language/AST/Location.php
Normal file
29
src/Language/AST/Location.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
use GraphQL\Language\Source;
|
||||
|
||||
class Location
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $start;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $end;
|
||||
|
||||
/**
|
||||
* @var Source|null
|
||||
*/
|
||||
public $source;
|
||||
|
||||
public function __construct($start, $end, Source $source = null)
|
||||
{
|
||||
$this->start = $start;
|
||||
$this->end = $end;
|
||||
$this->source = $source;
|
||||
}
|
||||
}
|
12
src/Language/AST/Name.php
Normal file
12
src/Language/AST/Name.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class Name extends Node implements Type
|
||||
{
|
||||
public $kind = Node::NAME;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $value;
|
||||
}
|
121
src/Language/AST/Node.php
Normal file
121
src/Language/AST/Node.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
use GraphQL\Utils;
|
||||
|
||||
abstract class Node
|
||||
{
|
||||
// constants from language/kinds.js:
|
||||
|
||||
const NAME = 'Name';
|
||||
|
||||
// Document
|
||||
|
||||
const DOCUMENT = 'Document';
|
||||
const OPERATION_DEFINITION = 'OperationDefinition';
|
||||
const VARIABLE_DEFINITION = 'VariableDefinition';
|
||||
const VARIABLE = 'Variable';
|
||||
const SELECTION_SET = 'SelectionSet';
|
||||
const FIELD = 'Field';
|
||||
const ARGUMENT = 'Argument';
|
||||
|
||||
// Fragments
|
||||
|
||||
const FRAGMENT_SPREAD = 'FragmentSpread';
|
||||
const INLINE_FRAGMENT = 'InlineFragment';
|
||||
const FRAGMENT_DEFINITION = 'FragmentDefinition';
|
||||
|
||||
// Values
|
||||
|
||||
const INT = 'IntValue';
|
||||
const FLOAT = 'FloatValue';
|
||||
const STRING = 'StringValue';
|
||||
const BOOLEAN = 'BooleanValue';
|
||||
const ENUM = 'EnumValue';
|
||||
const ARR = 'ArrayValue';
|
||||
const OBJECT = 'ObjectValue';
|
||||
const OBJECT_FIELD = 'ObjectField';
|
||||
|
||||
// Directives
|
||||
|
||||
const DIRECTIVE = 'Directive';
|
||||
|
||||
// Types
|
||||
|
||||
const TYPE = 'Type';
|
||||
const LIST_TYPE = 'ListType';
|
||||
const NON_NULL_TYPE = 'NonNullType';
|
||||
|
||||
|
||||
/**
|
||||
type Node = Name
|
||||
| Document
|
||||
| OperationDefinition
|
||||
| VariableDefinition
|
||||
| Variable
|
||||
| SelectionSet
|
||||
| Field
|
||||
| Argument
|
||||
| FragmentSpread
|
||||
| InlineFragment
|
||||
| FragmentDefinition
|
||||
| IntValue
|
||||
| FloatValue
|
||||
| StringValue
|
||||
| BooleanValue
|
||||
| EnumValue
|
||||
| ArrayValue
|
||||
| ObjectValue
|
||||
| ObjectField
|
||||
| Directive
|
||||
| ListType
|
||||
| NonNullType
|
||||
*/
|
||||
|
||||
public $kind;
|
||||
|
||||
/**
|
||||
* @var Location
|
||||
*/
|
||||
public $loc;
|
||||
|
||||
/**
|
||||
* @param array $vars
|
||||
*/
|
||||
public function __construct(array $vars)
|
||||
{
|
||||
Utils::assign($this, $vars);
|
||||
}
|
||||
|
||||
public function cloneDeep()
|
||||
{
|
||||
return $this->_cloneValue($this);
|
||||
}
|
||||
|
||||
private function _cloneValue($value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$cloned = [];
|
||||
foreach ($value as $key => $arrValue) {
|
||||
$cloned[$key] = $this->_cloneValue($arrValue);
|
||||
}
|
||||
} else if ($value instanceof Node) {
|
||||
$cloned = clone $value;
|
||||
foreach (get_object_vars($cloned) as $prop => $propValue) {
|
||||
$cloned->{$prop} = $this->_cloneValue($propValue);
|
||||
}
|
||||
} else {
|
||||
$cloned = $value;
|
||||
}
|
||||
|
||||
return $cloned;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return json_encode($this);
|
||||
}
|
||||
}
|
12
src/Language/AST/NonNullType.php
Normal file
12
src/Language/AST/NonNullType.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class NonNullType extends Node implements Type
|
||||
{
|
||||
public $kind = Node::NON_NULL_TYPE;
|
||||
|
||||
/**
|
||||
* @var Name | ListType
|
||||
*/
|
||||
public $type;
|
||||
}
|
18
src/Language/AST/ObjectField.php
Normal file
18
src/Language/AST/ObjectField.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
|
||||
class ObjectField extends Node
|
||||
{
|
||||
public $kind = Node::OBJECT_FIELD;
|
||||
|
||||
/**
|
||||
* @var Name
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var Value
|
||||
*/
|
||||
public $value;
|
||||
}
|
12
src/Language/AST/ObjectValue.php
Normal file
12
src/Language/AST/ObjectValue.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ObjectValue extends Node implements Value
|
||||
{
|
||||
public $kind = Node::OBJECT;
|
||||
|
||||
/**
|
||||
* @var array<ObjectField>
|
||||
*/
|
||||
public $fields;
|
||||
}
|
35
src/Language/AST/OperationDefinition.php
Normal file
35
src/Language/AST/OperationDefinition.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class OperationDefinition extends Node implements Definition
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $kind = Node::OPERATION_DEFINITION;
|
||||
|
||||
/**
|
||||
* @var string (oneOf 'query', 'mutation'))
|
||||
*/
|
||||
public $operation;
|
||||
|
||||
/**
|
||||
* @var Name|null
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var array<VariableDefinition>
|
||||
*/
|
||||
public $variableDefinitions;
|
||||
|
||||
/**
|
||||
* @var array<Directive>
|
||||
*/
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* @var SelectionSet
|
||||
*/
|
||||
public $selectionSet;
|
||||
}
|
9
src/Language/AST/Selection.php
Normal file
9
src/Language/AST/Selection.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
interface Selection
|
||||
{
|
||||
/**
|
||||
* export type Selection = Field | FragmentSpread | InlineFragment
|
||||
*/
|
||||
}
|
12
src/Language/AST/SelectionSet.php
Normal file
12
src/Language/AST/SelectionSet.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class SelectionSet extends Node
|
||||
{
|
||||
public $kind = Node::SELECTION_SET;
|
||||
|
||||
/**
|
||||
* @var array<Selection>
|
||||
*/
|
||||
public $selections;
|
||||
}
|
13
src/Language/AST/StringValue.php
Normal file
13
src/Language/AST/StringValue.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
|
||||
class StringValue extends Node implements Value
|
||||
{
|
||||
public $kind = Node::STRING;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $value;
|
||||
}
|
13
src/Language/AST/Type.php
Normal file
13
src/Language/AST/Type.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
|
||||
interface Type
|
||||
{
|
||||
/**
|
||||
export type Type = Name
|
||||
| ListType
|
||||
| NonNullType
|
||||
*/
|
||||
}
|
||||
|
16
src/Language/AST/Value.php
Normal file
16
src/Language/AST/Value.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
interface Value
|
||||
{
|
||||
/**
|
||||
export type Value = Variable
|
||||
| IntValue
|
||||
| FloatValue
|
||||
| StringValue
|
||||
| BooleanValue
|
||||
| EnumValue
|
||||
| ArrayValue
|
||||
| ObjectValue
|
||||
*/
|
||||
}
|
12
src/Language/AST/Variable.php
Normal file
12
src/Language/AST/Variable.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class Variable extends Node
|
||||
{
|
||||
public $kind = Node::VARIABLE;
|
||||
|
||||
/**
|
||||
* @var Name
|
||||
*/
|
||||
public $name;
|
||||
}
|
22
src/Language/AST/VariableDefinition.php
Normal file
22
src/Language/AST/VariableDefinition.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class VariableDefinition extends Node implements Definition
|
||||
{
|
||||
public $kind = Node::VARIABLE_DEFINITION;
|
||||
|
||||
/**
|
||||
* @var Variable
|
||||
*/
|
||||
public $variable;
|
||||
|
||||
/**
|
||||
* @var Type
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* @var Value|null
|
||||
*/
|
||||
public $defaultValue;
|
||||
}
|
59
src/Language/Exception.php
Normal file
59
src/Language/Exception.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
namespace GraphQL\Language;
|
||||
|
||||
class Exception extends \Exception
|
||||
{
|
||||
/**
|
||||
* @var Source
|
||||
*/
|
||||
public $source;
|
||||
|
||||
/**
|
||||
* @var number
|
||||
*/
|
||||
public $position;
|
||||
|
||||
public $location;
|
||||
|
||||
/**
|
||||
* @param Source $source
|
||||
* @param $position
|
||||
* @param $description
|
||||
* @return Exception
|
||||
*/
|
||||
public static function create(Source $source, $position, $description)
|
||||
{
|
||||
$location = $source->getLocation($position);
|
||||
$syntaxError = new self(
|
||||
"Syntax Error {$source->name} ({$location->line}:{$location->column}) $description\n\n" .
|
||||
self::highlightSourceAtLocation($source, $location)
|
||||
);
|
||||
$syntaxError->source = $source;
|
||||
$syntaxError->position = $position;
|
||||
$syntaxError->location = $location;
|
||||
|
||||
return $syntaxError;
|
||||
}
|
||||
|
||||
public static function highlightSourceAtLocation(Source $source, SourceLocation $location)
|
||||
{
|
||||
$line = $location->line;
|
||||
$prevLineNum = (string)($line - 1);
|
||||
$lineNum = (string)$line;
|
||||
$nextLineNum = (string)($line + 1);
|
||||
$padLen = mb_strlen($nextLineNum, 'UTF-8');
|
||||
|
||||
$unicodeChars = json_decode('"\u2028\u2029"'); // Quick hack to get js-compatible representation of these chars
|
||||
$lines = preg_split('/\r\n|[\n\r' . $unicodeChars . ']/su', $source->body);
|
||||
|
||||
$lpad = function($len, $str) {
|
||||
return str_pad($str, $len - mb_strlen($str, 'UTF-8') + 1, ' ', STR_PAD_LEFT);
|
||||
};
|
||||
|
||||
return
|
||||
($line >= 2 ? $lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2] . "\n" : '') .
|
||||
($lpad($padLen, $lineNum) . ': ' . $lines[$line - 1] . "\n") .
|
||||
(str_repeat(' ', 1 + $padLen + $location->column) . "^\n") .
|
||||
($line < count($lines) ? $lpad($padLen, $nextLineNum) . ': ' . $lines[$line] . "\n" : '');
|
||||
}
|
||||
}
|
303
src/Language/Lexer.php
Normal file
303
src/Language/Lexer.php
Normal file
@ -0,0 +1,303 @@
|
||||
<?php
|
||||
namespace GraphQL\Language;
|
||||
|
||||
use GraphQL\Utils;
|
||||
|
||||
// language/lexer.js
|
||||
|
||||
class Lexer
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $prevPosition;
|
||||
|
||||
/**
|
||||
* @var Source
|
||||
*/
|
||||
private $source;
|
||||
|
||||
public function __construct(Source $source)
|
||||
{
|
||||
$this->prevPosition = 0;
|
||||
$this->source = $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $resetPosition
|
||||
* @return Token
|
||||
*/
|
||||
public function nextToken($resetPosition = null)
|
||||
{
|
||||
$token = $this->readToken($resetPosition === null ? $this->prevPosition : $resetPosition);
|
||||
$this->prevPosition = $token->end;
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $fromPosition
|
||||
* @return Token
|
||||
* @throws Exception
|
||||
*/
|
||||
private function readToken($fromPosition)
|
||||
{
|
||||
$body = $this->source->body;
|
||||
$bodyLength = $this->source->length;
|
||||
|
||||
$position = $this->positionAfterWhitespace($body, $fromPosition);
|
||||
$code = Utils::charCodeAt($body, $position);
|
||||
|
||||
if ($position >= $bodyLength) {
|
||||
return new Token(Token::EOF, $position, $position);
|
||||
}
|
||||
|
||||
switch ($code) {
|
||||
// !
|
||||
case 33: return new Token(Token::BANG, $position, $position + 1);
|
||||
// $
|
||||
case 36: return new Token(Token::DOLLAR, $position, $position + 1);
|
||||
// (
|
||||
case 40: return new Token(Token::PAREN_L, $position, $position + 1);
|
||||
// )
|
||||
case 41: return new Token(Token::PAREN_R, $position, $position + 1);
|
||||
// .
|
||||
case 46:
|
||||
if (Utils::charCodeAt($body, $position+1) === 46 &&
|
||||
Utils::charCodeAt($body, $position+2) === 46) {
|
||||
return new Token(Token::SPREAD, $position, $position + 3);
|
||||
}
|
||||
break;
|
||||
// :
|
||||
case 58: return new Token(Token::COLON, $position, $position + 1);
|
||||
// =
|
||||
case 61: return new Token(Token::EQUALS, $position, $position + 1);
|
||||
// @
|
||||
case 64: return new Token(Token::AT, $position, $position + 1);
|
||||
// [
|
||||
case 91: return new Token(Token::BRACKET_L, $position, $position + 1);
|
||||
// ]
|
||||
case 93: return new Token(Token::BRACKET_R, $position, $position + 1);
|
||||
// {
|
||||
case 123: return new Token(Token::BRACE_L, $position, $position + 1);
|
||||
// |
|
||||
case 124: return new Token(Token::PIPE, $position, $position + 1);
|
||||
// }
|
||||
case 125: return new Token(Token::BRACE_R, $position, $position + 1);
|
||||
// A-Z
|
||||
case 65: case 66: case 67: case 68: case 69: case 70: case 71: case 72:
|
||||
case 73: case 74: case 75: case 76: case 77: case 78: case 79: case 80:
|
||||
case 81: case 82: case 83: case 84: case 85: case 86: case 87: case 88:
|
||||
case 89: case 90:
|
||||
// _
|
||||
case 95:
|
||||
// a-z
|
||||
case 97: case 98: case 99: case 100: case 101: case 102: case 103: case 104:
|
||||
case 105: case 106: case 107: case 108: case 109: case 110: case 111:
|
||||
case 112: case 113: case 114: case 115: case 116: case 117: case 118:
|
||||
case 119: case 120: case 121: case 122:
|
||||
return $this->readName($position);
|
||||
// -
|
||||
case 45:
|
||||
// 0-9
|
||||
case 48: case 49: case 50: case 51: case 52:
|
||||
case 53: case 54: case 55: case 56: case 57:
|
||||
return $this->readNumber($position, $code);
|
||||
// "
|
||||
case 34: return $this->readString($position);
|
||||
}
|
||||
|
||||
throw Exception::create($this->source, $position, 'Unexpected character "' . Utils::chr($code). '"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an alphanumeric + underscore name from the source.
|
||||
*
|
||||
* [_A-Za-z][_0-9A-Za-z]*
|
||||
* @param int $position
|
||||
* @return Token
|
||||
*/
|
||||
private function readName($position)
|
||||
{
|
||||
$body = $this->source->body;
|
||||
$bodyLength = $this->source->length;
|
||||
$end = $position + 1;
|
||||
|
||||
while (
|
||||
$end !== $bodyLength &&
|
||||
($code = Utils::charCodeAt($body, $end)) &&
|
||||
(
|
||||
$code === 95 || // _
|
||||
$code >= 48 && $code <= 57 || // 0-9
|
||||
$code >= 65 && $code <= 90 || // A-Z
|
||||
$code >= 97 && $code <= 122 // a-z
|
||||
)
|
||||
) {
|
||||
++$end;
|
||||
}
|
||||
return new Token(Token::NAME, $position, $end, mb_substr($body, $position, $end - $position, 'UTF-8'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a number token from the source file, either a float
|
||||
* or an int depending on whether a decimal point appears.
|
||||
*
|
||||
* Int: -?(0|[1-9][0-9]*)
|
||||
* Float: -?(0|[1-9][0-9]*)\.[0-9]+(e-?[0-9]+)?
|
||||
*
|
||||
* @param $start
|
||||
* @param $firstCode
|
||||
* @return Token
|
||||
* @throws Exception
|
||||
*/
|
||||
private function readNumber($start, $firstCode)
|
||||
{
|
||||
$code = $firstCode;
|
||||
$body = $this->source->body;
|
||||
$position = $start;
|
||||
$isFloat = false;
|
||||
|
||||
if ($code === 45) { // -
|
||||
$code = Utils::charCodeAt($body, ++$position);
|
||||
}
|
||||
|
||||
if ($code === 48) { // 0
|
||||
$code = Utils::charCodeAt($body, ++$position);
|
||||
} else if ($code >= 49 && $code <= 57) { // 1 - 9
|
||||
do {
|
||||
$code = Utils::charCodeAt($body, ++$position);
|
||||
} while ($code >= 48 && $code <= 57); // 0 - 9
|
||||
} else {
|
||||
throw Exception::create($this->source, $position, 'Invalid number');
|
||||
}
|
||||
|
||||
if ($code === 46) { // .
|
||||
$isFloat = true;
|
||||
|
||||
$code = Utils::charCodeAt($body, ++$position);
|
||||
if ($code >= 48 && $code <= 57) { // 0 - 9
|
||||
do {
|
||||
$code = Utils::charCodeAt($body, ++$position);
|
||||
} while ($code >= 48 && $code <= 57); // 0 - 9
|
||||
} else {
|
||||
throw Exception::create($this->source, $position, 'Invalid number');
|
||||
}
|
||||
|
||||
if ($code === 101) { // e
|
||||
$code = Utils::charCodeAt($body, ++$position);
|
||||
if ($code === 45) { // -
|
||||
$code = Utils::charCodeAt($body, ++$position);
|
||||
}
|
||||
if ($code >= 48 && $code <= 57) { // 0 - 9
|
||||
do {
|
||||
$code = Utils::charCodeAt($body, ++$position);
|
||||
} while ($code >= 48 && $code <= 57); // 0 - 9
|
||||
} else {
|
||||
throw Exception::create($this->source, $position, 'Invalid number');
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Token(
|
||||
$isFloat ? Token::FLOAT : Token::INT,
|
||||
$start,
|
||||
$position,
|
||||
mb_substr($body, $start, $position - $start, 'UTF-8')
|
||||
);
|
||||
}
|
||||
|
||||
private function readString($start)
|
||||
{
|
||||
$body = $this->source->body;
|
||||
$bodyLength = $this->source->length;
|
||||
|
||||
$position = $start + 1;
|
||||
$chunkStart = $position;
|
||||
$code = null;
|
||||
$value = '';
|
||||
|
||||
while (
|
||||
$position < $bodyLength &&
|
||||
($code = Utils::charCodeAt($body, $position)) &&
|
||||
$code !== 34 &&
|
||||
$code !== 10 && $code !== 13 && $code !== 0x2028 && $code !== 0x2029
|
||||
) {
|
||||
++$position;
|
||||
if ($code === 92) { // \
|
||||
$value .= mb_substr($body, $chunkStart, $position - 1 - $chunkStart, 'UTF-8');
|
||||
$code = Utils::charCodeAt($body, $position);
|
||||
switch ($code) {
|
||||
case 34: $value .= '"'; break;
|
||||
case 47: $value .= '\/'; break;
|
||||
case 92: $value .= '\\'; break;
|
||||
case 98: $value .= '\b'; break;
|
||||
case 102: $value .= '\f'; break;
|
||||
case 110: $value .= '\n'; break;
|
||||
case 114: $value .= '\r'; break;
|
||||
case 116: $value .= '\t'; break;
|
||||
case 117:
|
||||
$hex = mb_substr($body, $position + 1, 4);
|
||||
if (!preg_match('/[0-9a-fA-F]{4}/', $hex)) {
|
||||
throw Exception::create($this->source, $position, 'Bad character escape sequence');
|
||||
}
|
||||
$value .= Utils::chr(hexdec($hex));
|
||||
$position += 4;
|
||||
break;
|
||||
default:
|
||||
throw Exception::create($this->source, $position, 'Bad character escape sequence');
|
||||
}
|
||||
++$position;
|
||||
$chunkStart = $position;
|
||||
}
|
||||
}
|
||||
|
||||
if ($code !== 34) {
|
||||
throw Exception::create($this->source, $position, 'Unterminated string');
|
||||
}
|
||||
|
||||
$value .= mb_substr($body, $chunkStart, $position - $chunkStart, 'UTF-8');
|
||||
return new Token(Token::STRING, $start, $position + 1, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads from body starting at startPosition until it finds a non-whitespace
|
||||
* or commented character, then returns the position of that character for
|
||||
* lexing.
|
||||
*
|
||||
* @param $body
|
||||
* @param $startPosition
|
||||
* @return int
|
||||
*/
|
||||
private function positionAfterWhitespace($body, $startPosition)
|
||||
{
|
||||
$bodyLength = mb_strlen($body, 'UTF-8');
|
||||
$position = $startPosition;
|
||||
|
||||
while ($position < $bodyLength) {
|
||||
$code = Utils::charCodeAt($body, $position);
|
||||
|
||||
// Skip whitespace
|
||||
if (
|
||||
$code === 32 || // space
|
||||
$code === 44 || // comma
|
||||
$code === 160 || // '\xa0'
|
||||
$code === 0x2028 || // line separator
|
||||
$code === 0x2029 || // paragraph separator
|
||||
$code > 8 && $code < 14 // whitespace
|
||||
) {
|
||||
++$position;
|
||||
// Skip comments
|
||||
} else if ($code === 35) { // #
|
||||
++$position;
|
||||
while (
|
||||
$position < $bodyLength &&
|
||||
($code = Utils::charCodeAt($body, $position)) &&
|
||||
$code !== 10 && $code !== 13 && $code !== 0x2028 && $code !== 0x2029
|
||||
) {
|
||||
++$position;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $position;
|
||||
}
|
||||
}
|
675
src/Language/Parser.php
Normal file
675
src/Language/Parser.php
Normal file
@ -0,0 +1,675 @@
|
||||
<?php
|
||||
namespace GraphQL\Language;
|
||||
|
||||
// language/parser.js
|
||||
|
||||
use GraphQL\Language\AST\Argument;
|
||||
use GraphQL\Language\AST\ArrayValue;
|
||||
use GraphQL\Language\AST\BooleanValue;
|
||||
use GraphQL\Language\AST\Directive;
|
||||
use GraphQL\Language\AST\Document;
|
||||
use GraphQL\Language\AST\EnumValue;
|
||||
use GraphQL\Language\AST\Field;
|
||||
use GraphQL\Language\AST\FloatValue;
|
||||
use GraphQL\Language\AST\FragmentDefinition;
|
||||
use GraphQL\Language\AST\FragmentSpread;
|
||||
use GraphQL\Language\AST\InlineFragment;
|
||||
use GraphQL\Language\AST\IntValue;
|
||||
use GraphQL\Language\AST\ListType;
|
||||
use GraphQL\Language\AST\Location;
|
||||
use GraphQL\Language\AST\Name;
|
||||
use GraphQL\Language\AST\NonNullType;
|
||||
use GraphQL\Language\AST\ObjectField;
|
||||
use GraphQL\Language\AST\ObjectValue;
|
||||
use GraphQL\Language\AST\OperationDefinition;
|
||||
use GraphQL\Language\AST\SelectionSet;
|
||||
use GraphQL\Language\AST\StringValue;
|
||||
use GraphQL\Language\AST\Variable;
|
||||
use GraphQL\Language\AST\VariableDefinition;
|
||||
|
||||
class Parser
|
||||
{
|
||||
/**
|
||||
* Available options:
|
||||
*
|
||||
* noLocation: boolean,
|
||||
* (By default, the parser creates AST nodes that know the location
|
||||
* in the source that they correspond to. This configuration flag
|
||||
* disables that behavior for performance or testing.)
|
||||
*
|
||||
* noSource: boolean,
|
||||
* By default, the parser creates AST nodes that contain a reference
|
||||
* to the source that they were created from. This configuration flag
|
||||
* disables that behavior for performance or testing.
|
||||
*
|
||||
* @param Source|string $source
|
||||
* @param array $options
|
||||
* @return Document
|
||||
*/
|
||||
public static function parse($source, array $options = array())
|
||||
{
|
||||
$sourceObj = $source instanceof Source ? $source : new Source($source);
|
||||
$parser = new self($sourceObj, $options);
|
||||
return $parser->parseDocument();
|
||||
}
|
||||
|
||||
/**
|
||||
* @var Source
|
||||
*/
|
||||
private $source;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $prevEnd;
|
||||
|
||||
/**
|
||||
* @var Lexer
|
||||
*/
|
||||
private $lexer;
|
||||
|
||||
/**
|
||||
* @var Token
|
||||
*/
|
||||
private $token;
|
||||
|
||||
function __construct(Source $source, array $options = array())
|
||||
{
|
||||
$this->lexer = new Lexer($source);
|
||||
$this->source = $source;
|
||||
$this->options = $options;
|
||||
$this->prevEnd = 0;
|
||||
$this->token = $this->lexer->nextToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a location object, used to identify the place in
|
||||
* the source that created a given parsed object.
|
||||
*
|
||||
* @param int $start
|
||||
* @return Location|null
|
||||
*/
|
||||
function loc($start)
|
||||
{
|
||||
if (!empty($this->options['noLocation'])) {
|
||||
return null;
|
||||
}
|
||||
if (!empty($this->options['noSource'])) {
|
||||
return new Location($start, $this->prevEnd);
|
||||
}
|
||||
return new Location($start, $this->prevEnd, $this->source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal parser object to the next lexed token.
|
||||
*/
|
||||
function advance()
|
||||
{
|
||||
$prevEnd = $this->token->end;
|
||||
$this->prevEnd = $prevEnd;
|
||||
$this->token = $this->lexer->nextToken($prevEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the next token is of a given kind
|
||||
*
|
||||
* @param $kind
|
||||
* @return bool
|
||||
*/
|
||||
function peek($kind)
|
||||
{
|
||||
return $this->token->kind === $kind;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the next token is of the given kind, return true after advancing
|
||||
* the parser. Otherwise, do not change the parser state and return false.
|
||||
*
|
||||
* @param $kind
|
||||
* @return bool
|
||||
*/
|
||||
function skip($kind)
|
||||
{
|
||||
$match = $this->token->kind === $kind;
|
||||
|
||||
if ($match) {
|
||||
$this->advance();
|
||||
}
|
||||
return $match;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the next token is of the given kind, return that token after advancing
|
||||
* the parser. Otherwise, do not change the parser state and return false.
|
||||
* @param string $kind
|
||||
* @return Token
|
||||
* @throws Exception
|
||||
*/
|
||||
function expect($kind)
|
||||
{
|
||||
$token = $this->token;
|
||||
|
||||
if ($token->kind === $kind) {
|
||||
$this->advance();
|
||||
return $token;
|
||||
}
|
||||
|
||||
throw Exception::create(
|
||||
$this->source,
|
||||
$token->start,
|
||||
"Expected " . Token::getKindDescription($kind) . ", found " . $token->getDescription()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the next token is a keyword with the given value, return that token after
|
||||
* advancing the parser. Otherwise, do not change the parser state and return
|
||||
* false.
|
||||
*
|
||||
* @param string $value
|
||||
* @return Token
|
||||
* @throws Exception
|
||||
*/
|
||||
function expectKeyword($value)
|
||||
{
|
||||
$token = $this->token;
|
||||
|
||||
if ($token->kind === Token::NAME && $token->value === $value) {
|
||||
$this->advance();
|
||||
return $token;
|
||||
}
|
||||
throw Exception::create(
|
||||
$this->source,
|
||||
$token->start,
|
||||
'Expected "' . $value . '", found ' . $token->getDescription()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Token|null $atToken
|
||||
* @return Exception
|
||||
*/
|
||||
function unexpected(Token $atToken = null)
|
||||
{
|
||||
$token = $atToken ?: $this->token;
|
||||
return Exception::create($this->source, $token->start, "Unexpected " . $token->getDescription());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a possibly empty list of parse nodes, determined by
|
||||
* the parseFn. This list begins with a lex token of openKind
|
||||
* and ends with a lex token of closeKind. Advances the parser
|
||||
* to the next lex token after the closing token.
|
||||
*
|
||||
* @param int $openKind
|
||||
* @param callable $parseFn
|
||||
* @param int $closeKind
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
function any($openKind, $parseFn, $closeKind)
|
||||
{
|
||||
$this->expect($openKind);
|
||||
|
||||
$nodes = array();
|
||||
while (!$this->skip($closeKind)) {
|
||||
$nodes[] = $parseFn($this);
|
||||
}
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a non-empty list of parse nodes, determined by
|
||||
* the parseFn. This list begins with a lex token of openKind
|
||||
* and ends with a lex token of closeKind. Advances the parser
|
||||
* to the next lex token after the closing token.
|
||||
*
|
||||
* @param $openKind
|
||||
* @param $parseFn
|
||||
* @param $closeKind
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
function many($openKind, $parseFn, $closeKind)
|
||||
{
|
||||
$this->expect($openKind);
|
||||
|
||||
$nodes = array($parseFn($this));
|
||||
while (!$this->skip($closeKind)) {
|
||||
$nodes[] = $parseFn($this);
|
||||
}
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a name lex token into a name parse node.
|
||||
*
|
||||
* @return Name
|
||||
* @throws Exception
|
||||
*/
|
||||
function parseName()
|
||||
{
|
||||
$token = $this->expect(Token::NAME);
|
||||
|
||||
return new Name(array(
|
||||
'value' => $token->value,
|
||||
'loc' => $this->loc($token->start)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the parsing rules in the Document section.
|
||||
*
|
||||
* @return Document
|
||||
* @throws Exception
|
||||
*/
|
||||
function parseDocument()
|
||||
{
|
||||
$start = $this->token->start;
|
||||
$definitions = array();
|
||||
|
||||
do {
|
||||
if ($this->peek(Token::BRACE_L)) {
|
||||
$definitions[] = $this->parseOperationDefinition();
|
||||
} else if ($this->peek(Token::NAME)) {
|
||||
if ($this->token->value === 'query' || $this->token->value === 'mutation') {
|
||||
$definitions[] = $this->parseOperationDefinition();
|
||||
} else if ($this->token->value === 'fragment') {
|
||||
$definitions[] = $this->parseFragmentDefinition();
|
||||
} else {
|
||||
throw $this->unexpected();
|
||||
}
|
||||
} else {
|
||||
throw $this->unexpected();
|
||||
}
|
||||
} while (!$this->skip(Token::EOF));
|
||||
|
||||
return new Document(array(
|
||||
'definitions' => $definitions,
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
}
|
||||
|
||||
// Implements the parsing rules in the Operations section.
|
||||
|
||||
/**
|
||||
* @return OperationDefinition
|
||||
* @throws Exception
|
||||
*/
|
||||
function parseOperationDefinition()
|
||||
{
|
||||
$start = $this->token->start;
|
||||
if ($this->peek(Token::BRACE_L)) {
|
||||
return new OperationDefinition(array(
|
||||
'operation' => 'query',
|
||||
'name' => null,
|
||||
'variableDefinitions' => null,
|
||||
'directives' => array(),
|
||||
'selectionSet' => $this->parseSelectionSet(),
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
}
|
||||
|
||||
$operationToken = $this->expect(Token::NAME);
|
||||
$operation = $operationToken->value;
|
||||
|
||||
return new OperationDefinition(array(
|
||||
'operation' => $operation,
|
||||
'name' => $this->parseName(),
|
||||
'variableDefinitions' => $this->parseVariableDefinitions(),
|
||||
'directives' => $this->parseDirectives(),
|
||||
'selectionSet' => $this->parseSelectionSet(),
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<VariableDefinition>
|
||||
*/
|
||||
function parseVariableDefinitions()
|
||||
{
|
||||
return $this->peek(Token::PAREN_L) ?
|
||||
$this->many(
|
||||
Token::PAREN_L,
|
||||
array($this, 'parseVariableDefinition'),
|
||||
Token::PAREN_R
|
||||
) :
|
||||
array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return VariableDefinition
|
||||
* @throws Exception
|
||||
*/
|
||||
function parseVariableDefinition()
|
||||
{
|
||||
$start = $this->token->start;
|
||||
$var = $this->parseVariable();
|
||||
|
||||
$this->expect(Token::COLON);
|
||||
$type = $this->parseType();
|
||||
|
||||
return new VariableDefinition(array(
|
||||
'variable' => $var,
|
||||
'type' => $type,
|
||||
'defaultValue' =>
|
||||
($this->skip(Token::EQUALS) ? $this->parseValue(true) : null),
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Variable
|
||||
* @throws Exception
|
||||
*/
|
||||
function parseVariable() {
|
||||
$start = $this->token->start;
|
||||
$this->expect(Token::DOLLAR);
|
||||
|
||||
return new Variable(array(
|
||||
'name' => $this->parseName(),
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SelectionSet
|
||||
*/
|
||||
function parseSelectionSet() {
|
||||
$start = $this->token->start;
|
||||
return new SelectionSet(array(
|
||||
'selections' => $this->many(Token::BRACE_L, array($this, 'parseSelection'), Token::BRACE_R),
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
function parseSelection() {
|
||||
return $this->peek(Token::SPREAD) ?
|
||||
$this->parseFragment() :
|
||||
$this->parseField();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Field
|
||||
*/
|
||||
function parseField() {
|
||||
$start = $this->token->start;
|
||||
$nameOrAlias = $this->parseName();
|
||||
|
||||
if ($this->skip(Token::COLON)) {
|
||||
$alias = $nameOrAlias;
|
||||
$name = $this->parseName();
|
||||
} else {
|
||||
$alias = null;
|
||||
$name = $nameOrAlias;
|
||||
}
|
||||
|
||||
return new Field(array(
|
||||
'alias' => $alias,
|
||||
'name' => $name,
|
||||
'arguments' => $this->parseArguments(),
|
||||
'directives' => $this->parseDirectives(),
|
||||
'selectionSet' => $this->peek(Token::BRACE_L) ? $this->parseSelectionSet() : null,
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<Argument>
|
||||
*/
|
||||
function parseArguments() {
|
||||
return $this->peek(Token::PAREN_L) ?
|
||||
$this->many(Token::PAREN_L, array($this, 'parseArgument'), Token::PAREN_R) :
|
||||
array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Argument
|
||||
* @throws Exception
|
||||
*/
|
||||
function parseArgument()
|
||||
{
|
||||
$start = $this->token->start;
|
||||
$name = $this->parseName();
|
||||
|
||||
$this->expect(Token::COLON);
|
||||
$value = $this->parseValue(false);
|
||||
|
||||
return new Argument(array(
|
||||
'name' => $name,
|
||||
'value' => $value,
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
}
|
||||
|
||||
// Implements the parsing rules in the Fragments section.
|
||||
|
||||
/**
|
||||
* @return FragmentSpread|InlineFragment
|
||||
* @throws Exception
|
||||
*/
|
||||
function parseFragment() {
|
||||
$start = $this->token->start;
|
||||
$this->expect(Token::SPREAD);
|
||||
|
||||
if ($this->token->value === 'on') {
|
||||
$this->advance();
|
||||
return new InlineFragment(array(
|
||||
'typeCondition' => $this->parseName(),
|
||||
'directives' => $this->parseDirectives(),
|
||||
'selectionSet' => $this->parseSelectionSet(),
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
}
|
||||
return new FragmentSpread(array(
|
||||
'name' => $this->parseName(),
|
||||
'directives' => $this->parseDirectives(),
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FragmentDefinition
|
||||
* @throws Exception
|
||||
*/
|
||||
function parseFragmentDefinition() {
|
||||
$start = $this->token->start;
|
||||
$this->expectKeyword('fragment');
|
||||
|
||||
$name = $this->parseName();
|
||||
$this->expectKeyword('on');
|
||||
$typeCondition = $this->parseName();
|
||||
|
||||
return new FragmentDefinition(array(
|
||||
'name' => $name,
|
||||
'typeCondition' => $typeCondition,
|
||||
'directives' => $this->parseDirectives(),
|
||||
'selectionSet' => $this->parseSelectionSet(),
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
}
|
||||
|
||||
// Implements the parsing rules in the Values section.
|
||||
function parseVariableValue()
|
||||
{
|
||||
return $this->parseValue(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BooleanValue|EnumValue|FloatValue|IntValue|StringValue|Variable
|
||||
* @throws Exception
|
||||
*/
|
||||
function parseConstValue()
|
||||
{
|
||||
return $this->parseValue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $isConst
|
||||
* @return BooleanValue|EnumValue|FloatValue|IntValue|StringValue|Variable
|
||||
* @throws Exception
|
||||
*/
|
||||
function parseValue($isConst) {
|
||||
$token = $this->token;
|
||||
switch ($token->kind) {
|
||||
case Token::BRACKET_L:
|
||||
return $this->parseArray($isConst);
|
||||
case Token::BRACE_L:
|
||||
return $this->parseObject($isConst);
|
||||
case Token::INT:
|
||||
$this->advance();
|
||||
return new IntValue(array(
|
||||
'value' => $token->value,
|
||||
'loc' => $this->loc($token->start)
|
||||
));
|
||||
case Token::FLOAT:
|
||||
$this->advance();
|
||||
return new FloatValue(array(
|
||||
'value' => $token->value,
|
||||
'loc' => $this->loc($token->start)
|
||||
));
|
||||
case Token::STRING:
|
||||
$this->advance();
|
||||
return new StringValue(array(
|
||||
'value' => $token->value,
|
||||
'loc' => $this->loc($token->start)
|
||||
));
|
||||
case Token::NAME:
|
||||
$this->advance();
|
||||
switch ($token->value) {
|
||||
case 'true':
|
||||
case 'false':
|
||||
return new BooleanValue(array(
|
||||
'value' => $token->value === 'true',
|
||||
'loc' => $this->loc($token->start)
|
||||
));
|
||||
}
|
||||
return new EnumValue(array(
|
||||
'value' => $token->value,
|
||||
'loc' => $this->loc($token->start)
|
||||
));
|
||||
case Token::DOLLAR:
|
||||
if (!$isConst) {
|
||||
return $this->parseVariable();
|
||||
}
|
||||
break;
|
||||
}
|
||||
throw $this->unexpected();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isConst
|
||||
* @return ArrayValue
|
||||
*/
|
||||
function parseArray($isConst)
|
||||
{
|
||||
$start = $this->token->start;
|
||||
$item = $isConst ? 'parseConstValue' : 'parseVariableValue';
|
||||
return new ArrayValue(array(
|
||||
'values' => $this->any(Token::BRACKET_L, array($this, $item), Token::BRACKET_R),
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
}
|
||||
|
||||
function parseObject($isConst)
|
||||
{
|
||||
$start = $this->token->start;
|
||||
$this->expect(Token::BRACE_L);
|
||||
$fieldNames = array();
|
||||
$fields = array();
|
||||
while (!$this->skip(Token::BRACE_R)) {
|
||||
$fields[] = $this->parseObjectField($isConst, $fieldNames);
|
||||
}
|
||||
return new ObjectValue(array(
|
||||
'fields' => $fields,
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
}
|
||||
|
||||
function parseObjectField($isConst, &$fieldNames)
|
||||
{
|
||||
$start = $this->token->start;
|
||||
$name = $this->parseName();
|
||||
|
||||
if (array_key_exists($name->value, $fieldNames)) {
|
||||
throw Exception::create($this->source, $start, "Duplicate input object field " . $name->value . '.');
|
||||
}
|
||||
$fieldNames[$name->value] = true;
|
||||
$this->expect(Token::COLON);
|
||||
|
||||
return new ObjectField(array(
|
||||
'name' => $name,
|
||||
'value' => $this->parseValue($isConst),
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
}
|
||||
|
||||
// Implements the parsing rules in the Directives section.
|
||||
|
||||
/**
|
||||
* @return array<Directive>
|
||||
*/
|
||||
function parseDirectives()
|
||||
{
|
||||
$directives = array();
|
||||
while ($this->peek(Token::AT)) {
|
||||
$directives[] = $this->parseDirective();
|
||||
}
|
||||
return $directives;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Directive
|
||||
* @throws Exception
|
||||
*/
|
||||
function parseDirective()
|
||||
{
|
||||
$start = $this->token->start;
|
||||
$this->expect(Token::AT);
|
||||
return new Directive(array(
|
||||
'name' => $this->parseName(),
|
||||
'value' => $this->skip(Token::COLON) ? $this->parseValue(false) : null,
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
}
|
||||
|
||||
// Implements the parsing rules in the Types section.
|
||||
|
||||
/**
|
||||
* Handles the Type: TypeName, ListType, and NonNullType parsing rules.
|
||||
*
|
||||
* @return ListType|Name|NonNullType
|
||||
* @throws Exception
|
||||
*/
|
||||
function parseType()
|
||||
{
|
||||
$start = $this->token->start;
|
||||
|
||||
if ($this->skip(Token::BRACKET_L)) {
|
||||
$type = $this->parseType();
|
||||
$this->expect(Token::BRACKET_R);
|
||||
$type = new ListType(array(
|
||||
'type' => $type,
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
} else {
|
||||
$type = $this->parseName();
|
||||
}
|
||||
if ($this->skip(Token::BANG)) {
|
||||
return new NonNullType(array(
|
||||
'type' => $type,
|
||||
'loc' => $this->loc($start)
|
||||
));
|
||||
|
||||
}
|
||||
return $type;
|
||||
}
|
||||
}
|
149
src/Language/Printer.php
Normal file
149
src/Language/Printer.php
Normal file
@ -0,0 +1,149 @@
|
||||
<?php
|
||||
namespace GraphQL\Language;
|
||||
|
||||
|
||||
use GraphQL\Language\AST\Argument;
|
||||
use GraphQL\Language\AST\ArrayValue;
|
||||
use GraphQL\Language\AST\BooleanValue;
|
||||
use GraphQL\Language\AST\Directive;
|
||||
use GraphQL\Language\AST\Document;
|
||||
use GraphQL\Language\AST\EnumValue;
|
||||
use GraphQL\Language\AST\Field;
|
||||
use GraphQL\Language\AST\FloatValue;
|
||||
use GraphQL\Language\AST\FragmentDefinition;
|
||||
use GraphQL\Language\AST\FragmentSpread;
|
||||
use GraphQL\Language\AST\InlineFragment;
|
||||
use GraphQL\Language\AST\IntValue;
|
||||
use GraphQL\Language\AST\ListType;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NonNullType;
|
||||
use GraphQL\Language\AST\ObjectField;
|
||||
use GraphQL\Language\AST\ObjectValue;
|
||||
use GraphQL\Language\AST\OperationDefinition;
|
||||
use GraphQL\Language\AST\SelectionSet;
|
||||
use GraphQL\Language\AST\StringValue;
|
||||
use GraphQL\Language\AST\VariableDefinition;
|
||||
|
||||
class Printer
|
||||
{
|
||||
public static function doPrint($ast)
|
||||
{
|
||||
return Visitor::visit($ast, array(
|
||||
'leave' => array(
|
||||
Node::NAME => function($node) {return $node->value . '';},
|
||||
Node::VARIABLE => function($node) {return '$' . $node->name;},
|
||||
Node::DOCUMENT => function(Document $node) {return self::join($node->definitions, "\n\n") . "\n";},
|
||||
Node::OPERATION_DEFINITION => function(OperationDefinition $node) {
|
||||
$op = $node->operation;
|
||||
$name = $node->name;
|
||||
$defs = Printer::manyList('(', $node->variableDefinitions, ', ', ')');
|
||||
$directives = self::join($node->directives, ' ');
|
||||
$selectionSet = $node->selectionSet;
|
||||
return !$name ? $selectionSet :
|
||||
self::join([$op, self::join([$name, $defs]), $directives, $selectionSet], ' ');
|
||||
},
|
||||
Node::VARIABLE_DEFINITION => function(VariableDefinition $node) {
|
||||
return self::join([$node->variable . ': ' . $node->type, $node->defaultValue], ' = ');
|
||||
},
|
||||
Node::SELECTION_SET => function(SelectionSet $node) {
|
||||
return self::blockList($node->selections, ",\n");
|
||||
},
|
||||
Node::FIELD => function(Field $node) {
|
||||
$r11 = self::join([
|
||||
$node->alias,
|
||||
$node->name
|
||||
], ': ');
|
||||
|
||||
$r1 = self::join([
|
||||
$r11,
|
||||
self::manyList('(', $node->arguments, ', ', ')')
|
||||
]);
|
||||
|
||||
$r2 = self::join($node->directives, ' ');
|
||||
|
||||
return self::join([
|
||||
$r1,
|
||||
$r2,
|
||||
$node->selectionSet
|
||||
], ' ');
|
||||
},
|
||||
Node::ARGUMENT => function(Argument $node) {
|
||||
return $node->name . ': ' . $node->value;
|
||||
},
|
||||
|
||||
// Fragments
|
||||
Node::FRAGMENT_SPREAD => function(FragmentSpread $node) {
|
||||
return self::join(['...' . $node->name, self::join($node->directives, '')], ' ');
|
||||
},
|
||||
Node::INLINE_FRAGMENT => function(InlineFragment $node) {
|
||||
return self::join([
|
||||
'... on',
|
||||
$node->typeCondition,
|
||||
self::join($node->directives, ' '),
|
||||
$node->selectionSet
|
||||
], ' ');
|
||||
},
|
||||
Node::FRAGMENT_DEFINITION => function(FragmentDefinition $node) {
|
||||
return self::join([
|
||||
'fragment',
|
||||
$node->name,
|
||||
'on',
|
||||
$node->typeCondition,
|
||||
self::join($node->directives, ' '),
|
||||
$node->selectionSet
|
||||
], ' ');
|
||||
},
|
||||
|
||||
// Value
|
||||
Node::INT => function(IntValue $node) {return $node->value;},
|
||||
Node::FLOAT => function(FloatValue $node) {return $node->value;},
|
||||
Node::STRING => function(StringValue $node) {return json_encode($node->value);},
|
||||
Node::BOOLEAN => function(BooleanValue $node) {return $node->value ? 'true' : 'false';},
|
||||
Node::ENUM => function(EnumValue $node) {return $node->value;},
|
||||
Node::ARR => function(ArrayValue $node) {return '[' . self::join($node->values, ', ') . ']';},
|
||||
Node::OBJECT => function(ObjectValue $node) {return '{' . self::join($node->fields, ', ') . '}';},
|
||||
Node::OBJECT_FIELD => function(ObjectField $node) {return $node->name . ': ' . $node->value;},
|
||||
|
||||
// Directive
|
||||
Node::DIRECTIVE => function(Directive $node) {return self::join(['@' . $node->name, $node->value], ': ');},
|
||||
|
||||
// Type
|
||||
Node::LIST_TYPE => function(ListType $node) {return '[' . $node->type . ']';},
|
||||
Node::NON_NULL_TYPE => function(NonNullType $node) {return $node->type . '!';}
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
public static function blockList($list, $separator)
|
||||
{
|
||||
return self::length($list) === 0 ? null : self::indent("{\n" . self::join($list, $separator)) . "\n}";
|
||||
}
|
||||
|
||||
public static function indent($maybeString)
|
||||
{
|
||||
return $maybeString ? str_replace("\n", "\n ", $maybeString) : '';
|
||||
}
|
||||
|
||||
public static function manyList($start, $list, $separator, $end)
|
||||
{
|
||||
return self::length($list) === 0 ? null : ($start . self::join($list, $separator) . $end);
|
||||
}
|
||||
|
||||
public static function length($maybeArray)
|
||||
{
|
||||
return $maybeArray ? count($maybeArray) : 0;
|
||||
}
|
||||
|
||||
public static function join($maybeArray, $separator = '')
|
||||
{
|
||||
return $maybeArray
|
||||
? implode(
|
||||
$separator,
|
||||
array_filter(
|
||||
$maybeArray,
|
||||
function($x) { return !!$x;}
|
||||
)
|
||||
)
|
||||
: '';
|
||||
}
|
||||
}
|
49
src/Language/Source.php
Normal file
49
src/Language/Source.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
namespace GraphQL\Language;
|
||||
|
||||
class Source
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $body;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $length;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
public function __construct($body, $name = null)
|
||||
{
|
||||
$this->body = $body;
|
||||
$this->length = mb_strlen($body, 'UTF-8');
|
||||
$this->name = $name ?: 'GraphQL';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $position
|
||||
* @return SourceLocation
|
||||
*/
|
||||
public function getLocation($position)
|
||||
{
|
||||
$line = 1;
|
||||
$column = $position + 1;
|
||||
|
||||
$utfChars = json_decode('"\u2028\u2029"');
|
||||
$lineRegexp = '/\r\n|[\n\r'.$utfChars.']/su';
|
||||
$matches = array();
|
||||
preg_match_all($lineRegexp, mb_substr($this->body, 0, $position, 'UTF-8'), $matches, PREG_OFFSET_CAPTURE);
|
||||
|
||||
foreach ($matches[0] as $index => $match) {
|
||||
$line += 1;
|
||||
$column = $position + 1 - ($match[1] + mb_strlen($match[0], 'UTF-8'));
|
||||
}
|
||||
|
||||
return new SourceLocation($line, $column);
|
||||
}
|
||||
}
|
14
src/Language/SourceLocation.php
Normal file
14
src/Language/SourceLocation.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace GraphQL\Language;
|
||||
|
||||
class SourceLocation
|
||||
{
|
||||
public $line;
|
||||
public $column;
|
||||
|
||||
public function __construct($line, $col)
|
||||
{
|
||||
$this->line = $line;
|
||||
$this->column = $col;
|
||||
}
|
||||
}
|
89
src/Language/Token.php
Normal file
89
src/Language/Token.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
namespace GraphQL\Language;
|
||||
|
||||
// language/lexer.js
|
||||
|
||||
class Token
|
||||
{
|
||||
const EOF = 1;
|
||||
const BANG = 2;
|
||||
const DOLLAR = 3;
|
||||
const PAREN_L = 4;
|
||||
const PAREN_R = 5;
|
||||
const SPREAD = 6;
|
||||
const COLON = 7;
|
||||
const EQUALS = 8;
|
||||
const AT = 9;
|
||||
const BRACKET_L = 10;
|
||||
const BRACKET_R = 11;
|
||||
const BRACE_L = 12;
|
||||
const PIPE = 13;
|
||||
const BRACE_R = 14;
|
||||
const NAME = 15;
|
||||
const VARIABLE = 16;
|
||||
const INT = 17;
|
||||
const FLOAT = 18;
|
||||
const STRING = 19;
|
||||
|
||||
public static function getKindDescription($kind)
|
||||
{
|
||||
$description = array();
|
||||
$description[self::EOF] = 'EOF';
|
||||
$description[self::BANG] = '!';
|
||||
$description[self::DOLLAR] = '$';
|
||||
$description[self::PAREN_L] = '(';
|
||||
$description[self::PAREN_R] = ')';
|
||||
$description[self::SPREAD] = '...';
|
||||
$description[self::COLON] = ':';
|
||||
$description[self::EQUALS] = '=';
|
||||
$description[self::AT] = '@';
|
||||
$description[self::BRACKET_L] = '[';
|
||||
$description[self::BRACKET_R] = ']';
|
||||
$description[self::BRACE_L] = '{';
|
||||
$description[self::PIPE] = '|';
|
||||
$description[self::BRACE_R] = '}';
|
||||
$description[self::NAME] = 'Name';
|
||||
$description[self::VARIABLE] = 'Variable';
|
||||
$description[self::INT] = 'Int';
|
||||
$description[self::FLOAT] = 'Float';
|
||||
$description[self::STRING] = 'String';
|
||||
|
||||
return $description[$kind];
|
||||
}
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $kind;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $start;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $end;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $value;
|
||||
|
||||
public function __construct($kind, $start, $end, $value = null)
|
||||
{
|
||||
$this->kind = $kind;
|
||||
$this->start = (int) $start;
|
||||
$this->end = (int) $end;
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return self::getKindDescription($this->kind) . ($this->value ? ' "' . $this->value . '"' : '');
|
||||
}
|
||||
}
|
353
src/Language/Visitor.php
Normal file
353
src/Language/Visitor.php
Normal file
@ -0,0 +1,353 @@
|
||||
<?php
|
||||
namespace GraphQL\Language;
|
||||
|
||||
use GraphQL\Language\AST\Node;
|
||||
|
||||
class Visitor
|
||||
{
|
||||
const BREAK_VISIT = '@@BREAK@@';
|
||||
const CONTINUE_VISIT = '@@CONTINUE@@';
|
||||
|
||||
/**
|
||||
* Break visitor
|
||||
*
|
||||
* @return VisitorOperation
|
||||
*/
|
||||
public static function stop()
|
||||
{
|
||||
$r = new VisitorOperation();
|
||||
$r->doBreak = true;
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip current node
|
||||
*/
|
||||
public static function skipNode()
|
||||
{
|
||||
$r = new VisitorOperation();
|
||||
$r->doContinue = true;
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove current node
|
||||
*/
|
||||
public static function removeNode()
|
||||
{
|
||||
$r = new VisitorOperation();
|
||||
$r->removeNode = true;
|
||||
return $r;
|
||||
}
|
||||
|
||||
public static $visitorKeys = array(
|
||||
Node::NAME => [],
|
||||
Node::DOCUMENT => ['definitions'],
|
||||
Node::OPERATION_DEFINITION => ['name', 'variableDefinitions', 'directives', 'selectionSet'],
|
||||
Node::VARIABLE_DEFINITION => ['variable', 'type', 'defaultValue'],
|
||||
Node::VARIABLE => ['name'],
|
||||
Node::SELECTION_SET => ['selections'],
|
||||
Node::FIELD => ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
|
||||
Node::ARGUMENT => ['name', 'value'],
|
||||
Node::FRAGMENT_SPREAD => ['name', 'directives'],
|
||||
Node::INLINE_FRAGMENT => ['typeCondition', 'directives', 'selectionSet'],
|
||||
Node::FRAGMENT_DEFINITION => ['name', 'typeCondition', 'directives', 'selectionSet'],
|
||||
|
||||
Node::INT => [],
|
||||
Node::FLOAT => [],
|
||||
Node::STRING => [],
|
||||
Node::BOOLEAN => [],
|
||||
Node::ENUM => [],
|
||||
Node::ARR => ['values'],
|
||||
Node::OBJECT => ['fields'],
|
||||
Node::OBJECT_FIELD => ['name', 'value'],
|
||||
Node::DIRECTIVE => ['name', 'value'],
|
||||
Node::LIST_TYPE => ['type'],
|
||||
Node::NON_NULL_TYPE => ['type'],
|
||||
);
|
||||
|
||||
/**
|
||||
* visit() will walk through an AST using a depth first traversal, calling
|
||||
* the visitor's enter function at each node in the traversal, and calling the
|
||||
* leave function after visiting that node and all of it's child nodes.
|
||||
*
|
||||
* By returning different values from the enter and leave functions, the
|
||||
* behavior of the visitor can be altered, including skipping over a sub-tree of
|
||||
* the AST (by returning false), editing the AST by returning a value or null
|
||||
* to remove the value, or to stop the whole traversal by returning BREAK.
|
||||
*
|
||||
* When using visit() to edit an AST, the original AST will not be modified, and
|
||||
* a new version of the AST with the changes applied will be returned from the
|
||||
* visit function.
|
||||
*
|
||||
* var editedAST = visit(ast, {
|
||||
* enter(node, key, parent, path, ancestors) {
|
||||
* // @return
|
||||
* // undefined: no action
|
||||
* // false: skip visiting this node
|
||||
* // visitor.BREAK: stop visiting altogether
|
||||
* // null: delete this node
|
||||
* // any value: replace this node with the returned value
|
||||
* },
|
||||
* leave(node, key, parent, path, ancestors) {
|
||||
* // @return
|
||||
* // undefined: no action
|
||||
* // visitor.BREAK: stop visiting altogether
|
||||
* // null: delete this node
|
||||
* // any value: replace this node with the returned value
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* Alternatively to providing enter() and leave() functions, a visitor can
|
||||
* instead provide functions named the same as the kinds of AST nodes, or
|
||||
* enter/leave visitors at a named key, leading to four permutations of
|
||||
* visitor API:
|
||||
*
|
||||
* 1) Named visitors triggered when entering a node a specific kind.
|
||||
*
|
||||
* visit(ast, {
|
||||
* Kind(node) {
|
||||
* // enter the "Kind" node
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* 2) Named visitors that trigger upon entering and leaving a node of
|
||||
* a specific kind.
|
||||
*
|
||||
* visit(ast, {
|
||||
* Kind: {
|
||||
* enter(node) {
|
||||
* // enter the "Kind" node
|
||||
* }
|
||||
* leave(node) {
|
||||
* // leave the "Kind" node
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* 3) Generic visitors that trigger upon entering and leaving any node.
|
||||
*
|
||||
* visit(ast, {
|
||||
* enter(node) {
|
||||
* // enter any node
|
||||
* },
|
||||
* leave(node) {
|
||||
* // leave any node
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* 4) Parallel visitors for entering and leaving nodes of a specific kind.
|
||||
*
|
||||
* visit(ast, {
|
||||
* enter: {
|
||||
* Kind(node) {
|
||||
* // enter the "Kind" node
|
||||
* }
|
||||
* },
|
||||
* leave: {
|
||||
* Kind(node) {
|
||||
* // leave the "Kind" node
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
*/
|
||||
public static function visit($root, $visitor)
|
||||
{
|
||||
$visitorKeys = isset($visitor['keys']) ? $visitor['keys'] : self::$visitorKeys;
|
||||
|
||||
$stack = null;
|
||||
$inArray = is_array($root);
|
||||
$keys = [$root];
|
||||
$index = -1;
|
||||
$edits = [];
|
||||
$parent = null;
|
||||
$path = [];
|
||||
$ancestors = [];
|
||||
$newRoot = $root;
|
||||
|
||||
$UNDEFINED = null;
|
||||
|
||||
do {
|
||||
$index++;
|
||||
$isLeaving = $index === count($keys);
|
||||
$key = null;
|
||||
$node = null;
|
||||
$isEdited = $isLeaving && count($edits) !== 0;
|
||||
|
||||
if ($isLeaving) {
|
||||
$key = count($ancestors) === 0 ? $UNDEFINED : array_pop($path);
|
||||
$node = $parent;
|
||||
$parent = array_pop($ancestors);
|
||||
|
||||
if ($isEdited) {
|
||||
if ($inArray) {
|
||||
// $node = $node; // arrays are value types in PHP
|
||||
} else {
|
||||
$node = clone $node;
|
||||
}
|
||||
$editOffset = 0;
|
||||
for ($ii = 0; $ii < count($edits); $ii++) {
|
||||
$editKey = $edits[$ii][0];
|
||||
$editValue = $edits[$ii][1];
|
||||
|
||||
if ($inArray) {
|
||||
$editKey -= $editOffset;
|
||||
}
|
||||
if ($inArray && $editValue === null) {
|
||||
array_splice($node, $editKey, 1);
|
||||
$editOffset++;
|
||||
} else {
|
||||
if (is_array($node)) {
|
||||
$node[$editKey] = $editValue;
|
||||
} else {
|
||||
$node->{$editKey} = $editValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$index = $stack['index'];
|
||||
$keys = $stack['keys'];
|
||||
$edits = $stack['edits'];
|
||||
$inArray = $stack['inArray'];
|
||||
$stack = $stack['prev'];
|
||||
} else {
|
||||
$key = $parent ? ($inArray ? $index : $keys[$index]) : $UNDEFINED;
|
||||
$node = $parent ? (is_array($parent) ? $parent[$key] : $parent->{$key}) : $newRoot;
|
||||
if ($node === null || $node === $UNDEFINED) {
|
||||
continue;
|
||||
}
|
||||
if ($parent) {
|
||||
$path[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
$result = null;
|
||||
if (!is_array($node)) {
|
||||
if (!($node instanceof Node)) {
|
||||
throw new \Exception('Invalid AST Node: ' . json_encode($node));
|
||||
}
|
||||
|
||||
$visitFn = self::getVisitFn($visitor, $isLeaving, $node->kind);
|
||||
|
||||
if ($visitFn) {
|
||||
$result = call_user_func($visitFn, $node, $key, $parent, $path, $ancestors);
|
||||
|
||||
if ($result !== null) {
|
||||
if ($result instanceof VisitorOperation) {
|
||||
if ($result->doBreak) {
|
||||
break;
|
||||
}
|
||||
if (!$isLeaving && $result->doContinue) {
|
||||
array_pop($path);
|
||||
continue;
|
||||
}
|
||||
$editValue = null;
|
||||
} else {
|
||||
$editValue = $result;
|
||||
}
|
||||
|
||||
$edits[] = [$key, $editValue];
|
||||
if (!$isLeaving) {
|
||||
if ($editValue instanceof Node) {
|
||||
$node = $editValue;
|
||||
} else {
|
||||
array_pop($path);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($result === null && $isEdited) {
|
||||
$edits[] = [$key, $node];
|
||||
}
|
||||
|
||||
if (!$isLeaving) {
|
||||
$stack = array(
|
||||
'inArray' => $inArray,
|
||||
'index' => $index,
|
||||
'keys' => $keys,
|
||||
'edits' => $edits,
|
||||
'prev' => $stack
|
||||
);
|
||||
$inArray = is_array($node);
|
||||
|
||||
$keys = ($inArray ? $node : $visitorKeys[$node->kind]) ?: array();
|
||||
$index = -1;
|
||||
$edits = [];
|
||||
if ($parent) {
|
||||
$ancestors[] = $parent;
|
||||
}
|
||||
$parent = $node;
|
||||
}
|
||||
|
||||
} while ($stack);
|
||||
|
||||
if (count($edits) !== 0) {
|
||||
$newRoot = $edits[0][1];
|
||||
}
|
||||
|
||||
return $newRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $visitor
|
||||
* @param $isLeaving
|
||||
* @param $kind
|
||||
* @return null
|
||||
*/
|
||||
public static function getVisitFn($visitor, $isLeaving, $kind)
|
||||
{
|
||||
if (!$visitor) {
|
||||
return null;
|
||||
}
|
||||
$kindVisitor = isset($visitor[$kind]) ? $visitor[$kind] : null;
|
||||
|
||||
if (!$isLeaving && is_callable($kindVisitor)) {
|
||||
// { Kind() {} }
|
||||
return $kindVisitor;
|
||||
}
|
||||
|
||||
if (is_array($kindVisitor)) {
|
||||
if ($isLeaving) {
|
||||
$kindSpecificVisitor = isset($kindVisitor['leave']) ? $kindVisitor['leave'] : null;
|
||||
} else {
|
||||
$kindSpecificVisitor = isset($kindVisitor['enter']) ? $kindVisitor['enter'] : null;
|
||||
}
|
||||
|
||||
if ($kindSpecificVisitor && is_callable($kindSpecificVisitor)) {
|
||||
// { Kind: { enter() {}, leave() {} } }
|
||||
return $kindSpecificVisitor;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$visitor += ['leave' => null, 'enter' => null];
|
||||
$specificVisitor = $isLeaving ? $visitor['leave'] : $visitor['enter'];
|
||||
|
||||
if ($specificVisitor) {
|
||||
if (is_callable($specificVisitor)) {
|
||||
// { enter() {}, leave() {} }
|
||||
return $specificVisitor;
|
||||
}
|
||||
$specificKindVisitor = isset($specificVisitor[$kind]) ? $specificVisitor[$kind] : null;
|
||||
|
||||
if (is_callable($specificKindVisitor)) {
|
||||
// { enter: { Kind() {} }, leave: { Kind() {} } }
|
||||
return $specificKindVisitor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class VisitorOperation
|
||||
{
|
||||
public $doBreak;
|
||||
|
||||
public $doContinue;
|
||||
|
||||
public $removeNode;
|
||||
}
|
121
src/Schema.php
Normal file
121
src/Schema.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
namespace GraphQL;
|
||||
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
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 Schema
|
||||
{
|
||||
protected $querySchema;
|
||||
|
||||
protected $mutationSchema;
|
||||
|
||||
protected $_typeMap;
|
||||
|
||||
protected $_directives;
|
||||
|
||||
public function __construct(Type $querySchema = null, Type $mutationSchema = null)
|
||||
{
|
||||
Utils::invariant($querySchema || $mutationSchema, "Either query or mutation type must be set");
|
||||
$this->querySchema = $querySchema;
|
||||
$this->mutationSchema = $mutationSchema;
|
||||
}
|
||||
|
||||
public function getQueryType()
|
||||
{
|
||||
return $this->querySchema;
|
||||
}
|
||||
|
||||
public function getMutationType()
|
||||
{
|
||||
return $this->mutationSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* @return null
|
||||
*/
|
||||
public function getDirective($name)
|
||||
{
|
||||
foreach ($this->getDirectives() as $directive) {
|
||||
if ($directive->name === $name) {
|
||||
return $directive;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<Directive>
|
||||
*/
|
||||
public function getDirectives()
|
||||
{
|
||||
if (!$this->_directives) {
|
||||
$this->_directives = [
|
||||
Directive::ifDirective(),
|
||||
Directive::unlessDirective()
|
||||
];
|
||||
}
|
||||
return $this->_directives;
|
||||
}
|
||||
|
||||
public function getTypeMap()
|
||||
{
|
||||
if (null === $this->_typeMap) {
|
||||
$map = [];
|
||||
foreach ([$this->getQueryType(), $this->getMutationType(), Introspection::_schema()] as $type) {
|
||||
$this->_extractTypes($type, $map);
|
||||
}
|
||||
$this->_typeMap = $map + Type::getInternalTypes();
|
||||
}
|
||||
return $this->_typeMap;
|
||||
}
|
||||
|
||||
private function _extractTypes($type, &$map)
|
||||
{
|
||||
if ($type instanceof WrappingType) {
|
||||
return $this->_extractTypes($type->getWrappedType(), $map);
|
||||
}
|
||||
|
||||
if (!$type instanceof Type || !empty($map[$type->name])) {
|
||||
// TODO: warning?
|
||||
return $map;
|
||||
}
|
||||
|
||||
$map[$type->name] = $type;
|
||||
|
||||
$nestedTypes = [];
|
||||
|
||||
if ($type instanceof InterfaceType || $type instanceof UnionType) {
|
||||
$nestedTypes = $type->getPossibleTypes();
|
||||
}
|
||||
if ($type instanceof ObjectType) {
|
||||
$nestedTypes = array_merge($nestedTypes, $type->getInterfaces());
|
||||
}
|
||||
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
|
||||
foreach ((array) $type->getFields() as $fieldName => $field) {
|
||||
if (null === $field->args) {
|
||||
trigger_error('WTF ' . $field->name . ' has no args?'); // gg
|
||||
}
|
||||
$fieldArgTypes = array_map(function($arg) { return $arg->getType(); }, $field->args);
|
||||
$nestedTypes = array_merge($nestedTypes, $fieldArgTypes);
|
||||
$nestedTypes[] = $field->getType();
|
||||
}
|
||||
}
|
||||
foreach ($nestedTypes as $type) {
|
||||
$this->_extractTypes($type, $map);
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
public function getType($name)
|
||||
{
|
||||
$map = $this->getTypeMap();
|
||||
return isset($map[$name]) ? $map[$name] : null;
|
||||
}
|
||||
}
|
16
src/Type/Definition/AbstractType.php
Normal file
16
src/Type/Definition/AbstractType.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
|
||||
interface AbstractType
|
||||
{
|
||||
/*
|
||||
export type GraphQLAbstractType =
|
||||
GraphQLInterfaceType |
|
||||
GraphQLUnionType;
|
||||
*/
|
||||
/**
|
||||
* @return array<ObjectType>
|
||||
*/
|
||||
public function getPossibleTypes();
|
||||
}
|
22
src/Type/Definition/BooleanType.php
Normal file
22
src/Type/Definition/BooleanType.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Language\AST\BooleanValue;
|
||||
|
||||
class BooleanType extends ScalarType
|
||||
{
|
||||
public $name = Type::BOOLEAN;
|
||||
|
||||
public function coerce($value)
|
||||
{
|
||||
return !!$value;
|
||||
}
|
||||
|
||||
public function coerceLiteral($ast)
|
||||
{
|
||||
if ($ast instanceof BooleanValue) {
|
||||
return (bool) $ast->value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
13
src/Type/Definition/CompositeType.php
Normal file
13
src/Type/Definition/CompositeType.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
|
||||
interface CompositeType
|
||||
{
|
||||
/*
|
||||
export type GraphQLCompositeType =
|
||||
GraphQLObjectType |
|
||||
GraphQLInterfaceType |
|
||||
GraphQLUnionType;
|
||||
*/
|
||||
}
|
203
src/Type/Definition/Config.php
Normal file
203
src/Type/Definition/Config.php
Normal file
@ -0,0 +1,203 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Utils;
|
||||
|
||||
class Config
|
||||
{
|
||||
const BOOLEAN = 1;
|
||||
const STRING = 2;
|
||||
const INT = 4;
|
||||
const FLOAT = 8;
|
||||
const NUMERIC = 16;
|
||||
const SCALAR = 32;
|
||||
const CALLBACK = 64;
|
||||
const ANY = 128;
|
||||
|
||||
const OUTPUT_TYPE = 2048;
|
||||
const INPUT_TYPE = 4096;
|
||||
const INTERFACE_TYPE = 8192;
|
||||
const OBJECT_TYPE = 16384;
|
||||
|
||||
const REQUIRED = 65536;
|
||||
const KEY_AS_NAME = 131072;
|
||||
|
||||
private static $enableValidation = false;
|
||||
|
||||
public static function disableValidation()
|
||||
{
|
||||
self::$enableValidation = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable deep config validation (disabled by default because it creates significant performance overhead).
|
||||
* Useful only at development to catch type definition errors quickly.
|
||||
*/
|
||||
public static function enableValidation()
|
||||
{
|
||||
self::$enableValidation = true;
|
||||
}
|
||||
|
||||
public static function validate(array $config, array $definition)
|
||||
{
|
||||
if (self::$enableValidation) {
|
||||
self::_validateMap($config, $definition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $definition
|
||||
* @param int $flags
|
||||
* @return \stdClass
|
||||
*/
|
||||
public static function map(array $definition, $flags = 0)
|
||||
{
|
||||
$tmp = new \stdClass();
|
||||
$tmp->isMap = true;
|
||||
$tmp->definition = $definition;
|
||||
$tmp->flags = $flags;
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|int $definition
|
||||
* @param int $flags
|
||||
* @return \stdClass
|
||||
*/
|
||||
public static function arrayOf($definition, $flags = 0)
|
||||
{
|
||||
$tmp = new \stdClass();
|
||||
$tmp->isArray = true;
|
||||
$tmp->definition = $definition;
|
||||
$tmp->flags = (int) $flags;
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
private static function _validateMap(array $map, array $definitions, $pathStr = null)
|
||||
{
|
||||
$suffix = $pathStr ? " at $pathStr" : '';
|
||||
|
||||
// Make sure there are no unexpected keys in map
|
||||
$unexpectedKeys = array_keys(array_diff_key($map, $definitions));
|
||||
Utils::invariant(empty($unexpectedKeys), 'Unexpected keys "%s" ' . $suffix, implode(', ', $unexpectedKeys));
|
||||
|
||||
// Make sure that all required keys are present in map
|
||||
$requiredKeys = array_filter($definitions, function($def) {return (self::_getFlags($def) & self::REQUIRED) > 0;});
|
||||
$missingKeys = array_keys(array_diff_key($requiredKeys, $map));
|
||||
Utils::invariant(empty($missingKeys), 'Required keys missing: "%s"' . $suffix, implode(', ', $missingKeys));
|
||||
|
||||
// Make sure that every map value is valid given the definition
|
||||
foreach ($map as $key => $value) {
|
||||
self::_validateEntry($key, $value, $definitions[$key], $pathStr ? "$pathStr:$key" : $key);
|
||||
}
|
||||
}
|
||||
|
||||
private static function _validateEntry($key, $value, $def, $pathStr)
|
||||
{
|
||||
$type = Utils::getVariableType($value);
|
||||
$err = 'Expecting %s at "' . $pathStr . '", but got "' . $type . '"';
|
||||
|
||||
if ($def instanceof \stdClass) {
|
||||
if ($def->flags & self::REQUIRED === 0 && $value === null) {
|
||||
return ;
|
||||
}
|
||||
Utils::invariant(is_array($value), $err, 'array');
|
||||
|
||||
if (!empty($def->isMap)) {
|
||||
if ($def->flags & self::KEY_AS_NAME) {
|
||||
$value += ['name' => $key];
|
||||
}
|
||||
self::_validateMap($value, $def->definition, $pathStr);
|
||||
} else if (!empty($def->isArray)) {
|
||||
|
||||
if ($def->flags & self::REQUIRED) {
|
||||
Utils::invariant(!empty($value), "Value at '$pathStr' cannot be empty array");
|
||||
}
|
||||
|
||||
$err = "Each entry at '$pathStr' must be an array, but '%s' is '%s'";
|
||||
|
||||
foreach ($value as $arrKey => $arrValue) {
|
||||
if (is_array($def->definition)) {
|
||||
Utils::invariant(is_array($arrValue), $err, $arrKey, Utils::getVariableType($arrValue));
|
||||
|
||||
if ($def->flags & self::KEY_AS_NAME) {
|
||||
$arrValue += ['name' => $arrKey];
|
||||
}
|
||||
self::_validateMap($arrValue, $def->definition, "$pathStr:$arrKey");
|
||||
} else {
|
||||
self::_validateEntry($arrKey, $arrValue, $def->definition, "$pathStr:$arrKey");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new \Exception("Unexpected definition: " . print_r($def, true));
|
||||
}
|
||||
} else {
|
||||
Utils::invariant(is_int($def), "Definition for '$pathStr' is expected to be single integer value");
|
||||
|
||||
if ($def & self::REQUIRED) {
|
||||
Utils::invariant($value !== null, 'Value at "%s" can not be null', $pathStr);
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case $def & self::ANY:
|
||||
break;
|
||||
case $def & self::BOOLEAN:
|
||||
Utils::invariant(is_bool($value), $err, 'boolean');
|
||||
break;
|
||||
case $def & self::STRING:
|
||||
Utils::invariant(is_string($value), $err, 'string');
|
||||
break;
|
||||
case $def & self::NUMERIC:
|
||||
Utils::invariant(is_numeric($value), $err, 'numeric');
|
||||
break;
|
||||
case $def & self::FLOAT:
|
||||
Utils::invariant(is_float($value), $err, 'float');
|
||||
break;
|
||||
case $def & self::INT:
|
||||
Utils::invariant(is_int($value), $err, 'int');
|
||||
break;
|
||||
case $def & self::CALLBACK:
|
||||
Utils::invariant(is_callable($value), $err, 'callable');
|
||||
break;
|
||||
case $def & self::SCALAR:
|
||||
Utils::invariant(is_scalar($value), $err, 'scalar');
|
||||
break;
|
||||
case $def & self::INPUT_TYPE:
|
||||
Utils::invariant(
|
||||
is_callable($value) || $value instanceof InputType,
|
||||
$err,
|
||||
'callable or instance of GraphQL\Type\Definition\InputType'
|
||||
);
|
||||
break;
|
||||
case $def & self::OUTPUT_TYPE:
|
||||
Utils::invariant(
|
||||
is_callable($value) || $value instanceof OutputType,
|
||||
$err,
|
||||
'callable or instance of GraphQL\Type\Definition\OutputType'
|
||||
);
|
||||
break;
|
||||
case $def & self::INTERFACE_TYPE:
|
||||
Utils::invariant(
|
||||
is_callable($value) || $value instanceof InterfaceType,
|
||||
$err,
|
||||
'callable or instance of GraphQL\Type\Definition\InterfaceType'
|
||||
);
|
||||
break;
|
||||
case $def & self::OBJECT_TYPE:
|
||||
Utils::invariant(
|
||||
is_callable($value) || $value instanceof ObjectType,
|
||||
$err,
|
||||
'callable or instance of GraphQL\Type\Definition\ObjectType'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new \Exception("Unexpected validation rule: " . $def);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function _getFlags($def)
|
||||
{
|
||||
return is_object($def) ? $def->flags : $def;
|
||||
}
|
||||
}
|
87
src/Type/Definition/Directive.php
Normal file
87
src/Type/Definition/Directive.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
class Directive
|
||||
{
|
||||
public static $internalDirectives;
|
||||
|
||||
/**
|
||||
* @return Directive
|
||||
*/
|
||||
public static function ifDirective()
|
||||
{
|
||||
$internal = self::getInternalDirectives();
|
||||
return $internal['if'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Directive
|
||||
*/
|
||||
public static function unlessDirective()
|
||||
{
|
||||
$internal = self::getInternalDirectives();
|
||||
return $internal['unless'];
|
||||
}
|
||||
|
||||
public static function getInternalDirectives()
|
||||
{
|
||||
if (!self::$internalDirectives) {
|
||||
self::$internalDirectives = [
|
||||
'if' => new self([
|
||||
'name' => 'if',
|
||||
'description' => 'Directs the executor to omit this field if the argument provided is false.',
|
||||
'type' => Type::nonNull(Type::boolean()),
|
||||
'onOperation' => false,
|
||||
'onFragment' => false,
|
||||
'onField' => true
|
||||
]),
|
||||
'unless' => new self([
|
||||
'name' => 'unless',
|
||||
'description' => 'Directs the executor to omit this field if the argument provided is true.',
|
||||
'type' => Type::nonNull(Type::boolean()),
|
||||
'onOperation' => false,
|
||||
'onFragment' => false,
|
||||
'onField' => true
|
||||
])
|
||||
];
|
||||
}
|
||||
return self::$internalDirectives;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* @var Type
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
public $onOperation;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
public $onFragment;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
public $onField;
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
foreach ($config as $key => $value) {
|
||||
$this->{$key} = $value;
|
||||
}
|
||||
}
|
||||
}
|
107
src/Type/Definition/EnumType.php
Normal file
107
src/Type/Definition/EnumType.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Language\AST\EnumValue;
|
||||
use GraphQL\Utils;
|
||||
|
||||
class EnumType extends Type implements InputType, OutputType
|
||||
{
|
||||
/**
|
||||
* @var array<EnumValueDefinition>
|
||||
*/
|
||||
private $_values;
|
||||
|
||||
/**
|
||||
* @var \ArrayObject<mixed, EnumValueDefinition>
|
||||
*/
|
||||
private $_valueLookup;
|
||||
|
||||
/**
|
||||
* @var \ArrayObject<string, EnumValueDefinition>
|
||||
*/
|
||||
private $_nameLookup;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
Config::validate($config, [
|
||||
'name' => Config::STRING | Config::REQUIRED,
|
||||
'values' => Config::arrayOf([
|
||||
'name' => Config::STRING | Config::REQUIRED,
|
||||
'value' => Config::ANY,
|
||||
'deprecationReason' => Config::STRING,
|
||||
'description' => Config::STRING
|
||||
], Config::KEY_AS_NAME),
|
||||
'description' => Config::STRING
|
||||
]);
|
||||
|
||||
$this->name = $config['name'];
|
||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||
$this->_values = [];
|
||||
|
||||
if (!empty($config['values'])) {
|
||||
foreach ($config['values'] as $name => $value) {
|
||||
$this->_values[] = Utils::assign(new EnumValueDefinition(), $value + ['name' => $name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<EnumValueDefinition>
|
||||
*/
|
||||
public function getValues()
|
||||
{
|
||||
return $this->_values;
|
||||
}
|
||||
|
||||
public function coerce($value)
|
||||
{
|
||||
$enumValue = $this->_getValueLookup()->offsetGet($value);
|
||||
return $enumValue ? $enumValue->name : null;
|
||||
}
|
||||
|
||||
public function coerceLiteral($value)
|
||||
{
|
||||
if ($value instanceof EnumValue) {
|
||||
$lookup = $this->_getNameLookup();
|
||||
if (isset($lookup[$value->value])) {
|
||||
$enumValue = $lookup[$value->value];
|
||||
if ($enumValue) {
|
||||
return $enumValue->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Value lookup for any type, not just scalars
|
||||
* @return \ArrayObject<mixed, EnumValueDefinition>
|
||||
*/
|
||||
protected function _getValueLookup()
|
||||
{
|
||||
if (null === $this->_valueLookup) {
|
||||
$this->_valueLookup = new \ArrayObject();
|
||||
|
||||
foreach ($this->getValues() as $valueName => $value) {
|
||||
$this->_valueLookup[$value->value] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->_valueLookup;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \ArrayObject<string, GraphQLEnumValueDefinition>
|
||||
*/
|
||||
protected function _getNameLookup()
|
||||
{
|
||||
if (!$this->_nameLookup) {
|
||||
$lookup = new \ArrayObject();
|
||||
foreach ($this->getValues() as $value) {
|
||||
$lookup[$value->name] = $value;
|
||||
}
|
||||
$this->_nameLookup = $lookup;
|
||||
}
|
||||
return $this->_nameLookup;
|
||||
}
|
||||
}
|
25
src/Type/Definition/EnumValueDefinition.php
Normal file
25
src/Type/Definition/EnumValueDefinition.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
class EnumValueDefinition
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $deprecationReason;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $description;
|
||||
}
|
54
src/Type/Definition/FieldArgument.php
Normal file
54
src/Type/Definition/FieldArgument.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
|
||||
use GraphQL\Utils;
|
||||
|
||||
class FieldArgument
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var InputType
|
||||
*/
|
||||
private $type;
|
||||
|
||||
private $resolvedType;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
public $defaultValue;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $description;
|
||||
|
||||
public static function createMap(array $config)
|
||||
{
|
||||
$map = [];
|
||||
foreach ($config as $name => $argConfig) {
|
||||
$map[] = new self($argConfig + ['name' => $name]);
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
public function __construct(array $def)
|
||||
{
|
||||
foreach ($def as $key => $value) {
|
||||
$this->{$key} = $value;
|
||||
}
|
||||
}
|
||||
|
||||
public function getType()
|
||||
{
|
||||
if (null === $this->resolvedType) {
|
||||
$this->resolvedType = Type::resolve($this->type);
|
||||
}
|
||||
return $this->resolvedType;
|
||||
}
|
||||
}
|
111
src/Type/Definition/FieldDefinition.php
Normal file
111
src/Type/Definition/FieldDefinition.php
Normal file
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
class FieldDefinition
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var OutputType
|
||||
*/
|
||||
private $type;
|
||||
|
||||
private $resolvedType;
|
||||
|
||||
/**
|
||||
* @var array<GraphQLFieldArgument>
|
||||
*/
|
||||
public $args;
|
||||
|
||||
/**
|
||||
* source?: any,
|
||||
* args?: ?{[argName: string]: any},
|
||||
* context?: any,
|
||||
* fieldAST?: any,
|
||||
* fieldType?: any,
|
||||
* parentType?: any,
|
||||
* schema?: GraphQLSchema
|
||||
*
|
||||
* @var callable
|
||||
*/
|
||||
public $resolve;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $deprecationReason;
|
||||
|
||||
private static $def;
|
||||
|
||||
public static function getDefinition()
|
||||
{
|
||||
return self::$def ?: (self::$def = [
|
||||
'name' => Config::STRING | Config::REQUIRED,
|
||||
'type' => Config::OUTPUT_TYPE | Config::REQUIRED,
|
||||
'args' => Config::arrayOf([
|
||||
'name' => Config::STRING | Config::REQUIRED,
|
||||
'type' => Config::INPUT_TYPE | Config::REQUIRED,
|
||||
'defaultValue' => Config::ANY
|
||||
], Config::KEY_AS_NAME),
|
||||
'resolve' => Config::CALLBACK,
|
||||
'description' => Config::STRING,
|
||||
'deprecationReason' => Config::STRING,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|Config $fields
|
||||
* @return array
|
||||
*/
|
||||
public static function createMap(array $fields)
|
||||
{
|
||||
$map = [];
|
||||
foreach ($fields as $name => $field) {
|
||||
if (!isset($field['name'])) {
|
||||
$field['name'] = $name;
|
||||
}
|
||||
$map[$name] = self::create($field);
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|Config $field
|
||||
* @return FieldDefinition
|
||||
*/
|
||||
public static function create($field)
|
||||
{
|
||||
Config::validate($field, self::getDefinition());
|
||||
return new self($field);
|
||||
}
|
||||
|
||||
protected function __construct(array $config)
|
||||
{
|
||||
$this->name = $config['name'];
|
||||
$this->type = $config['type'];
|
||||
$this->resolve = isset($config['resolve']) ? $config['resolve'] : null;
|
||||
$this->args = isset($config['args']) ? FieldArgument::createMap($config['args']) : [];
|
||||
|
||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||
$this->deprecationReason = isset($config['deprecationReason']) ? $config['deprecationReason'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Type
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
if (null === $this->resolvedType) {
|
||||
$this->resolvedType = Type::resolve($this->type);
|
||||
}
|
||||
return $this->resolvedType;
|
||||
}
|
||||
}
|
23
src/Type/Definition/FloatType.php
Normal file
23
src/Type/Definition/FloatType.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Language\AST\FloatValue;
|
||||
use GraphQL\Language\AST\IntValue;
|
||||
|
||||
class FloatType extends ScalarType
|
||||
{
|
||||
public $name = Type::FLOAT;
|
||||
|
||||
public function coerce($value)
|
||||
{
|
||||
return is_numeric($value) || $value === true || $value === false ? (float) $value : null;
|
||||
}
|
||||
|
||||
public function coerceLiteral($ast)
|
||||
{
|
||||
if ($ast instanceof FloatValue || $ast instanceof IntValue) {
|
||||
return (float) $ast->value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
23
src/Type/Definition/IDType.php
Normal file
23
src/Type/Definition/IDType.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Language\AST\IntValue;
|
||||
use GraphQL\Language\AST\StringValue;
|
||||
|
||||
class IDType extends ScalarType
|
||||
{
|
||||
public $name = 'ID';
|
||||
|
||||
public function coerce($value)
|
||||
{
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
public function coerceLiteral($ast)
|
||||
{
|
||||
if ($ast instanceof StringValue || $ast instanceof IntValue) {
|
||||
return $ast->value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
39
src/Type/Definition/InputObjectField.php
Normal file
39
src/Type/Definition/InputObjectField.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Utils;
|
||||
|
||||
class InputObjectField
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var callback|InputType
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var mixed|null
|
||||
*/
|
||||
public $defaultValue;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $description;
|
||||
|
||||
public function __construct(array $opts)
|
||||
{
|
||||
foreach ($opts as $k => $v) {
|
||||
$this->{$k} = $v;
|
||||
}
|
||||
}
|
||||
|
||||
public function getType()
|
||||
{
|
||||
return Type::resolve($this->type);
|
||||
}
|
||||
}
|
41
src/Type/Definition/InputObjectType.php
Normal file
41
src/Type/Definition/InputObjectType.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
class InputObjectType extends Type implements InputType
|
||||
{
|
||||
/**
|
||||
* @var array<InputObjectField>
|
||||
*/
|
||||
private $_fields = [];
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
Config::validate($config, [
|
||||
'name' => Config::STRING | Config::REQUIRED,
|
||||
'fields' => Config::arrayOf([
|
||||
'name' => Config::STRING | Config::REQUIRED,
|
||||
'type' => Config::INPUT_TYPE | Config::REQUIRED,
|
||||
'defaultValue' => Config::ANY,
|
||||
'description' => Config::STRING
|
||||
], Config::KEY_AS_NAME),
|
||||
'description' => Config::STRING
|
||||
]);
|
||||
|
||||
if (!empty($config['fields'])) {
|
||||
foreach ($config['fields'] as $name => $field) {
|
||||
$this->_fields[$name] = new InputObjectField($field + ['name' => $name]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->name = $config['name'];
|
||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<InputObjectField>
|
||||
*/
|
||||
public function getFields()
|
||||
{
|
||||
return $this->_fields;
|
||||
}
|
||||
}
|
14
src/Type/Definition/InputType.php
Normal file
14
src/Type/Definition/InputType.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
interface InputType
|
||||
{
|
||||
/*
|
||||
export type GraphQLInputType =
|
||||
GraphQLScalarType |
|
||||
GraphQLEnumType |
|
||||
GraphQLInputObjectType |
|
||||
GraphQLList |
|
||||
GraphQLNonNull;
|
||||
*/
|
||||
}
|
31
src/Type/Definition/IntType.php
Normal file
31
src/Type/Definition/IntType.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Language\AST\IntValue;
|
||||
|
||||
class IntType extends ScalarType
|
||||
{
|
||||
public $name = Type::INT;
|
||||
|
||||
public function coerce($value)
|
||||
{
|
||||
if (false === $value || true === $value) {
|
||||
return (int) $value;
|
||||
}
|
||||
if (is_numeric($value) && $value <= PHP_INT_MAX && $value >= -1 * PHP_INT_MAX) {
|
||||
return (int) $value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function coerceLiteral($ast)
|
||||
{
|
||||
if ($ast instanceof IntValue) {
|
||||
$val = (int) $ast->value;
|
||||
if ($ast->value === (string) $val) {
|
||||
return $val;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
108
src/Type/Definition/InterfaceType.php
Normal file
108
src/Type/Definition/InterfaceType.php
Normal file
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
|
||||
use GraphQL\Utils;
|
||||
|
||||
class InterfaceType extends Type implements AbstractType, OutputType, CompositeType
|
||||
{
|
||||
/**
|
||||
* @var array<string,FieldDefinition>
|
||||
*/
|
||||
private $_fields;
|
||||
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* @var array<GraphQLObjectType>
|
||||
*/
|
||||
private $_implementations = [];
|
||||
|
||||
/**
|
||||
* @var {[typeName: string]: boolean}
|
||||
*/
|
||||
private $_possibleTypeNames;
|
||||
|
||||
/**
|
||||
* @var callback
|
||||
*/
|
||||
private $_resolveType;
|
||||
|
||||
/**
|
||||
* Update the interfaces to know about this implementation.
|
||||
* This is an rare and unfortunate use of mutation in the type definition
|
||||
* implementations, but avoids an expensive "getPossibleTypes"
|
||||
* implementation for Interface types.
|
||||
*
|
||||
* @param ObjectType $impl
|
||||
* @param array<InterfaceType> $interfaces
|
||||
*/
|
||||
public static function addImplementationToInterfaces(ObjectType $impl, array $interfaces)
|
||||
{
|
||||
foreach ($interfaces as $interface) {
|
||||
$interface->_implementations[] = $impl;
|
||||
}
|
||||
}
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
Config::validate($config, [
|
||||
'name' => Config::STRING,
|
||||
'fields' => Config::arrayOf(
|
||||
FieldDefinition::getDefinition(),
|
||||
Config::KEY_AS_NAME
|
||||
),
|
||||
'resolveType' => Config::CALLBACK,
|
||||
'description' => Config::STRING
|
||||
]);
|
||||
|
||||
$this->name = $config['name'];
|
||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||
$this->_fields = !empty($config['fields']) ? FieldDefinition::createMap($config['fields']) : [];
|
||||
$this->_resolveType = isset($config['resolveType']) ? $config['resolveType'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<FieldDefinition>
|
||||
*/
|
||||
public function getFields()
|
||||
{
|
||||
return $this->_fields;
|
||||
}
|
||||
|
||||
public function getField($name)
|
||||
{
|
||||
Utils::invariant(isset($this->_fields[$name]), 'Field "%s" is not defined for type "%s"', $name, $this->name);
|
||||
return $this->_fields[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<GraphQLObjectType>
|
||||
*/
|
||||
public function getPossibleTypes()
|
||||
{
|
||||
return $this->_implementations;
|
||||
}
|
||||
|
||||
public function isPossibleType(ObjectType $type)
|
||||
{
|
||||
$possibleTypeNames = $this->_possibleTypeNames;
|
||||
if (!$possibleTypeNames) {
|
||||
$this->_possibleTypeNames = $possibleTypeNames = array_reduce($this->getPossibleTypes(), function(&$map, Type $possibleType) {
|
||||
$map[$possibleType->name] = true;
|
||||
return $map;
|
||||
}, []);
|
||||
}
|
||||
return !empty($possibleTypeNames[$type->name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @return ObjectType|null
|
||||
*/
|
||||
public function resolveType($value)
|
||||
{
|
||||
$resolver = $this->_resolveType;
|
||||
return $resolver ? call_user_func($resolver, $value) : Type::getTypeOf($value, $this);
|
||||
}
|
||||
}
|
12
src/Type/Definition/LeafType.php
Normal file
12
src/Type/Definition/LeafType.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
|
||||
interface LeafType
|
||||
{
|
||||
/*
|
||||
export type GraphQLLeafType =
|
||||
GraphQLScalarType |
|
||||
GraphQLEnumType;
|
||||
*/
|
||||
}
|
43
src/Type/Definition/ListOfType.php
Normal file
43
src/Type/Definition/ListOfType.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
|
||||
use GraphQL\Utils;
|
||||
|
||||
class ListOfType extends Type implements WrappingType, OutputType, InputType
|
||||
{
|
||||
/**
|
||||
* @var callable|Type
|
||||
*/
|
||||
private $ofType;
|
||||
|
||||
/**
|
||||
* @param callable|Type $type
|
||||
*/
|
||||
public function __construct($type)
|
||||
{
|
||||
Utils::invariant(
|
||||
$type instanceof Type || is_callable($type),
|
||||
'Expecting instance of GraphQL\Type\Definition\Type or callable returning instance of that class'
|
||||
);
|
||||
|
||||
$this->ofType = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function toString()
|
||||
{
|
||||
$str = $this->ofType instanceof Type ? $this->ofType->toString() : (string) $this->ofType;
|
||||
return '[' . $str . ']';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Type
|
||||
*/
|
||||
public function getWrappedType()
|
||||
{
|
||||
return Type::resolve($this->ofType);
|
||||
}
|
||||
}
|
53
src/Type/Definition/NonNull.php
Normal file
53
src/Type/Definition/NonNull.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Type\TypeKind;
|
||||
use GraphQL\Utils;
|
||||
|
||||
class NonNull extends Type implements WrappingType, OutputType, InputType
|
||||
{
|
||||
/**
|
||||
* @var callable|Type
|
||||
*/
|
||||
protected $ofType;
|
||||
|
||||
/**
|
||||
* @param callable|Type $type
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct($type)
|
||||
{
|
||||
Utils::invariant(
|
||||
$type instanceof Type || is_callable($type),
|
||||
'Expecting instance of GraphQL\Type\Definition\Type or callable returning instance of that class'
|
||||
);
|
||||
Utils::invariant(
|
||||
!($type instanceof NonNull),
|
||||
'Cannot nest NonNull inside NonNull'
|
||||
);
|
||||
$this->ofType = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Type|callable
|
||||
*/
|
||||
public function getWrappedType()
|
||||
{
|
||||
$type = Type::resolve($this->ofType);
|
||||
|
||||
Utils::invariant(
|
||||
!($type instanceof NonNull),
|
||||
'Cannot nest NonNull inside NonNull'
|
||||
);
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function toString()
|
||||
{
|
||||
return $this->getWrappedType()->toString() . '!';
|
||||
}
|
||||
}
|
125
src/Type/Definition/ObjectType.php
Normal file
125
src/Type/Definition/ObjectType.php
Normal file
@ -0,0 +1,125 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
use GraphQL\Utils;
|
||||
|
||||
|
||||
/**
|
||||
* Object Type Definition
|
||||
*
|
||||
* Almost all of the GraphQL types you define will be object types. Object types
|
||||
* have a name, but most importantly describe their fields.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* var AddressType = new GraphQLObjectType({
|
||||
* name: 'Address',
|
||||
* fields: {
|
||||
* street: { type: GraphQLString },
|
||||
* number: { type: GraphQLInt },
|
||||
* formatted: {
|
||||
* type: GraphQLString,
|
||||
* resolve(obj) {
|
||||
* return obj.number + ' ' + obj.street
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* When two types need to refer to each other, or a type needs to refer to
|
||||
* itself in a field, you can use a function expression (aka a closure or a
|
||||
* thunk) to supply the fields lazily.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* var PersonType = new GraphQLObjectType({
|
||||
* name: 'Person',
|
||||
* fields: () => ({
|
||||
* name: { type: GraphQLString },
|
||||
* bestFriend: { type: PersonType },
|
||||
* })
|
||||
* });
|
||||
*
|
||||
*/
|
||||
class ObjectType extends Type implements OutputType, CompositeType
|
||||
{
|
||||
/**
|
||||
* @var array<Field>
|
||||
*/
|
||||
private $_fields = [];
|
||||
|
||||
/**
|
||||
* @var array<InterfaceType>
|
||||
*/
|
||||
private $_interfaces;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $_isTypeOf;
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
Config::validate($config, [
|
||||
'name' => Config::STRING | Config::REQUIRED,
|
||||
'fields' => Config::arrayOf(
|
||||
FieldDefinition::getDefinition(),
|
||||
Config::KEY_AS_NAME
|
||||
),
|
||||
'description' => Config::STRING,
|
||||
'interfaces' => Config::arrayOf(
|
||||
Config::INTERFACE_TYPE
|
||||
),
|
||||
'isTypeOf' => Config::CALLBACK,
|
||||
]);
|
||||
|
||||
$this->name = $config['name'];
|
||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||
|
||||
if (isset($config['fields'])) {
|
||||
$this->_fields = FieldDefinition::createMap($config['fields']);
|
||||
}
|
||||
|
||||
$this->_interfaces = isset($config['interfaces']) ? $config['interfaces'] : [];
|
||||
$this->_isTypeOf = isset($config['isTypeOf']) ? $config['isTypeOf'] : null;
|
||||
|
||||
if (!empty($this->_interfaces)) {
|
||||
InterfaceType::addImplementationToInterfaces($this, $this->_interfaces);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<FieldDefinition>
|
||||
*/
|
||||
public function getFields()
|
||||
{
|
||||
return $this->_fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return FieldDefinition
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getField($name)
|
||||
{
|
||||
Utils::invariant(isset($this->_fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name);
|
||||
return $this->_fields[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<InterfaceType>
|
||||
*/
|
||||
public function getInterfaces()
|
||||
{
|
||||
return $this->_interfaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @return bool|null
|
||||
*/
|
||||
public function isTypeOf($value)
|
||||
{
|
||||
return isset($this->_isTypeOf) ? call_user_func($this->_isTypeOf, $value) : null;
|
||||
}
|
||||
}
|
16
src/Type/Definition/OutputType.php
Normal file
16
src/Type/Definition/OutputType.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
|
||||
interface OutputType
|
||||
{
|
||||
/*
|
||||
GraphQLScalarType |
|
||||
GraphQLObjectType |
|
||||
GraphQLInterfaceType |
|
||||
GraphQLUnionType |
|
||||
GraphQLEnumType |
|
||||
GraphQLList |
|
||||
GraphQLNonNull;
|
||||
*/
|
||||
}
|
33
src/Type/Definition/ScalarType.php
Normal file
33
src/Type/Definition/ScalarType.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Utils;
|
||||
|
||||
/**
|
||||
* Scalar Type Definition
|
||||
*
|
||||
* The leaf values of any request and input values to arguments are
|
||||
* Scalars (or Enums) and are defined with a name and a series of coercion
|
||||
* functions used to ensure validity.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* var OddType = new GraphQLScalarType({
|
||||
* name: 'Odd',
|
||||
* coerce(value) {
|
||||
* return value % 2 === 1 ? value : null;
|
||||
* }
|
||||
* });
|
||||
*
|
||||
*/
|
||||
abstract class ScalarType extends Type implements OutputType, InputType
|
||||
{
|
||||
protected function __construct()
|
||||
{
|
||||
Utils::invariant($this->name, 'Type must be named.');
|
||||
}
|
||||
|
||||
abstract public function coerce($value);
|
||||
|
||||
abstract public function coerceLiteral($ast);
|
||||
}
|
26
src/Type/Definition/ScalarTypeConfig.php
Normal file
26
src/Type/Definition/ScalarTypeConfig.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
|
||||
class ScalarTypeConfig
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* @var \Closure
|
||||
*/
|
||||
public $coerce;
|
||||
|
||||
/**
|
||||
* @var \Closure
|
||||
*/
|
||||
public $coerceLiteral;
|
||||
}
|
28
src/Type/Definition/StringType.php
Normal file
28
src/Type/Definition/StringType.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Language\AST\StringValue;
|
||||
|
||||
class StringType extends ScalarType
|
||||
{
|
||||
public $name = Type::STRING;
|
||||
|
||||
public function coerce($value)
|
||||
{
|
||||
if ($value === true) {
|
||||
return 'true';
|
||||
}
|
||||
if ($value === false) {
|
||||
return 'false';
|
||||
}
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
public function coerceLiteral($ast)
|
||||
{
|
||||
if ($ast instanceof StringValue) {
|
||||
return $ast->value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
247
src/Type/Definition/Type.php
Normal file
247
src/Type/Definition/Type.php
Normal file
@ -0,0 +1,247 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Utils;
|
||||
|
||||
abstract class Type
|
||||
{
|
||||
/*
|
||||
export type GraphQLType =
|
||||
GraphQLScalarType |
|
||||
GraphQLObjectType |
|
||||
GraphQLInterfaceType |
|
||||
GraphQLUnionType |
|
||||
GraphQLEnumType |
|
||||
GraphQLInputObjectType |
|
||||
GraphQLList |
|
||||
GraphQLNonNull;
|
||||
*/
|
||||
const STRING = 'String';
|
||||
const INT = 'Int';
|
||||
const BOOLEAN = 'Boolean';
|
||||
const FLOAT = 'Float';
|
||||
const ID = 'ID';
|
||||
|
||||
private static $internalTypes;
|
||||
|
||||
public static function id()
|
||||
{
|
||||
return self::getInternalType(self::ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return StringType
|
||||
*/
|
||||
public static function string()
|
||||
{
|
||||
return self::getInternalType(self::STRING);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BooleanType
|
||||
*/
|
||||
public static function boolean()
|
||||
{
|
||||
return self::getInternalType(self::BOOLEAN);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IntType
|
||||
*/
|
||||
public static function int()
|
||||
{
|
||||
return self::getInternalType(self::INT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FloatType
|
||||
*/
|
||||
public static function float()
|
||||
{
|
||||
return self::getInternalType(self::FLOAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $wrappedType
|
||||
* @return ListOfType
|
||||
*/
|
||||
public static function listOf($wrappedType)
|
||||
{
|
||||
return new ListOfType($wrappedType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $wrappedType
|
||||
* @return NonNull
|
||||
*/
|
||||
public static function nonNull($wrappedType)
|
||||
{
|
||||
return new NonNull($wrappedType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* @return Type
|
||||
*/
|
||||
private static function getInternalType($name = null)
|
||||
{
|
||||
if (null === self::$internalTypes) {
|
||||
self::$internalTypes = [
|
||||
self::ID => new IDType(),
|
||||
self::STRING => new StringType(),
|
||||
self::FLOAT => new FloatType(),
|
||||
self::INT => new IntType(),
|
||||
self::BOOLEAN => new BooleanType()
|
||||
];
|
||||
}
|
||||
return $name ? self::$internalTypes[$name] : self::$internalTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Type
|
||||
*/
|
||||
public static function getInternalTypes()
|
||||
{
|
||||
return self::getInternalType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $type
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInputType($type)
|
||||
{
|
||||
$nakedType = self::getUnmodifiedType($type);
|
||||
return $nakedType instanceof InputType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $type
|
||||
* @return bool
|
||||
*/
|
||||
public static function isOutputType($type)
|
||||
{
|
||||
$nakedType = self::getUnmodifiedType($type);
|
||||
return $nakedType instanceof OutputType;
|
||||
}
|
||||
|
||||
public static function isLeafType($type)
|
||||
{
|
||||
$nakedType = self::getUnmodifiedType($type);
|
||||
return (
|
||||
$nakedType instanceof ScalarType ||
|
||||
$nakedType instanceof EnumType
|
||||
);
|
||||
}
|
||||
|
||||
public static function isCompositeType($type)
|
||||
{
|
||||
return (
|
||||
$type instanceof ObjectType ||
|
||||
$type instanceof InterfaceType ||
|
||||
$type instanceof UnionType
|
||||
);
|
||||
}
|
||||
|
||||
public static function isAbstractType($type)
|
||||
{
|
||||
return (
|
||||
$type instanceof InterfaceType ||
|
||||
$type instanceof UnionType
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $type
|
||||
* @return Type
|
||||
*/
|
||||
public static function getNullableType($type)
|
||||
{
|
||||
return $type instanceof NonNull ? $type->getWrappedType() : $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $type
|
||||
* @return UnmodifiedType
|
||||
*/
|
||||
public static function getUnmodifiedType($type)
|
||||
{
|
||||
if (null === $type) {
|
||||
return null;
|
||||
}
|
||||
while ($type instanceof WrappingType) {
|
||||
$type = $type->getWrappedType();
|
||||
}
|
||||
return self::resolve($type);
|
||||
}
|
||||
|
||||
public static function resolve($type)
|
||||
{
|
||||
if (is_callable($type)) {
|
||||
$type = $type();
|
||||
}
|
||||
|
||||
Utils::invariant(
|
||||
$type instanceof Type,
|
||||
'Expecting instance of ' . __CLASS__ . ' (or callable returning instance of that type), got "%s"',
|
||||
Utils::getVariableType($type)
|
||||
);
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @param AbstractType $abstractType
|
||||
* @return Type
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function getTypeOf($value, AbstractType $abstractType)
|
||||
{
|
||||
$possibleTypes = $abstractType->getPossibleTypes();
|
||||
|
||||
for ($i = 0; $i < count($possibleTypes); $i++) {
|
||||
/** @var ObjectType $type */
|
||||
$type = $possibleTypes[$i];
|
||||
$isTypeOf = $type->isTypeOf($value);
|
||||
|
||||
if ($isTypeOf === null) {
|
||||
// TODO: move this to a JS impl specific type system validation step
|
||||
// so the error can be found before execution.
|
||||
throw new \Exception(
|
||||
'Non-Object Type ' . $abstractType->name . ' does not implement ' .
|
||||
'resolveType and Object Type ' . $type->name . ' does not implement ' .
|
||||
'isTypeOf. There is no way to determine if a value is of this type.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($isTypeOf) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $description;
|
||||
|
||||
public function toString()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
try {
|
||||
return $this->toString();
|
||||
} catch (\Exception $e) {
|
||||
echo $e;
|
||||
}
|
||||
}
|
||||
}
|
81
src/Type/Definition/UnionType.php
Normal file
81
src/Type/Definition/UnionType.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Utils;
|
||||
|
||||
class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
||||
{
|
||||
/**
|
||||
* @var Array<GraphQLObjectType>
|
||||
*/
|
||||
private $_types;
|
||||
|
||||
/**
|
||||
* @var array<string, ObjectType>
|
||||
*/
|
||||
private $_possibleTypeNames;
|
||||
|
||||
/**
|
||||
* @var callback
|
||||
*/
|
||||
private $_resolveType;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
Config::validate($config, [
|
||||
'name' => Config::STRING | Config::REQUIRED,
|
||||
'types' => Config::arrayOf(Config::OBJECT_TYPE | Config::REQUIRED),
|
||||
'resolveType' => Config::CALLBACK,
|
||||
'description' => Config::STRING
|
||||
]);
|
||||
|
||||
Utils::invariant(!empty($config['types']), "");
|
||||
|
||||
/**
|
||||
* Optionally provide a custom type resolver function. If one is not provided,
|
||||
* the default implemenation will call `isTypeOf` on each implementing
|
||||
* Object type.
|
||||
*/
|
||||
$this->name = $config['name'];
|
||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||
$this->_types = $config['types'];
|
||||
$this->_resolveType = isset($config['resolveType']) ? $config['resolveType'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<ObjectType>
|
||||
*/
|
||||
public function getPossibleTypes()
|
||||
{
|
||||
return $this->_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Type $type
|
||||
* @return mixed
|
||||
*/
|
||||
public function isPossibleType(Type $type)
|
||||
{
|
||||
if (!$type instanceof ObjectType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null === $this->_possibleTypeNames) {
|
||||
$this->_possibleTypeNames = [];
|
||||
foreach ($this->getPossibleTypes() as $possibleType) {
|
||||
$this->_possibleTypeNames[$possibleType->name] = true;
|
||||
}
|
||||
}
|
||||
return $this->_possibleTypeNames[$type->name] === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType $value
|
||||
* @return Type
|
||||
*/
|
||||
public function resolveType($value)
|
||||
{
|
||||
$resolver = $this->_resolveType;
|
||||
return $resolver ? call_user_func($resolver, $value) : Type::getTypeOf($value, $this);
|
||||
}
|
||||
}
|
16
src/Type/Definition/UnmodifiedType.php
Normal file
16
src/Type/Definition/UnmodifiedType.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
|
||||
interface UnmodifiedType
|
||||
{
|
||||
/*
|
||||
export type GraphQLUnmodifiedType =
|
||||
GraphQLScalarType |
|
||||
GraphQLObjectType |
|
||||
GraphQLInterfaceType |
|
||||
GraphQLUnionType |
|
||||
GraphQLEnumType |
|
||||
GraphQLInputObjectType;
|
||||
*/
|
||||
}
|
12
src/Type/Definition/WrappingType.php
Normal file
12
src/Type/Definition/WrappingType.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
|
||||
interface WrappingType
|
||||
{
|
||||
/*
|
||||
NonNullType
|
||||
ListOfType
|
||||
*/
|
||||
public function getWrappedType();
|
||||
}
|
406
src/Type/Introspection.php
Normal file
406
src/Type/Introspection.php
Normal file
@ -0,0 +1,406 @@
|
||||
<?php
|
||||
namespace GraphQL\Type;
|
||||
|
||||
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
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\Definition\WrappingType;
|
||||
|
||||
class TypeKind {
|
||||
const SCALAR = 0;
|
||||
const OBJECT = 1;
|
||||
const INTERFACE_KIND = 2;
|
||||
const UNION = 3;
|
||||
const ENUM = 4;
|
||||
const INPUT_OBJECT = 5;
|
||||
const LIST_KIND = 6;
|
||||
const NON_NULL = 7;
|
||||
}
|
||||
|
||||
class Introspection
|
||||
{
|
||||
private static $_map = [];
|
||||
|
||||
public static function _schema()
|
||||
{
|
||||
if (!isset(self::$_map['__Schema'])) {
|
||||
self::$_map['__Schema'] = new ObjectType([
|
||||
'name' => '__Schema',
|
||||
'description' =>
|
||||
'A GraphQL Schema defines the capabilities of a GraphQL ' .
|
||||
'server. It exposes all available types and directives on ' .
|
||||
'the server, as well as the entry points for query and ' .
|
||||
'mutation operations.',
|
||||
'fields' => [
|
||||
'types' => [
|
||||
'description' => 'A list of all types supported by this server.',
|
||||
'type' => new NonNull(new ListOfType(new NonNull(self::_type()))),
|
||||
'resolve' => function (Schema $schema) {
|
||||
return array_values($schema->getTypeMap());
|
||||
}
|
||||
],
|
||||
'queryType' => [
|
||||
'description' => 'The type that query operations will be rooted at.',
|
||||
'type' => new NonNull(self::_type()),
|
||||
'resolve' => function (Schema $schema) {
|
||||
return $schema->getQueryType();
|
||||
}
|
||||
],
|
||||
'mutationType' => [
|
||||
'description' =>
|
||||
'If this server supports mutation, the type that ' .
|
||||
'mutation operations will be rooted at.',
|
||||
'type' => self::_type(),
|
||||
'resolve' => function (Schema $schema) {
|
||||
return $schema->getMutationType();
|
||||
}
|
||||
],
|
||||
'directives' => [
|
||||
'description' => 'A list of all directives supported by this server.',
|
||||
'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_directive()))),
|
||||
'resolve' => function(Schema $schema) {
|
||||
return $schema->getDirectives();
|
||||
}
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
return self::$_map['__Schema'];
|
||||
}
|
||||
|
||||
public static function _directive()
|
||||
{
|
||||
if (!isset(self::$_map['__Directive'])) {
|
||||
self::$_map['__Directive'] = new ObjectType([
|
||||
'name' => '__Directive',
|
||||
'fields' => [
|
||||
'name' => ['type' => Type::string()],
|
||||
'description' => ['type' => Type::string()],
|
||||
'type' => ['type' => [__CLASS__, '_type']],
|
||||
'onOperation' => ['type' => Type::boolean()],
|
||||
'onFragment' => ['type' => Type::boolean()],
|
||||
'onField' => ['type' => Type::boolean()]
|
||||
]
|
||||
]);
|
||||
}
|
||||
return self::$_map['__Directive'];
|
||||
}
|
||||
|
||||
public static function _type()
|
||||
{
|
||||
if (!isset(self::$_map['__Type'])) {
|
||||
self::$_map['__Type'] = new ObjectType([
|
||||
'name' => '__Type',
|
||||
'fields' => [
|
||||
'kind' => [
|
||||
'type' => Type::nonNull(self::_typeKind()),
|
||||
'resolve' => function (Type $type) {
|
||||
switch (true) {
|
||||
case $type instanceof ListOfType:
|
||||
return TypeKind::LIST_KIND;
|
||||
case $type instanceof NonNull:
|
||||
return TypeKind::NON_NULL;
|
||||
case $type instanceof ScalarType:
|
||||
return TypeKind::SCALAR;
|
||||
case $type instanceof ObjectType:
|
||||
return TypeKind::OBJECT;
|
||||
case $type instanceof EnumType:
|
||||
return TypeKind::ENUM;
|
||||
case $type instanceof InputObjectType:
|
||||
return TypeKind::INPUT_OBJECT;
|
||||
case $type instanceof InterfaceType:
|
||||
return TypeKind::INTERFACE_KIND;
|
||||
case $type instanceof UnionType:
|
||||
return TypeKind::UNION;
|
||||
default:
|
||||
throw new \Exception("Unknown kind of type: " . print_r($type, true));
|
||||
}
|
||||
}
|
||||
],
|
||||
'name' => ['type' => Type::string()],
|
||||
'description' => ['type' => Type::string()],
|
||||
'fields' => [
|
||||
'type' => Type::listOf(Type::nonNull(self::_field())),
|
||||
'args' => [
|
||||
'includeDeprecated' => ['type' => Type::boolean(), 'defaultValue' => false]
|
||||
],
|
||||
'resolve' => function (Type $type, $args) {
|
||||
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
|
||||
$fields = $type->getFields();
|
||||
|
||||
if (empty($args['includeDeprecated'])) {
|
||||
$fields = array_filter($fields, function (FieldDefinition $field) {
|
||||
return !$field->deprecationReason;
|
||||
});
|
||||
}
|
||||
return array_values($fields);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
],
|
||||
'interfaces' => [
|
||||
'type' => Type::listOf(Type::nonNull([__CLASS__, '_type'])),
|
||||
'resolve' => function ($type) {
|
||||
if ($type instanceof ObjectType) {
|
||||
return $type->getInterfaces();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
],
|
||||
'possibleTypes' => [
|
||||
'type' => Type::listOf(Type::nonNull([__CLASS__, '_type'])),
|
||||
'resolve' => function ($type) {
|
||||
if ($type instanceof InterfaceType || $type instanceof UnionType) {
|
||||
return $type->getPossibleTypes();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
],
|
||||
'enumValues' => [
|
||||
'type' => Type::listOf(Type::nonNull(self::_enumValue())),
|
||||
'args' => [
|
||||
'includeDeprecated' => ['type' => Type::boolean(), 'defaultValue' => false]
|
||||
],
|
||||
'resolve' => function ($type, $args) {
|
||||
if ($type instanceof EnumType) {
|
||||
$values = array_values($type->getValues());
|
||||
|
||||
if (empty($args['includeDeprecated'])) {
|
||||
$values = array_filter($values, function ($value) {
|
||||
return !$value->deprecationReason;
|
||||
});
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
],
|
||||
'inputFields' => [
|
||||
'type' => Type::listOf(Type::nonNull(self::_inputValue())),
|
||||
'resolve' => function ($type) {
|
||||
if ($type instanceof InputObjectType) {
|
||||
return array_values($type->getFields());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
],
|
||||
'ofType' => [
|
||||
'type' => [__CLASS__, '_type'],
|
||||
'resolve' => function($type) {
|
||||
if ($type instanceof WrappingType) {
|
||||
return $type->getWrappedType();
|
||||
}
|
||||
return null;
|
||||
}]
|
||||
]
|
||||
]);
|
||||
}
|
||||
return self::$_map['__Type'];
|
||||
}
|
||||
|
||||
public static function _field()
|
||||
{
|
||||
if (!isset(self::$_map['__Field'])) {
|
||||
|
||||
self::$_map['__Field'] = new ObjectType([
|
||||
'name' => '__Field',
|
||||
'fields' => [
|
||||
'name' => ['type' => Type::nonNull(Type::string())],
|
||||
'description' => ['type' => Type::string()],
|
||||
'args' => [
|
||||
'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))),
|
||||
'resolve' => function (FieldDefinition $field) {
|
||||
return empty($field->args) ? [] : $field->args;
|
||||
}
|
||||
],
|
||||
'type' => [
|
||||
'type' => Type::nonNull([__CLASS__, '_type']),
|
||||
'resolve' => function ($field) {
|
||||
return $field->getType();
|
||||
}
|
||||
],
|
||||
'isDeprecated' => [
|
||||
'type' => Type::nonNull(Type::boolean()),
|
||||
'resolve' => function (FieldDefinition $field) {
|
||||
return !!$field->deprecationReason;
|
||||
}
|
||||
],
|
||||
'deprecationReason' => [
|
||||
'type' => Type::string()
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
return self::$_map['__Field'];
|
||||
}
|
||||
|
||||
public static function _inputValue()
|
||||
{
|
||||
if (!isset(self::$_map['__InputValue'])) {
|
||||
self::$_map['__InputValue'] = new ObjectType([
|
||||
'name' => '__InputValue',
|
||||
'fields' => [
|
||||
'name' => ['type' => Type::nonNull(Type::string())],
|
||||
'description' => ['type' => Type::string()],
|
||||
'type' => [
|
||||
'type' => Type::nonNull([__CLASS__, '_type']),
|
||||
'resolve' => function($value) {
|
||||
return method_exists($value, 'getType') ? $value->getType() : $value->type;
|
||||
}
|
||||
],
|
||||
'defaultValue' => [
|
||||
'type' => Type::string(),
|
||||
'resolve' => function ($inputValue) {
|
||||
return $inputValue->defaultValue === null ? null : json_encode($inputValue->defaultValue);
|
||||
}
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
return self::$_map['__InputValue'];
|
||||
}
|
||||
|
||||
public static function _enumValue()
|
||||
{
|
||||
if (!isset(self::$_map['__EnumValue'])) {
|
||||
self::$_map['__EnumValue'] = new ObjectType([
|
||||
'name' => '__EnumValue',
|
||||
'fields' => [
|
||||
'name' => ['type' => Type::nonNull(Type::string())],
|
||||
'description' => ['type' => Type::string()],
|
||||
'isDeprecated' => [
|
||||
'type' => Type::nonNull(Type::boolean()),
|
||||
'resolve' => function ($enumValue) {
|
||||
return !!$enumValue->deprecationReason;
|
||||
}
|
||||
],
|
||||
'deprecationReason' => [
|
||||
'type' => Type::string()
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
return self::$_map['__EnumValue'];
|
||||
}
|
||||
|
||||
public static function _typeKind()
|
||||
{
|
||||
if (!isset(self::$_map['__TypeKind'])) {
|
||||
self::$_map['__TypeKind'] = new EnumType([
|
||||
'name' => '__TypeKind',
|
||||
'description' => 'An enum describing what kind of type a given __Type is',
|
||||
'values' => [
|
||||
'SCALAR' => [
|
||||
'value' => TypeKind::SCALAR,
|
||||
'description' => 'Indicates this type is a scalar.'
|
||||
],
|
||||
'OBJECT' => [
|
||||
'value' => TypeKind::OBJECT,
|
||||
'description' => 'Indicates this type is an object. `fields` and `interfaces` are valid fields.'
|
||||
],
|
||||
'INTERFACE' => [
|
||||
'value' => TypeKind::INTERFACE_KIND,
|
||||
'description' => 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.'
|
||||
],
|
||||
'UNION' => [
|
||||
'value' => TypeKind::UNION,
|
||||
'description' => 'Indicates this type is a union. `possibleTypes` is a valid field.'
|
||||
],
|
||||
'ENUM' => [
|
||||
'value' => TypeKind::ENUM,
|
||||
'description' => 'Indicates this type is an enum. `enumValues` is a valid field.'
|
||||
],
|
||||
'INPUT_OBJECT' => [
|
||||
'value' => TypeKind::INPUT_OBJECT,
|
||||
'description' => 'Indicates this type is an input object. `inputFields` is a valid field.'
|
||||
],
|
||||
'LIST' => [
|
||||
'value' => TypeKind::LIST_KIND,
|
||||
'description' => 'Indicates this type is a list. `ofType` is a valid field.'
|
||||
],
|
||||
'NON_NULL' => [
|
||||
'value' => TypeKind::NON_NULL,
|
||||
'description' => 'Indicates this type is a non-null. `ofType` is a valid field.'
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
return self::$_map['__TypeKind'];
|
||||
}
|
||||
|
||||
public static function schemaMetaFieldDef()
|
||||
{
|
||||
if (!isset(self::$_map['__schema'])) {
|
||||
self::$_map['__schema'] = FieldDefinition::create([
|
||||
'name' => '__schema',
|
||||
'type' => Type::nonNull(self::_schema()),
|
||||
'description' => 'Access the current type schema of this server.',
|
||||
'args' => [],
|
||||
'resolve' => function (
|
||||
$source,
|
||||
$args,
|
||||
$root,
|
||||
$fieldAST,
|
||||
$fieldType,
|
||||
$parentType,
|
||||
$schema
|
||||
) {
|
||||
// TODO: move 3+ args to separate object
|
||||
return $schema;
|
||||
}
|
||||
]);
|
||||
}
|
||||
return self::$_map['__schema'];
|
||||
}
|
||||
|
||||
public static function typeMetaFieldDef()
|
||||
{
|
||||
if (!isset(self::$_map['__type'])) {
|
||||
self::$_map['__type'] = FieldDefinition::create([
|
||||
'name' => '__type',
|
||||
'type' => self::_type(),
|
||||
'description' => 'Request the type information of a single type.',
|
||||
'args' => [
|
||||
['name' => 'name', 'type' => Type::nonNull(Type::string())]
|
||||
],
|
||||
'resolve' => function ($source, $args, $root, $fieldAST, $fieldType, $parentType, $schema) {
|
||||
return $schema->getType($args['name']);
|
||||
}
|
||||
]);
|
||||
}
|
||||
return self::$_map['__type'];
|
||||
}
|
||||
|
||||
public static function typeNameMetaFieldDef()
|
||||
{
|
||||
if (!isset(self::$_map['__typename'])) {
|
||||
self::$_map['__typename'] = FieldDefinition::create([
|
||||
'name' => '__typename',
|
||||
'type' => Type::nonNull(Type::string()),
|
||||
'description' => 'The name of the current Object type at runtime.',
|
||||
'args' => [],
|
||||
'resolve' => function (
|
||||
$source,
|
||||
$args,
|
||||
$root,
|
||||
$fieldAST,
|
||||
$fieldType,
|
||||
$parentType
|
||||
) {
|
||||
return $parentType->name;
|
||||
}
|
||||
]);
|
||||
}
|
||||
return self::$_map['__typename'];
|
||||
}
|
||||
}
|
179
src/Type/SchemaValidator.php
Normal file
179
src/Type/SchemaValidator.php
Normal file
@ -0,0 +1,179 @@
|
||||
<?php
|
||||
namespace GraphQL\Type;
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
|
||||
class SchemaValidator
|
||||
{
|
||||
private static $rules;
|
||||
|
||||
public static function getAllRules()
|
||||
{
|
||||
if (null === self::$rules) {
|
||||
self::$rules = [
|
||||
self::noInputTypesAsOutputFieldsRule(),
|
||||
self::noOutputTypesAsInputArgsRule(),
|
||||
self::typesInterfacesMustShowThemAsPossibleRule(),
|
||||
self::interfacePossibleTypesMustImplementTheInterfaceRule()
|
||||
];
|
||||
}
|
||||
return self::$rules;
|
||||
}
|
||||
|
||||
public static function noInputTypesAsOutputFieldsRule()
|
||||
{
|
||||
return function ($context) {
|
||||
$operationMayNotBeInputType = function (Type $type, $operation) {
|
||||
if (!Type::isOutputType($type)) {
|
||||
return new Error("Schema $operation type $type must be an object type!");
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/** @var Schema $schema */
|
||||
$schema = $context['schema'];
|
||||
$typeMap = $schema->getTypeMap();
|
||||
$errors = [];
|
||||
|
||||
$queryType = $schema->getQueryType();
|
||||
if ($queryType) {
|
||||
$queryError = $operationMayNotBeInputType($queryType, 'query');
|
||||
if ($queryError !== null) {
|
||||
$errors[] = $queryError;
|
||||
}
|
||||
}
|
||||
|
||||
$mutationType = $schema->getMutationType();
|
||||
if ($mutationType) {
|
||||
$mutationError = $operationMayNotBeInputType($mutationType, 'mutation');
|
||||
if ($mutationError !== null) {
|
||||
$errors[] = $mutationError;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($typeMap as $typeName => $type) {
|
||||
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
|
||||
$fields = $type->getFields();
|
||||
foreach ($fields as $fieldName => $field) {
|
||||
if ($field->getType() instanceof InputObjectType) {
|
||||
$errors[] = new Error(
|
||||
"Field $typeName.{$field->name} is of type " .
|
||||
"{$field->getType()->name}, which is an input type, but field types " .
|
||||
"must be output types!"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !empty($errors) ? $errors : null;
|
||||
};
|
||||
}
|
||||
|
||||
public static function noOutputTypesAsInputArgsRule()
|
||||
{
|
||||
return function($context) {
|
||||
/** @var Schema $schema */
|
||||
$schema = $context['schema'];
|
||||
$typeMap = $schema->getTypeMap();
|
||||
$errors = [];
|
||||
|
||||
foreach ($typeMap as $typeName => $type) {
|
||||
if ($type instanceof InputObjectType) {
|
||||
$fields = $type->getFields();
|
||||
|
||||
foreach ($fields as $fieldName => $field) {
|
||||
if (!Type::isInputType($field->getType())) {
|
||||
$errors[] = new Error(
|
||||
"Input field {$type->name}.{$field->name} has type ".
|
||||
"{$field->getType()}, which is not an input type!"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return !empty($errors) ? $errors : null;
|
||||
};
|
||||
}
|
||||
|
||||
public static function interfacePossibleTypesMustImplementTheInterfaceRule()
|
||||
{
|
||||
return function($context) {
|
||||
/** @var Schema $schema */
|
||||
$schema = $context['schema'];
|
||||
$typeMap = $schema->getTypeMap();
|
||||
$errors = [];
|
||||
|
||||
foreach ($typeMap as $typeName => $type) {
|
||||
if ($type instanceof InterfaceType) {
|
||||
$possibleTypes = $type->getPossibleTypes();
|
||||
foreach ($possibleTypes as $possibleType) {
|
||||
if (!in_array($type, $possibleType->getInterfaces())) {
|
||||
$errors[] = new Error(
|
||||
"$possibleType is a possible type of interface $type but does " .
|
||||
"not implement it!"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !empty($errors) ? $errors : null;
|
||||
};
|
||||
}
|
||||
|
||||
public static function typesInterfacesMustShowThemAsPossibleRule()
|
||||
{
|
||||
return function($context) {
|
||||
/** @var Schema $schema */
|
||||
$schema = $context['schema'];
|
||||
$typeMap = $schema->getTypeMap();
|
||||
$errors = [];
|
||||
|
||||
foreach ($typeMap as $typeName => $type) {
|
||||
if ($type instanceof ObjectType) {
|
||||
$interfaces = $type->getInterfaces();
|
||||
foreach ($interfaces as $interfaceType) {
|
||||
if (!$interfaceType->isPossibleType($type)) {
|
||||
$errors[] = new Error(
|
||||
"$typeName implements interface {$interfaceType->name}, but " .
|
||||
"{$interfaceType->name} does not list it as possible!"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return !empty($errors) ? $errors : null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @param array <callable>|null $argRules
|
||||
* @return array
|
||||
*/
|
||||
public static function validate(Schema $schema, $argRules = null)
|
||||
{
|
||||
$context = ['schema' => $schema];
|
||||
$errors = [];
|
||||
$rules = $argRules ?: self::getAllRules();
|
||||
|
||||
for ($i = 0; $i < count($rules); ++$i) {
|
||||
$newErrors = call_user_func($rules[$i], $context);
|
||||
if ($newErrors) {
|
||||
$errors = array_merge($errors, $newErrors);
|
||||
}
|
||||
}
|
||||
$isValid = empty($errors);
|
||||
$result = [
|
||||
'isValid' => $isValid,
|
||||
'errors' => $isValid ? null : array_map(['GraphQL\Error', 'formatError'], $errors)
|
||||
];
|
||||
return (object) $result;
|
||||
}
|
||||
}
|
9
src/Types.php
Normal file
9
src/Types.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
namespace GraphQL;
|
||||
|
||||
class Types
|
||||
{
|
||||
public static function Int()
|
||||
{
|
||||
}
|
||||
}
|
121
src/Utils.php
Normal file
121
src/Utils.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
namespace GraphQL;
|
||||
|
||||
class Utils
|
||||
{
|
||||
/**
|
||||
* @param $obj
|
||||
* @param array $vars
|
||||
* @return mixed
|
||||
*/
|
||||
public static function assign($obj, array $vars, array $requiredKeys = array())
|
||||
{
|
||||
foreach ($requiredKeys as $key) {
|
||||
if (!isset($key, $vars)) {
|
||||
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);
|
||||
trigger_error("Trying to set non-existing property '$key' on class '$cls'");
|
||||
}
|
||||
$obj->{$key} = $value;
|
||||
}
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $list
|
||||
* @param $predicate
|
||||
* @return null
|
||||
*/
|
||||
public static function find(array $list, $predicate)
|
||||
{
|
||||
for ($i = 0; $i < count($list); $i++) {
|
||||
if ($predicate($list[$i])) {
|
||||
return $list[$i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $test
|
||||
* @param string $message
|
||||
* @param mixed $sprintfParam1
|
||||
* @param mixed $sprintfParam2 ...
|
||||
* @throws \Exception
|
||||
*/
|
||||
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 \Exception($message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $var
|
||||
* @return string
|
||||
*/
|
||||
public static function getVariableType($var)
|
||||
{
|
||||
return is_object($var) ? get_class($var) : gettype($var);
|
||||
}
|
||||
|
||||
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 $char
|
||||
* @param string $encoding
|
||||
* @return mixed
|
||||
*/
|
||||
public static function ord($char, $encoding = 'UTF-8')
|
||||
{
|
||||
if (!isset($char[1])) {
|
||||
return ord($char);
|
||||
}
|
||||
if ($encoding === 'UCS-4BE') {
|
||||
list(, $ord) = (strlen($char) === 4) ? @unpack('N', $char) : @unpack('n', $char);
|
||||
return $ord;
|
||||
} else {
|
||||
return self::ord(mb_convert_encoding($char, 'UCS-4BE', $encoding), 'UCS-4BE');
|
||||
}
|
||||
}
|
||||
|
||||
public static function charCodeAt($string, $position)
|
||||
{
|
||||
$char = mb_substr($string, $position, 1, 'UTF-8');
|
||||
return self::ord($char);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public static function keyMap(array $list, callable $keyFn)
|
||||
{
|
||||
$map = [];
|
||||
foreach ($list as $value) {
|
||||
$map[$keyFn($value)] = $value;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
}
|
61
src/Utils/PairSet.php
Normal file
61
src/Utils/PairSet.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
|
||||
class PairSet
|
||||
{
|
||||
/**
|
||||
* @var \SplObjectStorage<any, Set<any>>
|
||||
*/
|
||||
private $_data;
|
||||
|
||||
private $_wrappers = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->_data = new \SplObjectStorage(); // SplObject hash instead?
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public function add($a, $b)
|
||||
{
|
||||
$this->_pairSetAdd($a, $b);
|
||||
$this->_pairSetAdd($b, $a);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
269
src/Utils/TypeInfo.php
Normal file
269
src/Utils/TypeInfo.php
Normal file
@ -0,0 +1,269 @@
|
||||
<?php
|
||||
namespace GraphQL\Utils;
|
||||
|
||||
use GraphQL\Language\AST\Field;
|
||||
use GraphQL\Language\AST\ListType;
|
||||
use GraphQL\Language\AST\Name;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NonNullType;
|
||||
use GraphQL\Schema;
|
||||
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\Introspection;
|
||||
use GraphQL\Utils;
|
||||
|
||||
class TypeInfo
|
||||
{
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @param $inputTypeAst
|
||||
* @return ListOfType|NonNull|Name
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function typeFromAST(Schema $schema, $inputTypeAst)
|
||||
{
|
||||
if ($inputTypeAst instanceof ListType) {
|
||||
$innerType = self::typeFromAST($schema, $inputTypeAst->type);
|
||||
return $innerType ? new ListOfType($innerType) : null;
|
||||
}
|
||||
if ($inputTypeAst instanceof NonNullType) {
|
||||
$innerType = self::typeFromAST($schema, $inputTypeAst->type);
|
||||
return $innerType ? new NonNull($innerType) : null;
|
||||
}
|
||||
|
||||
Utils::invariant($inputTypeAst instanceof Name, 'Must be a type name');
|
||||
return $schema->getType($inputTypeAst->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 _getFieldDef(Schema $schema, Type $parentType, Field $fieldAST)
|
||||
{
|
||||
$name = $fieldAST->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 ObjectType ||
|
||||
$parentType instanceof InterfaceType ||
|
||||
$parentType instanceof UnionType)
|
||||
) {
|
||||
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;
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
function enter(Node $node)
|
||||
{
|
||||
$schema = $this->_schema;
|
||||
|
||||
switch ($node->kind) {
|
||||
case Node::SELECTION_SET:
|
||||
// var $compositeType: ?GraphQLCompositeType;
|
||||
$rawType = Type::getUnmodifiedType($this->getType());
|
||||
$compositeType = null;
|
||||
if (Type::isCompositeType($rawType)) {
|
||||
// isCompositeType is a type refining predicate, so this is safe.
|
||||
$compositeType = $rawType;
|
||||
}
|
||||
array_push($this->_parentTypeStack, $compositeType);
|
||||
break;
|
||||
|
||||
case Node::FIELD:
|
||||
$parentType = $this->getParentType();
|
||||
$fieldDef = null;
|
||||
if ($parentType) {
|
||||
$fieldDef = self::_getFieldDef($schema, $parentType, $node);
|
||||
}
|
||||
array_push($this->_fieldDefStack, $fieldDef);
|
||||
array_push($this->_typeStack, $fieldDef ? $fieldDef->getType() : null);
|
||||
break;
|
||||
|
||||
case Node::OPERATION_DEFINITION:
|
||||
$type = null;
|
||||
if ($node->operation === 'query') {
|
||||
$type = $schema->getQueryType();
|
||||
} else if ($node->operation === 'mutation') {
|
||||
$type = $schema->getMutationType();
|
||||
}
|
||||
array_push($this->_typeStack, $type);
|
||||
break;
|
||||
|
||||
case Node::INLINE_FRAGMENT:
|
||||
case Node::FRAGMENT_DEFINITION:
|
||||
$type = $schema->getType($node->typeCondition->value);
|
||||
array_push($this->_typeStack, $type);
|
||||
break;
|
||||
|
||||
case Node::VARIABLE_DEFINITION:
|
||||
array_push($this->_inputTypeStack, self::typeFromAST($schema, $node->type));
|
||||
break;
|
||||
|
||||
case Node::ARGUMENT:
|
||||
$field = $this->getFieldDef();
|
||||
$argType = null;
|
||||
if ($field) {
|
||||
$argDef = Utils::find($field->args, function($arg) use ($node) {return $arg->name === $node->name->value;});
|
||||
if ($argDef) {
|
||||
$argType = $argDef->getType();
|
||||
}
|
||||
}
|
||||
array_push($this->_inputTypeStack, $argType);
|
||||
break;
|
||||
|
||||
case Node::DIRECTIVE:
|
||||
$directive = $schema->getDirective($node->name->value);
|
||||
array_push($this->_inputTypeStack, $directive ? $directive->type : null);
|
||||
break;
|
||||
|
||||
case Node::ARR:
|
||||
$arrayType = Type::getNullableType($this->getInputType());
|
||||
array_push(
|
||||
$this->_inputTypeStack,
|
||||
$arrayType instanceof ListOfType ? $arrayType->getWrappedType() : null
|
||||
);
|
||||
break;
|
||||
|
||||
case Node::OBJECT_FIELD:
|
||||
$objectType = Type::getUnmodifiedType($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;
|
||||
}
|
||||
array_push($this->_inputTypeStack, $fieldType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function leave(Node $node)
|
||||
{
|
||||
switch ($node->kind) {
|
||||
case Node::SELECTION_SET:
|
||||
array_pop($this->_parentTypeStack);
|
||||
break;
|
||||
case Node::FIELD:
|
||||
array_pop($this->_fieldDefStack);
|
||||
array_pop($this->_typeStack);
|
||||
break;
|
||||
case Node::OPERATION_DEFINITION:
|
||||
case Node::INLINE_FRAGMENT:
|
||||
case Node::FRAGMENT_DEFINITION:
|
||||
array_pop($this->_typeStack);
|
||||
break;
|
||||
case Node::VARIABLE_DEFINITION:
|
||||
case Node::ARGUMENT:
|
||||
case Node::DIRECTIVE:
|
||||
case Node::ARR:
|
||||
case Node::OBJECT_FIELD:
|
||||
array_pop($this->_inputTypeStack);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
318
src/Validator/DocumentValidator.php
Normal file
318
src/Validator/DocumentValidator.php
Normal file
@ -0,0 +1,318 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator;
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\ArrayValue;
|
||||
use GraphQL\Language\AST\Document;
|
||||
use GraphQL\Language\AST\FragmentSpread;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\Value;
|
||||
use GraphQL\Language\AST\Variable;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Language\VisitorOperation;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Utils;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Validator\Rules\ArgumentsOfCorrectType;
|
||||
use GraphQL\Validator\Rules\DefaultValuesOfCorrectType;
|
||||
use GraphQL\Validator\Rules\FieldsOnCorrectType;
|
||||
use GraphQL\Validator\Rules\FragmentsOnCompositeTypes;
|
||||
use GraphQL\Validator\Rules\KnownArgumentNames;
|
||||
use GraphQL\Validator\Rules\KnownDirectives;
|
||||
use GraphQL\Validator\Rules\KnownFragmentNames;
|
||||
use GraphQL\Validator\Rules\KnownTypeNames;
|
||||
use GraphQL\Validator\Rules\NoFragmentCycles;
|
||||
use GraphQL\Validator\Rules\NoUndefinedVariables;
|
||||
use GraphQL\Validator\Rules\NoUnusedFragments;
|
||||
use GraphQL\Validator\Rules\NoUnusedVariables;
|
||||
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
|
||||
use GraphQL\Validator\Rules\PossibleFragmentSpreads;
|
||||
use GraphQL\Validator\Rules\ScalarLeafs;
|
||||
use GraphQL\Validator\Rules\VariablesAreInputTypes;
|
||||
use GraphQL\Validator\Rules\VariablesInAllowedPosition;
|
||||
|
||||
class DocumentValidator
|
||||
{
|
||||
private static $allRules;
|
||||
|
||||
static function allRules()
|
||||
{
|
||||
if (null === self::$allRules) {
|
||||
self::$allRules = [
|
||||
new ArgumentsOfCorrectType(),
|
||||
new DefaultValuesOfCorrectType(),
|
||||
new FieldsOnCorrectType(),
|
||||
new FragmentsOnCompositeTypes(),
|
||||
new KnownArgumentNames(),
|
||||
new KnownDirectives(),
|
||||
new KnownFragmentNames(),
|
||||
new KnownTypeNames(),
|
||||
new NoFragmentCycles(),
|
||||
new NoUndefinedVariables(),
|
||||
new NoUnusedFragments(),
|
||||
new NoUnusedVariables(),
|
||||
new OverlappingFieldsCanBeMerged(),
|
||||
new PossibleFragmentSpreads(),
|
||||
new ScalarLeafs(),
|
||||
new VariablesAreInputTypes(),
|
||||
new VariablesInAllowedPosition()
|
||||
];
|
||||
}
|
||||
return self::$allRules;
|
||||
}
|
||||
|
||||
public static function validate(Schema $schema, Document $ast, array $rules = null)
|
||||
{
|
||||
$errors = self::visitUsingRules($schema, $ast, $rules ?: self::allRules());
|
||||
$isValid = empty($errors);
|
||||
|
||||
$result = [
|
||||
'isValid' => $isValid,
|
||||
'errors' => $isValid ? null : array_map(['GraphQL\Error', 'formatError'], $errors)
|
||||
];
|
||||
return $result;
|
||||
}
|
||||
|
||||
static function isError($value)
|
||||
{
|
||||
return is_array($value)
|
||||
? count(array_filter($value, function($item) { return $item instanceof \Exception;})) === count($value)
|
||||
: $value instanceof \Exception;
|
||||
}
|
||||
|
||||
static function append(&$arr, $items)
|
||||
{
|
||||
if (is_array($items)) {
|
||||
$arr = array_merge($arr, $items);
|
||||
} else {
|
||||
$arr[] = $items;
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
|
||||
static function isValidLiteralValue($valueAST, Type $type)
|
||||
{
|
||||
// A value can only be not provided if the type is nullable.
|
||||
if (!$valueAST) {
|
||||
return !($type instanceof NonNull);
|
||||
}
|
||||
|
||||
// Unwrap non-null.
|
||||
if ($type instanceof NonNull) {
|
||||
return self::isValidLiteralValue($valueAST, $type->getWrappedType());
|
||||
}
|
||||
|
||||
// This function only tests literals, and assumes variables will provide
|
||||
// values of the correct type.
|
||||
if ($valueAST instanceof Variable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$valueAST instanceof Value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Lists accept a non-list value as a list of one.
|
||||
if ($type instanceof ListOfType) {
|
||||
$itemType = $type->getWrappedType();
|
||||
if ($valueAST instanceof ArrayValue) {
|
||||
foreach($valueAST->values as $itemAST) {
|
||||
if (!self::isValidLiteralValue($itemAST, $itemType)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return self::isValidLiteralValue($valueAST, $itemType);
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar/Enum input checks to ensure the type can coerce the value to
|
||||
// a non-null value.
|
||||
if ($type instanceof ScalarType || $type instanceof EnumType) {
|
||||
return $type->coerceLiteral($valueAST) !== null;
|
||||
}
|
||||
|
||||
// Input objects check each defined field, ensuring it is of the correct
|
||||
// type and provided if non-nullable.
|
||||
if ($type instanceof InputObjectType) {
|
||||
$fields = $type->getFields();
|
||||
if ($valueAST->kind !== Node::OBJECT) {
|
||||
return false;
|
||||
}
|
||||
$fieldASTs = $valueAST->fields;
|
||||
$fieldASTMap = Utils::keyMap($fieldASTs, function($field) {return $field->name->value;});
|
||||
|
||||
foreach ($fields as $fieldKey => $field) {
|
||||
$fieldName = $field->name ?: $fieldKey;
|
||||
if (!isset($fieldASTMap[$fieldName]) && $field->getType() instanceof NonNull) {
|
||||
// Required fields missing
|
||||
return false;
|
||||
}
|
||||
}
|
||||
foreach ($fieldASTs as $fieldAST) {
|
||||
if (empty($fields[$fieldAST->name->value]) || !self::isValidLiteralValue($fieldAST->value, $fields[$fieldAST->name->value]->getType())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Any other kind of type is not an input type, and a literal cannot be used.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This uses a specialized visitor which runs multiple visitors in parallel,
|
||||
* while maintaining the visitor skip and break API.
|
||||
*
|
||||
* @param Schema $schema
|
||||
* @param Document $documentAST
|
||||
* @param array $rules
|
||||
* @return array
|
||||
*/
|
||||
public static function visitUsingRules(Schema $schema, Document $documentAST, array $rules)
|
||||
{
|
||||
$typeInfo = new TypeInfo($schema);
|
||||
$context = new ValidationContext($schema, $documentAST, $typeInfo);
|
||||
$errors = [];
|
||||
|
||||
// TODO: convert to class
|
||||
$visitInstances = function($ast, $instances) use ($typeInfo, $context, &$errors, &$visitInstances) {
|
||||
$skipUntil = new \SplFixedArray(count($instances));
|
||||
$skipCount = 0;
|
||||
|
||||
Visitor::visit($ast, [
|
||||
'enter' => function ($node, $key) use ($typeInfo, $instances, $skipUntil, &$skipCount, &$errors, $context, $visitInstances) {
|
||||
$typeInfo->enter($node);
|
||||
for ($i = 0; $i < count($instances); $i++) {
|
||||
// Do not visit this instance if it returned false for a previous node
|
||||
if ($skipUntil[$i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = null;
|
||||
|
||||
// Do not visit top level fragment definitions if this instance will
|
||||
// visit those fragments inline because it
|
||||
// provided `visitSpreadFragments`.
|
||||
if ($node->kind === Node::FRAGMENT_DEFINITION && $key !== null && !empty($instances[$i]['visitSpreadFragments'])) {
|
||||
$result = Visitor::skipNode();
|
||||
} else {
|
||||
$enter = Visitor::getVisitFn($instances[$i], false, $node->kind);
|
||||
if ($enter instanceof \Closure) {
|
||||
// $enter = $enter->bindTo($instances[$i]);
|
||||
$result = call_user_func_array($enter, func_get_args());
|
||||
} else {
|
||||
$result = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($result instanceof VisitorOperation) {
|
||||
if ($result->doContinue) {
|
||||
$skipUntil[$i] = $node;
|
||||
$skipCount++;
|
||||
// If all instances are being skipped over, skip deeper traversal
|
||||
if ($skipCount === count($instances)) {
|
||||
for ($k = 0; $k < count($instances); $k++) {
|
||||
if ($skipUntil[$k] === $node) {
|
||||
$skipUntil[$k] = null;
|
||||
$skipCount--;
|
||||
}
|
||||
}
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
} else if ($result->doBreak) {
|
||||
$instances[$i] = null;
|
||||
}
|
||||
} else if ($result && self::isError($result)) {
|
||||
self::append($errors, $result);
|
||||
for ($j = $i - 1; $j >= 0; $j--) {
|
||||
$leaveFn = Visitor::getVisitFn($instances[$j], true, $node->kind);
|
||||
if ($leaveFn) {
|
||||
// $leaveFn = $leaveFn->bindTo($instances[$j])
|
||||
$result = call_user_func_array($leaveFn, func_get_args());
|
||||
|
||||
if ($result instanceof VisitorOperation) {
|
||||
if ($result->doBreak) {
|
||||
$instances[$j] = null;
|
||||
}
|
||||
} else if (self::isError($result)) {
|
||||
self::append($errors, $result);
|
||||
} else if ($result !== null) {
|
||||
throw new \Exception("Config cannot edit document.");
|
||||
}
|
||||
}
|
||||
}
|
||||
$typeInfo->leave($node);
|
||||
return Visitor::skipNode();
|
||||
} else if ($result !== null) {
|
||||
throw new \Exception("Config cannot edit document.");
|
||||
}
|
||||
}
|
||||
|
||||
// If any validation instances provide the flag `visitSpreadFragments`
|
||||
// and this node is a fragment spread, validate the fragment from
|
||||
// this point.
|
||||
if ($node instanceof FragmentSpread) {
|
||||
$fragment = $context->getFragment($node->name->value);
|
||||
if ($fragment) {
|
||||
$fragVisitingInstances = [];
|
||||
foreach ($instances as $idx => $inst) {
|
||||
if (!empty($inst['visitSpreadFragments']) && !$skipUntil[$idx]) {
|
||||
$fragVisitingInstances[] = $inst;
|
||||
}
|
||||
}
|
||||
if (!empty($fragVisitingInstances)) {
|
||||
$visitInstances($fragment, $fragVisitingInstances);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'leave' => function ($node) use ($instances, $typeInfo, $skipUntil, &$skipCount, &$errors) {
|
||||
for ($i = count($instances) - 1; $i >= 0; $i--) {
|
||||
if ($skipUntil[$i]) {
|
||||
if ($skipUntil[$i] === $node) {
|
||||
$skipUntil[$i] = null;
|
||||
$skipCount--;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$leaveFn = Visitor::getVisitFn($instances[$i], true, $node->kind);
|
||||
|
||||
if ($leaveFn) {
|
||||
// $leaveFn = $leaveFn.bindTo($instances[$i]);
|
||||
$result = call_user_func_array($leaveFn, func_get_args());
|
||||
|
||||
if ($result instanceof VisitorOperation) {
|
||||
if ($result->doBreak) {
|
||||
$instances[$i] = null;
|
||||
}
|
||||
} if (self::isError($result)) {
|
||||
self::append($errors, $result);
|
||||
} else if ($result !== null) {
|
||||
throw new \Exception("Config cannot edit document.");
|
||||
}
|
||||
}
|
||||
}
|
||||
$typeInfo->leave($node);
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
// Visit the whole document with instances of all provided rules.
|
||||
$allRuleInstances = [];
|
||||
foreach ($rules as $rule) {
|
||||
$allRuleInstances[] = $rule($context);
|
||||
}
|
||||
$visitInstances($documentAST, $allRuleInstances);
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
160
src/Validator/Messages.php
Normal file
160
src/Validator/Messages.php
Normal file
@ -0,0 +1,160 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator;
|
||||
|
||||
class Messages
|
||||
{
|
||||
static function missingArgMessage($fieldName, $argName, $typeName)
|
||||
{
|
||||
return "Field $fieldName argument $argName of type $typeName, is required but not provided.";
|
||||
}
|
||||
|
||||
static function badValueMessage($argName, $typeName, $value)
|
||||
{
|
||||
return "Argument $argName expected type $typeName but got: $value.";
|
||||
}
|
||||
|
||||
static function defaultForNonNullArgMessage($varName, $typeName, $guessTypeName)
|
||||
{
|
||||
return "Variable \$$varName of type $typeName " .
|
||||
"is required and will never use the default value. " .
|
||||
"Perhaps you meant to use type $guessTypeName.";
|
||||
}
|
||||
|
||||
static function badValueForDefaultArgMessage($varName, $typeName, $value)
|
||||
{
|
||||
return "Variable \$$varName of type $typeName has invalid default value: $value.";
|
||||
}
|
||||
|
||||
static function undefinedFieldMessage($field, $type)
|
||||
{
|
||||
return 'Cannot query field ' . $field . ' on ' . $type;
|
||||
}
|
||||
|
||||
static function fragmentOnNonCompositeErrorMessage($fragName, $typeName)
|
||||
{
|
||||
return "Fragment $fragName cannot condition on non composite type \"$typeName\".";
|
||||
}
|
||||
|
||||
static function inlineFragmentOnNonCompositeErrorMessage($typeName)
|
||||
{
|
||||
return "Fragment cannot condition on non composite type \"$typeName\".";
|
||||
}
|
||||
|
||||
static function unknownArgMessage($argName, $fieldName, $typeName)
|
||||
{
|
||||
return "Unknown argument $argName on field $fieldName of type $typeName.";
|
||||
}
|
||||
|
||||
static function unknownTypeMessage($typeName)
|
||||
{
|
||||
return "Unknown type $typeName.";
|
||||
}
|
||||
|
||||
static function undefinedVarMessage($varName)
|
||||
{
|
||||
return "Variable \$$varName is not defined.";
|
||||
}
|
||||
|
||||
static function undefinedVarByOpMessage($varName, $opName)
|
||||
{
|
||||
return "Variable \$$varName is not defined by operation $opName.";
|
||||
}
|
||||
|
||||
static function unusedFragMessage($fragName)
|
||||
{
|
||||
return "Fragment $fragName is not used.";
|
||||
}
|
||||
|
||||
static function unusedVariableMessage($varName)
|
||||
{
|
||||
return "Variable \$$varName is not used.";
|
||||
}
|
||||
|
||||
static function typeIncompatibleSpreadMessage($fragName, $parentType, $fragType)
|
||||
{
|
||||
return "Fragment \"$fragName\" cannot be spread here as objects of " .
|
||||
"type \"$parentType\" can never be of type \"$fragType\".";
|
||||
}
|
||||
|
||||
static function typeIncompatibleAnonSpreadMessage($parentType, $fragType)
|
||||
{
|
||||
return "Fragment cannot be spread here as objects of " .
|
||||
"type \"$parentType\" can never be of type \"$fragType\".";
|
||||
}
|
||||
|
||||
static function noSubselectionAllowedMessage($field, $type)
|
||||
{
|
||||
return "Field \"$field\" of type $type must not have a sub selection.";
|
||||
}
|
||||
|
||||
static function requiredSubselectionMessage($field, $type)
|
||||
{
|
||||
return "Field \"$field\" of type $type must have a sub selection.";
|
||||
}
|
||||
|
||||
static function nonInputTypeOnVarMessage($variableName, $typeName)
|
||||
{
|
||||
return "Variable $${variableName} cannot be non input type $typeName.";
|
||||
}
|
||||
|
||||
static function cycleErrorMessage($fragmentName, $spreadNames)
|
||||
{
|
||||
return "Cannot spread fragment $fragmentName within itself" .
|
||||
(!empty($spreadNames) ? (" via " . implode(', ', $spreadNames)) : '') . '.';
|
||||
}
|
||||
|
||||
static function unknownDirectiveMessage($directiveName)
|
||||
{
|
||||
return "Unknown directive $directiveName.";
|
||||
}
|
||||
|
||||
static function misplacedDirectiveMessage($directiveName, $placement)
|
||||
{
|
||||
return "Directive $directiveName may not be used on $placement.";
|
||||
}
|
||||
|
||||
static function missingDirectiveValueMessage($directiveName, $typeName)
|
||||
{
|
||||
return "Directive $directiveName expects a value of type $typeName.";
|
||||
}
|
||||
|
||||
static function noDirectiveValueMessage($directiveName)
|
||||
{
|
||||
return "Directive $directiveName expects no value.";
|
||||
}
|
||||
|
||||
static function badDirectiveValueMessage($directiveName, $typeName, $value)
|
||||
{
|
||||
return "Directive $directiveName expected type $typeName but " .
|
||||
"got: $value.";
|
||||
}
|
||||
|
||||
static function badVarPosMessage($varName, $varType, $expectedType)
|
||||
{
|
||||
return "Variable \$$varName of type $varType used in position expecting ".
|
||||
"type $expectedType.";
|
||||
}
|
||||
|
||||
static function fieldsConflictMessage($responseName, $reason)
|
||||
{
|
||||
$reasonMessage = self::reasonMessage($reason);
|
||||
return "Fields $responseName conflict because {$reasonMessage}.";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $reason
|
||||
* @return array
|
||||
*/
|
||||
private static function reasonMessage($reason)
|
||||
{
|
||||
if (is_array($reason)) {
|
||||
$tmp = array_map(function($tmp) {
|
||||
list($responseName, $subReason) = $tmp;
|
||||
$reasonMessage = self::reasonMessage($subReason);
|
||||
return "subfields $responseName conflict because $reasonMessage";
|
||||
}, $reason);
|
||||
return implode(' and ', $tmp);
|
||||
}
|
||||
return $reason;
|
||||
}
|
||||
}
|
68
src/Validator/Rules/ArgumentsOfCorrectType.php
Normal file
68
src/Validator/Rules/ArgumentsOfCorrectType.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Argument;
|
||||
use GraphQL\Language\AST\Field;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Utils;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class ArgumentsOfCorrectType
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
Node::FIELD => function(Field $fieldAST) use ($context) {
|
||||
$fieldDef = $context->getFieldDef();
|
||||
if (!$fieldDef) {
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
$errors = [];
|
||||
$argASTs = $fieldAST->arguments ?: [];
|
||||
$argASTMap = Utils::keyMap($argASTs, function (Argument $arg) {
|
||||
return $arg->name->value;
|
||||
});
|
||||
|
||||
foreach ($fieldDef->args as $argDef) {
|
||||
$argAST = isset($argASTMap[$argDef->name]) ? $argASTMap[$argDef->name] : null;
|
||||
if (!$argAST && $argDef->getType() instanceof NonNull) {
|
||||
$errors[] = new Error(
|
||||
Messages::missingArgMessage(
|
||||
$fieldAST->name->value,
|
||||
$argDef->name,
|
||||
$argDef->getType()
|
||||
),
|
||||
[$fieldAST]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$argDefMap = Utils::keyMap($fieldDef->args, function ($def) {
|
||||
return $def->name;
|
||||
});
|
||||
foreach ($argASTs as $argAST) {
|
||||
$argDef = $argDefMap[$argAST->name->value];
|
||||
if ($argDef && !DocumentValidator::isValidLiteralValue($argAST->value, $argDef->getType())) {
|
||||
$errors[] = new Error(
|
||||
Messages::badValueMessage(
|
||||
$argAST->name->value,
|
||||
$argDef->getType(),
|
||||
Printer::doPrint($argAST->value)
|
||||
),
|
||||
[$argAST->value]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return !empty($errors) ? $errors : null;
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
40
src/Validator/Rules/DefaultValuesOfCorrectType.php
Normal file
40
src/Validator/Rules/DefaultValuesOfCorrectType.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\VariableDefinition;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class DefaultValuesOfCorrectType
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
Node::VARIABLE_DEFINITION => function(VariableDefinition $varDefAST) use ($context) {
|
||||
$name = $varDefAST->variable->name->value;
|
||||
$defaultValue = $varDefAST->defaultValue;
|
||||
$type = $context->getInputType();
|
||||
|
||||
if ($type instanceof NonNull && $defaultValue) {
|
||||
return new Error(
|
||||
Messages::defaultForNonNullArgMessage($name, $type, $type->getWrappedType()),
|
||||
[$defaultValue]
|
||||
);
|
||||
}
|
||||
if ($type && $defaultValue && !DocumentValidator::isValidLiteralValue($defaultValue, $type)) {
|
||||
return new Error(
|
||||
Messages::badValueForDefaultArgMessage($name, $type, Printer::doPrint($defaultValue)),
|
||||
[$defaultValue]
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
30
src/Validator/Rules/FieldsOnCorrectType.php
Normal file
30
src/Validator/Rules/FieldsOnCorrectType.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Field;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class FieldsOnCorrectType
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
Node::FIELD => function(Field $node) use ($context) {
|
||||
$type = $context->getParentType();
|
||||
if ($type) {
|
||||
$fieldDef = $context->getFieldDef();
|
||||
if (!$fieldDef) {
|
||||
return new Error(
|
||||
Messages::undefinedFieldMessage($node->name->value, $type->name),
|
||||
[$node]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
44
src/Validator/Rules/FragmentsOnCompositeTypes.php
Normal file
44
src/Validator/Rules/FragmentsOnCompositeTypes.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\FragmentDefinition;
|
||||
use GraphQL\Language\AST\InlineFragment;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Type\Definition\CompositeType;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class FragmentsOnCompositeTypes
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
Node::INLINE_FRAGMENT => function(InlineFragment $node) use ($context) {
|
||||
$typeName = $node->typeCondition->value;
|
||||
$type = $context->getSchema()->getType($typeName);
|
||||
$isCompositeType = $type instanceof CompositeType;
|
||||
|
||||
if (!$isCompositeType) {
|
||||
return new Error(
|
||||
"Fragment cannot condition on non composite type \"$typeName\".",
|
||||
[$node->typeCondition]
|
||||
);
|
||||
}
|
||||
},
|
||||
Node::FRAGMENT_DEFINITION => function(FragmentDefinition $node) use ($context) {
|
||||
$typeName = $node->typeCondition->value;
|
||||
$type = $context->getSchema()->getType($typeName);
|
||||
$isCompositeType = $type instanceof CompositeType;
|
||||
|
||||
if (!$isCompositeType) {
|
||||
return new Error(
|
||||
Messages::fragmentOnNonCompositeErrorMessage($node->name->value, $typeName),
|
||||
[$node->typeCondition]
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
40
src/Validator/Rules/KnownArgumentNames.php
Normal file
40
src/Validator/Rules/KnownArgumentNames.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Argument;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Utils;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class KnownArgumentNames
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
Node::ARGUMENT => function(Argument $node) use ($context) {
|
||||
$fieldDef = $context->getFieldDef();
|
||||
if ($fieldDef) {
|
||||
$argDef = null;
|
||||
foreach ($fieldDef->args as $arg) {
|
||||
if ($arg->name === $node->name->value) {
|
||||
$argDef = $arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$argDef) {
|
||||
$parentType = $context->getParentType();
|
||||
Utils::invariant($parentType);
|
||||
return new Error(
|
||||
Messages::unknownArgMessage($node->name->value, $fieldDef->name, $parentType->name),
|
||||
[$node]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
66
src/Validator/Rules/KnownDirectives.php
Normal file
66
src/Validator/Rules/KnownDirectives.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Directive;
|
||||
use GraphQL\Language\AST\Field;
|
||||
use GraphQL\Language\AST\FragmentDefinition;
|
||||
use GraphQL\Language\AST\FragmentSpread;
|
||||
use GraphQL\Language\AST\InlineFragment;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\OperationDefinition;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class KnownDirectives
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
Node::DIRECTIVE => function (Directive $node, $key, $parent, $path, $ancestors) use ($context) {
|
||||
$directiveDef = null;
|
||||
foreach ($context->getSchema()->getDirectives() as $def) {
|
||||
if ($def->name === $node->name->value) {
|
||||
$directiveDef = $def;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$directiveDef) {
|
||||
return new Error(
|
||||
Messages::unknownDirectiveMessage($node->name->value),
|
||||
[$node]
|
||||
);
|
||||
}
|
||||
$appliedTo = $ancestors[count($ancestors) - 1];
|
||||
|
||||
if ($appliedTo instanceof OperationDefinition && !$directiveDef->onOperation) {
|
||||
return new Error(
|
||||
Messages::misplacedDirectiveMessage($node->name->value, 'operation'),
|
||||
[$node]
|
||||
);
|
||||
}
|
||||
if ($appliedTo instanceof Field && !$directiveDef->onField) {
|
||||
return new Error(
|
||||
Messages::misplacedDirectiveMessage($node->name->value, 'field'),
|
||||
[$node]
|
||||
);
|
||||
}
|
||||
|
||||
$fragmentKind = (
|
||||
$appliedTo instanceof FragmentSpread ||
|
||||
$appliedTo instanceof InlineFragment ||
|
||||
$appliedTo instanceof FragmentDefinition
|
||||
);
|
||||
|
||||
if ($fragmentKind && !$directiveDef->onFragment) {
|
||||
return new Error(
|
||||
Messages::misplacedDirectiveMessage($node->name->value, 'fragment'),
|
||||
[$node]
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
27
src/Validator/Rules/KnownFragmentNames.php
Normal file
27
src/Validator/Rules/KnownFragmentNames.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\FragmentSpread;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class KnownFragmentNames
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
Node::FRAGMENT_SPREAD => function(FragmentSpread $node) use ($context) {
|
||||
$fragmentName = $node->name->value;
|
||||
$fragment = $context->getFragment($fragmentName);
|
||||
if (!$fragment) {
|
||||
return new Error(
|
||||
"Undefined fragment $fragmentName.",
|
||||
[$node->name]
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
28
src/Validator/Rules/KnownTypeNames.php
Normal file
28
src/Validator/Rules/KnownTypeNames.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Name;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class KnownTypeNames
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
Node::NAME => function(Name $node, $key) use ($context) {
|
||||
|
||||
if ($key === 'type' || $key === 'typeCondition') {
|
||||
$typeName = $node->value;
|
||||
$type = $context->getSchema()->getType($typeName);
|
||||
if (!$type) {
|
||||
return new Error(Messages::unknownTypeMessage($typeName), [$node]);
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
101
src/Validator/Rules/NoFragmentCycles.php
Normal file
101
src/Validator/Rules/NoFragmentCycles.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
/**
|
||||
* Created by PhpStorm.
|
||||
* User: Vladimir
|
||||
* Date: 11.07.2015
|
||||
* Time: 18:54
|
||||
*/
|
||||
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\FragmentDefinition;
|
||||
use GraphQL\Language\AST\FragmentSpread;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class NoFragmentCycles
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
// Gather all the fragment spreads ASTs for each fragment definition.
|
||||
// Importantly this does not include inline fragments.
|
||||
$definitions = $context->getDocument()->definitions;
|
||||
$spreadsInFragment = [];
|
||||
foreach ($definitions as $node) {
|
||||
if ($node instanceof FragmentDefinition) {
|
||||
$spreadsInFragment[$node->name->value] = $this->gatherSpreads($node);
|
||||
}
|
||||
}
|
||||
|
||||
// Tracks spreads known to lead to cycles to ensure that cycles are not
|
||||
// redundantly reported.
|
||||
$knownToLeadToCycle = new \SplObjectStorage();
|
||||
|
||||
return [
|
||||
Node::FRAGMENT_DEFINITION => function(FragmentDefinition $node) use ($spreadsInFragment, $knownToLeadToCycle) {
|
||||
$errors = [];
|
||||
$initialName = $node->name->value;
|
||||
|
||||
// Array of AST nodes used to produce meaningful errors
|
||||
$spreadPath = [];
|
||||
|
||||
$this->detectCycleRecursive($initialName, $spreadsInFragment, $knownToLeadToCycle, $initialName, $spreadPath, $errors);
|
||||
|
||||
if (!empty($errors)) {
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private function detectCycleRecursive($fragmentName, array $spreadsInFragment, \SplObjectStorage $knownToLeadToCycle, $initialName, array &$spreadPath, &$errors)
|
||||
{
|
||||
$spreadNodes = $spreadsInFragment[$fragmentName];
|
||||
|
||||
for ($i = 0; $i < count($spreadNodes); ++$i) {
|
||||
$spreadNode = $spreadNodes[$i];
|
||||
if (isset($knownToLeadToCycle[$spreadNode])) {
|
||||
continue ;
|
||||
}
|
||||
if ($spreadNode->name->value === $initialName) {
|
||||
$cyclePath = array_merge($spreadPath, [$spreadNode]);
|
||||
foreach ($cyclePath as $spread) {
|
||||
$knownToLeadToCycle[$spread] = true;
|
||||
}
|
||||
$errors[] = new Error(
|
||||
Messages::cycleErrorMessage($initialName, array_map(function ($s) {
|
||||
return $s->name->value;
|
||||
}, $spreadPath)),
|
||||
$cyclePath
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($spreadPath as $spread) {
|
||||
if ($spread === $spreadNode) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
$spreadPath[] = $spreadNode;
|
||||
$this->detectCycleRecursive($spreadNode->name->value, $spreadsInFragment, $knownToLeadToCycle, $initialName, $spreadPath, $errors);
|
||||
array_pop($spreadPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function gatherSpreads($node)
|
||||
{
|
||||
$spreadNodes = [];
|
||||
Visitor::visit($node, [
|
||||
Node::FRAGMENT_SPREAD => function(FragmentSpread $spread) use (&$spreadNodes) {
|
||||
$spreadNodes[] = $spread;
|
||||
}
|
||||
]);
|
||||
return $spreadNodes;
|
||||
}
|
||||
}
|
75
src/Validator/Rules/NoUndefinedVariables.php
Normal file
75
src/Validator/Rules/NoUndefinedVariables.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\FragmentDefinition;
|
||||
use GraphQL\Language\AST\FragmentSpread;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\OperationDefinition;
|
||||
use GraphQL\Language\AST\Variable;
|
||||
use GraphQL\Language\AST\VariableDefinition;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
/**
|
||||
* Class NoUndefinedVariables
|
||||
*
|
||||
* A GraphQL operation is only valid if all variables encountered, both directly
|
||||
* and via fragment spreads, are defined by that operation.
|
||||
*
|
||||
* @package GraphQL\Validator\Rules
|
||||
*/
|
||||
class NoUndefinedVariables
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
$operation = null;
|
||||
$visitedFragmentNames = [];
|
||||
$definedVariableNames = [];
|
||||
|
||||
return [
|
||||
// Visit FragmentDefinition after visiting FragmentSpread
|
||||
'visitSpreadFragments' => true,
|
||||
|
||||
Node::OPERATION_DEFINITION => function(OperationDefinition $node, $key, $parent, $path, $ancestors) use (&$operation, &$visitedFragmentNames, &$definedVariableNames) {
|
||||
$operation = $node;
|
||||
$visitedFragmentNames = [];
|
||||
$definedVariableNames = [];
|
||||
},
|
||||
Node::VARIABLE_DEFINITION => function(VariableDefinition $def) use (&$definedVariableNames) {
|
||||
$definedVariableNames[$def->variable->name->value] = true;
|
||||
},
|
||||
Node::VARIABLE => function(Variable $variable, $key, $parent, $path, $ancestors) use (&$definedVariableNames, &$visitedFragmentNames, &$operation) {
|
||||
$varName = $variable->name->value;
|
||||
if (empty($definedVariableNames[$varName])) {
|
||||
$withinFragment = false;
|
||||
foreach ($ancestors as $ancestor) {
|
||||
if ($ancestor instanceof FragmentDefinition) {
|
||||
$withinFragment = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($withinFragment && $operation && $operation->name) {
|
||||
return new Error(
|
||||
Messages::undefinedVarByOpMessage($varName, $operation->name->value),
|
||||
[$variable, $operation]
|
||||
);
|
||||
}
|
||||
return new Error(
|
||||
Messages::undefinedVarMessage($varName),
|
||||
[$variable]
|
||||
);
|
||||
}
|
||||
},
|
||||
Node::FRAGMENT_SPREAD => function(FragmentSpread $spreadAST) use (&$visitedFragmentNames) {
|
||||
// Only visit fragments of a particular name once per operation
|
||||
if (!empty($visitedFragmentNames[$spreadAST->name->value])) {
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
$visitedFragmentNames[$spreadAST->name->value] = true;
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
70
src/Validator/Rules/NoUnusedFragments.php
Normal file
70
src/Validator/Rules/NoUnusedFragments.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\FragmentDefinition;
|
||||
use GraphQL\Language\AST\FragmentSpread;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class NoUnusedFragments
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
$fragmentDefs = [];
|
||||
$spreadsWithinOperation = [];
|
||||
$fragAdjacencies = new \stdClass();
|
||||
$spreadNames = new \stdClass();
|
||||
|
||||
return [
|
||||
Node::OPERATION_DEFINITION => function() use (&$spreadNames, &$spreadsWithinOperation) {
|
||||
$spreadNames = new \stdClass();
|
||||
$spreadsWithinOperation[] = $spreadNames;
|
||||
},
|
||||
Node::FRAGMENT_DEFINITION => function(FragmentDefinition $def) use (&$fragmentDefs, &$spreadNames, &$fragAdjacencies) {
|
||||
$fragmentDefs[] = $def;
|
||||
$spreadNames = new \stdClass();
|
||||
$fragAdjacencies->{$def->name->value} = $spreadNames;
|
||||
},
|
||||
Node::FRAGMENT_SPREAD => function(FragmentSpread $spread) use (&$spreadNames) {
|
||||
$spreadNames->{$spread->name->value} = true;
|
||||
},
|
||||
Node::DOCUMENT => [
|
||||
'leave' => function() use (&$fragAdjacencies, &$spreadsWithinOperation, &$fragmentDefs) {
|
||||
$fragmentNameUsed = [];
|
||||
|
||||
foreach ($spreadsWithinOperation as $spreads) {
|
||||
$this->reduceSpreadFragments($spreads, $fragmentNameUsed, $fragAdjacencies);
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
foreach ($fragmentDefs as $def) {
|
||||
if (empty($fragmentNameUsed[$def->name->value])) {
|
||||
$errors[] = new Error(
|
||||
Messages::unusedFragMessage($def->name->value),
|
||||
[$def]
|
||||
);
|
||||
}
|
||||
}
|
||||
return !empty($errors) ? $errors : null;
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
private function reduceSpreadFragments($spreads, &$fragmentNameUsed, &$fragAdjacencies)
|
||||
{
|
||||
foreach ($spreads as $fragName => $fragment) {
|
||||
if (empty($fragmentNameUsed[$fragName])) {
|
||||
$fragmentNameUsed[$fragName] = true;
|
||||
$this->reduceSpreadFragments(
|
||||
$fragAdjacencies->{$fragName},
|
||||
$fragmentNameUsed,
|
||||
$fragAdjacencies
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
src/Validator/Rules/NoUnusedVariables.php
Normal file
57
src/Validator/Rules/NoUnusedVariables.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class NoUnusedVariables
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
$visitedFragmentNames = new \stdClass();
|
||||
$variableDefs = [];
|
||||
$variableNameUsed = new \stdClass();
|
||||
|
||||
return [
|
||||
// Visit FragmentDefinition after visiting FragmentSpread
|
||||
'visitSpreadFragments' => true,
|
||||
Node::OPERATION_DEFINITION => [
|
||||
'enter' => function() use (&$visitedFragmentNames, &$variableDefs, &$variableNameUsed) {
|
||||
$visitedFragmentNames = new \stdClass();
|
||||
$variableDefs = [];
|
||||
$variableNameUsed = new \stdClass();
|
||||
},
|
||||
'leave' => function() use (&$visitedFragmentNames, &$variableDefs, &$variableNameUsed) {
|
||||
$errors = [];
|
||||
foreach ($variableDefs as $def) {
|
||||
if (empty($variableNameUsed->{$def->variable->name->value})) {
|
||||
$errors[] = new Error(
|
||||
Messages::unusedVariableMessage($def->variable->name->value),
|
||||
[$def]
|
||||
);
|
||||
}
|
||||
}
|
||||
return !empty($errors) ? $errors : null;
|
||||
}
|
||||
],
|
||||
Node::VARIABLE_DEFINITION => function($def) use (&$variableDefs) {
|
||||
$variableDefs[] = $def;
|
||||
return Visitor::skipNode();
|
||||
},
|
||||
Node::VARIABLE => function($variable) use (&$variableNameUsed) {
|
||||
$variableNameUsed->{$variable->name->value} = true;
|
||||
},
|
||||
Node::FRAGMENT_SPREAD => function($spreadAST) use (&$visitedFragmentNames) {
|
||||
// Only visit fragments of a particular name once per operation
|
||||
if (!empty($visitedFragmentNames->{$spreadAST->name->value})) {
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
$visitedFragmentNames->{$spreadAST->name->value} = true;
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
276
src/Validator/Rules/OverlappingFieldsCanBeMerged.php
Normal file
276
src/Validator/Rules/OverlappingFieldsCanBeMerged.php
Normal file
@ -0,0 +1,276 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\FragmentSpread;
|
||||
use GraphQL\Language\AST\InlineFragment;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\SelectionSet;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Utils\PairSet;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class OverlappingFieldsCanBeMerged
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
$comparedSet = new PairSet();
|
||||
|
||||
return [
|
||||
Node::SELECTION_SET => [
|
||||
// Note: we validate on the reverse traversal so deeper conflicts will be
|
||||
// caught first, for clearer error messages.
|
||||
'leave' => function(SelectionSet $selectionSet) use ($context, $comparedSet) {
|
||||
$fieldMap = $this->collectFieldASTsAndDefs(
|
||||
$context,
|
||||
$context->getType(),
|
||||
$selectionSet
|
||||
);
|
||||
|
||||
$conflicts = $this->findConflicts($fieldMap, $context, $comparedSet);
|
||||
|
||||
if (!empty($conflicts)) {
|
||||
return array_map(function ($conflict) {
|
||||
$responseName = $conflict[0][0];
|
||||
$reason = $conflict[0][1];
|
||||
$blameNodes = $conflict[1];
|
||||
|
||||
return new Error(
|
||||
Messages::fieldsConflictMessage($responseName, $reason),
|
||||
$blameNodes
|
||||
);
|
||||
}, $conflicts);
|
||||
|
||||
}
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
private function findConflicts($fieldMap, ValidationContext $context, PairSet $comparedSet)
|
||||
{
|
||||
$conflicts = [];
|
||||
foreach ($fieldMap as $responseName => $fields) {
|
||||
$count = count($fields);
|
||||
if ($count > 1) {
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
for ($j = $i; $j < $count; $j++) {
|
||||
$conflict = $this->findConflict($responseName, $fields[$i], $fields[$j], $context, $comparedSet);
|
||||
if ($conflict) {
|
||||
$conflicts[] = $conflict;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $conflicts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ValidationContext $context
|
||||
* @param PairSet $comparedSet
|
||||
* @param $responseName
|
||||
* @param [Field, GraphQLFieldDefinition] $pair1
|
||||
* @param [Field, GraphQLFieldDefinition] $pair2
|
||||
* @return array|null
|
||||
*/
|
||||
private function findConflict($responseName, array $pair1, array $pair2, ValidationContext $context, PairSet $comparedSet)
|
||||
{
|
||||
list($ast1, $def1) = $pair1;
|
||||
list($ast2, $def2) = $pair2;
|
||||
|
||||
if ($ast1 === $ast2 || $comparedSet->has($ast1, $ast2)) {
|
||||
return null;
|
||||
}
|
||||
$comparedSet->add($ast1, $ast2);
|
||||
|
||||
$name1 = $ast1->name->value;
|
||||
$name2 = $ast2->name->value;
|
||||
|
||||
if ($name1 !== $name2) {
|
||||
return [
|
||||
[$responseName, "$name1 and $name2 are different fields"],
|
||||
[$ast1, $ast2]
|
||||
];
|
||||
}
|
||||
|
||||
$type1 = isset($def1) ? $def1->getType() : null;
|
||||
$type2 = isset($def2) ? $def2->getType() : null;
|
||||
|
||||
if (!$this->sameType($type1, $type2)) {
|
||||
return [
|
||||
[$responseName, "they return differing types $type1 and $type2"],
|
||||
[$ast1, $ast2]
|
||||
];
|
||||
}
|
||||
|
||||
$args1 = isset($ast1->arguments) ? $ast1->arguments : [];
|
||||
$args2 = isset($ast2->arguments) ? $ast2->arguments : [];
|
||||
|
||||
if (!$this->sameNameValuePairs($args1, $args2)) {
|
||||
return [
|
||||
[$responseName, 'they have differing arguments'],
|
||||
[$ast1, $ast2]
|
||||
];
|
||||
}
|
||||
|
||||
$directives1 = isset($ast1->directives) ? $ast1->directives : [];
|
||||
$directives2 = isset($ast2->directives) ? $ast2->directives : [];
|
||||
|
||||
if (!$this->sameNameValuePairs($directives1, $directives2)) {
|
||||
return [
|
||||
[$responseName, 'they have differing directives'],
|
||||
[$ast1, $ast2]
|
||||
];
|
||||
}
|
||||
|
||||
$selectionSet1 = isset($ast1->selectionSet) ? $ast1->selectionSet : null;
|
||||
$selectionSet2 = isset($ast2->selectionSet) ? $ast2->selectionSet : null;
|
||||
|
||||
if ($selectionSet1 && $selectionSet2) {
|
||||
$visitedFragmentNames = new \ArrayObject();
|
||||
|
||||
$subfieldMap = $this->collectFieldASTsAndDefs(
|
||||
$context,
|
||||
$type1,
|
||||
$selectionSet1,
|
||||
$visitedFragmentNames
|
||||
);
|
||||
$subfieldMap = $this->collectFieldASTsAndDefs(
|
||||
$context,
|
||||
$type2,
|
||||
$selectionSet2,
|
||||
$visitedFragmentNames,
|
||||
$subfieldMap
|
||||
);
|
||||
$conflicts = $this->findConflicts($subfieldMap, $context, $comparedSet);
|
||||
|
||||
if (!empty($conflicts)) {
|
||||
return [
|
||||
[$responseName, array_map(function ($conflict) { return $conflict[0]; }, $conflicts)],
|
||||
array_reduce($conflicts, function ($list, $conflict) { return array_merge($list, $conflict[1]); }, [$ast1, $ast2])
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a selectionSet, adds all of the fields in that selection to
|
||||
* the passed in map of fields, and returns it at the end.
|
||||
*
|
||||
* Note: This is not the same as execution's collectFields because at static
|
||||
* time we do not know what object type will be used, so we unconditionally
|
||||
* spread in all fragments.
|
||||
*
|
||||
* @param ValidationContext $context
|
||||
* @param Type|null $parentType
|
||||
* @param SelectionSet $selectionSet
|
||||
* @param \ArrayObject $visitedFragmentNames
|
||||
* @param \ArrayObject $astAndDefs
|
||||
* @return mixed
|
||||
*/
|
||||
private function collectFieldASTsAndDefs(ValidationContext $context, $parentType, SelectionSet $selectionSet, \ArrayObject $visitedFragmentNames = null, \ArrayObject $astAndDefs = null)
|
||||
{
|
||||
$_visitedFragmentNames = $visitedFragmentNames ?: new \ArrayObject();
|
||||
$_astAndDefs = $astAndDefs ?: new \ArrayObject();
|
||||
|
||||
for ($i = 0; $i < count($selectionSet->selections); $i++) {
|
||||
$selection = $selectionSet->selections[$i];
|
||||
|
||||
switch ($selection->kind) {
|
||||
case Node::FIELD:
|
||||
$fieldAST = $selection;
|
||||
$fieldName = $fieldAST->name->value;
|
||||
$fieldDef = null;
|
||||
if ($parentType && method_exists($parentType, 'getFields')) {
|
||||
$tmp = $parentType->getFields();
|
||||
if (isset($tmp[$fieldName])) {
|
||||
$fieldDef = $tmp[$fieldName];
|
||||
}
|
||||
}
|
||||
$responseName = $fieldAST->alias ? $fieldAST->alias->value : $fieldName;
|
||||
|
||||
if (!isset($_astAndDefs[$responseName])) {
|
||||
$_astAndDefs[$responseName] = new \ArrayObject();
|
||||
}
|
||||
$_astAndDefs[$responseName][] = [$fieldAST, $fieldDef];
|
||||
break;
|
||||
case Node::INLINE_FRAGMENT:
|
||||
/** @var InlineFragment $inlineFragment */
|
||||
$inlineFragment = $selection;
|
||||
$_astAndDefs = $this->collectFieldASTsAndDefs(
|
||||
$context,
|
||||
TypeInfo::typeFromAST($context->getSchema(), $inlineFragment->typeCondition),
|
||||
$inlineFragment->selectionSet,
|
||||
$_visitedFragmentNames,
|
||||
$_astAndDefs
|
||||
);
|
||||
break;
|
||||
case Node::FRAGMENT_SPREAD:
|
||||
/** @var FragmentSpread $fragmentSpread */
|
||||
$fragmentSpread = $selection;
|
||||
$fragName = $fragmentSpread->name->value;
|
||||
if (!empty($_visitedFragmentNames[$fragName])) {
|
||||
continue;
|
||||
}
|
||||
$_visitedFragmentNames[$fragName] = true;
|
||||
$fragment = $context->getFragment($fragName);
|
||||
if (!$fragment) {
|
||||
continue;
|
||||
}
|
||||
$_astAndDefs = $this->collectFieldASTsAndDefs(
|
||||
$context,
|
||||
TypeInfo::typeFromAST($context->getSchema(), $fragment->typeCondition),
|
||||
$fragment->selectionSet,
|
||||
$_visitedFragmentNames,
|
||||
$_astAndDefs
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $_astAndDefs;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Array<Argument | Directive> $pairs1
|
||||
* @param Array<Argument | Directive> $pairs2
|
||||
* @return bool|string
|
||||
*/
|
||||
private function sameNameValuePairs(array $pairs1, array $pairs2)
|
||||
{
|
||||
if (count($pairs1) !== count($pairs2)) {
|
||||
return false;
|
||||
}
|
||||
foreach ($pairs1 as $pair1) {
|
||||
$matchedPair2 = null;
|
||||
foreach ($pairs2 as $pair2) {
|
||||
if ($pair2->name->value === $pair1->name->value) {
|
||||
$matchedPair2 = $pair2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$matchedPair2) {
|
||||
return false;
|
||||
}
|
||||
if (!$this->sameValue($pair1->value, $matchedPair2->value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function sameValue($value1, $value2)
|
||||
{
|
||||
return (!$value1 && !$value2) || (Printer::doPrint($value1) === Printer::doPrint($value2));
|
||||
}
|
||||
|
||||
function sameType($type1, $type2)
|
||||
{
|
||||
return (!$type1 && !$type2) || (string) $type1 === (string) $type2;
|
||||
}
|
||||
}
|
79
src/Validator/Rules/PossibleFragmentSpreads.php
Normal file
79
src/Validator/Rules/PossibleFragmentSpreads.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\FragmentSpread;
|
||||
use GraphQL\Language\AST\InlineFragment;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Utils;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class PossibleFragmentSpreads
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
Node::INLINE_FRAGMENT => function(InlineFragment $node) use ($context) {
|
||||
$fragType = Type::getUnmodifiedType($context->getType());
|
||||
$parentType = $context->getParentType();
|
||||
if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) {
|
||||
return new Error(
|
||||
Messages::typeIncompatibleAnonSpreadMessage($parentType, $fragType),
|
||||
[$node]
|
||||
);
|
||||
}
|
||||
},
|
||||
Node::FRAGMENT_SPREAD => function(FragmentSpread $node) use ($context) {
|
||||
$fragName = $node->name->value;
|
||||
$fragType = Type::getUnmodifiedType($this->getFragmentType($context, $fragName));
|
||||
$parentType = $context->getParentType();
|
||||
|
||||
if ($fragType && $parentType && !$this->doTypesOverlap($fragType, $parentType)) {
|
||||
return new Error(
|
||||
Messages::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType),
|
||||
[$node]
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private function getFragmentType(ValidationContext $context, $name)
|
||||
{
|
||||
$frag = $context->getFragment($name);
|
||||
return $frag ? $context->getSchema()->getType($frag->typeCondition->value) : null;
|
||||
}
|
||||
|
||||
private function doTypesOverlap($t1, $t2)
|
||||
{
|
||||
if ($t1 === $t2) {
|
||||
return true;
|
||||
}
|
||||
if ($t1 instanceof ObjectType) {
|
||||
if ($t2 instanceof ObjectType) {
|
||||
return false;
|
||||
}
|
||||
return in_array($t1, $t2->getPossibleTypes());
|
||||
}
|
||||
if ($t1 instanceof InterfaceType || $t1 instanceof UnionType) {
|
||||
if ($t2 instanceof ObjectType) {
|
||||
return in_array($t2, $t1->getPossibleTypes());
|
||||
}
|
||||
$t1TypeNames = Utils::keyMap($t1->getPossibleTypes(), function ($type) {
|
||||
return $type->name;
|
||||
});
|
||||
foreach ($t2->getPossibleTypes() as $type) {
|
||||
if (!empty($t1TypeNames[$type->name])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
37
src/Validator/Rules/ScalarLeafs.php
Normal file
37
src/Validator/Rules/ScalarLeafs.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Field;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class ScalarLeafs
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
Node::FIELD => function(Field $node) use ($context) {
|
||||
$type = $context->getType();
|
||||
if ($type) {
|
||||
if (Type::isLeafType($type)) {
|
||||
if ($node->selectionSet) {
|
||||
return new Error(
|
||||
Messages::noSubselectionAllowedMessage($node->name->value, $type),
|
||||
[$node->selectionSet]
|
||||
);
|
||||
}
|
||||
} else if (!$node->selectionSet) {
|
||||
return new Error(
|
||||
Messages::requiredSubselectionMessage($node->name->value, $type),
|
||||
[$node]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
43
src/Validator/Rules/VariablesAreInputTypes.php
Normal file
43
src/Validator/Rules/VariablesAreInputTypes.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\Type;
|
||||
use GraphQL\Language\AST\VariableDefinition;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
use GraphQL\Utils;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class VariablesAreInputTypes
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
return [
|
||||
Node::VARIABLE_DEFINITION => function(VariableDefinition $node) use ($context) {
|
||||
$typeName = $this->getTypeASTName($node->type);
|
||||
$type = $context->getSchema()->getType($typeName);
|
||||
|
||||
if (!($type instanceof InputType)) {
|
||||
$variableName = $node->variable->name->value;
|
||||
return new Error(
|
||||
Messages::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)),
|
||||
[$node->type]
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private function getTypeASTName(Type $typeAST)
|
||||
{
|
||||
if ($typeAST->kind === Node::NAME) {
|
||||
return $typeAST->value;
|
||||
}
|
||||
Utils::invariant($typeAST->type, 'Must be wrapping type');
|
||||
return $this->getTypeASTName($typeAST->type);
|
||||
}
|
||||
}
|
86
src/Validator/Rules/VariablesInAllowedPosition.php
Normal file
86
src/Validator/Rules/VariablesInAllowedPosition.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator\Rules;
|
||||
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\FragmentSpread;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\Variable;
|
||||
use GraphQL\Language\AST\VariableDefinition;
|
||||
use GraphQL\Language\Visitor;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Validator\Messages;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
|
||||
class VariablesInAllowedPosition
|
||||
{
|
||||
public function __invoke(ValidationContext $context)
|
||||
{
|
||||
$varDefMap = new \ArrayObject();
|
||||
$visitedFragmentNames = new \ArrayObject();
|
||||
|
||||
return [
|
||||
// Visit FragmentDefinition after visiting FragmentSpread
|
||||
'visitSpreadFragments' => true,
|
||||
Node::OPERATION_DEFINITION => function () use (&$varDefMap, &$visitedFragmentNames) {
|
||||
$varDefMap = new \ArrayObject();
|
||||
$visitedFragmentNames = new \ArrayObject();
|
||||
},
|
||||
Node::VARIABLE_DEFINITION => function (VariableDefinition $varDefAST) use ($varDefMap) {
|
||||
$varDefMap[$varDefAST->variable->name->value] = $varDefAST;
|
||||
},
|
||||
Node::FRAGMENT_SPREAD => function (FragmentSpread $spreadAST) use ($visitedFragmentNames) {
|
||||
// Only visit fragments of a particular name once per operation
|
||||
if (!empty($visitedFragmentNames[$spreadAST->name->value])) {
|
||||
return Visitor::skipNode();
|
||||
}
|
||||
$visitedFragmentNames[$spreadAST->name->value] = true;
|
||||
},
|
||||
Node::VARIABLE => function (Variable $variableAST) use ($context, $varDefMap) {
|
||||
$varName = $variableAST->name->value;
|
||||
$varDef = isset($varDefMap[$varName]) ? $varDefMap[$varName] : null;
|
||||
$varType = $varDef ? TypeInfo::typeFromAST($context->getSchema(), $varDef->type) : null;
|
||||
$inputType = $context->getInputType();
|
||||
|
||||
if ($varType && $inputType &&
|
||||
!$this->varTypeAllowedForType($this->effectiveType($varType, $varDef), $inputType)
|
||||
) {
|
||||
return new Error(
|
||||
Messages::badVarPosMessage($varName, $varType, $inputType),
|
||||
[$variableAST]
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// A var type is allowed if it is the same or more strict than the expected
|
||||
// type. It can be more strict if the variable type is non-null when the
|
||||
// expected type is nullable. If both are list types, the variable item type can
|
||||
// be more strict than the expected item type.
|
||||
private function varTypeAllowedForType($varType, $expectedType)
|
||||
{
|
||||
if ($expectedType instanceof NonNull) {
|
||||
if ($varType instanceof NonNull) {
|
||||
return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if ($varType instanceof NonNull) {
|
||||
return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType);
|
||||
}
|
||||
if ($varType instanceof ListOfType && $expectedType instanceof ListOfType) {
|
||||
return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType());
|
||||
}
|
||||
return $varType === $expectedType;
|
||||
}
|
||||
|
||||
// If a variable definition has a default value, it's effectively non-null.
|
||||
private function effectiveType($varType, $varDef)
|
||||
{
|
||||
return (!$varDef->defaultValue || $varType instanceof NonNull) ? $varType : new NonNull($varType);
|
||||
}
|
||||
|
||||
}
|
116
src/Validator/ValidationContext.php
Normal file
116
src/Validator/ValidationContext.php
Normal file
@ -0,0 +1,116 @@
|
||||
<?php
|
||||
namespace GraphQL\Validator;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Language\AST\Document;
|
||||
use GraphQL\Language\AST\FragmentDefinition;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Type\Definition\CompositeType;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
use GraphQL\Type\Definition\OutputType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
|
||||
/**
|
||||
* An instance of this class is passed as the "this" context to all validators,
|
||||
* allowing access to commonly useful contextual information from within a
|
||||
* validation rule.
|
||||
*/
|
||||
class ValidationContext
|
||||
{
|
||||
/**
|
||||
* @var Schema
|
||||
*/
|
||||
private $_schema;
|
||||
|
||||
/**
|
||||
* @var Document
|
||||
*/
|
||||
private $_ast;
|
||||
|
||||
/**
|
||||
* @var TypeInfo
|
||||
*/
|
||||
private $_typeInfo;
|
||||
|
||||
/**
|
||||
* @var array<string, FragmentDefinition>
|
||||
*/
|
||||
private $_fragments;
|
||||
|
||||
function __construct(Schema $schema, Document $ast, TypeInfo $typeInfo)
|
||||
{
|
||||
$this->_schema = $schema;
|
||||
$this->_ast = $ast;
|
||||
$this->_typeInfo = $typeInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Schema
|
||||
*/
|
||||
function getSchema()
|
||||
{
|
||||
return $this->_schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Document
|
||||
*/
|
||||
function getDocument()
|
||||
{
|
||||
return $this->_ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* @return FragmentDefinition|null
|
||||
*/
|
||||
function getFragment($name)
|
||||
{
|
||||
$fragments = $this->_fragments;
|
||||
if (!$fragments) {
|
||||
$this->_fragments = $fragments =
|
||||
array_reduce($this->getDocument()->definitions, function($frags, $statement) {
|
||||
if ($statement->kind === Node::FRAGMENT_DEFINITION) {
|
||||
$frags[$statement->name->value] = $statement;
|
||||
}
|
||||
return $frags;
|
||||
}, []);
|
||||
}
|
||||
return isset($fragments[$name]) ? $fragments[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns OutputType
|
||||
*
|
||||
* @return Type
|
||||
*/
|
||||
function getType()
|
||||
{
|
||||
return $this->_typeInfo->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CompositeType
|
||||
*/
|
||||
function getParentType()
|
||||
{
|
||||
return $this->_typeInfo->getParentType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InputType
|
||||
*/
|
||||
function getInputType()
|
||||
{
|
||||
return $this->_typeInfo->getInputType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FieldDefinition
|
||||
*/
|
||||
function getFieldDef()
|
||||
{
|
||||
return $this->_typeInfo->getFieldDef();
|
||||
}
|
||||
}
|
225
tests/Executor/DirectivesTest.php
Normal file
225
tests/Executor/DirectivesTest.php
Normal file
@ -0,0 +1,225 @@
|
||||
<?php
|
||||
namespace GraphQL\Executor;
|
||||
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
|
||||
class DirectivesTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// Execute: handles directives
|
||||
// works without directives
|
||||
public function testWorksWithoutDirectives()
|
||||
{
|
||||
// basic query works
|
||||
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b }'));
|
||||
}
|
||||
|
||||
public function testWorksOnScalars()
|
||||
{
|
||||
// if true includes scalar
|
||||
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b @if:true }'));
|
||||
|
||||
// if false omits on scalar
|
||||
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery('{ a, b @if:false }'));
|
||||
|
||||
// unless false includes scalar
|
||||
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b @unless:false }'));
|
||||
|
||||
// unless true omits scalar
|
||||
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery('{ a, b @unless:true }'));
|
||||
}
|
||||
|
||||
public function testWorksOnFragmentSpreads()
|
||||
{
|
||||
// if false omits fragment spread
|
||||
$q = '
|
||||
query Q {
|
||||
a
|
||||
...Frag @if:false
|
||||
}
|
||||
fragment Frag on TestType {
|
||||
b
|
||||
}
|
||||
';
|
||||
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q));
|
||||
|
||||
// if true includes fragment spread
|
||||
$q = '
|
||||
query Q {
|
||||
a
|
||||
...Frag @if:true
|
||||
}
|
||||
fragment Frag on TestType {
|
||||
b
|
||||
}
|
||||
';
|
||||
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($q));
|
||||
|
||||
// unless false includes fragment spread
|
||||
$q = '
|
||||
query Q {
|
||||
a
|
||||
...Frag @unless:false
|
||||
}
|
||||
fragment Frag on TestType {
|
||||
b
|
||||
}
|
||||
';
|
||||
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($q));
|
||||
|
||||
// unless true omits fragment spread
|
||||
$q = '
|
||||
query Q {
|
||||
a
|
||||
...Frag @unless:true
|
||||
}
|
||||
fragment Frag on TestType {
|
||||
b
|
||||
}
|
||||
';
|
||||
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q));
|
||||
}
|
||||
|
||||
public function testWorksOnInlineFragment()
|
||||
{
|
||||
// if false omits inline fragment
|
||||
$q = '
|
||||
query Q {
|
||||
a
|
||||
... on TestType @if:false {
|
||||
b
|
||||
}
|
||||
}
|
||||
fragment Frag on TestType {
|
||||
b
|
||||
}
|
||||
';
|
||||
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q));
|
||||
|
||||
// if true includes inline fragment
|
||||
$q = '
|
||||
query Q {
|
||||
a
|
||||
... on TestType @if:true {
|
||||
b
|
||||
}
|
||||
}
|
||||
fragment Frag on TestType {
|
||||
b
|
||||
}
|
||||
';
|
||||
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($q));
|
||||
|
||||
// unless false includes inline fragment
|
||||
$q = '
|
||||
query Q {
|
||||
a
|
||||
... on TestType @unless:false {
|
||||
b
|
||||
}
|
||||
}
|
||||
fragment Frag on TestType {
|
||||
b
|
||||
}
|
||||
';
|
||||
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($q));
|
||||
|
||||
// unless true includes inline fragment
|
||||
$q = '
|
||||
query Q {
|
||||
a
|
||||
... on TestType @unless:true {
|
||||
b
|
||||
}
|
||||
}
|
||||
fragment Frag on TestType {
|
||||
b
|
||||
}
|
||||
';
|
||||
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q));
|
||||
}
|
||||
|
||||
public function testWorksOnFragment()
|
||||
{
|
||||
// if false omits fragment
|
||||
$q = '
|
||||
query Q {
|
||||
a
|
||||
...Frag
|
||||
}
|
||||
fragment Frag on TestType @if:false {
|
||||
b
|
||||
}
|
||||
';
|
||||
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q));
|
||||
|
||||
// if true includes fragment
|
||||
$q = '
|
||||
query Q {
|
||||
a
|
||||
...Frag
|
||||
}
|
||||
fragment Frag on TestType @if:true {
|
||||
b
|
||||
}
|
||||
';
|
||||
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($q));
|
||||
|
||||
// unless false includes fragment
|
||||
$q = '
|
||||
query Q {
|
||||
a
|
||||
...Frag
|
||||
}
|
||||
fragment Frag on TestType @unless:false {
|
||||
b
|
||||
}
|
||||
';
|
||||
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($q));
|
||||
|
||||
// unless true omits fragment
|
||||
$q = '
|
||||
query Q {
|
||||
a
|
||||
...Frag
|
||||
}
|
||||
fragment Frag on TestType @unless:true {
|
||||
b
|
||||
}
|
||||
';
|
||||
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private static $schema;
|
||||
|
||||
private static $data;
|
||||
|
||||
private static function getSchema()
|
||||
{
|
||||
return self::$schema ?: (self::$schema = new Schema(new ObjectType([
|
||||
'name' => 'TestType',
|
||||
'fields' => [
|
||||
'a' => ['type' => Type::string()],
|
||||
'b' => ['type' => Type::string()]
|
||||
]
|
||||
])));
|
||||
}
|
||||
|
||||
private static function getData()
|
||||
{
|
||||
return self::$data ?: (self::$data = [
|
||||
'a' => function() { return 'a'; },
|
||||
'b' => function() { return 'b'; }
|
||||
]);
|
||||
}
|
||||
|
||||
private function executeTestQuery($doc)
|
||||
{
|
||||
return Executor::execute(self::getSchema(), self::getData(), Parser::parse($doc));
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user