Added Server class as a new facade for library, including HTTP endpoint compatible with express-graphql format

This commit is contained in:
vladar 2016-12-19 20:16:27 +07:00
parent ff3a40d329
commit 296cc7530d
3 changed files with 534 additions and 2 deletions

View File

@ -3,7 +3,7 @@ namespace GraphQL\Executor;
use GraphQL\Error\Error; use GraphQL\Error\Error;
class ExecutionResult class ExecutionResult implements \JsonSerializable
{ {
/** /**
* @var array * @var array
@ -20,6 +20,11 @@ class ExecutionResult
*/ */
public $extensions; public $extensions;
/**
* @var callable
*/
private $errorFormatter = ['GraphQL\Error\Error', 'formatError'];
/** /**
* @param array $data * @param array $data
* @param array $errors * @param array $errors
@ -32,6 +37,16 @@ class ExecutionResult
$this->extensions = $extensions; $this->extensions = $extensions;
} }
/**
* @param callable $errorFormatter
* @return $this
*/
public function setErrorFormatter(callable $errorFormatter)
{
$this->errorFormatter = $errorFormatter;
return $this;
}
/** /**
* @return array * @return array
*/ */
@ -44,7 +59,7 @@ class ExecutionResult
} }
if (!empty($this->errors)) { if (!empty($this->errors)) {
$result['errors'] = array_map(['GraphQL\Error\Error', 'formatError'], $this->errors); $result['errors'] = array_map($this->errorFormatter, $this->errors);
} }
if (!empty($this->extensions)) { if (!empty($this->extensions)) {
@ -53,4 +68,9 @@ class ExecutionResult
return $result; return $result;
} }
public function jsonSerialize()
{
return $this->toArray();
}
} }

476
src/Server.php Normal file
View File

@ -0,0 +1,476 @@
<?php
namespace GraphQL;
use GraphQL\Error\Error;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\Parser;
use GraphQL\Type\Definition\Config;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Resolution;
use GraphQL\Validator\DocumentValidator;
class Server
{
const DEBUG_PHP_ERRORS = 1;
const DEBUG_EXCEPTIONS = 2;
const DEBUG_SCHEMA_CONFIGS = 4;
const DEBUG_ALL = 7;
private $queryType;
private $mutationType;
private $subscriptionType;
private $directives;
private $types = [];
private $schema;
private $debug = 0;
private $contextValue;
private $rootValue;
/**
* @var callable
*/
private $phpErrorFormatter = ['GraphQL\Error\FormattedError', 'createFromPHPError'];
/**
* @var callable
*/
private $exceptionFormatter = ['GraphQL\Error\FormattedError', 'createFromException'];
private $unexpectedErrorMessage = 'Unexpected Error';
private $unexpectedErrorStatus = 500;
private $validationRules;
/**
* @var Resolution
*/
private $typeResolutionStrategy;
/**
* @var PromiseAdapter
*/
private $promiseAdapter;
private $phpErrors = [];
/**
* @return static
*/
public static function create()
{
return new static();
}
/**
* @return ObjectType|null
*/
public function getQueryType()
{
return $this->queryType;
}
/**
* @param ObjectType $queryType
* @return Server
*/
public function setQueryType(ObjectType $queryType)
{
$this->queryType = $queryType;
return $this;
}
/**
* @return ObjectType|null
*/
public function getMutationType()
{
return $this->mutationType;
}
/**
* @param ObjectType $mutationType
* @return Server
*/
public function setMutationType(ObjectType $mutationType)
{
$this->mutationType = $mutationType;
return $this;
}
/**
* @return ObjectType|null
*/
public function getSubscriptionType()
{
return $this->subscriptionType;
}
/**
* @param ObjectType $subscriptionType
* @return Server
*/
public function setSubscriptionType($subscriptionType)
{
$this->subscriptionType = $subscriptionType;
return $this;
}
/**
* @param Type[] $types
* @return Server
*/
public function addTypes(array $types)
{
$this->types = array_merge($this->types, $types);
return $this;
}
public function getTypes()
{
return $this->types;
}
/**
* @return Directive[]
*/
public function getDirectives()
{
if (null === $this->directives) {
$this->directives = Directive::getInternalDirectives();
}
return $this->directives;
}
/**
* @param Directive[] $directives
* @return Server
*/
public function setDirectives(array $directives)
{
$this->directives = $directives;
return $this;
}
/**
* @return int
*/
public function getDebug()
{
return $this->debug;
}
/**
* @param int $debug
* @return Server
*/
public function setDebug($debug = self::DEBUG_ALL)
{
$this->debug = (int) $debug;
return $this;
}
/**
* @return mixed
*/
public function getContext()
{
return $this->contextValue;
}
/**
* @param mixed $context
* @return Server
*/
public function setContext($context)
{
$this->contextValue = $context;
return $this;
}
/**
* @param $rootValue
* @return Server
*/
public function setRootValue($rootValue)
{
$this->rootValue = $rootValue;
return $this;
}
/**
* @return mixed
*/
public function getRootValue()
{
return $this->rootValue;
}
/**
* @return Schema
*/
public function getSchema()
{
if (null === $this->schema) {
$this->schema = new Schema([
'query' => $this->queryType,
'mutation' => $this->mutationType,
'subscription' => $this->subscriptionType,
'directives' => $this->directives,
'types' => $this->types,
'typeResolution' => $this->typeResolutionStrategy
]);
}
return $this->schema;
}
/**
* @return callable
*/
public function getPhpErrorFormatter()
{
return $this->phpErrorFormatter;
}
/**
* @param callable $phpErrorFormatter
*/
public function setPhpErrorFormatter(callable $phpErrorFormatter)
{
$this->phpErrorFormatter = $phpErrorFormatter;
}
/**
* @return callable
*/
public function getExceptionFormatter()
{
return $this->exceptionFormatter;
}
/**
* @param callable $exceptionFormatter
*/
public function setExceptionFormatter(callable $exceptionFormatter)
{
$this->exceptionFormatter = $exceptionFormatter;
}
/**
* @return string
*/
public function getUnexpectedErrorMessage()
{
return $this->unexpectedErrorMessage;
}
/**
* @param string $unexpectedErrorMessage
*/
public function setUnexpectedErrorMessage($unexpectedErrorMessage)
{
$this->unexpectedErrorMessage = $unexpectedErrorMessage;
}
/**
* @return int
*/
public function getUnexpectedErrorStatus()
{
return $this->unexpectedErrorStatus;
}
/**
* @param int $unexpectedErrorStatus
*/
public function setUnexpectedErrorStatus($unexpectedErrorStatus)
{
$this->unexpectedErrorStatus = $unexpectedErrorStatus;
}
/**
* @param string $query
* @return Language\AST\DocumentNode
*/
public function parse($query)
{
return Parser::parse($query);
}
/**
* @return array
*/
public function getValidationRules()
{
if (null === $this->validationRules) {
$this->validationRules = DocumentValidator::allRules();
}
return $this->validationRules;
}
/**
* @param array $validationRules
*/
public function setValidationRules(array $validationRules)
{
$this->validationRules = $validationRules;
}
/**
* @return Resolution
*/
public function getTypeResolutionStrategy()
{
return $this->typeResolutionStrategy;
}
/**
* @param Resolution $typeResolutionStrategy
* @return Server
*/
public function setTypeResolutionStrategy(Resolution $typeResolutionStrategy)
{
$this->typeResolutionStrategy = $typeResolutionStrategy;
return $this;
}
/**
* @return PromiseAdapter
*/
public function getPromiseAdapter()
{
return $this->promiseAdapter;
}
/**
* See /docs/data-fetching.md#async-php
*
* @param PromiseAdapter $promiseAdapter
* @return Server
*/
public function setPromiseAdapter(PromiseAdapter $promiseAdapter)
{
$this->promiseAdapter = $promiseAdapter;
return $this;
}
/**
* Returns array with validation errors
*
* @param DocumentNode $query
* @return array
*/
public function validate(DocumentNode $query)
{
return DocumentValidator::validate($this->getSchema(), $query, $this->validationRules);
}
/**
* @param string|DocumentNode $query
* @param array|null $variables
* @param string|null $operationName
* @return ExecutionResult
*/
public function executeQuery($query, array $variables = null, $operationName = null)
{
$this->phpErrors = [];
if ($this->debug & static::DEBUG_PHP_ERRORS) {
// Catch custom errors (to report them in query results)
$lastDisplayErrors = ini_get('display_errors');
ini_set('display_errors', 0);
set_error_handler(function($severity, $message, $file, $line) {
$this->phpErrors[] = new \ErrorException($message, 0, $severity, $file, $line);
});
}
if ($this->debug & static::DEBUG_SCHEMA_CONFIGS) {
$isConfigValidationEnabled = Config::isValidationEnabled();
Config::enableValidation();
}
$result = GraphQL::executeAndReturnResult(
$this->getSchema(),
$query,
$this->getRootValue(),
$this->getContext(),
$variables,
$operationName
);
// Add details about original exception in error entry (if any)
if ($this->debug & static::DEBUG_EXCEPTIONS) {
$result->setErrorFormatter([$this, 'formatError']);
}
// Add reported PHP errors to result (if any)
if (!empty($this->phpErrors) && ($this->debug & static::DEBUG_PHP_ERRORS)) {
$result->extensions['phpErrors'] = array_map($this->phpErrorFormatter, $this->phpErrors);
}
if (isset($lastDisplayErrors)) {
ini_set('display_errors', $lastDisplayErrors);
restore_error_handler();
}
if (isset($isConfigValidationEnabled) && !$isConfigValidationEnabled) {
Config::disableValidation();
}
return $result;
}
public function handleRequest()
{
try {
$httpStatus = 200;
if (isset($_SERVER['CONTENT_TYPE']) && strpos($_SERVER['CONTENT_TYPE'], 'application/json') !== false) {
$raw = file_get_contents('php://input') ?: '';
$data = json_decode($raw, true);
} else {
$data = $_REQUEST;
}
$data += ['query' => null, 'variables' => null];
$result = $this->executeQuery($data['query'], (array) $data['variables'])->toArray();
} catch (\Exception $exception) {
// This is only possible for schema creation errors and some very unpredictable errors,
// (all errors which occur during query execution are caught and included in final response)
$httpStatus = $this->unexpectedErrorStatus;
$error = new Error($this->unexpectedErrorMessage, null, null, null, null, $exception);
$result = ['errors' => [$this->formatError($error)]];
}
header('Content-Type: application/json', true, $httpStatus);
echo json_encode($result);
}
private function formatException(\Exception $e)
{
$formatter = $this->exceptionFormatter;
return $formatter($e);
}
/**
* @param Error $e
* @return array
*/
public function formatError(\GraphQL\Error\Error $e)
{
$result = $e->toSerializableArray();
if (($this->debug & static::DEBUG_EXCEPTIONS) && $e->getPrevious()) {
$result['exception'] = $this->formatException($e->getPrevious());
}
return $result;
}
}

36
tests/ServerTest.php Normal file
View File

@ -0,0 +1,36 @@
<?php
namespace GraphQL\Tests;
use GraphQL\Error\InvariantViolation;
use GraphQL\Server;
use GraphQL\Type\Definition\Directive;
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(null, $server->getContext());
$this->assertEquals(null, $server->getRootValue());
$this->assertEquals(0, $server->getDebug());
$this->assertEquals(Directive::getInternalDirectives(), $server->getDirectives());
$this->assertEquals(['GraphQL\Error\FormattedError', 'createFromException'], $server->getExceptionFormatter());
$this->assertEquals(['GraphQL\Error\FormattedError', 'createFromPHPError'], $server->getPhpErrorFormatter());
$this->assertEquals(null, $server->getPromiseAdapter());
$this->assertEquals(null, $server->getTypeResolutionStrategy());
$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());
}
}
}