2017-07-14 15:08:47 +03:00
|
|
|
<?php
|
|
|
|
namespace GraphQL\Server;
|
|
|
|
|
2017-07-17 16:31:26 +03:00
|
|
|
use GraphQL\Error\Error;
|
2017-07-14 15:08:47 +03:00
|
|
|
use GraphQL\Error\FormattedError;
|
|
|
|
use GraphQL\Error\InvariantViolation;
|
|
|
|
use GraphQL\Error\UserError;
|
|
|
|
use GraphQL\Executor\ExecutionResult;
|
2017-07-17 16:31:26 +03:00
|
|
|
use GraphQL\Executor\Executor;
|
|
|
|
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
|
2017-07-14 15:08:47 +03:00
|
|
|
use GraphQL\Executor\Promise\Promise;
|
2017-07-17 16:31:26 +03:00
|
|
|
use GraphQL\Executor\Promise\PromiseAdapter;
|
2017-07-14 15:08:47 +03:00
|
|
|
use GraphQL\Language\AST\DocumentNode;
|
|
|
|
use GraphQL\Language\Parser;
|
|
|
|
use GraphQL\Utils\AST;
|
|
|
|
use GraphQL\Utils\Utils;
|
2017-07-17 16:31:26 +03:00
|
|
|
use GraphQL\Validator\DocumentValidator;
|
2017-07-14 15:08:47 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Class Helper
|
|
|
|
* Contains functionality that could be re-used by various server implementations
|
|
|
|
*
|
|
|
|
* @package GraphQL\Server
|
|
|
|
*/
|
|
|
|
class Helper
|
|
|
|
{
|
|
|
|
/**
|
2017-07-17 16:31:26 +03:00
|
|
|
* Executes GraphQL operation with given server configuration and returns execution result
|
|
|
|
* (or promise when promise adapter is different from SyncPromiseAdapter)
|
2017-07-14 15:08:47 +03:00
|
|
|
*
|
|
|
|
* @param ServerConfig $config
|
|
|
|
* @param OperationParams $op
|
|
|
|
*
|
|
|
|
* @return ExecutionResult|Promise
|
|
|
|
*/
|
2017-07-16 14:52:38 +03:00
|
|
|
public function executeOperation(ServerConfig $config, OperationParams $op)
|
2017-07-17 16:31:26 +03:00
|
|
|
{
|
|
|
|
$promiseAdapter = $config->getPromiseAdapter() ?: Executor::getPromiseAdapter();
|
|
|
|
$result = $this->promiseToExecuteOperation($promiseAdapter, $config, $op);
|
|
|
|
|
|
|
|
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
|
|
|
$result = $promiseAdapter->wait($result);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Executes batched GraphQL operations with shared promise queue
|
|
|
|
* (thus, effectively batching deferreds|promises of all queries at once)
|
|
|
|
*
|
|
|
|
* @param ServerConfig $config
|
|
|
|
* @param OperationParams[] $operations
|
|
|
|
* @return ExecutionResult[]|Promise
|
|
|
|
*/
|
|
|
|
public function executeBatch(ServerConfig $config, array $operations)
|
|
|
|
{
|
|
|
|
$promiseAdapter = $config->getPromiseAdapter() ?: Executor::getPromiseAdapter();
|
|
|
|
$result = [];
|
|
|
|
|
|
|
|
foreach ($operations as $operation) {
|
|
|
|
$result[] = $this->promiseToExecuteOperation($promiseAdapter, $config, $operation);
|
|
|
|
}
|
|
|
|
|
|
|
|
$result = $promiseAdapter->all($result);
|
|
|
|
|
|
|
|
// Wait for promised results when using sync promises
|
|
|
|
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
|
|
|
$result = $promiseAdapter->wait($result);
|
|
|
|
}
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param PromiseAdapter $promiseAdapter
|
|
|
|
* @param ServerConfig $config
|
|
|
|
* @param OperationParams $op
|
|
|
|
* @return Promise
|
|
|
|
*/
|
|
|
|
private function promiseToExecuteOperation(PromiseAdapter $promiseAdapter, ServerConfig $config, OperationParams $op)
|
2017-07-14 15:08:47 +03:00
|
|
|
{
|
|
|
|
$phpErrors = [];
|
|
|
|
|
2017-07-17 16:31:26 +03:00
|
|
|
$execute = function() use ($config, $op, $promiseAdapter) {
|
|
|
|
try {
|
|
|
|
$doc = $op->queryId ? static::loadPersistedQuery($config, $op) : $op->query;
|
2017-07-14 15:08:47 +03:00
|
|
|
|
2017-07-17 16:31:26 +03:00
|
|
|
if (!$doc instanceof DocumentNode) {
|
|
|
|
$doc = Parser::parse($doc);
|
|
|
|
}
|
|
|
|
if ($op->isReadOnly() && AST::isMutation($op->operation, $doc)) {
|
|
|
|
throw new UserError("Cannot execute mutation in read-only context");
|
|
|
|
}
|
|
|
|
|
|
|
|
$validationErrors = DocumentValidator::validate(
|
|
|
|
$config->getSchema(),
|
|
|
|
$doc,
|
|
|
|
$this->resolveValidationRules($config, $op)
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!empty($validationErrors)) {
|
|
|
|
return $promiseAdapter->createFulfilled(
|
|
|
|
new ExecutionResult(null, $validationErrors)
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return Executor::promiseToExecute(
|
|
|
|
$promiseAdapter,
|
|
|
|
$config->getSchema(),
|
|
|
|
$doc,
|
|
|
|
$config->getRootValue(),
|
|
|
|
$config->getContext(),
|
|
|
|
$op->variables,
|
|
|
|
$op->operation,
|
|
|
|
$config->getDefaultFieldResolver()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (Error $e) {
|
|
|
|
return $promiseAdapter->createFulfilled(
|
|
|
|
new ExecutionResult(null, [$e])
|
|
|
|
);
|
|
|
|
}
|
2017-07-14 15:08:47 +03:00
|
|
|
};
|
|
|
|
if ($config->getDebug()) {
|
|
|
|
$execute = Utils::withErrorHandling($execute, $phpErrors);
|
|
|
|
}
|
|
|
|
$result = $execute();
|
|
|
|
|
2017-07-17 16:31:26 +03:00
|
|
|
$applyErrorFormatting = function (ExecutionResult $result) use ($config, $phpErrors) {
|
2017-07-14 15:08:47 +03:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2017-07-17 16:31:26 +03:00
|
|
|
return $result->then($applyErrorFormatting);
|
2017-07-14 15:08:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param ServerConfig $config
|
|
|
|
* @param OperationParams $op
|
2017-07-17 16:31:26 +03:00
|
|
|
* @return mixed
|
|
|
|
* @throws Error
|
|
|
|
* @throws InvariantViolation
|
2017-07-14 15:08:47 +03:00
|
|
|
*/
|
2017-07-16 14:52:38 +03:00
|
|
|
public function loadPersistedQuery(ServerConfig $config, OperationParams $op)
|
2017-07-14 15:08:47 +03:00
|
|
|
{
|
2017-07-16 14:52:38 +03:00
|
|
|
if (!$op->queryId) {
|
|
|
|
throw new InvariantViolation("Could not load persisted query: queryId is not set");
|
|
|
|
}
|
|
|
|
|
2017-07-14 15:08:47 +03:00
|
|
|
// 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
|
|
|
|
*/
|
2017-07-16 14:52:38 +03:00
|
|
|
public function resolveValidationRules(ServerConfig $config, OperationParams $params)
|
2017-07-14 15:08:47 +03:00
|
|
|
{
|
|
|
|
// 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.
|
|
|
|
*
|
2017-07-17 12:57:30 +03:00
|
|
|
* This function doesn't check validity of these params.
|
2017-07-16 14:52:38 +03:00
|
|
|
*
|
|
|
|
* If $readRawBodyFn argument is not provided - will attempt to read raw request body from php://input stream
|
|
|
|
*
|
|
|
|
* @param callable|null $readRawBodyFn
|
2017-07-17 12:57:30 +03:00
|
|
|
* @return OperationParams|OperationParams[]
|
2017-07-16 14:52:38 +03:00
|
|
|
*/
|
2017-07-17 12:57:30 +03:00
|
|
|
public function parseHttpRequest(callable $readRawBodyFn = null)
|
2017-07-14 15:08:47 +03:00
|
|
|
{
|
2017-07-16 14:52:38 +03:00
|
|
|
$method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null;
|
2017-07-14 15:08:47 +03:00
|
|
|
|
2017-07-16 14:52:38 +03:00
|
|
|
if ($method === 'GET') {
|
|
|
|
$request = array_change_key_case($_GET);
|
|
|
|
|
|
|
|
if (isset($request['query']) || isset($request['queryid']) || isset($request['documentid'])) {
|
2017-07-17 12:57:30 +03:00
|
|
|
$result = OperationParams::create($_GET, true);
|
2017-07-16 14:52:38 +03:00
|
|
|
} else {
|
|
|
|
throw new UserError('Cannot execute GET request without "query" or "queryId" parameter');
|
|
|
|
}
|
|
|
|
} else if ($method === 'POST') {
|
|
|
|
$contentType = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : null;
|
|
|
|
|
|
|
|
if (stripos($contentType, 'application/graphql') !== false) {
|
|
|
|
$rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody();
|
2017-07-17 12:57:30 +03:00
|
|
|
$result = OperationParams::create(['query' => $rawBody ?: '']);
|
2017-07-16 14:52:38 +03:00
|
|
|
} else if (stripos($contentType, 'application/json') !== false) {
|
|
|
|
$rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody();
|
|
|
|
$body = json_decode($rawBody ?: '', true);
|
|
|
|
|
|
|
|
if (json_last_error()) {
|
|
|
|
throw new UserError("Could not parse JSON: " . json_last_error_msg());
|
|
|
|
}
|
|
|
|
if (!is_array($body)) {
|
|
|
|
throw new UserError(
|
|
|
|
"GraphQL Server expects JSON object or array, but got " .
|
|
|
|
Utils::printSafeJson($body)
|
|
|
|
);
|
|
|
|
}
|
2017-07-17 12:57:30 +03:00
|
|
|
if (isset($body[0])) {
|
|
|
|
$result = [];
|
|
|
|
foreach ($body as $index => $entry) {
|
|
|
|
$op = OperationParams::create($entry, true);
|
|
|
|
$result[] = $op;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$result = OperationParams::create($body);
|
|
|
|
}
|
2017-07-16 14:52:38 +03:00
|
|
|
} else if (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
|
2017-07-17 12:57:30 +03:00
|
|
|
$result = OperationParams::create($_POST);
|
2017-07-16 14:52:38 +03:00
|
|
|
} else if (null === $contentType) {
|
|
|
|
throw new UserError('Missing "Content-Type" header');
|
|
|
|
} else {
|
|
|
|
throw new UserError("Unexpected content type: " . Utils::printSafeJson($contentType));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new UserError('HTTP Method "' . $method . '" is not supported', 405);
|
|
|
|
}
|
2017-07-17 12:57:30 +03:00
|
|
|
return $result;
|
2017-07-16 14:52:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-07-17 12:57:30 +03:00
|
|
|
* @return bool|string
|
2017-07-16 14:52:38 +03:00
|
|
|
*/
|
2017-07-17 12:57:30 +03:00
|
|
|
public function readRawBody()
|
2017-07-16 14:52:38 +03:00
|
|
|
{
|
2017-07-17 12:57:30 +03:00
|
|
|
return file_get_contents('php://input');
|
2017-07-16 14:52:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-07-17 12:57:30 +03:00
|
|
|
* Checks validity of operation params and returns array of errors (empty array when params are valid)
|
|
|
|
*
|
|
|
|
* @param OperationParams $params
|
|
|
|
* @return array
|
2017-07-16 14:52:38 +03:00
|
|
|
*/
|
2017-07-17 12:57:30 +03:00
|
|
|
public function validateOperationParams(OperationParams $params)
|
2017-07-16 14:52:38 +03:00
|
|
|
{
|
2017-07-17 12:57:30 +03:00
|
|
|
$errors = [];
|
|
|
|
if (!$params->query && !$params->queryId) {
|
|
|
|
$errors[] = 'GraphQL Request must include at least one of those two parameters: "query" or "queryId"';
|
|
|
|
}
|
|
|
|
if ($params->query && $params->queryId) {
|
|
|
|
$errors[] = 'GraphQL Request parameters "query" and "queryId" are mutually exclusive';
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($params->query !== null && (!is_string($params->query) || empty($params->query))) {
|
|
|
|
$errors[] = 'GraphQL Request parameter "query" must be string, but got ' .
|
|
|
|
Utils::printSafeJson($params->query);
|
|
|
|
}
|
|
|
|
if ($params->queryId !== null && (!is_string($params->queryId) || empty($params->queryId))) {
|
|
|
|
$errors[] = 'GraphQL Request parameter "queryId" must be string, but got ' .
|
|
|
|
Utils::printSafeJson($params->queryId);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($params->operation !== null && (!is_string($params->operation) || empty($params->operation))) {
|
|
|
|
$errors[] = 'GraphQL Request parameter "operation" must be string, but got ' .
|
|
|
|
Utils::printSafeJson($params->operation);
|
|
|
|
}
|
|
|
|
if ($params->variables !== null && (!is_array($params->variables) || isset($params->variables[0]))) {
|
|
|
|
$errors[] = 'GraphQL Request parameter "variables" must be object, but got ' .
|
|
|
|
Utils::printSafeJson($params->variables);
|
|
|
|
}
|
|
|
|
return $errors;
|
2017-07-16 14:52:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Assertion to check that parsed body is valid instance of OperationParams (or array of instances)
|
|
|
|
*
|
2017-07-17 12:57:30 +03:00
|
|
|
* @param OperationParams|OperationParams[] $parsedBody
|
|
|
|
* @throws InvariantViolation
|
|
|
|
* @throws UserError
|
2017-07-16 14:52:38 +03:00
|
|
|
*/
|
2017-07-17 12:57:30 +03:00
|
|
|
public function assertValidRequest($parsedBody)
|
2017-07-16 14:52:38 +03:00
|
|
|
{
|
|
|
|
if (is_array($parsedBody)) {
|
|
|
|
foreach ($parsedBody as $index => $entry) {
|
|
|
|
if (!$entry instanceof OperationParams) {
|
|
|
|
throw new InvariantViolation(sprintf(
|
2017-07-17 12:57:30 +03:00
|
|
|
'GraphQL Server: Parsed http request must be an instance of %s or array of such instances, '.
|
|
|
|
'but got invalid array where entry at position %d is %s',
|
2017-07-16 14:52:38 +03:00
|
|
|
OperationParams::class,
|
|
|
|
$index,
|
|
|
|
Utils::printSafe($entry)
|
|
|
|
));
|
|
|
|
}
|
2017-07-17 12:57:30 +03:00
|
|
|
|
|
|
|
$errors = $this->validateOperationParams($entry);
|
2017-07-16 14:52:38 +03:00
|
|
|
|
|
|
|
if (!empty($errors[0])) {
|
|
|
|
$err = $index ? "Error in query #$index: {$errors[0]}" : $errors[0];
|
2017-07-17 12:57:30 +03:00
|
|
|
throw new UserError($err);
|
2017-07-14 15:08:47 +03:00
|
|
|
}
|
|
|
|
}
|
2017-07-17 12:57:30 +03:00
|
|
|
} else if ($parsedBody instanceof OperationParams) {
|
|
|
|
$errors = $this->validateOperationParams($parsedBody);
|
2017-07-16 14:52:38 +03:00
|
|
|
if (!empty($errors[0])) {
|
2017-07-17 12:57:30 +03:00
|
|
|
throw new UserError($errors[0]);
|
2017-07-14 15:08:47 +03:00
|
|
|
}
|
2017-07-17 12:57:30 +03:00
|
|
|
} else {
|
|
|
|
throw new InvariantViolation(sprintf(
|
|
|
|
'GraphQL Server: Parsed http request must be an instance of %s or array of such instances, but got %s',
|
|
|
|
OperationParams::class,
|
|
|
|
Utils::printSafe($parsedBody)
|
|
|
|
));
|
2017-07-14 15:08:47 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|