diff --git a/src/Executor/DeprecatedMappingExecutor.php b/src/Executor/DeprecatedMappingExecutor.php deleted file mode 100644 index 24a09fd..0000000 --- a/src/Executor/DeprecatedMappingExecutor.php +++ /dev/null @@ -1,773 +0,0 @@ -operation, $rootValue); - } catch (Error $e) { - $exeContext->addError($e); - $data = null; - } - - return new ExecutionResult($data, $exeContext->errors); - } - - /** - * 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, Document $documentAst, $rootValue, $rawVariableValues, $operationName = null) - { - $errors = []; - $operations = []; - $fragments = []; - - foreach ($documentAst->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 (empty($operations[$opName])) { - throw new Error('Unknown operation named ' . $opName); - } - $operation = $operations[$opName]; - - $variableValues = Values::getVariableValues($schema, $operation->variableDefinitions ?: [], $rawVariableValues ?: []); - $exeContext = new ExecutionContext($schema, $fragments, $rootValue, $operation, $variableValues, $errors); - return $exeContext; - } - - /** - * Implements the "Evaluating operations" section of the spec. - */ - private static function executeOperation(ExecutionContext $exeContext, OperationDefinition $operation, $rootValue) - { - $type = self::getOperationRootType($exeContext->schema, $operation); - $fields = self::collectFields($exeContext, $type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject()); - - if ($operation->operation === 'mutation') { - $result = self::executeFieldsSerially($exeContext, $type, [$rootValue], $fields); - } else { - $result = self::executeFields($exeContext, $type, [$rootValue], $fields); - } - - return null === $result || $result === [] ? [] : $result[0]; - } - - - /** - * 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. - * - * @param ExecutionContext $exeContext - * @param ObjectType $parentType - * @param $sourceList - * @param $fields - * @return array - * @throws Error - * @throws \Exception - */ - private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $sourceList, $fields) - { - $results = []; - foreach ($fields as $responseName => $fieldASTs) { - self::resolveField($exeContext, $parentType, $sourceList, $fieldASTs, $responseName, $results); - } - - return $results; - } - - /** - * Implements the "Evaluating selection sets" section of the spec - * for "read" mode. - * @param ExecutionContext $exeContext - * @param ObjectType $parentType - * @param $sourceList - * @param $fields - * @return array - */ - private static function executeFields(ExecutionContext $exeContext, ObjectType $parentType, $sourceList, $fields) - { - // Native PHP doesn't support promises. - // Custom executor should be built for platforms like ReactPHP - return self::executeFieldsSerially($exeContext, $parentType, $sourceList, $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 the @include and @skip - * directives, where @skip has higher precedence than @include. - */ - private static function shouldIncludeNode(ExecutionContext $exeContext, $directives) - { - $skipDirective = Directive::skipDirective(); - $includeDirective = Directive::includeDirective(); - - /** @var \GraphQL\Language\AST\Directive $skipAST */ - $skipAST = $directives - ? Utils::find($directives, function(\GraphQL\Language\AST\Directive $directive) use ($skipDirective) { - return $directive->name->value === $skipDirective->name; - }) - : null; - - if ($skipAST) { - $argValues = Values::getArgumentValues($skipDirective->args, $skipAST->arguments, $exeContext->variableValues); - return empty($argValues['if']); - } - - /** @var \GraphQL\Language\AST\Directive $includeAST */ - $includeAST = $directives - ? Utils::find($directives, function(\GraphQL\Language\AST\Directive $directive) use ($includeDirective) { - return $directive->name->value === $includeDirective->name; - }) - : null; - - if ($includeAST) { - $argValues = Values::getArgumentValues($includeDirective->args, $includeAST->arguments, $exeContext->variableValues); - return !empty($argValues['if']); - } - - 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; - } - - /** - * Given list of parent type values returns corresponding list of field values - * - * In particular, this - * figures out the value that the field returns by calling its `resolve` or `map` function, - * then calls `completeValue` on each value to serialize scalars, or execute the sub-selection-set - * for objects. - * - * @param ExecutionContext $exeContext - * @param ObjectType $parentType - * @param $sourceValueList - * @param $fieldASTs - * @return array - * @throws Error - */ - private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $sourceValueList, $fieldASTs, $responseName, &$resolveResult) - { - $fieldAST = $fieldASTs[0]; - $fieldName = $fieldAST->name->value; - - $fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName); - - if (!$fieldDef) { - return ; - } - - $returnType = $fieldDef->getType(); - - // Build hash 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 List type. - $args = Values::getArgumentValues( - $fieldDef->args, - $fieldAST->arguments, - $exeContext->variableValues - ); - - // The resolve function's optional third argument is a collection of - // information about the current execution state. - $info = new ResolveInfo([ - 'fieldName' => $fieldName, - 'fieldASTs' => $fieldASTs, - 'returnType' => $returnType, - 'parentType' => $parentType, - 'schema' => $exeContext->schema, - 'fragments' => $exeContext->fragments, - 'rootValue' => $exeContext->rootValue, - 'operation' => $exeContext->operation, - 'variableValues' => $exeContext->variableValues, - ]); - - $mapFn = $fieldDef->mapFn; - - // If an error occurs while calling the field `map` or `resolve` function, ensure that - // it is wrapped as a GraphQLError with locations. Log this error and return - // null if allowed, otherwise throw the error so the parent field can handle - // it. - if ($mapFn) { - try { - $mapped = call_user_func($mapFn, $sourceValueList, $args, $info); - $validType = is_array($mapped) || ($mapped instanceof \Traversable && $mapped instanceof \Countable); - $mappedCount = count($mapped); - $sourceCount = count($sourceValueList); - - Utils::invariant( - $validType && count($mapped) === count($sourceValueList), - "Function `map` of $parentType.$fieldName is expected to return array or " . - "countable traversable with exact same number of items as list being mapped. ". - "Got '%s' with count '$mappedCount' against '$sourceCount' expected.", - Utils::getVariableType($mapped) - ); - - } catch (\Exception $error) { - $reportedError = Error::createLocatedError($error, $fieldASTs); - - if ($returnType instanceof NonNull) { - throw $reportedError; - } - - $exeContext->addError($reportedError); - return null; - } - - foreach ($mapped as $index => $value) { - $resolveResult[$index][$responseName] = self::completeValueCatchingError( - $exeContext, - $returnType, - $fieldASTs, - $info, - $value - ); - } - } else { - if (isset($fieldDef->resolveFn)) { - $resolveFn = $fieldDef->resolveFn; - } else if (isset($parentType->resolveFieldFn)) { - $resolveFn = $parentType->resolveFieldFn; - } else { - $resolveFn = self::$defaultResolveFn; - } - - foreach ($sourceValueList as $index => $value) { - try { - $resolved = call_user_func($resolveFn, $value, $args, $info); - } catch (\Exception $error) { - $reportedError = Error::createLocatedError($error, $fieldASTs); - - if ($returnType instanceof NonNull) { - throw $reportedError; - } - - $exeContext->addError($reportedError); - $resolved = null; - } - - $resolveResult[$index][$responseName] = self::completeValueCatchingError( - $exeContext, - $returnType, - $fieldASTs, - $info, - $resolved - ); - } - } - } - - - public static function completeValueCatchingError( - ExecutionContext $exeContext, - Type $returnType, - $fieldASTs, - ResolveInfo $info, - $result - ) - { - // If the field type is non-nullable, then it is resolved without any - // protection from errors. - if ($returnType instanceof NonNull) { - return self::completeValue($exeContext, $returnType, $fieldASTs, $info, $result); - } - - // Otherwise, error protection is applied, logging the error and resolving - // a null value for this field if one is encountered. - try { - return self::completeValue($exeContext, $returnType, $fieldASTs, $info, $result); - } catch (Error $err) { - $exeContext->addError($err); - return null; - } - } - - /** - * 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 `serialize` 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 completeValue(ExecutionContext $exeContext, Type $returnType,/* Array */ $fieldASTs, ResolveInfo $info, &$result) - { - // If field type is NonNull, complete for inner type, and throw field error - // if result is null. - if ($returnType instanceof NonNull) { - $completed = self::completeValue( - $exeContext, - $returnType->getWrappedType(), - $fieldASTs, - $info, - $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 Scalar or Enum, serialize to a valid value, returning - // null if serialization is not possible. - if ($returnType instanceof ScalarType || - $returnType instanceof EnumType) { - return $returnType->serialize($result); - } - - // If field type is List, and return type is Composite - complete by executing these fields with list value as parameter - if ($returnType instanceof ListOfType) { - $itemType = $returnType->getWrappedType(); - - Utils::invariant( - is_array($result) || $result instanceof \Traversable, - 'User Error: expected iterable, but did not find one.' - ); - - // For Object[]: - // Allow all object fields to process list value in it's `map` callback: - if ($itemType instanceof ObjectType) { - // Filter out nulls (as `map` doesn't expect it): - $list = []; - foreach ($result as $index => $item) { - if (null !== $item) { - $list[] = $item; - } - } - - $subFieldASTs = self::collectSubFields($exeContext, $itemType, $fieldASTs); - $mapped = self::executeFields($exeContext, $itemType, $list, $subFieldASTs); - - $i = 0; - $completed = []; - foreach ($result as $index => $item) { - if (null === $item) { - // Complete nulls separately - $completed[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item); - } else { - // Assuming same order of mapped values - $completed[] = $mapped[$i++]; - } - } - return $completed; - - } else if ($itemType instanceof AbstractType) { - - // Values sharded by ObjectType - $listPerObjectType = []; - - // Helper structures to restore ordering after resolve calls - $resultTypeMap = []; - $typeNameMap = []; - $cursors = []; - $copied = []; - - foreach ($result as $index => $item) { - $copied[$index] = $item; - - if (null !== $item) { - $objectType = $itemType->getObjectType($item, $info); - - if ($objectType && !$itemType->isPossibleType($objectType)) { - $exeContext->addError(new Error( - "Runtime Object type \"$objectType\" is not a possible type for \"$itemType\"." - )); - $copied[$index] = null; - } else { - $listPerObjectType[$objectType->name][] = $item; - $resultTypeMap[$index] = $objectType->name; - $typeNameMap[$objectType->name] = $objectType; - } - } - } - - $mapped = []; - foreach ($listPerObjectType as $typeName => $list) { - $objectType = $typeNameMap[$typeName]; - $subFieldASTs = self::collectSubFields($exeContext, $objectType, $fieldASTs); - $mapped[$typeName] = self::executeFields($exeContext, $objectType, $list, $subFieldASTs); - $cursors[$typeName] = 0; - } - - // Restore order: - $completed = []; - foreach ($copied as $index => $item) { - if (null === $item) { - // Complete nulls separately - $completed[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item); - } else { - $typeName = $resultTypeMap[$index]; - $completed[] = $mapped[$typeName][$cursors[$typeName]++]; - } - } - - return $completed; - } else { - - // For simple lists: - $tmp = []; - foreach ($result as $item) { - $tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item); - } - return $tmp; - } - - } - - if ($returnType instanceof ObjectType) { - $objectType = $returnType; - - } else if ($returnType instanceof AbstractType) { - $objectType = $returnType->getObjectType($result, $info); - - if ($objectType && !$returnType->isPossibleType($objectType)) { - throw new Error( - "Runtime Object type \"$objectType\" is not a possible type for \"$returnType\"." - ); - } - } else { - $objectType = null; - } - - if (!$objectType) { - return null; - } - - // If there is an isTypeOf predicate function, call it with the - // current result. If isTypeOf returns false, then raise an error rather - // than continuing execution. - if (false === $objectType->isTypeOf($result, $info)) { - throw new Error( - "Expected value of type $objectType but got: $result.", - $fieldASTs - ); - } - - // Collect sub-fields to execute to complete this value. - $subFieldASTs = self::collectSubFields($exeContext, $objectType, $fieldASTs); - $executed = self::executeFields($exeContext, $objectType, [$result], $subFieldASTs); - return isset($executed[0]) ? $executed[0] : null; - } - - /** - * @param ExecutionContext $exeContext - * @param ObjectType $objectType - * @param $fieldASTs - * @return \ArrayObject - */ - private static function collectSubFields(ExecutionContext $exeContext, ObjectType $objectType, $fieldASTs) - { - $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 $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, ResolveInfo $info) - { - $fieldName = $info->fieldName; - $property = null; - - if (is_array($source) || $source instanceof \ArrayAccess) { - if (isset($source[$fieldName])) { - $property = $source[$fieldName]; - } - } else if (is_object($source)) { - if (isset($source->{$fieldName})) { - $property = $source->{$fieldName}; - } - } - - return $property instanceof \Closure ? $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, $fieldName) - { - $schemaMetaFieldDef = Introspection::schemaMetaFieldDef(); - $typeMetaFieldDef = Introspection::typeMetaFieldDef(); - $typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef(); - - if ($fieldName === $schemaMetaFieldDef->name && $schema->getQueryType() === $parentType) { - return $schemaMetaFieldDef; - } else if ($fieldName === $typeMetaFieldDef->name && $schema->getQueryType() === $parentType) { - return $typeMetaFieldDef; - } else if ($fieldName === $typeNameMetaFieldDef->name) { - return $typeNameMetaFieldDef; - } - - $tmp = $parentType->getFields(); - return isset($tmp[$fieldName]) ? $tmp[$fieldName] : null; - } -} diff --git a/src/Executor/ExecutionContext.php b/src/Executor/ExecutionContext.php index 6664eee..0566582 100644 --- a/src/Executor/ExecutionContext.php +++ b/src/Executor/ExecutionContext.php @@ -24,10 +24,15 @@ class ExecutionContext public $fragments; /** - * @var + * @var mixed */ public $rootValue; + /** + * @var mixed + */ + public $contextValue; + /** * @var OperationDefinition */ @@ -48,11 +53,12 @@ class ExecutionContext */ public $memoized = []; - public function __construct($schema, $fragments, $root, $operation, $variables, $errors) + public function __construct($schema, $fragments, $root, $contextValue, $operation, $variables, $errors) { $this->schema = $schema; $this->fragments = $fragments; $this->rootValue = $root; + $this->contextValue = $contextValue; $this->operation = $operation; $this->variableValues = $variables; $this->errors = $errors ?: []; diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index ffa7714..b45eba2 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -13,14 +13,12 @@ use GraphQL\Type\Definition\AbstractType; 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\ResolveInfo; use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; -use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Introspection; use GraphQL\Utils; @@ -65,11 +63,12 @@ class Executor * @param Schema $schema * @param Document $ast * @param $rootValue + * @param $contextValue * @param array|\ArrayAccess $variableValues * @param null $operationName * @return ExecutionResult */ - public static function execute(Schema $schema, Document $ast, $rootValue = null, $variableValues = null, $operationName = null) + public static function execute(Schema $schema, Document $ast, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null) { if (!self::$UNDEFINED) { self::$UNDEFINED = new \stdClass(); @@ -88,7 +87,7 @@ class Executor ); } - $exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $variableValues, $operationName); + $exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $contextValue, $variableValues, $operationName); try { $data = self::executeOperation($exeContext, $exeContext->operation, $rootValue); @@ -104,37 +103,58 @@ class Executor * 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, Document $documentAst, $rootValue, $rawVariableValues, $operationName = null) + private static function buildExecutionContext( + Schema $schema, + Document $documentAst, + $rootValue, + $contextValue, + $rawVariableValues, + $operationName = null + ) { $errors = []; - $operations = []; $fragments = []; + $operation = null; - foreach ($documentAst->definitions as $statement) { - switch ($statement->kind) { + foreach ($documentAst->definitions as $definition) { + switch ($definition->kind) { case Node::OPERATION_DEFINITION: - $operations[$statement->name ? $statement->name->value : ''] = $statement; + if (!$operationName && $operation) { + throw new Error( + 'Must provide operation name if query contains multiple operations.' + ); + } + if (!$operationName || + (isset($definition->name) && $definition->name->value === $operationName)) { + $operation = $definition; + } break; case Node::FRAGMENT_DEFINITION: - $fragments[$statement->name->value] = $statement; + $fragments[$definition->name->value] = $definition; break; + default: + throw new Error( + "GraphQL cannot execute a request containing a {$definition->kind}.", + [$definition] + ); } } - if (!$operationName && count($operations) !== 1) { - throw new Error( - 'Must provide operation name if query contains multiple operations.' - ); + if (!$operation) { + if ($operationName) { + throw new Error("Unknown operation named \"$operationName\"."); + } else { + throw new Error('Must provide an operation.'); + } } - $opName = $operationName ?: key($operations); - if (empty($operations[$opName])) { - throw new Error('Unknown operation named ' . $opName); - } - $operation = $operations[$opName]; + $variableValues = Values::getVariableValues( + $schema, + $operation->variableDefinitions ?: [], + $rawVariableValues ?: [] + ); - $variableValues = Values::getVariableValues($schema, $operation->variableDefinitions ?: [], $rawVariableValues ?: []); - $exeContext = new ExecutionContext($schema, $fragments, $rootValue, $operation, $variableValues, $errors); + $exeContext = new ExecutionContext($schema, $fragments, $rootValue, $contextValue, $operation, $variableValues, $errors); return $exeContext; } @@ -218,18 +238,21 @@ class Executor * Given a selectionSet, adds all of the fields in that selection to * the passed in map of fields, and returns it at the end. * + * CollectFields requires the "runtime type" of an object. For a field which + * returns and Interface or Union type, the "runtime type" will be the actual + * Object type returned by that field. + * * @return \ArrayObject */ private static function collectFields( ExecutionContext $exeContext, - ObjectType $type, + ObjectType $runtimeType, SelectionSet $selectionSet, $fields, $visitedFragmentNames ) { - for ($i = 0; $i < count($selectionSet->selections); $i++) { - $selection = $selectionSet->selections[$i]; + foreach ($selectionSet->selections as $selection) { switch ($selection->kind) { case Node::FIELD: if (!self::shouldIncludeNode($exeContext, $selection->directives)) { @@ -243,13 +266,13 @@ class Executor break; case Node::INLINE_FRAGMENT: if (!self::shouldIncludeNode($exeContext, $selection->directives) || - !self::doesFragmentConditionMatch($exeContext, $selection, $type) + !self::doesFragmentConditionMatch($exeContext, $selection, $runtimeType) ) { continue; } self::collectFields( $exeContext, - $type, + $runtimeType, $selection->selectionSet, $fields, $visitedFragmentNames @@ -264,15 +287,12 @@ class Executor /** @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) - ) { + if (!$fragment || !self::doesFragmentConditionMatch($exeContext, $fragment, $runtimeType)) { continue; } self::collectFields( $exeContext, - $type, + $runtimeType, $fragment->selectionSet, $fields, $visitedFragmentNames @@ -301,7 +321,9 @@ class Executor if ($skipAST) { $argValues = Values::getArgumentValues($skipDirective->args, $skipAST->arguments, $exeContext->variableValues); - return empty($argValues['if']); + if (isset($argValues['if']) && $argValues['if'] === true) { + return false; + } } /** @var \GraphQL\Language\AST\Directive $includeAST */ @@ -313,7 +335,9 @@ class Executor if ($includeAST) { $argValues = Values::getArgumentValues($includeDirective->args, $includeAST->arguments, $exeContext->variableValues); - return !empty($argValues['if']); + if (isset($argValues['if']) && $argValues['if'] === false) { + return false; + } } return true; @@ -334,10 +358,8 @@ class Executor if ($conditionalType === $type) { return true; } - if ($conditionalType instanceof InterfaceType || - $conditionalType instanceof UnionType - ) { - return $conditionalType->isPossibleType($type); + if ($conditionalType instanceof AbstractType) { + return $exeContext->schema->isPossibleType($conditionalType, $type); } return false; } @@ -401,9 +423,14 @@ class Executor $resolveFn = self::$defaultResolveFn; } + // The resolve function's optional third argument is a context value that + // is provided to every resolve function within an execution. It is commonly + // used to represent an authenticated user, or request-specific caches. + $context = $exeContext->contextValue; + // Get the resolve function, regardless of if its result is normal // or abrupt (error). - $result = self::resolveOrError($resolveFn, $source, $args, $info); + $result = self::resolveOrError($resolveFn, $source, $args, $context, $info); $result = self::completeValueCatchingError( $exeContext, @@ -418,15 +445,17 @@ class Executor // Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` // function. Returns the result of resolveFn or the abrupt-return Error object. - private static function resolveOrError($resolveFn, $source, $args, $info) + private static function resolveOrError($resolveFn, $source, $args, $context, $info) { try { - return call_user_func($resolveFn, $source, $args, $info); + return call_user_func($resolveFn, $source, $args, $context, $info); } catch (\Exception $error) { return $error; } } + // This is a small wrapper around completeValue which detects and logs errors + // in the execution context. public static function completeValueCatchingError( ExecutionContext $exeContext, Type $returnType, @@ -468,10 +497,22 @@ class Executor * value of the type by calling the `serialize` method of GraphQL type * definition. * + * If the field is an abstract type, determine the runtime type of the value + * and then complete based on that type + * * Otherwise, the field type expects a sub-selection set, and will complete the * value by evaluating all sub-selections. + * + * @param ExecutionContext $exeContext + * @param Type $returnType + * @param Field[] $fieldASTs + * @param ResolveInfo $info + * @param $result + * @return array|null + * @throws Error + * @throws \Exception */ - private static function completeValue(ExecutionContext $exeContext, Type $returnType,/* Array */ $fieldASTs, ResolveInfo $info, &$result) + private static function completeValue(ExecutionContext $exeContext, Type $returnType, $fieldASTs, ResolveInfo $info, &$result) { if ($result instanceof \Exception) { throw Error::createLocatedError($result, $fieldASTs); @@ -489,7 +530,7 @@ class Executor ); if ($completed === null) { throw new Error( - 'Cannot return null for non-nullable type.', + 'Cannot return null for non-nullable field ' . $info->parentType . '.' . $info->fieldName . '.', $fieldASTs instanceof \ArrayObject ? $fieldASTs->getArrayCopy() : $fieldASTs ); } @@ -503,81 +544,26 @@ class Executor // If field type is List, complete each item in the list with the inner type if ($returnType instanceof ListOfType) { - $itemType = $returnType->getWrappedType(); - Utils::invariant( - is_array($result) || $result instanceof \Traversable, - 'User Error: expected iterable, but did not find one.' - ); - - $tmp = []; - foreach ($result as $item) { - $tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item); - } - return $tmp; + return self::completeListValue($exeContext, $returnType, $fieldASTs, $info, $result); } // If field type is Scalar or Enum, serialize to a valid value, returning // null if serialization is not possible. if ($returnType instanceof ScalarType || $returnType instanceof EnumType) { - Utils::invariant(method_exists($returnType, 'serialize'), 'Missing serialize method on type'); - return $returnType->serialize($result); + return self::completeLeafValue($returnType, $result); + } + + if ($returnType instanceof AbstractType) { + return self::completeAbstractValue($exeContext, $returnType, $fieldASTs, $info, $result); } // Field type must be Object, Interface or Union and expect sub-selections. if ($returnType instanceof ObjectType) { - $runtimeType = $returnType; - } else if ($returnType instanceof AbstractType) { - $runtimeType = $returnType->getObjectType($result, $info); - - if ($runtimeType && !$returnType->isPossibleType($runtimeType)) { - throw new Error( - "Runtime Object type \"$runtimeType\" is not a possible type for \"$returnType\"." - ); - } - } else { - $runtimeType = null; + return self::completeObjectValue($exeContext, $returnType, $fieldASTs, $info, $result); } - if (!$runtimeType) { - return null; - } - - // If there is an isTypeOf predicate function, call it with the - // current result. If isTypeOf returns false, then raise an error rather - // than continuing execution. - if (false === $runtimeType->isTypeOf($result, $info)) { - throw new Error( - "Expected value of type $runtimeType but got: " . Utils::getVariableType($result), - $fieldASTs - ); - } - - // Collect sub-fields to execute to complete this value. - $subFieldASTs = new \ArrayObject(); - $visitedFragmentNames = new \ArrayObject(); - for ($i = 0; $i < count($fieldASTs); $i++) { - // Get memoized value if it exists - $uid = self::getFieldUid($fieldASTs[$i], $runtimeType); - if (isset($exeContext->memoized['collectSubFields'][$uid])) { - $subFieldASTs = $exeContext->memoized['collectSubFields'][$uid]; - } - else { - $selectionSet = $fieldASTs[$i]->selectionSet; - if ($selectionSet) { - $subFieldASTs = self::collectFields( - $exeContext, - $runtimeType, - $selectionSet, - $subFieldASTs, - $visitedFragmentNames - ); - $exeContext->memoized['collectSubFields'][$uid] = $subFieldASTs; - } - } - } - - return self::executeFields($exeContext, $runtimeType, $result, $subFieldASTs); + throw new Error("Cannot complete value of unexpected type \"{$returnType}\"."); } @@ -587,7 +573,7 @@ class Executor * 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, ResolveInfo $info) + public static function defaultResolveFn($source, $args, $context, ResolveInfo $info) { $fieldName = $info->fieldName; $property = null; @@ -644,4 +630,135 @@ class Executor { return $fieldAST->loc->start . '-' . $fieldAST->loc->end . '-' . $fieldType->name; } + + /** + * Complete a value of an abstract type by determining the runtime object type + * of that value, then complete the value for that type. + * + * @param ExecutionContext $exeContext + * @param AbstractType $returnType + * @param $fieldASTs + * @param ResolveInfo $info + * @param $result + * @return mixed + * @throws Error + */ + private static function completeAbstractValue(ExecutionContext $exeContext, AbstractType $returnType, $fieldASTs, ResolveInfo $info, &$result) + { + $resolveType = $returnType->getResolveTypeFn(); + + $runtimeType = $resolveType ? + call_user_func($resolveType, $result, $exeContext->contextValue, $info) : + Type::getTypeOf($result, $exeContext->contextValue, $info, $returnType); + + if (!($runtimeType instanceof ObjectType)) { + throw new Error( + "Abstract type {$returnType} must resolve to an Object type at runtime " . + "for field {$info->parentType}.{$info->fieldName} with value \"" . print_r($result, true) . "\"," . + "received \"$runtimeType\".", + $fieldASTs + ); + } + + if (!$exeContext->schema->isPossibleType($returnType, $runtimeType)) { + throw new Error( + "Runtime Object type \"$runtimeType\" is not a possible type for \"$returnType\".", + $fieldASTs + ); + } + return self::completeObjectValue($exeContext, $runtimeType, $fieldASTs, $info, $result); + } + + /** + * Complete a list value by completing each item in the list with the + * inner type + * + * @param ExecutionContext $exeContext + * @param ListOfType $returnType + * @param $fieldASTs + * @param ResolveInfo $info + * @param $result + * @return array + * @throws \Exception + */ + private static function completeListValue(ExecutionContext $exeContext, ListOfType $returnType, $fieldASTs, ResolveInfo $info, &$result) + { + $itemType = $returnType->getWrappedType(); + Utils::invariant( + is_array($result) || $result instanceof \Traversable, + 'User Error: expected iterable, but did not find one for field ' . $info->parentType . '.' . $info->fieldName . '.' + ); + + $tmp = []; + foreach ($result as $item) { + $tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item); + } + return $tmp; + } + + /** + * Complete a Scalar or Enum by serializing to a valid value, returning + * null if serialization is not possible. + * + * @param Type $returnType + * @param $result + * @return mixed + * @throws \Exception + */ + private static function completeLeafValue(Type $returnType, &$result) + { + Utils::invariant(method_exists($returnType, 'serialize'), 'Missing serialize method on type'); + return $returnType->serialize($result); + } + + /** + * Complete an Object value by executing all sub-selections. + * + * @param ExecutionContext $exeContext + * @param ObjectType $returnType + * @param $fieldASTs + * @param ResolveInfo $info + * @param $result + * @return array + * @throws Error + */ + private static function completeObjectValue(ExecutionContext $exeContext, ObjectType $returnType, $fieldASTs, ResolveInfo $info, &$result) + { + // If there is an isTypeOf predicate function, call it with the + // current result. If isTypeOf returns false, then raise an error rather + // than continuing execution. + if (false === $returnType->isTypeOf($result, $exeContext->contextValue, $info)) { + throw new Error( + "Expected value of type $returnType but got: " . Utils::getVariableType($result), + $fieldASTs + ); + } + + // Collect sub-fields to execute to complete this value. + $subFieldASTs = new \ArrayObject(); + $visitedFragmentNames = new \ArrayObject(); + + $fieldsCount = count($fieldASTs); + for ($i = 0; $i < $fieldsCount; $i++) { + // Get memoized value if it exists + $uid = self::getFieldUid($fieldASTs[$i], $returnType); + if (isset($exeContext->memoized['collectSubFields'][$uid])) { + $subFieldASTs = $exeContext->memoized['collectSubFields'][$uid]; + } else { + $selectionSet = $fieldASTs[$i]->selectionSet; + if ($selectionSet) { + $subFieldASTs = self::collectFields( + $exeContext, + $returnType, + $selectionSet, + $subFieldASTs, + $visitedFragmentNames + ); + $exeContext->memoized['collectSubFields'][$uid] = $subFieldASTs; + } + } + } + + return self::executeFields($exeContext, $returnType, $result, $subFieldASTs); + } } diff --git a/src/Executor/Values.php b/src/Executor/Values.php index 5c57c35..f4675a2 100644 --- a/src/Executor/Values.php +++ b/src/Executor/Values.php @@ -158,16 +158,14 @@ class Values if ($type instanceof ListOfType) { $itemType = $type->getWrappedType(); if (is_array($value)) { - return array_reduce( - $value, - function ($acc, $item, $index) use ($itemType) { - $errors = self::isValidPHPValue($item, $itemType); - return array_merge($acc, Utils::map($errors, function ($error) use ($index) { - return "In element #$index: $error"; - })); - }, - [] - ); + $tmp = []; + foreach ($value as $index => $item) { + $errors = self::isValidPHPValue($item, $itemType); + $tmp = array_merge($tmp, Utils::map($errors, function ($error) use ($index) { + return "In element #$index: $error"; + })); + } + return $tmp; } return self::isValidPHPValue($value, $itemType); } @@ -190,7 +188,7 @@ class Values // Ensure every defined field is valid. foreach ($fields as $fieldName => $tmp) { - $newErrors = self::isValidPHPValue($value[$fieldName], $fields[$fieldName]->getType()); + $newErrors = self::isValidPHPValue(isset($value[$fieldName]) ? $value[$fieldName] : null, $fields[$fieldName]->getType()); $errors = array_merge( $errors, Utils::map($newErrors, function ($error) use ($fieldName) { diff --git a/src/GraphQL.php b/src/GraphQL.php index 6ad0ec8..4e25181 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -3,6 +3,7 @@ namespace GraphQL; use GraphQL\Executor\ExecutionResult; use GraphQL\Executor\Executor; +use GraphQL\Language\AST\Document; use GraphQL\Language\Parser; use GraphQL\Language\Source; use GraphQL\Validator\DocumentValidator; @@ -18,9 +19,9 @@ class GraphQL * @param string|null $operationName * @return array */ - public static function execute(Schema $schema, $requestString, $rootValue = null, $variableValues = null, $operationName = null) + public static function execute(Schema $schema, $requestString, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null) { - return self::executeAndReturnResult($schema, $requestString, $rootValue, $variableValues, $operationName)->toArray(); + return self::executeAndReturnResult($schema, $requestString, $rootValue, $contextValue, $variableValues, $operationName)->toArray(); } /** @@ -31,11 +32,15 @@ class GraphQL * @param null $operationName * @return array|ExecutionResult */ - public static function executeAndReturnResult(Schema $schema, $requestString, $rootValue = null, $variableValues = null, $operationName = null) + public static function executeAndReturnResult(Schema $schema, $requestString, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null) { try { - $source = new Source($requestString ?: '', 'GraphQL request'); - $documentAST = Parser::parse($source); + if ($requestString instanceof Document) { + $documentAST = $requestString; + } else { + $source = new Source($requestString ?: '', 'GraphQL request'); + $documentAST = Parser::parse($source); + } /** @var QueryComplexity $queryComplexity */ $queryComplexity = DocumentValidator::getRule('QueryComplexity'); @@ -46,7 +51,7 @@ class GraphQL if (!empty($validationErrors)) { return new ExecutionResult(null, $validationErrors); } else { - return Executor::execute($schema, $documentAST, $rootValue, $variableValues, $operationName); + return Executor::execute($schema, $documentAST, $rootValue, $contextValue, $variableValues, $operationName); } } catch (Error $e) { return new ExecutionResult(null, [$e]); diff --git a/src/Schema.php b/src/Schema.php index 153d337..8431bc5 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -3,18 +3,13 @@ namespace GraphQL; use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\Directive; -use GraphQL\Type\Definition\FieldArgument; -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\Type; use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Definition\WrappingType; use GraphQL\Type\Introspection; -use GraphQL\Utils\TypeInfo; class Schema { @@ -79,20 +74,44 @@ class Schema protected function _init(array $config) { - Utils::invariant(isset($config['query']) || isset($config['mutation']), "Either query or mutation type must be set"); - $config += [ 'query' => null, 'mutation' => null, 'subscription' => null, + 'types' => [], 'directives' => [], 'validate' => true ]; + Utils::invariant( + $config['query'] instanceof ObjectType, + "Schema query must be Object Type but got: " . Utils::getVariableType($config['query']) + ); + $this->_queryType = $config['query']; + + Utils::invariant( + !$config['mutation'] || $config['mutation'] instanceof ObjectType, + "Schema mutation must be Object Type if provided but got: " . Utils::getVariableType($config['mutation']) + ); $this->_mutationType = $config['mutation']; + + Utils::invariant( + !$config['subscription'] || $config['subscription'] instanceof ObjectType, + "Schema subscription must be Object Type if provided but got: " . Utils::getVariableType($config['subscription']) + ); $this->_subscriptionType = $config['subscription']; + Utils::invariant( + !$config['types'] || is_array($config['types']), + "Schema types must be Array if provided but got: " . Utils::getVariableType($config['types']) + ); + + Utils::invariant( + !$config['directives'] || (is_array($config['directives']) && Utils::every($config['directives'], function($d) {return $d instanceof Directive;})), + "Schema directives must be Directive[] if provided but got " . Utils::getVariableType($config['directives']) + ); + $this->_directives = array_merge($config['directives'], [ Directive::includeDirective(), Directive::skipDirective() @@ -109,11 +128,10 @@ class Schema $initialTypes = array_merge($initialTypes, $config['types']); } - $map = []; foreach ($initialTypes as $type) { - $this->_extractTypes($type, $map); + $this->_extractTypes($type); } - $this->_typeMap = $map + Type::getInternalTypes(); + $this->_typeMap += Type::getInternalTypes(); // Keep track of all implementations by interface name. $this->_implementations = []; @@ -124,86 +142,6 @@ class Schema } } } - - if ($config['validate']) { - $this->validate(); - } - } - - /** - * Additionaly validate schema for integrity - */ - public function validate() - { - // Enforce correct interface implementations - foreach ($this->_typeMap as $typeName => $type) { - if ($type instanceof ObjectType) { - foreach ($type->getInterfaces() as $iface) { - $this->_assertObjectImplementsInterface($type, $iface); - } - } - } - } - - /** - * @param ObjectType $object - * @param InterfaceType $iface - * @throws \Exception - */ - protected function _assertObjectImplementsInterface(ObjectType $object, InterfaceType $iface) - { - $objectFieldMap = $object->getFields(); - $ifaceFieldMap = $iface->getFields(); - - foreach ($ifaceFieldMap as $fieldName => $ifaceField) { - Utils::invariant( - isset($objectFieldMap[$fieldName]), - "\"$iface\" expects field \"$fieldName\" but \"$object\" does not provide it" - ); - - /** @var $ifaceField FieldDefinition */ - /** @var $objectField FieldDefinition */ - $objectField = $objectFieldMap[$fieldName]; - - Utils::invariant( - TypeInfo::isTypeSubTypeOf($this, $objectField->getType(), $ifaceField->getType()), - "$iface.$fieldName expects type \"{$ifaceField->getType()}\" but " . - "$object.$fieldName provides type \"{$objectField->getType()}\"." - ); - - foreach ($ifaceField->args as $ifaceArg) { - /** @var $ifaceArg FieldArgument */ - /** @var $objectArg FieldArgument */ - $argName = $ifaceArg->name; - $objectArg = $objectField->getArg($argName); - - // Assert interface field arg exists on object field. - Utils::invariant( - $objectArg, - "$iface.$fieldName expects argument \"$argName\" but $object.$fieldName does not provide it." - ); - - // Assert interface field arg type matches object field arg type. - // (invariant) - Utils::invariant( - TypeInfo::isEqualType($ifaceArg->getType(), $objectArg->getType()), - "$iface.$fieldName($argName:) expects type \"{$ifaceArg->getType()}\" " . - "but $object.$fieldName($argName:) provides " . - "type \"{$objectArg->getType()}\"" - ); - - // Assert argument set invariance. - foreach ($objectField->args as $objectArg) { - $argName = $objectArg->name; - $ifaceArg = $ifaceField->getArg($argName); - Utils::invariant( - $ifaceArg, - "$iface.$fieldName does not define argument \"$argName\" but " . - "$object.$fieldName provides it." - ); - } - } - } } /** @@ -258,7 +196,7 @@ class Schema return $abstractType->getTypes(); } Utils::invariant($abstractType instanceof InterfaceType); - return $this->_implementations[$abstractType->name]; + return isset($this->_implementations[$abstractType->name]) ? $this->_implementations[$abstractType->name] : []; } /** @@ -305,24 +243,24 @@ class Schema return null; } - protected function _extractTypes($type, &$map) + protected function _extractTypes($type) { if (!$type) { - return $map; + return $this->_typeMap; } if ($type instanceof WrappingType) { - return $this->_extractTypes($type->getWrappedType(), $map); + return $this->_extractTypes($type->getWrappedType(true)); } - if (!empty($map[$type->name])) { + if (!empty($this->_typeMap[$type->name])) { Utils::invariant( - $map[$type->name] === $type, + $this->_typeMap[$type->name] === $type, "Schema must contain unique named types but contains multiple types named \"$type\"." ); - return $map; + return $this->_typeMap; } - $map[$type->name] = $type; + $this->_typeMap[$type->name] = $type; $nestedTypes = []; @@ -342,8 +280,8 @@ class Schema } } foreach ($nestedTypes as $type) { - $this->_extractTypes($type, $map); + $this->_extractTypes($type); } - return $map; + return $this->_typeMap; } } diff --git a/src/Type/Definition/AbstractType.php b/src/Type/Definition/AbstractType.php index 7b99cc1..e0f56c1 100644 --- a/src/Type/Definition/AbstractType.php +++ b/src/Type/Definition/AbstractType.php @@ -10,18 +10,7 @@ GraphQLInterfaceType | GraphQLUnionType; */ /** - * @return array + * @return callable|null */ - // public function getPossibleTypes(); - - /** - * @return ObjectType - */ - // public function getObjectType($value, ResolveInfo $info); - - /** - * @param Type $type - * @return bool - */ - // public function isPossibleType(Type $type); + public function getResolveTypeFn(); } diff --git a/src/Type/Definition/FieldDefinition.php b/src/Type/Definition/FieldDefinition.php index c17db33..928c78a 100644 --- a/src/Type/Definition/FieldDefinition.php +++ b/src/Type/Definition/FieldDefinition.php @@ -17,6 +17,9 @@ class FieldDefinition */ private $type; + /** + * @var OutputType + */ private $resolvedType; /** diff --git a/src/Type/Definition/InterfaceType.php b/src/Type/Definition/InterfaceType.php index dd5a6e2..f273a15 100644 --- a/src/Type/Definition/InterfaceType.php +++ b/src/Type/Definition/InterfaceType.php @@ -13,21 +13,6 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT public $description; - /** - * @var array - */ - private $_implementations = []; - - /** - * @var \Closure[] - */ - private static $_lazyLoadImplementations = []; - - /** - * @var {[typeName: string]: boolean} - */ - private $_possibleTypeNames; - /** * @var callback */ @@ -38,45 +23,6 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT */ public $config; - /** - * Queue the update of 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 - */ - public static function addImplementationToInterfaces(ObjectType $impl) - { - self::$_lazyLoadImplementations[] = function() use ($impl) { - /** @var self $interface */ - foreach ($impl->getInterfaces() as $interface) { - $interface->addImplementation($impl); - } - }; - } - - /** - * Process ImplementationToInterfaces Queue - */ - public static function loadImplementationToInterfaces() - { - foreach (self::$_lazyLoadImplementations as $lazyLoadImplementation) { - $lazyLoadImplementation(); - } - self::$_lazyLoadImplementations = []; - } - - /** - * Add a implemented object type to interface - * - * @param ObjectType $impl - */ - protected function addImplementation(ObjectType $impl) - { - $this->_implementations[] = $impl; - } - /** * InterfaceType constructor. * @param array $config @@ -89,7 +35,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT FieldDefinition::getDefinition(), Config::KEY_AS_NAME | Config::MAYBE_THUNK ), - 'resolveType' => Config::CALLBACK, // function($value, ResolveInfo $info) => ObjectType + 'resolveType' => Config::CALLBACK, // function($value, $context, ResolveInfo $info) => ObjectType 'description' => Config::STRING ]); @@ -128,38 +74,10 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT } /** - * @return array + * @return callable|null */ - public function getPossibleTypes() + public function getResolveTypeFn() { - return $this->_implementations; - } - - /** - * @param Type $type - * @return bool - */ - public function isPossibleType(Type $type) - { - $possibleTypeNames = $this->_possibleTypeNames; - if (null === $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 - * @param ResolveInfo $info - * @return Type|null - * @throws \Exception - */ - public function getObjectType($value, ResolveInfo $info) - { - $resolver = $this->_resolveTypeFn; - return $resolver ? call_user_func($resolver, $value, $info) : Type::getTypeOf($value, $info, $this); + return $this->_resolveTypeFn; } } diff --git a/src/Type/Definition/ObjectType.php b/src/Type/Definition/ObjectType.php index 675bcce..1c04baa 100644 --- a/src/Type/Definition/ObjectType.php +++ b/src/Type/Definition/ObjectType.php @@ -95,10 +95,6 @@ class ObjectType extends Type implements OutputType, CompositeType $this->resolveFieldFn = isset($config['resolveField']) ? $config['resolveField'] : null; $this->_isTypeOf = isset($config['isTypeOf']) ? $config['isTypeOf'] : null; $this->config = $config; - - if (isset($config['interfaces'])) { - InterfaceType::addImplementationToInterfaces($this); - } } /** @@ -152,10 +148,11 @@ class ObjectType extends Type implements OutputType, CompositeType /** * @param $value + * @param $context * @return bool|null */ - public function isTypeOf($value, ResolveInfo $info) + public function isTypeOf($value, $context, ResolveInfo $info) { - return isset($this->_isTypeOf) ? call_user_func($this->_isTypeOf, $value, $info) : null; + return isset($this->_isTypeOf) ? call_user_func($this->_isTypeOf, $value, $context, $info) : null; } } diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index 123bb5e..8e92f42 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -185,30 +185,18 @@ GraphQLNonNull; /** * @param $value + * @param mixed $context * @param AbstractType $abstractType * @return Type * @throws \Exception */ - public static function getTypeOf($value, ResolveInfo $info, AbstractType $abstractType) + public static function getTypeOf($value, $context, ResolveInfo $info, AbstractType $abstractType) { - $possibleTypes = $abstractType->getPossibleTypes(); + $possibleTypes = $info->schema->getPossibleTypes($abstractType); - for ($i = 0; $i < count($possibleTypes); $i++) { + foreach ($possibleTypes as $type) { /** @var ObjectType $type */ - $type = $possibleTypes[$i]; - $isTypeOf = $type->isTypeOf($value, $info); - - 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 ' . - 'getObjectType and Object Type ' . $type->name . ' does not implement ' . - 'isTypeOf. There is no way to determine if a value is of this type.' - ); - } - - if ($isTypeOf) { + if ($type->isTypeOf($value, $context, $info)) { return $type; } } diff --git a/src/Type/Definition/UnionType.php b/src/Type/Definition/UnionType.php index 9fcdac9..39e2caa 100644 --- a/src/Type/Definition/UnionType.php +++ b/src/Type/Definition/UnionType.php @@ -18,7 +18,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType /** * @var callback */ - private $_resolveType; + private $_resolveTypeFn; /** * @var array @@ -44,7 +44,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType $this->name = $config['name']; $this->description = isset($config['description']) ? $config['description'] : null; $this->_types = $config['types']; - $this->_resolveType = isset($config['resolveType']) ? $config['resolveType'] : null; + $this->_resolveTypeFn = isset($config['resolveType']) ? $config['resolveType'] : null; $this->_config = $config; } @@ -85,15 +85,10 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType } /** - * @param ObjectType $value - * @param ResolveInfo $info - * - * @return Type - * @throws \Exception + * @return callable|null */ - public function getObjectType($value, ResolveInfo $info) + public function getResolveTypeFn() { - $resolver = $this->_resolveType; - return $resolver ? call_user_func($resolver, $value, $info) : Type::getTypeOf($value, $info, $this); + return $this->_resolveTypeFn; } } diff --git a/src/Type/Introspection.php b/src/Type/Introspection.php index d670712..7fec3bb 100644 --- a/src/Type/Introspection.php +++ b/src/Type/Introspection.php @@ -426,7 +426,7 @@ EOD; ], 'possibleTypes' => [ 'type' => Type::listOf(Type::nonNull([__CLASS__, '_type'])), - 'resolve' => function ($type, $args, ResolveInfo $info) { + 'resolve' => function ($type, $args, $context, ResolveInfo $info) { if ($type instanceof InterfaceType || $type instanceof UnionType) { return $info->schema->getPossibleTypes($type); } @@ -635,6 +635,7 @@ EOD; 'resolve' => function ( $source, $args, + $context, ResolveInfo $info ) { return $info->schema; @@ -654,7 +655,7 @@ EOD; 'args' => [ ['name' => 'name', 'type' => Type::nonNull(Type::string())] ], - 'resolve' => function ($source, $args, ResolveInfo $info) { + 'resolve' => function ($source, $args, $context, ResolveInfo $info) { return $info->schema->getType($args['name']); } ]); @@ -673,6 +674,7 @@ EOD; 'resolve' => function ( $source, $args, + $context, ResolveInfo $info ) { return $info->parentType->name; diff --git a/src/Type/SchemaValidator.php b/src/Type/SchemaValidator.php index adc8ffb..a1a45a1 100644 --- a/src/Type/SchemaValidator.php +++ b/src/Type/SchemaValidator.php @@ -3,10 +3,13 @@ namespace GraphQL\Type; use GraphQL\Error; use GraphQL\Schema; +use GraphQL\Type\Definition\FieldArgument; +use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; +use GraphQL\Utils; class SchemaValidator { @@ -19,7 +22,8 @@ class SchemaValidator self::noInputTypesAsOutputFieldsRule(), self::noOutputTypesAsInputArgsRule(), self::typesInterfacesMustShowThemAsPossibleRule(), - self::interfacePossibleTypesMustImplementTheInterfaceRule() + self::interfacePossibleTypesMustImplementTheInterfaceRule(), + self::interfacesAreCorrectlyImplemented() ]; } return self::$rules; @@ -30,7 +34,7 @@ class SchemaValidator 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 new Error("Schema $operation must be Object Type but got: $type."); } return null; }; @@ -56,6 +60,14 @@ class SchemaValidator } } + $subscriptionType = $schema->getSubscriptionType(); + if ($subscriptionType) { + $subscriptionError = $operationMayNotBeInputType($subscriptionType, 'subscription'); + if ($subscriptionError !== null) { + $errors[] = $subscriptionError; + } + } + foreach ($typeMap as $typeName => $type) { if ($type instanceof ObjectType || $type instanceof InterfaceType) { $fields = $type->getFields(); @@ -152,6 +164,91 @@ class SchemaValidator }; } + // Enforce correct interface implementations + public static function interfacesAreCorrectlyImplemented() + { + return function($context) { + /** @var Schema $schema */ + $schema = $context['schema']; + + $errors = []; + foreach ($schema->getTypeMap() as $typeName => $type) { + if ($type instanceof ObjectType) { + foreach ($type->getInterfaces() as $iface) { + try { + // FIXME: rework to return errors instead + self::assertObjectImplementsInterface($schema, $type, $iface); + } catch (\Exception $e) { + $errors[] = $e; + } + } + } + } + return $errors; + }; + } + + /** + * @param ObjectType $object + * @param InterfaceType $iface + * @throws \Exception + */ + protected static function assertObjectImplementsInterface(Schema $schema, ObjectType $object, InterfaceType $iface) + { + $objectFieldMap = $object->getFields(); + $ifaceFieldMap = $iface->getFields(); + + foreach ($ifaceFieldMap as $fieldName => $ifaceField) { + Utils::invariant( + isset($objectFieldMap[$fieldName]), + "\"$iface\" expects field \"$fieldName\" but \"$object\" does not provide it" + ); + + /** @var $ifaceField FieldDefinition */ + /** @var $objectField FieldDefinition */ + $objectField = $objectFieldMap[$fieldName]; + + Utils::invariant( + Utils\TypeInfo::isTypeSubTypeOf($schema, $objectField->getType(), $ifaceField->getType()), + "$iface.$fieldName expects type \"{$ifaceField->getType()}\" but " . + "$object.$fieldName provides type \"{$objectField->getType()}\"." + ); + + foreach ($ifaceField->args as $ifaceArg) { + /** @var $ifaceArg FieldArgument */ + /** @var $objectArg FieldArgument */ + $argName = $ifaceArg->name; + $objectArg = $objectField->getArg($argName); + + // Assert interface field arg exists on object field. + Utils::invariant( + $objectArg, + "$iface.$fieldName expects argument \"$argName\" but $object.$fieldName does not provide it." + ); + + // Assert interface field arg type matches object field arg type. + // (invariant) + Utils::invariant( + Utils\TypeInfo::isEqualType($ifaceArg->getType(), $objectArg->getType()), + "$iface.$fieldName($argName:) expects type \"{$ifaceArg->getType()}\" " . + "but $object.$fieldName($argName:) provides " . + "type \"{$objectArg->getType()}\"" + ); + + // Assert argument set invariance. + foreach ($objectField->args as $objectArg) { + $argName = $objectArg->name; + $ifaceArg = $ifaceField->getArg($argName); + Utils::invariant( + $ifaceArg, + "$iface.$fieldName does not define argument \"$argName\" but " . + "$object.$fieldName provides it." + ); + } + } + } + } + /** * @param Schema $schema * @param array |null $argRules @@ -171,4 +268,4 @@ class SchemaValidator } return $errors; } -} \ No newline at end of file +} diff --git a/src/Utils.php b/src/Utils.php index d1d99a8..968842c 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -1,6 +1,9 @@ $value) { + if (!$predicate($value, $key)) { + return false; + } + } + return true; + } + /** * @param $test * @param string $message @@ -193,6 +211,13 @@ class Utils */ public static function getVariableType($var) { + if ($var instanceof Type) { + // FIXME: Replace with schema printer call + if ($var instanceof WrappingType) { + $var = $var->getWrappedType(true); + } + return $var->name; + } return is_object($var) ? get_class($var) : gettype($var); } diff --git a/tests/Executor/AbstractTest.php b/tests/Executor/AbstractTest.php index d3c86b9..f5f87c2 100644 --- a/tests/Executor/AbstractTest.php +++ b/tests/Executor/AbstractTest.php @@ -3,17 +3,25 @@ namespace GraphQL\Tests\Executor; use GraphQL\Executor\ExecutionResult; use GraphQL\Executor\Executor; +use GraphQL\FormattedError; +use GraphQL\GraphQL; use GraphQL\Language\Parser; +use GraphQL\Language\SourceLocation; use GraphQL\Schema; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; +spl_autoload_call('GraphQL\Tests\Executor\TestClasses'); + class AbstractTest extends \PHPUnit_Framework_TestCase { - // Execute: Handles execution of abstract types + + /** + * @it isTypeOf used to resolve runtime type for Interface + */ public function testIsTypeOfUsedToResolveRuntimeTypeForInterface() { // isTypeOf used to resolve runtime type for Interface @@ -58,7 +66,8 @@ class AbstractTest extends \PHPUnit_Framework_TestCase } ] ] - ]) + ]), + 'types' => [$catType, $dogType] ]); $query = '{ @@ -83,10 +92,11 @@ class AbstractTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Executor::execute($schema, Parser::parse($query))); } + /** + * @it isTypeOf used to resolve runtime type for Union + */ public function testIsTypeOfUsedToResolveRuntimeTypeForUnion() { - // isTypeOf used to resolve runtime type for Union - $dogType = new ObjectType([ 'name' => 'Dog', 'isTypeOf' => function($obj) { return $obj instanceof Dog; }, @@ -148,6 +158,9 @@ class AbstractTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Executor::execute($schema, Parser::parse($query))); } + /** + * @it resolveType on Interface yields useful error + */ function testResolveTypeOnInterfaceYieldsUsefulError() { $DogType = null; @@ -198,21 +211,24 @@ class AbstractTest extends \PHPUnit_Framework_TestCase ] ]); - $schema = new Schema(new ObjectType([ - 'name' => 'Query', - 'fields' => [ - 'pets' => [ - 'type' => Type::listOf($PetType), - 'resolve' => function () { - return [ - new Dog('Odie', true), - new Cat('Garfield', false), - new Human('Jon') - ]; - } - ] - ] - ])); + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'pets' => [ + 'type' => Type::listOf($PetType), + 'resolve' => function () { + return [ + new Dog('Odie', true), + new Cat('Garfield', false), + new Human('Jon') + ]; + } + ] + ], + ]), + 'types' => [$DogType, $CatType] + ]); $query = '{ @@ -236,11 +252,110 @@ class AbstractTest extends \PHPUnit_Framework_TestCase ] ], 'errors' => [ - [ 'message' => 'Runtime Object type "Human" is not a possible type for "Pet".' ] + FormattedError::create( + 'Runtime Object type "Human" is not a possible type for "Pet".', + [new SourceLocation(2, 11)] + ) ] ]; + $actual = GraphQL::execute($schema, $query); - $this->assertEquals($expected, Executor::execute($schema, Parser::parse($query))->toArray()); + $this->assertEquals($expected, $actual); } + /** + * @it resolveType on Union yields useful error + */ + public function testResolveTypeOnUnionYieldsUsefulError() + { + $HumanType = new ObjectType([ + 'name' => 'Human', + 'fields' => [ + 'name' => ['type' => Type::string()], + ] + ]); + + $DogType = new ObjectType([ + 'name' => 'Dog', + 'fields' => [ + 'name' => ['type' => Type::string()], + 'woofs' => ['type' => Type::boolean()], + ] + ]); + + $CatType = new ObjectType([ + 'name' => 'Cat', + 'fields' => [ + 'name' => ['type' => Type::string()], + 'meows' => ['type' => Type::boolean()], + ] + ]); + + $PetType = new UnionType([ + 'name' => 'Pet', + 'resolveType' => function ($obj) use ($DogType, $CatType, $HumanType) { + if ($obj instanceof Dog) { + return $DogType; + } + if ($obj instanceof Cat) { + return $CatType; + } + if ($obj instanceof Human) { + return $HumanType; + } + }, + 'types' => [$DogType, $CatType] + ]); + + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'pets' => [ + 'type' => Type::listOf($PetType), + 'resolve' => function () { + return [ + new Dog('Odie', true), + new Cat('Garfield', false), + new Human('Jon') + ]; + } + ] + ] + ]) + ]); + + $query = '{ + pets { + ... on Dog { + name + woofs + } + ... on Cat { + name + meows + } + } + }'; + + $result = GraphQL::execute($schema, $query); + $expected = [ + 'data' => [ + 'pets' => [ + ['name' => 'Odie', + 'woofs' => true], + ['name' => 'Garfield', + 'meows' => false], + null + ] + ], + 'errors' => [ + FormattedError::create( + 'Runtime Object type "Human" is not a possible type for "Pet".', + [new SourceLocation(2, 11)] + ) + ] + ]; + $this->assertEquals($expected, $result); + } } diff --git a/tests/Executor/DirectivesTest.php b/tests/Executor/DirectivesTest.php index 67a48fb..0e9656d 100644 --- a/tests/Executor/DirectivesTest.php +++ b/tests/Executor/DirectivesTest.php @@ -9,14 +9,20 @@ use GraphQL\Type\Definition\Type; class DirectivesTest extends \PHPUnit_Framework_TestCase { - // Execute: handles directives - // works without directives + // Describe: Execute: handles directives + + /** + * @describe works without directives + * @it basic query works + */ public function testWorksWithoutDirectives() { - // basic query works $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b }')); } + /** + * @describe works on scalars + */ public function testWorksOnScalars() { // if true includes scalar @@ -32,6 +38,9 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery('{ a, b @skip(if: true) }')); } + /** + * @describe works on fragment spreads + */ public function testWorksOnFragmentSpreads() { // if false omits fragment spread @@ -83,6 +92,9 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q)); } + /** + * @describe works on inline fragment + */ public function testWorksOnInlineFragment() { // if false omits inline fragment @@ -142,57 +154,80 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q)); } - public function testWorksOnFragment() + /** + * @describe works on anonymous inline fragment + */ + public function testWorksOnAnonymousInlineFragment() { - // if false omits fragment + // if false omits anonymous inline fragment $q = ' query Q { a - ...Frag - } - fragment Frag on TestType @include(if: false) { - b + ... @include(if: false) { + b + } } '; $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q)); - // if true includes fragment + // if true includes anonymous inline fragment $q = ' query Q { a - ...Frag - } - fragment Frag on TestType @include(if: true) { - b + ... @include(if: true) { + b + } } '; $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($q)); - // unless false includes fragment + // unless false includes anonymous inline fragment $q = ' query Q { a - ...Frag - } - fragment Frag on TestType @skip(if: false) { - b + ... @skip(if: false) { + b + } } '; $this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($q)); - // unless true omits fragment + // unless true includes anonymous inline fragment $q = ' query Q { a - ...Frag - } - fragment Frag on TestType @skip(if: true) { - b + ... @skip(if: true) { + b + } } '; $this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q)); } + /** + * @describe works with skip and include directives + */ + public function testWorksWithSkipAndIncludeDirectives() + { + // include and no skip + $this->assertEquals( + ['data' => ['a' => 'a', 'b' => 'b']], + $this->executeTestQuery('{ a, b @include(if: true) @skip(if: false) }') + ); + + // include and skip + $this->assertEquals( + ['data' => ['a' => 'a']], + $this->executeTestQuery('{ a, b @include(if: true) @skip(if: true) }') + ); + + // no include or skip + $this->assertEquals( + ['data' => ['a' => 'a']], + $this->executeTestQuery('{ a, b @include(if: false) @skip(if: false) }') + ); + } + @@ -202,13 +237,18 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase private static function getSchema() { - return self::$schema ?: (self::$schema = new Schema(new ObjectType([ - 'name' => 'TestType', - 'fields' => [ - 'a' => ['type' => Type::string()], - 'b' => ['type' => Type::string()] - ] - ]))); + if (!self::$schema) { + self::$schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'TestType', + 'fields' => [ + 'a' => ['type' => Type::string()], + 'b' => ['type' => Type::string()] + ] + ]) + ]); + } + return self::$schema; } private static function getData() diff --git a/tests/Executor/ExecutorSchemaTest.php b/tests/Executor/ExecutorSchemaTest.php index 9c42692..ff57ebb 100644 --- a/tests/Executor/ExecutorSchemaTest.php +++ b/tests/Executor/ExecutorSchemaTest.php @@ -11,6 +11,10 @@ use GraphQL\Type\Definition\Type; class ExecutorSchemaTest extends \PHPUnit_Framework_TestCase { // Execute: Handles execution with a complex schema + + /** + * @it executes using a schema + */ public function testExecutesUsingASchema() { $BlogArticle = null; @@ -83,7 +87,7 @@ class ExecutorSchemaTest extends \PHPUnit_Framework_TestCase ] ]); - $BlogSchema = new Schema($BlogQuery); + $BlogSchema = new Schema(['query' => $BlogQuery]); $request = ' diff --git a/tests/Executor/ExecutorTest.php b/tests/Executor/ExecutorTest.php index 9f8c39f..32759a5 100644 --- a/tests/Executor/ExecutorTest.php +++ b/tests/Executor/ExecutorTest.php @@ -137,9 +137,9 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase 'deeper' => [ 'type' => Type::listOf($dataType) ] ] ]); - $schema = new Schema($dataType); + $schema = new Schema(['query' => $dataType]); - $this->assertEquals($expected, Executor::execute($schema, $ast, $data, ['size' => 100], 'Example')->toArray()); + $this->assertEquals($expected, Executor::execute($schema, $ast, $data, null, ['size' => 100], 'Example')->toArray()); } public function testMergesParallelFragments() @@ -180,7 +180,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ] ] ]); - $schema = new Schema($Type); + $schema = new Schema(['query' => $Type]); $expected = [ 'data' => [ 'a' => 'Apple', @@ -212,20 +212,22 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ]; $ast = Parser::parse($doc); - $schema = new Schema(new ObjectType([ - 'name' => 'Type', - 'fields' => [ - 'a' => [ - 'type' => Type::string(), - 'resolve' => function ($context) use ($doc, &$gotHere) { - $this->assertEquals('thing', $context['contextThing']); - $gotHere = true; - } + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Type', + 'fields' => [ + 'a' => [ + 'type' => Type::string(), + 'resolve' => function ($context) use ($doc, &$gotHere) { + $this->assertEquals('thing', $context['contextThing']); + $gotHere = true; + } + ] ] - ] - ])); + ]) + ]); - Executor::execute($schema, $ast, $data, [], 'Example'); + Executor::execute($schema, $ast, $data, null, [], 'Example'); $this->assertEquals(true, $gotHere); } @@ -240,24 +242,26 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase $gotHere = false; $docAst = Parser::parse($doc); - $schema = new Schema(new ObjectType([ - 'name' => 'Type', - 'fields' => [ - 'b' => [ - 'args' => [ - 'numArg' => ['type' => Type::int()], - 'stringArg' => ['type' => Type::string()] - ], - 'type' => Type::string(), - 'resolve' => function ($_, $args) use (&$gotHere) { - $this->assertEquals(123, $args['numArg']); - $this->assertEquals('foo', $args['stringArg']); - $gotHere = true; - } + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Type', + 'fields' => [ + 'b' => [ + 'args' => [ + 'numArg' => ['type' => Type::int()], + 'stringArg' => ['type' => Type::string()] + ], + 'type' => Type::string(), + 'resolve' => function ($_, $args) use (&$gotHere) { + $this->assertEquals(123, $args['numArg']); + $this->assertEquals('foo', $args['stringArg']); + $gotHere = true; + } + ] ] - ] - ])); - Executor::execute($schema, $docAst, null, [], 'Example'); + ]) + ]); + Executor::execute($schema, $docAst, null, null, [], 'Example'); $this->assertSame($gotHere, true); } @@ -296,17 +300,19 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ]; $docAst = Parser::parse($doc); - $schema = new Schema(new ObjectType([ - 'name' => 'Type', - 'fields' => [ - 'sync' => ['type' => Type::string()], - 'syncError' => ['type' => Type::string()], - 'syncRawError' => [ 'type' => Type::string() ], - 'async' => ['type' => Type::string()], - 'asyncReject' => ['type' => Type::string() ], - 'asyncError' => ['type' => Type::string()], - ] - ])); + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Type', + 'fields' => [ + 'sync' => ['type' => Type::string()], + 'syncError' => ['type' => Type::string()], + 'syncRawError' => [ 'type' => Type::string() ], + 'async' => ['type' => Type::string()], + 'asyncReject' => ['type' => Type::string() ], + 'asyncError' => ['type' => Type::string()], + ] + ]) + ]); $expected = [ 'data' => [ @@ -336,12 +342,14 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase $doc = '{ a }'; $data = ['a' => 'b']; $ast = Parser::parse($doc); - $schema = new Schema(new ObjectType([ - 'name' => 'Type', - 'fields' => [ - 'a' => ['type' => Type::string()], - ] - ])); + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Type', + 'fields' => [ + 'a' => ['type' => Type::string()], + ] + ]) + ]); $ex = Executor::execute($schema, $ast, $data); @@ -353,12 +361,14 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase $doc = 'query Example { a }'; $data = [ 'a' => 'b' ]; $ast = Parser::parse($doc); - $schema = new Schema(new ObjectType([ - 'name' => 'Type', - 'fields' => [ - 'a' => [ 'type' => Type::string() ], - ] - ])); + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Type', + 'fields' => [ + 'a' => [ 'type' => Type::string() ], + ] + ]) + ]); $ex = Executor::execute($schema, $ast, $data); $this->assertEquals(['data' => ['a' => 'b']], $ex->toArray()); @@ -369,12 +379,14 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase $doc = 'query Example { a } query OtherExample { a }'; $data = [ 'a' => 'b' ]; $ast = Parser::parse($doc); - $schema = new Schema(new ObjectType([ - 'name' => 'Type', - 'fields' => [ - 'a' => [ 'type' => Type::string() ], - ] - ])); + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Type', + 'fields' => [ + 'a' => [ 'type' => Type::string() ], + ] + ]) + ]); try { Executor::execute($schema, $ast, $data); @@ -389,22 +401,22 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase $doc = 'query Q { a } mutation M { c }'; $data = ['a' => 'b', 'c' => 'd']; $ast = Parser::parse($doc); - $schema = new Schema( - new ObjectType([ + $schema = new Schema([ + 'query' => new ObjectType([ 'name' => 'Q', 'fields' => [ 'a' => ['type' => Type::string()], ] ]), - new ObjectType([ + 'mutation' => new ObjectType([ 'name' => 'M', 'fields' => [ 'c' => ['type' => Type::string()], ] ]) - ); + ]); - $queryResult = Executor::execute($schema, $ast, $data, [], 'Q'); + $queryResult = Executor::execute($schema, $ast, $data, null, [], 'Q'); $this->assertEquals(['data' => ['a' => 'b']], $queryResult->toArray()); } @@ -413,21 +425,21 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase $doc = 'query Q { a } mutation M { c }'; $data = [ 'a' => 'b', 'c' => 'd' ]; $ast = Parser::parse($doc); - $schema = new Schema( - new ObjectType([ + $schema = new Schema([ + 'query' => new ObjectType([ 'name' => 'Q', 'fields' => [ 'a' => ['type' => Type::string()], ] ]), - new ObjectType([ + 'mutation' => new ObjectType([ 'name' => 'M', 'fields' => [ 'c' => [ 'type' => Type::string() ], ] ]) - ); - $mutationResult = Executor::execute($schema, $ast, $data, [], 'M'); + ]); + $mutationResult = Executor::execute($schema, $ast, $data, null, [], 'M'); $this->assertEquals(['data' => ['c' => 'd']], $mutationResult->toArray()); } @@ -447,14 +459,16 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase '; $data = ['a' => 'b']; $ast = Parser::parse($doc); - $schema = new Schema(new ObjectType([ - 'name' => 'Type', - 'fields' => [ - 'a' => ['type' => Type::string()], - ] - ])); + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Type', + 'fields' => [ + 'a' => ['type' => Type::string()], + ] + ]) + ]); - $queryResult = Executor::execute($schema, $ast, $data, [], 'Q'); + $queryResult = Executor::execute($schema, $ast, $data, null, [], 'Q'); $this->assertEquals(['data' => ['a' => 'b']], $queryResult->toArray()); } @@ -464,28 +478,28 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase thisIsIllegalDontIncludeMe }'; $ast = Parser::parse($doc); - $schema = new Schema( - new ObjectType([ + $schema = new Schema([ + 'query' => new ObjectType([ 'name' => 'Q', 'fields' => [ 'a' => ['type' => Type::string()], ] ]), - new ObjectType([ + 'mutation' => new ObjectType([ 'name' => 'M', 'fields' => [ 'c' => ['type' => Type::string()], ] ]) - ); + ]); $mutationResult = Executor::execute($schema, $ast); $this->assertEquals(['data' => []], $mutationResult->toArray()); } public function testDoesNotIncludeArgumentsThatWereNotSet() { - $schema = new Schema( - new ObjectType([ + $schema = new Schema([ + 'query' => new ObjectType([ 'name' => 'Type', 'fields' => [ 'field' => [ @@ -501,7 +515,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ] ] ]) - ); + ]); $query = Parser::parse('{ field(a: true, c: false, e: 0) }'); $result = Executor::execute($schema, $query); @@ -516,8 +530,8 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase public function testSubstitutesArgumentWithDefaultValue() { - $schema = new Schema( - new ObjectType([ + $schema = new Schema([ + 'query' => new ObjectType([ 'name' => 'Type', 'fields' => [ 'field' => [ @@ -535,7 +549,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase ] ] ]) - ); + ]); $query = Parser::parse('{ field }'); $result = Executor::execute($schema, $query); diff --git a/tests/Executor/ListsTest.php b/tests/Executor/ListsTest.php index d618244..1588034 100644 --- a/tests/Executor/ListsTest.php +++ b/tests/Executor/ListsTest.php @@ -12,376 +12,186 @@ use GraphQL\Type\Definition\Type; class ListsTest extends \PHPUnit_Framework_TestCase { - // Execute: Handles list nullability - - public function testHandlesListsWhenTheyReturnNonNullValues() + private function check($testType, $testData, $expected) { - $doc = ' - query Q { - nest { - list, - } - } - '; + $data = ['test' => $testData]; + $dataType = null; - $ast = Parser::parse($doc); - $expected = ['data' => ['nest' => ['list' => [1,2]]]]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); - } - - public function testHandlesListsOfNonNullsWhenTheyReturnNonNullValues() - { - $doc = ' - query Q { - nest { - listOfNonNull, - } - } - '; - - $ast = Parser::parse($doc); - - $expected = [ - 'data' => [ - 'nest' => [ - 'listOfNonNull' => [1, 2], - ] - ] - ]; - - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); - } - - public function testHandlesNonNullListsOfWhenTheyReturnNonNullValues() - { - $doc = ' - query Q { - nest { - nonNullList, - } - } - '; - - $ast = Parser::parse($doc); - - $expected = [ - 'data' => [ - 'nest' => [ - 'nonNullList' => [1, 2], - ] - ] - ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); - } - - public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNonNullValues() - { - $doc = ' - query Q { - nest { - nonNullListOfNonNull, - } - } - '; - - $ast = Parser::parse($doc); - - $expected = [ - 'data' => [ - 'nest' => [ - 'nonNullListOfNonNull' => [1, 2], - ] - ] - ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); - } - - public function testHandlesListsWhenTheyReturnNullAsAValue() - { - $doc = ' - query Q { - nest { - listContainsNull, - } - } - '; - - $ast = Parser::parse($doc); - - $expected = [ - 'data' => [ - 'nest' => [ - 'listContainsNull' => [1, null, 2], - ] - ] - ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); - } - - public function testHandlesListsOfNonNullsWhenTheyReturnNullAsAValue() - { - $doc = ' - query Q { - nest { - listOfNonNullContainsNull, - } - } - '; - - $ast = Parser::parse($doc); - - $expected = [ - 'data' => [ - 'nest' => [ - 'listOfNonNullContainsNull' => null - ] - ], - 'errors' => [ - FormattedError::create( - 'Cannot return null for non-nullable type.', - [new SourceLocation(4, 11)] - ) - ] - ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); - } - - public function testHandlesNonNullListsOfWhenTheyReturnNullAsAValue() - { - $doc = ' - query Q { - nest { - nonNullListContainsNull, - } - } - '; - - $ast = Parser::parse($doc); - $expected = [ - 'data' => [ - 'nest' => ['nonNullListContainsNull' => [1, null, 2]] - ] - ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); - } - - public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNullAsAValue() - { - $doc = ' - query Q { - nest { - nonNullListOfNonNullContainsNull, - } - } - '; - - $ast = Parser::parse($doc); - - $expected = [ - 'data' => [ - 'nest' => null - ], - 'errors' => [ - FormattedError::create( - 'Cannot return null for non-nullable type.', - [new SourceLocation(4, 11)] - ) - ] - ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); - } - - public function testHandlesListsWhenTheyReturnNull() - { - $doc = ' - query Q { - nest { - listReturnsNull, - } - } - '; - - $ast = Parser::parse($doc); - - $expected = [ - 'data' => [ - 'nest' => [ - 'listReturnsNull' => null - ] - ] - ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); - } - - public function testHandlesListsOfNonNullsWhenTheyReturnNull() - { - $doc = ' - query Q { - nest { - listOfNonNullReturnsNull, - } - } - '; - - $ast = Parser::parse($doc); - - $expected = [ - 'data' => [ - 'nest' => [ - 'listOfNonNullReturnsNull' => null - ] - ] - ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); - } - - public function testHandlesNonNullListsOfWhenTheyReturnNull() - { - $doc = ' - query Q { - nest { - nonNullListReturnsNull, - } - } - '; - - $ast = Parser::parse($doc); - - $expected = [ - 'data' => [ - 'nest' => null, - ], - 'errors' => [ - FormattedError::create( - 'Cannot return null for non-nullable type.', - [new SourceLocation(4, 11)] - ) - ] - ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); - } - - public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNull() - { - $doc = ' - query Q { - nest { - nonNullListOfNonNullReturnsNull, - } - } - '; - - $ast = Parser::parse($doc); - - $expected = [ - 'data' => [ - 'nest' => null - ], - 'errors' => [ - FormattedError::create( - 'Cannot return null for non-nullable type.', - [new SourceLocation(4, 11)] - ) - ] - ]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray()); - } - - - - private function schema() - { $dataType = new ObjectType([ 'name' => 'DataType', - 'fields' => [ - 'list' => [ - 'type' => Type::listOf(Type::int()) - ], - 'listOfNonNull' => [ - 'type' => Type::listOf(Type::nonNull(Type::int())) - ], - 'nonNullList' => [ - 'type' => Type::nonNull(Type::listOf(Type::int())) - ], - 'nonNullListOfNonNull' => [ - 'type' => Type::nonNull(Type::listOf(Type::nonNull(Type::int()))) - ], - 'listContainsNull' => [ - 'type' => Type::listOf(Type::int()) - ], - 'listOfNonNullContainsNull' => [ - 'type' => Type::listOf(Type::nonNull(Type::int())), - ], - 'nonNullListContainsNull' => [ - 'type' => Type::nonNull(Type::listOf(Type::int())) - ], - 'nonNullListOfNonNullContainsNull' => [ - 'type' => Type::nonNull(Type::listOf(Type::nonNull(Type::int()))) - ], - 'listReturnsNull' => [ - 'type' => Type::listOf(Type::int()) - ], - 'listOfNonNullReturnsNull' => [ - 'type' => Type::listOf(Type::nonNull(Type::int())) - ], - 'nonNullListReturnsNull' => [ - 'type' => Type::nonNull(Type::listOf(Type::int())) - ], - 'nonNullListOfNonNullReturnsNull' => [ - 'type' => Type::nonNull(Type::listOf(Type::nonNull(Type::int()))) - ], - 'nest' => ['type' => function () use (&$dataType) { - return $dataType; - }] - ] + 'fields' => function () use (&$testType, &$dataType, $data) { + return [ + 'test' => [ + 'type' => $testType + ], + 'nest' => [ + 'type' => $dataType, + 'resolve' => function () use ($data) { + return $data; + } + ] + ]; + } ]); - $schema = new Schema($dataType); - return $schema; + $schema = new Schema([ + 'query' => $dataType + ]); + + $ast = Parser::parse('{ nest { test } }'); + + $result = Executor::execute($schema, $ast, $data); + $this->assertEquals($expected, $result->toArray()); } - private function data() + // Describe: Execute: Handles list nullability + + /** + * @describe [T] + */ + public function testHandlesNullableLists() { - return [ - 'list' => function () { - return [1, 2]; - }, - 'listOfNonNull' => function () { - return [1, 2]; - }, - 'nonNullList' => function () { - return [1, 2]; - }, - 'nonNullListOfNonNull' => function () { - return [1, 2]; - }, - 'listContainsNull' => function () { - return [1, null, 2]; - }, - 'listOfNonNullContainsNull' => function () { - return [1, null, 2]; - }, - 'nonNullListContainsNull' => function () { - return [1, null, 2]; - }, - 'nonNullListOfNonNullContainsNull' => function () { - return [1, null, 2]; - }, - 'listReturnsNull' => function () { - return null; - }, - 'listOfNonNullReturnsNull' => function () { - return null; - }, - 'nonNullListReturnsNull' => function () { - return null; - }, - 'nonNullListOfNonNullReturnsNull' => function () { - return null; - }, - 'nest' => function () { - return self::data(); - } - ]; + $type = Type::listOf(Type::int()); + + // Contains values + $this->check( + $type, + [ 1, 2 ], + [ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ] + ); + + // Contains null + $this->check( + $type, + [ 1, null, 2 ], + [ 'data' => [ 'nest' => [ 'test' => [ 1, null, 2 ] ] ] ] + ); + + // Returns null + $this->check( + $type, + null, + [ 'data' => [ 'nest' => [ 'test' => null ] ] ] + ); + } + + /** + * @describe [T]! + */ + public function testHandlesNonNullableLists() + { + $type = Type::nonNull(Type::listOf(Type::int())); + + // Contains values + $this->check( + $type, + [ 1, 2 ], + [ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ] + ); + + // Contains null + $this->check( + $type, + [ 1, null, 2 ], + [ 'data' => [ 'nest' => [ 'test' => [ 1, null, 2 ] ] ] ] + ); + + // Returns null + $this->check( + $type, + null, + [ + 'data' => [ 'nest' => null ], + 'errors' => [ + FormattedError::create( + 'Cannot return null for non-nullable field DataType.test.', + [ new SourceLocation(1, 10) ] + ) + ] + ] + ); + } + + /** + * @describe [T!] + */ + public function testHandlesListOfNonNulls() + { + $type = Type::listOf(Type::nonNull(Type::int())); + + // Contains values + $this->check( + $type, + [ 1, 2 ], + [ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ] + ); + + // Contains null + $this->check( + $type, + [ 1, null, 2 ], + [ + 'data' => [ 'nest' => [ 'test' => null ] ], + 'errors' => [ + FormattedError::create( + 'Cannot return null for non-nullable field DataType.test.', + [ new SourceLocation(1, 10) ] + ) + ] + ] + ); + + // Returns null + $this->check( + $type, + null, + [ 'data' => [ 'nest' => [ 'test' => null ] ] ] + ); + } + + /** + * @describe [T!]! + */ + public function testHandlesNonNullListOfNonNulls() + { + $type = Type::nonNull(Type::listOf(Type::nonNull(Type::int()))); + + // Contains values + $this->check( + $type, + [ 1, 2 ], + [ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ] + ); + + + // Contains null + $this->check( + $type, + [ 1, null, 2 ], + [ + 'data' => [ 'nest' => null ], + 'errors' => [ + FormattedError::create( + 'Cannot return null for non-nullable field DataType.test.', + [ new SourceLocation(1, 10) ] + ) + ] + ] + ); + + // Returns null + $this->check( + $type, + null, + [ + 'data' => [ 'nest' => null ], + 'errors' => [ + FormattedError::create( + 'Cannot return null for non-nullable field DataType.test.', + [ new SourceLocation(1, 10) ] + ) + ] + ] + ); } } diff --git a/tests/Executor/MutationsTest.php b/tests/Executor/MutationsTest.php index f9e6fb7..5f04302 100644 --- a/tests/Executor/MutationsTest.php +++ b/tests/Executor/MutationsTest.php @@ -12,6 +12,10 @@ use GraphQL\Type\Definition\Type; class MutationsTest extends \PHPUnit_Framework_TestCase { // Execute: Handles mutation execution ordering + + /** + * @it evaluates mutations serially + */ public function testEvaluatesMutationsSerially() { $doc = 'mutation M { @@ -32,7 +36,7 @@ class MutationsTest extends \PHPUnit_Framework_TestCase } }'; $ast = Parser::parse($doc); - $mutationResult = Executor::execute($this->schema(), $ast, new Root(6), null, 'M'); + $mutationResult = Executor::execute($this->schema(), $ast, new Root(6)); $expected = [ 'data' => [ 'first' => [ @@ -55,6 +59,9 @@ class MutationsTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, $mutationResult->toArray()); } + /** + * @it evaluates mutations correctly in the presense of a failed mutation + */ public function testEvaluatesMutationsCorrectlyInThePresenseOfAFailedMutation() { $doc = 'mutation M { @@ -78,7 +85,7 @@ class MutationsTest extends \PHPUnit_Framework_TestCase } }'; $ast = Parser::parse($doc); - $mutationResult = Executor::execute($this->schema(), $ast, new Root(6), null, 'M'); + $mutationResult = Executor::execute($this->schema(), $ast, new Root(6)); $expected = [ 'data' => [ 'first' => [ @@ -118,14 +125,14 @@ class MutationsTest extends \PHPUnit_Framework_TestCase ], 'name' => 'NumberHolder', ]); - $schema = new Schema( - new ObjectType([ + $schema = new Schema([ + 'query' => new ObjectType([ 'fields' => [ 'numberHolder' => ['type' => $numberHolderType], ], 'name' => 'Query', ]), - new ObjectType([ + 'mutation' => new ObjectType([ 'fields' => [ 'immediatelyChangeTheNumber' => [ 'type' => $numberHolderType, @@ -158,7 +165,7 @@ class MutationsTest extends \PHPUnit_Framework_TestCase ], 'name' => 'Mutation', ]) - ); + ]); return $schema; } } diff --git a/tests/Executor/NonNullTest.php b/tests/Executor/NonNullTest.php index 4b43982..43dbb40 100644 --- a/tests/Executor/NonNullTest.php +++ b/tests/Executor/NonNullTest.php @@ -68,10 +68,14 @@ class NonNullTest extends \PHPUnit_Framework_TestCase ] ]); - $this->schema = new Schema($dataType); + $this->schema = new Schema(['query' => $dataType]); } // Execute: handles non-nullable types + + /** + * @it nulls a nullable field that throws synchronously + */ public function testNullsANullableFieldThatThrowsSynchronously() { $doc = ' @@ -93,7 +97,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase ) ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, [], 'Q')->toArray()); + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray()); } public function testNullsASynchronouslyReturnedObjectThatContainsANonNullableFieldThatThrowsSynchronously() @@ -117,7 +121,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase FormattedError::create($this->nonNullSyncError->message, [new SourceLocation(4, 11)]) ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, [], 'Q')->toArray()); + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray()); } public function testNullsAComplexTreeOfNullableFieldsThatThrow() @@ -149,7 +153,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase FormattedError::create($this->syncError->message, [new SourceLocation(6, 13)]), ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, [], 'Q')->toArray()); + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray()); } public function testNullsANullableFieldThatSynchronouslyReturnsNull() @@ -167,7 +171,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase 'sync' => null, ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, [], 'Q')->toArray()); + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray()); } public function test4() @@ -188,10 +192,10 @@ class NonNullTest extends \PHPUnit_Framework_TestCase 'nest' => null ], 'errors' => [ - FormattedError::create('Cannot return null for non-nullable type.', [new SourceLocation(4, 11)]) + FormattedError::create('Cannot return null for non-nullable field DataType.nonNullSync.', [new SourceLocation(4, 11)]) ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, [], 'Q')->toArray()); + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray()); } public function test5() @@ -227,7 +231,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase ], ] ]; - $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, [], 'Q')->toArray()); + $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray()); } public function testNullsTheTopLevelIfSyncNonNullableFieldThrows() @@ -255,7 +259,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase $expected = [ 'data' => null, 'errors' => [ - FormattedError::create('Cannot return null for non-nullable type.', [new SourceLocation(2, 17)]), + FormattedError::create('Cannot return null for non-nullable field DataType.nonNullSync.', [new SourceLocation(2, 17)]), ] ]; $this->assertEquals($expected, Executor::execute($this->schema, Parser::parse($doc), $this->nullingData)->toArray()); diff --git a/tests/Executor/ResolveTest.php b/tests/Executor/ResolveTest.php new file mode 100644 index 0000000..94fd5a2 --- /dev/null +++ b/tests/Executor/ResolveTest.php @@ -0,0 +1,97 @@ + new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'test' => $testField + ] + ]) + ]); + } + + /** + * @it default function accesses properties + */ + public function testDefaultFunctionAccessesProperties() + { + $schema = $this->buildSchema(['type' => Type::string()]); + + $source = [ + 'test' => 'testValue' + ]; + + $this->assertEquals( + ['data' => ['test' => 'testValue']], + GraphQL::execute($schema, '{ test }', $source) + ); + } + + /** + * @it default function calls methods + */ + public function testDefaultFunctionCallsMethods() + { + $schema = $this->buildSchema(['type' => Type::string()]); + $_secret = 'secretValue' . uniqid(); + + $source = [ + 'test' => function () use ($_secret) { + return $_secret; + } + ]; + $this->assertEquals( + ['data' => ['test' => $_secret]], + GraphQL::execute($schema, '{ test }', $source) + ); + } + + /** + * @it uses provided resolve function + */ + public function testUsesProvidedResolveFunction() + { + $schema = $this->buildSchema([ + 'type' => Type::string(), + 'args' => [ + 'aStr' => ['type' => Type::string()], + 'aInt' => ['type' => Type::int()], + ], + 'resolve' => function ($source, $args) { + return json_encode([$source, $args]); + } + ]); + + $this->assertEquals( + ['data' => ['test' => '[null,[]]']], + GraphQL::execute($schema, '{ test }') + ); + + $this->assertEquals( + ['data' => ['test' => '["Source!",[]]']], + GraphQL::execute($schema, '{ test }', 'Source!') + ); + + $this->assertEquals( + ['data' => ['test' => '["Source!",{"aStr":"String!"}]']], + GraphQL::execute($schema, '{ test(aStr: "String!") }', 'Source!') + ); + + $this->assertEquals( + ['data' => ['test' => '["Source!",{"aStr":"String!","aInt":-123}]']], + GraphQL::execute($schema, '{ test(aInt: -123, aStr: "String!") }', 'Source!') + ); + } +} \ No newline at end of file diff --git a/tests/Executor/UnionInterfaceTest.php b/tests/Executor/UnionInterfaceTest.php index 2983968..8c2c0f8 100644 --- a/tests/Executor/UnionInterfaceTest.php +++ b/tests/Executor/UnionInterfaceTest.php @@ -4,11 +4,13 @@ namespace GraphQL\Tests\Executor; require_once __DIR__ . '/TestClasses.php'; use GraphQL\Executor\Executor; +use GraphQL\GraphQL; use GraphQL\Language\Parser; use GraphQL\Schema; use GraphQL\Type\Definition\Config; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; @@ -79,7 +81,10 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase } ]); - $this->schema = new Schema($PersonType); + $this->schema = new Schema([ + 'query' => $PersonType, + 'types' => [ $PetType ] + ]); $this->garfield = new Cat('Garfield', false); $this->odie = new Dog('Odie', true); @@ -89,6 +94,10 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase } // Execute: Union and intersection types + + /** + * @it can introspect on union and intersection types + */ public function testCanIntrospectOnUnionAndIntersectionTypes() { @@ -125,9 +134,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase ], 'interfaces' => null, 'possibleTypes' => [ + ['name' => 'Person'], ['name' => 'Dog'], - ['name' => 'Cat'], - ['name' => 'Person'] + ['name' => 'Cat'] ], 'enumValues' => null, 'inputFields' => null @@ -149,6 +158,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Executor::execute($this->schema, $ast)->toArray()); } + /** + * @it executes using union types + */ public function testExecutesUsingUnionTypes() { // NOTE: This is an *invalid* query, but it should be an *executable* query. @@ -178,6 +190,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray()); } + /** + * @it executes union types with inline fragments + */ public function testExecutesUnionTypesWithInlineFragments() { // This is the valid version of the query in the above test. @@ -212,6 +227,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray()); } + /** + * @it executes using interface types + */ public function testExecutesUsingInterfaceTypes() { // NOTE: This is an *invalid* query, but it should be an *executable* query. @@ -241,6 +259,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray()); } + /** + * @it executes interface types with inline fragments + */ public function testExecutesInterfaceTypesWithInlineFragments() { // This is the valid version of the query in the above test. @@ -274,6 +295,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray()); } + /** + * @it allows fragment conditions to be abstract types + */ public function testAllowsFragmentConditionsToBeAbstractTypes() { $ast = Parser::parse(' @@ -325,4 +349,55 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray()); } + + /** + * @it gets execution info in resolver + */ + public function testGetsExecutionInfoInResolver() + { + $encounteredContext = null; + $encounteredSchema = null; + $encounteredRootValue = null; + $PersonType2 = null; + + $NamedType2 = new InterfaceType([ + 'name' => 'Named', + 'fields' => [ + 'name' => ['type' => Type::string()] + ], + 'resolveType' => function ($obj, $context, ResolveInfo $info) use (&$encounteredContext, &$encounteredSchema, &$encounteredRootValue, &$PersonType2) { + $encounteredContext = $context; + $encounteredSchema = $info->schema; + $encounteredRootValue = $info->rootValue; + return $PersonType2; + } + ]); + + $PersonType2 = new ObjectType([ + 'name' => 'Person', + 'interfaces' => [$NamedType2], + 'fields' => [ + 'name' => ['type' => Type::string()], + 'friends' => ['type' => Type::listOf($NamedType2)], + ], + ]); + + $schema2 = new Schema([ + 'query' => $PersonType2 + ]); + + $john2 = new Person('John', [], [$this->liz]); + + $context = ['authToken' => '123abc']; + + $ast = Parser::parse('{ name, friends { name } }'); + + $this->assertEquals( + ['data' => ['name' => 'John', 'friends' => [['name' => 'Liz']]]], + GraphQL::execute($schema2, $ast, $john2, $context) + ); + $this->assertSame($context, $encounteredContext); + $this->assertSame($schema2, $encounteredSchema); + $this->assertSame($john2, $encounteredRootValue); + } } diff --git a/tests/Executor/VariablesTest.php b/tests/Executor/VariablesTest.php index 209ae33..e12763d 100644 --- a/tests/Executor/VariablesTest.php +++ b/tests/Executor/VariablesTest.php @@ -11,7 +11,6 @@ use GraphQL\Language\SourceLocation; use GraphQL\Schema; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\ObjectType; -use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; class VariablesTest extends \PHPUnit_Framework_TestCase @@ -19,6 +18,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase // Execute: Handles inputs // Handles objects and nullability + /** + * @describe using inline structs + */ public function testUsingInlineStructs() { // executes with complex input: @@ -46,10 +48,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $expected = ['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']]; $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); - } - public function testDoesNotUseIncorrectValue() - { + // does not use incorrect value $doc = ' { fieldWithObjectInput(input: ["foo", "bar", "baz"]) @@ -62,8 +62,23 @@ class VariablesTest extends \PHPUnit_Framework_TestCase 'data' => ['fieldWithObjectInput' => null] ]; $this->assertEquals($expected, $result); + + // properly runs parseLiteral on complex scalar types + $doc = ' + { + fieldWithObjectInput(input: {a: "foo", d: "SerializedValue"}) + } + '; + $ast = Parser::parse($doc); + $this->assertEquals( + ['data' => ['fieldWithObjectInput' => '{"a":"foo","d":"DeserializedValue"}']], + Executor::execute($this->schema(), $ast)->toArray() + ); } + /** + * @describe using variables + */ public function testUsingVariables() { // executes with complex input: @@ -78,7 +93,7 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $this->assertEquals( ['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']], - Executor::execute($schema, $ast, null, $params)->toArray() + Executor::execute($schema, $ast, null, null, $params)->toArray() ); // uses default value when not provided: @@ -94,17 +109,16 @@ class VariablesTest extends \PHPUnit_Framework_TestCase ]; $this->assertEquals($expected, $result); - // properly parses single value to array: $params = ['input' => ['a' => 'foo', 'b' => 'bar', 'c' => 'baz']]; $this->assertEquals( ['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']], - Executor::execute($schema, $ast, null, $params)->toArray() + Executor::execute($schema, $ast, null, null, $params)->toArray() ); // executes with complex scalar input: $params = [ 'input' => [ 'c' => 'foo', 'd' => 'SerializedValue' ] ]; - $result = Executor::execute($schema, $ast, null, $params)->toArray(); + $result = Executor::execute($schema, $ast, null, null, $params)->toArray(); $expected = [ 'data' => [ 'fieldWithObjectInput' => '{"c":"foo","d":"DeserializedValue"}' @@ -115,14 +129,13 @@ class VariablesTest extends \PHPUnit_Framework_TestCase // errors on null for nested non-null: $params = ['input' => ['a' => 'foo', 'b' => 'bar', 'c' => null]]; $expected = FormattedError::create( - 'Variable $input expected value of type ' . - 'TestInputObject but got: ' . - '{"a":"foo","b":"bar","c":null}.', + 'Variable "$input" got invalid value {"a":"foo","b":"bar","c":null}.'. "\n". + 'In field "c": Expected "String!", found null.', [new SourceLocation(2, 17)] ); try { - Executor::execute($schema, $ast, null, $params); + Executor::execute($schema, $ast, null, null, $params); $this->fail('Expected exception not thrown'); } catch (Error $err) { $this->assertEquals($expected, Error::formatError($err)); @@ -132,11 +145,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $params = [ 'input' => 'foo bar' ]; try { - Executor::execute($schema, $ast, null, $params); + Executor::execute($schema, $ast, null, null, $params); $this->fail('Expected exception not thrown'); } catch (Error $error) { $expected = FormattedError::create( - 'Variable $input expected value of type TestInputObject but got: "foo bar".', + 'Variable "$input" got invalid value "foo bar".'."\n". + 'Expected "TestInputObject", found not an object.', [new SourceLocation(2, 17)] ); $this->assertEquals($expected, Error::formatError($error)); @@ -146,36 +160,61 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $params = ['input' => ['a' => 'foo', 'b' => 'bar']]; try { - Executor::execute($schema, $ast, null, $params); + Executor::execute($schema, $ast, null, null, $params); $this->fail('Expected exception not thrown'); } catch (Error $e) { $expected = FormattedError::create( - 'Variable $input expected value of type ' . - 'TestInputObject but got: {"a":"foo","b":"bar"}.', + 'Variable "$input" got invalid value {"a":"foo","b":"bar"}.'. "\n". + 'In field "c": Expected "String!", found null.', [new SourceLocation(2, 17)] ); $this->assertEquals($expected, Error::formatError($e)); } + // errors on deep nested errors and with many errors + $nestedDoc = ' + query q($input: TestNestedInputObject) { + fieldWithNestedObjectInput(input: $input) + } + '; + $nestedAst = Parser::parse($nestedDoc); + $params = [ 'input' => [ 'na' => [ 'a' => 'foo' ] ] ]; + + try { + Executor::execute($schema, $nestedAst, null, null, $params); + $this->fail('Expected exception not thrown'); + } catch (Error $error) { + $expected = FormattedError::create( + 'Variable "$input" got invalid value {"na":{"a":"foo"}}.' . "\n" . + 'In field "na": In field "c": Expected "String!", found null.' . "\n" . + 'In field "nb": Expected "String!", found null.', + [new SourceLocation(2, 19)] + ); + $this->assertEquals($expected, Error::formatError($error)); + } + // errors on addition of unknown input field $params = ['input' => [ 'a' => 'foo', 'b' => 'bar', 'c' => 'baz', 'd' => 'dog' ]]; try { - Executor::execute($schema, $ast, null, $params); + Executor::execute($schema, $ast, null, null, $params); $this->fail('Expected exception not thrown'); } catch (Error $e) { $expected = FormattedError::create( - 'Variable $input expected value of type TestInputObject but ' . - 'got: {"a":"foo","b":"bar","c":"baz","d":"dog"}.', + 'Variable "$input" got invalid value {"a":"foo","b":"bar","c":"baz","d":"dog"}.'."\n". + 'In field "d": Expected type "ComplexScalar", found "dog".', [new SourceLocation(2, 17)] ); $this->assertEquals($expected, Error::formatError($e)); } } + // Describe: Handles nullable scalars - // Handles nullable scalars + /** + * @it allows nullable inputs to be omitted + */ public function testAllowsNullableInputsToBeOmitted() { $doc = ' @@ -191,6 +230,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); } + /** + * @it allows nullable inputs to be omitted in a variable + */ public function testAllowsNullableInputsToBeOmittedInAVariable() { $doc = ' @@ -204,6 +246,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); } + /** + * @it allows nullable inputs to be omitted in an unlisted variable + */ public function testAllowsNullableInputsToBeOmittedInAnUnlistedVariable() { $doc = ' @@ -216,6 +261,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); } + /** + * @it allows nullable inputs to be set to null in a variable + */ public function testAllowsNullableInputsToBeSetToNullInAVariable() { $doc = ' @@ -229,6 +277,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['value' => null])->toArray()); } + /** + * @it allows nullable inputs to be set to a value in a variable + */ public function testAllowsNullableInputsToBeSetToAValueInAVariable() { $doc = ' @@ -238,9 +289,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = ['data' => ['fieldWithNullableStringInput' => '"a"']]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['value' => 'a'])->toArray()); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['value' => 'a'])->toArray()); } + /** + * @it allows nullable inputs to be set to a value directly + */ public function testAllowsNullableInputsToBeSetToAValueDirectly() { $doc = ' @@ -254,10 +308,13 @@ class VariablesTest extends \PHPUnit_Framework_TestCase } - // Handles non-nullable scalars + // Describe: Handles non-nullable scalars + + /** + * @it does not allow non-nullable inputs to be omitted in a variable + */ public function testDoesntAllowNonNullableInputsToBeOmittedInAVariable() { - // does not allow non-nullable inputs to be omitted in a variable $doc = ' query SetsNonNullable($value: String!) { fieldWithNonNullableStringInput(input: $value) @@ -269,16 +326,18 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $this->fail('Expected exception not thrown'); } catch (Error $e) { $expected = FormattedError::create( - 'Variable $value expected value of type String! but got: null.', + 'Variable "$value" of required type "String!" was not provided.', [new SourceLocation(2, 31)] ); $this->assertEquals($expected, Error::formatError($e)); } } + /** + * @it does not allow non-nullable inputs to be set to null in a variable + */ public function testDoesNotAllowNonNullableInputsToBeSetToNullInAVariable() { - // does not allow non-nullable inputs to be set to null in a variable $doc = ' query SetsNonNullable($value: String!) { fieldWithNonNullableStringInput(input: $value) @@ -291,13 +350,16 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $this->fail('Expected exception not thrown'); } catch (Error $e) { $expected = FormattedError::create( - 'Variable $value expected value of type String! but got: null.', + 'Variable "$value" of required type "String!" was not provided.', [new SourceLocation(2, 31)] ); $this->assertEquals($expected, Error::formatError($e)); } } + /** + * @it allows non-nullable inputs to be set to a value in a variable + */ public function testAllowsNonNullableInputsToBeSetToAValueInAVariable() { $doc = ' @@ -307,9 +369,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = ['data' => ['fieldWithNonNullableStringInput' => '"a"']]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['value' => 'a'])->toArray()); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['value' => 'a'])->toArray()); } + /** + * @it allows non-nullable inputs to be set to a value directly + */ public function testAllowsNonNullableInputsToBeSetToAValueDirectly() { $doc = ' @@ -323,6 +388,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); } + /** + * @it passes along null for non-nullable inputs if explcitly set in the query + */ public function testPassesAlongNullForNonNullableInputsIfExplcitlySetInTheQuery() { $doc = ' @@ -335,7 +403,11 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray()); } - // Handles lists and nullability + // Describe: Handles lists and nullability + + /** + * @it allows lists to be null + */ public function testAllowsListsToBeNull() { $doc = ' @@ -349,6 +421,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => null])->toArray()); } + /** + * @it allows lists to contain values + */ public function testAllowsListsToContainValues() { $doc = ' @@ -358,9 +433,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = ['data' => ['list' => '["A"]']]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A']])->toArray()); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => ['A']])->toArray()); } + /** + * @it allows lists to contain null + */ public function testAllowsListsToContainNull() { $doc = ' @@ -370,9 +448,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = ['data' => ['list' => '["A",null,"B"]']]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A',null,'B']])->toArray()); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => ['A',null,'B']])->toArray()); } + /** + * @it does not allow non-null lists to be null + */ public function testDoesNotAllowNonNullListsToBeNull() { $doc = ' @@ -382,18 +463,21 @@ class VariablesTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = FormattedError::create( - 'Variable $input expected value of type [String]! but got: null.', + 'Variable "$input" of required type "[String]!" was not provided.', [new SourceLocation(2, 17)] ); try { - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => null])->toArray()); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => null])->toArray()); $this->fail('Expected exception not thrown'); } catch (Error $e) { $this->assertEquals($expected, Error::formatError($e)); } } + /** + * @it allows non-null lists to contain values + */ public function testAllowsNonNullListsToContainValues() { $doc = ' @@ -403,9 +487,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = ['data' => ['nnList' => '["A"]']]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => 'A'])->toArray()); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => 'A'])->toArray()); } + /** + * @it allows non-null lists to contain null + */ public function testAllowsNonNullListsToContainNull() { $doc = ' @@ -416,9 +503,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $ast = Parser::parse($doc); $expected = ['data' => ['nnList' => '["A",null,"B"]']]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A',null,'B']])->toArray()); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => ['A',null,'B']])->toArray()); } + /** + * @it allows lists of non-nulls to be null + */ public function testAllowsListsOfNonNullsToBeNull() { $doc = ' @@ -431,6 +521,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => null])->toArray()); } + /** + * @it allows lists of non-nulls to contain values + */ public function testAllowsListsOfNonNullsToContainValues() { $doc = ' @@ -441,9 +534,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase $ast = Parser::parse($doc); $expected = ['data' => ['listNN' => '["A"]']]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => 'A'])->toArray()); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => 'A'])->toArray()); } + /** + * @it does not allow lists of non-nulls to contain null + */ public function testDoesNotAllowListsOfNonNullsToContainNull() { $doc = ' @@ -453,18 +549,22 @@ class VariablesTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = FormattedError::create( - 'Variable $input expected value of type [String!] but got: ["A",null,"B"].', + 'Variable "$input" got invalid value ["A",null,"B"].' . "\n" . + 'In element #1: Expected "String!", found null.', [new SourceLocation(2, 17)] ); try { - Executor::execute($this->schema(), $ast, null, ['input' => ['A', null, 'B']]); + Executor::execute($this->schema(), $ast, null, null, ['input' => ['A', null, 'B']]); $this->fail('Expected exception not thrown'); } catch (Error $e) { $this->assertEquals($expected, Error::formatError($e)); } } + /** + * @it does not allow non-null lists of non-nulls to be null + */ public function testDoesNotAllowNonNullListsOfNonNullsToBeNull() { $doc = ' @@ -474,16 +574,20 @@ class VariablesTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = FormattedError::create( - 'Variable $input expected value of type [String!]! but got: null.', + 'Variable "$input" of required type "[String!]!" was not provided.', [new SourceLocation(2, 17)] ); try { - Executor::execute($this->schema(), $ast, null, ['input' => null]); + Executor::execute($this->schema(), $ast, null, null, ['input' => null]); + $this->fail('Expected exception not thrown'); } catch (Error $e) { $this->assertEquals($expected, Error::formatError($e)); } } + /** + * @it allows non-null lists of non-nulls to contain values + */ public function testAllowsNonNullListsOfNonNullsToContainValues() { $doc = ' @@ -493,9 +597,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = ['data' => ['nnListNN' => '["A"]']]; - $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A']])->toArray()); + $this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => ['A']])->toArray()); } + /** + * @it does not allow non-null lists of non-nulls to contain null + */ public function testDoesNotAllowNonNullListsOfNonNullsToContainNull() { $doc = ' @@ -505,16 +612,116 @@ class VariablesTest extends \PHPUnit_Framework_TestCase '; $ast = Parser::parse($doc); $expected = FormattedError::create( - 'Variable $input expected value of type [String!]! but got: ["A",null,"B"].', + 'Variable "$input" got invalid value ["A",null,"B"].'."\n". + 'In element #1: Expected "String!", found null.', [new SourceLocation(2, 17)] ); try { - Executor::execute($this->schema(), $ast, null, ['input' => ['A', null, 'B']]); + Executor::execute($this->schema(), $ast, null, null, ['input' => ['A', null, 'B']]); + $this->fail('Expected exception not thrown'); } catch (Error $e) { $this->assertEquals($expected, Error::formatError($e)); } } + /** + * @it does not allow invalid types to be used as values + */ + public function testDoesNotAllowInvalidTypesToBeUsedAsValues() + { + $doc = ' + query q($input: TestType!) { + fieldWithObjectInput(input: $input) + } + '; + $ast = Parser::parse($doc); + $vars = [ 'input' => [ 'list' => [ 'A', 'B' ] ] ]; + + try { + Executor::execute($this->schema(), $ast, null, null, $vars); + $this->fail('Expected exception not thrown'); + } catch (Error $error) { + $expected = FormattedError::create( + 'Variable "$input" expected value of type "TestType!" which cannot ' . + 'be used as an input type.', + [new SourceLocation(2, 17)] + ); + $this->assertEquals($expected, Error::formatError($error)); + } + } + + /** + * @it does not allow unknown types to be used as values + */ + public function testDoesNotAllowUnknownTypesToBeUsedAsValues() + { + $doc = ' + query q($input: UnknownType!) { + fieldWithObjectInput(input: $input) + } + '; + $ast = Parser::parse($doc); + $vars = ['input' => 'whoknows']; + + try { + Executor::execute($this->schema(), $ast, null, null, $vars); + $this->fail('Expected exception not thrown'); + } catch (Error $error) { + $expected = FormattedError::create( + 'Variable "$input" expected value of type "UnknownType!" which ' . + 'cannot be used as an input type.', + [new SourceLocation(2, 17)] + ); + $this->assertEquals($expected, Error::formatError($error)); + } + } + + // Describe: Execute: Uses argument default values + /** + * @it when no argument provided + */ + public function testWhenNoArgumentProvided() + { + $ast = Parser::parse('{ + fieldWithDefaultArgumentValue + }'); + + $this->assertEquals( + ['data' => ['fieldWithDefaultArgumentValue' => '"Hello World"']], + Executor::execute($this->schema(), $ast)->toArray() + ); + } + + /** + * @it when nullable variable provided + */ + public function testWhenNullableVariableProvided() + { + $ast = Parser::parse('query optionalVariable($optional: String) { + fieldWithDefaultArgumentValue(input: $optional) + }'); + + $this->assertEquals( + ['data' => ['fieldWithDefaultArgumentValue' => '"Hello World"']], + Executor::execute($this->schema(), $ast)->toArray() + ); + } + + /** + * @it when argument provided cannot be parsed + */ + public function testWhenArgumentProvidedCannotBeParsed() + { + $ast = Parser::parse('{ + fieldWithDefaultArgumentValue(input: WRONG_TYPE) + }'); + + $this->assertEquals( + ['data' => ['fieldWithDefaultArgumentValue' => '"Hello World"']], + Executor::execute($this->schema(), $ast)->toArray() + ); + } + public function schema() { @@ -530,6 +737,14 @@ class VariablesTest extends \PHPUnit_Framework_TestCase ] ]); + $TestNestedInputObject = new InputObjectType([ + 'name' => 'TestNestedInputObject', + 'fields' => [ + 'na' => [ 'type' => Type::nonNull($TestInputObject) ], + 'nb' => [ 'type' => Type::nonNull(Type::string()) ], + ], + ]); + $TestType = new ObjectType([ 'name' => 'TestType', 'fields' => [ @@ -561,6 +776,18 @@ class VariablesTest extends \PHPUnit_Framework_TestCase return isset($args['input']) ? json_encode($args['input']) : null; } ], + 'fieldWithNestedInputObject' => [ + 'type' => Type::string(), + 'args' => [ + 'input' => [ + 'type' => $TestNestedInputObject, + 'defaultValue' => 'Hello World' + ] + ], + 'resolve' => function($_, $args) { + return isset($args['input']) ? json_encode($args['input']) : null; + } + ], 'list' => [ 'type' => Type::string(), 'args' => ['input' => ['type' => Type::listOf(Type::string())]], @@ -592,7 +819,7 @@ class VariablesTest extends \PHPUnit_Framework_TestCase ] ]); - $schema = new Schema($TestType); + $schema = new Schema(['query' => $TestType]); return $schema; } } diff --git a/tests/StarWarsIntrospectionTest.php b/tests/StarWarsIntrospectionTest.php index bdd7f03..53c67ed 100644 --- a/tests/StarWarsIntrospectionTest.php +++ b/tests/StarWarsIntrospectionTest.php @@ -8,7 +8,10 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase { // Star Wars Introspection Tests // Basic Introspection - // it('Allows querying the schema for types') + + /** + * @it Allows querying the schema for types + */ public function testAllowsQueryingTheSchemaForTypes() { $query = ' @@ -26,8 +29,8 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase ['name' => 'Query'], ['name' => 'Episode'], ['name' => 'Character'], - ['name' => 'Human'], ['name' => 'String'], + ['name' => 'Human'], ['name' => 'Droid'], ['name' => '__Schema'], ['name' => '__Type'], @@ -37,6 +40,7 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase ['name' => '__InputValue'], ['name' => '__EnumValue'], ['name' => '__Directive'], + ['name' => '__DirectiveLocation'], ['name' => 'ID'], ['name' => 'Float'], ['name' => 'Int'] @@ -46,7 +50,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase $this->assertValidQuery($query, $expected); } - // it('Allows querying the schema for query type') + /** + * @it Allows querying the schema for query type + */ public function testAllowsQueryingTheSchemaForQueryType() { $query = ' @@ -68,7 +74,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase $this->assertValidQuery($query, $expected); } - // it('Allows querying the schema for a specific type') + /** + * @it Allows querying the schema for a specific type + */ public function testAllowsQueryingTheSchemaForASpecificType() { $query = ' @@ -86,7 +94,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase $this->assertValidQuery($query, $expected); } - // it('Allows querying the schema for an object kind') + /** + * @it Allows querying the schema for an object kind + */ public function testAllowsQueryingForAnObjectKind() { $query = ' @@ -106,7 +116,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase $this->assertValidQuery($query, $expected); } - // it('Allows querying the schema for an interface kind') + /** + * @it Allows querying the schema for an interface kind + */ public function testAllowsQueryingForInterfaceKind() { $query = ' @@ -126,7 +138,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase $this->assertValidQuery($query, $expected); } - // it('Allows querying the schema for object fields') + /** + * @it Allows querying the schema for object fields + */ public function testAllowsQueryingForObjectFields() { $query = ' @@ -188,7 +202,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase $this->assertValidQuery($query, $expected); } - // it('Allows querying the schema for nested object fields') + /** + * @it Allows querying the schema for nested object fields + */ public function testAllowsQueryingTheSchemaForNestedObjectFields() { $query = ' @@ -268,6 +284,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase $this->assertValidQuery($query, $expected); } + /** + * @it Allows querying the schema for field args + */ public function testAllowsQueryingTheSchemaForFieldArgs() { $query = ' @@ -359,7 +378,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase $this->assertValidQuery($query, $expected); } - // it('Allows querying the schema for documentation') + /** + * @it Allows querying the schema for documentation + */ public function testAllowsQueryingTheSchemaForDocumentation() { $query = ' diff --git a/tests/StarWarsQueryTest.php b/tests/StarWarsQueryTest.php index 1eff3be..206ef5d 100644 --- a/tests/StarWarsQueryTest.php +++ b/tests/StarWarsQueryTest.php @@ -9,9 +9,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase // Star Wars Query Tests // Basic Queries + /** + * @it Correctly identifies R2-D2 as the hero of the Star Wars Saga + */ public function testCorrectlyIdentifiesR2D2AsTheHeroOfTheStarWarsSaga() { - // Correctly identifies R2-D2 as the hero of the Star Wars Saga $query = ' query HeroNameQuery { hero { @@ -27,6 +29,9 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase $this->assertValidQuery($query, $expected); } + /** + * @it Allows us to query for the ID and friends of R2-D2 + */ public function testAllowsUsToQueryForTheIDAndFriendsOfR2D2() { $query = ' @@ -60,7 +65,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase $this->assertValidQuery($query, $expected); } - // Nested Queries + // Describe: Nested Queries + + /** + * @it Allows us to query for the friends of friends of R2-D2 + */ public function testAllowsUsToQueryForTheFriendsOfFriendsOfR2D2() { $query = ' @@ -117,7 +126,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase $this->assertValidQuery($query, $expected); } - // Using IDs and query parameters to refetch objects + // Describe: Using IDs and query parameters to refetch objects + + /** + * @it Using IDs and query parameters to refetch objects + */ public function testAllowsUsToQueryForLukeSkywalkerDirectlyUsingHisID() { $query = ' @@ -136,9 +149,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase $this->assertValidQuery($query, $expected); } + /** + * @it Allows us to create a generic query, then use it to fetch Luke Skywalker using his ID + */ public function testGenericQueryToGetLukeSkywalkerById() { - // Allows us to create a generic query, then use it to fetch Luke Skywalker using his ID $query = ' query FetchSomeIDQuery($someId: String!) { human(id: $someId) { @@ -158,9 +173,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase $this->assertValidQueryWithParams($query, $params, $expected); } + /** + * @it Allows us to create a generic query, then use it to fetch Han Solo using his ID + */ public function testGenericQueryToGetHanSoloById() { - // Allows us to create a generic query, then use it to fetch Han Solo using his ID $query = ' query FetchSomeIDQuery($someId: String!) { human(id: $someId) { @@ -179,9 +196,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase $this->assertValidQueryWithParams($query, $params, $expected); } + /** + * @it Allows us to create a generic query, then pass an invalid ID to get null back + */ public function testGenericQueryWithInvalidId() { - // Allows us to create a generic query, then pass an invalid ID to get null back $query = ' query humanQuery($id: String!) { human(id: $id) { @@ -199,9 +218,12 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase } // Using aliases to change the key in the response + + /** + * @it Allows us to query for Luke, changing his key with an alias + */ function testLukeKeyAlias() { - // Allows us to query for Luke, changing his key with an alias $query = ' query FetchLukeAliased { luke: human(id: "1000") { @@ -217,9 +239,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase $this->assertValidQuery($query, $expected); } + /** + * @it Allows us to query for both Luke and Leia, using two root fields and an alias + */ function testTwoRootKeysAsAnAlias() { - // Allows us to query for both Luke and Leia, using two root fields and an alias $query = ' query FetchLukeAndLeiaAliased { luke: human(id: "1000") { @@ -242,9 +266,12 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase } // Uses fragments to express more complex queries + + /** + * @it Allows us to query using duplicated content + */ function testQueryUsingDuplicatedContent() { - // Allows us to query using duplicated content $query = ' query DuplicateFields { luke: human(id: "1000") { @@ -270,9 +297,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase $this->assertValidQuery($query, $expected); } + /** + * @it Allows us to use a fragment to avoid duplicating content + */ function testUsingFragment() { - // Allows us to use a fragment to avoid duplicating content $query = ' query UseFragment { luke: human(id: "1000") { @@ -302,58 +331,9 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase $this->assertValidQuery($query, $expected); } - function testFragmentWithProjection() - { - $query = ' - query UseFragment { - human(id: "1003") { - name - ...fa - ...fb - } - } - - fragment fa on Character { - friends { - id - } - } - fragment fb on Character { - friends { - name - } - } - '; - - $expected = [ - 'human' => [ - 'name' => 'Leia Organa', - 'friends' => [ - [ - 'name' => 'Luke Skywalker', - 'id' => '1000' - - ], - [ - 'name' => 'Han Solo', - 'id' => '1002' - ], - [ - 'name' => 'C-3PO', - 'id' => '2000' - ], - [ - 'name' => 'R2-D2', - 'id' => '2001' - ] - ] - ] - ]; - - $this->assertValidQuery($query, $expected); - } - - // Using __typename to find the type of an object + /** + * @it Using __typename to find the type of an object + */ public function testVerifyThatR2D2IsADroid() { $query = ' @@ -373,6 +353,9 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase $this->assertValidQuery($query, $expected); } + /** + * @it Allows us to verify that Luke is a human + */ public function testVerifyThatLukeIsHuman() { $query = ' @@ -407,6 +390,6 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase */ private function assertValidQueryWithParams($query, $params, $expected) { - $this->assertEquals(['data' => $expected], GraphQL::execute(StarWarsSchema::build(), $query, null, $params)); + $this->assertEquals(['data' => $expected], GraphQL::execute(StarWarsSchema::build(), $query, null, null, $params)); } } diff --git a/tests/StarWarsSchema.php b/tests/StarWarsSchema.php index dc2a572..205ff62 100644 --- a/tests/StarWarsSchema.php +++ b/tests/StarWarsSchema.php @@ -291,6 +291,6 @@ class StarWarsSchema ] ]); - return new Schema($queryType); + return new Schema(['query' => $queryType]); } } diff --git a/tests/StarWarsValidationTest.php b/tests/StarWarsValidationTest.php index 8a68168..e2a2a40 100644 --- a/tests/StarWarsValidationTest.php +++ b/tests/StarWarsValidationTest.php @@ -8,6 +8,10 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase { // Star Wars Validation Tests // Basic Queries + + /** + * @it Validates a complex but valid query + */ public function testValidatesAComplexButValidQuery() { $query = ' @@ -32,9 +36,11 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase $this->assertEquals(true, empty($errors)); } + /** + * @it Notes that non-existent fields are invalid + */ public function testThatNonExistentFieldsAreInvalid() { - // Notes that non-existent fields are invalid $query = ' query HeroSpaceshipQuery { hero { @@ -46,6 +52,9 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase $this->assertEquals(false, empty($errors)); } + /** + * @it Requires fields on objects + */ public function testRequiresFieldsOnObjects() { $query = ' @@ -58,9 +67,11 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase $this->assertEquals(false, empty($errors)); } + /** + * @it Disallows fields on scalars + */ public function testDisallowsFieldsOnScalars() { - $query = ' query HeroFieldsOnScalarQuery { hero { @@ -74,6 +85,9 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase $this->assertEquals(false, empty($errors)); } + /** + * @it Disallows object fields on interfaces + */ public function testDisallowsObjectFieldsOnInterfaces() { $query = ' @@ -88,6 +102,9 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase $this->assertEquals(false, empty($errors)); } + /** + * @it Allows object fields in fragments + */ public function testAllowsObjectFieldsInFragments() { $query = ' @@ -106,6 +123,9 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase $this->assertEquals(true, empty($errors)); } + /** + * @it Allows object fields in inline fragments + */ public function testAllowsObjectFieldsInInlineFragments() { $query = ' diff --git a/tests/Type/DefinitionTest.php b/tests/Type/DefinitionTest.php index 767535d..62f5b5f 100644 --- a/tests/Type/DefinitionTest.php +++ b/tests/Type/DefinitionTest.php @@ -11,6 +11,7 @@ use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; +use GraphQL\Utils; class DefinitionTest extends \PHPUnit_Framework_TestCase { @@ -415,7 +416,7 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase $this->fail('Expected exception not thrown'); } catch (\Exception $e) { $this->assertSame( - 'Error in "BadUnion" type definition: expecting callable or instance of GraphQL\Type\Definition\ObjectType at "types:0", but got "' . get_class($type) . '"', + 'Error in "BadUnion" type definition: expecting callable or instance of GraphQL\Type\Definition\ObjectType at "types:0", but got "' . Utils::getVariableType($type) . '"', $e->getMessage() ); } diff --git a/tests/Type/EnumTypeTest.php b/tests/Type/EnumTypeTest.php index a7f2be2..cf060b3 100644 --- a/tests/Type/EnumTypeTest.php +++ b/tests/Type/EnumTypeTest.php @@ -189,6 +189,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase $this->schema, 'query test($color: Color!) { colorEnum(fromEnum: $color) }', null, + null, ['color' => 'BLUE'] ) ); @@ -205,6 +206,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase $this->schema, 'mutation x($color: Color!) { favoriteEnum(color: $color) }', null, + null, ['color' => 'GREEN'] ) ); @@ -224,6 +226,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase $this->schema, 'subscription x($color: Color!) { subscribeToEnum(color: $color) }', null, + null, ['color' => 'GREEN'] ) ); @@ -295,7 +298,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase private function expectFailure($query, $vars, $err) { - $result = GraphQL::executeAndReturnResult($this->schema, $query, null, $vars); + $result = GraphQL::executeAndReturnResult($this->schema, $query, null, null, $vars); $this->assertEquals(1, count($result->errors)); $this->assertEquals( diff --git a/tests/Type/IntrospectionTest.php b/tests/Type/IntrospectionTest.php index e9a9684..0822a77 100644 --- a/tests/Type/IntrospectionTest.php +++ b/tests/Type/IntrospectionTest.php @@ -1099,9 +1099,6 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase $actual = GraphQL::execute($emptySchema, $request); - // print_r($actual); - // exit; - $this->assertEquals($expected, $actual); } diff --git a/tests/Type/ResolveInfoTest.php b/tests/Type/ResolveInfoTest.php index fe92137..31c956b 100644 --- a/tests/Type/ResolveInfoTest.php +++ b/tests/Type/ResolveInfoTest.php @@ -135,7 +135,7 @@ class ResolveInfoTest extends \PHPUnit_Framework_TestCase 'fields' => [ 'article' => [ 'type' => $article, - 'resolve' => function($value, $args, ResolveInfo $info) use (&$hasCalled, &$actualDefaultSelection, &$actualDeepSelection) { + 'resolve' => function($value, $args, $context, ResolveInfo $info) use (&$hasCalled, &$actualDefaultSelection, &$actualDeepSelection) { $hasCalled = true; $actualDefaultSelection = $info->getFieldSelection(); $actualDeepSelection = $info->getFieldSelection(5); @@ -145,7 +145,7 @@ class ResolveInfoTest extends \PHPUnit_Framework_TestCase ] ]); - $schema = new Schema($blogQuery); + $schema = new Schema(['query' => $blogQuery]); $result = GraphQL::execute($schema, $doc); $this->assertTrue($hasCalled); diff --git a/tests/Type/SchemaValidatorTest.php b/tests/Type/SchemaValidatorTest.php index 87408e5..b8c3e77 100644 --- a/tests/Type/SchemaValidatorTest.php +++ b/tests/Type/SchemaValidatorTest.php @@ -2,6 +2,8 @@ namespace GraphQL\Tests\Type; use GraphQL\Schema; +use GraphQL\Type\Definition\CustomScalarType; +use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ListOfType; @@ -11,63 +13,402 @@ use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Introspection; use GraphQL\Type\SchemaValidator; +use GraphQL\Utils; class SchemaValidatorTest extends \PHPUnit_Framework_TestCase { - public $someInputType; + private $someInputObjectType; + + private $someScalarType; + + private $someObjectType; + + private $objectWithIsTypeOf; + + private $someEnumType; + + private $someUnionType; + + private $someInterfaceType; + + private $outputTypes; + + private $noOutputTypes; + + private $inputTypes; + + private $noInputTypes; public function setUp() { - $this->someInputType = new InputObjectType([ - 'name' => 'SomeInputType', + $this->someScalarType = new CustomScalarType([ + 'name' => 'SomeScalar', + 'serialize' => function() {}, + 'parseValue' => function() {}, + 'parseLiteral' => function() {} + ]); + + $this->someObjectType = new ObjectType([ + 'name' => 'SomeObject', + 'fields' => ['f' => ['type' => Type::string()]] + ]); + + $this->objectWithIsTypeOf = new ObjectType([ + 'name' => 'ObjectWithIsTypeOf', + 'isTypeOf' => function() {return true;}, + 'fields' => ['f' => ['type' => Type::string()]] + ]); + + $this->someUnionType = new UnionType([ + 'name' => 'SomeUnion', + 'resolveType' => function() {return null;}, + 'types' => [$this->someObjectType] + ]); + + $this->someInterfaceType = new InterfaceType([ + 'name' => 'SomeInterface', + 'resolveType' => function() {return null;}, + 'fields' => ['f' => ['type' => Type::string()]] + ]); + + $this->someEnumType = new EnumType([ + 'name' => 'SomeEnum', + 'resolveType' => function() {return null;}, + 'fields' => ['f' => ['type' => Type::string()]] + ]); + + $this->someInputObjectType = new InputObjectType([ + 'name' => 'SomeInputObject', 'fields' => [ - 'val' => [ 'type' => Type::float(), 'defaultValue' => 42 ] + 'val' => ['type' => Type::float(), 'defaultValue' => 42] ] ]); + + $this->outputTypes = $this->withModifiers([ + Type::string(), + $this->someScalarType, + $this->someEnumType, + $this->someObjectType, + $this->someUnionType, + $this->someInterfaceType + ]); + + $this->noOutputTypes = $this->withModifiers([ + $this->someInputObjectType + ]); + $this->noOutputTypes[] = 'SomeString'; + + $this->inputTypes = $this->withModifiers([ + Type::string(), + $this->someScalarType, + $this->someEnumType, + $this->someInputObjectType + ]); + + $this->noInputTypes = $this->withModifiers([ + $this->someObjectType, + $this->someUnionType, + $this->someInterfaceType + ]); + $this->noInputTypes[] = 'SomeString'; + } + + private function withModifiers($types) + { + return array_merge( + Utils::map($types, function($type) {return Type::listOf($type);}), + Utils::map($types, function($type) {return Type::nonNull($type);}), + Utils::map($types, function($type) {return Type::nonNull(Type::listOf($type));}) + ); + } + + private function schemaWithFieldType($type) + { + return [ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => ['f' => ['type' => $type]] + ]), + 'types' => [$type], + ]; + } + + private function expectPasses($schemaConfig) + { + $schema = new Schema(['validate' => true] + $schemaConfig); + $errors = SchemaValidator::validate($schema); + $this->assertEquals([], $errors); + } + + private function expectFails($schemaConfig, $error) + { + try { + $schema = new Schema($schemaConfig); + $errors = SchemaValidator::validate($schema); + if ($errors) { + throw $errors[0]; + } + $this->fail('Expected exception not thrown'); + } catch (\Exception $e) { + $this->assertEquals($e->getMessage(), $error); + } + } + + // Type System: A Schema must have Object root types + + /** + * @it accepts a Schema whose query type is an object type + */ + public function testAcceptsSchemaWithQueryTypeOfObjectType() + { + $this->expectPasses([ + 'query' => $this->someObjectType + ]); } + /** + * @it accepts a Schema whose query and mutation types are object types + */ + public function testAcceptsSchemaWithQueryAndMutationTypesOfObjectType() + { + $MutationType = new ObjectType([ + 'name' => 'Mutation', + 'fields' => ['edit' => ['type' => Type::string()]] + ]); + + $this->expectPasses([ + 'query' => $this->someObjectType, + 'mutation' => $MutationType + ]); + } + + /** + * @it accepts a Schema whose query and subscription types are object types + */ + public function testAcceptsSchemaWhoseQueryAndSubscriptionTypesAreObjectTypes() + { + $SubscriptionType = new ObjectType([ + 'name' => 'Subscription', + 'fields' => ['subscribe' => ['type' => Type::string()]] + ]); + + $this->expectPasses([ + 'query' => $this->someObjectType, + 'subscription' => $SubscriptionType + ]); + } + + /** + * @it rejects a Schema without a query type + */ + public function testRejectsSchemaWithoutQueryType() + { + $this->expectFails([], 'Schema query must be Object Type but got: NULL'); + } + + /** + * @it rejects a Schema whose query type is an input type + */ + public function testRejectsSchemaWhoseQueryTypeIsAnInputType() + { + $this->expectFails( + ['query' => $this->someInputObjectType], + 'Schema query must be Object Type but got: SomeInputObject' + ); + } + + /** + * @it rejects a Schema whose mutation type is an input type + */ + public function testRejectsSchemaWhoseMutationTypeIsInputType() + { + $this->expectFails( + ['query' => $this->someObjectType, 'mutation' => $this->someInputObjectType], + 'Schema mutation must be Object Type if provided but got: SomeInputObject' + ); + } + + /** + * @it rejects a Schema whose subscription type is an input type + */ + public function testRejectsSchemaWhoseSubscriptionTypeIsInputType() + { + $this->expectFails( + [ + 'query' => $this->someObjectType, + 'subscription' => $this->someInputObjectType + ], + 'Schema subscription must be Object Type if provided but got: SomeInputObject' + ); + } + + /** + * @it rejects a Schema whose directives are incorrectly typed + */ + public function testRejectsSchemaWhoseDirectivesAreIncorrectlyTyped() + { + $this->expectFails( + [ + 'query' => $this->someObjectType, + 'directives' => [ 'somedirective' ] + ], + 'Schema directives must be Directive[] if provided but got array' + ); + } + + // Type System: A Schema must contain uniquely named types + + /** + * @it rejects a Schema which redefines a built-in type + */ + public function testRejectsSchemaWhichRedefinesBuiltInType() + { + $FakeString = new CustomScalarType([ + 'name' => 'String', + 'serialize' => function() {return null;}, + ]); + + $QueryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'normal' => [ 'type' => Type::string() ], + 'fake' => [ 'type' => $FakeString ], + ] + ]); + + $this->expectFails( + [ 'query' => $QueryType ], + 'Schema must contain unique named types but contains multiple types named "String".' + ); + } + + /** + * @it rejects a Schema which defines an object type twice + */ + public function testRejectsSchemaWhichDefinesObjectTypeTwice() + { + $A = new ObjectType([ + 'name' => 'SameName', + 'fields' => ['f' => ['type' => Type::string()]], + ]); + + $B = new ObjectType([ + 'name' => 'SameName', + 'fields' => ['f' => ['type' => Type::string()]], + ]); + + $QueryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'a' => ['type' => $A], + 'b' => ['type' => $B] + ] + ]); + + $this->expectFails( + ['query' => $QueryType], + 'Schema must contain unique named types but contains multiple types named "SameName".' + ); + } + + /** + * @it rejects a Schema which have same named objects implementing an interface + */ + public function testRejectsSchemaWhichHaveSameNamedObjectsImplementingInterface() + { + $AnotherInterface = new InterfaceType([ + 'name' => 'AnotherInterface', + 'resolveType' => function () { + return null; + }, + 'fields' => ['f' => ['type' => Type::string()]], + ]); + + $FirstBadObject = new ObjectType([ + 'name' => 'BadObject', + 'interfaces' => [$AnotherInterface], + 'fields' => ['f' => ['type' => Type::string()]], + ]); + + $SecondBadObject = new ObjectType([ + 'name' => 'BadObject', + 'interfaces' => [$AnotherInterface], + 'fields' => ['f' => ['type' => Type::string()]], + ]); + + $QueryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'iface' => ['type' => $AnotherInterface], + ] + ]); + + $this->expectFails( + [ + 'query' => $QueryType, + 'types' => [$FirstBadObject, $SecondBadObject] + ], + 'Schema must contain unique named types but contains multiple types named "BadObject".' + ); + } + + // Type System: Objects must have fields + + /** + * @it accepts an Object type with fields object + */ + public function testAcceptsAnObjectTypeWithFieldsObject() + { + $schemaConfig = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'fields' => [ + 'f' => [ 'type' => Type::string() ] + ] + ])); + $this->expectPasses($schemaConfig); + } + + /** + * @it accepts an Object type with a field function + */ + public function testAcceptsAnObjectTypeWithFieldFunction() + { + $schemaConfig = $this->schemaWithFieldType(new ObjectType([ + 'name' => 'SomeObject', + 'fields' => function() { + return [ + 'f' => ['type' => Type::string()] + ]; + } + ])); + $this->expectPasses($schemaConfig); + } // Type System Config public function testPassesOnTheIntrospectionSchema() { - $schema = new Schema(Introspection::_schema()); - $errors = SchemaValidator::validate($schema); - $this->assertEmpty($errors); - } - - - // Rule: NoInputTypesAsOutputFields - public function testRejectsSchemaWhoseQueryOrMutationTypeIsAnInputType() - { - $schema = new Schema($this->someInputType); - $validationResult = SchemaValidator::validate($schema, [SchemaValidator::noInputTypesAsOutputFieldsRule()]); - $this->checkValidationResult($validationResult, 'query'); - - $schema = new Schema(null, $this->someInputType); - $validationResult = SchemaValidator::validate($schema, [SchemaValidator::noInputTypesAsOutputFieldsRule()]); - $this->checkValidationResult($validationResult, 'mutation'); + $this->expectPasses(['query' => Introspection::_schema()]); } public function testRejectsASchemaThatUsesAnInputTypeAsAField() { $kinds = [ 'GraphQL\Type\Definition\ObjectType', - 'GraphQL\Type\Definition\InterfaceType', ]; foreach ($kinds as $kind) { $someOutputType = new $kind([ 'name' => 'SomeOutputType', 'fields' => [ - 'sneaky' => ['type' => function() {return $this->someInputType;}] + 'sneaky' => ['type' => function() {return $this->someInputObjectType;}] ] ]); - $schema = new Schema($someOutputType); + $schema = new Schema(['query' => $someOutputType]); $validationResult = SchemaValidator::validate($schema, [SchemaValidator::noInputTypesAsOutputFieldsRule()]); $this->assertSame(1, count($validationResult)); $this->assertSame( - 'Field SomeOutputType.sneaky is of type SomeInputType, which is an ' . + 'Field SomeOutputType.sneaky is of type SomeInputObject, which is an ' . 'input type, but field types must be output types!', $validationResult[0]->message ); @@ -85,13 +426,13 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase 'name' => 'SomeOutputType', 'fields' => [ 'fieldWithArg' => [ - 'args' => ['someArg' => ['type' => $this->someInputType]], + 'args' => ['someArg' => ['type' => $this->someInputObjectType]], 'type' => Type::float() ] ] ]); - $schema = new Schema($someOutputType); + $schema = new Schema(['query' => $someOutputType]); $errors = SchemaValidator::validate($schema, [$rule]); $this->assertEmpty($errors); } @@ -101,7 +442,7 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase $this->assertNotEmpty($validationErrors, "Should not validate"); $this->assertEquals(1, count($validationErrors)); $this->assertEquals( - "Schema $operationType type SomeInputType must be an object type!", + "Schema $operationType must be Object Type but got: SomeInputObject.", $validationErrors[0]->message ); } @@ -153,7 +494,7 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase { // rejects a schema with a list of objects as an input field arg $listObjects = new ListOfType(new ObjectType([ - 'name' => 'SomeInputType', + 'name' => 'SomeInputObject', 'fields' => ['f' => ['type' => Type::float()]] ])); $this->assertRejectingFieldArgOfType($listObjects); @@ -174,7 +515,7 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase { // accepts a schema with a list of input type as an input field arg $this->assertAcceptingFieldArgOfType(new ListOfType(new InputObjectType([ - 'name' => 'SomeInputType' + 'name' => 'SomeInputObject' ]))); } @@ -182,7 +523,7 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase { // accepts a schema with a nonnull input type as an input field arg $this->assertAcceptingFieldArgOfType(new NonNull(new InputObjectType([ - 'name' => 'SomeInputType' + 'name' => 'SomeInputObject' ]))); } @@ -219,7 +560,7 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase ] ]); - return new Schema($queryType); + return new Schema(['query' => $queryType]); } private function expectRejectionBecauseFieldIsNotInputType($errors, $fieldTypeName) @@ -233,83 +574,8 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase } - // Rule: InterfacePossibleTypesMustImplementTheInterface - - public function testAcceptsInterfaceWithSubtypeDeclaredUsingOurInfra() - { - // accepts an interface with a subtype declared using our infra - $this->assertAcceptingAnInterfaceWithANormalSubtype(SchemaValidator::interfacePossibleTypesMustImplementTheInterfaceRule()); - } - public function testRejectsWhenAPossibleTypeDoesNotImplementTheInterface() { // TODO: Validation for interfaces / implementors } - - private function assertAcceptingAnInterfaceWithANormalSubtype($rule) - { - $interfaceType = new InterfaceType([ - 'name' => 'InterfaceType', - 'fields' => [] - ]); - - $subType = new ObjectType([ - 'name' => 'SubType', - 'fields' => [], - 'interfaces' => [$interfaceType] - ]); - - $schema = new Schema($interfaceType, $subType); - - $errors = SchemaValidator::validate($schema, [$rule]); - $this->assertEmpty($errors); - } - - - // Rule: TypesInterfacesMustShowThemAsPossible - - public function testAcceptsInterfaceWithASubtypeDeclaredUsingOurInfra() - { - // accepts an interface with a subtype declared using our infra - $this->assertAcceptingAnInterfaceWithANormalSubtype(SchemaValidator::typesInterfacesMustShowThemAsPossibleRule()); - } - - public function testRejectsWhenAnImplementationIsNotAPossibleType() - { - // rejects when an implementation is not a possible type - $interfaceType = new InterfaceType([ - 'name' => 'InterfaceType', - 'fields' => [] - ]); - - $subType = new ObjectType([ - 'name' => 'SubType', - 'fields' => [], - 'interfaces' => [] - ]); - - $tmp = new \ReflectionObject($subType); - $prop = $tmp->getProperty('_interfaces'); - $prop->setAccessible(true); - $prop->setValue($subType, [$interfaceType]); - - // Sanity check the test. - $this->assertEquals([$interfaceType], $subType->getInterfaces()); - $this->assertSame(false, $interfaceType->isPossibleType($subType)); - - // Need to make sure SubType is in the schema! We rely on - // possibleTypes to be able to see it unless it's explicitly used. - $schema = new Schema($interfaceType, $subType); - - // Another sanity check. - $this->assertSame($subType, $schema->getType('SubType')); - - $errors = SchemaValidator::validate($schema, [SchemaValidator::typesInterfacesMustShowThemAsPossibleRule()]); - $this->assertSame(1, count($errors)); - $this->assertSame( - 'SubType implements interface InterfaceType, but InterfaceType does ' . - 'not list it as possible!', - $errors[0]->message - ); - } }