mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-25 06:16:05 +03:00
Initial pass on standard server implementation (also deprecated current GraphQL\Server which is undocumented anyway)
This commit is contained in:
parent
a3b40db0fb
commit
794d3672ef
@ -8,53 +8,49 @@ use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Class FormattedError
|
||||
* @todo move this class to Utils/ErrorUtils
|
||||
*
|
||||
* @package GraphQL\Error
|
||||
*/
|
||||
class FormattedError
|
||||
{
|
||||
/**
|
||||
* @deprecated as of 8.0
|
||||
* @param $error
|
||||
* @param SourceLocation[] $locations
|
||||
* @param \Throwable $e
|
||||
* @param $debug
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function create($error, array $locations = [])
|
||||
public static function createFromException($e, $debug = false)
|
||||
{
|
||||
$formatted = [
|
||||
'message' => $error
|
||||
];
|
||||
|
||||
if (!empty($locations)) {
|
||||
$formatted['locations'] = array_map(function($loc) { return $loc->toArray();}, $locations);
|
||||
if ($e instanceof Error) {
|
||||
$result = $e->toSerializableArray();
|
||||
} else if ($e instanceof \ErrorException) {
|
||||
$result = [
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
* @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())
|
||||
];
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -133,4 +129,37 @@ class FormattedError
|
||||
}
|
||||
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())
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,11 @@ use GraphQL\Type\Resolution;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
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
|
||||
{
|
||||
const DEBUG_PHP_ERRORS = 1;
|
||||
|
191
src/Server/Helper.php
Normal file
191
src/Server/Helper.php
Normal 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;
|
||||
}
|
||||
}
|
120
src/Server/OperationParams.php
Normal file
120
src/Server/OperationParams.php
Normal 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
257
src/Server/ServerConfig.php
Normal 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;
|
||||
}
|
||||
}
|
106
src/Server/StandardServer.php
Normal file
106
src/Server/StandardServer.php
Normal 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)
|
||||
));
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ namespace GraphQL\Utils;
|
||||
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Language\AST\BooleanValueNode;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\AST\EnumValueNode;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\FloatValueNode;
|
||||
@ -12,6 +13,7 @@ use GraphQL\Language\AST\NameNode;
|
||||
use GraphQL\Language\AST\NullValueNode;
|
||||
use GraphQL\Language\AST\ObjectFieldNode;
|
||||
use GraphQL\Language\AST\ObjectValueNode;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\AST\StringValueNode;
|
||||
use GraphQL\Language\AST\ValueNode;
|
||||
use GraphQL\Language\AST\VariableNode;
|
||||
@ -332,4 +334,23 @@ class AST
|
||||
return $valueNode instanceof VariableNode &&
|
||||
(!$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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
379
tests/Server/QueryExecutionTest.php
Normal file
379
tests/Server/QueryExecutionTest.php
Normal 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;
|
||||
}
|
||||
}
|
144
tests/Server/ServerConfigTest.php
Normal file
144
tests/Server/ServerConfigTest.php
Normal 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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user