Version 0.1

This commit is contained in:
vladar 2015-07-15 23:05:46 +06:00
parent 85bd2efaf7
commit 20c482ce2f
139 changed files with 18852 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
### Intellij ###
.idea/

32
composer.json Normal file
View 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
View 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;
}
}
}
}
}

View 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
View 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 fields 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
View 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
View 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
View 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)];
}
}
}

View 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;
}

View 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;
}

View File

@ -0,0 +1,13 @@
<?php
namespace GraphQL\Language\AST;
class BooleanValue extends Node implements Value
{
public $kind = Node::BOOLEAN;
/**
* @var string
*/
public $value;
}

View File

@ -0,0 +1,10 @@
<?php
namespace GraphQL\Language\AST;
interface Definition
{
/**
* export type Definition = OperationDefinition
* | FragmentDefinition
*/
}

View 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;
}

View File

@ -0,0 +1,12 @@
<?php
namespace GraphQL\Language\AST;
class Document extends Node
{
public $kind = Node::DOCUMENT;
/**
* @var array<Definition>
*/
public $definitions;
}

View File

@ -0,0 +1,12 @@
<?php
namespace GraphQL\Language\AST;
class EnumValue extends Node implements Value
{
public $kind = Node::ENUM;
/**
* @var string
*/
public $value;
}

View 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;
}

View File

@ -0,0 +1,13 @@
<?php
namespace GraphQL\Language\AST;
class FloatValue extends Node implements Value
{
public $kind = Node::FLOAT;
/**
* @var string
*/
public $value;
}

View 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;
}

View 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;
}

View 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;
}

View File

@ -0,0 +1,13 @@
<?php
namespace GraphQL\Language\AST;
class IntValue extends Node implements Value
{
public $kind = Node::INT;
/**
* @var string
*/
public $value;
}

View 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;
}

View 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
View 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
View 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);
}
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View File

@ -0,0 +1,9 @@
<?php
namespace GraphQL\Language\AST;
interface Selection
{
/**
* export type Selection = Field | FragmentSpread | InlineFragment
*/
}

View File

@ -0,0 +1,12 @@
<?php
namespace GraphQL\Language\AST;
class SelectionSet extends Node
{
public $kind = Node::SELECTION_SET;
/**
* @var array<Selection>
*/
public $selections;
}

View 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
View File

@ -0,0 +1,13 @@
<?php
namespace GraphQL\Language\AST;
interface Type
{
/**
export type Type = Name
| ListType
| NonNullType
*/
}

View File

@ -0,0 +1,16 @@
<?php
namespace GraphQL\Language\AST;
interface Value
{
/**
export type Value = Variable
| IntValue
| FloatValue
| StringValue
| BooleanValue
| EnumValue
| ArrayValue
| ObjectValue
*/
}

View File

@ -0,0 +1,12 @@
<?php
namespace GraphQL\Language\AST;
class Variable extends Node
{
public $kind = Node::VARIABLE;
/**
* @var Name
*/
public $name;
}

View 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;
}

View 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
View 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
View 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
View 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
View 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);
}
}

View 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
View 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
View 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
View 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;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace GraphQL\Type\Definition;
interface AbstractType
{
/*
export type GraphQLAbstractType =
GraphQLInterfaceType |
GraphQLUnionType;
*/
/**
* @return array<ObjectType>
*/
public function getPossibleTypes();
}

View 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;
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace GraphQL\Type\Definition;
interface CompositeType
{
/*
export type GraphQLCompositeType =
GraphQLObjectType |
GraphQLInterfaceType |
GraphQLUnionType;
*/
}

View 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;
}
}

View 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;
}
}
}

View 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;
}
}

View 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;
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace GraphQL\Type\Definition;
interface InputType
{
/*
export type GraphQLInputType =
GraphQLScalarType |
GraphQLEnumType |
GraphQLInputObjectType |
GraphQLList |
GraphQLNonNull;
*/
}

View 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;
}
}

View 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);
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace GraphQL\Type\Definition;
interface LeafType
{
/*
export type GraphQLLeafType =
GraphQLScalarType |
GraphQLEnumType;
*/
}

View 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);
}
}

View 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() . '!';
}
}

View 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;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace GraphQL\Type\Definition;
interface OutputType
{
/*
GraphQLScalarType |
GraphQLObjectType |
GraphQLInterfaceType |
GraphQLUnionType |
GraphQLEnumType |
GraphQLList |
GraphQLNonNull;
*/
}

View 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);
}

View 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;
}

View 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;
}
}

View 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;
}
}
}

View 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);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace GraphQL\Type\Definition;
interface UnmodifiedType
{
/*
export type GraphQLUnmodifiedType =
GraphQLScalarType |
GraphQLObjectType |
GraphQLInterfaceType |
GraphQLUnionType |
GraphQLEnumType |
GraphQLInputObjectType;
*/
}

View File

@ -0,0 +1,12 @@
<?php
namespace GraphQL\Type\Definition;
interface WrappingType
{
/*
NonNullType
ListOfType
*/
public function getWrappedType();
}

406
src/Type/Introspection.php Normal file
View 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'];
}
}

View 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
View File

@ -0,0 +1,9 @@
<?php
namespace GraphQL;
class Types
{
public static function Int()
{
}
}

121
src/Utils.php Normal file
View 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
View 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
View 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;
}
}
}

View 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
View 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;
}
}

View 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;
}
];
}
}

View 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;
}
];
}
}

View 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]
);
}
}
}
];
}
}

View 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]
);
}
}
];
}
}

View 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]
);
}
}
}
];
}
}

View 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]
);
}
}
];
}
}

View 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]
);
}
}
];
}
}

View 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]);
}
}
}
];
}
}

View 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;
}
}

View 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;
}
];
}
}

View 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
);
}
}
}
}

View 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;
}
];
}
}

View 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;
}
}

View 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;
}
}

View 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]
);
}
}
}
];
}
}

View 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);
}
}

View 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);
}
}

View 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();
}
}

View 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