Vendor
This commit is contained in:
+65
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL;
|
||||
|
||||
use Exception;
|
||||
use GraphQL\Executor\Promise\Adapter\SyncPromise;
|
||||
use SplQueue;
|
||||
use Throwable;
|
||||
|
||||
class Deferred
|
||||
{
|
||||
/** @var SplQueue|null */
|
||||
private static $queue;
|
||||
|
||||
/** @var callable */
|
||||
private $callback;
|
||||
|
||||
/** @var SyncPromise */
|
||||
public $promise;
|
||||
|
||||
public function __construct(callable $callback)
|
||||
{
|
||||
$this->callback = $callback;
|
||||
$this->promise = new SyncPromise();
|
||||
self::getQueue()->enqueue($this);
|
||||
}
|
||||
|
||||
public static function getQueue() : SplQueue
|
||||
{
|
||||
if (self::$queue === null) {
|
||||
self::$queue = new SplQueue();
|
||||
}
|
||||
|
||||
return self::$queue;
|
||||
}
|
||||
|
||||
public static function runQueue() : void
|
||||
{
|
||||
$queue = self::getQueue();
|
||||
while (! $queue->isEmpty()) {
|
||||
/** @var self $dequeuedNodeValue */
|
||||
$dequeuedNodeValue = $queue->dequeue();
|
||||
$dequeuedNodeValue->run();
|
||||
}
|
||||
}
|
||||
|
||||
public function then($onFulfilled = null, $onRejected = null)
|
||||
{
|
||||
return $this->promise->then($onFulfilled, $onRejected);
|
||||
}
|
||||
|
||||
public function run() : void
|
||||
{
|
||||
try {
|
||||
$cb = $this->callback;
|
||||
$this->promise->resolve($cb());
|
||||
} catch (Exception $e) {
|
||||
$this->promise->reject($e);
|
||||
} catch (Throwable $e) {
|
||||
$this->promise->reject($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Error;
|
||||
|
||||
/**
|
||||
* This interface is used for [default error formatting](error-handling.md).
|
||||
*
|
||||
* Only errors implementing this interface (and returning true from `isClientSafe()`)
|
||||
* will be formatted with original error message.
|
||||
*
|
||||
* All other errors will be formatted with generic "Internal server error".
|
||||
*/
|
||||
interface ClientAware
|
||||
{
|
||||
/**
|
||||
* Returns true when exception message is safe to be displayed to a client.
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function isClientSafe();
|
||||
|
||||
/**
|
||||
* Returns string describing a category of the error.
|
||||
*
|
||||
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getCategory();
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Error;
|
||||
|
||||
/**
|
||||
* Collection of flags for [error debugging](error-handling.md#debugging-tools).
|
||||
*/
|
||||
class Debug
|
||||
{
|
||||
const INCLUDE_DEBUG_MESSAGE = 1;
|
||||
const INCLUDE_TRACE = 2;
|
||||
const RETHROW_INTERNAL_EXCEPTIONS = 4;
|
||||
const RETHROW_UNSAFE_EXCEPTIONS = 8;
|
||||
}
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Error;
|
||||
|
||||
use Exception;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\Source;
|
||||
use GraphQL\Language\SourceLocation;
|
||||
use GraphQL\Utils\Utils;
|
||||
use JsonSerializable;
|
||||
use Throwable;
|
||||
use Traversable;
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
use function is_array;
|
||||
use function iterator_to_array;
|
||||
|
||||
/**
|
||||
* Describes an Error found during the parse, validate, or
|
||||
* execute phases of performing a GraphQL operation. In addition to a message
|
||||
* and stack trace, it also includes information about the locations in a
|
||||
* GraphQL document and/or execution result that correspond to the Error.
|
||||
*
|
||||
* When the error was caused by an exception thrown in resolver, original exception
|
||||
* is available via `getPrevious()`.
|
||||
*
|
||||
* Also read related docs on [error handling](error-handling.md)
|
||||
*
|
||||
* Class extends standard PHP `\Exception`, so all standard methods of base `\Exception` class
|
||||
* are available in addition to those listed below.
|
||||
*/
|
||||
class Error extends Exception implements JsonSerializable, ClientAware
|
||||
{
|
||||
const CATEGORY_GRAPHQL = 'graphql';
|
||||
const CATEGORY_INTERNAL = 'internal';
|
||||
|
||||
/**
|
||||
* A message describing the Error for debugging purposes.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $message;
|
||||
|
||||
/** @var SourceLocation[] */
|
||||
private $locations;
|
||||
|
||||
/**
|
||||
* An array describing the JSON-path into the execution response which
|
||||
* corresponds to this error. Only included for errors during execution.
|
||||
*
|
||||
* @var mixed[]|null
|
||||
*/
|
||||
public $path;
|
||||
|
||||
/**
|
||||
* An array of GraphQL AST Nodes corresponding to this error.
|
||||
*
|
||||
* @var Node[]|null
|
||||
*/
|
||||
public $nodes;
|
||||
|
||||
/**
|
||||
* The source GraphQL document for the first location of this error.
|
||||
*
|
||||
* Note that if this Error represents more than one node, the source may not
|
||||
* represent nodes after the first node.
|
||||
*
|
||||
* @var Source|null
|
||||
*/
|
||||
private $source;
|
||||
|
||||
/** @var int[]|null */
|
||||
private $positions;
|
||||
|
||||
/** @var bool */
|
||||
private $isClientSafe;
|
||||
|
||||
/** @var string */
|
||||
protected $category;
|
||||
|
||||
/** @var mixed[]|null */
|
||||
protected $extensions;
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @param Node|Node[]|Traversable|null $nodes
|
||||
* @param mixed[]|null $positions
|
||||
* @param mixed[]|null $path
|
||||
* @param Throwable $previous
|
||||
* @param mixed[] $extensions
|
||||
*/
|
||||
public function __construct(
|
||||
$message,
|
||||
$nodes = null,
|
||||
?Source $source = null,
|
||||
$positions = null,
|
||||
$path = null,
|
||||
$previous = null,
|
||||
array $extensions = []
|
||||
) {
|
||||
parent::__construct($message, 0, $previous);
|
||||
|
||||
// Compute list of blame nodes.
|
||||
if ($nodes instanceof Traversable) {
|
||||
$nodes = iterator_to_array($nodes);
|
||||
} elseif ($nodes && ! is_array($nodes)) {
|
||||
$nodes = [$nodes];
|
||||
}
|
||||
|
||||
$this->nodes = $nodes;
|
||||
$this->source = $source;
|
||||
$this->positions = $positions;
|
||||
$this->path = $path;
|
||||
$this->extensions = $extensions ?: (
|
||||
$previous && $previous instanceof self
|
||||
? $previous->extensions
|
||||
: []
|
||||
);
|
||||
|
||||
if ($previous instanceof ClientAware) {
|
||||
$this->isClientSafe = $previous->isClientSafe();
|
||||
$this->category = $previous->getCategory() ?: self::CATEGORY_INTERNAL;
|
||||
} elseif ($previous) {
|
||||
$this->isClientSafe = false;
|
||||
$this->category = self::CATEGORY_INTERNAL;
|
||||
} else {
|
||||
$this->isClientSafe = true;
|
||||
$this->category = self::CATEGORY_GRAPHQL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an arbitrary Error, presumably thrown while attempting to execute a
|
||||
* GraphQL operation, produce a new GraphQLError aware of the location in the
|
||||
* document responsible for the original Error.
|
||||
*
|
||||
* @param mixed $error
|
||||
* @param Node[]|null $nodes
|
||||
* @param mixed[]|null $path
|
||||
*
|
||||
* @return Error
|
||||
*/
|
||||
public static function createLocatedError($error, $nodes = null, $path = null)
|
||||
{
|
||||
if ($error instanceof self) {
|
||||
if ($error->path && $error->nodes) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$nodes = $nodes ?: $error->nodes;
|
||||
$path = $path ?: $error->path;
|
||||
}
|
||||
|
||||
$source = $positions = $originalError = null;
|
||||
$extensions = [];
|
||||
|
||||
if ($error instanceof self) {
|
||||
$message = $error->getMessage();
|
||||
$originalError = $error;
|
||||
$nodes = $error->nodes ?: $nodes;
|
||||
$source = $error->source;
|
||||
$positions = $error->positions;
|
||||
$extensions = $error->extensions;
|
||||
} elseif ($error instanceof Exception || $error instanceof Throwable) {
|
||||
$message = $error->getMessage();
|
||||
$originalError = $error;
|
||||
} else {
|
||||
$message = (string) $error;
|
||||
}
|
||||
|
||||
return new static(
|
||||
$message ?: 'An unknown error occurred.',
|
||||
$nodes,
|
||||
$source,
|
||||
$positions,
|
||||
$path,
|
||||
$originalError,
|
||||
$extensions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function formatError(Error $error)
|
||||
{
|
||||
return $error->toSerializableArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function isClientSafe()
|
||||
{
|
||||
return $this->isClientSafe;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Source|null
|
||||
*/
|
||||
public function getSource()
|
||||
{
|
||||
if ($this->source === null) {
|
||||
if (! empty($this->nodes[0]) && ! empty($this->nodes[0]->loc)) {
|
||||
$this->source = $this->nodes[0]->loc->source;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function getPositions()
|
||||
{
|
||||
if ($this->positions === null && ! empty($this->nodes)) {
|
||||
$positions = array_map(
|
||||
static function ($node) {
|
||||
return isset($node->loc) ? $node->loc->start : null;
|
||||
},
|
||||
$this->nodes
|
||||
);
|
||||
|
||||
$positions = array_filter(
|
||||
$positions,
|
||||
static function ($p) {
|
||||
return $p !== null;
|
||||
}
|
||||
);
|
||||
|
||||
$this->positions = array_values($positions);
|
||||
}
|
||||
|
||||
return $this->positions;
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of locations within the source GraphQL document which correspond to this error.
|
||||
*
|
||||
* Each entry has information about `line` and `column` within source GraphQL document:
|
||||
* $location->line;
|
||||
* $location->column;
|
||||
*
|
||||
* Errors during validation often contain multiple locations, for example to
|
||||
* point out to field mentioned in multiple fragments. Errors during execution include a
|
||||
* single location, the field which produced the error.
|
||||
*
|
||||
* @return SourceLocation[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getLocations()
|
||||
{
|
||||
if ($this->locations === null) {
|
||||
$positions = $this->getPositions();
|
||||
$source = $this->getSource();
|
||||
$nodes = $this->nodes;
|
||||
|
||||
if ($positions && $source) {
|
||||
$this->locations = array_map(
|
||||
static function ($pos) use ($source) {
|
||||
return $source->getLocation($pos);
|
||||
},
|
||||
$positions
|
||||
);
|
||||
} elseif ($nodes) {
|
||||
$locations = array_filter(
|
||||
array_map(
|
||||
static function ($node) {
|
||||
if ($node->loc && $node->loc->source) {
|
||||
return $node->loc->source->getLocation($node->loc->start);
|
||||
}
|
||||
},
|
||||
$nodes
|
||||
)
|
||||
);
|
||||
$this->locations = array_values($locations);
|
||||
} else {
|
||||
$this->locations = [];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->locations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Node[]|null
|
||||
*/
|
||||
public function getNodes()
|
||||
{
|
||||
return $this->nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array describing the path from the root value to the field which produced this error.
|
||||
* Only included for execution errors.
|
||||
*
|
||||
* @return mixed[]|null
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getExtensions()
|
||||
{
|
||||
return $this->extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array representation of error suitable for serialization
|
||||
*
|
||||
* @deprecated Use FormattedError::createFromException() instead
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function toSerializableArray()
|
||||
{
|
||||
$arr = [
|
||||
'message' => $this->getMessage(),
|
||||
];
|
||||
|
||||
$locations = Utils::map(
|
||||
$this->getLocations(),
|
||||
static function (SourceLocation $loc) {
|
||||
return $loc->toSerializableArray();
|
||||
}
|
||||
);
|
||||
|
||||
if (! empty($locations)) {
|
||||
$arr['locations'] = $locations;
|
||||
}
|
||||
if (! empty($this->path)) {
|
||||
$arr['path'] = $this->path;
|
||||
}
|
||||
if (! empty($this->extensions)) {
|
||||
$arr['extensions'] = $this->extensions;
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify data which should be serialized to JSON
|
||||
*
|
||||
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
|
||||
*
|
||||
* @return mixed data which can be serialized by <b>json_encode</b>,
|
||||
* which is a value of any type other than a resource.
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toSerializableArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return FormattedError::printError($this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Error;
|
||||
|
||||
use Countable;
|
||||
use ErrorException;
|
||||
use Exception;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\Source;
|
||||
use GraphQL\Language\SourceLocation;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\WrappingType;
|
||||
use GraphQL\Utils\Utils;
|
||||
use Throwable;
|
||||
use function addcslashes;
|
||||
use function array_filter;
|
||||
use function array_intersect_key;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_shift;
|
||||
use function count;
|
||||
use function get_class;
|
||||
use function gettype;
|
||||
use function implode;
|
||||
use function is_array;
|
||||
use function is_bool;
|
||||
use function is_object;
|
||||
use function is_scalar;
|
||||
use function is_string;
|
||||
use function mb_strlen;
|
||||
use function preg_split;
|
||||
use function sprintf;
|
||||
use function str_repeat;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* This class is used for [default error formatting](error-handling.md).
|
||||
* It converts PHP exceptions to [spec-compliant errors](https://facebook.github.io/graphql/#sec-Errors)
|
||||
* and provides tools for error debugging.
|
||||
*/
|
||||
class FormattedError
|
||||
{
|
||||
/** @var string */
|
||||
private static $internalErrorMessage = 'Internal server error';
|
||||
|
||||
/**
|
||||
* Set default error message for internal errors formatted using createFormattedError().
|
||||
* This value can be overridden by passing 3rd argument to `createFormattedError()`.
|
||||
*
|
||||
* @param string $msg
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function setInternalErrorMessage($msg)
|
||||
{
|
||||
self::$internalErrorMessage = $msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a GraphQLError to a string, representing useful location information
|
||||
* about the error's position in the source.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function printError(Error $error)
|
||||
{
|
||||
$printedLocations = [];
|
||||
if ($error->nodes) {
|
||||
/** @var Node $node */
|
||||
foreach ($error->nodes as $node) {
|
||||
if (! $node->loc) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($node->loc->source === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$printedLocations[] = self::highlightSourceAtLocation(
|
||||
$node->loc->source,
|
||||
$node->loc->source->getLocation($node->loc->start)
|
||||
);
|
||||
}
|
||||
} elseif ($error->getSource() && $error->getLocations()) {
|
||||
$source = $error->getSource();
|
||||
foreach ($error->getLocations() as $location) {
|
||||
$printedLocations[] = self::highlightSourceAtLocation($source, $location);
|
||||
}
|
||||
}
|
||||
|
||||
return ! $printedLocations
|
||||
? $error->getMessage()
|
||||
: implode("\n\n", array_merge([$error->getMessage()], $printedLocations)) . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a helpful description of the location of the error in the GraphQL
|
||||
* Source document.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function highlightSourceAtLocation(Source $source, SourceLocation $location)
|
||||
{
|
||||
$line = $location->line;
|
||||
$lineOffset = $source->locationOffset->line - 1;
|
||||
$columnOffset = self::getColumnOffset($source, $location);
|
||||
$contextLine = $line + $lineOffset;
|
||||
$contextColumn = $location->column + $columnOffset;
|
||||
$prevLineNum = (string) ($contextLine - 1);
|
||||
$lineNum = (string) $contextLine;
|
||||
$nextLineNum = (string) ($contextLine + 1);
|
||||
$padLen = strlen($nextLineNum);
|
||||
$lines = preg_split('/\r\n|[\n\r]/', $source->body);
|
||||
|
||||
$lines[0] = self::whitespace($source->locationOffset->column - 1) . $lines[0];
|
||||
|
||||
$outputLines = [
|
||||
sprintf('%s (%s:%s)', $source->name, $contextLine, $contextColumn),
|
||||
$line >= 2 ? (self::lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2]) : null,
|
||||
self::lpad($padLen, $lineNum) . ': ' . $lines[$line - 1],
|
||||
self::whitespace(2 + $padLen + $contextColumn - 1) . '^',
|
||||
$line < count($lines) ? self::lpad($padLen, $nextLineNum) . ': ' . $lines[$line] : null,
|
||||
];
|
||||
|
||||
return implode("\n", array_filter($outputLines));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
private static function getColumnOffset(Source $source, SourceLocation $location)
|
||||
{
|
||||
return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $len
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function whitespace($len)
|
||||
{
|
||||
return str_repeat(' ', $len);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $len
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function lpad($len, $str)
|
||||
{
|
||||
return self::whitespace($len - mb_strlen($str)) . $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard GraphQL error formatter. Converts any exception to array
|
||||
* conforming to GraphQL spec.
|
||||
*
|
||||
* This method only exposes exception message when exception implements ClientAware interface
|
||||
* (or when debug flags are passed).
|
||||
*
|
||||
* For a list of available debug flags see GraphQL\Error\Debug constants.
|
||||
*
|
||||
* @param Throwable $e
|
||||
* @param bool|int $debug
|
||||
* @param string $internalErrorMessage
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @throws Throwable
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function createFromException($e, $debug = false, $internalErrorMessage = null)
|
||||
{
|
||||
Utils::invariant(
|
||||
$e instanceof Exception || $e instanceof Throwable,
|
||||
'Expected exception, got %s',
|
||||
Utils::getVariableType($e)
|
||||
);
|
||||
|
||||
$internalErrorMessage = $internalErrorMessage ?: self::$internalErrorMessage;
|
||||
|
||||
if ($e instanceof ClientAware) {
|
||||
$formattedError = [
|
||||
'message' => $e->isClientSafe() ? $e->getMessage() : $internalErrorMessage,
|
||||
'extensions' => [
|
||||
'category' => $e->getCategory(),
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$formattedError = [
|
||||
'message' => $internalErrorMessage,
|
||||
'extensions' => [
|
||||
'category' => Error::CATEGORY_INTERNAL,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if ($e instanceof Error) {
|
||||
$locations = Utils::map(
|
||||
$e->getLocations(),
|
||||
static function (SourceLocation $loc) {
|
||||
return $loc->toSerializableArray();
|
||||
}
|
||||
);
|
||||
if (! empty($locations)) {
|
||||
$formattedError['locations'] = $locations;
|
||||
}
|
||||
if (! empty($e->path)) {
|
||||
$formattedError['path'] = $e->path;
|
||||
}
|
||||
if (! empty($e->getExtensions())) {
|
||||
$formattedError['extensions'] = $e->getExtensions() + $formattedError['extensions'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($debug) {
|
||||
$formattedError = self::addDebugEntries($formattedError, $e, $debug);
|
||||
}
|
||||
|
||||
return $formattedError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorates spec-compliant $formattedError with debug entries according to $debug flags
|
||||
* (see GraphQL\Error\Debug for available flags)
|
||||
*
|
||||
* @param mixed[] $formattedError
|
||||
* @param Throwable $e
|
||||
* @param bool|int $debug
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function addDebugEntries(array $formattedError, $e, $debug)
|
||||
{
|
||||
if (! $debug) {
|
||||
return $formattedError;
|
||||
}
|
||||
|
||||
Utils::invariant(
|
||||
$e instanceof Exception || $e instanceof Throwable,
|
||||
'Expected exception, got %s',
|
||||
Utils::getVariableType($e)
|
||||
);
|
||||
|
||||
$debug = (int) $debug;
|
||||
|
||||
if ($debug & Debug::RETHROW_INTERNAL_EXCEPTIONS) {
|
||||
if (! $e instanceof Error) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($e->getPrevious()) {
|
||||
throw $e->getPrevious();
|
||||
}
|
||||
}
|
||||
|
||||
$isUnsafe = ! $e instanceof ClientAware || ! $e->isClientSafe();
|
||||
|
||||
if (($debug & Debug::RETHROW_UNSAFE_EXCEPTIONS) && $isUnsafe) {
|
||||
if ($e->getPrevious()) {
|
||||
throw $e->getPrevious();
|
||||
}
|
||||
}
|
||||
|
||||
if (($debug & Debug::INCLUDE_DEBUG_MESSAGE) && $isUnsafe) {
|
||||
// Displaying debugMessage as a first entry:
|
||||
$formattedError = ['debugMessage' => $e->getMessage()] + $formattedError;
|
||||
}
|
||||
|
||||
if ($debug & Debug::INCLUDE_TRACE) {
|
||||
if ($e instanceof ErrorException || $e instanceof \Error) {
|
||||
$formattedError += [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
];
|
||||
}
|
||||
|
||||
$isTrivial = $e instanceof Error && ! $e->getPrevious();
|
||||
|
||||
if (! $isTrivial) {
|
||||
$debugging = $e->getPrevious() ?: $e;
|
||||
$formattedError['trace'] = static::toSafeTrace($debugging);
|
||||
}
|
||||
}
|
||||
|
||||
return $formattedError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares final error formatter taking in account $debug flags.
|
||||
* If initial formatter is not set, FormattedError::createFromException is used
|
||||
*
|
||||
* @param bool|int $debug
|
||||
*
|
||||
* @return callable|callable
|
||||
*/
|
||||
public static function prepareFormatter(?callable $formatter = null, $debug)
|
||||
{
|
||||
$formatter = $formatter ?: static function ($e) {
|
||||
return FormattedError::createFromException($e);
|
||||
};
|
||||
if ($debug) {
|
||||
$formatter = static function ($e) use ($formatter, $debug) {
|
||||
return FormattedError::addDebugEntries($formatter($e), $e, $debug);
|
||||
};
|
||||
}
|
||||
|
||||
return $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns error trace as serializable array
|
||||
*
|
||||
* @param Throwable $error
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function toSafeTrace($error)
|
||||
{
|
||||
$trace = $error->getTrace();
|
||||
|
||||
if (isset($trace[0]['function']) && isset($trace[0]['class']) &&
|
||||
// Remove invariant entries as they don't provide much value:
|
||||
($trace[0]['class'] . '::' . $trace[0]['function'] === 'GraphQL\Utils\Utils::invariant')) {
|
||||
array_shift($trace);
|
||||
} elseif (! isset($trace[0]['file'])) {
|
||||
// Remove root call as it's likely error handler trace:
|
||||
array_shift($trace);
|
||||
}
|
||||
|
||||
return array_map(
|
||||
static function ($err) {
|
||||
$safeErr = array_intersect_key($err, ['file' => true, 'line' => true]);
|
||||
|
||||
if (isset($err['function'])) {
|
||||
$func = $err['function'];
|
||||
$args = ! empty($err['args']) ? array_map([self::class, 'printVar'], $err['args']) : [];
|
||||
$funcStr = $func . '(' . implode(', ', $args) . ')';
|
||||
|
||||
if (isset($err['class'])) {
|
||||
$safeErr['call'] = $err['class'] . '::' . $funcStr;
|
||||
} else {
|
||||
$safeErr['function'] = $funcStr;
|
||||
}
|
||||
}
|
||||
|
||||
return $safeErr;
|
||||
},
|
||||
$trace
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $var
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function printVar($var)
|
||||
{
|
||||
if ($var instanceof Type) {
|
||||
// FIXME: Replace with schema printer call
|
||||
if ($var instanceof WrappingType) {
|
||||
$var = $var->getWrappedType(true);
|
||||
}
|
||||
|
||||
return 'GraphQLType: ' . $var->name;
|
||||
}
|
||||
|
||||
if (is_object($var)) {
|
||||
return 'instance of ' . get_class($var) . ($var instanceof Countable ? '(' . count($var) . ')' : '');
|
||||
}
|
||||
if (is_array($var)) {
|
||||
return 'array(' . count($var) . ')';
|
||||
}
|
||||
if ($var === '') {
|
||||
return '(empty string)';
|
||||
}
|
||||
if (is_string($var)) {
|
||||
return "'" . addcslashes($var, "'") . "'";
|
||||
}
|
||||
if (is_bool($var)) {
|
||||
return $var ? 'true' : 'false';
|
||||
}
|
||||
if (is_scalar($var)) {
|
||||
return $var;
|
||||
}
|
||||
if ($var === null) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
return gettype($var);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated as of v0.8.0
|
||||
*
|
||||
* @param string $error
|
||||
* @param SourceLocation[] $locations
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function create($error, array $locations = [])
|
||||
{
|
||||
$formatted = ['message' => $error];
|
||||
|
||||
if (! empty($locations)) {
|
||||
$formatted['locations'] = array_map(
|
||||
static function ($loc) {
|
||||
return $loc->toArray();
|
||||
},
|
||||
$locations
|
||||
);
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated as of v0.10.0, use general purpose method createFromException() instead
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function createFromPHPError(ErrorException $e)
|
||||
{
|
||||
return [
|
||||
'message' => $e->getMessage(),
|
||||
'severity' => $e->getSeverity(),
|
||||
'trace' => self::toSafeTrace($e),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Error;
|
||||
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* Note:
|
||||
* This exception should not inherit base Error exception as it is raised when there is an error somewhere in
|
||||
* user-land code
|
||||
*/
|
||||
class InvariantViolation extends LogicException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Error;
|
||||
|
||||
use GraphQL\Language\Source;
|
||||
use function sprintf;
|
||||
|
||||
class SyntaxError extends Error
|
||||
{
|
||||
/**
|
||||
* @param int $position
|
||||
* @param string $description
|
||||
*/
|
||||
public function __construct(Source $source, $position, $description)
|
||||
{
|
||||
parent::__construct(
|
||||
sprintf('Syntax Error: %s', $description),
|
||||
null,
|
||||
$source,
|
||||
[$position]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Error;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Error caused by actions of GraphQL clients. Can be safely displayed to a client...
|
||||
*/
|
||||
class UserError extends RuntimeException implements ClientAware
|
||||
{
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isClientSafe()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
return 'user';
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Error;
|
||||
|
||||
use function trigger_error;
|
||||
use const E_USER_WARNING;
|
||||
|
||||
/**
|
||||
* Encapsulates warnings produced by the library.
|
||||
*
|
||||
* Warnings can be suppressed (individually or all) if required.
|
||||
* Also it is possible to override warning handler (which is **trigger_error()** by default)
|
||||
*/
|
||||
final class Warning
|
||||
{
|
||||
const WARNING_ASSIGN = 2;
|
||||
const WARNING_CONFIG = 4;
|
||||
const WARNING_FULL_SCHEMA_SCAN = 8;
|
||||
const WARNING_CONFIG_DEPRECATION = 16;
|
||||
const WARNING_NOT_A_TYPE = 32;
|
||||
const ALL = 63;
|
||||
|
||||
/** @var int */
|
||||
private static $enableWarnings = self::ALL;
|
||||
|
||||
/** @var mixed[] */
|
||||
private static $warned = [];
|
||||
|
||||
/** @var callable|null */
|
||||
private static $warningHandler;
|
||||
|
||||
/**
|
||||
* Sets warning handler which can intercept all system warnings.
|
||||
* When not set, trigger_error() is used to notify about warnings.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function setWarningHandler(?callable $warningHandler = null)
|
||||
{
|
||||
self::$warningHandler = $warningHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress warning by id (has no effect when custom warning handler is set)
|
||||
*
|
||||
* Usage example:
|
||||
* Warning::suppress(Warning::WARNING_NOT_A_TYPE)
|
||||
*
|
||||
* When passing true - suppresses all warnings.
|
||||
*
|
||||
* @param bool|int $suppress
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function suppress($suppress = true)
|
||||
{
|
||||
if ($suppress === true) {
|
||||
self::$enableWarnings = 0;
|
||||
} elseif ($suppress === false) {
|
||||
self::$enableWarnings = self::ALL;
|
||||
} else {
|
||||
self::$enableWarnings &= ~$suppress;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-enable previously suppressed warning by id
|
||||
*
|
||||
* Usage example:
|
||||
* Warning::suppress(Warning::WARNING_NOT_A_TYPE)
|
||||
*
|
||||
* When passing true - re-enables all warnings.
|
||||
*
|
||||
* @param bool|int $enable
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function enable($enable = true)
|
||||
{
|
||||
if ($enable === true) {
|
||||
self::$enableWarnings = self::ALL;
|
||||
} elseif ($enable === false) {
|
||||
self::$enableWarnings = 0;
|
||||
} else {
|
||||
self::$enableWarnings |= $enable;
|
||||
}
|
||||
}
|
||||
|
||||
public static function warnOnce($errorMessage, $warningId, $messageLevel = null)
|
||||
{
|
||||
if (self::$warningHandler) {
|
||||
$fn = self::$warningHandler;
|
||||
$fn($errorMessage, $warningId);
|
||||
} elseif ((self::$enableWarnings & $warningId) > 0 && ! isset(self::$warned[$warningId])) {
|
||||
self::$warned[$warningId] = true;
|
||||
trigger_error($errorMessage, $messageLevel ?: E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
public static function warn($errorMessage, $warningId, $messageLevel = null)
|
||||
{
|
||||
if (self::$warningHandler) {
|
||||
$fn = self::$warningHandler;
|
||||
$fn($errorMessage, $warningId);
|
||||
} elseif ((self::$enableWarnings & $warningId) > 0) {
|
||||
trigger_error($errorMessage, $messageLevel ?: E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Executor;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Executor\Promise\PromiseAdapter;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Type\Schema;
|
||||
|
||||
/**
|
||||
* Data that must be available at all points during query execution.
|
||||
*
|
||||
* Namely, schema of the type system that is currently executing,
|
||||
* and the fragments defined in the query document
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ExecutionContext
|
||||
{
|
||||
/** @var Schema */
|
||||
public $schema;
|
||||
|
||||
/** @var FragmentDefinitionNode[] */
|
||||
public $fragments;
|
||||
|
||||
/** @var mixed */
|
||||
public $rootValue;
|
||||
|
||||
/** @var mixed */
|
||||
public $contextValue;
|
||||
|
||||
/** @var OperationDefinitionNode */
|
||||
public $operation;
|
||||
|
||||
/** @var mixed[] */
|
||||
public $variableValues;
|
||||
|
||||
/** @var callable */
|
||||
public $fieldResolver;
|
||||
|
||||
/** @var Error[] */
|
||||
public $errors;
|
||||
|
||||
/** @var PromiseAdapter */
|
||||
public $promises;
|
||||
|
||||
public function __construct(
|
||||
$schema,
|
||||
$fragments,
|
||||
$root,
|
||||
$contextValue,
|
||||
$operation,
|
||||
$variables,
|
||||
$errors,
|
||||
$fieldResolver,
|
||||
$promiseAdapter
|
||||
) {
|
||||
$this->schema = $schema;
|
||||
$this->fragments = $fragments;
|
||||
$this->rootValue = $root;
|
||||
$this->contextValue = $contextValue;
|
||||
$this->operation = $operation;
|
||||
$this->variableValues = $variables;
|
||||
$this->errors = $errors ?: [];
|
||||
$this->fieldResolver = $fieldResolver;
|
||||
$this->promises = $promiseAdapter;
|
||||
}
|
||||
|
||||
public function addError(Error $error)
|
||||
{
|
||||
$this->errors[] = $error;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Executor;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\FormattedError;
|
||||
use JsonSerializable;
|
||||
use function array_map;
|
||||
|
||||
/**
|
||||
* Returned after [query execution](executing-queries.md).
|
||||
* Represents both - result of successful execution and of a failed one
|
||||
* (with errors collected in `errors` prop)
|
||||
*
|
||||
* Could be converted to [spec-compliant](https://facebook.github.io/graphql/#sec-Response-Format)
|
||||
* serializable array using `toArray()`
|
||||
*/
|
||||
class ExecutionResult implements JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Data collected from resolvers during query execution
|
||||
*
|
||||
* @api
|
||||
* @var mixed[]
|
||||
*/
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* Errors registered during query execution.
|
||||
*
|
||||
* If an error was caused by exception thrown in resolver, $error->getPrevious() would
|
||||
* contain original exception.
|
||||
*
|
||||
* @api
|
||||
* @var Error[]
|
||||
*/
|
||||
public $errors;
|
||||
|
||||
/**
|
||||
* User-defined serializable array of extensions included in serialized result.
|
||||
* Conforms to
|
||||
*
|
||||
* @api
|
||||
* @var mixed[]
|
||||
*/
|
||||
public $extensions;
|
||||
|
||||
/** @var callable */
|
||||
private $errorFormatter;
|
||||
|
||||
/** @var callable */
|
||||
private $errorsHandler;
|
||||
|
||||
/**
|
||||
* @param mixed[] $data
|
||||
* @param Error[] $errors
|
||||
* @param mixed[] $extensions
|
||||
*/
|
||||
public function __construct($data = null, array $errors = [], array $extensions = [])
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->errors = $errors;
|
||||
$this->extensions = $extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define custom error formatting (must conform to http://facebook.github.io/graphql/#sec-Errors)
|
||||
*
|
||||
* Expected signature is: function (GraphQL\Error\Error $error): array
|
||||
*
|
||||
* Default formatter is "GraphQL\Error\FormattedError::createFromException"
|
||||
*
|
||||
* Expected returned value must be an array:
|
||||
* array(
|
||||
* 'message' => 'errorMessage',
|
||||
* // ... other keys
|
||||
* );
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function setErrorFormatter(callable $errorFormatter)
|
||||
{
|
||||
$this->errorFormatter = $errorFormatter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define custom logic for error handling (filtering, logging, etc).
|
||||
*
|
||||
* Expected handler signature is: function (array $errors, callable $formatter): array
|
||||
*
|
||||
* Default handler is:
|
||||
* function (array $errors, callable $formatter) {
|
||||
* return array_map($formatter, $errors);
|
||||
* }
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function setErrorsHandler(callable $handler)
|
||||
{
|
||||
$this->errorsHandler = $handler;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts GraphQL query result to spec-compliant serializable array using provided
|
||||
* errors handler and formatter.
|
||||
*
|
||||
* If debug argument is passed, output of error formatter is enriched which debugging information
|
||||
* ("debugMessage", "trace" keys depending on flags).
|
||||
*
|
||||
* $debug argument must be either bool (only adds "debugMessage" to result) or sum of flags from
|
||||
* GraphQL\Error\Debug
|
||||
*
|
||||
* @param bool|int $debug
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function toArray($debug = false)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
if (! empty($this->errors)) {
|
||||
$errorsHandler = $this->errorsHandler ?: static function (array $errors, callable $formatter) {
|
||||
return array_map($formatter, $errors);
|
||||
};
|
||||
|
||||
$result['errors'] = $errorsHandler(
|
||||
$this->errors,
|
||||
FormattedError::prepareFormatter($this->errorFormatter, $debug)
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->data !== null) {
|
||||
$result['data'] = $this->data;
|
||||
}
|
||||
|
||||
if (! empty($this->extensions)) {
|
||||
$result['extensions'] = $this->extensions;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Executor;
|
||||
|
||||
use ArrayAccess;
|
||||
use Closure;
|
||||
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
|
||||
use GraphQL\Executor\Promise\Promise;
|
||||
use GraphQL\Executor\Promise\PromiseAdapter;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
use GraphQL\Type\Schema;
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
|
||||
/**
|
||||
* Implements the "Evaluating requests" section of the GraphQL specification.
|
||||
*/
|
||||
class Executor
|
||||
{
|
||||
/** @var callable|string[] */
|
||||
private static $defaultFieldResolver = [self::class, 'defaultFieldResolver'];
|
||||
|
||||
/** @var PromiseAdapter */
|
||||
private static $defaultPromiseAdapter;
|
||||
|
||||
/** @var callable */
|
||||
private static $implementationFactory = [ReferenceExecutor::class, 'create'];
|
||||
|
||||
public static function getDefaultFieldResolver() : callable
|
||||
{
|
||||
return self::$defaultFieldResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom default resolve function.
|
||||
*/
|
||||
public static function setDefaultFieldResolver(callable $fieldResolver)
|
||||
{
|
||||
self::$defaultFieldResolver = $fieldResolver;
|
||||
}
|
||||
|
||||
public static function getPromiseAdapter() : PromiseAdapter
|
||||
{
|
||||
return self::$defaultPromiseAdapter ?: (self::$defaultPromiseAdapter = new SyncPromiseAdapter());
|
||||
}
|
||||
|
||||
public static function setPromiseAdapter(?PromiseAdapter $defaultPromiseAdapter = null)
|
||||
{
|
||||
self::$defaultPromiseAdapter = $defaultPromiseAdapter;
|
||||
}
|
||||
|
||||
public static function getImplementationFactory() : callable
|
||||
{
|
||||
return self::$implementationFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom executor implementation factory.
|
||||
*
|
||||
* Will be called with as
|
||||
*/
|
||||
public static function setImplementationFactory(callable $implementationFactory)
|
||||
{
|
||||
self::$implementationFactory = $implementationFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes DocumentNode against given $schema.
|
||||
*
|
||||
* Always returns ExecutionResult and never throws. All errors which occur during operation
|
||||
* execution are collected in `$result->errors`.
|
||||
*
|
||||
* @param mixed|null $rootValue
|
||||
* @param mixed|null $contextValue
|
||||
* @param mixed[]|ArrayAccess|null $variableValues
|
||||
* @param string|null $operationName
|
||||
*
|
||||
* @return ExecutionResult|Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function execute(
|
||||
Schema $schema,
|
||||
DocumentNode $documentNode,
|
||||
$rootValue = null,
|
||||
$contextValue = null,
|
||||
$variableValues = null,
|
||||
$operationName = null,
|
||||
?callable $fieldResolver = null
|
||||
) {
|
||||
// TODO: deprecate (just always use SyncAdapter here) and have `promiseToExecute()` for other cases
|
||||
|
||||
$promiseAdapter = static::getPromiseAdapter();
|
||||
|
||||
$result = static::promiseToExecute(
|
||||
$promiseAdapter,
|
||||
$schema,
|
||||
$documentNode,
|
||||
$rootValue,
|
||||
$contextValue,
|
||||
$variableValues,
|
||||
$operationName,
|
||||
$fieldResolver
|
||||
);
|
||||
|
||||
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
||||
$result = $promiseAdapter->wait($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as execute(), but requires promise adapter and returns a promise which is always
|
||||
* fulfilled with an instance of ExecutionResult and never rejected.
|
||||
*
|
||||
* Useful for async PHP platforms.
|
||||
*
|
||||
* @param mixed|null $rootValue
|
||||
* @param mixed|null $contextValue
|
||||
* @param mixed[]|null $variableValues
|
||||
* @param string|null $operationName
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function promiseToExecute(
|
||||
PromiseAdapter $promiseAdapter,
|
||||
Schema $schema,
|
||||
DocumentNode $documentNode,
|
||||
$rootValue = null,
|
||||
$contextValue = null,
|
||||
$variableValues = null,
|
||||
$operationName = null,
|
||||
?callable $fieldResolver = null
|
||||
) {
|
||||
$factory = self::$implementationFactory;
|
||||
|
||||
/** @var ExecutorImplementation $executor */
|
||||
$executor = $factory(
|
||||
$promiseAdapter,
|
||||
$schema,
|
||||
$documentNode,
|
||||
$rootValue,
|
||||
$contextValue,
|
||||
$variableValues,
|
||||
$operationName,
|
||||
$fieldResolver ?: self::$defaultFieldResolver
|
||||
);
|
||||
|
||||
return $executor->doExecute();
|
||||
}
|
||||
|
||||
/**
|
||||
* If a resolve function is not given, then a default resolve behavior is used
|
||||
* which takes the property of the source object of the same name as the field
|
||||
* and returns it as the result, or if it's a function, returns the result
|
||||
* of calling that function while passing along args and context.
|
||||
*
|
||||
* @param mixed $source
|
||||
* @param mixed[] $args
|
||||
* @param mixed|null $context
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
public static function defaultFieldResolver($source, $args, $context, ResolveInfo $info)
|
||||
{
|
||||
$fieldName = $info->fieldName;
|
||||
$property = null;
|
||||
|
||||
if (is_array($source) || $source instanceof ArrayAccess) {
|
||||
if (isset($source[$fieldName])) {
|
||||
$property = $source[$fieldName];
|
||||
}
|
||||
} elseif (is_object($source)) {
|
||||
if (isset($source->{$fieldName})) {
|
||||
$property = $source->{$fieldName};
|
||||
}
|
||||
}
|
||||
|
||||
return $property instanceof Closure ? $property($source, $args, $context, $info) : $property;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Executor;
|
||||
|
||||
use GraphQL\Executor\Promise\Promise;
|
||||
|
||||
interface ExecutorImplementation
|
||||
{
|
||||
/**
|
||||
* Returns promise of {@link ExecutionResult}. Promise should always resolve, never reject.
|
||||
*/
|
||||
public function doExecute() : Promise;
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Executor\Promise\Adapter;
|
||||
|
||||
use GraphQL\Executor\Promise\Promise;
|
||||
use GraphQL\Executor\Promise\PromiseAdapter;
|
||||
use GraphQL\Utils\Utils;
|
||||
use React\Promise\Promise as ReactPromise;
|
||||
use React\Promise\PromiseInterface as ReactPromiseInterface;
|
||||
use function React\Promise\all;
|
||||
use function React\Promise\reject;
|
||||
use function React\Promise\resolve;
|
||||
|
||||
class ReactPromiseAdapter implements PromiseAdapter
|
||||
{
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function isThenable($value)
|
||||
{
|
||||
return $value instanceof ReactPromiseInterface;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function convertThenable($thenable)
|
||||
{
|
||||
return new Promise($thenable, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null)
|
||||
{
|
||||
/** @var ReactPromiseInterface $adoptedPromise */
|
||||
$adoptedPromise = $promise->adoptedPromise;
|
||||
|
||||
return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function create(callable $resolver)
|
||||
{
|
||||
$promise = new ReactPromise($resolver);
|
||||
|
||||
return new Promise($promise, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function createFulfilled($value = null)
|
||||
{
|
||||
$promise = resolve($value);
|
||||
|
||||
return new Promise($promise, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function createRejected($reason)
|
||||
{
|
||||
$promise = reject($reason);
|
||||
|
||||
return new Promise($promise, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function all(array $promisesOrValues)
|
||||
{
|
||||
// TODO: rework with generators when PHP minimum required version is changed to 5.5+
|
||||
$promisesOrValues = Utils::map(
|
||||
$promisesOrValues,
|
||||
static function ($item) {
|
||||
return $item instanceof Promise ? $item->adoptedPromise : $item;
|
||||
}
|
||||
);
|
||||
|
||||
$promise = all($promisesOrValues)->then(static function ($values) use ($promisesOrValues) {
|
||||
$orderedResults = [];
|
||||
|
||||
foreach ($promisesOrValues as $key => $value) {
|
||||
$orderedResults[$key] = $values[$key];
|
||||
}
|
||||
|
||||
return $orderedResults;
|
||||
});
|
||||
|
||||
return new Promise($promise, $this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Executor\Promise\Adapter;
|
||||
|
||||
use Exception;
|
||||
use GraphQL\Executor\ExecutionResult;
|
||||
use GraphQL\Utils\Utils;
|
||||
use SplQueue;
|
||||
use Throwable;
|
||||
use function is_object;
|
||||
use function method_exists;
|
||||
|
||||
/**
|
||||
* Simplistic (yet full-featured) implementation of Promises A+ spec for regular PHP `sync` mode
|
||||
* (using queue to defer promises execution)
|
||||
*/
|
||||
class SyncPromise
|
||||
{
|
||||
const PENDING = 'pending';
|
||||
const FULFILLED = 'fulfilled';
|
||||
const REJECTED = 'rejected';
|
||||
|
||||
/** @var SplQueue */
|
||||
public static $queue;
|
||||
|
||||
/** @var string */
|
||||
public $state = self::PENDING;
|
||||
|
||||
/** @var ExecutionResult|Throwable */
|
||||
public $result;
|
||||
|
||||
/**
|
||||
* Promises created in `then` method of this promise and awaiting for resolution of this promise
|
||||
*
|
||||
* @var mixed[][]
|
||||
*/
|
||||
private $waiting = [];
|
||||
|
||||
public static function runQueue()
|
||||
{
|
||||
$q = self::$queue;
|
||||
while ($q && ! $q->isEmpty()) {
|
||||
$task = $q->dequeue();
|
||||
$task();
|
||||
}
|
||||
}
|
||||
|
||||
public function resolve($value)
|
||||
{
|
||||
switch ($this->state) {
|
||||
case self::PENDING:
|
||||
if ($value === $this) {
|
||||
throw new Exception('Cannot resolve promise with self');
|
||||
}
|
||||
if (is_object($value) && method_exists($value, 'then')) {
|
||||
$value->then(
|
||||
function ($resolvedValue) {
|
||||
$this->resolve($resolvedValue);
|
||||
},
|
||||
function ($reason) {
|
||||
$this->reject($reason);
|
||||
}
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->state = self::FULFILLED;
|
||||
$this->result = $value;
|
||||
$this->enqueueWaitingPromises();
|
||||
break;
|
||||
case self::FULFILLED:
|
||||
if ($this->result !== $value) {
|
||||
throw new Exception('Cannot change value of fulfilled promise');
|
||||
}
|
||||
break;
|
||||
case self::REJECTED:
|
||||
throw new Exception('Cannot resolve rejected promise');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function reject($reason)
|
||||
{
|
||||
if (! $reason instanceof Exception && ! $reason instanceof Throwable) {
|
||||
throw new Exception('SyncPromise::reject() has to be called with an instance of \Throwable');
|
||||
}
|
||||
|
||||
switch ($this->state) {
|
||||
case self::PENDING:
|
||||
$this->state = self::REJECTED;
|
||||
$this->result = $reason;
|
||||
$this->enqueueWaitingPromises();
|
||||
break;
|
||||
case self::REJECTED:
|
||||
if ($reason !== $this->result) {
|
||||
throw new Exception('Cannot change rejection reason');
|
||||
}
|
||||
break;
|
||||
case self::FULFILLED:
|
||||
throw new Exception('Cannot reject fulfilled promise');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function enqueueWaitingPromises()
|
||||
{
|
||||
Utils::invariant(
|
||||
$this->state !== self::PENDING,
|
||||
'Cannot enqueue derived promises when parent is still pending'
|
||||
);
|
||||
|
||||
foreach ($this->waiting as $descriptor) {
|
||||
self::getQueue()->enqueue(function () use ($descriptor) {
|
||||
/** @var $promise self */
|
||||
[$promise, $onFulfilled, $onRejected] = $descriptor;
|
||||
|
||||
if ($this->state === self::FULFILLED) {
|
||||
try {
|
||||
$promise->resolve($onFulfilled === null ? $this->result : $onFulfilled($this->result));
|
||||
} catch (Exception $e) {
|
||||
$promise->reject($e);
|
||||
} catch (Throwable $e) {
|
||||
$promise->reject($e);
|
||||
}
|
||||
} elseif ($this->state === self::REJECTED) {
|
||||
try {
|
||||
if ($onRejected === null) {
|
||||
$promise->reject($this->result);
|
||||
} else {
|
||||
$promise->resolve($onRejected($this->result));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$promise->reject($e);
|
||||
} catch (Throwable $e) {
|
||||
$promise->reject($e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
$this->waiting = [];
|
||||
}
|
||||
|
||||
public static function getQueue()
|
||||
{
|
||||
return self::$queue ?: self::$queue = new SplQueue();
|
||||
}
|
||||
|
||||
public function then(?callable $onFulfilled = null, ?callable $onRejected = null)
|
||||
{
|
||||
if ($this->state === self::REJECTED && ! $onRejected) {
|
||||
return $this;
|
||||
}
|
||||
if ($this->state === self::FULFILLED && ! $onFulfilled) {
|
||||
return $this;
|
||||
}
|
||||
$tmp = new self();
|
||||
$this->waiting[] = [$tmp, $onFulfilled, $onRejected];
|
||||
|
||||
if ($this->state !== self::PENDING) {
|
||||
$this->enqueueWaitingPromises();
|
||||
}
|
||||
|
||||
return $tmp;
|
||||
}
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Executor\Promise\Adapter;
|
||||
|
||||
use Exception;
|
||||
use GraphQL\Deferred;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Executor\ExecutionResult;
|
||||
use GraphQL\Executor\Promise\Promise;
|
||||
use GraphQL\Executor\Promise\PromiseAdapter;
|
||||
use GraphQL\Utils\Utils;
|
||||
use Throwable;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* Allows changing order of field resolution even in sync environments
|
||||
* (by leveraging queue of deferreds and promises)
|
||||
*/
|
||||
class SyncPromiseAdapter implements PromiseAdapter
|
||||
{
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function isThenable($value)
|
||||
{
|
||||
return $value instanceof Deferred;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function convertThenable($thenable)
|
||||
{
|
||||
if (! $thenable instanceof Deferred) {
|
||||
throw new InvariantViolation('Expected instance of GraphQL\Deferred, got ' . Utils::printSafe($thenable));
|
||||
}
|
||||
|
||||
return new Promise($thenable->promise, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null)
|
||||
{
|
||||
/** @var SyncPromise $adoptedPromise */
|
||||
$adoptedPromise = $promise->adoptedPromise;
|
||||
|
||||
return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function create(callable $resolver)
|
||||
{
|
||||
$promise = new SyncPromise();
|
||||
|
||||
try {
|
||||
$resolver(
|
||||
[
|
||||
$promise,
|
||||
'resolve',
|
||||
],
|
||||
[
|
||||
$promise,
|
||||
'reject',
|
||||
]
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
$promise->reject($e);
|
||||
} catch (Throwable $e) {
|
||||
$promise->reject($e);
|
||||
}
|
||||
|
||||
return new Promise($promise, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function createFulfilled($value = null)
|
||||
{
|
||||
$promise = new SyncPromise();
|
||||
|
||||
return new Promise($promise->resolve($value), $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function createRejected($reason)
|
||||
{
|
||||
$promise = new SyncPromise();
|
||||
|
||||
return new Promise($promise->reject($reason), $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function all(array $promisesOrValues)
|
||||
{
|
||||
$all = new SyncPromise();
|
||||
|
||||
$total = count($promisesOrValues);
|
||||
$count = 0;
|
||||
$result = [];
|
||||
|
||||
foreach ($promisesOrValues as $index => $promiseOrValue) {
|
||||
if ($promiseOrValue instanceof Promise) {
|
||||
$result[$index] = null;
|
||||
$promiseOrValue->then(
|
||||
static function ($value) use ($index, &$count, $total, &$result, $all) {
|
||||
$result[$index] = $value;
|
||||
$count++;
|
||||
if ($count < $total) {
|
||||
return;
|
||||
}
|
||||
|
||||
$all->resolve($result);
|
||||
},
|
||||
[$all, 'reject']
|
||||
);
|
||||
} else {
|
||||
$result[$index] = $promiseOrValue;
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
if ($count === $total) {
|
||||
$all->resolve($result);
|
||||
}
|
||||
|
||||
return new Promise($all, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously wait when promise completes
|
||||
*
|
||||
* @return ExecutionResult
|
||||
*/
|
||||
public function wait(Promise $promise)
|
||||
{
|
||||
$this->beforeWait($promise);
|
||||
$dfdQueue = Deferred::getQueue();
|
||||
$promiseQueue = SyncPromise::getQueue();
|
||||
|
||||
while ($promise->adoptedPromise->state === SyncPromise::PENDING &&
|
||||
! ($dfdQueue->isEmpty() && $promiseQueue->isEmpty())
|
||||
) {
|
||||
Deferred::runQueue();
|
||||
SyncPromise::runQueue();
|
||||
$this->onWait($promise);
|
||||
}
|
||||
|
||||
/** @var SyncPromise $syncPromise */
|
||||
$syncPromise = $promise->adoptedPromise;
|
||||
|
||||
if ($syncPromise->state === SyncPromise::FULFILLED) {
|
||||
return $syncPromise->result;
|
||||
}
|
||||
|
||||
if ($syncPromise->state === SyncPromise::REJECTED) {
|
||||
throw $syncPromise->result;
|
||||
}
|
||||
|
||||
throw new InvariantViolation('Could not resolve promise');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute just before starting to run promise completion
|
||||
*/
|
||||
protected function beforeWait(Promise $promise)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute while running promise completion
|
||||
*/
|
||||
protected function onWait(Promise $promise)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Executor\Promise;
|
||||
|
||||
use GraphQL\Executor\Promise\Adapter\SyncPromise;
|
||||
use GraphQL\Utils\Utils;
|
||||
use React\Promise\Promise as ReactPromise;
|
||||
|
||||
/**
|
||||
* Convenience wrapper for promises represented by Promise Adapter
|
||||
*/
|
||||
class Promise
|
||||
{
|
||||
/** @var SyncPromise|ReactPromise */
|
||||
public $adoptedPromise;
|
||||
|
||||
/** @var PromiseAdapter */
|
||||
private $adapter;
|
||||
|
||||
/**
|
||||
* @param mixed $adoptedPromise
|
||||
*/
|
||||
public function __construct($adoptedPromise, PromiseAdapter $adapter)
|
||||
{
|
||||
Utils::invariant(! $adoptedPromise instanceof self, 'Expecting promise from adapted system, got ' . self::class);
|
||||
|
||||
$this->adapter = $adapter;
|
||||
$this->adoptedPromise = $adoptedPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Promise
|
||||
*/
|
||||
public function then(?callable $onFulfilled = null, ?callable $onRejected = null)
|
||||
{
|
||||
return $this->adapter->then($this, $onFulfilled, $onRejected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Executor\Promise;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Provides a means for integration of async PHP platforms ([related docs](data-fetching.md#async-php))
|
||||
*/
|
||||
interface PromiseAdapter
|
||||
{
|
||||
/**
|
||||
* Return true if the value is a promise or a deferred of the underlying platform
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function isThenable($value);
|
||||
|
||||
/**
|
||||
* Converts thenable of the underlying platform into GraphQL\Executor\Promise\Promise instance
|
||||
*
|
||||
* @param object $thenable
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function convertThenable($thenable);
|
||||
|
||||
/**
|
||||
* Accepts our Promise wrapper, extracts adopted promise out of it and executes actual `then` logic described
|
||||
* in Promises/A+ specs. Then returns new wrapped instance of GraphQL\Executor\Promise\Promise.
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null);
|
||||
|
||||
/**
|
||||
* Creates a Promise
|
||||
*
|
||||
* Expected resolver signature:
|
||||
* function(callable $resolve, callable $reject)
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function create(callable $resolver);
|
||||
|
||||
/**
|
||||
* Creates a fulfilled Promise for a value if the value is not a promise.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function createFulfilled($value = null);
|
||||
|
||||
/**
|
||||
* Creates a rejected promise for a reason if the reason is not a promise. If
|
||||
* the provided reason is a promise, then it is returned as-is.
|
||||
*
|
||||
* @param Throwable $reason
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function createRejected($reason);
|
||||
|
||||
/**
|
||||
* Given an array of promises (or values), returns a promise that is fulfilled when all the
|
||||
* items in the array are fulfilled.
|
||||
*
|
||||
* @param Promise[]|mixed[] $promisesOrValues Promises or values.
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function all(array $promisesOrValues);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+278
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Executor;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\ArgumentNode;
|
||||
use GraphQL\Language\AST\DirectiveNode;
|
||||
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\InlineFragmentNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeList;
|
||||
use GraphQL\Language\AST\ValueNode;
|
||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||
use GraphQL\Language\AST\VariableNode;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Utils\AST;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Utils\Value;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
use function array_key_exists;
|
||||
use function array_map;
|
||||
use function sprintf;
|
||||
|
||||
class Values
|
||||
{
|
||||
/**
|
||||
* Prepares an object map of variables of the correct type based on the provided
|
||||
* variable definitions and arbitrary input. If the input cannot be coerced
|
||||
* to match the variable definitions, a Error will be thrown.
|
||||
*
|
||||
* @param VariableDefinitionNode[] $varDefNodes
|
||||
* @param mixed[] $inputs
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function getVariableValues(Schema $schema, $varDefNodes, array $inputs)
|
||||
{
|
||||
$errors = [];
|
||||
$coercedValues = [];
|
||||
foreach ($varDefNodes as $varDefNode) {
|
||||
$varName = $varDefNode->variable->name->value;
|
||||
/** @var InputType|Type $varType */
|
||||
$varType = TypeInfo::typeFromAST($schema, $varDefNode->type);
|
||||
|
||||
if (Type::isInputType($varType)) {
|
||||
if (array_key_exists($varName, $inputs)) {
|
||||
$value = $inputs[$varName];
|
||||
$coerced = Value::coerceValue($value, $varType, $varDefNode);
|
||||
/** @var Error[] $coercionErrors */
|
||||
$coercionErrors = $coerced['errors'];
|
||||
if (empty($coercionErrors)) {
|
||||
$coercedValues[$varName] = $coerced['value'];
|
||||
} else {
|
||||
$messagePrelude = sprintf(
|
||||
'Variable "$%s" got invalid value %s; ',
|
||||
$varName,
|
||||
Utils::printSafeJson($value)
|
||||
);
|
||||
|
||||
foreach ($coercionErrors as $error) {
|
||||
$errors[] = new Error(
|
||||
$messagePrelude . $error->getMessage(),
|
||||
$error->getNodes(),
|
||||
$error->getSource(),
|
||||
$error->getPositions(),
|
||||
$error->getPath(),
|
||||
$error->getPrevious(),
|
||||
$error->getExtensions()
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($varType instanceof NonNull) {
|
||||
$errors[] = new Error(
|
||||
sprintf(
|
||||
'Variable "$%s" of required type "%s" was not provided.',
|
||||
$varName,
|
||||
$varType
|
||||
),
|
||||
[$varDefNode]
|
||||
);
|
||||
} elseif ($varDefNode->defaultValue) {
|
||||
$coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$errors[] = new Error(
|
||||
sprintf(
|
||||
'Variable "$%s" expected value of type "%s" which cannot be used as an input type.',
|
||||
$varName,
|
||||
Printer::doPrint($varDefNode->type)
|
||||
),
|
||||
[$varDefNode->type]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($errors)) {
|
||||
return [$errors, null];
|
||||
}
|
||||
|
||||
return [null, $coercedValues];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares an object map of argument values given a directive definition
|
||||
* and a AST node which may contain directives. Optionally also accepts a map
|
||||
* of variable values.
|
||||
*
|
||||
* If the directive does not exist on the node, returns undefined.
|
||||
*
|
||||
* @param FragmentSpreadNode|FieldNode|InlineFragmentNode|EnumValueDefinitionNode|FieldDefinitionNode $node
|
||||
* @param mixed[]|null $variableValues
|
||||
*
|
||||
* @return mixed[]|null
|
||||
*/
|
||||
public static function getDirectiveValues(Directive $directiveDef, $node, $variableValues = null)
|
||||
{
|
||||
if (isset($node->directives) && $node->directives instanceof NodeList) {
|
||||
$directiveNode = Utils::find(
|
||||
$node->directives,
|
||||
static function (DirectiveNode $directive) use ($directiveDef) {
|
||||
return $directive->name->value === $directiveDef->name;
|
||||
}
|
||||
);
|
||||
|
||||
if ($directiveNode !== null) {
|
||||
return self::getArgumentValues($directiveDef, $directiveNode, $variableValues);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares an object map of argument values given a list of argument
|
||||
* definitions and list of argument AST nodes.
|
||||
*
|
||||
* @param FieldDefinition|Directive $def
|
||||
* @param FieldNode|DirectiveNode $node
|
||||
* @param mixed[] $variableValues
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public static function getArgumentValues($def, $node, $variableValues = null)
|
||||
{
|
||||
if (empty($def->args)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$argumentNodes = $node->arguments;
|
||||
if (empty($argumentNodes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$argumentValueMap = [];
|
||||
foreach ($argumentNodes as $argumentNode) {
|
||||
$argumentValueMap[$argumentNode->name->value] = $argumentNode->value;
|
||||
}
|
||||
|
||||
return static::getArgumentValuesForMap($def, $argumentValueMap, $variableValues, $node);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FieldDefinition|Directive $fieldDefinition
|
||||
* @param ArgumentNode[] $argumentValueMap
|
||||
* @param mixed[] $variableValues
|
||||
* @param Node|null $referenceNode
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public static function getArgumentValuesForMap($fieldDefinition, $argumentValueMap, $variableValues = null, $referenceNode = null)
|
||||
{
|
||||
$argumentDefinitions = $fieldDefinition->args;
|
||||
$coercedValues = [];
|
||||
|
||||
foreach ($argumentDefinitions as $argumentDefinition) {
|
||||
$name = $argumentDefinition->name;
|
||||
$argType = $argumentDefinition->getType();
|
||||
$argumentValueNode = $argumentValueMap[$name] ?? null;
|
||||
|
||||
if (! $argumentValueNode) {
|
||||
if ($argumentDefinition->defaultValueExists()) {
|
||||
$coercedValues[$name] = $argumentDefinition->defaultValue;
|
||||
} elseif ($argType instanceof NonNull) {
|
||||
throw new Error(
|
||||
'Argument "' . $name . '" of required type ' .
|
||||
'"' . Utils::printSafe($argType) . '" was not provided.',
|
||||
$referenceNode
|
||||
);
|
||||
}
|
||||
} elseif ($argumentValueNode instanceof VariableNode) {
|
||||
$variableName = $argumentValueNode->name->value;
|
||||
|
||||
if ($variableValues && array_key_exists($variableName, $variableValues)) {
|
||||
// Note: this does not check that this variable value is correct.
|
||||
// This assumes that this query has been validated and the variable
|
||||
// usage here is of the correct type.
|
||||
$coercedValues[$name] = $variableValues[$variableName];
|
||||
} elseif ($argumentDefinition->defaultValueExists()) {
|
||||
$coercedValues[$name] = $argumentDefinition->defaultValue;
|
||||
} elseif ($argType instanceof NonNull) {
|
||||
throw new Error(
|
||||
'Argument "' . $name . '" of required type "' . Utils::printSafe($argType) . '" was ' .
|
||||
'provided the variable "$' . $variableName . '" which was not provided ' .
|
||||
'a runtime value.',
|
||||
[$argumentValueNode]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$valueNode = $argumentValueNode;
|
||||
$coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues);
|
||||
if (Utils::isInvalid($coercedValue)) {
|
||||
// Note: ValuesOfCorrectType validation should catch this before
|
||||
// execution. This is a runtime check to ensure execution does not
|
||||
// continue with an invalid argument value.
|
||||
throw new Error(
|
||||
'Argument "' . $name . '" has invalid value ' . Printer::doPrint($valueNode) . '.',
|
||||
[$argumentValueNode]
|
||||
);
|
||||
}
|
||||
$coercedValues[$name] = $coercedValue;
|
||||
}
|
||||
}
|
||||
|
||||
return $coercedValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated as of 8.0 (Moved to \GraphQL\Utils\AST::valueFromAST)
|
||||
*
|
||||
* @param ValueNode $valueNode
|
||||
* @param mixed[]|null $variables
|
||||
*
|
||||
* @return mixed[]|stdClass|null
|
||||
*/
|
||||
public static function valueFromAST($valueNode, InputType $type, ?array $variables = null)
|
||||
{
|
||||
return AST::valueFromAST($valueNode, $type, $variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated as of 0.12 (Use coerceValue() directly for richer information)
|
||||
*
|
||||
* @param mixed[] $value
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function isValidPHPValue($value, InputType $type)
|
||||
{
|
||||
$errors = Value::coerceValue($value, $type)['errors'];
|
||||
|
||||
return $errors
|
||||
? array_map(
|
||||
static function (Throwable $error) {
|
||||
return $error->getMessage();
|
||||
},
|
||||
$errors
|
||||
) : [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Experimental\Executor;
|
||||
|
||||
use Generator;
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\DefinitionNode;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\InlineFragmentNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Language\AST\ValueNode;
|
||||
use GraphQL\Type\Definition\AbstractType;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Introspection;
|
||||
use GraphQL\Type\Schema;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Collector
|
||||
{
|
||||
/** @var Schema */
|
||||
private $schema;
|
||||
|
||||
/** @var Runtime */
|
||||
private $runtime;
|
||||
|
||||
/** @var OperationDefinitionNode|null */
|
||||
public $operation = null;
|
||||
|
||||
/** @var FragmentDefinitionNode[] */
|
||||
public $fragments = [];
|
||||
|
||||
/** @var ObjectType|null */
|
||||
public $rootType;
|
||||
|
||||
/** @var FieldNode[][] */
|
||||
private $fields;
|
||||
|
||||
/** @var string[] */
|
||||
private $visitedFragments;
|
||||
|
||||
public function __construct(Schema $schema, Runtime $runtime)
|
||||
{
|
||||
$this->schema = $schema;
|
||||
$this->runtime = $runtime;
|
||||
}
|
||||
|
||||
public function initialize(DocumentNode $documentNode, ?string $operationName = null)
|
||||
{
|
||||
$hasMultipleAssumedOperations = false;
|
||||
|
||||
foreach ($documentNode->definitions as $definitionNode) {
|
||||
/** @var DefinitionNode|Node $definitionNode */
|
||||
|
||||
if ($definitionNode->kind === NodeKind::OPERATION_DEFINITION) {
|
||||
/** @var OperationDefinitionNode $definitionNode */
|
||||
if ($operationName === null && $this->operation !== null) {
|
||||
$hasMultipleAssumedOperations = true;
|
||||
}
|
||||
if ($operationName === null ||
|
||||
(isset($definitionNode->name) && $definitionNode->name->value === $operationName)
|
||||
) {
|
||||
$this->operation = $definitionNode;
|
||||
}
|
||||
} elseif ($definitionNode->kind === NodeKind::FRAGMENT_DEFINITION) {
|
||||
/** @var FragmentDefinitionNode $definitionNode */
|
||||
$this->fragments[$definitionNode->name->value] = $definitionNode;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->operation === null) {
|
||||
if ($operationName !== null) {
|
||||
$this->runtime->addError(new Error(sprintf('Unknown operation named "%s".', $operationName)));
|
||||
} else {
|
||||
$this->runtime->addError(new Error('Must provide an operation.'));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($hasMultipleAssumedOperations) {
|
||||
$this->runtime->addError(new Error('Must provide operation name if query contains multiple operations.'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->operation->operation === 'query') {
|
||||
$this->rootType = $this->schema->getQueryType();
|
||||
} elseif ($this->operation->operation === 'mutation') {
|
||||
$this->rootType = $this->schema->getMutationType();
|
||||
} elseif ($this->operation->operation === 'subscription') {
|
||||
$this->rootType = $this->schema->getSubscriptionType();
|
||||
} else {
|
||||
$this->runtime->addError(new Error(sprintf('Cannot initialize collector with operation type "%s".', $this->operation->operation)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Generator
|
||||
*/
|
||||
public function collectFields(ObjectType $runtimeType, ?SelectionSetNode $selectionSet)
|
||||
{
|
||||
$this->fields = [];
|
||||
$this->visitedFragments = [];
|
||||
|
||||
$this->doCollectFields($runtimeType, $selectionSet);
|
||||
|
||||
foreach ($this->fields as $resultName => $fieldNodes) {
|
||||
$fieldNode = $fieldNodes[0];
|
||||
$fieldName = $fieldNode->name->value;
|
||||
|
||||
$argumentValueMap = null;
|
||||
if (! empty($fieldNode->arguments)) {
|
||||
foreach ($fieldNode->arguments as $argumentNode) {
|
||||
$argumentValueMap = $argumentValueMap ?? [];
|
||||
$argumentValueMap[$argumentNode->name->value] = $argumentNode->value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($fieldName !== Introspection::TYPE_NAME_FIELD_NAME &&
|
||||
! ($runtimeType === $this->schema->getQueryType() && ($fieldName === Introspection::SCHEMA_FIELD_NAME || $fieldName === Introspection::TYPE_FIELD_NAME)) &&
|
||||
! $runtimeType->hasField($fieldName)
|
||||
) {
|
||||
// do not emit error
|
||||
continue;
|
||||
}
|
||||
|
||||
yield new CoroutineContextShared($fieldNodes, $fieldName, $resultName, $argumentValueMap);
|
||||
}
|
||||
}
|
||||
|
||||
private function doCollectFields(ObjectType $runtimeType, ?SelectionSetNode $selectionSet)
|
||||
{
|
||||
if ($selectionSet === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($selectionSet->selections as $selection) {
|
||||
/** @var FieldNode|FragmentSpreadNode|InlineFragmentNode $selection */
|
||||
|
||||
if (! empty($selection->directives)) {
|
||||
foreach ($selection->directives as $directiveNode) {
|
||||
if ($directiveNode->name->value === Directive::SKIP_NAME) {
|
||||
/** @var ValueNode|null $condition */
|
||||
$condition = null;
|
||||
foreach ($directiveNode->arguments as $argumentNode) {
|
||||
if ($argumentNode->name->value === Directive::IF_ARGUMENT_NAME) {
|
||||
$condition = $argumentNode->value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($condition === null) {
|
||||
$this->runtime->addError(new Error(
|
||||
sprintf('@%s directive is missing "%s" argument.', Directive::SKIP_NAME, Directive::IF_ARGUMENT_NAME),
|
||||
$selection
|
||||
));
|
||||
} else {
|
||||
if ($this->runtime->evaluate($condition, Type::boolean()) === true) {
|
||||
continue 2; // !!! advances outer loop
|
||||
}
|
||||
}
|
||||
} elseif ($directiveNode->name->value === Directive::INCLUDE_NAME) {
|
||||
/** @var ValueNode|null $condition */
|
||||
$condition = null;
|
||||
foreach ($directiveNode->arguments as $argumentNode) {
|
||||
if ($argumentNode->name->value === Directive::IF_ARGUMENT_NAME) {
|
||||
$condition = $argumentNode->value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($condition === null) {
|
||||
$this->runtime->addError(new Error(
|
||||
sprintf('@%s directive is missing "%s" argument.', Directive::INCLUDE_NAME, Directive::IF_ARGUMENT_NAME),
|
||||
$selection
|
||||
));
|
||||
} else {
|
||||
if ($this->runtime->evaluate($condition, Type::boolean()) !== true) {
|
||||
continue 2; // !!! advances outer loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($selection->kind === NodeKind::FIELD) {
|
||||
/** @var FieldNode $selection */
|
||||
|
||||
$resultName = $selection->alias ? $selection->alias->value : $selection->name->value;
|
||||
|
||||
if (! isset($this->fields[$resultName])) {
|
||||
$this->fields[$resultName] = [];
|
||||
}
|
||||
|
||||
$this->fields[$resultName][] = $selection;
|
||||
} elseif ($selection->kind === NodeKind::FRAGMENT_SPREAD) {
|
||||
/** @var FragmentSpreadNode $selection */
|
||||
|
||||
$fragmentName = $selection->name->value;
|
||||
|
||||
if (isset($this->visitedFragments[$fragmentName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! isset($this->fragments[$fragmentName])) {
|
||||
$this->runtime->addError(new Error(
|
||||
sprintf('Fragment "%s" does not exist.', $fragmentName),
|
||||
$selection
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->visitedFragments[$fragmentName] = true;
|
||||
|
||||
$fragmentDefinition = $this->fragments[$fragmentName];
|
||||
$conditionTypeName = $fragmentDefinition->typeCondition->name->value;
|
||||
|
||||
if (! $this->schema->hasType($conditionTypeName)) {
|
||||
$this->runtime->addError(new Error(
|
||||
sprintf('Cannot spread fragment "%s", type "%s" does not exist.', $fragmentName, $conditionTypeName),
|
||||
$selection
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
$conditionType = $this->schema->getType($conditionTypeName);
|
||||
|
||||
if ($conditionType instanceof ObjectType) {
|
||||
if ($runtimeType->name !== $conditionType->name) {
|
||||
continue;
|
||||
}
|
||||
} elseif ($conditionType instanceof AbstractType) {
|
||||
if (! $this->schema->isPossibleType($conditionType, $runtimeType)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$this->doCollectFields($runtimeType, $fragmentDefinition->selectionSet);
|
||||
} elseif ($selection->kind === NodeKind::INLINE_FRAGMENT) {
|
||||
/** @var InlineFragmentNode $selection */
|
||||
|
||||
if ($selection->typeCondition !== null) {
|
||||
$conditionTypeName = $selection->typeCondition->name->value;
|
||||
|
||||
if (! $this->schema->hasType($conditionTypeName)) {
|
||||
$this->runtime->addError(new Error(
|
||||
sprintf('Cannot spread inline fragment, type "%s" does not exist.', $conditionTypeName),
|
||||
$selection
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
$conditionType = $this->schema->getType($conditionTypeName);
|
||||
|
||||
if ($conditionType instanceof ObjectType) {
|
||||
if ($runtimeType->name !== $conditionType->name) {
|
||||
continue;
|
||||
}
|
||||
} elseif ($conditionType instanceof AbstractType) {
|
||||
if (! $this->schema->isPossibleType($conditionType, $runtimeType)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->doCollectFields($runtimeType, $selection->selectionSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Experimental\Executor;
|
||||
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CoroutineContext
|
||||
{
|
||||
/** @var CoroutineContextShared */
|
||||
public $shared;
|
||||
|
||||
/** @var ObjectType */
|
||||
public $type;
|
||||
|
||||
/** @var mixed */
|
||||
public $value;
|
||||
|
||||
/** @var object */
|
||||
public $result;
|
||||
|
||||
/** @var string[] */
|
||||
public $path;
|
||||
|
||||
/** @var ResolveInfo|null */
|
||||
public $resolveInfo;
|
||||
|
||||
/** @var string[]|null */
|
||||
public $nullFence;
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param object $result
|
||||
* @param string[] $path
|
||||
* @param string[]|null $nullFence
|
||||
*/
|
||||
public function __construct(
|
||||
CoroutineContextShared $shared,
|
||||
ObjectType $type,
|
||||
$value,
|
||||
$result,
|
||||
array $path,
|
||||
?array $nullFence = null
|
||||
) {
|
||||
$this->shared = $shared;
|
||||
$this->type = $type;
|
||||
$this->value = $value;
|
||||
$this->result = $result;
|
||||
$this->path = $path;
|
||||
$this->nullFence = $nullFence;
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Experimental\Executor;
|
||||
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Language\AST\ValueNode;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CoroutineContextShared
|
||||
{
|
||||
/** @var FieldNode[] */
|
||||
public $fieldNodes;
|
||||
|
||||
/** @var string */
|
||||
public $fieldName;
|
||||
|
||||
/** @var string */
|
||||
public $resultName;
|
||||
|
||||
/** @var ValueNode[]|null */
|
||||
public $argumentValueMap;
|
||||
|
||||
/** @var SelectionSetNode|null */
|
||||
public $mergedSelectionSet;
|
||||
|
||||
/** @var ObjectType|null */
|
||||
public $typeGuard1;
|
||||
|
||||
/** @var callable|null */
|
||||
public $resolveIfType1;
|
||||
|
||||
/** @var mixed */
|
||||
public $argumentsIfType1;
|
||||
|
||||
/** @var ResolveInfo|null */
|
||||
public $resolveInfoIfType1;
|
||||
|
||||
/** @var ObjectType|null */
|
||||
public $typeGuard2;
|
||||
|
||||
/** @var CoroutineContext[]|null */
|
||||
public $childContextsIfType2;
|
||||
|
||||
/**
|
||||
* @param FieldNode[] $fieldNodes
|
||||
* @param mixed[]|null $argumentValueMap
|
||||
*/
|
||||
public function __construct(array $fieldNodes, string $fieldName, string $resultName, ?array $argumentValueMap)
|
||||
{
|
||||
$this->fieldNodes = $fieldNodes;
|
||||
$this->fieldName = $fieldName;
|
||||
$this->resultName = $resultName;
|
||||
$this->argumentValueMap = $argumentValueMap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,948 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Experimental\Executor;
|
||||
|
||||
use Generator;
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Error\Warning;
|
||||
use GraphQL\Executor\ExecutionResult;
|
||||
use GraphQL\Executor\ExecutorImplementation;
|
||||
use GraphQL\Executor\Promise\Promise;
|
||||
use GraphQL\Executor\Promise\PromiseAdapter;
|
||||
use GraphQL\Executor\Values;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Language\AST\ValueNode;
|
||||
use GraphQL\Type\Definition\AbstractType;
|
||||
use GraphQL\Type\Definition\CompositeType;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\LeafType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Introspection;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Utils\AST;
|
||||
use GraphQL\Utils\Utils;
|
||||
use SplQueue;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use function sprintf;
|
||||
|
||||
class CoroutineExecutor implements Runtime, ExecutorImplementation
|
||||
{
|
||||
/** @var object */
|
||||
private static $undefined;
|
||||
|
||||
/** @var Schema */
|
||||
private $schema;
|
||||
|
||||
/** @var callable */
|
||||
private $fieldResolver;
|
||||
|
||||
/** @var PromiseAdapter */
|
||||
private $promiseAdapter;
|
||||
|
||||
/** @var mixed|null */
|
||||
private $rootValue;
|
||||
|
||||
/** @var mixed|null */
|
||||
private $contextValue;
|
||||
|
||||
/** @var mixed|null */
|
||||
private $rawVariableValues;
|
||||
|
||||
/** @var mixed|null */
|
||||
private $variableValues;
|
||||
|
||||
/** @var DocumentNode */
|
||||
private $documentNode;
|
||||
|
||||
/** @var string|null */
|
||||
private $operationName;
|
||||
|
||||
/** @var Collector */
|
||||
private $collector;
|
||||
|
||||
/** @var Error[] */
|
||||
private $errors;
|
||||
|
||||
/** @var SplQueue */
|
||||
private $queue;
|
||||
|
||||
/** @var SplQueue */
|
||||
private $schedule;
|
||||
|
||||
/** @var stdClass */
|
||||
private $rootResult;
|
||||
|
||||
/** @var int */
|
||||
private $pending;
|
||||
|
||||
/** @var callable */
|
||||
private $doResolve;
|
||||
|
||||
public function __construct(
|
||||
PromiseAdapter $promiseAdapter,
|
||||
Schema $schema,
|
||||
DocumentNode $documentNode,
|
||||
$rootValue,
|
||||
$contextValue,
|
||||
$rawVariableValues,
|
||||
?string $operationName,
|
||||
callable $fieldResolver
|
||||
) {
|
||||
if (self::$undefined === null) {
|
||||
self::$undefined = Utils::undefined();
|
||||
}
|
||||
|
||||
$this->schema = $schema;
|
||||
$this->fieldResolver = $fieldResolver;
|
||||
$this->promiseAdapter = $promiseAdapter;
|
||||
$this->rootValue = $rootValue;
|
||||
$this->contextValue = $contextValue;
|
||||
$this->rawVariableValues = $rawVariableValues;
|
||||
$this->documentNode = $documentNode;
|
||||
$this->operationName = $operationName;
|
||||
}
|
||||
|
||||
public static function create(
|
||||
PromiseAdapter $promiseAdapter,
|
||||
Schema $schema,
|
||||
DocumentNode $documentNode,
|
||||
$rootValue,
|
||||
$contextValue,
|
||||
$variableValues,
|
||||
?string $operationName,
|
||||
callable $fieldResolver
|
||||
) {
|
||||
return new static(
|
||||
$promiseAdapter,
|
||||
$schema,
|
||||
$documentNode,
|
||||
$rootValue,
|
||||
$contextValue,
|
||||
$variableValues,
|
||||
$operationName,
|
||||
$fieldResolver
|
||||
);
|
||||
}
|
||||
|
||||
private static function resultToArray($value, $emptyObjectAsStdClass = true)
|
||||
{
|
||||
if ($value instanceof stdClass) {
|
||||
$array = [];
|
||||
foreach ($value as $propertyName => $propertyValue) {
|
||||
$array[$propertyName] = self::resultToArray($propertyValue);
|
||||
}
|
||||
if ($emptyObjectAsStdClass && empty($array)) {
|
||||
return new stdClass();
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$array = [];
|
||||
foreach ($value as $key => $item) {
|
||||
$array[$key] = self::resultToArray($item);
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function doExecute() : Promise
|
||||
{
|
||||
$this->rootResult = new stdClass();
|
||||
$this->errors = [];
|
||||
$this->queue = new SplQueue();
|
||||
$this->schedule = new SplQueue();
|
||||
$this->pending = 0;
|
||||
|
||||
$this->collector = new Collector($this->schema, $this);
|
||||
$this->collector->initialize($this->documentNode, $this->operationName);
|
||||
|
||||
if (! empty($this->errors)) {
|
||||
return $this->promiseAdapter->createFulfilled($this->finishExecute(null, $this->errors));
|
||||
}
|
||||
|
||||
[$errors, $coercedVariableValues] = Values::getVariableValues(
|
||||
$this->schema,
|
||||
$this->collector->operation->variableDefinitions ?: [],
|
||||
$this->rawVariableValues ?: []
|
||||
);
|
||||
|
||||
if (! empty($errors)) {
|
||||
return $this->promiseAdapter->createFulfilled($this->finishExecute(null, $errors));
|
||||
}
|
||||
|
||||
$this->variableValues = $coercedVariableValues;
|
||||
|
||||
foreach ($this->collector->collectFields($this->collector->rootType, $this->collector->operation->selectionSet) as $shared) {
|
||||
/** @var CoroutineContextShared $shared */
|
||||
|
||||
// !!! assign to keep object keys sorted
|
||||
$this->rootResult->{$shared->resultName} = null;
|
||||
|
||||
$ctx = new CoroutineContext(
|
||||
$shared,
|
||||
$this->collector->rootType,
|
||||
$this->rootValue,
|
||||
$this->rootResult,
|
||||
[$shared->resultName]
|
||||
);
|
||||
|
||||
$fieldDefinition = $this->findFieldDefinition($ctx);
|
||||
if (! $fieldDefinition->getType() instanceof NonNull) {
|
||||
$ctx->nullFence = [$shared->resultName];
|
||||
}
|
||||
|
||||
if ($this->collector->operation->operation === 'mutation' && ! $this->queue->isEmpty()) {
|
||||
$this->schedule->enqueue($ctx);
|
||||
} else {
|
||||
$this->queue->enqueue(new Strand($this->spawn($ctx)));
|
||||
}
|
||||
}
|
||||
|
||||
$this->run();
|
||||
|
||||
if ($this->pending > 0) {
|
||||
return $this->promiseAdapter->create(function (callable $resolve) {
|
||||
$this->doResolve = $resolve;
|
||||
});
|
||||
}
|
||||
|
||||
return $this->promiseAdapter->createFulfilled($this->finishExecute($this->rootResult, $this->errors));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object|null $value
|
||||
* @param Error[] $errors
|
||||
*/
|
||||
private function finishExecute($value, array $errors) : ExecutionResult
|
||||
{
|
||||
$this->rootResult = null;
|
||||
$this->errors = null;
|
||||
$this->queue = null;
|
||||
$this->schedule = null;
|
||||
$this->pending = null;
|
||||
$this->collector = null;
|
||||
$this->variableValues = null;
|
||||
|
||||
if ($value !== null) {
|
||||
$value = self::resultToArray($value, false);
|
||||
}
|
||||
|
||||
return new ExecutionResult($value, $errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function evaluate(ValueNode $valueNode, InputType $type)
|
||||
{
|
||||
return AST::valueFromAST($valueNode, $type, $this->variableValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function addError($error)
|
||||
{
|
||||
$this->errors[] = $error;
|
||||
}
|
||||
|
||||
private function run()
|
||||
{
|
||||
RUN:
|
||||
while (! $this->queue->isEmpty()) {
|
||||
/** @var Strand $strand */
|
||||
$strand = $this->queue->dequeue();
|
||||
|
||||
try {
|
||||
if ($strand->success !== null) {
|
||||
RESUME:
|
||||
|
||||
if ($strand->success) {
|
||||
$strand->current->send($strand->value);
|
||||
} else {
|
||||
$strand->current->throw($strand->value);
|
||||
}
|
||||
|
||||
$strand->success = null;
|
||||
$strand->value = null;
|
||||
}
|
||||
|
||||
START:
|
||||
if ($strand->current->valid()) {
|
||||
$value = $strand->current->current();
|
||||
|
||||
if ($value instanceof Generator) {
|
||||
$strand->stack[$strand->depth++] = $strand->current;
|
||||
$strand->current = $value;
|
||||
goto START;
|
||||
} elseif ($this->isPromise($value)) {
|
||||
// !!! increment pending before calling ->then() as it may invoke the callback right away
|
||||
++$this->pending;
|
||||
|
||||
if (! $value instanceof Promise) {
|
||||
$value = $this->promiseAdapter->convertThenable($value);
|
||||
}
|
||||
|
||||
$this->promiseAdapter
|
||||
->then(
|
||||
$value,
|
||||
function ($value) use ($strand) {
|
||||
$strand->success = true;
|
||||
$strand->value = $value;
|
||||
$this->queue->enqueue($strand);
|
||||
$this->done();
|
||||
},
|
||||
function (Throwable $throwable) use ($strand) {
|
||||
$strand->success = false;
|
||||
$strand->value = $throwable;
|
||||
$this->queue->enqueue($strand);
|
||||
$this->done();
|
||||
}
|
||||
);
|
||||
continue;
|
||||
} else {
|
||||
$strand->success = true;
|
||||
$strand->value = $value;
|
||||
goto RESUME;
|
||||
}
|
||||
}
|
||||
|
||||
$strand->success = true;
|
||||
$strand->value = $strand->current->getReturn();
|
||||
} catch (Throwable $reason) {
|
||||
$strand->success = false;
|
||||
$strand->value = $reason;
|
||||
}
|
||||
|
||||
if ($strand->depth <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$current = &$strand->stack[--$strand->depth];
|
||||
$strand->current = $current;
|
||||
$current = null;
|
||||
goto RESUME;
|
||||
}
|
||||
|
||||
if ($this->pending > 0 || $this->schedule->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var CoroutineContext $ctx */
|
||||
$ctx = $this->schedule->dequeue();
|
||||
$this->queue->enqueue(new Strand($this->spawn($ctx)));
|
||||
goto RUN;
|
||||
}
|
||||
|
||||
private function done()
|
||||
{
|
||||
--$this->pending;
|
||||
|
||||
$this->run();
|
||||
|
||||
if ($this->pending > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$doResolve = $this->doResolve;
|
||||
$doResolve($this->finishExecute($this->rootResult, $this->errors));
|
||||
}
|
||||
|
||||
private function spawn(CoroutineContext $ctx)
|
||||
{
|
||||
// short-circuit evaluation for __typename
|
||||
if ($ctx->shared->fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
|
||||
$ctx->result->{$ctx->shared->resultName} = $ctx->type->name;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($ctx->shared->typeGuard1 === $ctx->type) {
|
||||
$resolve = $ctx->shared->resolveIfType1;
|
||||
$ctx->resolveInfo = clone $ctx->shared->resolveInfoIfType1;
|
||||
$ctx->resolveInfo->path = $ctx->path;
|
||||
$arguments = $ctx->shared->argumentsIfType1;
|
||||
$returnType = $ctx->resolveInfo->returnType;
|
||||
} else {
|
||||
$fieldDefinition = $this->findFieldDefinition($ctx);
|
||||
|
||||
if ($fieldDefinition->resolveFn !== null) {
|
||||
$resolve = $fieldDefinition->resolveFn;
|
||||
} elseif ($ctx->type->resolveFieldFn !== null) {
|
||||
$resolve = $ctx->type->resolveFieldFn;
|
||||
} else {
|
||||
$resolve = $this->fieldResolver;
|
||||
}
|
||||
|
||||
$returnType = $fieldDefinition->getType();
|
||||
|
||||
$ctx->resolveInfo = new ResolveInfo(
|
||||
$ctx->shared->fieldName,
|
||||
$ctx->shared->fieldNodes,
|
||||
$returnType,
|
||||
$ctx->type,
|
||||
$ctx->path,
|
||||
$this->schema,
|
||||
$this->collector->fragments,
|
||||
$this->rootValue,
|
||||
$this->collector->operation,
|
||||
$this->variableValues
|
||||
);
|
||||
|
||||
$arguments = Values::getArgumentValuesForMap(
|
||||
$fieldDefinition,
|
||||
$ctx->shared->argumentValueMap,
|
||||
$this->variableValues
|
||||
);
|
||||
|
||||
// !!! assign only in batch when no exception can be thrown in-between
|
||||
$ctx->shared->typeGuard1 = $ctx->type;
|
||||
$ctx->shared->resolveIfType1 = $resolve;
|
||||
$ctx->shared->argumentsIfType1 = $arguments;
|
||||
$ctx->shared->resolveInfoIfType1 = $ctx->resolveInfo;
|
||||
}
|
||||
|
||||
$value = $resolve($ctx->value, $arguments, $this->contextValue, $ctx->resolveInfo);
|
||||
|
||||
if (! $this->completeValueFast($ctx, $returnType, $value, $ctx->path, $returnValue)) {
|
||||
$returnValue = yield $this->completeValue(
|
||||
$ctx,
|
||||
$returnType,
|
||||
$value,
|
||||
$ctx->path,
|
||||
$ctx->nullFence
|
||||
);
|
||||
}
|
||||
} catch (Throwable $reason) {
|
||||
$this->addError(Error::createLocatedError(
|
||||
$reason,
|
||||
$ctx->shared->fieldNodes,
|
||||
$ctx->path
|
||||
));
|
||||
|
||||
$returnValue = self::$undefined;
|
||||
}
|
||||
|
||||
if ($returnValue !== self::$undefined) {
|
||||
$ctx->result->{$ctx->shared->resultName} = $returnValue;
|
||||
} elseif ($ctx->resolveInfo !== null && $ctx->resolveInfo->returnType instanceof NonNull) { // !!! $ctx->resolveInfo might not have been initialized yet
|
||||
$result =& $this->rootResult;
|
||||
foreach ($ctx->nullFence ?? [] as $key) {
|
||||
if (is_string($key)) {
|
||||
$result =& $result->{$key};
|
||||
} else {
|
||||
$result =& $result[$key];
|
||||
}
|
||||
}
|
||||
$result = null;
|
||||
}
|
||||
}
|
||||
|
||||
private function findFieldDefinition(CoroutineContext $ctx)
|
||||
{
|
||||
if ($ctx->shared->fieldName === Introspection::SCHEMA_FIELD_NAME && $ctx->type === $this->schema->getQueryType()) {
|
||||
return Introspection::schemaMetaFieldDef();
|
||||
}
|
||||
|
||||
if ($ctx->shared->fieldName === Introspection::TYPE_FIELD_NAME && $ctx->type === $this->schema->getQueryType()) {
|
||||
return Introspection::typeMetaFieldDef();
|
||||
}
|
||||
|
||||
if ($ctx->shared->fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
|
||||
return Introspection::typeNameMetaFieldDef();
|
||||
}
|
||||
|
||||
return $ctx->type->getField($ctx->shared->fieldName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param string[] $path
|
||||
* @param mixed $returnValue
|
||||
*/
|
||||
private function completeValueFast(CoroutineContext $ctx, Type $type, $value, array $path, &$returnValue) : bool
|
||||
{
|
||||
// special handling of Throwable inherited from JS reference implementation, but makes no sense in this PHP
|
||||
if ($this->isPromise($value) || $value instanceof Throwable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$nonNull = false;
|
||||
if ($type instanceof NonNull) {
|
||||
$nonNull = true;
|
||||
$type = $type->getWrappedType();
|
||||
}
|
||||
|
||||
if (! $type instanceof LeafType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type !== $this->schema->getType($type->name)) {
|
||||
$hint = '';
|
||||
if ($this->schema->getConfig()->typeLoader) {
|
||||
$hint = sprintf(
|
||||
'Make sure that type loader returns the same instance as defined in %s.%s',
|
||||
$ctx->type,
|
||||
$ctx->shared->fieldName
|
||||
);
|
||||
}
|
||||
$this->addError(Error::createLocatedError(
|
||||
new InvariantViolation(
|
||||
sprintf(
|
||||
'Schema must contain unique named types but contains multiple types named "%s". %s ' .
|
||||
'(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
|
||||
$type->name,
|
||||
$hint
|
||||
)
|
||||
),
|
||||
$ctx->shared->fieldNodes,
|
||||
$path
|
||||
));
|
||||
|
||||
$value = null;
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
$returnValue = null;
|
||||
} else {
|
||||
try {
|
||||
$returnValue = $type->serialize($value);
|
||||
} catch (Throwable $error) {
|
||||
$this->addError(Error::createLocatedError(
|
||||
new InvariantViolation(
|
||||
'Expected a value of type "' . Utils::printSafe($type) . '" but received: ' . Utils::printSafe($value),
|
||||
0,
|
||||
$error
|
||||
),
|
||||
$ctx->shared->fieldNodes,
|
||||
$path
|
||||
));
|
||||
$returnValue = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($nonNull && $returnValue === null) {
|
||||
$this->addError(Error::createLocatedError(
|
||||
new InvariantViolation(sprintf(
|
||||
'Cannot return null for non-nullable field %s.%s.',
|
||||
$ctx->type->name,
|
||||
$ctx->shared->fieldName
|
||||
)),
|
||||
$ctx->shared->fieldNodes,
|
||||
$path
|
||||
));
|
||||
|
||||
$returnValue = self::$undefined;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param string[] $path
|
||||
* @param string[]|null $nullFence
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function completeValue(CoroutineContext $ctx, Type $type, $value, array $path, ?array $nullFence)
|
||||
{
|
||||
$nonNull = false;
|
||||
$returnValue = null;
|
||||
|
||||
if ($type instanceof NonNull) {
|
||||
$nonNull = true;
|
||||
$type = $type->getWrappedType();
|
||||
} else {
|
||||
$nullFence = $path;
|
||||
}
|
||||
|
||||
// !!! $value might be promise, yield to resolve
|
||||
try {
|
||||
if ($this->isPromise($value)) {
|
||||
$value = yield $value;
|
||||
}
|
||||
} catch (Throwable $reason) {
|
||||
$this->addError(Error::createLocatedError(
|
||||
$reason,
|
||||
$ctx->shared->fieldNodes,
|
||||
$path
|
||||
));
|
||||
if ($nonNull) {
|
||||
$returnValue = self::$undefined;
|
||||
} else {
|
||||
$returnValue = null;
|
||||
}
|
||||
goto CHECKED_RETURN;
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
$returnValue = $value;
|
||||
goto CHECKED_RETURN;
|
||||
} elseif ($value instanceof Throwable) {
|
||||
// special handling of Throwable inherited from JS reference implementation, but makes no sense in this PHP
|
||||
$this->addError(Error::createLocatedError(
|
||||
$value,
|
||||
$ctx->shared->fieldNodes,
|
||||
$path
|
||||
));
|
||||
if ($nonNull) {
|
||||
$returnValue = self::$undefined;
|
||||
} else {
|
||||
$returnValue = null;
|
||||
}
|
||||
goto CHECKED_RETURN;
|
||||
}
|
||||
|
||||
if ($type instanceof ListOfType) {
|
||||
$returnValue = [];
|
||||
$index = -1;
|
||||
$itemType = $type->getWrappedType();
|
||||
foreach ($value as $itemValue) {
|
||||
++$index;
|
||||
|
||||
$itemPath = $path;
|
||||
$itemPath[] = $index; // !!! use arrays COW semantics
|
||||
|
||||
try {
|
||||
if (! $this->completeValueFast($ctx, $itemType, $itemValue, $itemPath, $itemReturnValue)) {
|
||||
$itemReturnValue = yield $this->completeValue($ctx, $itemType, $itemValue, $itemPath, $nullFence);
|
||||
}
|
||||
} catch (Throwable $reason) {
|
||||
$this->addError(Error::createLocatedError(
|
||||
$reason,
|
||||
$ctx->shared->fieldNodes,
|
||||
$itemPath
|
||||
));
|
||||
$itemReturnValue = null;
|
||||
}
|
||||
if ($itemReturnValue === self::$undefined) {
|
||||
$returnValue = self::$undefined;
|
||||
goto CHECKED_RETURN;
|
||||
}
|
||||
$returnValue[$index] = $itemReturnValue;
|
||||
}
|
||||
|
||||
goto CHECKED_RETURN;
|
||||
} else {
|
||||
if ($type !== $this->schema->getType($type->name)) {
|
||||
$hint = '';
|
||||
if ($this->schema->getConfig()->typeLoader) {
|
||||
$hint = sprintf(
|
||||
'Make sure that type loader returns the same instance as defined in %s.%s',
|
||||
$ctx->type,
|
||||
$ctx->shared->fieldName
|
||||
);
|
||||
}
|
||||
$this->addError(Error::createLocatedError(
|
||||
new InvariantViolation(
|
||||
sprintf(
|
||||
'Schema must contain unique named types but contains multiple types named "%s". %s ' .
|
||||
'(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
|
||||
$type->name,
|
||||
$hint
|
||||
)
|
||||
),
|
||||
$ctx->shared->fieldNodes,
|
||||
$path
|
||||
));
|
||||
|
||||
$returnValue = null;
|
||||
goto CHECKED_RETURN;
|
||||
}
|
||||
|
||||
if ($type instanceof LeafType) {
|
||||
try {
|
||||
$returnValue = $type->serialize($value);
|
||||
} catch (Throwable $error) {
|
||||
$this->addError(Error::createLocatedError(
|
||||
new InvariantViolation(
|
||||
'Expected a value of type "' . Utils::printSafe($type) . '" but received: ' . Utils::printSafe($value),
|
||||
0,
|
||||
$error
|
||||
),
|
||||
$ctx->shared->fieldNodes,
|
||||
$path
|
||||
));
|
||||
$returnValue = null;
|
||||
}
|
||||
goto CHECKED_RETURN;
|
||||
} elseif ($type instanceof CompositeType) {
|
||||
/** @var ObjectType|null $objectType */
|
||||
$objectType = null;
|
||||
if ($type instanceof InterfaceType || $type instanceof UnionType) {
|
||||
$objectType = $type->resolveType($value, $this->contextValue, $ctx->resolveInfo);
|
||||
|
||||
if ($objectType === null) {
|
||||
$objectType = yield $this->resolveTypeSlow($ctx, $value, $type);
|
||||
}
|
||||
|
||||
// !!! $objectType->resolveType() might return promise, yield to resolve
|
||||
$objectType = yield $objectType;
|
||||
if (is_string($objectType)) {
|
||||
$objectType = $this->schema->getType($objectType);
|
||||
}
|
||||
|
||||
if ($objectType === null) {
|
||||
$this->addError(Error::createLocatedError(
|
||||
sprintf(
|
||||
'Composite type "%s" did not resolve concrete object type for value: %s.',
|
||||
$type->name,
|
||||
Utils::printSafe($value)
|
||||
),
|
||||
$ctx->shared->fieldNodes,
|
||||
$path
|
||||
));
|
||||
|
||||
$returnValue = self::$undefined;
|
||||
goto CHECKED_RETURN;
|
||||
} elseif (! $objectType instanceof ObjectType) {
|
||||
$this->addError(Error::createLocatedError(
|
||||
new InvariantViolation(sprintf(
|
||||
'Abstract type %s must resolve to an Object type at ' .
|
||||
'runtime for field %s.%s with value "%s", received "%s". ' .
|
||||
'Either the %s type should provide a "resolveType" ' .
|
||||
'function or each possible type should provide an "isTypeOf" function.',
|
||||
$type,
|
||||
$ctx->resolveInfo->parentType,
|
||||
$ctx->resolveInfo->fieldName,
|
||||
Utils::printSafe($value),
|
||||
Utils::printSafe($objectType),
|
||||
$type
|
||||
)),
|
||||
$ctx->shared->fieldNodes,
|
||||
$path
|
||||
));
|
||||
|
||||
$returnValue = null;
|
||||
goto CHECKED_RETURN;
|
||||
} elseif (! $this->schema->isPossibleType($type, $objectType)) {
|
||||
$this->addError(Error::createLocatedError(
|
||||
new InvariantViolation(sprintf(
|
||||
'Runtime Object type "%s" is not a possible type for "%s".',
|
||||
$objectType,
|
||||
$type
|
||||
)),
|
||||
$ctx->shared->fieldNodes,
|
||||
$path
|
||||
));
|
||||
|
||||
$returnValue = null;
|
||||
goto CHECKED_RETURN;
|
||||
} elseif ($objectType !== $this->schema->getType($objectType->name)) {
|
||||
$this->addError(Error::createLocatedError(
|
||||
new InvariantViolation(
|
||||
sprintf(
|
||||
'Schema must contain unique named types but contains multiple types named "%s". ' .
|
||||
'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
|
||||
'type instance as referenced anywhere else within the schema ' .
|
||||
'(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
|
||||
$objectType,
|
||||
$type
|
||||
)
|
||||
),
|
||||
$ctx->shared->fieldNodes,
|
||||
$path
|
||||
));
|
||||
|
||||
$returnValue = null;
|
||||
goto CHECKED_RETURN;
|
||||
}
|
||||
} elseif ($type instanceof ObjectType) {
|
||||
$objectType = $type;
|
||||
} else {
|
||||
$this->addError(Error::createLocatedError(
|
||||
sprintf(
|
||||
'Unexpected field type "%s".',
|
||||
Utils::printSafe($type)
|
||||
),
|
||||
$ctx->shared->fieldNodes,
|
||||
$path
|
||||
));
|
||||
|
||||
$returnValue = self::$undefined;
|
||||
goto CHECKED_RETURN;
|
||||
}
|
||||
|
||||
$typeCheck = $objectType->isTypeOf($value, $this->contextValue, $ctx->resolveInfo);
|
||||
if ($typeCheck !== null) {
|
||||
// !!! $objectType->isTypeOf() might return promise, yield to resolve
|
||||
$typeCheck = yield $typeCheck;
|
||||
if (! $typeCheck) {
|
||||
$this->addError(Error::createLocatedError(
|
||||
sprintf('Expected value of type "%s" but got: %s.', $type->name, Utils::printSafe($value)),
|
||||
$ctx->shared->fieldNodes,
|
||||
$path
|
||||
));
|
||||
|
||||
$returnValue = null;
|
||||
goto CHECKED_RETURN;
|
||||
}
|
||||
}
|
||||
|
||||
$returnValue = new stdClass();
|
||||
|
||||
if ($ctx->shared->typeGuard2 === $objectType) {
|
||||
foreach ($ctx->shared->childContextsIfType2 as $childCtx) {
|
||||
$childCtx = clone $childCtx;
|
||||
$childCtx->type = $objectType;
|
||||
$childCtx->value = $value;
|
||||
$childCtx->result = $returnValue;
|
||||
$childCtx->path = $path;
|
||||
$childCtx->path[] = $childCtx->shared->resultName; // !!! uses array COW semantics
|
||||
$childCtx->nullFence = $nullFence;
|
||||
$childCtx->resolveInfo = null;
|
||||
|
||||
$this->queue->enqueue(new Strand($this->spawn($childCtx)));
|
||||
|
||||
// !!! assign null to keep object keys sorted
|
||||
$returnValue->{$childCtx->shared->resultName} = null;
|
||||
}
|
||||
} else {
|
||||
$childContexts = [];
|
||||
|
||||
foreach ($this->collector->collectFields($objectType, $ctx->shared->mergedSelectionSet ?? $this->mergeSelectionSets($ctx)) as $childShared) {
|
||||
/** @var CoroutineContextShared $childShared */
|
||||
|
||||
$childPath = $path;
|
||||
$childPath[] = $childShared->resultName; // !!! uses array COW semantics
|
||||
$childCtx = new CoroutineContext(
|
||||
$childShared,
|
||||
$objectType,
|
||||
$value,
|
||||
$returnValue,
|
||||
$childPath,
|
||||
$nullFence
|
||||
);
|
||||
|
||||
$childContexts[] = $childCtx;
|
||||
|
||||
$this->queue->enqueue(new Strand($this->spawn($childCtx)));
|
||||
|
||||
// !!! assign null to keep object keys sorted
|
||||
$returnValue->{$childShared->resultName} = null;
|
||||
}
|
||||
|
||||
$ctx->shared->typeGuard2 = $objectType;
|
||||
$ctx->shared->childContextsIfType2 = $childContexts;
|
||||
}
|
||||
|
||||
goto CHECKED_RETURN;
|
||||
} else {
|
||||
$this->addError(Error::createLocatedError(
|
||||
sprintf('Unhandled type "%s".', Utils::printSafe($type)),
|
||||
$ctx->shared->fieldNodes,
|
||||
$path
|
||||
));
|
||||
|
||||
$returnValue = null;
|
||||
goto CHECKED_RETURN;
|
||||
}
|
||||
}
|
||||
|
||||
CHECKED_RETURN:
|
||||
if ($nonNull && $returnValue === null) {
|
||||
$this->addError(Error::createLocatedError(
|
||||
new InvariantViolation(sprintf(
|
||||
'Cannot return null for non-nullable field %s.%s.',
|
||||
$ctx->type->name,
|
||||
$ctx->shared->fieldName
|
||||
)),
|
||||
$ctx->shared->fieldNodes,
|
||||
$path
|
||||
));
|
||||
|
||||
return self::$undefined;
|
||||
}
|
||||
|
||||
return $returnValue;
|
||||
}
|
||||
|
||||
private function mergeSelectionSets(CoroutineContext $ctx)
|
||||
{
|
||||
$selections = [];
|
||||
|
||||
foreach ($ctx->shared->fieldNodes as $fieldNode) {
|
||||
if ($fieldNode->selectionSet === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($fieldNode->selectionSet->selections as $selection) {
|
||||
$selections[] = $selection;
|
||||
}
|
||||
}
|
||||
|
||||
return $ctx->shared->mergedSelectionSet = new SelectionSetNode(['selections' => $selections]);
|
||||
}
|
||||
|
||||
private function resolveTypeSlow(CoroutineContext $ctx, $value, AbstractType $abstractType)
|
||||
{
|
||||
if ($value !== null &&
|
||||
is_array($value) &&
|
||||
isset($value['__typename']) &&
|
||||
is_string($value['__typename'])
|
||||
) {
|
||||
return $this->schema->getType($value['__typename']);
|
||||
}
|
||||
|
||||
if ($abstractType instanceof InterfaceType && $this->schema->getConfig()->typeLoader) {
|
||||
Warning::warnOnce(
|
||||
sprintf(
|
||||
'GraphQL Interface Type `%s` returned `null` from its `resolveType` function ' .
|
||||
'for value: %s. Switching to slow resolution method using `isTypeOf` ' .
|
||||
'of all possible implementations. It requires full schema scan and degrades query performance significantly. ' .
|
||||
' Make sure your `resolveType` always returns valid implementation or throws.',
|
||||
$abstractType->name,
|
||||
Utils::printSafe($value)
|
||||
),
|
||||
Warning::WARNING_FULL_SCHEMA_SCAN
|
||||
);
|
||||
}
|
||||
|
||||
$possibleTypes = $this->schema->getPossibleTypes($abstractType);
|
||||
|
||||
// to be backward-compatible with old executor, ->isTypeOf() is called for all possible types,
|
||||
// it cannot short-circuit when the match is found
|
||||
|
||||
$selectedType = null;
|
||||
foreach ($possibleTypes as $type) {
|
||||
$typeCheck = yield $type->isTypeOf($value, $this->contextValue, $ctx->resolveInfo);
|
||||
if ($selectedType !== null || $typeCheck !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$selectedType = $type;
|
||||
}
|
||||
|
||||
return $selectedType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isPromise($value)
|
||||
{
|
||||
return $value instanceof Promise || $this->promiseAdapter->isThenable($value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Experimental\Executor;
|
||||
|
||||
use GraphQL\Language\AST\ValueNode;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
interface Runtime
|
||||
{
|
||||
public function evaluate(ValueNode $valueNode, InputType $type);
|
||||
|
||||
public function addError($error);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Experimental\Executor;
|
||||
|
||||
use Generator;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Strand
|
||||
{
|
||||
/** @var Generator */
|
||||
public $current;
|
||||
|
||||
/** @var Generator[] */
|
||||
public $stack;
|
||||
|
||||
/** @var int */
|
||||
public $depth;
|
||||
|
||||
/** @var bool|null */
|
||||
public $success;
|
||||
|
||||
/** @var mixed */
|
||||
public $value;
|
||||
|
||||
public function __construct(Generator $coroutine)
|
||||
{
|
||||
$this->current = $coroutine;
|
||||
$this->stack = [];
|
||||
$this->depth = 0;
|
||||
}
|
||||
}
|
||||
+351
@@ -0,0 +1,351 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
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\Executor\ReferenceExecutor;
|
||||
use GraphQL\Experimental\Executor\CoroutineExecutor;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Language\Source;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Schema as SchemaType;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
use GraphQL\Validator\Rules\QueryComplexity;
|
||||
use GraphQL\Validator\Rules\ValidationRule;
|
||||
use function array_values;
|
||||
use function trigger_error;
|
||||
use const E_USER_DEPRECATED;
|
||||
|
||||
/**
|
||||
* This is the primary facade for fulfilling GraphQL operations.
|
||||
* See [related documentation](executing-queries.md).
|
||||
*/
|
||||
class GraphQL
|
||||
{
|
||||
/**
|
||||
* Executes graphql query.
|
||||
*
|
||||
* More sophisticated GraphQL servers, such as those which persist queries,
|
||||
* may wish to separate the validation and execution phases to a static time
|
||||
* tooling step, and a server runtime step.
|
||||
*
|
||||
* Available options:
|
||||
*
|
||||
* schema:
|
||||
* The GraphQL type system to use when validating and executing a query.
|
||||
* source:
|
||||
* A GraphQL language formatted string representing the requested operation.
|
||||
* rootValue:
|
||||
* The value provided as the first argument to resolver functions on the top
|
||||
* level type (e.g. the query object type).
|
||||
* context:
|
||||
* The value provided as the third argument to all resolvers.
|
||||
* Use this to pass current session, user data, etc
|
||||
* variableValues:
|
||||
* A mapping of variable name to runtime value to use for all variables
|
||||
* defined in the requestString.
|
||||
* operationName:
|
||||
* The name of the operation to use if requestString contains multiple
|
||||
* possible operations. Can be omitted if requestString contains only
|
||||
* one operation.
|
||||
* fieldResolver:
|
||||
* A resolver function to use when one is not provided by the schema.
|
||||
* If not provided, the default field resolver is used (which looks for a
|
||||
* value on the source value with the field's name).
|
||||
* validationRules:
|
||||
* A set of rules for query validation step. Default value is all available rules.
|
||||
* Empty array would allow to skip query validation (may be convenient for persisted
|
||||
* queries which are validated before persisting and assumed valid during execution)
|
||||
*
|
||||
* @param string|DocumentNode $source
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $context
|
||||
* @param mixed[]|null $variableValues
|
||||
* @param ValidationRule[] $validationRules
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function executeQuery(
|
||||
SchemaType $schema,
|
||||
$source,
|
||||
$rootValue = null,
|
||||
$context = null,
|
||||
$variableValues = null,
|
||||
?string $operationName = null,
|
||||
?callable $fieldResolver = null,
|
||||
?array $validationRules = null
|
||||
) : ExecutionResult {
|
||||
$promiseAdapter = new SyncPromiseAdapter();
|
||||
|
||||
$promise = self::promiseToExecute(
|
||||
$promiseAdapter,
|
||||
$schema,
|
||||
$source,
|
||||
$rootValue,
|
||||
$context,
|
||||
$variableValues,
|
||||
$operationName,
|
||||
$fieldResolver,
|
||||
$validationRules
|
||||
);
|
||||
|
||||
return $promiseAdapter->wait($promise);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as executeQuery(), but requires PromiseAdapter and always returns a Promise.
|
||||
* Useful for Async PHP platforms.
|
||||
*
|
||||
* @param string|DocumentNode $source
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $context
|
||||
* @param mixed[]|null $variableValues
|
||||
* @param ValidationRule[]|null $validationRules
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function promiseToExecute(
|
||||
PromiseAdapter $promiseAdapter,
|
||||
SchemaType $schema,
|
||||
$source,
|
||||
$rootValue = null,
|
||||
$context = null,
|
||||
$variableValues = null,
|
||||
?string $operationName = null,
|
||||
?callable $fieldResolver = null,
|
||||
?array $validationRules = null
|
||||
) : Promise {
|
||||
try {
|
||||
if ($source instanceof DocumentNode) {
|
||||
$documentNode = $source;
|
||||
} else {
|
||||
$documentNode = Parser::parse(new Source($source ?: '', 'GraphQL'));
|
||||
}
|
||||
|
||||
// FIXME
|
||||
if (empty($validationRules)) {
|
||||
/** @var QueryComplexity $queryComplexity */
|
||||
$queryComplexity = DocumentValidator::getRule(QueryComplexity::class);
|
||||
$queryComplexity->setRawVariableValues($variableValues);
|
||||
} else {
|
||||
foreach ($validationRules as $rule) {
|
||||
if (! ($rule instanceof QueryComplexity)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rule->setRawVariableValues($variableValues);
|
||||
}
|
||||
}
|
||||
|
||||
$validationErrors = DocumentValidator::validate($schema, $documentNode, $validationRules);
|
||||
|
||||
if (! empty($validationErrors)) {
|
||||
return $promiseAdapter->createFulfilled(
|
||||
new ExecutionResult(null, $validationErrors)
|
||||
);
|
||||
}
|
||||
|
||||
return Executor::promiseToExecute(
|
||||
$promiseAdapter,
|
||||
$schema,
|
||||
$documentNode,
|
||||
$rootValue,
|
||||
$context,
|
||||
$variableValues,
|
||||
$operationName,
|
||||
$fieldResolver
|
||||
);
|
||||
} catch (Error $e) {
|
||||
return $promiseAdapter->createFulfilled(
|
||||
new ExecutionResult(null, [$e])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use executeQuery()->toArray() instead
|
||||
*
|
||||
* @param string|DocumentNode $source
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $contextValue
|
||||
* @param mixed[]|null $variableValues
|
||||
*
|
||||
* @return Promise|mixed[]
|
||||
*/
|
||||
public static function execute(
|
||||
SchemaType $schema,
|
||||
$source,
|
||||
$rootValue = null,
|
||||
$contextValue = null,
|
||||
$variableValues = null,
|
||||
?string $operationName = null
|
||||
) {
|
||||
trigger_error(
|
||||
__METHOD__ . ' is deprecated, use GraphQL::executeQuery()->toArray() as a quick replacement',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
|
||||
$promiseAdapter = Executor::getPromiseAdapter();
|
||||
$result = self::promiseToExecute(
|
||||
$promiseAdapter,
|
||||
$schema,
|
||||
$source,
|
||||
$rootValue,
|
||||
$contextValue,
|
||||
$variableValues,
|
||||
$operationName
|
||||
);
|
||||
|
||||
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
||||
$result = $promiseAdapter->wait($result)->toArray();
|
||||
} else {
|
||||
$result = $result->then(static function (ExecutionResult $r) {
|
||||
return $r->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated renamed to executeQuery()
|
||||
*
|
||||
* @param string|DocumentNode $source
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $contextValue
|
||||
* @param mixed[]|null $variableValues
|
||||
*
|
||||
* @return ExecutionResult|Promise
|
||||
*/
|
||||
public static function executeAndReturnResult(
|
||||
SchemaType $schema,
|
||||
$source,
|
||||
$rootValue = null,
|
||||
$contextValue = null,
|
||||
$variableValues = null,
|
||||
?string $operationName = null
|
||||
) {
|
||||
trigger_error(
|
||||
__METHOD__ . ' is deprecated, use GraphQL::executeQuery() as a quick replacement',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
|
||||
$promiseAdapter = Executor::getPromiseAdapter();
|
||||
$result = self::promiseToExecute(
|
||||
$promiseAdapter,
|
||||
$schema,
|
||||
$source,
|
||||
$rootValue,
|
||||
$contextValue,
|
||||
$variableValues,
|
||||
$operationName
|
||||
);
|
||||
|
||||
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
||||
$result = $promiseAdapter->wait($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns directives defined in GraphQL spec
|
||||
*
|
||||
* @return Directive[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function getStandardDirectives() : array
|
||||
{
|
||||
return array_values(Directive::getInternalDirectives());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns types defined in GraphQL spec
|
||||
*
|
||||
* @return Type[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function getStandardTypes() : array
|
||||
{
|
||||
return array_values(Type::getStandardTypes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces standard types with types from this list (matching by name)
|
||||
* Standard types not listed here remain untouched.
|
||||
*
|
||||
* @param Type[] $types
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function overrideStandardTypes(array $types)
|
||||
{
|
||||
Type::overrideStandardTypes($types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns standard validation rules implementing GraphQL spec
|
||||
*
|
||||
* @return ValidationRule[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function getStandardValidationRules() : array
|
||||
{
|
||||
return array_values(DocumentValidator::defaultRules());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default resolver implementation
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function setDefaultFieldResolver(callable $fn) : void
|
||||
{
|
||||
Executor::setDefaultFieldResolver($fn);
|
||||
}
|
||||
|
||||
public static function setPromiseAdapter(?PromiseAdapter $promiseAdapter = null) : void
|
||||
{
|
||||
Executor::setPromiseAdapter($promiseAdapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Experimental: Switch to the new executor
|
||||
*/
|
||||
public static function useExperimentalExecutor()
|
||||
{
|
||||
Executor::setImplementationFactory([CoroutineExecutor::class, 'create']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Experimental: Switch back to the default executor
|
||||
*/
|
||||
public static function useReferenceExecutor()
|
||||
{
|
||||
Executor::setImplementationFactory([ReferenceExecutor::class, 'create']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns directives defined in GraphQL spec
|
||||
*
|
||||
* @deprecated Renamed to getStandardDirectives
|
||||
*
|
||||
* @return Directive[]
|
||||
*/
|
||||
public static function getInternalDirectives() : array
|
||||
{
|
||||
return self::getStandardDirectives();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ArgumentNode extends Node
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::ARGUMENT;
|
||||
|
||||
/** @var ValueNode */
|
||||
public $value;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class BooleanValueNode extends Node implements ValueNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::BOOLEAN;
|
||||
|
||||
/** @var bool */
|
||||
public $value;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
/**
|
||||
* export type DefinitionNode =
|
||||
* | ExecutableDefinitionNode
|
||||
* | TypeSystemDefinitionNode; // experimental non-spec addition.
|
||||
*/
|
||||
interface DefinitionNode
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class DirectiveDefinitionNode extends Node implements TypeSystemDefinitionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::DIRECTIVE_DEFINITION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var ArgumentNode[] */
|
||||
public $arguments;
|
||||
|
||||
/** @var NameNode[] */
|
||||
public $locations;
|
||||
|
||||
/** @var StringValueNode|null */
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class DirectiveNode extends Node
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::DIRECTIVE;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var ArgumentNode[] */
|
||||
public $arguments;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class DocumentNode extends Node
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::DOCUMENT;
|
||||
|
||||
/** @var NodeList|DefinitionNode[] */
|
||||
public $definitions;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class EnumTypeDefinitionNode extends Node implements TypeDefinitionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::ENUM_TYPE_DEFINITION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var DirectiveNode[] */
|
||||
public $directives;
|
||||
|
||||
/** @var EnumValueDefinitionNode[]|NodeList|null */
|
||||
public $values;
|
||||
|
||||
/** @var StringValueNode|null */
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class EnumTypeExtensionNode extends Node implements TypeExtensionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::ENUM_TYPE_EXTENSION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
|
||||
/** @var EnumValueDefinitionNode[]|null */
|
||||
public $values;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class EnumValueDefinitionNode extends Node
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::ENUM_VALUE_DEFINITION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var DirectiveNode[] */
|
||||
public $directives;
|
||||
|
||||
/** @var StringValueNode|null */
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class EnumValueNode extends Node implements ValueNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::ENUM;
|
||||
|
||||
/** @var string */
|
||||
public $value;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
/**
|
||||
* export type ExecutableDefinitionNode =
|
||||
* | OperationDefinitionNode
|
||||
* | FragmentDefinitionNode;
|
||||
*/
|
||||
interface ExecutableDefinitionNode extends DefinitionNode
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class FieldDefinitionNode extends Node
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::FIELD_DEFINITION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var InputValueDefinitionNode[]|NodeList */
|
||||
public $arguments;
|
||||
|
||||
/** @var TypeNode */
|
||||
public $type;
|
||||
|
||||
/** @var DirectiveNode[]|NodeList */
|
||||
public $directives;
|
||||
|
||||
/** @var StringValueNode|null */
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class FieldNode extends Node implements SelectionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::FIELD;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var NameNode|null */
|
||||
public $alias;
|
||||
|
||||
/** @var ArgumentNode[]|null */
|
||||
public $arguments;
|
||||
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
|
||||
/** @var SelectionSetNode|null */
|
||||
public $selectionSet;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class FloatValueNode extends Node implements ValueNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::FLOAT;
|
||||
|
||||
/** @var string */
|
||||
public $value;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class FragmentDefinitionNode extends Node implements ExecutableDefinitionNode, HasSelectionSet
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::FRAGMENT_DEFINITION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* Note: fragment variable definitions are experimental and may be changed
|
||||
* or removed in the future.
|
||||
*
|
||||
* @var VariableDefinitionNode[]|NodeList
|
||||
*/
|
||||
public $variableDefinitions;
|
||||
|
||||
/** @var NamedTypeNode */
|
||||
public $typeCondition;
|
||||
|
||||
/** @var DirectiveNode[]|NodeList */
|
||||
public $directives;
|
||||
|
||||
/** @var SelectionSetNode */
|
||||
public $selectionSet;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class FragmentSpreadNode extends Node implements SelectionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::FRAGMENT_SPREAD;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var DirectiveNode[] */
|
||||
public $directives;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
interface HasSelectionSet
|
||||
{
|
||||
/**
|
||||
* export type DefinitionNode = OperationDefinitionNode
|
||||
* | FragmentDefinitionNode
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class InlineFragmentNode extends Node implements SelectionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::INLINE_FRAGMENT;
|
||||
|
||||
/** @var NamedTypeNode */
|
||||
public $typeCondition;
|
||||
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
|
||||
/** @var SelectionSetNode */
|
||||
public $selectionSet;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class InputObjectTypeDefinitionNode extends Node implements TypeDefinitionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::INPUT_OBJECT_TYPE_DEFINITION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
|
||||
/** @var InputValueDefinitionNode[]|null */
|
||||
public $fields;
|
||||
|
||||
/** @var StringValueNode|null */
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class InputObjectTypeExtensionNode extends Node implements TypeExtensionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::INPUT_OBJECT_TYPE_EXTENSION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
|
||||
/** @var InputValueDefinitionNode[]|null */
|
||||
public $fields;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class InputValueDefinitionNode extends Node
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::INPUT_VALUE_DEFINITION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var TypeNode */
|
||||
public $type;
|
||||
|
||||
/** @var ValueNode */
|
||||
public $defaultValue;
|
||||
|
||||
/** @var DirectiveNode[] */
|
||||
public $directives;
|
||||
|
||||
/** @var StringValueNode|null */
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class IntValueNode extends Node implements ValueNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::INT;
|
||||
|
||||
/** @var string */
|
||||
public $value;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class InterfaceTypeDefinitionNode extends Node implements TypeDefinitionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::INTERFACE_TYPE_DEFINITION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
|
||||
/** @var FieldDefinitionNode[]|null */
|
||||
public $fields;
|
||||
|
||||
/** @var StringValueNode|null */
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class InterfaceTypeExtensionNode extends Node implements TypeExtensionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::INTERFACE_TYPE_EXTENSION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
|
||||
/** @var FieldDefinitionNode[]|null */
|
||||
public $fields;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ListTypeNode extends Node implements TypeNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::LIST_TYPE;
|
||||
|
||||
/** @var Node */
|
||||
public $type;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ListValueNode extends Node implements ValueNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::LST;
|
||||
|
||||
/** @var ValueNode[]|NodeList */
|
||||
public $values;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
use GraphQL\Language\Source;
|
||||
use GraphQL\Language\Token;
|
||||
|
||||
/**
|
||||
* Contains a range of UTF-8 character offsets and token references that
|
||||
* identify the region of the source from which the AST derived.
|
||||
*/
|
||||
class Location
|
||||
{
|
||||
/**
|
||||
* The character offset at which this Node begins.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $start;
|
||||
|
||||
/**
|
||||
* The character offset at which this Node ends.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $end;
|
||||
|
||||
/**
|
||||
* The Token at which this Node begins.
|
||||
*
|
||||
* @var Token
|
||||
*/
|
||||
public $startToken;
|
||||
|
||||
/**
|
||||
* The Token at which this Node ends.
|
||||
*
|
||||
* @var Token
|
||||
*/
|
||||
public $endToken;
|
||||
|
||||
/**
|
||||
* The Source document the AST represents.
|
||||
*
|
||||
* @var Source|null
|
||||
*/
|
||||
public $source;
|
||||
|
||||
/**
|
||||
* @param int $start
|
||||
* @param int $end
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function create($start, $end)
|
||||
{
|
||||
$tmp = new static();
|
||||
$tmp->start = $start;
|
||||
$tmp->end = $end;
|
||||
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
public function __construct(?Token $startToken = null, ?Token $endToken = null, ?Source $source = null)
|
||||
{
|
||||
$this->startToken = $startToken;
|
||||
$this->endToken = $endToken;
|
||||
$this->source = $source;
|
||||
|
||||
if (! $startToken || ! $endToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->start = $startToken->start;
|
||||
$this->end = $endToken->end;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class NameNode extends Node implements TypeNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::NAME;
|
||||
|
||||
/** @var string */
|
||||
public $value;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class NamedTypeNode extends Node implements TypeNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::NAMED_TYPE;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
use GraphQL\Utils\Utils;
|
||||
use function get_object_vars;
|
||||
use function is_array;
|
||||
use function is_scalar;
|
||||
use function json_encode;
|
||||
|
||||
/**
|
||||
* type Node = NameNode
|
||||
* | DocumentNode
|
||||
* | OperationDefinitionNode
|
||||
* | VariableDefinitionNode
|
||||
* | VariableNode
|
||||
* | SelectionSetNode
|
||||
* | FieldNode
|
||||
* | ArgumentNode
|
||||
* | FragmentSpreadNode
|
||||
* | InlineFragmentNode
|
||||
* | FragmentDefinitionNode
|
||||
* | IntValueNode
|
||||
* | FloatValueNode
|
||||
* | StringValueNode
|
||||
* | BooleanValueNode
|
||||
* | EnumValueNode
|
||||
* | ListValueNode
|
||||
* | ObjectValueNode
|
||||
* | ObjectFieldNode
|
||||
* | DirectiveNode
|
||||
* | ListTypeNode
|
||||
* | NonNullTypeNode
|
||||
*/
|
||||
abstract class Node
|
||||
{
|
||||
/** @var Location */
|
||||
public $loc;
|
||||
|
||||
/**
|
||||
* @param (NameNode|NodeList|SelectionSetNode|Location|string|int|bool|float|null)[] $vars
|
||||
*/
|
||||
public function __construct(array $vars)
|
||||
{
|
||||
if (empty($vars)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Utils::assign($this, $vars);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*/
|
||||
public function cloneDeep()
|
||||
{
|
||||
return $this->cloneValue($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|NodeList|Location|Node|(Node|NodeList|Location)[] $value
|
||||
*
|
||||
* @return string|NodeList|Location|Node
|
||||
*/
|
||||
private function cloneValue($value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$cloned = [];
|
||||
foreach ($value as $key => $arrValue) {
|
||||
$cloned[$key] = $this->cloneValue($arrValue);
|
||||
}
|
||||
} elseif ($value instanceof self) {
|
||||
$cloned = clone $value;
|
||||
foreach (get_object_vars($cloned) as $prop => $propValue) {
|
||||
$cloned->{$prop} = $this->cloneValue($propValue);
|
||||
}
|
||||
} else {
|
||||
$cloned = $value;
|
||||
}
|
||||
|
||||
return $cloned;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$tmp = $this->toArray(true);
|
||||
|
||||
return (string) json_encode($tmp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $recursive
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function toArray($recursive = false)
|
||||
{
|
||||
if ($recursive) {
|
||||
return $this->recursiveToArray($this);
|
||||
}
|
||||
|
||||
$tmp = (array) $this;
|
||||
|
||||
if ($this->loc) {
|
||||
$tmp['loc'] = [
|
||||
'start' => $this->loc->start,
|
||||
'end' => $this->loc->end,
|
||||
];
|
||||
}
|
||||
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
private function recursiveToArray(Node $node)
|
||||
{
|
||||
$result = [
|
||||
'kind' => $node->kind,
|
||||
];
|
||||
|
||||
if ($node->loc) {
|
||||
$result['loc'] = [
|
||||
'start' => $node->loc->start,
|
||||
'end' => $node->loc->end,
|
||||
];
|
||||
}
|
||||
|
||||
foreach (get_object_vars($node) as $prop => $propValue) {
|
||||
if (isset($result[$prop])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($propValue === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($propValue) || $propValue instanceof NodeList) {
|
||||
$tmp = [];
|
||||
foreach ($propValue as $tmp1) {
|
||||
$tmp[] = $tmp1 instanceof Node ? $this->recursiveToArray($tmp1) : (array) $tmp1;
|
||||
}
|
||||
} elseif ($propValue instanceof Node) {
|
||||
$tmp = $this->recursiveToArray($propValue);
|
||||
} elseif (is_scalar($propValue) || $propValue === null) {
|
||||
$tmp = $propValue;
|
||||
} else {
|
||||
$tmp = null;
|
||||
}
|
||||
|
||||
$result[$prop] = $tmp;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class NodeKind
|
||||
{
|
||||
// constants from language/kinds.js:
|
||||
|
||||
const NAME = 'Name';
|
||||
// Document
|
||||
|
||||
const DOCUMENT = 'Document';
|
||||
const OPERATION_DEFINITION = 'OperationDefinition';
|
||||
const VARIABLE_DEFINITION = 'VariableDefinition';
|
||||
const VARIABLE = 'Variable';
|
||||
const SELECTION_SET = 'SelectionSet';
|
||||
const FIELD = 'Field';
|
||||
const ARGUMENT = 'Argument';
|
||||
// Fragments
|
||||
|
||||
const FRAGMENT_SPREAD = 'FragmentSpread';
|
||||
const INLINE_FRAGMENT = 'InlineFragment';
|
||||
const FRAGMENT_DEFINITION = 'FragmentDefinition';
|
||||
// Values
|
||||
|
||||
const INT = 'IntValue';
|
||||
const FLOAT = 'FloatValue';
|
||||
const STRING = 'StringValue';
|
||||
const BOOLEAN = 'BooleanValue';
|
||||
const ENUM = 'EnumValue';
|
||||
const NULL = 'NullValue';
|
||||
const LST = 'ListValue';
|
||||
const OBJECT = 'ObjectValue';
|
||||
const OBJECT_FIELD = 'ObjectField';
|
||||
// Directives
|
||||
|
||||
const DIRECTIVE = 'Directive';
|
||||
// Types
|
||||
|
||||
const NAMED_TYPE = 'NamedType';
|
||||
const LIST_TYPE = 'ListType';
|
||||
const NON_NULL_TYPE = 'NonNullType';
|
||||
// Type System Definitions
|
||||
|
||||
const SCHEMA_DEFINITION = 'SchemaDefinition';
|
||||
const OPERATION_TYPE_DEFINITION = 'OperationTypeDefinition';
|
||||
// Type Definitions
|
||||
|
||||
const SCALAR_TYPE_DEFINITION = 'ScalarTypeDefinition';
|
||||
const OBJECT_TYPE_DEFINITION = 'ObjectTypeDefinition';
|
||||
const FIELD_DEFINITION = 'FieldDefinition';
|
||||
const INPUT_VALUE_DEFINITION = 'InputValueDefinition';
|
||||
const INTERFACE_TYPE_DEFINITION = 'InterfaceTypeDefinition';
|
||||
const UNION_TYPE_DEFINITION = 'UnionTypeDefinition';
|
||||
const ENUM_TYPE_DEFINITION = 'EnumTypeDefinition';
|
||||
const ENUM_VALUE_DEFINITION = 'EnumValueDefinition';
|
||||
const INPUT_OBJECT_TYPE_DEFINITION = 'InputObjectTypeDefinition';
|
||||
// Type Extensions
|
||||
|
||||
const SCALAR_TYPE_EXTENSION = 'ScalarTypeExtension';
|
||||
const OBJECT_TYPE_EXTENSION = 'ObjectTypeExtension';
|
||||
const INTERFACE_TYPE_EXTENSION = 'InterfaceTypeExtension';
|
||||
const UNION_TYPE_EXTENSION = 'UnionTypeExtension';
|
||||
const ENUM_TYPE_EXTENSION = 'EnumTypeExtension';
|
||||
const INPUT_OBJECT_TYPE_EXTENSION = 'InputObjectTypeExtension';
|
||||
// Directive Definitions
|
||||
|
||||
const DIRECTIVE_DEFINITION = 'DirectiveDefinition';
|
||||
|
||||
// Type System Extensions
|
||||
const SCHEMA_EXTENSION = 'SchemaExtension';
|
||||
|
||||
/** @var string[] */
|
||||
public static $classMap = [
|
||||
self::NAME => NameNode::class,
|
||||
|
||||
// Document
|
||||
self::DOCUMENT => DocumentNode::class,
|
||||
self::OPERATION_DEFINITION => OperationDefinitionNode::class,
|
||||
self::VARIABLE_DEFINITION => VariableDefinitionNode::class,
|
||||
self::VARIABLE => VariableNode::class,
|
||||
self::SELECTION_SET => SelectionSetNode::class,
|
||||
self::FIELD => FieldNode::class,
|
||||
self::ARGUMENT => ArgumentNode::class,
|
||||
|
||||
// Fragments
|
||||
self::FRAGMENT_SPREAD => FragmentSpreadNode::class,
|
||||
self::INLINE_FRAGMENT => InlineFragmentNode::class,
|
||||
self::FRAGMENT_DEFINITION => FragmentDefinitionNode::class,
|
||||
|
||||
// Values
|
||||
self::INT => IntValueNode::class,
|
||||
self::FLOAT => FloatValueNode::class,
|
||||
self::STRING => StringValueNode::class,
|
||||
self::BOOLEAN => BooleanValueNode::class,
|
||||
self::ENUM => EnumValueNode::class,
|
||||
self::NULL => NullValueNode::class,
|
||||
self::LST => ListValueNode::class,
|
||||
self::OBJECT => ObjectValueNode::class,
|
||||
self::OBJECT_FIELD => ObjectFieldNode::class,
|
||||
|
||||
// Directives
|
||||
self::DIRECTIVE => DirectiveNode::class,
|
||||
|
||||
// Types
|
||||
self::NAMED_TYPE => NamedTypeNode::class,
|
||||
self::LIST_TYPE => ListTypeNode::class,
|
||||
self::NON_NULL_TYPE => NonNullTypeNode::class,
|
||||
|
||||
// Type System Definitions
|
||||
self::SCHEMA_DEFINITION => SchemaDefinitionNode::class,
|
||||
self::OPERATION_TYPE_DEFINITION => OperationTypeDefinitionNode::class,
|
||||
|
||||
// Type Definitions
|
||||
self::SCALAR_TYPE_DEFINITION => ScalarTypeDefinitionNode::class,
|
||||
self::OBJECT_TYPE_DEFINITION => ObjectTypeDefinitionNode::class,
|
||||
self::FIELD_DEFINITION => FieldDefinitionNode::class,
|
||||
self::INPUT_VALUE_DEFINITION => InputValueDefinitionNode::class,
|
||||
self::INTERFACE_TYPE_DEFINITION => InterfaceTypeDefinitionNode::class,
|
||||
self::UNION_TYPE_DEFINITION => UnionTypeDefinitionNode::class,
|
||||
self::ENUM_TYPE_DEFINITION => EnumTypeDefinitionNode::class,
|
||||
self::ENUM_VALUE_DEFINITION => EnumValueDefinitionNode::class,
|
||||
self::INPUT_OBJECT_TYPE_DEFINITION => InputObjectTypeDefinitionNode::class,
|
||||
|
||||
// Type Extensions
|
||||
self::SCALAR_TYPE_EXTENSION => ScalarTypeExtensionNode::class,
|
||||
self::OBJECT_TYPE_EXTENSION => ObjectTypeExtensionNode::class,
|
||||
self::INTERFACE_TYPE_EXTENSION => InterfaceTypeExtensionNode::class,
|
||||
self::UNION_TYPE_EXTENSION => UnionTypeExtensionNode::class,
|
||||
self::ENUM_TYPE_EXTENSION => EnumTypeExtensionNode::class,
|
||||
self::INPUT_OBJECT_TYPE_EXTENSION => InputObjectTypeExtensionNode::class,
|
||||
|
||||
// Directive Definitions
|
||||
self::DIRECTIVE_DEFINITION => DirectiveDefinitionNode::class,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
use ArrayAccess;
|
||||
use Countable;
|
||||
use Generator;
|
||||
use GraphQL\Utils\AST;
|
||||
use IteratorAggregate;
|
||||
use function array_merge;
|
||||
use function array_splice;
|
||||
use function count;
|
||||
use function is_array;
|
||||
|
||||
class NodeList implements ArrayAccess, IteratorAggregate, Countable
|
||||
{
|
||||
/** @var Node[]|mixed[] */
|
||||
private $nodes;
|
||||
|
||||
/**
|
||||
* @param Node[]|mixed[] $nodes
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function create(array $nodes)
|
||||
{
|
||||
return new static($nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node[]|mixed[] $nodes
|
||||
*/
|
||||
public function __construct(array $nodes)
|
||||
{
|
||||
$this->nodes = $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
return isset($this->nodes[$offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
$item = $this->nodes[$offset];
|
||||
|
||||
if (is_array($item) && isset($item['kind'])) {
|
||||
$this->nodes[$offset] = $item = AST::fromArray($item);
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
if (is_array($value) && isset($value['kind'])) {
|
||||
$value = AST::fromArray($value);
|
||||
}
|
||||
$this->nodes[$offset] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $offset
|
||||
*/
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
unset($this->nodes[$offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $offset
|
||||
* @param int $length
|
||||
* @param mixed $replacement
|
||||
*
|
||||
* @return NodeList
|
||||
*/
|
||||
public function splice($offset, $length, $replacement = null)
|
||||
{
|
||||
return new NodeList(array_splice($this->nodes, $offset, $length, $replacement));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param NodeList|Node[] $list
|
||||
*
|
||||
* @return NodeList
|
||||
*/
|
||||
public function merge($list)
|
||||
{
|
||||
if ($list instanceof self) {
|
||||
$list = $list->nodes;
|
||||
}
|
||||
|
||||
return new NodeList(array_merge($this->nodes, $list));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Generator
|
||||
*/
|
||||
public function getIterator()
|
||||
{
|
||||
foreach ($this->nodes as $key => $_) {
|
||||
yield $this->offsetGet($key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return count($this->nodes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class NonNullTypeNode extends Node implements TypeNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::NON_NULL_TYPE;
|
||||
|
||||
/** @var NameNode | ListTypeNode */
|
||||
public $type;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class NullValueNode extends Node implements ValueNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::NULL;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ObjectFieldNode extends Node
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::OBJECT_FIELD;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var ValueNode */
|
||||
public $value;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ObjectTypeDefinitionNode extends Node implements TypeDefinitionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::OBJECT_TYPE_DEFINITION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var NamedTypeNode[] */
|
||||
public $interfaces = [];
|
||||
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
|
||||
/** @var FieldDefinitionNode[]|null */
|
||||
public $fields;
|
||||
|
||||
/** @var StringValueNode|null */
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ObjectTypeExtensionNode extends Node implements TypeExtensionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::OBJECT_TYPE_EXTENSION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var NamedTypeNode[] */
|
||||
public $interfaces = [];
|
||||
|
||||
/** @var DirectiveNode[] */
|
||||
public $directives;
|
||||
|
||||
/** @var FieldDefinitionNode[] */
|
||||
public $fields;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ObjectValueNode extends Node implements ValueNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::OBJECT;
|
||||
|
||||
/** @var ObjectFieldNode[]|NodeList */
|
||||
public $fields;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class OperationDefinitionNode extends Node implements ExecutableDefinitionNode, HasSelectionSet
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::OPERATION_DEFINITION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var string (oneOf 'query', 'mutation')) */
|
||||
public $operation;
|
||||
|
||||
/** @var VariableDefinitionNode[] */
|
||||
public $variableDefinitions;
|
||||
|
||||
/** @var DirectiveNode[] */
|
||||
public $directives;
|
||||
|
||||
/** @var SelectionSetNode */
|
||||
public $selectionSet;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class OperationTypeDefinitionNode extends Node
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::OPERATION_TYPE_DEFINITION;
|
||||
|
||||
/**
|
||||
* One of 'query' | 'mutation' | 'subscription'
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $operation;
|
||||
|
||||
/** @var NamedTypeNode */
|
||||
public $type;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ScalarTypeDefinitionNode extends Node implements TypeDefinitionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::SCALAR_TYPE_DEFINITION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var DirectiveNode[] */
|
||||
public $directives;
|
||||
|
||||
/** @var StringValueNode|null */
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ScalarTypeExtensionNode extends Node implements TypeExtensionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::SCALAR_TYPE_EXTENSION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class SchemaDefinitionNode extends Node implements TypeSystemDefinitionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::SCHEMA_DEFINITION;
|
||||
|
||||
/** @var DirectiveNode[] */
|
||||
public $directives;
|
||||
|
||||
/** @var OperationTypeDefinitionNode[] */
|
||||
public $operationTypes;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class SchemaTypeExtensionNode extends Node implements TypeExtensionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::SCHEMA_EXTENSION;
|
||||
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
|
||||
/** @var OperationTypeDefinitionNode[]|null */
|
||||
public $operationTypes;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
/**
|
||||
* export type SelectionNode = FieldNode | FragmentSpreadNode | InlineFragmentNode
|
||||
*/
|
||||
interface SelectionNode
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class SelectionSetNode extends Node
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::SELECTION_SET;
|
||||
|
||||
/** @var SelectionNode[] */
|
||||
public $selections;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class StringValueNode extends Node implements ValueNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::STRING;
|
||||
|
||||
/** @var string */
|
||||
public $value;
|
||||
|
||||
/** @var bool|null */
|
||||
public $block;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
/**
|
||||
* export type TypeDefinitionNode = ScalarTypeDefinitionNode
|
||||
* | ObjectTypeDefinitionNode
|
||||
* | InterfaceTypeDefinitionNode
|
||||
* | UnionTypeDefinitionNode
|
||||
* | EnumTypeDefinitionNode
|
||||
* | InputObjectTypeDefinitionNode
|
||||
*/
|
||||
interface TypeDefinitionNode extends TypeSystemDefinitionNode
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
/**
|
||||
* export type TypeExtensionNode =
|
||||
* | ScalarTypeExtensionNode
|
||||
* | ObjectTypeExtensionNode
|
||||
* | InterfaceTypeExtensionNode
|
||||
* | UnionTypeExtensionNode
|
||||
* | EnumTypeExtensionNode
|
||||
* | InputObjectTypeExtensionNode;
|
||||
*/
|
||||
interface TypeExtensionNode extends TypeSystemDefinitionNode
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
/**
|
||||
* export type TypeNode = NamedTypeNode
|
||||
* | ListTypeNode
|
||||
* | NonNullTypeNode
|
||||
*/
|
||||
interface TypeNode
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
/**
|
||||
* export type TypeSystemDefinitionNode =
|
||||
* | SchemaDefinitionNode
|
||||
* | TypeDefinitionNode
|
||||
* | TypeExtensionNode
|
||||
* | DirectiveDefinitionNode
|
||||
*/
|
||||
interface TypeSystemDefinitionNode extends DefinitionNode
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class UnionTypeDefinitionNode extends Node implements TypeDefinitionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::UNION_TYPE_DEFINITION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var DirectiveNode[] */
|
||||
public $directives;
|
||||
|
||||
/** @var NamedTypeNode[]|null */
|
||||
public $types;
|
||||
|
||||
/** @var StringValueNode|null */
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class UnionTypeExtensionNode extends Node implements TypeExtensionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::UNION_TYPE_EXTENSION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
|
||||
/** @var NamedTypeNode[]|null */
|
||||
public $types;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
/**
|
||||
export type ValueNode = VariableNode
|
||||
| NullValueNode
|
||||
| IntValueNode
|
||||
| FloatValueNode
|
||||
| StringValueNode
|
||||
| BooleanValueNode
|
||||
| EnumValueNode
|
||||
| ListValueNode
|
||||
| ObjectValueNode
|
||||
*/
|
||||
interface ValueNode
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class VariableDefinitionNode extends Node implements DefinitionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::VARIABLE_DEFINITION;
|
||||
|
||||
/** @var VariableNode */
|
||||
public $variable;
|
||||
|
||||
/** @var TypeNode */
|
||||
public $type;
|
||||
|
||||
/** @var ValueNode|null */
|
||||
public $defaultValue;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class VariableNode extends Node implements ValueNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::VARIABLE;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language;
|
||||
|
||||
/**
|
||||
* List of available directive locations
|
||||
*/
|
||||
class DirectiveLocation
|
||||
{
|
||||
// Request Definitions
|
||||
const QUERY = 'QUERY';
|
||||
const MUTATION = 'MUTATION';
|
||||
const SUBSCRIPTION = 'SUBSCRIPTION';
|
||||
const FIELD = 'FIELD';
|
||||
const FRAGMENT_DEFINITION = 'FRAGMENT_DEFINITION';
|
||||
const FRAGMENT_SPREAD = 'FRAGMENT_SPREAD';
|
||||
const INLINE_FRAGMENT = 'INLINE_FRAGMENT';
|
||||
|
||||
// Type System Definitions
|
||||
const SCHEMA = 'SCHEMA';
|
||||
const SCALAR = 'SCALAR';
|
||||
const OBJECT = 'OBJECT';
|
||||
const FIELD_DEFINITION = 'FIELD_DEFINITION';
|
||||
const ARGUMENT_DEFINITION = 'ARGUMENT_DEFINITION';
|
||||
const IFACE = 'INTERFACE';
|
||||
const UNION = 'UNION';
|
||||
const ENUM = 'ENUM';
|
||||
const ENUM_VALUE = 'ENUM_VALUE';
|
||||
const INPUT_OBJECT = 'INPUT_OBJECT';
|
||||
const INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION';
|
||||
|
||||
/** @var string[] */
|
||||
private static $locations = [
|
||||
self::QUERY => self::QUERY,
|
||||
self::MUTATION => self::MUTATION,
|
||||
self::SUBSCRIPTION => self::SUBSCRIPTION,
|
||||
self::FIELD => self::FIELD,
|
||||
self::FRAGMENT_DEFINITION => self::FRAGMENT_DEFINITION,
|
||||
self::FRAGMENT_SPREAD => self::FRAGMENT_SPREAD,
|
||||
self::INLINE_FRAGMENT => self::INLINE_FRAGMENT,
|
||||
self::SCHEMA => self::SCHEMA,
|
||||
self::SCALAR => self::SCALAR,
|
||||
self::OBJECT => self::OBJECT,
|
||||
self::FIELD_DEFINITION => self::FIELD_DEFINITION,
|
||||
self::ARGUMENT_DEFINITION => self::ARGUMENT_DEFINITION,
|
||||
self::IFACE => self::IFACE,
|
||||
self::UNION => self::UNION,
|
||||
self::ENUM => self::ENUM,
|
||||
self::ENUM_VALUE => self::ENUM_VALUE,
|
||||
self::INPUT_OBJECT => self::INPUT_OBJECT,
|
||||
self::INPUT_FIELD_DEFINITION => self::INPUT_FIELD_DEFINITION,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function has($name)
|
||||
{
|
||||
return isset(self::$locations[$name]);
|
||||
}
|
||||
}
|
||||
+804
@@ -0,0 +1,804 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language;
|
||||
|
||||
use GraphQL\Error\SyntaxError;
|
||||
use GraphQL\Utils\BlockString;
|
||||
use GraphQL\Utils\Utils;
|
||||
use function chr;
|
||||
use function hexdec;
|
||||
use function ord;
|
||||
use function preg_match;
|
||||
|
||||
/**
|
||||
* A Lexer is a stateful stream generator in that every time
|
||||
* it is advanced, it returns the next token in the Source. Assuming the
|
||||
* source lexes, the final Token emitted by the lexer will be of kind
|
||||
* EOF, after which the lexer will repeatedly return the same EOF token
|
||||
* whenever called.
|
||||
*
|
||||
* Algorithm is O(N) both on memory and time
|
||||
*/
|
||||
class Lexer
|
||||
{
|
||||
private const TOKEN_BANG = 33;
|
||||
private const TOKEN_HASH = 35;
|
||||
private const TOKEN_DOLLAR = 36;
|
||||
private const TOKEN_AMP = 38;
|
||||
private const TOKEN_PAREN_L = 40;
|
||||
private const TOKEN_PAREN_R = 41;
|
||||
private const TOKEN_DOT = 46;
|
||||
private const TOKEN_COLON = 58;
|
||||
private const TOKEN_EQUALS = 61;
|
||||
private const TOKEN_AT = 64;
|
||||
private const TOKEN_BRACKET_L = 91;
|
||||
private const TOKEN_BRACKET_R = 93;
|
||||
private const TOKEN_BRACE_L = 123;
|
||||
private const TOKEN_PIPE = 124;
|
||||
private const TOKEN_BRACE_R = 125;
|
||||
|
||||
/** @var Source */
|
||||
public $source;
|
||||
|
||||
/** @var bool[] */
|
||||
public $options;
|
||||
|
||||
/**
|
||||
* The previously focused non-ignored token.
|
||||
*
|
||||
* @var Token
|
||||
*/
|
||||
public $lastToken;
|
||||
|
||||
/**
|
||||
* The currently focused non-ignored token.
|
||||
*
|
||||
* @var Token
|
||||
*/
|
||||
public $token;
|
||||
|
||||
/**
|
||||
* The (1-indexed) line containing the current token.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $line;
|
||||
|
||||
/**
|
||||
* The character offset at which the current line begins.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $lineStart;
|
||||
|
||||
/**
|
||||
* Current cursor position for UTF8 encoding of the source
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $position;
|
||||
|
||||
/**
|
||||
* Current cursor position for ASCII representation of the source
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $byteStreamPosition;
|
||||
|
||||
/**
|
||||
* @param bool[] $options
|
||||
*/
|
||||
public function __construct(Source $source, array $options = [])
|
||||
{
|
||||
$startOfFileToken = new Token(Token::SOF, 0, 0, 0, 0, null);
|
||||
|
||||
$this->source = $source;
|
||||
$this->options = $options;
|
||||
$this->lastToken = $startOfFileToken;
|
||||
$this->token = $startOfFileToken;
|
||||
$this->line = 1;
|
||||
$this->lineStart = 0;
|
||||
$this->position = $this->byteStreamPosition = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Token
|
||||
*/
|
||||
public function advance()
|
||||
{
|
||||
$this->lastToken = $this->token;
|
||||
|
||||
return $this->token = $this->lookahead();
|
||||
}
|
||||
|
||||
public function lookahead()
|
||||
{
|
||||
$token = $this->token;
|
||||
if ($token->kind !== Token::EOF) {
|
||||
do {
|
||||
$token = $token->next ?: ($token->next = $this->readToken($token));
|
||||
} while ($token->kind === Token::COMMENT);
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Token
|
||||
*
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
private function readToken(Token $prev)
|
||||
{
|
||||
$bodyLength = $this->source->length;
|
||||
|
||||
$this->positionAfterWhitespace();
|
||||
$position = $this->position;
|
||||
|
||||
$line = $this->line;
|
||||
$col = 1 + $position - $this->lineStart;
|
||||
|
||||
if ($position >= $bodyLength) {
|
||||
return new Token(Token::EOF, $bodyLength, $bodyLength, $line, $col, $prev);
|
||||
}
|
||||
|
||||
// Read next char and advance string cursor:
|
||||
[, $code, $bytes] = $this->readChar(true);
|
||||
|
||||
switch ($code) {
|
||||
case self::TOKEN_BANG:
|
||||
return new Token(Token::BANG, $position, $position + 1, $line, $col, $prev);
|
||||
case self::TOKEN_HASH: // #
|
||||
$this->moveStringCursor(-1, -1 * $bytes);
|
||||
|
||||
return $this->readComment($line, $col, $prev);
|
||||
case self::TOKEN_DOLLAR:
|
||||
return new Token(Token::DOLLAR, $position, $position + 1, $line, $col, $prev);
|
||||
case self::TOKEN_AMP:
|
||||
return new Token(Token::AMP, $position, $position + 1, $line, $col, $prev);
|
||||
case self::TOKEN_PAREN_L:
|
||||
return new Token(Token::PAREN_L, $position, $position + 1, $line, $col, $prev);
|
||||
case self::TOKEN_PAREN_R:
|
||||
return new Token(Token::PAREN_R, $position, $position + 1, $line, $col, $prev);
|
||||
case self::TOKEN_DOT: // .
|
||||
[, $charCode1] = $this->readChar(true);
|
||||
[, $charCode2] = $this->readChar(true);
|
||||
|
||||
if ($charCode1 === self::TOKEN_DOT && $charCode2 === self::TOKEN_DOT) {
|
||||
return new Token(Token::SPREAD, $position, $position + 3, $line, $col, $prev);
|
||||
}
|
||||
break;
|
||||
case self::TOKEN_COLON:
|
||||
return new Token(Token::COLON, $position, $position + 1, $line, $col, $prev);
|
||||
case self::TOKEN_EQUALS:
|
||||
return new Token(Token::EQUALS, $position, $position + 1, $line, $col, $prev);
|
||||
case self::TOKEN_AT:
|
||||
return new Token(Token::AT, $position, $position + 1, $line, $col, $prev);
|
||||
case self::TOKEN_BRACKET_L:
|
||||
return new Token(Token::BRACKET_L, $position, $position + 1, $line, $col, $prev);
|
||||
case self::TOKEN_BRACKET_R:
|
||||
return new Token(Token::BRACKET_R, $position, $position + 1, $line, $col, $prev);
|
||||
case self::TOKEN_BRACE_L:
|
||||
return new Token(Token::BRACE_L, $position, $position + 1, $line, $col, $prev);
|
||||
case self::TOKEN_PIPE:
|
||||
return new Token(Token::PIPE, $position, $position + 1, $line, $col, $prev);
|
||||
case self::TOKEN_BRACE_R:
|
||||
return new Token(Token::BRACE_R, $position, $position + 1, $line, $col, $prev);
|
||||
|
||||
// A-Z
|
||||
case 65:
|
||||
case 66:
|
||||
case 67:
|
||||
case 68:
|
||||
case 69:
|
||||
case 70:
|
||||
case 71:
|
||||
case 72:
|
||||
case 73:
|
||||
case 74:
|
||||
case 75:
|
||||
case 76:
|
||||
case 77:
|
||||
case 78:
|
||||
case 79:
|
||||
case 80:
|
||||
case 81:
|
||||
case 82:
|
||||
case 83:
|
||||
case 84:
|
||||
case 85:
|
||||
case 86:
|
||||
case 87:
|
||||
case 88:
|
||||
case 89:
|
||||
case 90:
|
||||
// _
|
||||
case 95:
|
||||
// a-z
|
||||
case 97:
|
||||
case 98:
|
||||
case 99:
|
||||
case 100:
|
||||
case 101:
|
||||
case 102:
|
||||
case 103:
|
||||
case 104:
|
||||
case 105:
|
||||
case 106:
|
||||
case 107:
|
||||
case 108:
|
||||
case 109:
|
||||
case 110:
|
||||
case 111:
|
||||
case 112:
|
||||
case 113:
|
||||
case 114:
|
||||
case 115:
|
||||
case 116:
|
||||
case 117:
|
||||
case 118:
|
||||
case 119:
|
||||
case 120:
|
||||
case 121:
|
||||
case 122:
|
||||
return $this->moveStringCursor(-1, -1 * $bytes)
|
||||
->readName($line, $col, $prev);
|
||||
|
||||
// -
|
||||
case 45:
|
||||
// 0-9
|
||||
case 48:
|
||||
case 49:
|
||||
case 50:
|
||||
case 51:
|
||||
case 52:
|
||||
case 53:
|
||||
case 54:
|
||||
case 55:
|
||||
case 56:
|
||||
case 57:
|
||||
return $this->moveStringCursor(-1, -1 * $bytes)
|
||||
->readNumber($line, $col, $prev);
|
||||
|
||||
// "
|
||||
case 34:
|
||||
[, $nextCode] = $this->readChar();
|
||||
[, $nextNextCode] = $this->moveStringCursor(1, 1)->readChar();
|
||||
|
||||
if ($nextCode === 34 && $nextNextCode === 34) {
|
||||
return $this->moveStringCursor(-2, (-1 * $bytes) - 1)
|
||||
->readBlockString($line, $col, $prev);
|
||||
}
|
||||
|
||||
return $this->moveStringCursor(-2, (-1 * $bytes) - 1)
|
||||
->readString($line, $col, $prev);
|
||||
}
|
||||
|
||||
throw new SyntaxError(
|
||||
$this->source,
|
||||
$position,
|
||||
$this->unexpectedCharacterMessage($code)
|
||||
);
|
||||
}
|
||||
|
||||
private function unexpectedCharacterMessage($code)
|
||||
{
|
||||
// SourceCharacter
|
||||
if ($code < 0x0020 && $code !== 0x0009 && $code !== 0x000A && $code !== 0x000D) {
|
||||
return 'Cannot contain the invalid character ' . Utils::printCharCode($code);
|
||||
}
|
||||
|
||||
if ($code === 39) {
|
||||
return "Unexpected single quote character ('), did you mean to use " .
|
||||
'a double quote (")?';
|
||||
}
|
||||
|
||||
return 'Cannot parse the unexpected character ' . Utils::printCharCode($code) . '.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an alphanumeric + underscore name from the source.
|
||||
*
|
||||
* [_A-Za-z][_0-9A-Za-z]*
|
||||
*
|
||||
* @param int $line
|
||||
* @param int $col
|
||||
*
|
||||
* @return Token
|
||||
*/
|
||||
private function readName($line, $col, Token $prev)
|
||||
{
|
||||
$value = '';
|
||||
$start = $this->position;
|
||||
[$char, $code] = $this->readChar();
|
||||
|
||||
while ($code && (
|
||||
$code === 95 || // _
|
||||
$code >= 48 && $code <= 57 || // 0-9
|
||||
$code >= 65 && $code <= 90 || // A-Z
|
||||
$code >= 97 && $code <= 122 // a-z
|
||||
)) {
|
||||
$value .= $char;
|
||||
[$char, $code] = $this->moveStringCursor(1, 1)->readChar();
|
||||
}
|
||||
|
||||
return new Token(
|
||||
Token::NAME,
|
||||
$start,
|
||||
$this->position,
|
||||
$line,
|
||||
$col,
|
||||
$prev,
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a number token from the source file, either a float
|
||||
* or an int depending on whether a decimal point appears.
|
||||
*
|
||||
* Int: -?(0|[1-9][0-9]*)
|
||||
* Float: -?(0|[1-9][0-9]*)(\.[0-9]+)?((E|e)(+|-)?[0-9]+)?
|
||||
*
|
||||
* @param int $line
|
||||
* @param int $col
|
||||
*
|
||||
* @return Token
|
||||
*
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
private function readNumber($line, $col, Token $prev)
|
||||
{
|
||||
$value = '';
|
||||
$start = $this->position;
|
||||
[$char, $code] = $this->readChar();
|
||||
|
||||
$isFloat = false;
|
||||
|
||||
if ($code === 45) { // -
|
||||
$value .= $char;
|
||||
[$char, $code] = $this->moveStringCursor(1, 1)->readChar();
|
||||
}
|
||||
|
||||
// guard against leading zero's
|
||||
if ($code === 48) { // 0
|
||||
$value .= $char;
|
||||
[$char, $code] = $this->moveStringCursor(1, 1)->readChar();
|
||||
|
||||
if ($code >= 48 && $code <= 57) {
|
||||
throw new SyntaxError(
|
||||
$this->source,
|
||||
$this->position,
|
||||
'Invalid number, unexpected digit after 0: ' . Utils::printCharCode($code)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$value .= $this->readDigits();
|
||||
[$char, $code] = $this->readChar();
|
||||
}
|
||||
|
||||
if ($code === 46) { // .
|
||||
$isFloat = true;
|
||||
$this->moveStringCursor(1, 1);
|
||||
|
||||
$value .= $char;
|
||||
$value .= $this->readDigits();
|
||||
[$char, $code] = $this->readChar();
|
||||
}
|
||||
|
||||
if ($code === 69 || $code === 101) { // E e
|
||||
$isFloat = true;
|
||||
$value .= $char;
|
||||
[$char, $code] = $this->moveStringCursor(1, 1)->readChar();
|
||||
|
||||
if ($code === 43 || $code === 45) { // + -
|
||||
$value .= $char;
|
||||
$this->moveStringCursor(1, 1);
|
||||
}
|
||||
$value .= $this->readDigits();
|
||||
}
|
||||
|
||||
return new Token(
|
||||
$isFloat ? Token::FLOAT : Token::INT,
|
||||
$start,
|
||||
$this->position,
|
||||
$line,
|
||||
$col,
|
||||
$prev,
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns string with all digits + changes current string cursor position to point to the first char after digits
|
||||
*/
|
||||
private function readDigits()
|
||||
{
|
||||
[$char, $code] = $this->readChar();
|
||||
|
||||
if ($code >= 48 && $code <= 57) { // 0 - 9
|
||||
$value = '';
|
||||
|
||||
do {
|
||||
$value .= $char;
|
||||
[$char, $code] = $this->moveStringCursor(1, 1)->readChar();
|
||||
} while ($code >= 48 && $code <= 57); // 0 - 9
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($this->position > $this->source->length - 1) {
|
||||
$code = null;
|
||||
}
|
||||
|
||||
throw new SyntaxError(
|
||||
$this->source,
|
||||
$this->position,
|
||||
'Invalid number, expected digit but got: ' . Utils::printCharCode($code)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $line
|
||||
* @param int $col
|
||||
*
|
||||
* @return Token
|
||||
*
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
private function readString($line, $col, Token $prev)
|
||||
{
|
||||
$start = $this->position;
|
||||
|
||||
// Skip leading quote and read first string char:
|
||||
[$char, $code, $bytes] = $this->moveStringCursor(1, 1)->readChar();
|
||||
|
||||
$chunk = '';
|
||||
$value = '';
|
||||
|
||||
while ($code !== null &&
|
||||
// not LineTerminator
|
||||
$code !== 10 && $code !== 13
|
||||
) {
|
||||
// Closing Quote (")
|
||||
if ($code === 34) {
|
||||
$value .= $chunk;
|
||||
|
||||
// Skip quote
|
||||
$this->moveStringCursor(1, 1);
|
||||
|
||||
return new Token(
|
||||
Token::STRING,
|
||||
$start,
|
||||
$this->position,
|
||||
$line,
|
||||
$col,
|
||||
$prev,
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
$this->assertValidStringCharacterCode($code, $this->position);
|
||||
$this->moveStringCursor(1, $bytes);
|
||||
|
||||
if ($code === 92) { // \
|
||||
$value .= $chunk;
|
||||
[, $code] = $this->readChar(true);
|
||||
|
||||
switch ($code) {
|
||||
case 34:
|
||||
$value .= '"';
|
||||
break;
|
||||
case 47:
|
||||
$value .= '/';
|
||||
break;
|
||||
case 92:
|
||||
$value .= '\\';
|
||||
break;
|
||||
case 98:
|
||||
$value .= chr(8);
|
||||
break; // \b (backspace)
|
||||
case 102:
|
||||
$value .= "\f";
|
||||
break;
|
||||
case 110:
|
||||
$value .= "\n";
|
||||
break;
|
||||
case 114:
|
||||
$value .= "\r";
|
||||
break;
|
||||
case 116:
|
||||
$value .= "\t";
|
||||
break;
|
||||
case 117:
|
||||
$position = $this->position;
|
||||
[$hex] = $this->readChars(4, true);
|
||||
if (! preg_match('/[0-9a-fA-F]{4}/', $hex)) {
|
||||
throw new SyntaxError(
|
||||
$this->source,
|
||||
$position - 1,
|
||||
'Invalid character escape sequence: \\u' . $hex
|
||||
);
|
||||
}
|
||||
$code = hexdec($hex);
|
||||
$this->assertValidStringCharacterCode($code, $position - 2);
|
||||
$value .= Utils::chr($code);
|
||||
break;
|
||||
default:
|
||||
throw new SyntaxError(
|
||||
$this->source,
|
||||
$this->position - 1,
|
||||
'Invalid character escape sequence: \\' . Utils::chr($code)
|
||||
);
|
||||
}
|
||||
$chunk = '';
|
||||
} else {
|
||||
$chunk .= $char;
|
||||
}
|
||||
|
||||
[$char, $code, $bytes] = $this->readChar();
|
||||
}
|
||||
|
||||
throw new SyntaxError(
|
||||
$this->source,
|
||||
$this->position,
|
||||
'Unterminated string.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a block string token from the source file.
|
||||
*
|
||||
* """("?"?(\\"""|\\(?!=""")|[^"\\]))*"""
|
||||
*/
|
||||
private function readBlockString($line, $col, Token $prev)
|
||||
{
|
||||
$start = $this->position;
|
||||
|
||||
// Skip leading quotes and read first string char:
|
||||
[$char, $code, $bytes] = $this->moveStringCursor(3, 3)->readChar();
|
||||
|
||||
$chunk = '';
|
||||
$value = '';
|
||||
|
||||
while ($code !== null) {
|
||||
// Closing Triple-Quote (""")
|
||||
if ($code === 34) {
|
||||
// Move 2 quotes
|
||||
[, $nextCode] = $this->moveStringCursor(1, 1)->readChar();
|
||||
[, $nextNextCode] = $this->moveStringCursor(1, 1)->readChar();
|
||||
|
||||
if ($nextCode === 34 && $nextNextCode === 34) {
|
||||
$value .= $chunk;
|
||||
|
||||
$this->moveStringCursor(1, 1);
|
||||
|
||||
return new Token(
|
||||
Token::BLOCK_STRING,
|
||||
$start,
|
||||
$this->position,
|
||||
$line,
|
||||
$col,
|
||||
$prev,
|
||||
BlockString::value($value)
|
||||
);
|
||||
}
|
||||
|
||||
// move cursor back to before the first quote
|
||||
$this->moveStringCursor(-2, -2);
|
||||
}
|
||||
|
||||
$this->assertValidBlockStringCharacterCode($code, $this->position);
|
||||
$this->moveStringCursor(1, $bytes);
|
||||
|
||||
[, $nextCode] = $this->readChar();
|
||||
[, $nextNextCode] = $this->moveStringCursor(1, 1)->readChar();
|
||||
[, $nextNextNextCode] = $this->moveStringCursor(1, 1)->readChar();
|
||||
|
||||
// Escape Triple-Quote (\""")
|
||||
if ($code === 92 &&
|
||||
$nextCode === 34 &&
|
||||
$nextNextCode === 34 &&
|
||||
$nextNextNextCode === 34
|
||||
) {
|
||||
$this->moveStringCursor(1, 1);
|
||||
$value .= $chunk . '"""';
|
||||
$chunk = '';
|
||||
} else {
|
||||
$this->moveStringCursor(-2, -2);
|
||||
$chunk .= $char;
|
||||
}
|
||||
|
||||
[$char, $code, $bytes] = $this->readChar();
|
||||
}
|
||||
|
||||
throw new SyntaxError(
|
||||
$this->source,
|
||||
$this->position,
|
||||
'Unterminated string.'
|
||||
);
|
||||
}
|
||||
|
||||
private function assertValidStringCharacterCode($code, $position)
|
||||
{
|
||||
// SourceCharacter
|
||||
if ($code < 0x0020 && $code !== 0x0009) {
|
||||
throw new SyntaxError(
|
||||
$this->source,
|
||||
$position,
|
||||
'Invalid character within String: ' . Utils::printCharCode($code)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function assertValidBlockStringCharacterCode($code, $position)
|
||||
{
|
||||
// SourceCharacter
|
||||
if ($code < 0x0020 && $code !== 0x0009 && $code !== 0x000A && $code !== 0x000D) {
|
||||
throw new SyntaxError(
|
||||
$this->source,
|
||||
$position,
|
||||
'Invalid character within String: ' . Utils::printCharCode($code)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads from body starting at startPosition until it finds a non-whitespace
|
||||
* or commented character, then places cursor to the position of that character.
|
||||
*/
|
||||
private function positionAfterWhitespace()
|
||||
{
|
||||
while ($this->position < $this->source->length) {
|
||||
[, $code, $bytes] = $this->readChar();
|
||||
|
||||
// Skip whitespace
|
||||
// tab | space | comma | BOM
|
||||
if ($code === 9 || $code === 32 || $code === 44 || $code === 0xFEFF) {
|
||||
$this->moveStringCursor(1, $bytes);
|
||||
} elseif ($code === 10) { // new line
|
||||
$this->moveStringCursor(1, $bytes);
|
||||
$this->line++;
|
||||
$this->lineStart = $this->position;
|
||||
} elseif ($code === 13) { // carriage return
|
||||
[, $nextCode, $nextBytes] = $this->moveStringCursor(1, $bytes)->readChar();
|
||||
|
||||
if ($nextCode === 10) { // lf after cr
|
||||
$this->moveStringCursor(1, $nextBytes);
|
||||
}
|
||||
$this->line++;
|
||||
$this->lineStart = $this->position;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a comment token from the source file.
|
||||
*
|
||||
* #[\u0009\u0020-\uFFFF]*
|
||||
*
|
||||
* @param int $line
|
||||
* @param int $col
|
||||
*
|
||||
* @return Token
|
||||
*/
|
||||
private function readComment($line, $col, Token $prev)
|
||||
{
|
||||
$start = $this->position;
|
||||
$value = '';
|
||||
$bytes = 1;
|
||||
|
||||
do {
|
||||
[$char, $code, $bytes] = $this->moveStringCursor(1, $bytes)->readChar();
|
||||
$value .= $char;
|
||||
} while ($code &&
|
||||
// SourceCharacter but not LineTerminator
|
||||
($code > 0x001F || $code === 0x0009)
|
||||
);
|
||||
|
||||
return new Token(
|
||||
Token::COMMENT,
|
||||
$start,
|
||||
$this->position,
|
||||
$line,
|
||||
$col,
|
||||
$prev,
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads next UTF8Character from the byte stream, starting from $byteStreamPosition.
|
||||
*
|
||||
* @param bool $advance
|
||||
* @param int $byteStreamPosition
|
||||
*
|
||||
* @return (string|int)[]
|
||||
*/
|
||||
private function readChar($advance = false, $byteStreamPosition = null)
|
||||
{
|
||||
if ($byteStreamPosition === null) {
|
||||
$byteStreamPosition = $this->byteStreamPosition;
|
||||
}
|
||||
|
||||
$code = null;
|
||||
$utf8char = '';
|
||||
$bytes = 0;
|
||||
$positionOffset = 0;
|
||||
|
||||
if (isset($this->source->body[$byteStreamPosition])) {
|
||||
$ord = ord($this->source->body[$byteStreamPosition]);
|
||||
|
||||
if ($ord < 128) {
|
||||
$bytes = 1;
|
||||
} elseif ($ord < 224) {
|
||||
$bytes = 2;
|
||||
} elseif ($ord < 240) {
|
||||
$bytes = 3;
|
||||
} else {
|
||||
$bytes = 4;
|
||||
}
|
||||
|
||||
$utf8char = '';
|
||||
for ($pos = $byteStreamPosition; $pos < $byteStreamPosition + $bytes; $pos++) {
|
||||
$utf8char .= $this->source->body[$pos];
|
||||
}
|
||||
$positionOffset = 1;
|
||||
$code = $bytes === 1 ? $ord : Utils::ord($utf8char);
|
||||
}
|
||||
|
||||
if ($advance) {
|
||||
$this->moveStringCursor($positionOffset, $bytes);
|
||||
}
|
||||
|
||||
return [$utf8char, $code, $bytes];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads next $numberOfChars UTF8 characters from the byte stream, starting from $byteStreamPosition.
|
||||
*
|
||||
* @param int $charCount
|
||||
* @param bool $advance
|
||||
* @param null $byteStreamPosition
|
||||
*
|
||||
* @return (string|int)[]
|
||||
*/
|
||||
private function readChars($charCount, $advance = false, $byteStreamPosition = null)
|
||||
{
|
||||
$result = '';
|
||||
$totalBytes = 0;
|
||||
$byteOffset = $byteStreamPosition ?: $this->byteStreamPosition;
|
||||
|
||||
for ($i = 0; $i < $charCount; $i++) {
|
||||
[$char, $code, $bytes] = $this->readChar(false, $byteOffset);
|
||||
$totalBytes += $bytes;
|
||||
$byteOffset += $bytes;
|
||||
$result .= $char;
|
||||
}
|
||||
if ($advance) {
|
||||
$this->moveStringCursor($charCount, $totalBytes);
|
||||
}
|
||||
|
||||
return [$result, $totalBytes];
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves internal string cursor position
|
||||
*
|
||||
* @param int $positionOffset
|
||||
* @param int $byteStreamOffset
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function moveStringCursor($positionOffset, $byteStreamOffset)
|
||||
{
|
||||
$this->position += $positionOffset;
|
||||
$this->byteStreamPosition += $byteStreamOffset;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
+1786
File diff suppressed because it is too large
Load Diff
+520
@@ -0,0 +1,520 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language;
|
||||
|
||||
use GraphQL\Language\AST\ArgumentNode;
|
||||
use GraphQL\Language\AST\BooleanValueNode;
|
||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||
use GraphQL\Language\AST\DirectiveNode;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\AST\EnumTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\EnumTypeExtensionNode;
|
||||
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
||||
use GraphQL\Language\AST\EnumValueNode;
|
||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\FloatValueNode;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\InlineFragmentNode;
|
||||
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\InputObjectTypeExtensionNode;
|
||||
use GraphQL\Language\AST\InputValueDefinitionNode;
|
||||
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
|
||||
use GraphQL\Language\AST\IntValueNode;
|
||||
use GraphQL\Language\AST\ListTypeNode;
|
||||
use GraphQL\Language\AST\ListValueNode;
|
||||
use GraphQL\Language\AST\NamedTypeNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\NonNullTypeNode;
|
||||
use GraphQL\Language\AST\NullValueNode;
|
||||
use GraphQL\Language\AST\ObjectFieldNode;
|
||||
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\ObjectTypeExtensionNode;
|
||||
use GraphQL\Language\AST\ObjectValueNode;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\AST\OperationTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\ScalarTypeExtensionNode;
|
||||
use GraphQL\Language\AST\SchemaDefinitionNode;
|
||||
use GraphQL\Language\AST\SchemaTypeExtensionNode;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Language\AST\StringValueNode;
|
||||
use GraphQL\Language\AST\UnionTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\UnionTypeExtensionNode;
|
||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||
use GraphQL\Utils\Utils;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function json_encode;
|
||||
use function preg_replace;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
use function strpos;
|
||||
|
||||
/**
|
||||
* Prints AST to string. Capable of printing GraphQL queries and Type definition language.
|
||||
* Useful for pretty-printing queries or printing back AST for logging, documentation, etc.
|
||||
*
|
||||
* Usage example:
|
||||
*
|
||||
* ```php
|
||||
* $query = 'query myQuery {someField}';
|
||||
* $ast = GraphQL\Language\Parser::parse($query);
|
||||
* $printed = GraphQL\Language\Printer::doPrint($ast);
|
||||
* ```
|
||||
*/
|
||||
class Printer
|
||||
{
|
||||
/**
|
||||
* Prints AST to string. Capable of printing GraphQL queries and Type definition language.
|
||||
*
|
||||
* @param Node $ast
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function doPrint($ast)
|
||||
{
|
||||
static $instance;
|
||||
$instance = $instance ?: new static();
|
||||
|
||||
return $instance->printAST($ast);
|
||||
}
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function printAST($ast)
|
||||
{
|
||||
return Visitor::visit(
|
||||
$ast,
|
||||
[
|
||||
'leave' => [
|
||||
NodeKind::NAME => static function (Node $node) {
|
||||
return '' . $node->value;
|
||||
},
|
||||
|
||||
NodeKind::VARIABLE => static function ($node) {
|
||||
return '$' . $node->name;
|
||||
},
|
||||
|
||||
NodeKind::DOCUMENT => function (DocumentNode $node) {
|
||||
return $this->join($node->definitions, "\n\n") . "\n";
|
||||
},
|
||||
|
||||
NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) {
|
||||
$op = $node->operation;
|
||||
$name = $node->name;
|
||||
$varDefs = $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')');
|
||||
$directives = $this->join($node->directives, ' ');
|
||||
$selectionSet = $node->selectionSet;
|
||||
|
||||
// Anonymous queries with no directives or variable definitions can use
|
||||
// the query short form.
|
||||
return ! $name && ! $directives && ! $varDefs && $op === 'query'
|
||||
? $selectionSet
|
||||
: $this->join([$op, $this->join([$name, $varDefs]), $directives, $selectionSet], ' ');
|
||||
},
|
||||
|
||||
NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) {
|
||||
return $node->variable . ': ' . $node->type . $this->wrap(' = ', $node->defaultValue);
|
||||
},
|
||||
|
||||
NodeKind::SELECTION_SET => function (SelectionSetNode $node) {
|
||||
return $this->block($node->selections);
|
||||
},
|
||||
|
||||
NodeKind::FIELD => function (FieldNode $node) {
|
||||
return $this->join(
|
||||
[
|
||||
$this->wrap('', $node->alias, ': ') . $node->name . $this->wrap(
|
||||
'(',
|
||||
$this->join($node->arguments, ', '),
|
||||
')'
|
||||
),
|
||||
$this->join($node->directives, ' '),
|
||||
$node->selectionSet,
|
||||
],
|
||||
' '
|
||||
);
|
||||
},
|
||||
|
||||
NodeKind::ARGUMENT => static function (ArgumentNode $node) {
|
||||
return $node->name . ': ' . $node->value;
|
||||
},
|
||||
|
||||
NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) {
|
||||
return '...' . $node->name . $this->wrap(' ', $this->join($node->directives, ' '));
|
||||
},
|
||||
|
||||
NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) {
|
||||
return $this->join(
|
||||
[
|
||||
'...',
|
||||
$this->wrap('on ', $node->typeCondition),
|
||||
$this->join($node->directives, ' '),
|
||||
$node->selectionSet,
|
||||
],
|
||||
' '
|
||||
);
|
||||
},
|
||||
|
||||
NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) {
|
||||
// Note: fragment variable definitions are experimental and may be changed or removed in the future.
|
||||
return sprintf('fragment %s', $node->name)
|
||||
. $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')')
|
||||
. sprintf(' on %s ', $node->typeCondition)
|
||||
. $this->wrap('', $this->join($node->directives, ' '), ' ')
|
||||
. $node->selectionSet;
|
||||
},
|
||||
|
||||
NodeKind::INT => static function (IntValueNode $node) {
|
||||
return $node->value;
|
||||
},
|
||||
|
||||
NodeKind::FLOAT => static function (FloatValueNode $node) {
|
||||
return $node->value;
|
||||
},
|
||||
|
||||
NodeKind::STRING => function (StringValueNode $node, $key) {
|
||||
if ($node->block) {
|
||||
return $this->printBlockString($node->value, $key === 'description');
|
||||
}
|
||||
|
||||
return json_encode($node->value);
|
||||
},
|
||||
|
||||
NodeKind::BOOLEAN => static function (BooleanValueNode $node) {
|
||||
return $node->value ? 'true' : 'false';
|
||||
},
|
||||
|
||||
NodeKind::NULL => static function (NullValueNode $node) {
|
||||
return 'null';
|
||||
},
|
||||
|
||||
NodeKind::ENUM => static function (EnumValueNode $node) {
|
||||
return $node->value;
|
||||
},
|
||||
|
||||
NodeKind::LST => function (ListValueNode $node) {
|
||||
return '[' . $this->join($node->values, ', ') . ']';
|
||||
},
|
||||
|
||||
NodeKind::OBJECT => function (ObjectValueNode $node) {
|
||||
return '{' . $this->join($node->fields, ', ') . '}';
|
||||
},
|
||||
|
||||
NodeKind::OBJECT_FIELD => static function (ObjectFieldNode $node) {
|
||||
return $node->name . ': ' . $node->value;
|
||||
},
|
||||
|
||||
NodeKind::DIRECTIVE => function (DirectiveNode $node) {
|
||||
return '@' . $node->name . $this->wrap('(', $this->join($node->arguments, ', '), ')');
|
||||
},
|
||||
|
||||
NodeKind::NAMED_TYPE => static function (NamedTypeNode $node) {
|
||||
return $node->name;
|
||||
},
|
||||
|
||||
NodeKind::LIST_TYPE => static function (ListTypeNode $node) {
|
||||
return '[' . $node->type . ']';
|
||||
},
|
||||
|
||||
NodeKind::NON_NULL_TYPE => static function (NonNullTypeNode $node) {
|
||||
return $node->type . '!';
|
||||
},
|
||||
|
||||
NodeKind::SCHEMA_DEFINITION => function (SchemaDefinitionNode $def) {
|
||||
return $this->join(
|
||||
[
|
||||
'schema',
|
||||
$this->join($def->directives, ' '),
|
||||
$this->block($def->operationTypes),
|
||||
],
|
||||
' '
|
||||
);
|
||||
},
|
||||
|
||||
NodeKind::OPERATION_TYPE_DEFINITION => static function (OperationTypeDefinitionNode $def) {
|
||||
return $def->operation . ': ' . $def->type;
|
||||
},
|
||||
|
||||
NodeKind::SCALAR_TYPE_DEFINITION => $this->addDescription(function (ScalarTypeDefinitionNode $def) {
|
||||
return $this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' ');
|
||||
}),
|
||||
|
||||
NodeKind::OBJECT_TYPE_DEFINITION => $this->addDescription(function (ObjectTypeDefinitionNode $def) {
|
||||
return $this->join(
|
||||
[
|
||||
'type',
|
||||
$def->name,
|
||||
$this->wrap('implements ', $this->join($def->interfaces, ' & ')),
|
||||
$this->join($def->directives, ' '),
|
||||
$this->block($def->fields),
|
||||
],
|
||||
' '
|
||||
);
|
||||
}),
|
||||
|
||||
NodeKind::FIELD_DEFINITION => $this->addDescription(function (FieldDefinitionNode $def) {
|
||||
$noIndent = Utils::every($def->arguments, static function (string $arg) {
|
||||
return strpos($arg, "\n") === false;
|
||||
});
|
||||
|
||||
return $def->name
|
||||
. ($noIndent
|
||||
? $this->wrap('(', $this->join($def->arguments, ', '), ')')
|
||||
: $this->wrap("(\n", $this->indent($this->join($def->arguments, "\n")), "\n)"))
|
||||
. ': ' . $def->type
|
||||
. $this->wrap(' ', $this->join($def->directives, ' '));
|
||||
}),
|
||||
|
||||
NodeKind::INPUT_VALUE_DEFINITION => $this->addDescription(function (InputValueDefinitionNode $def) {
|
||||
return $this->join(
|
||||
[
|
||||
$def->name . ': ' . $def->type,
|
||||
$this->wrap('= ', $def->defaultValue),
|
||||
$this->join($def->directives, ' '),
|
||||
],
|
||||
' '
|
||||
);
|
||||
}),
|
||||
|
||||
NodeKind::INTERFACE_TYPE_DEFINITION => $this->addDescription(
|
||||
function (InterfaceTypeDefinitionNode $def) {
|
||||
return $this->join(
|
||||
[
|
||||
'interface',
|
||||
$def->name,
|
||||
$this->join($def->directives, ' '),
|
||||
$this->block($def->fields),
|
||||
],
|
||||
' '
|
||||
);
|
||||
}
|
||||
),
|
||||
|
||||
NodeKind::UNION_TYPE_DEFINITION => $this->addDescription(function (UnionTypeDefinitionNode $def) {
|
||||
return $this->join(
|
||||
[
|
||||
'union',
|
||||
$def->name,
|
||||
$this->join($def->directives, ' '),
|
||||
$def->types
|
||||
? '= ' . $this->join($def->types, ' | ')
|
||||
: '',
|
||||
],
|
||||
' '
|
||||
);
|
||||
}),
|
||||
|
||||
NodeKind::ENUM_TYPE_DEFINITION => $this->addDescription(function (EnumTypeDefinitionNode $def) {
|
||||
return $this->join(
|
||||
[
|
||||
'enum',
|
||||
$def->name,
|
||||
$this->join($def->directives, ' '),
|
||||
$this->block($def->values),
|
||||
],
|
||||
' '
|
||||
);
|
||||
}),
|
||||
|
||||
NodeKind::ENUM_VALUE_DEFINITION => $this->addDescription(function (EnumValueDefinitionNode $def) {
|
||||
return $this->join([$def->name, $this->join($def->directives, ' ')], ' ');
|
||||
}),
|
||||
|
||||
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $this->addDescription(function (
|
||||
InputObjectTypeDefinitionNode $def
|
||||
) {
|
||||
return $this->join(
|
||||
[
|
||||
'input',
|
||||
$def->name,
|
||||
$this->join($def->directives, ' '),
|
||||
$this->block($def->fields),
|
||||
],
|
||||
' '
|
||||
);
|
||||
}),
|
||||
|
||||
NodeKind::SCHEMA_EXTENSION => function (SchemaTypeExtensionNode $def) {
|
||||
return $this->join(
|
||||
[
|
||||
'extend schema',
|
||||
$this->join($def->directives, ' '),
|
||||
$this->block($def->operationTypes),
|
||||
],
|
||||
' '
|
||||
);
|
||||
},
|
||||
|
||||
NodeKind::SCALAR_TYPE_EXTENSION => function (ScalarTypeExtensionNode $def) {
|
||||
return $this->join(
|
||||
[
|
||||
'extend scalar',
|
||||
$def->name,
|
||||
$this->join($def->directives, ' '),
|
||||
],
|
||||
' '
|
||||
);
|
||||
},
|
||||
|
||||
NodeKind::OBJECT_TYPE_EXTENSION => function (ObjectTypeExtensionNode $def) {
|
||||
return $this->join(
|
||||
[
|
||||
'extend type',
|
||||
$def->name,
|
||||
$this->wrap('implements ', $this->join($def->interfaces, ' & ')),
|
||||
$this->join($def->directives, ' '),
|
||||
$this->block($def->fields),
|
||||
],
|
||||
' '
|
||||
);
|
||||
},
|
||||
|
||||
NodeKind::INTERFACE_TYPE_EXTENSION => function (InterfaceTypeExtensionNode $def) {
|
||||
return $this->join(
|
||||
[
|
||||
'extend interface',
|
||||
$def->name,
|
||||
$this->join($def->directives, ' '),
|
||||
$this->block($def->fields),
|
||||
],
|
||||
' '
|
||||
);
|
||||
},
|
||||
|
||||
NodeKind::UNION_TYPE_EXTENSION => function (UnionTypeExtensionNode $def) {
|
||||
return $this->join(
|
||||
[
|
||||
'extend union',
|
||||
$def->name,
|
||||
$this->join($def->directives, ' '),
|
||||
$def->types
|
||||
? '= ' . $this->join($def->types, ' | ')
|
||||
: '',
|
||||
],
|
||||
' '
|
||||
);
|
||||
},
|
||||
|
||||
NodeKind::ENUM_TYPE_EXTENSION => function (EnumTypeExtensionNode $def) {
|
||||
return $this->join(
|
||||
[
|
||||
'extend enum',
|
||||
$def->name,
|
||||
$this->join($def->directives, ' '),
|
||||
$this->block($def->values),
|
||||
],
|
||||
' '
|
||||
);
|
||||
},
|
||||
|
||||
NodeKind::INPUT_OBJECT_TYPE_EXTENSION => function (InputObjectTypeExtensionNode $def) {
|
||||
return $this->join(
|
||||
[
|
||||
'extend input',
|
||||
$def->name,
|
||||
$this->join($def->directives, ' '),
|
||||
$this->block($def->fields),
|
||||
],
|
||||
' '
|
||||
);
|
||||
},
|
||||
|
||||
NodeKind::DIRECTIVE_DEFINITION => $this->addDescription(function (DirectiveDefinitionNode $def) {
|
||||
$noIndent = Utils::every($def->arguments, static function (string $arg) {
|
||||
return strpos($arg, "\n") === false;
|
||||
});
|
||||
|
||||
return 'directive @'
|
||||
. $def->name
|
||||
. ($noIndent
|
||||
? $this->wrap('(', $this->join($def->arguments, ', '), ')')
|
||||
: $this->wrap("(\n", $this->indent($this->join($def->arguments, "\n")), "\n"))
|
||||
. ' on ' . $this->join($def->locations, ' | ');
|
||||
}),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function addDescription(callable $cb)
|
||||
{
|
||||
return function ($node) use ($cb) {
|
||||
return $this->join([$node->description, $cb($node)], "\n");
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If maybeString is not null or empty, then wrap with start and end, otherwise
|
||||
* print an empty string.
|
||||
*/
|
||||
public function wrap($start, $maybeString, $end = '')
|
||||
{
|
||||
return $maybeString ? ($start . $maybeString . $end) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Given array, print each item on its own line, wrapped in an
|
||||
* indented "{ }" block.
|
||||
*/
|
||||
public function block($array)
|
||||
{
|
||||
return $array && $this->length($array)
|
||||
? "{\n" . $this->indent($this->join($array, "\n")) . "\n}"
|
||||
: '';
|
||||
}
|
||||
|
||||
public function indent($maybeString)
|
||||
{
|
||||
return $maybeString ? ' ' . str_replace("\n", "\n ", $maybeString) : '';
|
||||
}
|
||||
|
||||
public function manyList($start, $list, $separator, $end)
|
||||
{
|
||||
return $this->length($list) === 0 ? null : ($start . $this->join($list, $separator) . $end);
|
||||
}
|
||||
|
||||
public function length($maybeArray)
|
||||
{
|
||||
return $maybeArray ? count($maybeArray) : 0;
|
||||
}
|
||||
|
||||
public function join($maybeArray, $separator = '')
|
||||
{
|
||||
return $maybeArray
|
||||
? implode(
|
||||
$separator,
|
||||
Utils::filter(
|
||||
$maybeArray,
|
||||
static function ($x) {
|
||||
return (bool) $x;
|
||||
}
|
||||
)
|
||||
)
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a block string in the indented block form by adding a leading and
|
||||
* trailing blank line. However, if a block string starts with whitespace and is
|
||||
* a single-line, adding a leading blank line would strip that whitespace.
|
||||
*/
|
||||
private function printBlockString($value, $isDescription)
|
||||
{
|
||||
$escaped = str_replace('"""', '\\"""', $value);
|
||||
|
||||
return ($value[0] === ' ' || $value[0] === "\t") && strpos($value, "\n") === false
|
||||
? ('"""' . preg_replace('/"$/', "\"\n", $escaped) . '"""')
|
||||
: ('"""' . "\n" . ($isDescription ? $escaped : $this->indent($escaped)) . "\n" . '"""');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language;
|
||||
|
||||
use GraphQL\Utils\Utils;
|
||||
use function is_string;
|
||||
use function json_decode;
|
||||
use function mb_strlen;
|
||||
use function mb_substr;
|
||||
use function preg_match_all;
|
||||
use const PREG_OFFSET_CAPTURE;
|
||||
|
||||
class Source
|
||||
{
|
||||
/** @var string */
|
||||
public $body;
|
||||
|
||||
/** @var int */
|
||||
public $length;
|
||||
|
||||
/** @var string */
|
||||
public $name;
|
||||
|
||||
/** @var SourceLocation */
|
||||
public $locationOffset;
|
||||
|
||||
/**
|
||||
* A representation of source input to GraphQL.
|
||||
* `name` and `locationOffset` are optional. They are useful for clients who
|
||||
* store GraphQL documents in source files; for example, if the GraphQL input
|
||||
* starts at line 40 in a file named Foo.graphql, it might be useful for name to
|
||||
* be "Foo.graphql" and location to be `{ line: 40, column: 0 }`.
|
||||
* line and column in locationOffset are 1-indexed
|
||||
*
|
||||
* @param string $body
|
||||
* @param string|null $name
|
||||
*/
|
||||
public function __construct($body, $name = null, ?SourceLocation $location = null)
|
||||
{
|
||||
Utils::invariant(
|
||||
is_string($body),
|
||||
'GraphQL query body is expected to be string, but got ' . Utils::getVariableType($body)
|
||||
);
|
||||
|
||||
$this->body = $body;
|
||||
$this->length = mb_strlen($body, 'UTF-8');
|
||||
$this->name = $name ?: 'GraphQL request';
|
||||
$this->locationOffset = $location ?: new SourceLocation(1, 1);
|
||||
|
||||
Utils::invariant(
|
||||
$this->locationOffset->line > 0,
|
||||
'line in locationOffset is 1-indexed and must be positive'
|
||||
);
|
||||
Utils::invariant(
|
||||
$this->locationOffset->column > 0,
|
||||
'column in locationOffset is 1-indexed and must be positive'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $position
|
||||
*
|
||||
* @return SourceLocation
|
||||
*/
|
||||
public function getLocation($position)
|
||||
{
|
||||
$line = 1;
|
||||
$column = $position + 1;
|
||||
|
||||
$utfChars = json_decode('"\u2028\u2029"');
|
||||
$lineRegexp = '/\r\n|[\n\r' . $utfChars . ']/su';
|
||||
$matches = [];
|
||||
preg_match_all($lineRegexp, mb_substr($this->body, 0, $position, 'UTF-8'), $matches, PREG_OFFSET_CAPTURE);
|
||||
|
||||
foreach ($matches[0] as $index => $match) {
|
||||
$line += 1;
|
||||
|
||||
$column = $position + 1 - ($match[1] + mb_strlen($match[0], 'UTF-8'));
|
||||
}
|
||||
|
||||
return new SourceLocation($line, $column);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
class SourceLocation implements JsonSerializable
|
||||
{
|
||||
/** @var int */
|
||||
public $line;
|
||||
|
||||
/** @var int */
|
||||
public $column;
|
||||
|
||||
/**
|
||||
* @param int $line
|
||||
* @param int $col
|
||||
*/
|
||||
public function __construct($line, $col)
|
||||
{
|
||||
$this->line = $line;
|
||||
$this->column = $col;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'line' => $this->line,
|
||||
'column' => $this->column,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function toSerializableArray()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toSerializableArray();
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language;
|
||||
|
||||
/**
|
||||
* Represents a range of characters represented by a lexical token
|
||||
* within a Source.
|
||||
*/
|
||||
class Token
|
||||
{
|
||||
// Each kind of token.
|
||||
const SOF = '<SOF>';
|
||||
const EOF = '<EOF>';
|
||||
const BANG = '!';
|
||||
const DOLLAR = '$';
|
||||
const AMP = '&';
|
||||
const PAREN_L = '(';
|
||||
const PAREN_R = ')';
|
||||
const SPREAD = '...';
|
||||
const COLON = ':';
|
||||
const EQUALS = '=';
|
||||
const AT = '@';
|
||||
const BRACKET_L = '[';
|
||||
const BRACKET_R = ']';
|
||||
const BRACE_L = '{';
|
||||
const PIPE = '|';
|
||||
const BRACE_R = '}';
|
||||
const NAME = 'Name';
|
||||
const INT = 'Int';
|
||||
const FLOAT = 'Float';
|
||||
const STRING = 'String';
|
||||
const BLOCK_STRING = 'BlockString';
|
||||
const COMMENT = 'Comment';
|
||||
|
||||
/**
|
||||
* The kind of Token (see one of constants above).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $kind;
|
||||
|
||||
/**
|
||||
* The character offset at which this Node begins.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $start;
|
||||
|
||||
/**
|
||||
* The character offset at which this Node ends.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $end;
|
||||
|
||||
/**
|
||||
* The 1-indexed line number on which this Token appears.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $line;
|
||||
|
||||
/**
|
||||
* The 1-indexed column number at which this Token begins.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $column;
|
||||
|
||||
/** @var string|null */
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* Tokens exist as nodes in a double-linked-list amongst all tokens
|
||||
* including ignored tokens. <SOF> is always the first node and <EOF>
|
||||
* the last.
|
||||
*
|
||||
* @var Token
|
||||
*/
|
||||
public $prev;
|
||||
|
||||
/** @var Token */
|
||||
public $next;
|
||||
|
||||
/**
|
||||
* @param string $kind
|
||||
* @param int $start
|
||||
* @param int $end
|
||||
* @param int $line
|
||||
* @param int $column
|
||||
* @param mixed|null $value
|
||||
*/
|
||||
public function __construct($kind, $start, $end, $line, $column, ?Token $previous = null, $value = null)
|
||||
{
|
||||
$this->kind = $kind;
|
||||
$this->start = $start;
|
||||
$this->end = $end;
|
||||
$this->line = $line;
|
||||
$this->column = $column;
|
||||
$this->prev = $previous;
|
||||
$this->next = null;
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->kind . ($this->value ? ' "' . $this->value . '"' : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return (string|int|null)[]
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'kind' => $this->kind,
|
||||
'value' => $this->value,
|
||||
'line' => $this->line,
|
||||
'column' => $this->column,
|
||||
];
|
||||
}
|
||||
}
|
||||
+543
@@ -0,0 +1,543 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language;
|
||||
|
||||
use ArrayObject;
|
||||
use Exception;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\NodeList;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use SplFixedArray;
|
||||
use stdClass;
|
||||
use function array_pop;
|
||||
use function array_splice;
|
||||
use function call_user_func;
|
||||
use function call_user_func_array;
|
||||
use function count;
|
||||
use function func_get_args;
|
||||
use function is_array;
|
||||
use function is_callable;
|
||||
use function json_encode;
|
||||
|
||||
/**
|
||||
* Utility for efficient AST traversal and modification.
|
||||
*
|
||||
* `visit()` will walk through an AST using a depth first traversal, calling
|
||||
* the visitor's enter function at each node in the traversal, and calling the
|
||||
* leave function after visiting that node and all of it's child nodes.
|
||||
*
|
||||
* By returning different values from the enter and leave functions, the
|
||||
* behavior of the visitor can be altered, including skipping over a sub-tree of
|
||||
* the AST (by returning false), editing the AST by returning a value or null
|
||||
* to remove the value, or to stop the whole traversal by returning BREAK.
|
||||
*
|
||||
* When using `visit()` to edit an AST, the original AST will not be modified, and
|
||||
* a new version of the AST with the changes applied will be returned from the
|
||||
* visit function.
|
||||
*
|
||||
* $editedAST = Visitor::visit($ast, [
|
||||
* 'enter' => function ($node, $key, $parent, $path, $ancestors) {
|
||||
* // return
|
||||
* // null: no action
|
||||
* // Visitor::skipNode(): skip visiting this node
|
||||
* // Visitor::stop(): stop visiting altogether
|
||||
* // Visitor::removeNode(): delete this node
|
||||
* // any value: replace this node with the returned value
|
||||
* },
|
||||
* 'leave' => function ($node, $key, $parent, $path, $ancestors) {
|
||||
* // return
|
||||
* // null: no action
|
||||
* // Visitor::stop(): stop visiting altogether
|
||||
* // Visitor::removeNode(): delete this node
|
||||
* // any value: replace this node with the returned value
|
||||
* }
|
||||
* ]);
|
||||
*
|
||||
* Alternatively to providing enter() and leave() functions, a visitor can
|
||||
* instead provide functions named the same as the [kinds of AST nodes](reference.md#graphqllanguageastnodekind),
|
||||
* or enter/leave visitors at a named key, leading to four permutations of
|
||||
* visitor API:
|
||||
*
|
||||
* 1) Named visitors triggered when entering a node a specific kind.
|
||||
*
|
||||
* Visitor::visit($ast, [
|
||||
* 'Kind' => function ($node) {
|
||||
* // enter the "Kind" node
|
||||
* }
|
||||
* ]);
|
||||
*
|
||||
* 2) Named visitors that trigger upon entering and leaving a node of
|
||||
* a specific kind.
|
||||
*
|
||||
* Visitor::visit($ast, [
|
||||
* 'Kind' => [
|
||||
* 'enter' => function ($node) {
|
||||
* // enter the "Kind" node
|
||||
* }
|
||||
* 'leave' => function ($node) {
|
||||
* // leave the "Kind" node
|
||||
* }
|
||||
* ]
|
||||
* ]);
|
||||
*
|
||||
* 3) Generic visitors that trigger upon entering and leaving any node.
|
||||
*
|
||||
* Visitor::visit($ast, [
|
||||
* 'enter' => function ($node) {
|
||||
* // enter any node
|
||||
* },
|
||||
* 'leave' => function ($node) {
|
||||
* // leave any node
|
||||
* }
|
||||
* ]);
|
||||
*
|
||||
* 4) Parallel visitors for entering and leaving nodes of a specific kind.
|
||||
*
|
||||
* Visitor::visit($ast, [
|
||||
* 'enter' => [
|
||||
* 'Kind' => function($node) {
|
||||
* // enter the "Kind" node
|
||||
* }
|
||||
* },
|
||||
* 'leave' => [
|
||||
* 'Kind' => function ($node) {
|
||||
* // leave the "Kind" node
|
||||
* }
|
||||
* ]
|
||||
* ]);
|
||||
*/
|
||||
class Visitor
|
||||
{
|
||||
/** @var string[][] */
|
||||
public static $visitorKeys = [
|
||||
NodeKind::NAME => [],
|
||||
NodeKind::DOCUMENT => ['definitions'],
|
||||
NodeKind::OPERATION_DEFINITION => ['name', 'variableDefinitions', 'directives', 'selectionSet'],
|
||||
NodeKind::VARIABLE_DEFINITION => ['variable', 'type', 'defaultValue'],
|
||||
NodeKind::VARIABLE => ['name'],
|
||||
NodeKind::SELECTION_SET => ['selections'],
|
||||
NodeKind::FIELD => ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
|
||||
NodeKind::ARGUMENT => ['name', 'value'],
|
||||
NodeKind::FRAGMENT_SPREAD => ['name', 'directives'],
|
||||
NodeKind::INLINE_FRAGMENT => ['typeCondition', 'directives', 'selectionSet'],
|
||||
NodeKind::FRAGMENT_DEFINITION => [
|
||||
'name',
|
||||
// Note: fragment variable definitions are experimental and may be changed
|
||||
// or removed in the future.
|
||||
'variableDefinitions',
|
||||
'typeCondition',
|
||||
'directives',
|
||||
'selectionSet',
|
||||
],
|
||||
|
||||
NodeKind::INT => [],
|
||||
NodeKind::FLOAT => [],
|
||||
NodeKind::STRING => [],
|
||||
NodeKind::BOOLEAN => [],
|
||||
NodeKind::NULL => [],
|
||||
NodeKind::ENUM => [],
|
||||
NodeKind::LST => ['values'],
|
||||
NodeKind::OBJECT => ['fields'],
|
||||
NodeKind::OBJECT_FIELD => ['name', 'value'],
|
||||
NodeKind::DIRECTIVE => ['name', 'arguments'],
|
||||
NodeKind::NAMED_TYPE => ['name'],
|
||||
NodeKind::LIST_TYPE => ['type'],
|
||||
NodeKind::NON_NULL_TYPE => ['type'],
|
||||
|
||||
NodeKind::SCHEMA_DEFINITION => ['directives', 'operationTypes'],
|
||||
NodeKind::OPERATION_TYPE_DEFINITION => ['type'],
|
||||
NodeKind::SCALAR_TYPE_DEFINITION => ['description', 'name', 'directives'],
|
||||
NodeKind::OBJECT_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'],
|
||||
NodeKind::FIELD_DEFINITION => ['description', 'name', 'arguments', 'type', 'directives'],
|
||||
NodeKind::INPUT_VALUE_DEFINITION => ['description', 'name', 'type', 'defaultValue', 'directives'],
|
||||
NodeKind::INTERFACE_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'],
|
||||
NodeKind::UNION_TYPE_DEFINITION => ['description', 'name', 'directives', 'types'],
|
||||
NodeKind::ENUM_TYPE_DEFINITION => ['description', 'name', 'directives', 'values'],
|
||||
NodeKind::ENUM_VALUE_DEFINITION => ['description', 'name', 'directives'],
|
||||
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'],
|
||||
|
||||
NodeKind::SCALAR_TYPE_EXTENSION => ['name', 'directives'],
|
||||
NodeKind::OBJECT_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'],
|
||||
NodeKind::INTERFACE_TYPE_EXTENSION => ['name', 'directives', 'fields'],
|
||||
NodeKind::UNION_TYPE_EXTENSION => ['name', 'directives', 'types'],
|
||||
NodeKind::ENUM_TYPE_EXTENSION => ['name', 'directives', 'values'],
|
||||
NodeKind::INPUT_OBJECT_TYPE_EXTENSION => ['name', 'directives', 'fields'],
|
||||
|
||||
NodeKind::DIRECTIVE_DEFINITION => ['description', 'name', 'arguments', 'locations'],
|
||||
|
||||
NodeKind::SCHEMA_EXTENSION => ['directives', 'operationTypes'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Visit the AST (see class description for details)
|
||||
*
|
||||
* @param Node|ArrayObject|stdClass $root
|
||||
* @param callable[] $visitor
|
||||
* @param mixed[]|null $keyMap
|
||||
*
|
||||
* @return Node|mixed
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function visit($root, $visitor, $keyMap = null)
|
||||
{
|
||||
$visitorKeys = $keyMap ?: self::$visitorKeys;
|
||||
|
||||
$stack = null;
|
||||
$inArray = $root instanceof NodeList || is_array($root);
|
||||
$keys = [$root];
|
||||
$index = -1;
|
||||
$edits = [];
|
||||
$parent = null;
|
||||
$path = [];
|
||||
$ancestors = [];
|
||||
$newRoot = $root;
|
||||
|
||||
$UNDEFINED = null;
|
||||
|
||||
do {
|
||||
$index++;
|
||||
$isLeaving = $index === count($keys);
|
||||
$key = null;
|
||||
$node = null;
|
||||
$isEdited = $isLeaving && count($edits) !== 0;
|
||||
|
||||
if ($isLeaving) {
|
||||
$key = ! $ancestors ? $UNDEFINED : $path[count($path) - 1];
|
||||
$node = $parent;
|
||||
$parent = array_pop($ancestors);
|
||||
|
||||
if ($isEdited) {
|
||||
if ($inArray) {
|
||||
// $node = $node; // arrays are value types in PHP
|
||||
if ($node instanceof NodeList) {
|
||||
$node = clone $node;
|
||||
}
|
||||
} else {
|
||||
$node = clone $node;
|
||||
}
|
||||
$editOffset = 0;
|
||||
for ($ii = 0; $ii < count($edits); $ii++) {
|
||||
$editKey = $edits[$ii][0];
|
||||
$editValue = $edits[$ii][1];
|
||||
|
||||
if ($inArray) {
|
||||
$editKey -= $editOffset;
|
||||
}
|
||||
if ($inArray && $editValue === null) {
|
||||
if ($node instanceof NodeList) {
|
||||
$node->splice($editKey, 1);
|
||||
} else {
|
||||
array_splice($node, $editKey, 1);
|
||||
}
|
||||
$editOffset++;
|
||||
} else {
|
||||
if ($node instanceof NodeList || is_array($node)) {
|
||||
$node[$editKey] = $editValue;
|
||||
} else {
|
||||
$node->{$editKey} = $editValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$index = $stack['index'];
|
||||
$keys = $stack['keys'];
|
||||
$edits = $stack['edits'];
|
||||
$inArray = $stack['inArray'];
|
||||
$stack = $stack['prev'];
|
||||
} else {
|
||||
$key = $parent !== null ? ($inArray ? $index : $keys[$index]) : $UNDEFINED;
|
||||
$node = $parent !== null ? ($parent instanceof NodeList || is_array($parent) ? $parent[$key] : $parent->{$key}) : $newRoot;
|
||||
if ($node === null || $node === $UNDEFINED) {
|
||||
continue;
|
||||
}
|
||||
if ($parent !== null) {
|
||||
$path[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
$result = null;
|
||||
if (! $node instanceof NodeList && ! is_array($node)) {
|
||||
if (! ($node instanceof Node)) {
|
||||
throw new Exception('Invalid AST Node: ' . json_encode($node));
|
||||
}
|
||||
|
||||
$visitFn = self::getVisitFn($visitor, $node->kind, $isLeaving);
|
||||
|
||||
if ($visitFn) {
|
||||
$result = call_user_func($visitFn, $node, $key, $parent, $path, $ancestors);
|
||||
$editValue = null;
|
||||
|
||||
if ($result !== null) {
|
||||
if ($result instanceof VisitorOperation) {
|
||||
if ($result->doBreak) {
|
||||
break;
|
||||
}
|
||||
if (! $isLeaving && $result->doContinue) {
|
||||
array_pop($path);
|
||||
continue;
|
||||
}
|
||||
if ($result->removeNode) {
|
||||
$editValue = null;
|
||||
}
|
||||
} else {
|
||||
$editValue = $result;
|
||||
}
|
||||
|
||||
$edits[] = [$key, $editValue];
|
||||
if (! $isLeaving) {
|
||||
if (! ($editValue instanceof Node)) {
|
||||
array_pop($path);
|
||||
continue;
|
||||
}
|
||||
|
||||
$node = $editValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($result === null && $isEdited) {
|
||||
$edits[] = [$key, $node];
|
||||
}
|
||||
|
||||
if ($isLeaving) {
|
||||
array_pop($path);
|
||||
} else {
|
||||
$stack = [
|
||||
'inArray' => $inArray,
|
||||
'index' => $index,
|
||||
'keys' => $keys,
|
||||
'edits' => $edits,
|
||||
'prev' => $stack,
|
||||
];
|
||||
$inArray = $node instanceof NodeList || is_array($node);
|
||||
|
||||
$keys = ($inArray ? $node : $visitorKeys[$node->kind]) ?: [];
|
||||
$index = -1;
|
||||
$edits = [];
|
||||
if ($parent !== null) {
|
||||
$ancestors[] = $parent;
|
||||
}
|
||||
$parent = $node;
|
||||
}
|
||||
} while ($stack);
|
||||
|
||||
if (count($edits) !== 0) {
|
||||
$newRoot = $edits[0][1];
|
||||
}
|
||||
|
||||
return $newRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns marker for visitor break
|
||||
*
|
||||
* @return VisitorOperation
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function stop()
|
||||
{
|
||||
$r = new VisitorOperation();
|
||||
$r->doBreak = true;
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns marker for skipping current node
|
||||
*
|
||||
* @return VisitorOperation
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function skipNode()
|
||||
{
|
||||
$r = new VisitorOperation();
|
||||
$r->doContinue = true;
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns marker for removing a node
|
||||
*
|
||||
* @return VisitorOperation
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function removeNode()
|
||||
{
|
||||
$r = new VisitorOperation();
|
||||
$r->removeNode = true;
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable[][] $visitors
|
||||
*
|
||||
* @return callable[][]
|
||||
*/
|
||||
public static function visitInParallel($visitors)
|
||||
{
|
||||
$visitorsCount = count($visitors);
|
||||
$skipping = new SplFixedArray($visitorsCount);
|
||||
|
||||
return [
|
||||
'enter' => static function (Node $node) use ($visitors, $skipping, $visitorsCount) {
|
||||
for ($i = 0; $i < $visitorsCount; $i++) {
|
||||
if (! empty($skipping[$i])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fn = self::getVisitFn(
|
||||
$visitors[$i],
|
||||
$node->kind, /* isLeaving */
|
||||
false
|
||||
);
|
||||
|
||||
if (! $fn) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = call_user_func_array($fn, func_get_args());
|
||||
|
||||
if ($result instanceof VisitorOperation) {
|
||||
if ($result->doContinue) {
|
||||
$skipping[$i] = $node;
|
||||
} elseif ($result->doBreak) {
|
||||
$skipping[$i] = $result;
|
||||
} elseif ($result->removeNode) {
|
||||
return $result;
|
||||
}
|
||||
} elseif ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
},
|
||||
'leave' => static function (Node $node) use ($visitors, $skipping, $visitorsCount) {
|
||||
for ($i = 0; $i < $visitorsCount; $i++) {
|
||||
if (empty($skipping[$i])) {
|
||||
$fn = self::getVisitFn(
|
||||
$visitors[$i],
|
||||
$node->kind, /* isLeaving */
|
||||
true
|
||||
);
|
||||
|
||||
if ($fn) {
|
||||
$result = call_user_func_array($fn, func_get_args());
|
||||
if ($result instanceof VisitorOperation) {
|
||||
if ($result->doBreak) {
|
||||
$skipping[$i] = $result;
|
||||
} elseif ($result->removeNode) {
|
||||
return $result;
|
||||
}
|
||||
} elseif ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
} elseif ($skipping[$i] === $node) {
|
||||
$skipping[$i] = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new visitor instance which maintains a provided TypeInfo instance
|
||||
* along with visiting visitor.
|
||||
*/
|
||||
public static function visitWithTypeInfo(TypeInfo $typeInfo, $visitor)
|
||||
{
|
||||
return [
|
||||
'enter' => static function (Node $node) use ($typeInfo, $visitor) {
|
||||
$typeInfo->enter($node);
|
||||
$fn = self::getVisitFn($visitor, $node->kind, false);
|
||||
|
||||
if ($fn) {
|
||||
$result = call_user_func_array($fn, func_get_args());
|
||||
if ($result !== null) {
|
||||
$typeInfo->leave($node);
|
||||
if ($result instanceof Node) {
|
||||
$typeInfo->enter($result);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
'leave' => static function (Node $node) use ($typeInfo, $visitor) {
|
||||
$fn = self::getVisitFn($visitor, $node->kind, true);
|
||||
$result = $fn ? call_user_func_array($fn, func_get_args()) : null;
|
||||
$typeInfo->leave($node);
|
||||
|
||||
return $result;
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable[]|null $visitor
|
||||
* @param string $kind
|
||||
* @param bool $isLeaving
|
||||
*
|
||||
* @return callable|null
|
||||
*/
|
||||
public static function getVisitFn($visitor, $kind, $isLeaving)
|
||||
{
|
||||
if ($visitor === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$kindVisitor = $visitor[$kind] ?? null;
|
||||
|
||||
if (! $isLeaving && is_callable($kindVisitor)) {
|
||||
// { Kind() {} }
|
||||
return $kindVisitor;
|
||||
}
|
||||
|
||||
if (is_array($kindVisitor)) {
|
||||
if ($isLeaving) {
|
||||
$kindSpecificVisitor = $kindVisitor['leave'] ?? null;
|
||||
} else {
|
||||
$kindSpecificVisitor = $kindVisitor['enter'] ?? null;
|
||||
}
|
||||
|
||||
if ($kindSpecificVisitor && is_callable($kindSpecificVisitor)) {
|
||||
// { Kind: { enter() {}, leave() {} } }
|
||||
return $kindSpecificVisitor;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$visitor += ['leave' => null, 'enter' => null];
|
||||
|
||||
$specificVisitor = $isLeaving ? $visitor['leave'] : $visitor['enter'];
|
||||
|
||||
if ($specificVisitor) {
|
||||
if (is_callable($specificVisitor)) {
|
||||
// { enter() {}, leave() {} }
|
||||
return $specificVisitor;
|
||||
}
|
||||
$specificKindVisitor = $specificVisitor[$kind] ?? null;
|
||||
|
||||
if (is_callable($specificKindVisitor)) {
|
||||
// { enter: { Kind() {} }, leave: { Kind() {} } }
|
||||
return $specificKindVisitor;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language;
|
||||
|
||||
class VisitorOperation
|
||||
{
|
||||
/** @var bool */
|
||||
public $doBreak;
|
||||
|
||||
/** @var bool */
|
||||
public $doContinue;
|
||||
|
||||
/** @var bool */
|
||||
public $removeNode;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL;
|
||||
|
||||
use function trigger_error;
|
||||
use const E_USER_DEPRECATED;
|
||||
|
||||
trigger_error(
|
||||
'GraphQL\Schema is moved to GraphQL\Type\Schema',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
|
||||
/**
|
||||
* Schema Definition
|
||||
*
|
||||
* @deprecated moved to GraphQL\Type\Schema
|
||||
*/
|
||||
class Schema extends \GraphQL\Type\Schema
|
||||
{
|
||||
}
|
||||
+591
@@ -0,0 +1,591 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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 JsonSerializable;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use function file_get_contents;
|
||||
use function header;
|
||||
use function is_array;
|
||||
use function is_callable;
|
||||
use function is_string;
|
||||
use function json_decode;
|
||||
use function json_encode;
|
||||
use function json_last_error;
|
||||
use function json_last_error_msg;
|
||||
use function sprintf;
|
||||
use function stripos;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return OperationParams|OperationParams[]
|
||||
*
|
||||
* @throws RequestError
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function parseHttpRequest(?callable $readRawBodyFn = null)
|
||||
{
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? null;
|
||||
$bodyParams = [];
|
||||
$urlParams = $_GET;
|
||||
|
||||
if ($method === 'POST') {
|
||||
$contentType = $_SERVER['CONTENT_TYPE'] ?? null;
|
||||
|
||||
if ($contentType === null) {
|
||||
throw new RequestError('Missing "Content-Type" header');
|
||||
}
|
||||
|
||||
if (stripos($contentType, 'application/graphql') !== false) {
|
||||
$rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody();
|
||||
$bodyParams = ['query' => $rawBody ?: ''];
|
||||
} elseif (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)
|
||||
);
|
||||
}
|
||||
} elseif (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
|
||||
$bodyParams = $_POST;
|
||||
} elseif (stripos($contentType, 'multipart/form-data') !== false) {
|
||||
$bodyParams = $_POST;
|
||||
} 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)
|
||||
*
|
||||
* @param string $method
|
||||
* @param mixed[] $bodyParams
|
||||
* @param mixed[] $queryParams
|
||||
*
|
||||
* @return OperationParams|OperationParams[]
|
||||
*
|
||||
* @throws RequestError
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function parseRequestParams($method, array $bodyParams, array $queryParams)
|
||||
{
|
||||
if ($method === 'GET') {
|
||||
$result = OperationParams::create($queryParams, true);
|
||||
} elseif ($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)
|
||||
*
|
||||
* @return Error[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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)
|
||||
*
|
||||
* @return ExecutionResult|Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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)
|
||||
*
|
||||
* @param OperationParams[] $operations
|
||||
*
|
||||
* @return ExecutionResult|ExecutionResult[]|Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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 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,
|
||||
static 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 ($operationType !== 'query' && $op->isReadOnly()) {
|
||||
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 = static 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*
|
||||
* @throws RequestError
|
||||
*/
|
||||
private function loadPersistedQuery(ServerConfig $config, OperationParams $operationParams)
|
||||
{
|
||||
// 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($operationParams->queryId, $operationParams);
|
||||
|
||||
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 string $operationType
|
||||
*
|
||||
* @return mixed[]|null
|
||||
*/
|
||||
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 string $operationType
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function resolveRootValue(ServerConfig $config, OperationParams $params, DocumentNode $doc, $operationType)
|
||||
{
|
||||
$root = $config->getRootValue();
|
||||
|
||||
if (is_callable($root)) {
|
||||
$root = $root($params, $doc, $operationType);
|
||||
}
|
||||
|
||||
return $root;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $operationType
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function resolveContextValue(
|
||||
ServerConfig $config,
|
||||
OperationParams $params,
|
||||
DocumentNode $doc,
|
||||
$operationType
|
||||
) {
|
||||
$context = $config->getContext();
|
||||
|
||||
if (is_callable($context)) {
|
||||
$context = $context($params, $doc, $operationType);
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send response using standard PHP `header()` and `echo`.
|
||||
*
|
||||
* @param Promise|ExecutionResult|ExecutionResult[] $result
|
||||
* @param bool $exitWhenDone
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private function doSendResponse($result, $exitWhenDone)
|
||||
{
|
||||
$httpStatus = $this->resolveHttpStatus($result);
|
||||
$this->emitResponse($result, $httpStatus, $exitWhenDone);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[]|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 ExecutionResult|mixed[] $result
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function resolveHttpStatus($result)
|
||||
{
|
||||
if (is_array($result) && isset($result[0])) {
|
||||
Utils::each(
|
||||
$result,
|
||||
static 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[]
|
||||
*
|
||||
* @return OperationParams[]|OperationParams
|
||||
*
|
||||
* @throws RequestError
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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($contentType[0], 'application/graphql') !== false) {
|
||||
$bodyParams = ['query' => $request->getBody()->getContents()];
|
||||
} elseif (stripos($contentType[0], 'application/json') !== false) {
|
||||
$bodyParams = $request->getParsedBody();
|
||||
|
||||
if ($bodyParams === null) {
|
||||
throw new InvariantViolation(
|
||||
'PSR-7 request is expected to provide parsed body for "application/json" requests but got null'
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
*
|
||||
* @param Promise|ExecutionResult|ExecutionResult[] $result
|
||||
*
|
||||
* @return Promise|ResponseInterface
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Server;
|
||||
|
||||
use function array_change_key_case;
|
||||
use function is_string;
|
||||
use function json_decode;
|
||||
use function json_last_error;
|
||||
use const CASE_LOWER;
|
||||
|
||||
/**
|
||||
* 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 mixed[]|null
|
||||
*/
|
||||
public $variables;
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @var mixed[]|null
|
||||
*/
|
||||
public $extensions;
|
||||
|
||||
/** @var mixed[] */
|
||||
private $originalInput;
|
||||
|
||||
/** @var bool */
|
||||
private $readOnly;
|
||||
|
||||
/**
|
||||
* Creates an instance from given array
|
||||
*
|
||||
* @param mixed[] $params
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function create(array $params, bool $readonly = false) : OperationParams
|
||||
{
|
||||
$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
|
||||
'operationname' => null,
|
||||
'variables' => null,
|
||||
'extensions' => null,
|
||||
];
|
||||
|
||||
if ($params['variables'] === '') {
|
||||
$params['variables'] = null;
|
||||
}
|
||||
|
||||
// Some parameters could be provided as serialized JSON.
|
||||
foreach (['extensions', 'variables'] as $param) {
|
||||
if (! is_string($params[$param])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tmp = json_decode($params[$param], true);
|
||||
if (json_last_error()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$params[$param] = $tmp;
|
||||
}
|
||||
|
||||
$instance->query = $params['query'];
|
||||
$instance->queryId = $params['queryid'] ?: $params['documentid'] ?: $params['id'];
|
||||
$instance->operation = $params['operationname'];
|
||||
$instance->variables = $params['variables'];
|
||||
$instance->extensions = $params['extensions'];
|
||||
$instance->readOnly = $readonly;
|
||||
|
||||
// Apollo server/client compatibility: look for the queryid in extensions
|
||||
if (isset($instance->extensions['persistedQuery']['sha256Hash']) && empty($instance->query) && empty($instance->queryId)) {
|
||||
$instance->queryId = $instance->extensions['persistedQuery']['sha256Hash'];
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getOriginalInput($key)
|
||||
{
|
||||
return $this->originalInput[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that operation is executed in read-only context
|
||||
* (e.g. via HTTP GET request)
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function isReadOnly()
|
||||
{
|
||||
return $this->readOnly;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Server;
|
||||
|
||||
use Exception;
|
||||
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';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Server;
|
||||
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Executor\Promise\PromiseAdapter;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Validator\Rules\ValidationRule;
|
||||
use function is_array;
|
||||
use function is_callable;
|
||||
use function method_exists;
|
||||
use function sprintf;
|
||||
use function ucfirst;
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*
|
||||
* @param mixed[] $config
|
||||
*
|
||||
* @return ServerConfig
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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(sprintf('Unknown server config option "%s"', $key));
|
||||
}
|
||||
$instance->$method($value);
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/** @var Schema */
|
||||
private $schema;
|
||||
|
||||
/** @var mixed|callable */
|
||||
private $context;
|
||||
|
||||
/** @var mixed|callable */
|
||||
private $rootValue;
|
||||
|
||||
/** @var callable|null */
|
||||
private $errorFormatter;
|
||||
|
||||
/** @var callable|null */
|
||||
private $errorsHandler;
|
||||
|
||||
/** @var bool */
|
||||
private $debug = false;
|
||||
|
||||
/** @var bool */
|
||||
private $queryBatching = false;
|
||||
|
||||
/** @var ValidationRule[]|callable */
|
||||
private $validationRules;
|
||||
|
||||
/** @var callable */
|
||||
private $fieldResolver;
|
||||
|
||||
/** @var PromiseAdapter */
|
||||
private $promiseAdapter;
|
||||
|
||||
/** @var callable */
|
||||
private $persistentQueryLoader;
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function setSchema(Schema $schema)
|
||||
{
|
||||
$this->schema = $schema;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed|callable $context
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function setContext($context)
|
||||
{
|
||||
$this->context = $context;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed|callable $rootValue
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function setRootValue($rootValue)
|
||||
{
|
||||
$this->rootValue = $rootValue;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects function(Throwable $e) : array
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function setErrorFormatter(callable $errorFormatter)
|
||||
{
|
||||
$this->errorFormatter = $errorFormatter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects function(array $errors, callable $formatter) : array
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function setErrorsHandler(callable $handler)
|
||||
{
|
||||
$this->errorsHandler = $handler;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set validation rules for this server.
|
||||
*
|
||||
* @param ValidationRule[]|callable $validationRules
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param bool|int $set
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function setDebug($set = true)
|
||||
{
|
||||
$this->debug = $set;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow batching queries (disabled by default)
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function setQueryBatching(bool $enableBatching) : self
|
||||
{
|
||||
$this->queryBatching = $enableBatching;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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 ValidationRule[]|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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Server;
|
||||
|
||||
use GraphQL\Error\FormattedError;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Executor\ExecutionResult;
|
||||
use GraphQL\Executor\Promise\Promise;
|
||||
use GraphQL\Utils\Utils;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Throwable;
|
||||
use function is_array;
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*
|
||||
* @param Throwable $error
|
||||
* @param bool $debug
|
||||
* @param bool $exitWhenDone
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param ServerConfig|mixed[] $config
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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)
|
||||
*
|
||||
* @param OperationParams|OperationParams[] $parsedBody
|
||||
* @param bool $exitWhenDone
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param OperationParams|OperationParams[] $parsedBody
|
||||
*
|
||||
* @return ExecutionResult|ExecutionResult[]|Promise
|
||||
*
|
||||
* @throws InvariantViolation
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function executeRequest($parsedBody = null)
|
||||
{
|
||||
if ($parsedBody === null) {
|
||||
$parsedBody = $this->helper->parseHttpRequest();
|
||||
}
|
||||
|
||||
if (is_array($parsedBody)) {
|
||||
return $this->helper->executeBatch($this->config, $parsedBody);
|
||||
}
|
||||
|
||||
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).
|
||||
*
|
||||
* @return ResponseInterface|Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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)
|
||||
*
|
||||
* @return ExecutionResult|ExecutionResult[]|Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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)
|
||||
*
|
||||
* @return Helper
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getHelper()
|
||||
{
|
||||
return $this->helper;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
/*
|
||||
export type GraphQLAbstractType =
|
||||
GraphQLInterfaceType |
|
||||
GraphQLUnionType;
|
||||
*/
|
||||
|
||||
interface AbstractType
|
||||
{
|
||||
/**
|
||||
* Resolves concrete ObjectType for given object value
|
||||
*
|
||||
* @param object $objectValue
|
||||
* @param mixed[] $context
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function resolveType($objectValue, $context, ResolveInfo $info);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use Exception;
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\BooleanValueNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Utils\Utils;
|
||||
use function is_bool;
|
||||
|
||||
class BooleanType extends ScalarType
|
||||
{
|
||||
/** @var string */
|
||||
public $name = Type::BOOLEAN;
|
||||
|
||||
/** @var string */
|
||||
public $description = 'The `Boolean` scalar type represents `true` or `false`.';
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function serialize($value)
|
||||
{
|
||||
return (bool) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public function parseValue($value)
|
||||
{
|
||||
if (is_bool($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
throw new Error('Cannot represent value as boolean: ' . Utils::printSafe($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $valueNode
|
||||
* @param mixed[]|null $variables
|
||||
*
|
||||
* @return bool|null
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function parseLiteral($valueNode, ?array $variables = null)
|
||||
{
|
||||
if (! $valueNode instanceof BooleanValueNode) {
|
||||
// Intentionally without message, as all information already in wrapped Exception
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
return $valueNode->value;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user