Version 4.5.0

This commit is contained in:
Neo committed 2025-12-19 05:51:58 -08:00
1 parent 1a0c7fd3c2
commit f79dcf067a
3791 files changed
+248032 -76372

No files matched your search

+559
View File
@@ -0,0 +1,559 @@
<?php
namespace GraphQL\Server;
use GraphQL\Error\Error;
use GraphQL\Error\FormattedError;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Executor;
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\GraphQL;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\Parser;
use GraphQL\Utils\AST;
use GraphQL\Utils\Utils;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
/**
* Contains functionality that could be re-used by various server implementations
*/
class Helper
{
/**
* Parses HTTP request using PHP globals and returns GraphQL OperationParams
* contained in this request. For batched requests it returns an array of OperationParams.
*
* This function does not check validity of these params
* (validation is performed separately in validateOperationParams() method).
*
* If $readRawBodyFn argument is not provided - will attempt to read raw request body
* from `php://input` stream.
*
* Internally it normalizes input to $method, $bodyParams and $queryParams and
* calls `parseRequestParams()` to produce actual return value.
*
* For PSR-7 request parsing use `parsePsrRequest()` instead.
*
* @api
* @param callable|null $readRawBodyFn
* @return OperationParams|OperationParams[]
* @throws RequestError
*/
public function parseHttpRequest(callable $readRawBodyFn = null)
{
$method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null;
$bodyParams = [];
$urlParams = $_GET;
if ($method === 'POST') {
$contentType = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : null;
if (stripos($contentType, 'application/graphql') !== false) {
$rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody();
$bodyParams = ['query' => $rawBody ?: ''];
} else if (stripos($contentType, 'application/json') !== false) {
$rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody();
$bodyParams = json_decode($rawBody ?: '', true);
if (json_last_error()) {
throw new RequestError("Could not parse JSON: " . json_last_error_msg());
}
if (!is_array($bodyParams)) {
throw new RequestError(
"GraphQL Server expects JSON object or array, but got " .
Utils::printSafeJson($bodyParams)
);
}
} else if (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
$bodyParams = $_POST;
} else if (null === $contentType) {
throw new RequestError('Missing "Content-Type" header');
} else {
throw new RequestError("Unexpected content type: " . Utils::printSafeJson($contentType));
}
}
return $this->parseRequestParams($method, $bodyParams, $urlParams);
}
/**
* Parses normalized request params and returns instance of OperationParams
* or array of OperationParams in case of batch operation.
*
* Returned value is a suitable input for `executeOperation` or `executeBatch` (if array)
*
* @api
* @param string $method
* @param array $bodyParams
* @param array $queryParams
* @return OperationParams|OperationParams[]
* @throws RequestError
*/
public function parseRequestParams($method, array $bodyParams, array $queryParams)
{
if ($method === 'GET') {
$result = OperationParams::create($queryParams, true);
} else if ($method === 'POST') {
if (isset($bodyParams[0])) {
$result = [];
foreach ($bodyParams as $index => $entry) {
$op = OperationParams::create($entry);
$result[] = $op;
}
} else {
$result = OperationParams::create($bodyParams);
}
} else {
throw new RequestError('HTTP Method "' . $method . '" is not supported');
}
return $result;
}
/**
* Checks validity of OperationParams extracted from HTTP request and returns an array of errors
* if params are invalid (or empty array when params are valid)
*
* @api
* @param OperationParams $params
* @return Error[]
*/
public function validateOperationParams(OperationParams $params)
{
$errors = [];
if (!$params->query && !$params->queryId) {
$errors[] = new RequestError('GraphQL Request must include at least one of those two parameters: "query" or "queryId"');
}
if ($params->query && $params->queryId) {
$errors[] = new RequestError('GraphQL Request parameters "query" and "queryId" are mutually exclusive');
}
if ($params->query !== null && (!is_string($params->query) || empty($params->query))) {
$errors[] = new RequestError(
'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[] = new RequestError(
'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[] = new RequestError(
'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[] = new RequestError(
'GraphQL Request parameter "variables" must be object or JSON string parsed to object, but got ' .
Utils::printSafeJson($params->getOriginalInput('variables'))
);
}
return $errors;
}
/**
* Executes GraphQL operation with given server configuration and returns execution result
* (or promise when promise adapter is different from SyncPromiseAdapter)
*
* @api
* @param ServerConfig $config
* @param OperationParams $op
*
* @return ExecutionResult|Promise
*/
public function executeOperation(ServerConfig $config, OperationParams $op)
{
$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)
*
* @api
* @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, true);
}
$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
* @param bool $isBatch
* @return Promise
*/
private function promiseToExecuteOperation(PromiseAdapter $promiseAdapter, ServerConfig $config, OperationParams $op, $isBatch = false)
{
try {
if (!$config->getSchema()) {
throw new InvariantViolation("Schema is required for the server");
}
if ($isBatch && !$config->getQueryBatching()) {
throw new RequestError("Batched queries are not supported by this server");
}
$errors = $this->validateOperationParams($op);
if (!empty($errors)) {
$errors = Utils::map($errors, function(RequestError $err) {
return Error::createLocatedError($err, null, null);
});
return $promiseAdapter->createFulfilled(
new ExecutionResult(null, $errors)
);
}
$doc = $op->queryId ? $this->loadPersistedQuery($config, $op) : $op->query;
if (!$doc instanceof DocumentNode) {
$doc = Parser::parse($doc);
}
$operationType = AST::getOperation($doc, $op->operation);
if ($op->isReadOnly() && $operationType !== 'query') {
throw new RequestError("GET supports only query operation");
}
$result = GraphQL::promiseToExecute(
$promiseAdapter,
$config->getSchema(),
$doc,
$this->resolveRootValue($config, $op, $doc, $operationType),
$this->resolveContextValue($config, $op, $doc, $operationType),
$op->variables,
$op->operation,
$config->getFieldResolver(),
$this->resolveValidationRules($config, $op, $doc, $operationType)
);
} catch (RequestError $e) {
$result = $promiseAdapter->createFulfilled(
new ExecutionResult(null, [Error::createLocatedError($e)])
);
} catch (Error $e) {
$result = $promiseAdapter->createFulfilled(
new ExecutionResult(null, [$e])
);
}
$applyErrorHandling = function (ExecutionResult $result) use ($config) {
if ($config->getErrorsHandler()) {
$result->setErrorsHandler($config->getErrorsHandler());
}
if ($config->getErrorFormatter() || $config->getDebug()) {
$result->setErrorFormatter(
FormattedError::prepareFormatter($config->getErrorFormatter(),
$config->getDebug())
);
}
return $result;
};
return $result->then($applyErrorHandling);
}
/**
* @param ServerConfig $config
* @param OperationParams $op
* @return mixed
* @throws RequestError
*/
private function loadPersistedQuery(ServerConfig $config, OperationParams $op)
{
// Load query if we got persisted query id:
$loader = $config->getPersistentQueryLoader();
if (!$loader) {
throw new RequestError("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
* @param DocumentNode $doc
* @param $operationType
* @return array
*/
private function resolveValidationRules(ServerConfig $config, OperationParams $params, DocumentNode $doc, $operationType)
{
// Allow customizing validation rules per operation:
$validationRules = $config->getValidationRules();
if (is_callable($validationRules)) {
$validationRules = $validationRules($params, $doc, $operationType);
if (!is_array($validationRules)) {
throw new InvariantViolation(sprintf(
"Expecting validation rules to be array or callable returning array, but got: %s",
Utils::printSafe($validationRules)
));
}
}
return $validationRules;
}
/**
* @param ServerConfig $config
* @param OperationParams $params
* @param DocumentNode $doc
* @param $operationType
* @return mixed
*/
private function resolveRootValue(ServerConfig $config, OperationParams $params, DocumentNode $doc, $operationType)
{
$root = $config->getRootValue();
if ($root instanceof \Closure) {
$root = $root($params, $doc, $operationType);
}
return $root;
}
/**
* @param ServerConfig $config
* @param OperationParams $params
* @param DocumentNode $doc
* @param $operationType
* @return mixed
*/
private function resolveContextValue(ServerConfig $config, OperationParams $params, DocumentNode $doc, $operationType)
{
$context = $config->getContext();
if ($context instanceof \Closure) {
$context = $context($params, $doc, $operationType);
}
return $context;
}
/**
* Send response using standard PHP `header()` and `echo`.
*
* @api
* @param Promise|ExecutionResult|ExecutionResult[] $result
* @param bool $exitWhenDone
*/
public function sendResponse($result, $exitWhenDone = false)
{
if ($result instanceof Promise) {
$result->then(function($actualResult) use ($exitWhenDone) {
$this->doSendResponse($actualResult, $exitWhenDone);
});
} else {
$this->doSendResponse($result, $exitWhenDone);
}
}
/**
* @param $result
* @param $exitWhenDone
*/
private function doSendResponse($result, $exitWhenDone)
{
$httpStatus = $this->resolveHttpStatus($result);
$this->emitResponse($result, $httpStatus, $exitWhenDone);
}
/**
* @param array|\JsonSerializable $jsonSerializable
* @param int $httpStatus
* @param bool $exitWhenDone
*/
public function emitResponse($jsonSerializable, $httpStatus, $exitWhenDone)
{
$body = json_encode($jsonSerializable);
header('Content-Type: application/json', true, $httpStatus);
echo $body;
if ($exitWhenDone) {
exit;
}
}
/**
* @return bool|string
*/
private function readRawBody()
{
return file_get_contents('php://input');
}
/**
* @param $result
* @return int
*/
private function resolveHttpStatus($result)
{
if (is_array($result) && isset($result[0])) {
Utils::each($result, function ($executionResult, $index) {
if (!$executionResult instanceof ExecutionResult) {
throw new InvariantViolation(sprintf(
"Expecting every entry of batched query result to be instance of %s but entry at position %d is %s",
ExecutionResult::class,
$index,
Utils::printSafe($executionResult)
));
}
});
$httpStatus = 200;
} else {
if (!$result instanceof ExecutionResult) {
throw new InvariantViolation(sprintf(
"Expecting query result to be instance of %s but got %s",
ExecutionResult::class,
Utils::printSafe($result)
));
}
if ($result->data === null && !empty($result->errors)) {
$httpStatus = 400;
} else {
$httpStatus = 200;
}
}
return $httpStatus;
}
/**
* Converts PSR-7 request to OperationParams[]
*
* @api
* @param ServerRequestInterface $request
* @return array|Helper
* @throws RequestError
*/
public function parsePsrRequest(ServerRequestInterface $request)
{
if ($request->getMethod() === 'GET') {
$bodyParams = [];
} else {
$contentType = $request->getHeader('content-type');
if (!isset($contentType[0])) {
throw new RequestError('Missing "Content-Type" header');
}
if (stripos('application/graphql', $contentType[0]) !== false) {
$bodyParams = ['query' => $request->getBody()->getContents()];
} else if (stripos('application/json', $contentType[0]) !== false) {
$bodyParams = $request->getParsedBody();
if (null === $bodyParams) {
throw new InvariantViolation(
"PSR-7 request is expected to provide parsed body for \"application/json\" requests but got null"
);
}
// Try parsing ourselves if PSR-7 implementation doesn't parse JSON automatically
if (is_array($bodyParams) && empty($bodyParams)) {
$bodyParams = json_decode($request->getBody(), true);
if (json_last_error()) {
throw new RequestError("Could not parse JSON: " . json_last_error_msg());
}
}
if (!is_array($bodyParams)) {
throw new RequestError(
"GraphQL Server expects JSON object or array, but got " .
Utils::printSafeJson($bodyParams)
);
}
} else {
$bodyParams = $request->getParsedBody();
if (!is_array($bodyParams)) {
throw new RequestError("Unexpected content type: " . Utils::printSafeJson($contentType[0]));
}
}
}
return $this->parseRequestParams(
$request->getMethod(),
$bodyParams,
$request->getQueryParams()
);
}
/**
* Converts query execution result to PSR-7 response
*
* @api
* @param Promise|ExecutionResult|ExecutionResult[] $result
* @param ResponseInterface $response
* @param StreamInterface $writableBodyStream
* @return Promise|ResponseInterface
*/
public function toPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream)
{
if ($result instanceof Promise) {
return $result->then(function($actualResult) use ($response, $writableBodyStream) {
return $this->doConvertToPsrResponse($actualResult, $response, $writableBodyStream);
});
} else {
return $this->doConvertToPsrResponse($result, $response, $writableBodyStream);
}
}
private function doConvertToPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream)
{
$httpStatus = $this->resolveHttpStatus($result);
$result = json_encode($result);
$writableBodyStream->write($result);
return $response
->withStatus($httpStatus)
->withHeader('Content-Type', 'application/json')
->withBody($writableBodyStream);
}
}
+115
View File
@@ -0,0 +1,115 @@
<?php
namespace GraphQL\Server;
/**
* Structure representing parsed HTTP parameters for GraphQL operation
*/
class OperationParams
{
/**
* Id of the query (when using persistent queries).
*
* Valid aliases (case-insensitive):
* - id
* - queryId
* - documentId
*
* @api
* @var string
*/
public $queryId;
/**
* @api
* @var string
*/
public $query;
/**
* @api
* @var string
*/
public $operation;
/**
* @api
* @var array
*/
public $variables;
/**
* @var array
*/
private $originalInput;
/**
* @var bool
*/
private $readOnly;
/**
* Creates an instance from given array
*
* @api
* @param array $params
* @param bool $readonly
* @return OperationParams
*/
public static function create(array $params, $readonly = false)
{
$instance = new static();
$params = array_change_key_case($params, CASE_LOWER);
$instance->originalInput = $params;
$params += [
'query' => null,
'queryid' => null,
'documentid' => null, // alias to queryid
'id' => null, // alias to queryid
'operation' => null,
'variables' => null
];
if ($params['variables'] === "") {
$params['variables'] = null;
}
if (is_string($params['variables'])) {
$tmp = json_decode($params['variables'], true);
if (!json_last_error()) {
$params['variables'] = $tmp;
}
}
$instance->query = $params['query'];
$instance->queryId = $params['queryid'] ?: $params['documentid'] ?: $params['id'];
$instance->operation = $params['operation'];
$instance->variables = $params['variables'];
$instance->readOnly = (bool) $readonly;
return $instance;
}
/**
* @api
* @param string $key
* @return mixed
*/
public function getOriginalInput($key)
{
return isset($this->originalInput[$key]) ? $this->originalInput[$key] : null;
}
/**
* Indicates that operation is executed in read-only context
* (e.g. via HTTP GET request)
*
* @api
* @return bool
*/
public function isReadOnly()
{
return $this->readOnly;
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace GraphQL\Server;
use GraphQL\Error\ClientAware;
class RequestError extends \Exception implements ClientAware
{
/**
* Returns true when exception message is safe to be displayed to client
*
* @return bool
*/
public function isClientSafe()
{
return true;
}
/**
* Returns string describing error category. E.g. "validation" for your own validation errors.
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*
* @return string
*/
public function getCategory()
{
return 'request';
}
}
+329
View File
@@ -0,0 +1,329 @@
<?php
namespace GraphQL\Server;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Type\Schema;
use GraphQL\Utils\Utils;
/**
* Server configuration class.
* Could be passed directly to server constructor. List of options accepted by **create** method is
* [described in docs](executing-queries.md#server-configuration-options).
*
* Usage example:
*
* $config = GraphQL\Server\ServerConfig::create()
* ->setSchema($mySchema)
* ->setContext($myContext);
*
* $server = new GraphQL\Server\StandardServer($config);
*/
class ServerConfig
{
/**
* Converts an array of options to instance of ServerConfig
* (or just returns empty config when array is not passed).
*
* @api
* @param array $config
* @return ServerConfig
*/
public static function create(array $config = [])
{
$instance = new static();
foreach ($config as $key => $value) {
$method = 'set' . ucfirst($key);
if (!method_exists($instance, $method)) {
throw new InvariantViolation("Unknown server config option \"$key\"");
}
$instance->$method($value);
}
return $instance;
}
/**
* @var Schema
*/
private $schema;
/**
* @var mixed|\Closure
*/
private $context;
/**
* @var mixed|\Closure
*/
private $rootValue;
/**
* @var callable|null
*/
private $errorFormatter;
/**
* @var callable|null
*/
private $errorsHandler;
/**
* @var bool
*/
private $debug = false;
/**
* @var bool
*/
private $queryBatching = false;
/**
* @var array|callable
*/
private $validationRules;
/**
* @var callable
*/
private $fieldResolver;
/**
* @var PromiseAdapter
*/
private $promiseAdapter;
/**
* @var callable
*/
private $persistentQueryLoader;
/**
* @api
* @param Schema $schema
* @return $this
*/
public function setSchema(Schema $schema)
{
$this->schema = $schema;
return $this;
}
/**
* @api
* @param mixed|\Closure $context
* @return $this
*/
public function setContext($context)
{
$this->context = $context;
return $this;
}
/**
* @api
* @param mixed|\Closure $rootValue
* @return $this
*/
public function setRootValue($rootValue)
{
$this->rootValue = $rootValue;
return $this;
}
/**
* Expects function(Throwable $e) : array
*
* @api
* @param callable $errorFormatter
* @return $this
*/
public function setErrorFormatter(callable $errorFormatter)
{
$this->errorFormatter = $errorFormatter;
return $this;
}
/**
* Expects function(array $errors, callable $formatter) : array
*
* @api
* @param callable $handler
* @return $this
*/
public function setErrorsHandler(callable $handler)
{
$this->errorsHandler = $handler;
return $this;
}
/**
* Set validation rules for this server.
*
* @api
* @param array|callable
* @return $this
*/
public function setValidationRules($validationRules)
{
if (!is_callable($validationRules) && !is_array($validationRules) && $validationRules !== null) {
throw new InvariantViolation(
'Server config expects array of validation rules or callable returning such array, but got ' .
Utils::printSafe($validationRules)
);
}
$this->validationRules = $validationRules;
return $this;
}
/**
* @api
* @param callable $fieldResolver
* @return $this
*/
public function setFieldResolver(callable $fieldResolver)
{
$this->fieldResolver = $fieldResolver;
return $this;
}
/**
* Expects function($queryId, OperationParams $params) : string|DocumentNode
*
* This function must return query string or valid DocumentNode.
*
* @api
* @param callable $persistentQueryLoader
* @return $this
*/
public function setPersistentQueryLoader(callable $persistentQueryLoader)
{
$this->persistentQueryLoader = $persistentQueryLoader;
return $this;
}
/**
* Set response debug flags. See GraphQL\Error\Debug class for a list of all available flags
*
* @api
* @param bool|int $set
* @return $this
*/
public function setDebug($set = true)
{
$this->debug = $set;
return $this;
}
/**
* Allow batching queries (disabled by default)
*
* @api
* @param bool $enableBatching
* @return $this
*/
public function setQueryBatching($enableBatching)
{
$this->queryBatching = (bool) $enableBatching;
return $this;
}
/**
* @api
* @param PromiseAdapter $promiseAdapter
* @return $this
*/
public function setPromiseAdapter(PromiseAdapter $promiseAdapter)
{
$this->promiseAdapter = $promiseAdapter;
return $this;
}
/**
* @return mixed|callable
*/
public function getContext()
{
return $this->context;
}
/**
* @return mixed|callable
*/
public function getRootValue()
{
return $this->rootValue;
}
/**
* @return Schema
*/
public function getSchema()
{
return $this->schema;
}
/**
* @return callable|null
*/
public function getErrorFormatter()
{
return $this->errorFormatter;
}
/**
* @return callable|null
*/
public function getErrorsHandler()
{
return $this->errorsHandler;
}
/**
* @return PromiseAdapter
*/
public function getPromiseAdapter()
{
return $this->promiseAdapter;
}
/**
* @return array|callable
*/
public function getValidationRules()
{
return $this->validationRules;
}
/**
* @return callable
*/
public function getFieldResolver()
{
return $this->fieldResolver;
}
/**
* @return callable
*/
public function getPersistentQueryLoader()
{
return $this->persistentQueryLoader;
}
/**
* @return bool
*/
public function getDebug()
{
return $this->debug;
}
/**
* @return bool
*/
public function getQueryBatching()
{
return $this->queryBatching;
}
}
+181
View File
@@ -0,0 +1,181 @@
<?php
namespace GraphQL\Server;
use GraphQL\Error\FormattedError;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Utils;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
/**
* GraphQL server compatible with both: [express-graphql](https://github.com/graphql/express-graphql)
* and [Apollo Server](https://github.com/apollographql/graphql-server).
* Usage Example:
*
* $server = new StandardServer([
* 'schema' => $mySchema
* ]);
* $server->handleRequest();
*
* Or using [ServerConfig](reference.md#graphqlserverserverconfig) instance:
*
* $config = GraphQL\Server\ServerConfig::create()
* ->setSchema($mySchema)
* ->setContext($myContext);
*
* $server = new GraphQL\Server\StandardServer($config);
* $server->handleRequest();
*
* See [dedicated section in docs](executing-queries.md#using-server) for details.
*
*/
class StandardServer
{
/**
* @var ServerConfig
*/
private $config;
/**
* @var Helper
*/
private $helper;
/**
* Converts and exception to error and sends spec-compliant HTTP 500 error.
* Useful when an exception is thrown somewhere outside of server execution context
* (e.g. during schema instantiation).
*
* @api
* @param \Throwable $error
* @param bool $debug
* @param bool $exitWhenDone
*/
public static function send500Error($error, $debug = false, $exitWhenDone = false)
{
$response = [
'errors' => [
FormattedError::createFromException($error, $debug)
]
];
$helper = new Helper();
$helper->emitResponse($response, 500, $exitWhenDone);
}
/**
* Creates new instance of a standard GraphQL HTTP server
*
* @api
* @param ServerConfig|array $config
*/
public function __construct($config)
{
if (is_array($config)) {
$config = ServerConfig::create($config);
}
if (!$config instanceof ServerConfig) {
throw new InvariantViolation("Expecting valid server config, but got " . Utils::printSafe($config));
}
$this->config = $config;
$this->helper = new Helper();
}
/**
* Parses HTTP request, executes and emits response (using standard PHP `header` function and `echo`)
*
* By default (when $parsedBody is not set) it uses PHP globals to parse a request.
* It is possible to implement request parsing elsewhere (e.g. using framework Request instance)
* and then pass it to the server.
*
* See `executeRequest()` if you prefer to emit response yourself
* (e.g. using Response object of some framework)
*
* @api
* @param OperationParams|OperationParams[] $parsedBody
* @param bool $exitWhenDone
*/
public function handleRequest($parsedBody = null, $exitWhenDone = false)
{
$result = $this->executeRequest($parsedBody);
$this->helper->sendResponse($result, $exitWhenDone);
}
/**
* Executes GraphQL operation and returns execution result
* (or promise when promise adapter is different from SyncPromiseAdapter).
*
* By default (when $parsedBody is not set) it uses PHP globals to parse a request.
* It is possible to implement request parsing elsewhere (e.g. using framework Request instance)
* and then pass it to the server.
*
* PSR-7 compatible method executePsrRequest() does exactly this.
*
* @api
* @param OperationParams|OperationParams[] $parsedBody
* @return ExecutionResult|ExecutionResult[]|Promise
* @throws InvariantViolation
*/
public function executeRequest($parsedBody = null)
{
if (null === $parsedBody) {
$parsedBody = $this->helper->parseHttpRequest();
}
if (is_array($parsedBody)) {
return $this->helper->executeBatch($this->config, $parsedBody);
} else {
return $this->helper->executeOperation($this->config, $parsedBody);
}
}
/**
* Executes PSR-7 request and fulfills PSR-7 response.
*
* See `executePsrRequest()` if you prefer to create response yourself
* (e.g. using specific JsonResponse instance of some framework).
*
* @api
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param StreamInterface $writableBodyStream
* @return ResponseInterface|Promise
*/
public function processPsrRequest(
ServerRequestInterface $request,
ResponseInterface $response,
StreamInterface $writableBodyStream
)
{
$result = $this->executePsrRequest($request);
return $this->helper->toPsrResponse($result, $response, $writableBodyStream);
}
/**
* Executes GraphQL operation and returns execution result
* (or promise when promise adapter is different from SyncPromiseAdapter)
*
* @api
* @param ServerRequestInterface $request
* @return ExecutionResult|ExecutionResult[]|Promise
*/
public function executePsrRequest(ServerRequestInterface $request)
{
$parsedBody = $this->helper->parsePsrRequest($request);
return $this->executeRequest($parsedBody);
}
/**
* Returns an instance of Server helper, which contains most of the actual logic for
* parsing / validating / executing request (which could be re-used by other server implementations)
*
* @api
* @return Helper
*/
public function getHelper()
{
return $this->helper;
}
}