Initial pass on standard server implementation (also deprecated current GraphQL\Server which is undocumented anyway)

This commit is contained in:
Vladimir Razuvaev 2017-07-14 19:08:47 +07:00
parent a3b40db0fb
commit 794d3672ef
11 changed files with 1312 additions and 616 deletions

View File

@ -8,53 +8,49 @@ use GraphQL\Utils\Utils;
/** /**
* Class FormattedError * Class FormattedError
* @todo move this class to Utils/ErrorUtils *
* @package GraphQL\Error * @package GraphQL\Error
*/ */
class FormattedError class FormattedError
{ {
/** /**
* @deprecated as of 8.0 * @param \Throwable $e
* @param $error * @param $debug
* @param SourceLocation[] $locations *
* @return array * @return array
*/ */
public static function create($error, array $locations = []) public static function createFromException($e, $debug = false)
{ {
$formatted = [ if ($e instanceof Error) {
'message' => $error $result = $e->toSerializableArray();
]; } else if ($e instanceof \ErrorException) {
$result = [
if (!empty($locations)) { 'message' => $e->getMessage(),
$formatted['locations'] = array_map(function($loc) { return $loc->toArray();}, $locations); ];
if ($debug) {
$result += [
'file' => $e->getFile(),
'line' => $e->getLine(),
'severity' => $e->getSeverity()
];
}
} else {
Utils::invariant(
$e instanceof \Exception || $e instanceof \Throwable,
"Expected exception, got %s",
Utils::getVariableType($e)
);
$result = [
'message' => $e->getMessage()
];
} }
return $formatted; if ($debug) {
} $debugging = $e->getPrevious() ?: $e;
$result['trace'] = static::toSafeTrace($debugging->getTrace());
}
/** return $result;
* @param \ErrorException $e
* @return array
*/
public static function createFromPHPError(\ErrorException $e)
{
return [
'message' => $e->getMessage(),
'severity' => $e->getSeverity(),
'trace' => self::toSafeTrace($e->getTrace())
];
}
/**
* @param \Throwable $e
* @return array
*/
public static function createFromException($e)
{
return [
'message' => $e->getMessage(),
'trace' => self::toSafeTrace($e->getTrace())
];
} }
/** /**
@ -133,4 +129,37 @@ class FormattedError
} }
return gettype($var); return gettype($var);
} }
/**
* @deprecated as of v0.8.0
* @param $error
* @param SourceLocation[] $locations
* @return array
*/
public static function create($error, array $locations = [])
{
$formatted = [
'message' => $error
];
if (!empty($locations)) {
$formatted['locations'] = array_map(function($loc) { return $loc->toArray();}, $locations);
}
return $formatted;
}
/**
* @param \ErrorException $e
* @deprecated as of v0.10.0, use general purpose method createFromException() instead
* @return array
*/
public static function createFromPHPError(\ErrorException $e)
{
return [
'message' => $e->getMessage(),
'severity' => $e->getSeverity(),
'trace' => self::toSafeTrace($e->getTrace())
];
}
} }

View File

@ -15,6 +15,11 @@ use GraphQL\Type\Resolution;
use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\DocumentValidator;
use GraphQL\Utils\Utils; use GraphQL\Utils\Utils;
trigger_error(
'GraphQL\Server is deprecated in favor of new implementation: GraphQL\Server\StandardServer and will be removed in next version',
E_USER_DEPRECATED
);
class Server class Server
{ {
const DEBUG_PHP_ERRORS = 1; const DEBUG_PHP_ERRORS = 1;

191
src/Server/Helper.php Normal file
View File

@ -0,0 +1,191 @@
<?php
namespace GraphQL\Server;
use GraphQL\Error\FormattedError;
use GraphQL\Error\InvariantViolation;
use GraphQL\Error\UserError;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Promise\Promise;
use GraphQL\GraphQL;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\Parser;
use GraphQL\Utils\AST;
use GraphQL\Utils\Utils;
/**
* Class Helper
* Contains functionality that could be re-used by various server implementations
*
* @package GraphQL\Server
*/
class Helper
{
/**
* Executes GraphQL operation with given server configuration and returns execution result (or promise)
*
* @param ServerConfig $config
* @param OperationParams $op
*
* @return ExecutionResult|Promise
*/
public static function executeOperation(ServerConfig $config, OperationParams $op)
{
$phpErrors = [];
$execute = function() use ($config, $op) {
$doc = $op->queryId ? static::loadPersistedQuery($config, $op) : $op->query;
if (!$doc instanceof DocumentNode) {
$doc = Parser::parse($doc);
}
if (!$op->allowsMutation() && AST::isMutation($op->operation, $doc)) {
throw new UserError("Cannot execute mutation in read-only context");
}
return GraphQL::executeAndReturnResult(
$config->getSchema(),
$doc,
$config->getRootValue(),
$config->getContext(),
$op->variables,
$op->operation,
$config->getDefaultFieldResolver(),
static::resolveValidationRules($config, $op),
$config->getPromiseAdapter()
);
};
if ($config->getDebug()) {
$execute = Utils::withErrorHandling($execute, $phpErrors);
}
$result = $execute();
$applyErrorFormatting = function(ExecutionResult $result) use ($config, $phpErrors) {
if ($config->getDebug()) {
$errorFormatter = function($e) {
return FormattedError::createFromException($e, true);
};
} else {
$errorFormatter = $config->getErrorFormatter();
}
if (!empty($phpErrors)) {
$result->extensions['phpErrors'] = array_map($errorFormatter, $phpErrors);
}
$result->setErrorFormatter($errorFormatter);
return $result;
};
return $result instanceof Promise ?
$result->then($applyErrorFormatting) :
$applyErrorFormatting($result);
}
/**
* @param ServerConfig $config
* @param OperationParams $op
* @return string|DocumentNode
*/
private static function loadPersistedQuery(ServerConfig $config, OperationParams $op)
{
// Load query if we got persisted query id:
$loader = $config->getPersistentQueryLoader();
if (!$loader) {
throw new UserError("Persisted queries are not supported by this server");
}
$source = $loader($op->queryId, $op);
if (!is_string($source) && !$source instanceof DocumentNode) {
throw new InvariantViolation(sprintf(
"Persistent query loader must return query string or instance of %s but got: %s",
DocumentNode::class,
Utils::printSafe($source)
));
}
return $source;
}
/**
* @param ServerConfig $config
* @param OperationParams $params
* @return array
*/
private static function resolveValidationRules(ServerConfig $config, OperationParams $params)
{
// Allow customizing validation rules per operation:
$validationRules = $config->getValidationRules();
if (is_callable($validationRules)) {
$validationRules = $validationRules($params);
if (!is_array($validationRules)) {
throw new InvariantViolation(
"Validation rules callable must return array of rules, but got: %s" .
Utils::printSafe($validationRules)
);
}
}
return $validationRules;
}
/**
* Parses HTTP request and returns GraphQL QueryParams contained in this request.
* For batched requests it returns an array of QueryParams.
*
* @return OperationParams|OperationParams[]
*/
public static function parseHttpRequest()
{
$contentType = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : null;
$assertValid = function (OperationParams $opParams, $queryNum = null) {
$errors = $opParams->validate();
if (!empty($errors[0])) {
$err = $queryNum ? "Error in query #$queryNum: {$errors[0]}" : $errors[0];
throw new UserError($err);
}
};
if (stripos($contentType, 'application/graphql' !== false)) {
$body = file_get_contents('php://input') ?: '';
$op = OperationParams::create(['query' => $body]);
$assertValid($op);
} else if (stripos($contentType, 'application/json') !== false || stripos($contentType, 'text/json') !== false) {
$body = file_get_contents('php://input') ?: '';
$data = json_decode($body, true);
if (json_last_error()) {
throw new UserError("Could not parse JSON: " . json_last_error_msg());
}
if (!is_array($data)) {
throw new UserError(
"GraphQL Server expects JSON object or array, but got %s" .
Utils::printSafe($data)
);
}
if (isset($data[0])) {
$op = [];
foreach ($data as $index => $entry) {
$params = OperationParams::create($entry);
$assertValid($params, $index);
$op[] = $params;
}
} else {
$op = OperationParams::create($data);
$assertValid($op);
}
} else if (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$op = OperationParams::create($_GET, false);
} else {
$op = OperationParams::create($_POST);
}
$assertValid($op);
} else {
throw new UserError("Bad request: unexpected content type: " . Utils::printSafe($contentType));
}
return $op;
}
}

View File

@ -0,0 +1,120 @@
<?php
namespace GraphQL\Server;
use GraphQL\Utils;
/**
* Class QueryParams
* Represents all available parsed query parameters
*
* @package GraphQL\Server
*/
class OperationParams
{
/**
* @var string
*/
public $query;
/**
* @var string
*/
public $queryId;
/**
* @var string
*/
public $operation;
/**
* @var array
*/
public $variables;
/**
* @var array
*/
private $originalInput;
/**
* @var bool
*/
private $allowsMutations;
/**
* Creates an instance from given array
*
* @param array $params
* @param bool $allowsMutations
*
* @return static
*/
public static function create(array $params, $allowsMutations = true)
{
$instance = new static();
$instance->originalInput = $params;
$params = array_change_key_case($params, CASE_LOWER);
$params += [
'query' => null,
'queryid' => null,
'documentid' => null, // alias to queryid
'operation' => null,
'variables' => null
];
$instance->query = $params['query'];
$instance->queryId = $params['queryid'] ?: $params['documentid'];
$instance->operation = $params['operation'];
$instance->variables = $params['variables'];
$instance->allowsMutations = (bool) $allowsMutations;
return $instance;
}
/**
* @return array
*/
public function validate()
{
$errors = [];
if (!$this->query && !$this->queryId) {
$errors[] = 'GraphQL Request must include at least one of those two parameters: "query" or "queryId"';
}
if ($this->query && $this->queryId) {
$errors[] = 'GraphQL Request parameters: "query" and "queryId" are mutually exclusive';
}
if ($this->query !== null && (!is_string($this->query) || empty($this->query))) {
$errors[] = 'GraphQL Request parameter "query" must be string, but got: ' .
Utils::printSafe($this->query);
}
if ($this->queryId !== null && (!is_string($this->query) || empty($this->query))) {
$errors[] = 'GraphQL Request parameter "queryId" must be string, but got: ' .
Utils::printSafe($this->query);
}
if ($this->operation !== null && (!is_string($this->operation) || empty($this->operation))) {
$errors[] = 'GraphQL Request parameter "operation" must be string, but got: ' .
Utils::printSafe($this->operation);
}
if ($this->variables !== null && (!is_array($this->variables) || isset($this->variables[0]))) {
$errors[] = 'GraphQL Request parameter "variables" must be associative array, but got: ' .
Utils::printSafe($this->variables);
}
return $errors;
}
public function getOriginalInput()
{
return $this->originalInput;
}
/**
* @return bool
*/
public function allowsMutation()
{
return $this->allowsMutations;
}
}

257
src/Server/ServerConfig.php Normal file
View File

@ -0,0 +1,257 @@
<?php
namespace GraphQL\Server;
use GraphQL\Error\FormattedError;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Schema;
use GraphQL\Utils\Utils;
class ServerConfig
{
/**
* @return static
*/
public static function create()
{
return new static();
}
/**
* @var Schema
*/
private $schema;
/**
* @var mixed
*/
private $context;
/**
* @var mixed
*/
private $rootValue;
/**
* @var callable
*/
private $errorFormatter = [FormattedError::class, 'createFromException'];
/**
* @var bool
*/
private $debug = false;
/**
* @var array|callable
*/
private $validationRules;
/**
* @var callable
*/
private $defaultFieldResolver;
/**
* @var PromiseAdapter
*/
private $promiseAdapter;
/**
* @var callable
*/
private $persistentQueryLoader;
/**
* @return mixed
*/
public function getContext()
{
return $this->context;
}
/**
* @param mixed $context
* @return $this
*/
public function setContext($context)
{
$this->context = $context;
return $this;
}
/**
* @param mixed $rootValue
* @return $this
*/
public function setRootValue($rootValue)
{
$this->rootValue = $rootValue;
return $this;
}
/**
* @return mixed
*/
public function getRootValue()
{
return $this->rootValue;
}
/**
* Set schema instance
*
* @param Schema $schema
* @return $this
*/
public function setSchema(Schema $schema)
{
$this->schema = $schema;
return $this;
}
/**
* @return Schema
*/
public function getSchema()
{
return $this->schema;
}
/**
* @return callable
*/
public function getErrorFormatter()
{
return $this->errorFormatter;
}
/**
* Expects function(Throwable $e) : array
*
* @param callable $errorFormatter
* @return $this
*/
public function setErrorFormatter(callable $errorFormatter)
{
$this->errorFormatter = $errorFormatter;
return $this;
}
/**
* @return PromiseAdapter
*/
public function getPromiseAdapter()
{
return $this->promiseAdapter;
}
/**
* @param PromiseAdapter $promiseAdapter
* @return $this
*/
public function setPromiseAdapter(PromiseAdapter $promiseAdapter)
{
$this->promiseAdapter = $promiseAdapter;
return $this;
}
/**
* @return array|callable
*/
public function getValidationRules()
{
return $this->validationRules;
}
/**
* Set validation rules for this server.
*
* @param array|callable
* @return $this
*/
public function setValidationRules($validationRules)
{
if (!is_callable($validationRules) && !is_array($validationRules) && $validationRules !== null) {
throw new InvariantViolation(
__METHOD__ . ' expects array of validation rules or callable returning such array, but got: %s' .
Utils::printSafe($validationRules)
);
}
$this->validationRules = $validationRules;
return $this;
}
/**
* @return callable
*/
public function getDefaultFieldResolver()
{
return $this->defaultFieldResolver;
}
/**
* @param callable $defaultFieldResolver
* @return $this
*/
public function setDefaultFieldResolver(callable $defaultFieldResolver)
{
$this->defaultFieldResolver = $defaultFieldResolver;
return $this;
}
/**
* @return callable
*/
public function getPersistentQueryLoader()
{
return $this->persistentQueryLoader;
}
/**
* A function that takes an input id and returns a valid Document.
* If provided, this will allow your GraphQL endpoint to execute a document specified via `queryId`.
*
* @param callable $persistentQueryLoader
* @return ServerConfig
*/
public function setPersistentQueryLoader(callable $persistentQueryLoader)
{
$this->persistentQueryLoader = $persistentQueryLoader;
return $this;
}
/**
* @return bool
*/
public function getDebug()
{
return $this->debug;
}
/**
* Settings this option has two effects:
*
* 1. Replaces current error formatter with the one for debugging (has precedence over `setErrorFormatter()`).
* This error formatter adds `trace` entry for all errors in ExecutionResult when it is converted to array.
*
* 2. All PHP errors are intercepted during query execution (including warnings, notices and deprecations).
*
* These PHP errors are converted to arrays with `message`, `file`, `line`, `trace` keys and then added to
* `extensions` section of ExecutionResult under key `phpErrors`.
*
* After query execution error handler will be removed from stack,
* so any errors occurring after execution will not be caught.
*
* Use this feature for development and debugging only.
*
* @param bool $set
* @return $this
*/
public function setDebug($set = true)
{
$this->debug = (bool) $set;
return $this;
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace GraphQL\Server;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Promise\Promise;
/**
* Class StandardServer
*
* GraphQL server compatible with both:
* https://github.com/graphql/express-graphql and https://github.com/apollographql/graphql-server
*
* @package GraphQL\Server
*/
class StandardServer
{
/**
* Creates new server
*
* @param ServerConfig $config
* @return static
*/
public static function create(ServerConfig $config)
{
return new static($config);
}
/**
* @var ServerConfig
*/
private $config;
/**
* StandardServer constructor.
* @param ServerConfig $config
*/
protected function __construct(ServerConfig $config)
{
$this->config = $config;
}
/**
* @param OperationParams|OperationParams[] $parsedBody
* @return ExecutionResult|ExecutionResult[]|Promise
*/
public function executeRequest($parsedBody = null)
{
if (null !== $parsedBody) {
$this->assertBodyIsParsedProperly(__METHOD__, $parsedBody);
} else {
$parsedBody = Helper::parseHttpRequest();
}
$batched = is_array($parsedBody);
$result = [];
foreach ((array) $parsedBody as $index => $operationParams) {
$result[] = Helper::executeOperation($this->config, $operationParams);
}
return $batched ? $result : $result[0];
}
/**
* @param $method
* @param $parsedBody
*/
private function assertBodyIsParsedProperly($method, $parsedBody)
{
if (is_array($parsedBody)) {
foreach ($parsedBody as $index => $entry) {
if (!$entry instanceof OperationParams) {
throw new InvariantViolation(sprintf(
'%s expects instance of %s or array of instances. Got invalid array where entry at position %d is %s',
$method,
OperationParams::class,
$index,
Utils::printSafe($entry)
));
}
$errors = $entry->validate();
if (!empty($errors[0])) {
$err = $index ? "Error in query #$index: {$errors[0]}" : $errors[0];
throw new InvariantViolation($err);
}
}
}
if ($parsedBody instanceof OperationParams) {
$errors = $parsedBody->validate();
if (!empty($errors[0])) {
throw new InvariantViolation($errors[0]);
}
}
throw new InvariantViolation(sprintf(
'%s expects instance of %s or array of instances, but got %s',
$method,
OperationParams::class,
Utils::printSafe($parsedBody)
));
}
}

View File

@ -3,6 +3,7 @@ namespace GraphQL\Utils;
use GraphQL\Error\InvariantViolation; use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\BooleanValueNode; use GraphQL\Language\AST\BooleanValueNode;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\EnumValueNode; use GraphQL\Language\AST\EnumValueNode;
use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FloatValueNode; use GraphQL\Language\AST\FloatValueNode;
@ -12,6 +13,7 @@ use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\NullValueNode; use GraphQL\Language\AST\NullValueNode;
use GraphQL\Language\AST\ObjectFieldNode; use GraphQL\Language\AST\ObjectFieldNode;
use GraphQL\Language\AST\ObjectValueNode; use GraphQL\Language\AST\ObjectValueNode;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\StringValueNode; use GraphQL\Language\AST\StringValueNode;
use GraphQL\Language\AST\ValueNode; use GraphQL\Language\AST\ValueNode;
use GraphQL\Language\AST\VariableNode; use GraphQL\Language\AST\VariableNode;
@ -332,4 +334,23 @@ class AST
return $valueNode instanceof VariableNode && return $valueNode instanceof VariableNode &&
(!$variables || !array_key_exists($valueNode->name->value, $variables)); (!$variables || !array_key_exists($valueNode->name->value, $variables));
} }
/**
* @param string $operation
* @param DocumentNode $document
* @return bool
*/
public static function isMutation($operation, DocumentNode $document)
{
if (is_array($document->definitions)) {
foreach ($document->definitions as $def) {
if ($def instanceof OperationDefinitionNode) {
if ($def->operation === 'mutation' && $def->name->value === $operation) {
return true;
}
}
}
}
return false;
}
} }

View File

@ -385,4 +385,28 @@ class Utils
); );
} }
} }
/**
* Wraps original closure with PHP error handling (using set_error_handler).
* Resulting closure will collect all PHP errors that occur during the call in $errors array.
*
* @param callable $fn
* @param \ErrorException[] $errors
* @return \Closure
*/
public static function withErrorHandling(callable $fn, array &$errors)
{
return function() use ($fn, &$errors) {
// Catch custom errors (to report them in query results)
set_error_handler(function ($severity, $message, $file, $line) use (&$errors) {
$errors[] = new \ErrorException($message, 0, $severity, $file, $line);
});
try {
return $fn();
} finally {
restore_error_handler();
}
};
}
} }

View File

@ -0,0 +1,379 @@
<?php
namespace GraphQL\Tests\Server;
use GraphQL\Error\Error;
use GraphQL\Error\UserError;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Language\Parser;
use GraphQL\Schema;
use GraphQL\Server\Helper;
use GraphQL\Server\OperationParams;
use GraphQL\Server\ServerConfig;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\ValidationContext;
class QueryExecutionTest extends \PHPUnit_Framework_TestCase
{
/**
* @var ServerConfig
*/
private $config;
public function setUp()
{
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => [
'f1' => [
'type' => Type::string(),
'resolve' => function($root, $args, $context, $info) {
return $info->fieldName;
}
],
'fieldWithPhpError' => [
'type' => Type::string(),
'resolve' => function($root, $args, $context, $info) {
trigger_error('deprecated', E_USER_DEPRECATED);
trigger_error('notice', E_USER_NOTICE);
trigger_error('warning', E_USER_WARNING);
$a = [];
$a['test']; // should produce PHP notice
return $info->fieldName;
}
],
'fieldWithException' => [
'type' => Type::string(),
'resolve' => function($root, $args, $context, $info) {
throw new \Exception("This is the exception we want");
}
],
'testContextAndRootValue' => [
'type' => Type::string(),
'resolve' => function($root, $args, $context, $info) {
$context->testedRootValue = $root;
return $info->fieldName;
}
],
'fieldWithArg' => [
'type' => Type::string(),
'args' => [
'arg' => [
'type' => Type::nonNull(Type::string())
],
],
'resolve' => function($root, $args) {
return $args['arg'];
}
]
]
])
]);
$this->config = ServerConfig::create()->setSchema($schema);
}
public function testSimpleQueryExecution()
{
$query = '{f1}';
$expected = [
'data' => [
'f1' => 'f1'
]
];
$this->assertQueryResultEquals($expected, $query);
}
public function testDebugPhpErrors()
{
$this->config->setDebug(true);
$query = '
{
fieldWithPhpError
f1
}
';
$expected = [
'data' => [
'fieldWithPhpError' => 'fieldWithPhpError',
'f1' => 'f1'
],
'extensions' => [
'phpErrors' => [
['message' => 'deprecated', 'severity' => 16384],
['message' => 'notice', 'severity' => 1024],
['message' => 'warning', 'severity' => 512],
['message' => 'Undefined index: test', 'severity' => 8],
]
]
];
$result = $this->assertQueryResultEquals($expected, $query);
// Assert php errors contain trace:
$this->assertArrayHasKey('trace', $result->extensions['phpErrors'][0]);
$this->assertArrayHasKey('trace', $result->extensions['phpErrors'][1]);
$this->assertArrayHasKey('trace', $result->extensions['phpErrors'][2]);
$this->assertArrayHasKey('trace', $result->extensions['phpErrors'][3]);
}
public function testDebugExceptions()
{
$this->config->setDebug(true);
$query = '
{
fieldWithException
f1
}
';
$expected = [
'data' => [
'fieldWithException' => null,
'f1' => 'f1'
],
'errors' => [
[
'message' => 'This is the exception we want',
'path' => ['fieldWithException'],
'trace' => []
]
]
];
$result = $this->executeQuery($query)->toArray();
$this->assertArraySubset($expected, $result);
}
public function testPassesRootValueAndContext()
{
$rootValue = 'myRootValue';
$context = new \stdClass();
$this->config
->setContext($context)
->setRootValue($rootValue);
$query = '
{
testContextAndRootValue
}
';
$this->assertTrue(!isset($context->testedRootValue));
$this->executeQuery($query);
$this->assertSame($rootValue, $context->testedRootValue);
}
public function testPassesVariables()
{
$variables = ['a' => 'a', 'b' => 'b'];
$query = '
query ($a: String!, $b: String!) {
a: fieldWithArg(arg: $a)
b: fieldWithArg(arg: $b)
}
';
$expected = [
'data' => [
'a' => 'a',
'b' => 'b'
]
];
$this->assertQueryResultEquals($expected, $query, $variables);
}
public function testPassesCustomValidationRules()
{
$query = '
{nonExistentField}
';
$expected = [
'errors' => [
['message' => 'Cannot query field "nonExistentField" on type "Query".']
]
];
$this->assertQueryResultEquals($expected, $query);
$called = false;
$rules = [
function() use (&$called) {
$called = true;
return [];
}
];
$this->config->setValidationRules($rules);
$expected = [
'data' => []
];
$this->assertQueryResultEquals($expected, $query);
$this->assertTrue($called);
}
public function testAllowsDifferentValidationRulesDependingOnOperation()
{
$q1 = '{f1}';
$q2 = '{invalid}';
$called1 = false;
$called2 = false;
$this->config->setValidationRules(function(OperationParams $params) use ($q1, $q2, &$called1, &$called2) {
if ($params->query === $q1) {
$called1 = true;
return DocumentValidator::allRules();
} else {
$called2 = true;
return [
function(ValidationContext $context) {
$context->reportError(new Error("This is the error we are looking for!"));
}
];
}
});
$expected = ['data' => ['f1' => 'f1']];
$this->assertQueryResultEquals($expected, $q1);
$this->assertTrue($called1);
$this->assertFalse($called2);
$called1 = false;
$called2 = false;
$expected = ['errors' => [['message' => 'This is the error we are looking for!']]];
$this->assertQueryResultEquals($expected, $q2);
$this->assertFalse($called1);
$this->assertTrue($called2);
}
public function testAllowsSkippingValidation()
{
$this->config->setValidationRules([]);
$query = '{nonExistentField}';
$expected = ['data' => []];
$this->assertQueryResultEquals($expected, $query);
}
public function testPersistedQueriesAreDisabledByDefault()
{
$this->setExpectedException(UserError::class, 'Persisted queries are not supported by this server');
$this->executePersistedQuery('some-id');
}
public function testAllowsPersistentQueries()
{
$called = false;
$this->config->setPersistentQueryLoader(function($queryId, OperationParams $params) use (&$called) {
$called = true;
$this->assertEquals('some-id', $queryId);
return '{f1}';
});
$result = $this->executePersistedQuery('some-id');
$this->assertTrue($called);
$expected = [
'data' => [
'f1' => 'f1'
]
];
$this->assertEquals($expected, $result->toArray());
// Make sure it allows returning document node:
$called = false;
$this->config->setPersistentQueryLoader(function($queryId, OperationParams $params) use (&$called) {
$called = true;
$this->assertEquals('some-id', $queryId);
return Parser::parse('{f1}');
});
$result = $this->executePersistedQuery('some-id');
$this->assertTrue($called);
$this->assertEquals($expected, $result->toArray());
}
public function testPersistedQueriesAreStillValidatedByDefault()
{
$this->config->setPersistentQueryLoader(function() {
return '{invalid}';
});
$result = $this->executePersistedQuery('some-id');
$expected = [
'errors' => [
[
'message' => 'Cannot query field "invalid" on type "Query".',
'locations' => [ ['line' => 1, 'column' => 2] ]
]
]
];
$this->assertEquals($expected, $result->toArray());
}
public function testAllowSkippingValidationForPersistedQueries()
{
$this->config
->setPersistentQueryLoader(function($queryId) {
if ($queryId === 'some-id') {
return '{invalid}';
} else {
return '{invalid2}';
}
})
->setValidationRules(function(OperationParams $params) {
if ($params->queryId === 'some-id') {
return [];
} else {
return DocumentValidator::allRules();
}
});
$result = $this->executePersistedQuery('some-id');
$expected = [
'data' => []
];
$this->assertEquals($expected, $result->toArray());
$result = $this->executePersistedQuery('some-other-id');
$expected = [
'errors' => [
[
'message' => 'Cannot query field "invalid2" on type "Query".',
'locations' => [ ['line' => 1, 'column' => 2] ]
]
]
];
$this->assertEquals($expected, $result->toArray());
}
private function executePersistedQuery($queryId, $variables = null)
{
$op = OperationParams::create(['queryId' => $queryId, 'variables' => $variables]);
$result = Helper::executeOperation($this->config, $op);
$this->assertInstanceOf(ExecutionResult::class, $result);
return $result;
}
private function executeQuery($query, $variables = null)
{
$op = OperationParams::create(['query' => $query, 'variables' => $variables]);
$result = Helper::executeOperation($this->config, $op);
$this->assertInstanceOf(ExecutionResult::class, $result);
return $result;
}
private function assertQueryResultEquals($expected, $query, $variables = null)
{
$result = $this->executeQuery($query, $variables);
$this->assertArraySubset($expected, $result->toArray());
return $result;
}
}

View File

@ -0,0 +1,144 @@
<?php
namespace GraphQL\Tests\Server;
use GraphQL\Error\FormattedError;
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
use GraphQL\Schema;
use GraphQL\Server\ServerConfig;
use GraphQL\Type\Definition\ObjectType;
class ServerConfigTest extends \PHPUnit_Framework_TestCase
{
public function testDefaults()
{
$config = ServerConfig::create();
$this->assertEquals(null, $config->getSchema());
$this->assertEquals(null, $config->getContext());
$this->assertEquals(null, $config->getRootValue());
$this->assertEquals([FormattedError::class, 'createFromException'], $config->getErrorFormatter());
$this->assertEquals(null, $config->getPromiseAdapter());
$this->assertEquals(null, $config->getValidationRules());
$this->assertEquals(null, $config->getDefaultFieldResolver());
$this->assertEquals(null, $config->getPersistentQueryLoader());
$this->assertEquals(false, $config->getDebug());
}
public function testAllowsSettingSchema()
{
$schema = new Schema(['query' => new ObjectType(['name' => 'a', 'fields' => []])]);
$config = ServerConfig::create()
->setSchema($schema);
$this->assertSame($schema, $config->getSchema());
$schema2 = new Schema(['query' => new ObjectType(['name' => 'a', 'fields' => []])]);
$config->setSchema($schema2);
$this->assertSame($schema2, $config->getSchema());
}
public function testAllowsSettingContext()
{
$config = ServerConfig::create();
$context = [];
$config->setContext($context);
$this->assertSame($context, $config->getContext());
$context2 = new \stdClass();
$config->setContext($context2);
$this->assertSame($context2, $config->getContext());
}
public function testAllowsSettingRootValue()
{
$config = ServerConfig::create();
$rootValue = [];
$config->setRootValue($rootValue);
$this->assertSame($rootValue, $config->getRootValue());
$context2 = new \stdClass();
$config->setRootValue($context2);
$this->assertSame($context2, $config->getRootValue());
}
public function testAllowsSettingErrorFormatter()
{
$config = ServerConfig::create();
$formatter = function() {};
$config->setErrorFormatter($formatter);
$this->assertSame($formatter, $config->getErrorFormatter());
$formatter = 'date'; // test for callable
$config->setErrorFormatter($formatter);
$this->assertSame($formatter, $config->getErrorFormatter());
}
public function testAllowsSettingPromiseAdapter()
{
$config = ServerConfig::create();
$adapter1 = new SyncPromiseAdapter();
$config->setPromiseAdapter($adapter1);
$this->assertSame($adapter1, $config->getPromiseAdapter());
$adapter2 = new SyncPromiseAdapter();
$config->setPromiseAdapter($adapter2);
$this->assertSame($adapter2, $config->getPromiseAdapter());
}
public function testAllowsSettingValidationRules()
{
$config = ServerConfig::create();
$rules = [];
$config->setValidationRules($rules);
$this->assertSame($rules, $config->getValidationRules());
$rules = [function() {}];
$config->setValidationRules($rules);
$this->assertSame($rules, $config->getValidationRules());
$rules = function() {return [function() {}];};
$config->setValidationRules($rules);
$this->assertSame($rules, $config->getValidationRules());
}
public function testAllowsSettingDefaultFieldResolver()
{
$config = ServerConfig::create();
$resolver = function() {};
$config->setDefaultFieldResolver($resolver);
$this->assertSame($resolver, $config->getDefaultFieldResolver());
$resolver = 'date'; // test for callable
$config->setDefaultFieldResolver($resolver);
$this->assertSame($resolver, $config->getDefaultFieldResolver());
}
public function testAllowsSettingPersistedQueryLoader()
{
$config = ServerConfig::create();
$loader = function() {};
$config->setPersistentQueryLoader($loader);
$this->assertSame($loader, $config->getPersistentQueryLoader());
$loader = 'date'; // test for callable
$config->setPersistentQueryLoader($loader);
$this->assertSame($loader, $config->getPersistentQueryLoader());
}
public function testAllowsSettingCatchPhpErrors()
{
$config = ServerConfig::create();
$config->setDebug(true);
$this->assertSame(true, $config->getDebug());
$config->setDebug(false);
$this->assertSame(false, $config->getDebug());
}
}

View File

@ -1,580 +0,0 @@
<?php
namespace GraphQL\Tests;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
use GraphQL\Schema;
use GraphQL\Server;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\EagerResolution;
use GraphQL\Validator\DocumentValidator;
class ServerTest extends \PHPUnit_Framework_TestCase
{
public function testDefaults()
{
$server = new Server();
$this->assertEquals(null, $server->getQueryType());
$this->assertEquals(null, $server->getMutationType());
$this->assertEquals(null, $server->getSubscriptionType());
$this->assertEquals(Directive::getInternalDirectives(), $server->getDirectives());
$this->assertEquals([], $server->getTypes());
$this->assertEquals(null, $server->getTypeResolutionStrategy());
$this->assertEquals(null, $server->getContext());
$this->assertEquals(null, $server->getRootValue());
$this->assertEquals(0, $server->getDebug());
$this->assertEquals(['GraphQL\Error\FormattedError', 'createFromException'], $server->getExceptionFormatter());
$this->assertEquals(['GraphQL\Error\FormattedError', 'createFromPHPError'], $server->getPhpErrorFormatter());
$this->assertEquals(null, $server->getPromiseAdapter());
$this->assertEquals('Unexpected Error', $server->getUnexpectedErrorMessage());
$this->assertEquals(500, $server->getUnexpectedErrorStatus());
$this->assertEquals(DocumentValidator::allRules(), $server->getValidationRules());
try {
$server->getSchema();
$this->fail('Expected exception not thrown');
} catch (InvariantViolation $e) {
$this->assertEquals('Schema query must be Object Type but got: NULL', $e->getMessage());
}
}
public function testSchemaDefinition()
{
$mutationType = $queryType = $subscriptionType = new ObjectType(['name' => 'A', 'fields' => []]);
$schema = new Schema([
'query' => $queryType
]);
try {
Server::create()
->setQueryType($queryType)
->setSchema($schema);
$this->fail('Expected exception not thrown');
} catch (InvariantViolation $e) {
$this->assertEquals(
'Cannot set Schema on Server: Query Type is already set '.
'(GraphQL\Server::setQueryType is mutually exclusive with GraphQL\Server::setSchema)',
$e->getMessage()
);
}
try {
Server::create()
->setMutationType($mutationType)
->setSchema($schema);
$this->fail('Expected exception not thrown');
} catch (InvariantViolation $e) {
$this->assertEquals(
'Cannot set Schema on Server: Mutation Type is already set '.
'(GraphQL\Server::setMutationType is mutually exclusive with GraphQL\Server::setSchema)',
$e->getMessage()
);
}
try {
Server::create()
->setSubscriptionType($subscriptionType)
->setSchema($schema);
$this->fail('Expected exception not thrown');
} catch (InvariantViolation $e) {
$this->assertEquals(
'Cannot set Schema on Server: Subscription Type is already set '.
'(GraphQL\Server::setSubscriptionType is mutually exclusive with GraphQL\Server::setSchema)',
$e->getMessage()
);
}
try {
Server::create()
->setDirectives(Directive::getInternalDirectives())
->setSchema($schema);
$this->fail('Expected exception not thrown');
} catch (InvariantViolation $e) {
$this->assertEquals(
'Cannot set Schema on Server: Directives are already set '.
'(GraphQL\Server::setDirectives is mutually exclusive with GraphQL\Server::setSchema)',
$e->getMessage()
);
}
try {
Server::create()
->addTypes([$queryType, $mutationType])
->setSchema($schema);
$this->fail('Expected exception not thrown');
} catch (InvariantViolation $e) {
$this->assertEquals(
'Cannot set Schema on Server: Additional types are already set '.
'(GraphQL\Server::addTypes is mutually exclusive with GraphQL\Server::setSchema)',
$e->getMessage()
);
}
try {
Server::create()
->setTypeResolutionStrategy(new EagerResolution([$queryType, $mutationType]))
->setSchema($schema);
$this->fail('Expected exception not thrown');
} catch (InvariantViolation $e) {
$this->assertEquals(
'Cannot set Schema on Server: Type Resolution Strategy is already set '.
'(GraphQL\Server::setTypeResolutionStrategy is mutually exclusive with GraphQL\Server::setSchema)',
$e->getMessage()
);
}
try {
Server::create()
->setSchema($schema)
->setQueryType($queryType);
$this->fail('Expected exception not thrown');
} catch (InvariantViolation $e) {
$this->assertEquals(
'Cannot set Query Type on Server: Schema is already set '.
'(GraphQL\Server::setQueryType is mutually exclusive with GraphQL\Server::setSchema)',
$e->getMessage()
);
}
try {
Server::create()
->setSchema($schema)
->setMutationType($mutationType);
$this->fail('Expected exception not thrown');
} catch (InvariantViolation $e) {
$this->assertEquals(
'Cannot set Mutation Type on Server: Schema is already set '.
'(GraphQL\Server::setMutationType is mutually exclusive with GraphQL\Server::setSchema)',
$e->getMessage()
);
}
try {
Server::create()
->setSchema($schema)
->setSubscriptionType($subscriptionType);
$this->fail('Expected exception not thrown');
} catch (InvariantViolation $e) {
$this->assertEquals(
'Cannot set Subscription Type on Server: Schema is already set '.
'(GraphQL\Server::setSubscriptionType is mutually exclusive with GraphQL\Server::setSchema)',
$e->getMessage()
);
}
try {
Server::create()
->setSchema($schema)
->setDirectives([]);
$this->fail('Expected exception not thrown');
} catch (InvariantViolation $e) {
$this->assertEquals(
'Cannot set Directives on Server: Schema is already set '.
'(GraphQL\Server::setDirectives is mutually exclusive with GraphQL\Server::setSchema)',
$e->getMessage()
);
}
try {
Server::create()
->setSchema($schema)
->addTypes([$queryType, $mutationType]);
$this->fail('Expected exception not thrown');
} catch (InvariantViolation $e) {
$this->assertEquals(
'Cannot set Types on Server: Schema is already set '.
'(GraphQL\Server::addTypes is mutually exclusive with GraphQL\Server::setSchema)',
$e->getMessage()
);
}
// But empty types should work (as they don't change anything):
Server::create()
->setSchema($schema)
->addTypes([]);
try {
Server::create()
->setSchema($schema)
->setTypeResolutionStrategy(new EagerResolution([$queryType, $mutationType]));
$this->fail('Expected exception not thrown');
} catch (InvariantViolation $e) {
$this->assertEquals(
'Cannot set Type Resolution Strategy on Server: Schema is already set '.
'(GraphQL\Server::setTypeResolutionStrategy is mutually exclusive with GraphQL\Server::setSchema)',
$e->getMessage()
);
}
try {
Server::create()
->setSchema($schema)
->setSchema(new Schema(['query' => $queryType]));
$this->fail('Expected exception not thrown');
} catch (InvariantViolation $e) {
$this->assertEquals(
'Cannot set Schema on Server: Different schema is already set',
$e->getMessage()
);
}
// This should not throw:
$server = Server::create()
->setSchema($schema);
$this->assertSame($schema, $server->getSchema());
$server = Server::create()
->setQueryType($queryType);
$this->assertSame($queryType, $server->getQueryType());
$this->assertSame($queryType, $server->getSchema()->getQueryType());
$server = Server::create()
->setQueryType($queryType)
->setMutationType($mutationType);
$this->assertSame($mutationType, $server->getMutationType());
$this->assertSame($mutationType, $server->getSchema()->getMutationType());
$server = Server::create()
->setQueryType($queryType)
->setSubscriptionType($subscriptionType);
$this->assertSame($subscriptionType, $server->getSubscriptionType());
$this->assertSame($subscriptionType, $server->getSchema()->getSubscriptionType());
$server = Server::create()
->setQueryType($queryType)
->addTypes($types = [$queryType, $subscriptionType]);
$this->assertSame($types, $server->getTypes());
$server->addTypes([$mutationType]);
$this->assertSame(array_merge($types, [$mutationType]), $server->getTypes());
$server = Server::create()
->setDirectives($directives = []);
$this->assertSame($directives, $server->getDirectives());
}
public function testParse()
{
$server = Server::create();
$ast = $server->parse('{q}');
$this->assertInstanceOf('GraphQL\Language\AST\DocumentNode', $ast);
try {
$server->parse('{q');
$this->fail('Expected exception not thrown');
} catch (\GraphQL\Error\SyntaxError $e) {
$this->assertContains('{q', $e->getMessage());
}
}
public function testValidate()
{
$server = Server::create()
->setQueryType(new ObjectType(['name' => 'Q', 'fields' => []]));
$ast = $server->parse('{q}');
$errors = $server->validate($ast);
$this->assertInternalType('array', $errors);
$this->assertNotEmpty($errors);
try {
$server = Server::create();
$server->validate($ast);
$this->fail('Expected exception not thrown');
} catch (InvariantViolation $e) {
$this->assertEquals(
'Cannot validate, schema contains errors: Schema query must be Object Type but got: NULL',
$e->getMessage()
);
}
}
public function testPromiseAdapter()
{
$adapter1 = new SyncPromiseAdapter();
$adapter2 = new SyncPromiseAdapter();
$server = Server::create()
->setPromiseAdapter($adapter1);
$this->assertSame($adapter1, $server->getPromiseAdapter());
$server->setPromiseAdapter($adapter1);
try {
$server->setPromiseAdapter($adapter2);
$this->fail('Expected exception not thrown');
} catch (InvariantViolation $e) {
$this->assertEquals(
'Cannot set promise adapter: Different adapter is already set',
$e->getMessage()
);
}
}
public function testValidationRules()
{
$rules = [];
$server = Server::create()
->setValidationRules($rules);
$this->assertSame($rules, $server->getValidationRules());
}
public function testExecuteQuery()
{
$called = false;
$queryType = new ObjectType([
'name' => 'Q',
'fields' => [
'field' => [
'type' => Type::string(),
'resolve' => function($value, $args, $context, ResolveInfo $info) use (&$called) {
$called = true;
$this->assertEquals(null, $context);
$this->assertEquals(null, $value);
$this->assertEquals(null, $info->rootValue);
return 'ok';
}
]
]
]);
$server = Server::create()
->setQueryType($queryType);
$result = $server->executeQuery('{field}');
$this->assertEquals(true, $called);
$this->assertInstanceOf('GraphQL\Executor\ExecutionResult', $result);
$this->assertEquals(['data' => ['field' => 'ok']], $result->toArray());
$called = false;
$contextValue = new \stdClass();
$rootValue = new \stdClass();
$queryType = new ObjectType([
'name' => 'QueryType',
'fields' => [
'field' => [
'type' => Type::string(),
'resolve' => function($value, $args, $context, ResolveInfo $info) use (&$called, $contextValue, $rootValue) {
$called = true;
$this->assertSame($rootValue, $value);
$this->assertSame($contextValue, $context);
$this->assertEquals($rootValue, $info->rootValue);
return 'ok';
}
]
]
]);
$server = Server::create()
->setQueryType($queryType)
->setRootValue($rootValue)
->setContext($contextValue);
$result = $server->executeQuery('{field}');
$this->assertEquals(true, $called);
$this->assertInstanceOf('GraphQL\Executor\ExecutionResult', $result);
$this->assertEquals(['data' => ['field' => 'ok']], $result->toArray());
}
public function testDebugPhpErrors()
{
$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'err' => [
'type' => Type::string(),
'resolve' => function() {
trigger_error('notice', E_USER_NOTICE);
return 'err';
}
]
]
]);
$server = Server::create()
->setDebug(0)
->setQueryType($queryType);
$prevEnabled = \PHPUnit_Framework_Error_Notice::$enabled;
\PHPUnit_Framework_Error_Notice::$enabled = false;
$result = @$server->executeQuery('{err}');
$expected = [
'data' => ['err' => 'err']
];
$this->assertEquals($expected, $result->toArray());
$server->setDebug(Server::DEBUG_PHP_ERRORS);
$result = @$server->executeQuery('{err}');
$expected = [
'data' => ['err' => 'err'],
'extensions' => [
'phpErrors' => [
[
'message' => 'notice',
'severity' => 1024,
// 'trace' => [...]
]
]
]
];
$this->assertArraySubset($expected, $result->toArray());
$server->setPhpErrorFormatter(function(\ErrorException $e) {
return ['test' => $e->getMessage()];
});
$result = $server->executeQuery('{err}');
$expected = [
'data' => ['err' => 'err'],
'extensions' => [
'phpErrors' => [
[
'test' => 'notice'
]
]
]
];
$this->assertEquals($expected, $result->toArray());
\PHPUnit_Framework_Error_Notice::$enabled = $prevEnabled;
}
public function testDebugExceptions()
{
$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'withException' => [
'type' => Type::string(),
'resolve' => function() {
throw new \Exception("Error");
}
]
]
]);
$server = Server::create()
->setDebug(0)
->setQueryType($queryType);
$result = $server->executeQuery('{withException}');
$expected = [
'data' => [
'withException' => null
],
'errors' => [[
'message' => 'Error',
'path' => ['withException'],
'locations' => [[
'line' => 1,
'column' => 2
]]
]]
];
$this->assertEquals($expected, $result->toArray());
$server->setDebug(Server::DEBUG_EXCEPTIONS);
$result = $server->executeQuery('{withException}');
$expected['errors'][0]['exception'] = ['message' => 'Error', 'trace' => []];
$this->assertArraySubset($expected, $result->toArray());
$server->setExceptionFormatter(function(\Exception $e) {
return ['test' => $e->getMessage()];
});
$result = $server->executeQuery('{withException}');
$expected['errors'][0]['exception'] = ['test' => 'Error'];
$this->assertEquals($expected, $result->toArray());
}
public function testHandleRequest()
{
$mock = $this->getMockBuilder('GraphQL\Server')
->setMethods(['readInput', 'produceOutput'])
->getMock()
;
$mock->method('readInput')
->will($this->returnValue(json_encode(['query' => '{err}'])));
$output = null;
$mock->method('produceOutput')
->will($this->returnCallback(function($a1, $a2) use (&$output) {
$output = func_get_args();
}));
/** @var $mock Server */
$mock->handleRequest();
$this->assertInternalType('array', $output);
$this->assertEquals(['errors' => [['message' => 'Unexpected Error']]], $output[0]);
$this->assertEquals(500, $output[1]);
$output = null;
$mock->setUnexpectedErrorMessage($newErr = 'Hey! Something went wrong!');
$mock->setUnexpectedErrorStatus(501);
$mock->handleRequest();
$this->assertInternalType('array', $output);
$this->assertEquals(['errors' => [['message' => $newErr]]], $output[0]);
$this->assertEquals(501, $output[1]);
$mock->setQueryType(new ObjectType([
'name' => 'Query',
'fields' => [
'test' => [
'type' => Type::string(),
'resolve' => function() {
return 'ok';
}
]
]
]));
$_REQUEST = ['query' => '{err}'];
$output = null;
$mock->handleRequest();
$this->assertInternalType('array', $output);
$expectedOutput = [
['errors' => [[
'message' => 'Cannot query field "err" on type "Query".',
'locations' => [[
'line' => 1,
'column' => 2
]]
]]],
200
];
$this->assertEquals($expectedOutput, $output);
$output = null;
$_SERVER['CONTENT_TYPE'] = 'application/json';
$_REQUEST = [];
$mock->handleRequest();
$this->assertInternalType('array', $output);
$this->assertEquals($expectedOutput, $output);
}
}