Version 4.5.0
This commit is contained in:
1 parent
1a0c7fd3c2
commit
f79dcf067a
3791 files changed
+248032
-76372
No files matched your search
+61
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
namespace GraphQL;
|
||||
|
||||
use GraphQL\Executor\Promise\Adapter\SyncPromise;
|
||||
|
||||
class Deferred
|
||||
{
|
||||
/**
|
||||
* @var \SplQueue
|
||||
*/
|
||||
private static $queue;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $callback;
|
||||
|
||||
/**
|
||||
* @var SyncPromise
|
||||
*/
|
||||
public $promise;
|
||||
|
||||
public static function getQueue()
|
||||
{
|
||||
return self::$queue ?: self::$queue = new \SplQueue();
|
||||
}
|
||||
|
||||
public static function runQueue()
|
||||
{
|
||||
$q = self::$queue;
|
||||
while ($q && !$q->isEmpty()) {
|
||||
/** @var self $dfd */
|
||||
$dfd = $q->dequeue();
|
||||
$dfd->run();
|
||||
}
|
||||
}
|
||||
|
||||
public function __construct(callable $callback)
|
||||
{
|
||||
$this->callback = $callback;
|
||||
$this->promise = new SyncPromise();
|
||||
self::getQueue()->enqueue($this);
|
||||
}
|
||||
|
||||
public function then($onFulfilled = null, $onRejected = null)
|
||||
{
|
||||
return $this->promise->then($onFulfilled, $onRejected);
|
||||
}
|
||||
|
||||
private function run()
|
||||
{
|
||||
try {
|
||||
$cb = $this->callback;
|
||||
$this->promise->resolve($cb());
|
||||
} catch (\Exception $e) {
|
||||
$this->promise->reject($e);
|
||||
} catch (\Throwable $e) {
|
||||
$this->promise->reject($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
namespace GraphQL;
|
||||
|
||||
trigger_error(
|
||||
'GraphQL\Error was moved to GraphQL\Error\Error and will be deleted on next release',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Class Error
|
||||
*
|
||||
* @deprecated as of 8.0 in favor of GraphQL\Error\Error
|
||||
* @package GraphQL
|
||||
*/
|
||||
class Error extends \GraphQL\Error\Error
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
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.
|
||||
*
|
||||
* @api
|
||||
* @return bool
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @api
|
||||
* @return string
|
||||
*/
|
||||
public function getCategory();
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
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;
|
||||
}
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
namespace GraphQL\Error;
|
||||
|
||||
use GraphQL\Language\Source;
|
||||
use GraphQL\Language\SourceLocation;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* 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 array
|
||||
*/
|
||||
public $path;
|
||||
|
||||
/**
|
||||
* An array of GraphQL AST Nodes corresponding to this error.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $nodes;
|
||||
|
||||
/**
|
||||
* The source GraphQL document corresponding to this error.
|
||||
*
|
||||
* @var Source|null
|
||||
*/
|
||||
private $source;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $positions;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isClientSafe;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $category;
|
||||
|
||||
/**
|
||||
* 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 $error
|
||||
* @param array|null $nodes
|
||||
* @param array|null $path
|
||||
* @return Error
|
||||
*/
|
||||
public static function createLocatedError($error, $nodes = null, $path = null)
|
||||
{
|
||||
if ($error instanceof self) {
|
||||
if ($error->path && $error->nodes) {
|
||||
return $error;
|
||||
} else {
|
||||
$nodes = $nodes ?: $error->nodes;
|
||||
$path = $path ?: $error->path;
|
||||
}
|
||||
}
|
||||
|
||||
$source = $positions = $originalError = null;
|
||||
|
||||
if ($error instanceof self) {
|
||||
$message = $error->getMessage();
|
||||
$originalError = $error;
|
||||
$nodes = $error->nodes ?: $nodes;
|
||||
$source = $error->source;
|
||||
$positions = $error->positions;
|
||||
} else if ($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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Error $error
|
||||
* @return array
|
||||
*/
|
||||
public static function formatError(Error $error)
|
||||
{
|
||||
return $error->toSerializableArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @param array|null $nodes
|
||||
* @param Source $source
|
||||
* @param array|null $positions
|
||||
* @param array|null $path
|
||||
* @param \Throwable $previous
|
||||
*/
|
||||
public function __construct(
|
||||
$message,
|
||||
$nodes = null,
|
||||
Source $source = null,
|
||||
$positions = null,
|
||||
$path = null,
|
||||
$previous = null
|
||||
)
|
||||
{
|
||||
parent::__construct($message, 0, $previous);
|
||||
|
||||
if ($nodes instanceof \Traversable) {
|
||||
$nodes = iterator_to_array($nodes);
|
||||
}
|
||||
|
||||
$this->nodes = $nodes;
|
||||
$this->source = $source;
|
||||
$this->positions = $positions;
|
||||
$this->path = $path;
|
||||
|
||||
if ($previous instanceof ClientAware) {
|
||||
$this->isClientSafe = $previous->isClientSafe();
|
||||
$this->category = $previous->getCategory() ?: static::CATEGORY_INTERNAL;
|
||||
} else if ($previous) {
|
||||
$this->isClientSafe = false;
|
||||
$this->category = static::CATEGORY_INTERNAL;
|
||||
} else {
|
||||
$this->isClientSafe = true;
|
||||
$this->category = static::CATEGORY_GRAPHQL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function isClientSafe()
|
||||
{
|
||||
return $this->isClientSafe;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Source|null
|
||||
*/
|
||||
public function getSource()
|
||||
{
|
||||
if (null === $this->source) {
|
||||
if (!empty($this->nodes[0]) && !empty($this->nodes[0]->loc)) {
|
||||
$this->source = $this->nodes[0]->loc->source;
|
||||
}
|
||||
}
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getPositions()
|
||||
{
|
||||
if (null === $this->positions) {
|
||||
if (!empty($this->nodes)) {
|
||||
$positions = array_map(function($node) {
|
||||
return isset($node->loc) ? $node->loc->start : null;
|
||||
}, $this->nodes);
|
||||
$this->positions = array_filter($positions, function($p) {
|
||||
return $p !== null;
|
||||
});
|
||||
}
|
||||
}
|
||||
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.
|
||||
*
|
||||
* @api
|
||||
* @return SourceLocation[]
|
||||
*/
|
||||
public function getLocations()
|
||||
{
|
||||
if (null === $this->locations) {
|
||||
$positions = $this->getPositions();
|
||||
$source = $this->getSource();
|
||||
|
||||
if ($positions && $source) {
|
||||
$this->locations = array_map(function ($pos) use ($source) {
|
||||
return $source->getLocation($pos);
|
||||
}, $positions);
|
||||
} else {
|
||||
$this->locations = [];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->locations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array describing the path from the root value to the field which produced this error.
|
||||
* Only included for execution errors.
|
||||
*
|
||||
* @api
|
||||
* @return array|null
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array representation of error suitable for serialization
|
||||
*
|
||||
* @deprecated Use FormattedError::createFromException() instead
|
||||
* @return array
|
||||
*/
|
||||
public function toSerializableArray()
|
||||
{
|
||||
$arr = [
|
||||
'message' => $this->getMessage()
|
||||
];
|
||||
|
||||
$locations = Utils::map($this->getLocations(), function(SourceLocation $loc) {
|
||||
return $loc->toSerializableArray();
|
||||
});
|
||||
|
||||
if (!empty($locations)) {
|
||||
$arr['locations'] = $locations;
|
||||
}
|
||||
if (!empty($this->path)) {
|
||||
$arr['path'] = $this->path;
|
||||
}
|
||||
|
||||
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.
|
||||
* @since 5.4.0
|
||||
*/
|
||||
function jsonSerialize()
|
||||
{
|
||||
return $this->toSerializableArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
namespace GraphQL\Error;
|
||||
|
||||
use GraphQL\Language\SourceLocation;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\WrappingType;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
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()`.
|
||||
*
|
||||
* @api
|
||||
* @param string $msg
|
||||
*/
|
||||
public static function setInternalErrorMessage($msg)
|
||||
{
|
||||
self::$internalErrorMessage = $msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @api
|
||||
* @param \Throwable $e
|
||||
* @param bool|int $debug
|
||||
* @param string $internalErrorMessage
|
||||
* @return array
|
||||
* @throws \Throwable
|
||||
*/
|
||||
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,
|
||||
'category' => $e->getCategory()
|
||||
];
|
||||
} else {
|
||||
$formattedError = [
|
||||
'message' => $internalErrorMessage,
|
||||
'category' => Error::CATEGORY_INTERNAL
|
||||
];
|
||||
}
|
||||
|
||||
if ($e instanceof Error) {
|
||||
$locations = Utils::map($e->getLocations(), function(SourceLocation $loc) {
|
||||
return $loc->toSerializableArray();
|
||||
});
|
||||
|
||||
if (!empty($locations)) {
|
||||
$formattedError['locations'] = $locations;
|
||||
}
|
||||
if (!empty($e->path)) {
|
||||
$formattedError['path'] = $e->path;
|
||||
}
|
||||
}
|
||||
|
||||
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 array $formattedError
|
||||
* @param \Throwable $e
|
||||
* @param bool $debug
|
||||
* @return array
|
||||
* @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;
|
||||
} else if ($e->getPrevious()) {
|
||||
throw $e->getPrevious();
|
||||
}
|
||||
}
|
||||
|
||||
$isInternal = !$e instanceof ClientAware || !$e->isClientSafe();
|
||||
|
||||
if (($debug & Debug::INCLUDE_DEBUG_MESSAGE) && $isInternal) {
|
||||
// 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 callable|null $formatter
|
||||
* @param $debug
|
||||
* @return callable|\Closure
|
||||
*/
|
||||
public static function prepareFormatter(callable $formatter = null, $debug)
|
||||
{
|
||||
$formatter = $formatter ?: function($e) {
|
||||
return FormattedError::createFromException($e);
|
||||
};
|
||||
if ($debug) {
|
||||
$formatter = function($e) use ($formatter, $debug) {
|
||||
return FormattedError::addDebugEntries($formatter($e), $e, $debug);
|
||||
};
|
||||
}
|
||||
return $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns error trace as serializable array
|
||||
*
|
||||
* @api
|
||||
* @param \Throwable $error
|
||||
* @return array
|
||||
*/
|
||||
public static function toSafeTrace($error)
|
||||
{
|
||||
$trace = $error->getTrace();
|
||||
|
||||
// Remove invariant entries as they don't provide much value:
|
||||
if (
|
||||
isset($trace[0]['function']) && isset($trace[0]['class']) &&
|
||||
('GraphQL\Utils\Utils::invariant' === $trace[0]['class'].'::'.$trace[0]['function'])) {
|
||||
array_shift($trace);
|
||||
}
|
||||
|
||||
// Remove root call as it's likely error handler trace:
|
||||
else if (!isset($trace[0]['file'])) {
|
||||
array_shift($trace);
|
||||
}
|
||||
|
||||
return array_map(function($err) {
|
||||
$safeErr = array_intersect_key($err, ['file' => true, 'line' => true]);
|
||||
|
||||
if (isset($err['function'])) {
|
||||
$func = $err['function'];
|
||||
$args = !empty($err['args']) ? array_map([__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 $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 (null === $var) {
|
||||
return 'null';
|
||||
}
|
||||
return gettype($var);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated as of v0.8.0
|
||||
* @param $error
|
||||
* @param SourceLocation[] $locations
|
||||
* @return array
|
||||
*/
|
||||
public static function create($error, array $locations = [])
|
||||
{
|
||||
$formatted = [
|
||||
'message' => $error
|
||||
];
|
||||
|
||||
if (!empty($locations)) {
|
||||
$formatted['locations'] = array_map(function($loc) { return $loc->toArray();}, $locations);
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ErrorException $e
|
||||
* @deprecated as of v0.10.0, use general purpose method createFromException() instead
|
||||
* @return array
|
||||
*/
|
||||
public static function createFromPHPError(\ErrorException $e)
|
||||
{
|
||||
return [
|
||||
'message' => $e->getMessage(),
|
||||
'severity' => $e->getSeverity(),
|
||||
'trace' => self::toSafeTrace($e)
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace GraphQL\Error;
|
||||
|
||||
/**
|
||||
* Class InvariantVoilation
|
||||
*
|
||||
* Note:
|
||||
* This exception should not inherit base Error exception as it is raised when there is an error somewhere in
|
||||
* user-land code
|
||||
*
|
||||
* @package GraphQL\Error
|
||||
*/
|
||||
class InvariantViolation extends \LogicException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
namespace GraphQL\Error;
|
||||
|
||||
use GraphQL\Language\Source;
|
||||
use GraphQL\Language\SourceLocation;
|
||||
|
||||
class SyntaxError extends Error
|
||||
{
|
||||
/**
|
||||
* @param Source $source
|
||||
* @param int $position
|
||||
* @param string $description
|
||||
*/
|
||||
public function __construct(Source $source, $position, $description)
|
||||
{
|
||||
$location = $source->getLocation($position);
|
||||
$line = $location->line + $source->locationOffset->line - 1;
|
||||
$columnOffset = self::getColumnOffset($source, $location);
|
||||
$column = $location->column + $columnOffset;
|
||||
|
||||
$syntaxError =
|
||||
"Syntax Error {$source->name} ({$line}:{$column}) $description\n" .
|
||||
"\n".
|
||||
self::highlightSourceAtLocation($source, $location);
|
||||
|
||||
parent::__construct($syntaxError, null, $source, [$position]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Source $source
|
||||
* @param SourceLocation $location
|
||||
* @return string
|
||||
*/
|
||||
public static function highlightSourceAtLocation(Source $source, SourceLocation $location)
|
||||
{
|
||||
$line = $location->line;
|
||||
$lineOffset = $source->locationOffset->line - 1;
|
||||
$columnOffset = self::getColumnOffset($source, $location);
|
||||
|
||||
$contextLine = $line + $lineOffset;
|
||||
$prevLineNum = (string) ($contextLine - 1);
|
||||
$lineNum = (string) $contextLine;
|
||||
$nextLineNum = (string) ($contextLine + 1);
|
||||
$padLen = mb_strlen($nextLineNum, 'UTF-8');
|
||||
|
||||
$unicodeChars = json_decode('"\u2028\u2029"'); // Quick hack to get js-compatible representation of these chars
|
||||
$lines = preg_split('/\r\n|[\n\r' . $unicodeChars . ']/su', $source->body);
|
||||
|
||||
$whitespace = function ($len) {
|
||||
return str_repeat(' ', $len);
|
||||
};
|
||||
|
||||
$lpad = function ($len, $str) {
|
||||
return str_pad($str, $len - mb_strlen($str, 'UTF-8') + 1, ' ', STR_PAD_LEFT);
|
||||
};
|
||||
|
||||
$lines[0] = $whitespace($source->locationOffset->column - 1) . $lines[0];
|
||||
|
||||
return
|
||||
($line >= 2 ? $lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2] . "\n" : '') .
|
||||
($lpad($padLen, $lineNum) . ': ' . $lines[$line - 1] . "\n") .
|
||||
($whitespace(2 + $padLen + $location->column - 1 + $columnOffset) . "^\n") .
|
||||
($line < count($lines) ? $lpad($padLen, $nextLineNum) . ': ' . $lines[$line] . "\n" : '');
|
||||
}
|
||||
|
||||
public static function getColumnOffset(Source $source, SourceLocation $location)
|
||||
{
|
||||
return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace GraphQL\Error;
|
||||
|
||||
/**
|
||||
* Class UserError
|
||||
*
|
||||
* Error caused by actions of GraphQL clients. Can be safely displayed to a client...
|
||||
*
|
||||
* @package GraphQL\Error
|
||||
*/
|
||||
class UserError extends \RuntimeException implements ClientAware
|
||||
{
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isClientSafe()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
return 'user';
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
namespace GraphQL\Error;
|
||||
|
||||
/**
|
||||
* 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_NAME = 1;
|
||||
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;
|
||||
|
||||
static $enableWarnings = self::ALL;
|
||||
|
||||
static $warned = [];
|
||||
|
||||
static private $warningHandler;
|
||||
|
||||
/**
|
||||
* Sets warning handler which can intercept all system warnings.
|
||||
* When not set, trigger_error() is used to notify about warnings.
|
||||
*
|
||||
* @api
|
||||
* @param callable|null $warningHandler
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @api
|
||||
* @param bool|int $suppress
|
||||
*/
|
||||
static function suppress($suppress = true)
|
||||
{
|
||||
if (true === $suppress) {
|
||||
self::$enableWarnings = 0;
|
||||
} else if (false === $suppress) {
|
||||
self::$enableWarnings = self::ALL;
|
||||
} else {
|
||||
$suppress = (int) $suppress;
|
||||
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.
|
||||
*
|
||||
* @api
|
||||
* @param bool|int $enable
|
||||
*/
|
||||
public static function enable($enable = true)
|
||||
{
|
||||
if (true === $enable) {
|
||||
self::$enableWarnings = self::ALL;
|
||||
} else if (false === $enable) {
|
||||
self::$enableWarnings = 0;
|
||||
} else {
|
||||
$enable = (int) $enable;
|
||||
self::$enableWarnings |= $enable;
|
||||
}
|
||||
}
|
||||
|
||||
static function warnOnce($errorMessage, $warningId, $messageLevel = null)
|
||||
{
|
||||
if (self::$warningHandler) {
|
||||
$fn = self::$warningHandler;
|
||||
$fn($errorMessage, $warningId);
|
||||
} else if ((self::$enableWarnings & $warningId) > 0 && !isset(self::$warned[$warningId])) {
|
||||
self::$warned[$warningId] = true;
|
||||
trigger_error($errorMessage, $messageLevel ?: E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
static function warn($errorMessage, $warningId, $messageLevel = null)
|
||||
{
|
||||
if (self::$warningHandler) {
|
||||
$fn = self::$warningHandler;
|
||||
$fn($errorMessage, $warningId);
|
||||
} else if ((self::$enableWarnings & $warningId) > 0) {
|
||||
trigger_error($errorMessage, $messageLevel ?: E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
namespace GraphQL\Executor;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
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 array
|
||||
*/
|
||||
public $variableValues;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
public $fieldResolver;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $errors;
|
||||
|
||||
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,158 @@
|
||||
<?php
|
||||
namespace GraphQL\Executor;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\FormattedError;
|
||||
|
||||
/**
|
||||
* 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 array
|
||||
*/
|
||||
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 \GraphQL\Error\Error[]
|
||||
*/
|
||||
public $errors;
|
||||
|
||||
/**
|
||||
* User-defined serializable array of extensions included in serialized result.
|
||||
* Conforms to
|
||||
*
|
||||
* @api
|
||||
* @var array
|
||||
*/
|
||||
public $extensions;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $errorFormatter;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $errorsHandler;
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param array $errors
|
||||
* @param array $extensions
|
||||
*/
|
||||
public function __construct(array $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
|
||||
* );
|
||||
*
|
||||
* @api
|
||||
* @param callable $errorFormatter
|
||||
* @return $this
|
||||
*/
|
||||
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);
|
||||
* }
|
||||
*
|
||||
* @api
|
||||
* @param callable $handler
|
||||
* @return $this
|
||||
*/
|
||||
public function setErrorsHandler(callable $handler)
|
||||
{
|
||||
$this->errorsHandler = $handler;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @api
|
||||
* @param bool|int $debug
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($debug = false)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
if (!empty($this->errors)) {
|
||||
$errorsHandler = $this->errorsHandler ?: function(array $errors, callable $formatter) {
|
||||
return array_map($formatter, $errors);
|
||||
};
|
||||
$result['errors'] = $errorsHandler(
|
||||
$this->errors,
|
||||
FormattedError::prepareFormatter($this->errorFormatter, $debug)
|
||||
);
|
||||
}
|
||||
|
||||
if (null !== $this->data) {
|
||||
$result['data'] = $this->data;
|
||||
}
|
||||
|
||||
if (!empty($this->extensions)) {
|
||||
$result['extensions'] = (array) $this->extensions;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Part of \JsonSerializable interface
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
+1403
File diff suppressed because it is too large.
Load diff
+86
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
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;
|
||||
|
||||
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 $adoptedPromise ReactPromiseInterface */
|
||||
$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 = \React\Promise\resolve($value);
|
||||
return new Promise($promise, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function createRejected($reason)
|
||||
{
|
||||
$promise = \React\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, function ($item) {
|
||||
return $item instanceof Promise ? $item->adoptedPromise : $item;
|
||||
});
|
||||
|
||||
$promise = \React\Promise\all($promisesOrValues)->then(function($values) use ($promisesOrValues) {
|
||||
$orderedResults = [];
|
||||
|
||||
foreach ($promisesOrValues as $key => $value) {
|
||||
$orderedResults[$key] = $values[$key];
|
||||
}
|
||||
|
||||
return $orderedResults;
|
||||
});
|
||||
return new Promise($promise, $this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
namespace GraphQL\Executor\Promise\Adapter;
|
||||
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Class SyncPromise
|
||||
*
|
||||
* Simplistic (yet full-featured) implementation of Promises A+ spec for regular PHP `sync` mode
|
||||
* (using queue to defer promises execution)
|
||||
*
|
||||
* @package GraphQL\Executor\Promise\Adapter
|
||||
*/
|
||||
class SyncPromise
|
||||
{
|
||||
const PENDING = 'pending';
|
||||
const FULFILLED = 'fulfilled';
|
||||
const REJECTED = 'rejected';
|
||||
|
||||
/**
|
||||
* @var \SplQueue
|
||||
*/
|
||||
public static $queue;
|
||||
|
||||
public static function getQueue()
|
||||
{
|
||||
return self::$queue ?: self::$queue = new \SplQueue();
|
||||
}
|
||||
|
||||
public static function runQueue()
|
||||
{
|
||||
$q = self::$queue;
|
||||
while ($q && !$q->isEmpty()) {
|
||||
$task = $q->dequeue();
|
||||
$task();
|
||||
}
|
||||
}
|
||||
|
||||
public $state = self::PENDING;
|
||||
|
||||
public $result;
|
||||
|
||||
/**
|
||||
* Promises created in `then` method of this promise and awaiting for resolution of this promise
|
||||
* @var array
|
||||
*/
|
||||
private $waiting = [];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
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 */
|
||||
list($promise, $onFulfilled, $onRejected) = $descriptor;
|
||||
|
||||
if ($this->state === self::FULFILLED) {
|
||||
try {
|
||||
$promise->resolve($onFulfilled ? $onFulfilled($this->result) : $this->result);
|
||||
} catch (\Exception $e) {
|
||||
$promise->reject($e);
|
||||
} catch (\Throwable $e) {
|
||||
$promise->reject($e);
|
||||
}
|
||||
} else if ($this->state === self::REJECTED) {
|
||||
try {
|
||||
if ($onRejected) {
|
||||
$promise->resolve($onRejected($this->result));
|
||||
} else {
|
||||
$promise->reject($this->result);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$promise->reject($e);
|
||||
} catch (\Throwable $e) {
|
||||
$promise->reject($e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
$this->waiting = [];
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
namespace GraphQL\Executor\Promise\Adapter;
|
||||
|
||||
use GraphQL\Deferred;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Executor\Promise\Promise;
|
||||
use GraphQL\Executor\Promise\PromiseAdapter;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Class SyncPromiseAdapter
|
||||
*
|
||||
* Allows changing order of field resolution even in sync environments
|
||||
* (by leveraging queue of deferreds and promises)
|
||||
*
|
||||
* @package GraphQL\Executor\Promise\Adapter
|
||||
*/
|
||||
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 $promise */
|
||||
$promise = $promise->adoptedPromise;
|
||||
return new Promise($promise->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(
|
||||
function($value) use ($index, &$count, $total, &$result, $all) {
|
||||
$result[$index] = $value;
|
||||
$count++;
|
||||
if ($count >= $total) {
|
||||
$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
|
||||
*
|
||||
* @param Promise $promise
|
||||
* @return mixed
|
||||
*/
|
||||
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;
|
||||
} else if ($syncPromise->state === SyncPromise::REJECTED) {
|
||||
throw $syncPromise->result;
|
||||
}
|
||||
|
||||
throw new InvariantViolation("Could not resolve promise");
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute just before starting to run promise completion
|
||||
*
|
||||
* @param Promise $promise
|
||||
*/
|
||||
protected function beforeWait(Promise $promise)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute while running promise completion
|
||||
*
|
||||
* @param Promise $promise
|
||||
*/
|
||||
protected function onWait(Promise $promise)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
namespace GraphQL\Executor\Promise;
|
||||
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Convenience wrapper for promises represented by Promise Adapter
|
||||
*/
|
||||
class Promise
|
||||
{
|
||||
private $adapter;
|
||||
|
||||
public $adoptedPromise;
|
||||
|
||||
/**
|
||||
* Promise constructor.
|
||||
*
|
||||
* @param mixed $adoptedPromise
|
||||
* @param PromiseAdapter $adapter
|
||||
*/
|
||||
public function __construct($adoptedPromise, PromiseAdapter $adapter)
|
||||
{
|
||||
Utils::invariant(!$adoptedPromise instanceof self, 'Expecting promise from adapted system, got ' . __CLASS__);
|
||||
|
||||
$this->adapter = $adapter;
|
||||
$this->adoptedPromise = $adoptedPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable|null $onFulfilled
|
||||
* @param callable|null $onRejected
|
||||
*
|
||||
* @return Promise
|
||||
*/
|
||||
public function then(callable $onFulfilled = null, callable $onRejected = null)
|
||||
{
|
||||
return $this->adapter->then($this, $onFulfilled, $onRejected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
namespace GraphQL\Executor\Promise;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @api
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isThenable($value);
|
||||
|
||||
/**
|
||||
* Converts thenable of the underlying platform into GraphQL\Executor\Promise\Promise instance
|
||||
*
|
||||
* @api
|
||||
* @param object $thenable
|
||||
* @return Promise
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @api
|
||||
* @param Promise $promise
|
||||
* @param callable|null $onFulfilled
|
||||
* @param callable|null $onRejected
|
||||
*
|
||||
* @return Promise
|
||||
*/
|
||||
public function then(Promise $promise, callable $onFulfilled = null, callable $onRejected = null);
|
||||
|
||||
/**
|
||||
* Creates a Promise
|
||||
*
|
||||
* Expected resolver signature:
|
||||
* function(callable $resolve, callable $reject)
|
||||
*
|
||||
* @api
|
||||
* @param callable $resolver
|
||||
* @return Promise
|
||||
*/
|
||||
public function create(callable $resolver);
|
||||
|
||||
/**
|
||||
* Creates a fulfilled Promise for a value if the value is not a promise.
|
||||
*
|
||||
* @api
|
||||
* @param mixed $value
|
||||
* @return Promise
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @api
|
||||
* @param \Throwable $reason
|
||||
* @return Promise
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @api
|
||||
* @param array $promisesOrValues Promises or values.
|
||||
* @return Promise
|
||||
*/
|
||||
public function all(array $promisesOrValues);
|
||||
}
|
||||
+385
@@ -0,0 +1,385 @@
|
||||
<?php
|
||||
namespace GraphQL\Executor;
|
||||
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
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\NodeList;
|
||||
use GraphQL\Language\AST\VariableNode;
|
||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
use GraphQL\Type\Definition\LeafType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Utils\AST;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
|
||||
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 Schema $schema
|
||||
* @param VariableDefinitionNode[] $definitionNodes
|
||||
* @param array $inputs
|
||||
* @return array
|
||||
* @throws Error
|
||||
*/
|
||||
public static function getVariableValues(Schema $schema, $definitionNodes, array $inputs)
|
||||
{
|
||||
$coercedValues = [];
|
||||
foreach ($definitionNodes as $definitionNode) {
|
||||
$varName = $definitionNode->variable->name->value;
|
||||
$varType = TypeInfo::typeFromAST($schema, $definitionNode->type);
|
||||
|
||||
if (!Type::isInputType($varType)) {
|
||||
throw new Error(
|
||||
'Variable "$'.$varName.'" expected value of type ' .
|
||||
'"' . Printer::doPrint($definitionNode->type) . '" which cannot be used as an input type.',
|
||||
[$definitionNode->type]
|
||||
);
|
||||
}
|
||||
|
||||
if (!array_key_exists($varName, $inputs)) {
|
||||
$defaultValue = $definitionNode->defaultValue;
|
||||
if ($defaultValue) {
|
||||
$coercedValues[$varName] = AST::valueFromAST($defaultValue, $varType);
|
||||
}
|
||||
if ($varType instanceof NonNull) {
|
||||
throw new Error(
|
||||
'Variable "$'.$varName .'" of required type ' .
|
||||
'"'. Utils::printSafe($varType) . '" was not provided.',
|
||||
[$definitionNode]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$value = $inputs[$varName];
|
||||
$errors = self::isValidPHPValue($value, $varType);
|
||||
if (!empty($errors)) {
|
||||
$message = "\n" . implode("\n", $errors);
|
||||
throw new Error(
|
||||
'Variable "$' . $varName . '" got invalid value ' .
|
||||
json_encode($value) . '.' . $message,
|
||||
[$definitionNode]
|
||||
);
|
||||
}
|
||||
|
||||
$coercedValue = self::coerceValue($varType, $value);
|
||||
Utils::invariant($coercedValue !== Utils::undefined(), 'Should have reported error.');
|
||||
$coercedValues[$varName] = $coercedValue;
|
||||
}
|
||||
}
|
||||
return $coercedValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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|\GraphQL\Language\AST\DirectiveNode $node
|
||||
* @param $variableValues
|
||||
* @return array
|
||||
* @throws Error
|
||||
*/
|
||||
public static function getArgumentValues($def, $node, $variableValues = null)
|
||||
{
|
||||
$argDefs = $def->args;
|
||||
$argNodes = $node->arguments;
|
||||
|
||||
if (!$argDefs || null === $argNodes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$coercedValues = [];
|
||||
$undefined = Utils::undefined();
|
||||
|
||||
/** @var ArgumentNode[] $argNodeMap */
|
||||
$argNodeMap = $argNodes ? Utils::keyMap($argNodes, function (ArgumentNode $arg) {
|
||||
return $arg->name->value;
|
||||
}) : [];
|
||||
|
||||
foreach ($argDefs as $argDef) {
|
||||
$name = $argDef->name;
|
||||
$argType = $argDef->getType();
|
||||
$argumentNode = isset($argNodeMap[$name]) ? $argNodeMap[$name] : null;
|
||||
|
||||
if (!$argumentNode) {
|
||||
if ($argDef->defaultValueExists()) {
|
||||
$coercedValues[$name] = $argDef->defaultValue;
|
||||
} else if ($argType instanceof NonNull) {
|
||||
throw new Error(
|
||||
'Argument "' . $name . '" of required type ' .
|
||||
'"' . Utils::printSafe($argType) . '" was not provided.',
|
||||
[$node]
|
||||
);
|
||||
}
|
||||
} else if ($argumentNode->value instanceof VariableNode) {
|
||||
$variableName = $argumentNode->value->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];
|
||||
} else if ($argDef->defaultValueExists()) {
|
||||
$coercedValues[$name] = $argDef->defaultValue;
|
||||
} else if ($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.',
|
||||
[ $argumentNode->value ]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$valueNode = $argumentNode->value;
|
||||
$coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues);
|
||||
if ($coercedValue === $undefined) {
|
||||
$errors = DocumentValidator::isValidLiteralValue($argType, $valueNode);
|
||||
$message = !empty($errors) ? ("\n" . implode("\n", $errors)) : '';
|
||||
throw new Error(
|
||||
'Argument "' . $name . '" got invalid value ' . Printer::doPrint($valueNode) . '.' . $message,
|
||||
[ $argumentNode->value ]
|
||||
);
|
||||
}
|
||||
$coercedValues[$name] = $coercedValue;
|
||||
}
|
||||
}
|
||||
return $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 Directive $directiveDef
|
||||
* @param FragmentSpreadNode | FieldNode | InlineFragmentNode | EnumValueDefinitionNode | FieldDefinitionNode $node
|
||||
* @param array|null $variableValues
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public static function getDirectiveValues(Directive $directiveDef, $node, $variableValues = null)
|
||||
{
|
||||
if (isset($node->directives) && $node->directives instanceof NodeList) {
|
||||
$directiveNode = Utils::find($node->directives, function(DirectiveNode $directive) use ($directiveDef) {
|
||||
return $directive->name->value === $directiveDef->name;
|
||||
});
|
||||
|
||||
if ($directiveNode) {
|
||||
return self::getArgumentValues($directiveDef, $directiveNode, $variableValues);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated as of 8.0 (Moved to \GraphQL\Utils\AST::valueFromAST)
|
||||
*
|
||||
* @param $valueNode
|
||||
* @param InputType $type
|
||||
* @param null $variables
|
||||
* @return array|null|\stdClass
|
||||
*/
|
||||
public static function valueFromAST($valueNode, InputType $type, $variables = null)
|
||||
{
|
||||
return AST::valueFromAST($valueNode, $type, $variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a PHP value and a GraphQL type, determine if the value will be
|
||||
* accepted for that type. This is primarily useful for validating the
|
||||
* runtime values of query variables.
|
||||
*
|
||||
* @param $value
|
||||
* @param InputType $type
|
||||
* @return array
|
||||
*/
|
||||
public static function isValidPHPValue($value, InputType $type)
|
||||
{
|
||||
// A value must be provided if the type is non-null.
|
||||
if ($type instanceof NonNull) {
|
||||
if (null === $value) {
|
||||
return ['Expected "' . Utils::printSafe($type) . '", found null.'];
|
||||
}
|
||||
return self::isValidPHPValue($value, $type->getWrappedType());
|
||||
}
|
||||
|
||||
if (null === $value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Lists accept a non-list value as a list of one.
|
||||
if ($type instanceof ListOfType) {
|
||||
$itemType = $type->getWrappedType();
|
||||
if (is_array($value)) {
|
||||
$tmp = [];
|
||||
foreach ($value as $index => $item) {
|
||||
$errors = self::isValidPHPValue($item, $itemType);
|
||||
$tmp = array_merge($tmp, Utils::map($errors, function ($error) use ($index) {
|
||||
return "In element #$index: $error";
|
||||
}));
|
||||
}
|
||||
return $tmp;
|
||||
}
|
||||
return self::isValidPHPValue($value, $itemType);
|
||||
}
|
||||
|
||||
// Input objects check each defined field.
|
||||
if ($type instanceof InputObjectType) {
|
||||
if (!is_object($value) && !is_array($value)) {
|
||||
return ["Expected \"{$type->name}\", found not an object."];
|
||||
}
|
||||
$fields = $type->getFields();
|
||||
$errors = [];
|
||||
|
||||
// Ensure every provided field is defined.
|
||||
$props = is_object($value) ? get_object_vars($value) : $value;
|
||||
foreach ($props as $providedField => $tmp) {
|
||||
if (!isset($fields[$providedField])) {
|
||||
$errors[] = "In field \"{$providedField}\": Unknown field.";
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure every defined field is valid.
|
||||
foreach ($fields as $fieldName => $tmp) {
|
||||
$newErrors = self::isValidPHPValue(isset($value[$fieldName]) ? $value[$fieldName] : null, $fields[$fieldName]->getType());
|
||||
$errors = array_merge(
|
||||
$errors,
|
||||
Utils::map($newErrors, function ($error) use ($fieldName) {
|
||||
return "In field \"{$fieldName}\": {$error}";
|
||||
})
|
||||
);
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
|
||||
if ($type instanceof LeafType) {
|
||||
try {
|
||||
// Scalar/Enum input checks to ensure the type can parse the value to
|
||||
// a non-null value.
|
||||
$parseResult = $type->parseValue($value);
|
||||
if (null === $parseResult && !$type->isValidValue($value)) {
|
||||
$v = Utils::printSafeJson($value);
|
||||
return [
|
||||
"Expected type \"{$type->name}\", found $v."
|
||||
];
|
||||
}
|
||||
return [];
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
"Expected type \"{$type->name}\", found " . Utils::printSafeJson($value) . ': ' .
|
||||
$e->getMessage()
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
"Expected type \"{$type->name}\", found " . Utils::printSafeJson($value) . ': ' .
|
||||
$e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvariantViolation('Must be input type');
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a type and any value, return a runtime value coerced to match the type.
|
||||
*/
|
||||
private static function coerceValue(Type $type, $value)
|
||||
{
|
||||
$undefined = Utils::undefined();
|
||||
if ($value === $undefined) {
|
||||
return $undefined;
|
||||
}
|
||||
|
||||
if ($type instanceof NonNull) {
|
||||
if ($value === null) {
|
||||
// Intentionally return no value.
|
||||
return $undefined;
|
||||
}
|
||||
return self::coerceValue($type->getWrappedType(), $value);
|
||||
}
|
||||
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type instanceof ListOfType) {
|
||||
$itemType = $type->getWrappedType();
|
||||
if (is_array($value) || $value instanceof \Traversable) {
|
||||
$coercedValues = [];
|
||||
foreach ($value as $item) {
|
||||
$itemValue = self::coerceValue($itemType, $item);
|
||||
if ($undefined === $itemValue) {
|
||||
// Intentionally return no value.
|
||||
return $undefined;
|
||||
}
|
||||
$coercedValues[] = $itemValue;
|
||||
}
|
||||
return $coercedValues;
|
||||
} else {
|
||||
$coercedValue = self::coerceValue($itemType, $value);
|
||||
if ($coercedValue === $undefined) {
|
||||
// Intentionally return no value.
|
||||
return $undefined;
|
||||
}
|
||||
return [$coercedValue];
|
||||
}
|
||||
}
|
||||
|
||||
if ($type instanceof InputObjectType) {
|
||||
$coercedObj = [];
|
||||
$fields = $type->getFields();
|
||||
foreach ($fields as $fieldName => $field) {
|
||||
if (!array_key_exists($fieldName, $value)) {
|
||||
if ($field->defaultValueExists()) {
|
||||
$coercedObj[$fieldName] = $field->defaultValue;
|
||||
} else if ($field->getType() instanceof NonNull) {
|
||||
// Intentionally return no value.
|
||||
return $undefined;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$fieldValue = self::coerceValue($field->getType(), $value[$fieldName]);
|
||||
if ($fieldValue === $undefined) {
|
||||
// Intentionally return no value.
|
||||
return $undefined;
|
||||
}
|
||||
$coercedObj[$fieldName] = $fieldValue;
|
||||
}
|
||||
return $coercedObj;
|
||||
}
|
||||
|
||||
if ($type instanceof LeafType) {
|
||||
$parsed = $type->parseValue($value);
|
||||
if (null === $parsed) {
|
||||
// null or invalid values represent a failure to parse correctly,
|
||||
// in which case no value is returned.
|
||||
return $undefined;
|
||||
}
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
throw new InvariantViolation('Must be input type');
|
||||
}
|
||||
}
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
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\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Language\Source;
|
||||
use GraphQL\Executor\Promise\PromiseAdapter;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
use GraphQL\Validator\Rules\AbstractValidationRule;
|
||||
use GraphQL\Validator\Rules\QueryComplexity;
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*
|
||||
* @api
|
||||
* @param \GraphQL\Type\Schema $schema
|
||||
* @param string|DocumentNode $source
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $context
|
||||
* @param array|null $variableValues
|
||||
* @param string|null $operationName
|
||||
* @param callable $fieldResolver
|
||||
* @param array $validationRules
|
||||
*
|
||||
* @return ExecutionResult
|
||||
*/
|
||||
public static function executeQuery(
|
||||
\GraphQL\Type\Schema $schema,
|
||||
$source,
|
||||
$rootValue = null,
|
||||
$context = null,
|
||||
$variableValues = null,
|
||||
$operationName = null,
|
||||
callable $fieldResolver = null,
|
||||
array $validationRules = null
|
||||
)
|
||||
{
|
||||
$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.
|
||||
*
|
||||
* @api
|
||||
* @param PromiseAdapter $promiseAdapter
|
||||
* @param \GraphQL\Type\Schema $schema
|
||||
* @param string|DocumentNode $source
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $context
|
||||
* @param array|null $variableValues
|
||||
* @param string|null $operationName
|
||||
* @param callable $fieldResolver
|
||||
* @param array $validationRules
|
||||
*
|
||||
* @return Promise
|
||||
*/
|
||||
public static function promiseToExecute(
|
||||
PromiseAdapter $promiseAdapter,
|
||||
\GraphQL\Type\Schema $schema,
|
||||
$source,
|
||||
$rootValue = null,
|
||||
$context = null,
|
||||
$variableValues = null,
|
||||
$operationName = null,
|
||||
callable $fieldResolver = null,
|
||||
array $validationRules = null
|
||||
)
|
||||
{
|
||||
try {
|
||||
if ($source instanceof DocumentNode) {
|
||||
$documentNode = $source;
|
||||
} else {
|
||||
$documentNode = Parser::parse(new Source($source ?: '', 'GraphQL'));
|
||||
}
|
||||
|
||||
// FIXME
|
||||
if (!empty($validationRules)) {
|
||||
foreach ($validationRules as $rule) {
|
||||
if ($rule instanceof QueryComplexity) {
|
||||
$rule->setRawVariableValues($variableValues);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/** @var QueryComplexity $queryComplexity */
|
||||
$queryComplexity = DocumentValidator::getRule(QueryComplexity::class);
|
||||
$queryComplexity->setRawVariableValues($variableValues);
|
||||
}
|
||||
|
||||
$validationErrors = DocumentValidator::validate($schema, $documentNode, $validationRules);
|
||||
|
||||
if (!empty($validationErrors)) {
|
||||
return $promiseAdapter->createFulfilled(
|
||||
new ExecutionResult(null, $validationErrors)
|
||||
);
|
||||
} else {
|
||||
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 \GraphQL\Type\Schema $schema
|
||||
* @param string|DocumentNode $source
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $contextValue
|
||||
* @param array|null $variableValues
|
||||
* @param string|null $operationName
|
||||
* @return Promise|array
|
||||
*/
|
||||
public static function execute(
|
||||
\GraphQL\Type\Schema $schema,
|
||||
$source,
|
||||
$rootValue = null,
|
||||
$contextValue = null,
|
||||
$variableValues = null,
|
||||
$operationName = null
|
||||
)
|
||||
{
|
||||
$result = self::promiseToExecute(
|
||||
$promiseAdapter = Executor::getPromiseAdapter(),
|
||||
$schema,
|
||||
$source,
|
||||
$rootValue,
|
||||
$contextValue,
|
||||
$variableValues,
|
||||
$operationName
|
||||
);
|
||||
|
||||
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
||||
$result = $promiseAdapter->wait($result)->toArray();
|
||||
} else {
|
||||
$result = $result->then(function(ExecutionResult $r) {
|
||||
return $r->toArray();
|
||||
});
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated renamed to executeQuery()
|
||||
*
|
||||
* @param \GraphQL\Type\Schema $schema
|
||||
* @param string|DocumentNode $source
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $contextValue
|
||||
* @param array|null $variableValues
|
||||
* @param string|null $operationName
|
||||
*
|
||||
* @return ExecutionResult|Promise
|
||||
*/
|
||||
public static function executeAndReturnResult(
|
||||
\GraphQL\Type\Schema $schema,
|
||||
$source,
|
||||
$rootValue = null,
|
||||
$contextValue = null,
|
||||
$variableValues = null,
|
||||
$operationName = null
|
||||
)
|
||||
{
|
||||
$result = self::promiseToExecute(
|
||||
$promiseAdapter = Executor::getPromiseAdapter(),
|
||||
$schema,
|
||||
$source,
|
||||
$rootValue,
|
||||
$contextValue,
|
||||
$variableValues,
|
||||
$operationName
|
||||
);
|
||||
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
||||
$result = $promiseAdapter->wait($result);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns directives defined in GraphQL spec
|
||||
*
|
||||
* @api
|
||||
* @return Directive[]
|
||||
*/
|
||||
public static function getStandardDirectives()
|
||||
{
|
||||
return array_values(Directive::getInternalDirectives());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns types defined in GraphQL spec
|
||||
*
|
||||
* @api
|
||||
* @return Type[]
|
||||
*/
|
||||
public static function getStandardTypes()
|
||||
{
|
||||
return array_values(Type::getInternalTypes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns standard validation rules implementing GraphQL spec
|
||||
*
|
||||
* @api
|
||||
* @return AbstractValidationRule[]
|
||||
*/
|
||||
public static function getStandardValidationRules()
|
||||
{
|
||||
return array_values(DocumentValidator::defaultRules());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $fn
|
||||
*/
|
||||
public static function setDefaultFieldResolver(callable $fn)
|
||||
{
|
||||
Executor::setDefaultFieldResolver($fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PromiseAdapter|null $promiseAdapter
|
||||
*/
|
||||
public static function setPromiseAdapter(PromiseAdapter $promiseAdapter = null)
|
||||
{
|
||||
Executor::setPromiseAdapter($promiseAdapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns directives defined in GraphQL spec
|
||||
*
|
||||
* @deprecated Renamed to getStandardDirectives
|
||||
* @return Directive[]
|
||||
*/
|
||||
public static function getInternalDirectives()
|
||||
{
|
||||
return self::getStandardDirectives();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ArgumentNode extends Node
|
||||
{
|
||||
public $kind = NodeKind::ARGUMENT;
|
||||
|
||||
/**
|
||||
* @var ValueNode
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* @var NameNode
|
||||
*/
|
||||
public $name;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
|
||||
class BooleanValueNode extends Node implements ValueNode
|
||||
{
|
||||
public $kind = NodeKind::BOOLEAN;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $value;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
interface DefinitionNode
|
||||
{
|
||||
/**
|
||||
* export type DefinitionNode = OperationDefinitionNode
|
||||
* | FragmentDefinitionNode
|
||||
* | TypeSystemDefinitionNode // experimental non-spec addition.
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class DirectiveNode extends Node
|
||||
{
|
||||
public $kind = NodeKind::DIRECTIVE;
|
||||
|
||||
/**
|
||||
* @var NameNode
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var ArgumentNode[]
|
||||
*/
|
||||
public $arguments;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class DocumentNode extends Node
|
||||
{
|
||||
public $kind = NodeKind::DOCUMENT;
|
||||
|
||||
/**
|
||||
* @var DefinitionNode[]
|
||||
*/
|
||||
public $definitions;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
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[]
|
||||
*/
|
||||
public $values;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
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 string
|
||||
*/
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class EnumValueNode extends Node implements ValueNode
|
||||
{
|
||||
public $kind = NodeKind::ENUM;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $value;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class FieldDefinitionNode extends Node
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $kind = NodeKind::FIELD_DEFINITION;
|
||||
|
||||
/**
|
||||
* @var NameNode
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var InputValueDefinitionNode[]
|
||||
*/
|
||||
public $arguments;
|
||||
|
||||
/**
|
||||
* @var TypeNode
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* @var DirectiveNode[]
|
||||
*/
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class FieldNode extends Node implements SelectionNode
|
||||
{
|
||||
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,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class FloatValueNode extends Node implements ValueNode
|
||||
{
|
||||
public $kind = NodeKind::FLOAT;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $value;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class FragmentDefinitionNode extends Node implements DefinitionNode, HasSelectionSet
|
||||
{
|
||||
public $kind = NodeKind::FRAGMENT_DEFINITION;
|
||||
|
||||
/**
|
||||
* @var NameNode
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var NamedTypeNode
|
||||
*/
|
||||
public $typeCondition;
|
||||
|
||||
/**
|
||||
* @var DirectiveNode[]
|
||||
*/
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* @var SelectionSetNode
|
||||
*/
|
||||
public $selectionSet;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class FragmentSpreadNode extends Node implements SelectionNode
|
||||
{
|
||||
public $kind = NodeKind::FRAGMENT_SPREAD;
|
||||
|
||||
/**
|
||||
* @var NameNode
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var DirectiveNode[]
|
||||
*/
|
||||
public $directives;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
interface HasSelectionSet
|
||||
{
|
||||
/**
|
||||
* export type DefinitionNode = OperationDefinitionNode
|
||||
* | FragmentDefinitionNode
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class InlineFragmentNode extends Node implements SelectionNode
|
||||
{
|
||||
public $kind = NodeKind::INLINE_FRAGMENT;
|
||||
|
||||
/**
|
||||
* @var NamedTypeNode
|
||||
*/
|
||||
public $typeCondition;
|
||||
|
||||
/**
|
||||
* @var DirectiveNode[]|null
|
||||
*/
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* @var SelectionSetNode
|
||||
*/
|
||||
public $selectionSet;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
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[]
|
||||
*/
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* @var InputValueDefinitionNode[]
|
||||
*/
|
||||
public $fields;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
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 string
|
||||
*/
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class IntValueNode extends Node implements ValueNode
|
||||
{
|
||||
public $kind = NodeKind::INT;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $value;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class InterfaceTypeDefinitionNode extends Node implements TypeDefinitionNode
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $kind = NodeKind::INTERFACE_TYPE_DEFINITION;
|
||||
|
||||
/**
|
||||
* @var NameNode
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var DirectiveNode[]
|
||||
*/
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* @var FieldDefinitionNode[]
|
||||
*/
|
||||
public $fields = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ListTypeNode extends Node implements TypeNode
|
||||
{
|
||||
public $kind = NodeKind::LIST_TYPE;
|
||||
|
||||
/**
|
||||
* @var Node
|
||||
*/
|
||||
public $type;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ListValueNode extends Node implements ValueNode
|
||||
{
|
||||
public $kind = NodeKind::LST;
|
||||
|
||||
/**
|
||||
* @var ValueNode[]
|
||||
*/
|
||||
public $values;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
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 $start
|
||||
* @param $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) {
|
||||
$this->start = $startToken->start;
|
||||
$this->end = $endToken->end;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class NameNode extends Node implements TypeNode
|
||||
{
|
||||
public $kind = NodeKind::NAME;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $value;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class NamedTypeNode extends Node implements TypeNode
|
||||
{
|
||||
public $kind = NodeKind::NAMED_TYPE;
|
||||
|
||||
/**
|
||||
* @var NameNode
|
||||
*/
|
||||
public $name;
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
abstract class Node
|
||||
{
|
||||
/**
|
||||
type Node = NameNode
|
||||
| DocumentNode
|
||||
| OperationDefinitionNode
|
||||
| VariableDefinitionNode
|
||||
| VariableNode
|
||||
| SelectionSetNode
|
||||
| FieldNode
|
||||
| ArgumentNode
|
||||
| FragmentSpreadNode
|
||||
| InlineFragmentNode
|
||||
| FragmentDefinitionNode
|
||||
| IntValueNode
|
||||
| FloatValueNode
|
||||
| StringValueNode
|
||||
| BooleanValueNode
|
||||
| EnumValueNode
|
||||
| ListValueNode
|
||||
| ObjectValueNode
|
||||
| ObjectFieldNode
|
||||
| DirectiveNode
|
||||
| ListTypeNode
|
||||
| NonNullTypeNode
|
||||
*/
|
||||
|
||||
public $kind;
|
||||
|
||||
/**
|
||||
* @var Location
|
||||
*/
|
||||
public $loc;
|
||||
|
||||
/**
|
||||
* @param array $vars
|
||||
*/
|
||||
public function __construct(array $vars)
|
||||
{
|
||||
if (!empty($vars)) {
|
||||
Utils::assign($this, $vars);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function cloneDeep()
|
||||
{
|
||||
return $this->cloneValue($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @return array|Node
|
||||
*/
|
||||
private function cloneValue($value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$cloned = [];
|
||||
foreach ($value as $key => $arrValue) {
|
||||
$cloned[$key] = $this->cloneValue($arrValue);
|
||||
}
|
||||
} else if ($value instanceof Node) {
|
||||
$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 json_encode($tmp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $recursive
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($recursive = false)
|
||||
{
|
||||
if ($recursive) {
|
||||
return $this->recursiveToArray($this);
|
||||
} else {
|
||||
$tmp = (array) $this;
|
||||
|
||||
$tmp['loc'] = [
|
||||
'start' => $this->loc->start,
|
||||
'end' => $this->loc->end
|
||||
];
|
||||
|
||||
return $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $node
|
||||
* @return array
|
||||
*/
|
||||
private function recursiveToArray(Node $node)
|
||||
{
|
||||
$result = [
|
||||
'kind' => $node->kind,
|
||||
'loc' => [
|
||||
'start' => $node->loc->start,
|
||||
'end' => $node->loc->end
|
||||
]
|
||||
];
|
||||
|
||||
foreach (get_object_vars($node) as $prop => $propValue) {
|
||||
if (isset($result[$prop]))
|
||||
continue;
|
||||
|
||||
if (is_array($propValue) || $propValue instanceof NodeList) {
|
||||
$tmp = [];
|
||||
foreach ($propValue as $tmp1) {
|
||||
$tmp[] = $tmp1 instanceof Node ? $this->recursiveToArray($tmp1) : (array) $tmp1;
|
||||
}
|
||||
} else if ($propValue instanceof Node) {
|
||||
$tmp = $this->recursiveToArray($propValue);
|
||||
} else if (is_scalar($propValue) || null === $propValue) {
|
||||
$tmp = $propValue;
|
||||
} else {
|
||||
$tmp = null;
|
||||
}
|
||||
|
||||
$result[$prop] = $tmp;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
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 TYPE_EXTENSION_DEFINITION = 'TypeExtensionDefinition';
|
||||
|
||||
// Directive Definitions
|
||||
|
||||
const DIRECTIVE_DEFINITION = 'DirectiveDefinition';
|
||||
|
||||
/**
|
||||
* @todo conver to const array when moving to PHP5.6
|
||||
* @var array
|
||||
*/
|
||||
public static $classMap = [
|
||||
NodeKind::NAME => NameNode::class,
|
||||
|
||||
// Document
|
||||
NodeKind::DOCUMENT => DocumentNode::class,
|
||||
NodeKind::OPERATION_DEFINITION => OperationDefinitionNode::class,
|
||||
NodeKind::VARIABLE_DEFINITION => VariableDefinitionNode::class,
|
||||
NodeKind::VARIABLE => VariableNode::class,
|
||||
NodeKind::SELECTION_SET => SelectionSetNode::class,
|
||||
NodeKind::FIELD => FieldNode::class,
|
||||
NodeKind::ARGUMENT => ArgumentNode::class,
|
||||
|
||||
// Fragments
|
||||
NodeKind::FRAGMENT_SPREAD => FragmentSpreadNode::class,
|
||||
NodeKind::INLINE_FRAGMENT => InlineFragmentNode::class,
|
||||
NodeKind::FRAGMENT_DEFINITION => FragmentDefinitionNode::class,
|
||||
|
||||
// Values
|
||||
NodeKind::INT => IntValueNode::class,
|
||||
NodeKind::FLOAT => FloatValueNode::class,
|
||||
NodeKind::STRING => StringValueNode::class,
|
||||
NodeKind::BOOLEAN => BooleanValueNode::class,
|
||||
NodeKind::ENUM => EnumValueNode::class,
|
||||
NodeKind::NULL => NullValueNode::class,
|
||||
NodeKind::LST => ListValueNode::class,
|
||||
NodeKind::OBJECT => ObjectValueNode::class,
|
||||
NodeKind::OBJECT_FIELD => ObjectFieldNode::class,
|
||||
|
||||
// Directives
|
||||
NodeKind::DIRECTIVE => DirectiveNode::class,
|
||||
|
||||
// Types
|
||||
NodeKind::NAMED_TYPE => NamedTypeNode::class,
|
||||
NodeKind::LIST_TYPE => ListTypeNode::class,
|
||||
NodeKind::NON_NULL_TYPE => NonNullTypeNode::class,
|
||||
|
||||
// Type System Definitions
|
||||
NodeKind::SCHEMA_DEFINITION => SchemaDefinitionNode::class,
|
||||
NodeKind::OPERATION_TYPE_DEFINITION => OperationTypeDefinitionNode::class,
|
||||
|
||||
// Type Definitions
|
||||
NodeKind::SCALAR_TYPE_DEFINITION => ScalarTypeDefinitionNode::class,
|
||||
NodeKind::OBJECT_TYPE_DEFINITION => ObjectTypeDefinitionNode::class,
|
||||
NodeKind::FIELD_DEFINITION => FieldDefinitionNode::class,
|
||||
NodeKind::INPUT_VALUE_DEFINITION => InputValueDefinitionNode::class,
|
||||
NodeKind::INTERFACE_TYPE_DEFINITION => InterfaceTypeDefinitionNode::class,
|
||||
NodeKind::UNION_TYPE_DEFINITION => UnionTypeDefinitionNode::class,
|
||||
NodeKind::ENUM_TYPE_DEFINITION => EnumTypeDefinitionNode::class,
|
||||
NodeKind::ENUM_VALUE_DEFINITION => EnumValueDefinitionNode::class,
|
||||
NodeKind::INPUT_OBJECT_TYPE_DEFINITION =>InputObjectTypeDefinitionNode::class,
|
||||
|
||||
// Type Extensions
|
||||
NodeKind::TYPE_EXTENSION_DEFINITION => TypeExtensionDefinitionNode::class,
|
||||
|
||||
// Directive Definitions
|
||||
NodeKind::DIRECTIVE_DEFINITION => DirectiveDefinitionNode::class
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
use GraphQL\Utils\AST;
|
||||
|
||||
/**
|
||||
* Class NodeList
|
||||
*
|
||||
* @package GraphQL\Utils
|
||||
*/
|
||||
class NodeList implements \ArrayAccess, \IteratorAggregate, \Countable
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $nodes;
|
||||
|
||||
/**
|
||||
* @param array $nodes
|
||||
* @return static
|
||||
*/
|
||||
public static function create(array $nodes)
|
||||
{
|
||||
return new static($nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* NodeList constructor.
|
||||
* @param array $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 $list
|
||||
* @return NodeList
|
||||
*/
|
||||
public function merge($list)
|
||||
{
|
||||
if ($list instanceof NodeList) {
|
||||
$list = $list->nodes;
|
||||
}
|
||||
return new NodeList(array_merge($this->nodes, $list));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Generator
|
||||
*/
|
||||
public function getIterator()
|
||||
{
|
||||
$count = count($this->nodes);
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
yield $this->offsetGet($i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function count()
|
||||
{
|
||||
return count($this->nodes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class NonNullTypeNode extends Node implements TypeNode
|
||||
{
|
||||
public $kind = NodeKind::NON_NULL_TYPE;
|
||||
|
||||
/**
|
||||
* @var NameNode | ListTypeNode
|
||||
*/
|
||||
public $type;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class NullValueNode extends Node implements ValueNode
|
||||
{
|
||||
public $kind = NodeKind::NULL;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
|
||||
class ObjectFieldNode extends Node
|
||||
{
|
||||
public $kind = NodeKind::OBJECT_FIELD;
|
||||
|
||||
/**
|
||||
* @var NameNode
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var ValueNode
|
||||
*/
|
||||
public $value;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
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[]
|
||||
*/
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* @var FieldDefinitionNode[]
|
||||
*/
|
||||
public $fields;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ObjectValueNode extends Node implements ValueNode
|
||||
{
|
||||
public $kind = NodeKind::OBJECT;
|
||||
|
||||
/**
|
||||
* @var ObjectFieldNode[]
|
||||
*/
|
||||
public $fields;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class OperationDefinitionNode extends Node implements DefinitionNode, 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,22 @@
|
||||
<?php
|
||||
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,25 @@
|
||||
<?php
|
||||
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 string
|
||||
*/
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
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,9 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
interface SelectionNode
|
||||
{
|
||||
/**
|
||||
* export type SelectionNode = FieldNode | FragmentSpreadNode | InlineFragmentNode
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class SelectionSetNode extends Node
|
||||
{
|
||||
public $kind = NodeKind::SELECTION_SET;
|
||||
|
||||
/**
|
||||
* @var SelectionNode[]
|
||||
*/
|
||||
public $selections;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class StringValueNode extends Node implements ValueNode
|
||||
{
|
||||
public $kind = NodeKind::STRING;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $value;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
interface TypeDefinitionNode extends TypeSystemDefinitionNode
|
||||
{
|
||||
/**
|
||||
export type TypeDefinitionNode = ScalarTypeDefinitionNode
|
||||
| ObjectTypeDefinitionNode
|
||||
| InterfaceTypeDefinitionNode
|
||||
| UnionTypeDefinitionNode
|
||||
| EnumTypeDefinitionNode
|
||||
| InputObjectTypeDefinitionNode
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class TypeExtensionDefinitionNode extends Node implements TypeSystemDefinitionNode
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $kind = NodeKind::TYPE_EXTENSION_DEFINITION;
|
||||
|
||||
/**
|
||||
* @var ObjectTypeDefinitionNode
|
||||
*/
|
||||
public $definition;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
|
||||
interface TypeNode
|
||||
{
|
||||
/**
|
||||
export type TypeNode = NamedTypeNode
|
||||
| ListTypeNode
|
||||
| NonNullTypeNode
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
interface TypeSystemDefinitionNode extends DefinitionNode
|
||||
{
|
||||
/**
|
||||
export type TypeSystemDefinitionNode = SchemaDefinitionNode
|
||||
| TypeDefinitionNode
|
||||
| TypeExtensionDefinitionNode
|
||||
| DirectiveDefinitionNode
|
||||
*/
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
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[]
|
||||
*/
|
||||
public $types = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $description;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
/**
|
||||
export type ValueNode = VariableNode
|
||||
| IntValueNode
|
||||
| FloatValueNode
|
||||
| StringValueNode
|
||||
| BooleanValueNode
|
||||
| EnumValueNode
|
||||
| ListValueNode
|
||||
| ObjectValueNode
|
||||
*/
|
||||
interface ValueNode
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class VariableDefinitionNode extends Node implements DefinitionNode
|
||||
{
|
||||
public $kind = NodeKind::VARIABLE_DEFINITION;
|
||||
|
||||
/**
|
||||
* @var VariableNode
|
||||
*/
|
||||
public $variable;
|
||||
|
||||
/**
|
||||
* @var TypeNode
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* @var ValueNode|null
|
||||
*/
|
||||
public $defaultValue;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class VariableNode extends Node
|
||||
{
|
||||
public $kind = NodeKind::VARIABLE;
|
||||
|
||||
/**
|
||||
* @var NameNode
|
||||
*/
|
||||
public $name;
|
||||
}
|
||||
+612
@@ -0,0 +1,612 @@
|
||||
<?php
|
||||
namespace GraphQL\Language;
|
||||
|
||||
use GraphQL\Error\SyntaxError;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
/**
|
||||
* @var Source
|
||||
*/
|
||||
public $source;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Lexer constructor.
|
||||
*
|
||||
* @param Source $source
|
||||
* @param array $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()
|
||||
{
|
||||
$token = $this->lastToken = $this->token;
|
||||
|
||||
if ($token->kind !== Token::EOF) {
|
||||
do {
|
||||
$token = $token->next = $this->readToken($token);
|
||||
} while ($token->kind === Token::COMMENT);
|
||||
$this->token = $token;
|
||||
}
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Token
|
||||
*/
|
||||
public function nextToken()
|
||||
{
|
||||
trigger_error(__METHOD__ . ' is deprecated in favor of advance()', E_USER_DEPRECATED);
|
||||
return $this->advance();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Token $prev
|
||||
* @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:
|
||||
list (, $code, $bytes) = $this->readChar(true);
|
||||
|
||||
// SourceCharacter
|
||||
if ($code < 0x0020 && $code !== 0x0009 && $code !== 0x000A && $code !== 0x000D) {
|
||||
throw new SyntaxError(
|
||||
$this->source,
|
||||
$position,
|
||||
'Cannot contain the invalid character ' . Utils::printCharCode($code)
|
||||
);
|
||||
}
|
||||
|
||||
switch ($code) {
|
||||
case 33: // !
|
||||
return new Token(Token::BANG, $position, $position + 1, $line, $col, $prev);
|
||||
case 35: // #
|
||||
$this->moveStringCursor(-1, -1 * $bytes);
|
||||
return $this->readComment($line, $col, $prev);
|
||||
case 36: // $
|
||||
return new Token(Token::DOLLAR, $position, $position + 1, $line, $col, $prev);
|
||||
case 40: // (
|
||||
return new Token(Token::PAREN_L, $position, $position + 1, $line, $col, $prev);
|
||||
case 41: // )
|
||||
return new Token(Token::PAREN_R, $position, $position + 1, $line, $col, $prev);
|
||||
case 46: // .
|
||||
list (, $charCode1) = $this->readChar(true);
|
||||
list (, $charCode2) = $this->readChar(true);
|
||||
|
||||
if ($charCode1 === 46 && $charCode2 === 46) {
|
||||
return new Token(Token::SPREAD, $position, $position + 3, $line, $col, $prev);
|
||||
}
|
||||
break;
|
||||
case 58: // :
|
||||
return new Token(Token::COLON, $position, $position + 1, $line, $col, $prev);
|
||||
case 61: // =
|
||||
return new Token(Token::EQUALS, $position, $position + 1, $line, $col, $prev);
|
||||
case 64: // @
|
||||
return new Token(Token::AT, $position, $position + 1, $line, $col, $prev);
|
||||
case 91: // [
|
||||
return new Token(Token::BRACKET_L, $position, $position + 1, $line, $col, $prev);
|
||||
case 93: // ]
|
||||
return new Token(Token::BRACKET_R, $position, $position + 1, $line, $col, $prev);
|
||||
case 123: // {
|
||||
return new Token(Token::BRACE_L, $position, $position + 1, $line, $col, $prev);
|
||||
case 124: // |
|
||||
return new Token(Token::PIPE, $position, $position + 1, $line, $col, $prev);
|
||||
case 125: // }
|
||||
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:
|
||||
return $this->moveStringCursor(-1, -1 * $bytes)
|
||||
->readString($line, $col, $prev);
|
||||
}
|
||||
|
||||
$errMessage = $code === 39
|
||||
? "Unexpected single quote character ('), did you mean to use ". 'a double quote (")?'
|
||||
: 'Cannot parse the unexpected character ' . Utils::printCharCode($code) . '.';
|
||||
|
||||
throw new SyntaxError(
|
||||
$this->source,
|
||||
$position,
|
||||
$errMessage
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an alphanumeric + underscore name from the source.
|
||||
*
|
||||
* [_A-Za-z][_0-9A-Za-z]*
|
||||
*
|
||||
* @param int $line
|
||||
* @param int $col
|
||||
* @param Token $prev
|
||||
* @return Token
|
||||
*/
|
||||
private function readName($line, $col, Token $prev)
|
||||
{
|
||||
$value = '';
|
||||
$start = $this->position;
|
||||
list ($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;
|
||||
list ($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
|
||||
* @param Token $prev
|
||||
* @return Token
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
private function readNumber($line, $col, Token $prev)
|
||||
{
|
||||
$value = '';
|
||||
$start = $this->position;
|
||||
list ($char, $code) = $this->readChar();
|
||||
|
||||
$isFloat = false;
|
||||
|
||||
if ($code === 45) { // -
|
||||
$value .= $char;
|
||||
list ($char, $code) = $this->moveStringCursor(1, 1)->readChar();
|
||||
}
|
||||
|
||||
// guard against leading zero's
|
||||
if ($code === 48) { // 0
|
||||
$value .= $char;
|
||||
list ($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();
|
||||
list ($char, $code) = $this->readChar();
|
||||
}
|
||||
|
||||
if ($code === 46) { // .
|
||||
$isFloat = true;
|
||||
$this->moveStringCursor(1, 1);
|
||||
|
||||
$value .= $char;
|
||||
$value .= $this->readDigits();
|
||||
list ($char, $code) = $this->readChar();
|
||||
}
|
||||
|
||||
if ($code === 69 || $code === 101) { // E e
|
||||
$isFloat = true;
|
||||
$value .= $char;
|
||||
list ($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()
|
||||
{
|
||||
list ($char, $code) = $this->readChar();
|
||||
|
||||
if ($code >= 48 && $code <= 57) { // 0 - 9
|
||||
$value = '';
|
||||
|
||||
do {
|
||||
$value .= $char;
|
||||
list ($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
|
||||
* @param Token $prev
|
||||
* @return Token
|
||||
* @throws SyntaxError
|
||||
*/
|
||||
private function readString($line, $col, Token $prev)
|
||||
{
|
||||
$start = $this->position;
|
||||
|
||||
// Skip leading quote and read first string char:
|
||||
list ($char, $code, $bytes) = $this->moveStringCursor(1, 1)->readChar();
|
||||
|
||||
$chunk = '';
|
||||
$value = '';
|
||||
|
||||
while (
|
||||
$code &&
|
||||
// not LineTerminator
|
||||
$code !== 10 && $code !== 13 &&
|
||||
// not Quote (")
|
||||
$code !== 34
|
||||
) {
|
||||
$this->assertValidStringCharacterCode($code, $this->position);
|
||||
$this->moveStringCursor(1, $bytes);
|
||||
|
||||
if ($code === 92) { // \
|
||||
$value .= $chunk;
|
||||
list (, $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;
|
||||
list ($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;
|
||||
}
|
||||
|
||||
list ($char, $code, $bytes) = $this->readChar();
|
||||
}
|
||||
|
||||
if ($code !== 34) {
|
||||
throw new SyntaxError(
|
||||
$this->source,
|
||||
$this->position,
|
||||
'Unterminated string.'
|
||||
);
|
||||
}
|
||||
|
||||
$value .= $chunk;
|
||||
|
||||
// Skip trailing quote:
|
||||
$this->moveStringCursor(1, 1);
|
||||
|
||||
return new Token(
|
||||
Token::STRING,
|
||||
$start,
|
||||
$this->position,
|
||||
$line,
|
||||
$col,
|
||||
$prev,
|
||||
$value
|
||||
);
|
||||
}
|
||||
|
||||
private function assertValidStringCharacterCode($code, $position)
|
||||
{
|
||||
// SourceCharacter
|
||||
if ($code < 0x0020 && $code !== 0x0009) {
|
||||
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) {
|
||||
list(, $code, $bytes) = $this->readChar();
|
||||
|
||||
// Skip whitespace
|
||||
// tab | space | comma | BOM
|
||||
if ($code === 9 || $code === 32 || $code === 44 || $code === 0xFEFF) {
|
||||
$this->moveStringCursor(1, $bytes);
|
||||
} else if ($code === 10) { // new line
|
||||
$this->moveStringCursor(1, $bytes);
|
||||
$this->line++;
|
||||
$this->lineStart = $this->position;
|
||||
} else if ($code === 13) { // carriage return
|
||||
list(, $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 $line
|
||||
* @param $col
|
||||
* @param Token $prev
|
||||
* @return Token
|
||||
*/
|
||||
private function readComment($line, $col, Token $prev)
|
||||
{
|
||||
$start = $this->position;
|
||||
$value = '';
|
||||
$bytes = 1;
|
||||
|
||||
do {
|
||||
list ($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 array
|
||||
*/
|
||||
private function readChar($advance = false, $byteStreamPosition = null)
|
||||
{
|
||||
if ($byteStreamPosition === null) {
|
||||
$byteStreamPosition = $this->byteStreamPosition;
|
||||
}
|
||||
|
||||
$code = 0;
|
||||
$utf8char = '';
|
||||
$bytes = 0;
|
||||
$positionOffset = 0;
|
||||
|
||||
if (isset($this->source->body[$byteStreamPosition])) {
|
||||
$ord = ord($this->source->body[$byteStreamPosition]);
|
||||
|
||||
if ($ord < 128) {
|
||||
$bytes = 1;
|
||||
} else if ($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 $numberOfChars
|
||||
* @param bool $advance
|
||||
* @param null $byteStreamPosition
|
||||
* @return array
|
||||
*/
|
||||
private function readChars($numberOfChars, $advance = false, $byteStreamPosition = null)
|
||||
{
|
||||
$result = '';
|
||||
$totalBytes = 0;
|
||||
$byteOffset = $byteStreamPosition ?: $this->byteStreamPosition;
|
||||
|
||||
for ($i = 0; $i < $numberOfChars; $i++) {
|
||||
list ($char, $code, $bytes) = $this->readChar(false, $byteOffset);
|
||||
$totalBytes += $bytes;
|
||||
$byteOffset += $bytes;
|
||||
$result .= $char;
|
||||
}
|
||||
if ($advance) {
|
||||
$this->moveStringCursor($numberOfChars, $totalBytes);
|
||||
}
|
||||
return [$result, $totalBytes];
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves internal string cursor position
|
||||
*
|
||||
* @param $positionOffset
|
||||
* @param $byteStreamOffset
|
||||
* @return $this
|
||||
*/
|
||||
private function moveStringCursor($positionOffset, $byteStreamOffset)
|
||||
{
|
||||
$this->position += $positionOffset;
|
||||
$this->byteStreamPosition += $byteStreamOffset;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
+1259
File diff suppressed because it is too large.
Load diff
+310
@@ -0,0 +1,310 @@
|
||||
<?php
|
||||
namespace GraphQL\Language;
|
||||
|
||||
use GraphQL\Language\AST\ArgumentNode;
|
||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||
use GraphQL\Language\AST\EnumTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\InputValueDefinitionNode;
|
||||
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\ListValueNode;
|
||||
use GraphQL\Language\AST\BooleanValueNode;
|
||||
use GraphQL\Language\AST\DirectiveNode;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\AST\EnumValueNode;
|
||||
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\IntValueNode;
|
||||
use GraphQL\Language\AST\ListTypeNode;
|
||||
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\ObjectValueNode;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\AST\OperationTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\SchemaDefinitionNode;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Language\AST\StringValueNode;
|
||||
use GraphQL\Language\AST\TypeExtensionDefinitionNode;
|
||||
use GraphQL\Language\AST\UnionTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @api
|
||||
* @param Node $ast
|
||||
* @return string
|
||||
*/
|
||||
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 => function(Node $node) {
|
||||
return '' . $node->value;
|
||||
},
|
||||
NodeKind::VARIABLE => 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 => function(ArgumentNode $node) {
|
||||
return $node->name . ': ' . $node->value;
|
||||
},
|
||||
|
||||
// Fragments
|
||||
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) {
|
||||
return "fragment {$node->name} on {$node->typeCondition} "
|
||||
. $this->wrap('', $this->join($node->directives, ' '), ' ')
|
||||
. $node->selectionSet;
|
||||
},
|
||||
|
||||
// Value
|
||||
NodeKind::INT => function(IntValueNode $node) {
|
||||
return $node->value;
|
||||
},
|
||||
NodeKind::FLOAT => function(FloatValueNode $node) {
|
||||
return $node->value;
|
||||
},
|
||||
NodeKind::STRING => function(StringValueNode $node) {
|
||||
return json_encode($node->value);
|
||||
},
|
||||
NodeKind::BOOLEAN => function(BooleanValueNode $node) {
|
||||
return $node->value ? 'true' : 'false';
|
||||
},
|
||||
NodeKind::NULL => function(NullValueNode $node) {
|
||||
return 'null';
|
||||
},
|
||||
NodeKind::ENUM => 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 => function(ObjectFieldNode $node) {
|
||||
return $node->name . ': ' . $node->value;
|
||||
},
|
||||
|
||||
// DirectiveNode
|
||||
NodeKind::DIRECTIVE => function(DirectiveNode $node) {
|
||||
return '@' . $node->name . $this->wrap('(', $this->join($node->arguments, ', '), ')');
|
||||
},
|
||||
|
||||
// Type
|
||||
NodeKind::NAMED_TYPE => function(NamedTypeNode $node) {
|
||||
return $node->name;
|
||||
},
|
||||
NodeKind::LIST_TYPE => function(ListTypeNode $node) {
|
||||
return '[' . $node->type . ']';
|
||||
},
|
||||
NodeKind::NON_NULL_TYPE => function(NonNullTypeNode $node) {
|
||||
return $node->type . '!';
|
||||
},
|
||||
|
||||
// Type System Definitions
|
||||
NodeKind::SCHEMA_DEFINITION => function(SchemaDefinitionNode $def) {
|
||||
return $this->join([
|
||||
'schema',
|
||||
$this->join($def->directives, ' '),
|
||||
$this->block($def->operationTypes)
|
||||
], ' ');
|
||||
},
|
||||
NodeKind::OPERATION_TYPE_DEFINITION => function(OperationTypeDefinitionNode $def) {
|
||||
return $def->operation . ': ' . $def->type;
|
||||
},
|
||||
|
||||
NodeKind::SCALAR_TYPE_DEFINITION => function(ScalarTypeDefinitionNode $def) {
|
||||
return $this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' ');
|
||||
},
|
||||
NodeKind::OBJECT_TYPE_DEFINITION => 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 => function(FieldDefinitionNode $def) {
|
||||
return $def->name
|
||||
. $this->wrap('(', $this->join($def->arguments, ', '), ')')
|
||||
. ': ' . $def->type
|
||||
. $this->wrap(' ', $this->join($def->directives, ' '));
|
||||
},
|
||||
NodeKind::INPUT_VALUE_DEFINITION => function(InputValueDefinitionNode $def) {
|
||||
return $this->join([
|
||||
$def->name . ': ' . $def->type,
|
||||
$this->wrap('= ', $def->defaultValue),
|
||||
$this->join($def->directives, ' ')
|
||||
], ' ');
|
||||
},
|
||||
NodeKind::INTERFACE_TYPE_DEFINITION => function(InterfaceTypeDefinitionNode $def) {
|
||||
return $this->join([
|
||||
'interface',
|
||||
$def->name,
|
||||
$this->join($def->directives, ' '),
|
||||
$this->block($def->fields)
|
||||
], ' ');
|
||||
},
|
||||
NodeKind::UNION_TYPE_DEFINITION => function(UnionTypeDefinitionNode $def) {
|
||||
return $this->join([
|
||||
'union',
|
||||
$def->name,
|
||||
$this->join($def->directives, ' '),
|
||||
'= ' . $this->join($def->types, ' | ')
|
||||
], ' ');
|
||||
},
|
||||
NodeKind::ENUM_TYPE_DEFINITION => function(EnumTypeDefinitionNode $def) {
|
||||
return $this->join([
|
||||
'enum',
|
||||
$def->name,
|
||||
$this->join($def->directives, ' '),
|
||||
$this->block($def->values)
|
||||
], ' ');
|
||||
},
|
||||
NodeKind::ENUM_VALUE_DEFINITION => function(EnumValueDefinitionNode $def) {
|
||||
return $this->join([
|
||||
$def->name,
|
||||
$this->join($def->directives, ' ')
|
||||
], ' ');
|
||||
},
|
||||
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => function(InputObjectTypeDefinitionNode $def) {
|
||||
return $this->join([
|
||||
'input',
|
||||
$def->name,
|
||||
$this->join($def->directives, ' '),
|
||||
$this->block($def->fields)
|
||||
], ' ');
|
||||
},
|
||||
NodeKind::TYPE_EXTENSION_DEFINITION => function(TypeExtensionDefinitionNode $def) {
|
||||
return "extend {$def->definition}";
|
||||
},
|
||||
NodeKind::DIRECTIVE_DEFINITION => function(DirectiveDefinitionNode $def) {
|
||||
return 'directive @' . $def->name . $this->wrap('(', $this->join($def->arguments, ', '), ')')
|
||||
. ' on ' . $this->join($def->locations, ' | ');
|
||||
}
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) ? $this->indent("{\n" . $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,
|
||||
function($x) { return !!$x;}
|
||||
)
|
||||
)
|
||||
: '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
namespace GraphQL\Language;
|
||||
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Class Source
|
||||
* @package GraphQL\Language
|
||||
*/
|
||||
class Source
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $body;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $length;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var SourceLocation
|
||||
*/
|
||||
public $locationOffset;
|
||||
|
||||
/**
|
||||
* Source constructor.
|
||||
*
|
||||
* 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 $body
|
||||
* @param null $name
|
||||
* @param SourceLocation|null $location
|
||||
*/
|
||||
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';
|
||||
$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 $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,45 @@
|
||||
<?php
|
||||
namespace GraphQL\Language;
|
||||
|
||||
class SourceLocation implements \JsonSerializable
|
||||
{
|
||||
public $line;
|
||||
public $column;
|
||||
|
||||
public function __construct($line, $col)
|
||||
{
|
||||
$this->line = $line;
|
||||
$this->column = $col;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'line' => $this->line,
|
||||
'column' => $this->column
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toSerializableArray()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @since 5.4.0
|
||||
*/
|
||||
function jsonSerialize()
|
||||
{
|
||||
return $this->toSerializableArray();
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
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 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 COMMENT = 'Comment';
|
||||
|
||||
/**
|
||||
* @param $kind
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getKindDescription($kind)
|
||||
{
|
||||
trigger_error('Deprecated as of 16.10.2016 ($kind itself contains description string now)', E_USER_DEPRECATED);
|
||||
|
||||
$description = [];
|
||||
$description[self::SOF] = '<SOF>';
|
||||
$description[self::EOF] = '<EOF>';
|
||||
$description[self::BANG] = '!';
|
||||
$description[self::DOLLAR] = '$';
|
||||
$description[self::PAREN_L] = '(';
|
||||
$description[self::PAREN_R] = ')';
|
||||
$description[self::SPREAD] = '...';
|
||||
$description[self::COLON] = ':';
|
||||
$description[self::EQUALS] = '=';
|
||||
$description[self::AT] = '@';
|
||||
$description[self::BRACKET_L] = '[';
|
||||
$description[self::BRACKET_R] = ']';
|
||||
$description[self::BRACE_L] = '{';
|
||||
$description[self::PIPE] = '|';
|
||||
$description[self::BRACE_R] = '}';
|
||||
$description[self::NAME] = 'Name';
|
||||
$description[self::INT] = 'Int';
|
||||
$description[self::FLOAT] = 'Float';
|
||||
$description[self::STRING] = 'String';
|
||||
$description[self::COMMENT] = 'Comment';
|
||||
|
||||
return $description[$kind];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Token constructor.
|
||||
* @param $kind
|
||||
* @param $start
|
||||
* @param $end
|
||||
* @param $line
|
||||
* @param $column
|
||||
* @param Token $previous
|
||||
* @param null $value
|
||||
*/
|
||||
public function __construct($kind, $start, $end, $line, $column, Token $previous = null, $value = null)
|
||||
{
|
||||
$this->kind = $kind;
|
||||
$this->start = (int) $start;
|
||||
$this->end = (int) $end;
|
||||
$this->line = (int) $line;
|
||||
$this->column = (int) $column;
|
||||
$this->prev = $previous;
|
||||
$this->next = null;
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->kind . ($this->value ? ' "' . $this->value . '"' : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'kind' => $this->kind,
|
||||
'value' => $this->value,
|
||||
'line' => $this->line,
|
||||
'column' => $this->column
|
||||
];
|
||||
}
|
||||
}
|
||||
+486
@@ -0,0 +1,486 @@
|
||||
<?php
|
||||
namespace GraphQL\Language;
|
||||
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\NodeList;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
|
||||
class VisitorOperation
|
||||
{
|
||||
public $doBreak;
|
||||
|
||||
public $doContinue;
|
||||
|
||||
public $removeNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
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', '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 => ['name', 'directives'],
|
||||
NodeKind::OBJECT_TYPE_DEFINITION => ['name', 'interfaces', 'directives', 'fields'],
|
||||
NodeKind::FIELD_DEFINITION => ['name', 'arguments', 'type', 'directives'],
|
||||
NodeKind::INPUT_VALUE_DEFINITION => ['name', 'type', 'defaultValue', 'directives'],
|
||||
NodeKind::INTERFACE_TYPE_DEFINITION => [ 'name', 'directives', 'fields' ],
|
||||
NodeKind::UNION_TYPE_DEFINITION => [ 'name', 'directives', 'types' ],
|
||||
NodeKind::ENUM_TYPE_DEFINITION => [ 'name', 'directives', 'values' ],
|
||||
NodeKind::ENUM_VALUE_DEFINITION => [ 'name', 'directives' ],
|
||||
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => [ 'name', 'directives', 'fields' ],
|
||||
NodeKind::TYPE_EXTENSION_DEFINITION => [ 'definition' ],
|
||||
NodeKind::DIRECTIVE_DEFINITION => [ 'name', 'arguments', 'locations' ]
|
||||
];
|
||||
|
||||
/**
|
||||
* Visit the AST (see class description for details)
|
||||
*
|
||||
* @api
|
||||
* @param Node $root
|
||||
* @param array $visitor
|
||||
* @param array $keyMap
|
||||
* @return Node|mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
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 = count($ancestors) === 0 ? $UNDEFINED : array_pop($path);
|
||||
$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 ? ($inArray ? $index : $keys[$index]) : $UNDEFINED;
|
||||
$node = $parent ? (($parent instanceof NodeList || is_array($parent)) ? $parent[$key] : $parent->{$key}) : $newRoot;
|
||||
if ($node === null || $node === $UNDEFINED) {
|
||||
continue;
|
||||
}
|
||||
if ($parent) {
|
||||
$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);
|
||||
|
||||
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) {
|
||||
$node = $editValue;
|
||||
} else {
|
||||
array_pop($path);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($result === null && $isEdited) {
|
||||
$edits[] = [$key, $node];
|
||||
}
|
||||
|
||||
if (!$isLeaving) {
|
||||
$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) {
|
||||
$ancestors[] = $parent;
|
||||
}
|
||||
$parent = $node;
|
||||
}
|
||||
|
||||
} while ($stack);
|
||||
|
||||
if (count($edits) !== 0) {
|
||||
$newRoot = $edits[0][1];
|
||||
}
|
||||
|
||||
return $newRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns marker for visitor break
|
||||
*
|
||||
* @api
|
||||
* @return VisitorOperation
|
||||
*/
|
||||
public static function stop()
|
||||
{
|
||||
$r = new VisitorOperation();
|
||||
$r->doBreak = true;
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns marker for skipping current node
|
||||
*
|
||||
* @api
|
||||
* @return VisitorOperation
|
||||
*/
|
||||
public static function skipNode()
|
||||
{
|
||||
$r = new VisitorOperation();
|
||||
$r->doContinue = true;
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns marker for removing a node
|
||||
*
|
||||
* @api
|
||||
* @return VisitorOperation
|
||||
*/
|
||||
public static function removeNode()
|
||||
{
|
||||
$r = new VisitorOperation();
|
||||
$r->removeNode = true;
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $visitors
|
||||
* @return array
|
||||
*/
|
||||
static function visitInParallel($visitors)
|
||||
{
|
||||
$visitorsCount = count($visitors);
|
||||
$skipping = new \SplFixedArray($visitorsCount);
|
||||
|
||||
return [
|
||||
'enter' => function ($node) use ($visitors, $skipping, $visitorsCount) {
|
||||
for ($i = 0; $i < $visitorsCount; $i++) {
|
||||
if (empty($skipping[$i])) {
|
||||
$fn = self::getVisitFn($visitors[$i], $node->kind, /* isLeaving */ false);
|
||||
|
||||
if ($fn) {
|
||||
$result = call_user_func_array($fn, func_get_args());
|
||||
|
||||
if ($result instanceof VisitorOperation) {
|
||||
if ($result->doContinue) {
|
||||
$skipping[$i] = $node;
|
||||
} else if ($result->doBreak) {
|
||||
$skipping[$i] = $result;
|
||||
} else if ($result->removeNode) {
|
||||
return $result;
|
||||
}
|
||||
} else if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'leave' => function ($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;
|
||||
} else if ($result->removeNode) {
|
||||
return $result;
|
||||
}
|
||||
} else if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
} else if ($skipping[$i] === $node) {
|
||||
$skipping[$i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new visitor instance which maintains a provided TypeInfo instance
|
||||
* along with visiting visitor.
|
||||
*/
|
||||
static function visitWithTypeInfo(TypeInfo $typeInfo, $visitor)
|
||||
{
|
||||
return [
|
||||
'enter' => function ($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) {
|
||||
$typeInfo->leave($node);
|
||||
if ($result instanceof Node) {
|
||||
$typeInfo->enter($result);
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
'leave' => function ($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 $visitor
|
||||
* @param $kind
|
||||
* @param $isLeaving
|
||||
* @return null
|
||||
*/
|
||||
public static function getVisitFn($visitor, $kind, $isLeaving)
|
||||
{
|
||||
if (!$visitor) {
|
||||
return null;
|
||||
}
|
||||
$kindVisitor = isset($visitor[$kind]) ? $visitor[$kind] : null;
|
||||
|
||||
if (!$isLeaving && is_callable($kindVisitor)) {
|
||||
// { Kind() {} }
|
||||
return $kindVisitor;
|
||||
}
|
||||
|
||||
if (is_array($kindVisitor)) {
|
||||
if ($isLeaving) {
|
||||
$kindSpecificVisitor = isset($kindVisitor['leave']) ? $kindVisitor['leave'] : null;
|
||||
} else {
|
||||
$kindSpecificVisitor = isset($kindVisitor['enter']) ? $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 = isset($specificVisitor[$kind]) ? $specificVisitor[$kind] : null;
|
||||
|
||||
if (is_callable($specificKindVisitor)) {
|
||||
// { enter: { Kind() {} }, leave: { Kind() {} } }
|
||||
return $specificKindVisitor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL;
|
||||
|
||||
|
||||
/**
|
||||
* Schema Definition
|
||||
*
|
||||
* @deprecated moved to GraphQL\Type\Schema
|
||||
*/
|
||||
class Schema extends \GraphQL\Type\Schema
|
||||
{
|
||||
}
|
||||
+603
@@ -0,0 +1,603 @@
|
||||
<?php
|
||||
namespace GraphQL;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Executor\ExecutionResult;
|
||||
use GraphQL\Executor\Executor;
|
||||
use GraphQL\Executor\Promise\PromiseAdapter;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Resolution;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
trigger_error(
|
||||
'GraphQL\Server is deprecated in favor of new implementation: GraphQL\Server\StandardServer and will be removed in next version',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
|
||||
class Server
|
||||
{
|
||||
const DEBUG_PHP_ERRORS = 1;
|
||||
const DEBUG_EXCEPTIONS = 2;
|
||||
const DEBUG_ALL = 7;
|
||||
|
||||
private $queryType;
|
||||
|
||||
private $mutationType;
|
||||
|
||||
private $subscriptionType;
|
||||
|
||||
private $directives;
|
||||
|
||||
private $types = [];
|
||||
|
||||
private $schema;
|
||||
|
||||
private $debug = 0;
|
||||
|
||||
private $contextValue;
|
||||
|
||||
private $rootValue;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $phpErrorFormatter = ['GraphQL\Error\FormattedError', 'createFromPHPError'];
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $exceptionFormatter = ['GraphQL\Error\FormattedError', 'createFromException'];
|
||||
|
||||
private $unexpectedErrorMessage = 'Unexpected Error';
|
||||
|
||||
private $unexpectedErrorStatus = 500;
|
||||
|
||||
private $validationRules;
|
||||
|
||||
/**
|
||||
* @var Resolution
|
||||
*/
|
||||
private $typeResolutionStrategy;
|
||||
|
||||
/**
|
||||
* @var PromiseAdapter
|
||||
*/
|
||||
private $promiseAdapter;
|
||||
|
||||
private $phpErrors = [];
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function create()
|
||||
{
|
||||
return new static();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ObjectType|null
|
||||
*/
|
||||
public function getQueryType()
|
||||
{
|
||||
return $this->queryType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType $queryType
|
||||
* @return Server
|
||||
*/
|
||||
public function setQueryType(ObjectType $queryType)
|
||||
{
|
||||
$this->assertSchemaNotSet('Query Type', __METHOD__);
|
||||
$this->queryType = $queryType;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ObjectType|null
|
||||
*/
|
||||
public function getMutationType()
|
||||
{
|
||||
return $this->mutationType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType $mutationType
|
||||
* @return Server
|
||||
*/
|
||||
public function setMutationType(ObjectType $mutationType)
|
||||
{
|
||||
$this->assertSchemaNotSet('Mutation Type', __METHOD__);
|
||||
$this->mutationType = $mutationType;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ObjectType|null
|
||||
*/
|
||||
public function getSubscriptionType()
|
||||
{
|
||||
return $this->subscriptionType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType $subscriptionType
|
||||
* @return Server
|
||||
*/
|
||||
public function setSubscriptionType($subscriptionType)
|
||||
{
|
||||
$this->assertSchemaNotSet('Subscription Type', __METHOD__);
|
||||
$this->subscriptionType = $subscriptionType;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Type[] $types
|
||||
* @return Server
|
||||
*/
|
||||
public function addTypes(array $types)
|
||||
{
|
||||
if (!empty($types)) {
|
||||
$this->assertSchemaNotSet('Types', __METHOD__);
|
||||
$this->types = array_merge($this->types, $types);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getTypes()
|
||||
{
|
||||
return $this->types;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Directive[]
|
||||
*/
|
||||
public function getDirectives()
|
||||
{
|
||||
if (null === $this->directives) {
|
||||
$this->directives = Directive::getInternalDirectives();
|
||||
}
|
||||
|
||||
return $this->directives;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Directive[] $directives
|
||||
* @return Server
|
||||
*/
|
||||
public function setDirectives(array $directives)
|
||||
{
|
||||
$this->assertSchemaNotSet('Directives', __METHOD__);
|
||||
$this->directives = $directives;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDebug()
|
||||
{
|
||||
return $this->debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $debug
|
||||
* @return Server
|
||||
*/
|
||||
public function setDebug($debug = self::DEBUG_ALL)
|
||||
{
|
||||
$this->debug = (int) $debug;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getContext()
|
||||
{
|
||||
return $this->contextValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $context
|
||||
* @return Server
|
||||
*/
|
||||
public function setContext($context)
|
||||
{
|
||||
$this->contextValue = $context;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $rootValue
|
||||
* @return Server
|
||||
*/
|
||||
public function setRootValue($rootValue)
|
||||
{
|
||||
$this->rootValue = $rootValue;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRootValue()
|
||||
{
|
||||
return $this->rootValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set schema instance manually. Mutually exclusive with `setQueryType`, `setMutationType`, `setSubscriptionType`, `setDirectives`, `addTypes`
|
||||
*
|
||||
* @param Schema $schema
|
||||
* @return $this
|
||||
*/
|
||||
public function setSchema(Schema $schema)
|
||||
{
|
||||
if ($this->queryType) {
|
||||
$err = 'Query Type is already set';
|
||||
$errMethod = __CLASS__ . '::setQueryType';
|
||||
} else if ($this->mutationType) {
|
||||
$err = 'Mutation Type is already set';
|
||||
$errMethod = __CLASS__ . '::setMutationType';
|
||||
} else if ($this->subscriptionType) {
|
||||
$err = 'Subscription Type is already set';
|
||||
$errMethod = __CLASS__ . '::setSubscriptionType';
|
||||
} else if ($this->directives) {
|
||||
$err = 'Directives are already set';
|
||||
$errMethod = __CLASS__ . '::setDirectives';
|
||||
} else if ($this->types) {
|
||||
$err = 'Additional types are already set';
|
||||
$errMethod = __CLASS__ . '::addTypes';
|
||||
} else if ($this->typeResolutionStrategy) {
|
||||
$err = 'Type Resolution Strategy is already set';
|
||||
$errMethod = __CLASS__ . '::setTypeResolutionStrategy';
|
||||
} else if ($this->schema && $this->schema !== $schema) {
|
||||
$err = 'Different schema is already set';
|
||||
}
|
||||
|
||||
if (isset($err)) {
|
||||
if (isset($errMethod)) {
|
||||
$err .= " ($errMethod is mutually exclusive with " . __METHOD__ . ")";
|
||||
}
|
||||
throw new InvariantViolation("Cannot set Schema on Server: $err");
|
||||
}
|
||||
$this->schema = $schema;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $entry
|
||||
* @param $mutuallyExclusiveMethod
|
||||
*/
|
||||
private function assertSchemaNotSet($entry, $mutuallyExclusiveMethod)
|
||||
{
|
||||
if ($this->schema) {
|
||||
$schemaMethod = __CLASS__ . '::setSchema';
|
||||
throw new InvariantViolation("Cannot set $entry on Server: Schema is already set ($mutuallyExclusiveMethod is mutually exclusive with $schemaMethod)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Schema
|
||||
*/
|
||||
public function getSchema()
|
||||
{
|
||||
if (null === $this->schema) {
|
||||
$this->schema = new Schema([
|
||||
'query' => $this->queryType,
|
||||
'mutation' => $this->mutationType,
|
||||
'subscription' => $this->subscriptionType,
|
||||
'directives' => $this->directives,
|
||||
'types' => $this->types,
|
||||
'typeResolution' => $this->typeResolutionStrategy
|
||||
]);
|
||||
}
|
||||
return $this->schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return callable
|
||||
*/
|
||||
public function getPhpErrorFormatter()
|
||||
{
|
||||
return $this->phpErrorFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects function(\ErrorException $e) : array
|
||||
*
|
||||
* @param callable $phpErrorFormatter
|
||||
* @return $this
|
||||
*/
|
||||
public function setPhpErrorFormatter(callable $phpErrorFormatter)
|
||||
{
|
||||
$this->phpErrorFormatter = $phpErrorFormatter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return callable
|
||||
*/
|
||||
public function getExceptionFormatter()
|
||||
{
|
||||
return $this->exceptionFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects function(Exception $e) : array
|
||||
*
|
||||
* @param callable $exceptionFormatter
|
||||
* @return $this
|
||||
*/
|
||||
public function setExceptionFormatter(callable $exceptionFormatter)
|
||||
{
|
||||
$this->exceptionFormatter = $exceptionFormatter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getUnexpectedErrorMessage()
|
||||
{
|
||||
return $this->unexpectedErrorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $unexpectedErrorMessage
|
||||
* @return $this
|
||||
*/
|
||||
public function setUnexpectedErrorMessage($unexpectedErrorMessage)
|
||||
{
|
||||
$this->unexpectedErrorMessage = $unexpectedErrorMessage;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getUnexpectedErrorStatus()
|
||||
{
|
||||
return $this->unexpectedErrorStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $unexpectedErrorStatus
|
||||
* @return $this
|
||||
*/
|
||||
public function setUnexpectedErrorStatus($unexpectedErrorStatus)
|
||||
{
|
||||
$this->unexpectedErrorStatus = $unexpectedErrorStatus;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses GraphQL query string and returns Abstract Syntax Tree for this query
|
||||
*
|
||||
* @param string $query
|
||||
* @return Language\AST\DocumentNode
|
||||
* @throws \GraphQL\Error\SyntaxError
|
||||
*/
|
||||
public function parse($query)
|
||||
{
|
||||
return Parser::parse($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getValidationRules()
|
||||
{
|
||||
if (null === $this->validationRules) {
|
||||
$this->validationRules = DocumentValidator::allRules();
|
||||
}
|
||||
return $this->validationRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $validationRules
|
||||
* @return $this
|
||||
*/
|
||||
public function setValidationRules(array $validationRules)
|
||||
{
|
||||
$this->validationRules = $validationRules;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL!
|
||||
* This method can be removed or changed in future versions without a prior notice.
|
||||
*
|
||||
* @return Resolution
|
||||
*/
|
||||
public function getTypeResolutionStrategy()
|
||||
{
|
||||
return $this->typeResolutionStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL!
|
||||
* This method can be removed or changed in future versions without a prior notice.
|
||||
*
|
||||
* @param Resolution $typeResolutionStrategy
|
||||
* @return Server
|
||||
*/
|
||||
public function setTypeResolutionStrategy(Resolution $typeResolutionStrategy)
|
||||
{
|
||||
$this->assertSchemaNotSet('Type Resolution Strategy', __METHOD__);
|
||||
$this->typeResolutionStrategy = $typeResolutionStrategy;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PromiseAdapter|null
|
||||
*/
|
||||
public function getPromiseAdapter()
|
||||
{
|
||||
return $this->promiseAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* See /docs/data-fetching.md#async-php
|
||||
*
|
||||
* @param PromiseAdapter $promiseAdapter
|
||||
* @return Server
|
||||
*/
|
||||
public function setPromiseAdapter(PromiseAdapter $promiseAdapter)
|
||||
{
|
||||
if ($this->promiseAdapter && $promiseAdapter !== $this->promiseAdapter) {
|
||||
throw new InvariantViolation("Cannot set promise adapter: Different adapter is already set");
|
||||
}
|
||||
$this->promiseAdapter = $promiseAdapter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array with validation errors (empty when there are no validation errors)
|
||||
*
|
||||
* @param DocumentNode $query
|
||||
* @return array
|
||||
*/
|
||||
public function validate(DocumentNode $query)
|
||||
{
|
||||
try {
|
||||
$schema = $this->getSchema();
|
||||
} catch (InvariantViolation $e) {
|
||||
throw new InvariantViolation("Cannot validate, schema contains errors: {$e->getMessage()}", null, $e);
|
||||
}
|
||||
|
||||
return DocumentValidator::validate($schema, $query, $this->validationRules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes GraphQL query against this server and returns execution result
|
||||
*
|
||||
* @param string|DocumentNode $query
|
||||
* @param array|null $variables
|
||||
* @param string|null $operationName
|
||||
* @return ExecutionResult
|
||||
*/
|
||||
public function executeQuery($query, array $variables = null, $operationName = null)
|
||||
{
|
||||
$this->phpErrors = [];
|
||||
if ($this->debug & static::DEBUG_PHP_ERRORS) {
|
||||
// Catch custom errors (to report them in query results)
|
||||
set_error_handler(function($severity, $message, $file, $line) {
|
||||
$this->phpErrors[] = new \ErrorException($message, 0, $severity, $file, $line);
|
||||
});
|
||||
}
|
||||
|
||||
if ($this->promiseAdapter) {
|
||||
// TODO: inline GraphQL::executeAndReturnResult and pass promise adapter to executor constructor directly
|
||||
$promiseAdapter = Executor::getPromiseAdapter();
|
||||
Executor::setPromiseAdapter($this->promiseAdapter);
|
||||
}
|
||||
|
||||
$result = GraphQL::executeAndReturnResult(
|
||||
$this->getSchema(),
|
||||
$query,
|
||||
$this->getRootValue(),
|
||||
$this->getContext(),
|
||||
$variables,
|
||||
$operationName
|
||||
);
|
||||
|
||||
if (isset($promiseAdapter)) {
|
||||
Executor::setPromiseAdapter($promiseAdapter);
|
||||
}
|
||||
|
||||
// Add details about original exception in error entry (if any)
|
||||
if ($this->debug & static::DEBUG_EXCEPTIONS) {
|
||||
$result->setErrorFormatter([$this, 'formatError']);
|
||||
}
|
||||
|
||||
// Add reported PHP errors to result (if any)
|
||||
if (!empty($this->phpErrors) && ($this->debug & static::DEBUG_PHP_ERRORS)) {
|
||||
$result->extensions['phpErrors'] = array_map($this->phpErrorFormatter, $this->phpErrors);
|
||||
}
|
||||
|
||||
if ($this->debug & static::DEBUG_PHP_ERRORS) {
|
||||
restore_error_handler();
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphQL HTTP endpoint compatible with express-graphql
|
||||
*/
|
||||
public function handleRequest()
|
||||
{
|
||||
try {
|
||||
$httpStatus = 200;
|
||||
if (isset($_SERVER['CONTENT_TYPE']) && strpos($_SERVER['CONTENT_TYPE'], 'application/json') !== false) {
|
||||
$raw = $this->readInput();
|
||||
$data = json_decode($raw, true);
|
||||
|
||||
Utils::invariant(
|
||||
is_array($data),
|
||||
"GraphQL Server expects JSON object with keys 'query' and 'variables', but got " . Utils::getVariableType($data)
|
||||
);
|
||||
} else {
|
||||
$data = $_REQUEST ?: [];
|
||||
}
|
||||
$data += ['query' => null, 'variables' => null];
|
||||
$result = $this->executeQuery($data['query'], (array) $data['variables'])->toArray();
|
||||
} catch (\Exception $e) {
|
||||
// This is only possible for schema creation errors and some very unpredictable errors,
|
||||
// (all errors which occur during query execution are caught and included in final response)
|
||||
$httpStatus = $this->unexpectedErrorStatus;
|
||||
$error = new Error($this->unexpectedErrorMessage, null, null, null, null, $e);
|
||||
$result = ['errors' => [$this->formatError($error)]];
|
||||
} catch (\Throwable $e) {
|
||||
$httpStatus = $this->unexpectedErrorStatus;
|
||||
$error = new Error($this->unexpectedErrorMessage, null, null, null, null, $e);
|
||||
$result = ['errors' => [$this->formatError($error)]];
|
||||
}
|
||||
|
||||
$this->produceOutput($result, $httpStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Throwable $e
|
||||
*/
|
||||
private function formatException($e)
|
||||
{
|
||||
$formatter = $this->exceptionFormatter;
|
||||
return $formatter($e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Error $e
|
||||
* @return array
|
||||
*/
|
||||
public function formatError(\GraphQL\Error\Error $e)
|
||||
{
|
||||
$result = $e->toSerializableArray();
|
||||
|
||||
if (($this->debug & static::DEBUG_EXCEPTIONS) && $e->getPrevious()) {
|
||||
$result['exception'] = $this->formatException($e->getPrevious());
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function readInput()
|
||||
{
|
||||
return file_get_contents('php://input') ?: '';
|
||||
}
|
||||
|
||||
protected function produceOutput(array $result, $httpStatus)
|
||||
{
|
||||
header('Content-Type: application/json', true, $httpStatus);
|
||||
echo json_encode($result);
|
||||
}
|
||||
}
|
||||
+559
@@ -0,0 +1,559 @@
|
||||
<?php
|
||||
namespace GraphQL\Server;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\FormattedError;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Executor\ExecutionResult;
|
||||
use GraphQL\Executor\Executor;
|
||||
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
|
||||
use GraphQL\Executor\Promise\Promise;
|
||||
use GraphQL\Executor\Promise\PromiseAdapter;
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Utils\AST;
|
||||
use GraphQL\Utils\Utils;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* Contains functionality that could be re-used by various server implementations
|
||||
*/
|
||||
class Helper
|
||||
{
|
||||
|
||||
/**
|
||||
* Parses HTTP request using PHP globals and returns GraphQL OperationParams
|
||||
* contained in this request. For batched requests it returns an array of OperationParams.
|
||||
*
|
||||
* This function does not check validity of these params
|
||||
* (validation is performed separately in validateOperationParams() method).
|
||||
*
|
||||
* If $readRawBodyFn argument is not provided - will attempt to read raw request body
|
||||
* from `php://input` stream.
|
||||
*
|
||||
* Internally it normalizes input to $method, $bodyParams and $queryParams and
|
||||
* calls `parseRequestParams()` to produce actual return value.
|
||||
*
|
||||
* For PSR-7 request parsing use `parsePsrRequest()` instead.
|
||||
*
|
||||
* @api
|
||||
* @param callable|null $readRawBodyFn
|
||||
* @return OperationParams|OperationParams[]
|
||||
* @throws RequestError
|
||||
*/
|
||||
public function parseHttpRequest(callable $readRawBodyFn = null)
|
||||
{
|
||||
$method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null;
|
||||
$bodyParams = [];
|
||||
$urlParams = $_GET;
|
||||
|
||||
if ($method === 'POST') {
|
||||
$contentType = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : null;
|
||||
|
||||
if (stripos($contentType, 'application/graphql') !== false) {
|
||||
$rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody();
|
||||
$bodyParams = ['query' => $rawBody ?: ''];
|
||||
} else if (stripos($contentType, 'application/json') !== false) {
|
||||
$rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody();
|
||||
$bodyParams = json_decode($rawBody ?: '', true);
|
||||
|
||||
if (json_last_error()) {
|
||||
throw new RequestError("Could not parse JSON: " . json_last_error_msg());
|
||||
}
|
||||
if (!is_array($bodyParams)) {
|
||||
throw new RequestError(
|
||||
"GraphQL Server expects JSON object or array, but got " .
|
||||
Utils::printSafeJson($bodyParams)
|
||||
);
|
||||
}
|
||||
} else if (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
|
||||
$bodyParams = $_POST;
|
||||
} else if (null === $contentType) {
|
||||
throw new RequestError('Missing "Content-Type" header');
|
||||
} else {
|
||||
throw new RequestError("Unexpected content type: " . Utils::printSafeJson($contentType));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->parseRequestParams($method, $bodyParams, $urlParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses normalized request params and returns instance of OperationParams
|
||||
* or array of OperationParams in case of batch operation.
|
||||
*
|
||||
* Returned value is a suitable input for `executeOperation` or `executeBatch` (if array)
|
||||
*
|
||||
* @api
|
||||
* @param string $method
|
||||
* @param array $bodyParams
|
||||
* @param array $queryParams
|
||||
* @return OperationParams|OperationParams[]
|
||||
* @throws RequestError
|
||||
*/
|
||||
public function parseRequestParams($method, array $bodyParams, array $queryParams)
|
||||
{
|
||||
if ($method === 'GET') {
|
||||
$result = OperationParams::create($queryParams, true);
|
||||
} else if ($method === 'POST') {
|
||||
if (isset($bodyParams[0])) {
|
||||
$result = [];
|
||||
foreach ($bodyParams as $index => $entry) {
|
||||
$op = OperationParams::create($entry);
|
||||
$result[] = $op;
|
||||
}
|
||||
} else {
|
||||
$result = OperationParams::create($bodyParams);
|
||||
}
|
||||
} else {
|
||||
throw new RequestError('HTTP Method "' . $method . '" is not supported');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks validity of OperationParams extracted from HTTP request and returns an array of errors
|
||||
* if params are invalid (or empty array when params are valid)
|
||||
*
|
||||
* @api
|
||||
* @param OperationParams $params
|
||||
* @return Error[]
|
||||
*/
|
||||
public function validateOperationParams(OperationParams $params)
|
||||
{
|
||||
$errors = [];
|
||||
if (!$params->query && !$params->queryId) {
|
||||
$errors[] = new RequestError('GraphQL Request must include at least one of those two parameters: "query" or "queryId"');
|
||||
}
|
||||
if ($params->query && $params->queryId) {
|
||||
$errors[] = new RequestError('GraphQL Request parameters "query" and "queryId" are mutually exclusive');
|
||||
}
|
||||
|
||||
if ($params->query !== null && (!is_string($params->query) || empty($params->query))) {
|
||||
$errors[] = new RequestError(
|
||||
'GraphQL Request parameter "query" must be string, but got ' .
|
||||
Utils::printSafeJson($params->query)
|
||||
);
|
||||
}
|
||||
if ($params->queryId !== null && (!is_string($params->queryId) || empty($params->queryId))) {
|
||||
$errors[] = new RequestError(
|
||||
'GraphQL Request parameter "queryId" must be string, but got ' .
|
||||
Utils::printSafeJson($params->queryId)
|
||||
);
|
||||
}
|
||||
|
||||
if ($params->operation !== null && (!is_string($params->operation) || empty($params->operation))) {
|
||||
$errors[] = new RequestError(
|
||||
'GraphQL Request parameter "operation" must be string, but got ' .
|
||||
Utils::printSafeJson($params->operation)
|
||||
);
|
||||
}
|
||||
if ($params->variables !== null && (!is_array($params->variables) || isset($params->variables[0]))) {
|
||||
$errors[] = new RequestError(
|
||||
'GraphQL Request parameter "variables" must be object or JSON string parsed to object, but got ' .
|
||||
Utils::printSafeJson($params->getOriginalInput('variables'))
|
||||
);
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes GraphQL operation with given server configuration and returns execution result
|
||||
* (or promise when promise adapter is different from SyncPromiseAdapter)
|
||||
*
|
||||
* @api
|
||||
* @param ServerConfig $config
|
||||
* @param OperationParams $op
|
||||
*
|
||||
* @return ExecutionResult|Promise
|
||||
*/
|
||||
public function executeOperation(ServerConfig $config, OperationParams $op)
|
||||
{
|
||||
$promiseAdapter = $config->getPromiseAdapter() ?: Executor::getPromiseAdapter();
|
||||
$result = $this->promiseToExecuteOperation($promiseAdapter, $config, $op);
|
||||
|
||||
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
||||
$result = $promiseAdapter->wait($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes batched GraphQL operations with shared promise queue
|
||||
* (thus, effectively batching deferreds|promises of all queries at once)
|
||||
*
|
||||
* @api
|
||||
* @param ServerConfig $config
|
||||
* @param OperationParams[] $operations
|
||||
* @return ExecutionResult[]|Promise
|
||||
*/
|
||||
public function executeBatch(ServerConfig $config, array $operations)
|
||||
{
|
||||
$promiseAdapter = $config->getPromiseAdapter() ?: Executor::getPromiseAdapter();
|
||||
$result = [];
|
||||
|
||||
foreach ($operations as $operation) {
|
||||
$result[] = $this->promiseToExecuteOperation($promiseAdapter, $config, $operation, true);
|
||||
}
|
||||
|
||||
$result = $promiseAdapter->all($result);
|
||||
|
||||
// Wait for promised results when using sync promises
|
||||
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
||||
$result = $promiseAdapter->wait($result);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PromiseAdapter $promiseAdapter
|
||||
* @param ServerConfig $config
|
||||
* @param OperationParams $op
|
||||
* @param bool $isBatch
|
||||
* @return Promise
|
||||
*/
|
||||
private function promiseToExecuteOperation(PromiseAdapter $promiseAdapter, ServerConfig $config, OperationParams $op, $isBatch = false)
|
||||
{
|
||||
try {
|
||||
if (!$config->getSchema()) {
|
||||
throw new InvariantViolation("Schema is required for the server");
|
||||
}
|
||||
if ($isBatch && !$config->getQueryBatching()) {
|
||||
throw new RequestError("Batched queries are not supported by this server");
|
||||
}
|
||||
|
||||
$errors = $this->validateOperationParams($op);
|
||||
|
||||
if (!empty($errors)) {
|
||||
$errors = Utils::map($errors, function(RequestError $err) {
|
||||
return Error::createLocatedError($err, null, null);
|
||||
});
|
||||
return $promiseAdapter->createFulfilled(
|
||||
new ExecutionResult(null, $errors)
|
||||
);
|
||||
}
|
||||
|
||||
$doc = $op->queryId ? $this->loadPersistedQuery($config, $op) : $op->query;
|
||||
|
||||
if (!$doc instanceof DocumentNode) {
|
||||
$doc = Parser::parse($doc);
|
||||
}
|
||||
|
||||
$operationType = AST::getOperation($doc, $op->operation);
|
||||
if ($op->isReadOnly() && $operationType !== 'query') {
|
||||
throw new RequestError("GET supports only query operation");
|
||||
}
|
||||
|
||||
$result = GraphQL::promiseToExecute(
|
||||
$promiseAdapter,
|
||||
$config->getSchema(),
|
||||
$doc,
|
||||
$this->resolveRootValue($config, $op, $doc, $operationType),
|
||||
$this->resolveContextValue($config, $op, $doc, $operationType),
|
||||
$op->variables,
|
||||
$op->operation,
|
||||
$config->getFieldResolver(),
|
||||
$this->resolveValidationRules($config, $op, $doc, $operationType)
|
||||
);
|
||||
|
||||
} catch (RequestError $e) {
|
||||
$result = $promiseAdapter->createFulfilled(
|
||||
new ExecutionResult(null, [Error::createLocatedError($e)])
|
||||
);
|
||||
} catch (Error $e) {
|
||||
$result = $promiseAdapter->createFulfilled(
|
||||
new ExecutionResult(null, [$e])
|
||||
);
|
||||
}
|
||||
|
||||
$applyErrorHandling = function (ExecutionResult $result) use ($config) {
|
||||
if ($config->getErrorsHandler()) {
|
||||
$result->setErrorsHandler($config->getErrorsHandler());
|
||||
}
|
||||
if ($config->getErrorFormatter() || $config->getDebug()) {
|
||||
$result->setErrorFormatter(
|
||||
FormattedError::prepareFormatter($config->getErrorFormatter(),
|
||||
$config->getDebug())
|
||||
);
|
||||
}
|
||||
return $result;
|
||||
};
|
||||
|
||||
return $result->then($applyErrorHandling);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerConfig $config
|
||||
* @param OperationParams $op
|
||||
* @return mixed
|
||||
* @throws RequestError
|
||||
*/
|
||||
private function loadPersistedQuery(ServerConfig $config, OperationParams $op)
|
||||
{
|
||||
// Load query if we got persisted query id:
|
||||
$loader = $config->getPersistentQueryLoader();
|
||||
|
||||
if (!$loader) {
|
||||
throw new RequestError("Persisted queries are not supported by this server");
|
||||
}
|
||||
|
||||
$source = $loader($op->queryId, $op);
|
||||
|
||||
if (!is_string($source) && !$source instanceof DocumentNode) {
|
||||
throw new InvariantViolation(sprintf(
|
||||
"Persistent query loader must return query string or instance of %s but got: %s",
|
||||
DocumentNode::class,
|
||||
Utils::printSafe($source)
|
||||
));
|
||||
}
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerConfig $config
|
||||
* @param OperationParams $params
|
||||
* @param DocumentNode $doc
|
||||
* @param $operationType
|
||||
* @return array
|
||||
*/
|
||||
private function resolveValidationRules(ServerConfig $config, OperationParams $params, DocumentNode $doc, $operationType)
|
||||
{
|
||||
// Allow customizing validation rules per operation:
|
||||
$validationRules = $config->getValidationRules();
|
||||
|
||||
if (is_callable($validationRules)) {
|
||||
$validationRules = $validationRules($params, $doc, $operationType);
|
||||
|
||||
if (!is_array($validationRules)) {
|
||||
throw new InvariantViolation(sprintf(
|
||||
"Expecting validation rules to be array or callable returning array, but got: %s",
|
||||
Utils::printSafe($validationRules)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return $validationRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerConfig $config
|
||||
* @param OperationParams $params
|
||||
* @param DocumentNode $doc
|
||||
* @param $operationType
|
||||
* @return mixed
|
||||
*/
|
||||
private function resolveRootValue(ServerConfig $config, OperationParams $params, DocumentNode $doc, $operationType)
|
||||
{
|
||||
$root = $config->getRootValue();
|
||||
|
||||
if ($root instanceof \Closure) {
|
||||
$root = $root($params, $doc, $operationType);
|
||||
}
|
||||
|
||||
return $root;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerConfig $config
|
||||
* @param OperationParams $params
|
||||
* @param DocumentNode $doc
|
||||
* @param $operationType
|
||||
* @return mixed
|
||||
*/
|
||||
private function resolveContextValue(ServerConfig $config, OperationParams $params, DocumentNode $doc, $operationType)
|
||||
{
|
||||
$context = $config->getContext();
|
||||
|
||||
if ($context instanceof \Closure) {
|
||||
$context = $context($params, $doc, $operationType);
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send response using standard PHP `header()` and `echo`.
|
||||
*
|
||||
* @api
|
||||
* @param Promise|ExecutionResult|ExecutionResult[] $result
|
||||
* @param bool $exitWhenDone
|
||||
*/
|
||||
public function sendResponse($result, $exitWhenDone = false)
|
||||
{
|
||||
if ($result instanceof Promise) {
|
||||
$result->then(function($actualResult) use ($exitWhenDone) {
|
||||
$this->doSendResponse($actualResult, $exitWhenDone);
|
||||
});
|
||||
} else {
|
||||
$this->doSendResponse($result, $exitWhenDone);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $result
|
||||
* @param $exitWhenDone
|
||||
*/
|
||||
private function doSendResponse($result, $exitWhenDone)
|
||||
{
|
||||
$httpStatus = $this->resolveHttpStatus($result);
|
||||
$this->emitResponse($result, $httpStatus, $exitWhenDone);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|\JsonSerializable $jsonSerializable
|
||||
* @param int $httpStatus
|
||||
* @param bool $exitWhenDone
|
||||
*/
|
||||
public function emitResponse($jsonSerializable, $httpStatus, $exitWhenDone)
|
||||
{
|
||||
$body = json_encode($jsonSerializable);
|
||||
header('Content-Type: application/json', true, $httpStatus);
|
||||
echo $body;
|
||||
|
||||
if ($exitWhenDone) {
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|string
|
||||
*/
|
||||
private function readRawBody()
|
||||
{
|
||||
return file_get_contents('php://input');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $result
|
||||
* @return int
|
||||
*/
|
||||
private function resolveHttpStatus($result)
|
||||
{
|
||||
if (is_array($result) && isset($result[0])) {
|
||||
Utils::each($result, function ($executionResult, $index) {
|
||||
if (!$executionResult instanceof ExecutionResult) {
|
||||
throw new InvariantViolation(sprintf(
|
||||
"Expecting every entry of batched query result to be instance of %s but entry at position %d is %s",
|
||||
ExecutionResult::class,
|
||||
$index,
|
||||
Utils::printSafe($executionResult)
|
||||
));
|
||||
}
|
||||
});
|
||||
$httpStatus = 200;
|
||||
} else {
|
||||
if (!$result instanceof ExecutionResult) {
|
||||
throw new InvariantViolation(sprintf(
|
||||
"Expecting query result to be instance of %s but got %s",
|
||||
ExecutionResult::class,
|
||||
Utils::printSafe($result)
|
||||
));
|
||||
}
|
||||
if ($result->data === null && !empty($result->errors)) {
|
||||
$httpStatus = 400;
|
||||
} else {
|
||||
$httpStatus = 200;
|
||||
}
|
||||
}
|
||||
return $httpStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts PSR-7 request to OperationParams[]
|
||||
*
|
||||
* @api
|
||||
* @param ServerRequestInterface $request
|
||||
* @return array|Helper
|
||||
* @throws RequestError
|
||||
*/
|
||||
public function parsePsrRequest(ServerRequestInterface $request)
|
||||
{
|
||||
if ($request->getMethod() === 'GET') {
|
||||
$bodyParams = [];
|
||||
} else {
|
||||
$contentType = $request->getHeader('content-type');
|
||||
|
||||
if (!isset($contentType[0])) {
|
||||
throw new RequestError('Missing "Content-Type" header');
|
||||
}
|
||||
|
||||
if (stripos('application/graphql', $contentType[0]) !== false) {
|
||||
$bodyParams = ['query' => $request->getBody()->getContents()];
|
||||
} else if (stripos('application/json', $contentType[0]) !== false) {
|
||||
$bodyParams = $request->getParsedBody();
|
||||
|
||||
if (null === $bodyParams) {
|
||||
throw new InvariantViolation(
|
||||
"PSR-7 request is expected to provide parsed body for \"application/json\" requests but got null"
|
||||
);
|
||||
}
|
||||
|
||||
// Try parsing ourselves if PSR-7 implementation doesn't parse JSON automatically
|
||||
if (is_array($bodyParams) && empty($bodyParams)) {
|
||||
$bodyParams = json_decode($request->getBody(), true);
|
||||
|
||||
if (json_last_error()) {
|
||||
throw new RequestError("Could not parse JSON: " . json_last_error_msg());
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_array($bodyParams)) {
|
||||
throw new RequestError(
|
||||
"GraphQL Server expects JSON object or array, but got " .
|
||||
Utils::printSafeJson($bodyParams)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$bodyParams = $request->getParsedBody();
|
||||
|
||||
if (!is_array($bodyParams)) {
|
||||
throw new RequestError("Unexpected content type: " . Utils::printSafeJson($contentType[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->parseRequestParams(
|
||||
$request->getMethod(),
|
||||
$bodyParams,
|
||||
$request->getQueryParams()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts query execution result to PSR-7 response
|
||||
*
|
||||
* @api
|
||||
* @param Promise|ExecutionResult|ExecutionResult[] $result
|
||||
* @param ResponseInterface $response
|
||||
* @param StreamInterface $writableBodyStream
|
||||
* @return Promise|ResponseInterface
|
||||
*/
|
||||
public function toPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream)
|
||||
{
|
||||
if ($result instanceof Promise) {
|
||||
return $result->then(function($actualResult) use ($response, $writableBodyStream) {
|
||||
return $this->doConvertToPsrResponse($actualResult, $response, $writableBodyStream);
|
||||
});
|
||||
} else {
|
||||
return $this->doConvertToPsrResponse($result, $response, $writableBodyStream);
|
||||
}
|
||||
}
|
||||
|
||||
private function doConvertToPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream)
|
||||
{
|
||||
$httpStatus = $this->resolveHttpStatus($result);
|
||||
|
||||
$result = json_encode($result);
|
||||
$writableBodyStream->write($result);
|
||||
|
||||
return $response
|
||||
->withStatus($httpStatus)
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withBody($writableBodyStream);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
namespace GraphQL\Server;
|
||||
|
||||
/**
|
||||
* Structure representing parsed HTTP parameters for GraphQL operation
|
||||
*/
|
||||
class OperationParams
|
||||
{
|
||||
/**
|
||||
* Id of the query (when using persistent queries).
|
||||
*
|
||||
* Valid aliases (case-insensitive):
|
||||
* - id
|
||||
* - queryId
|
||||
* - documentId
|
||||
*
|
||||
* @api
|
||||
* @var string
|
||||
*/
|
||||
public $queryId;
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @var string
|
||||
*/
|
||||
public $query;
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @var string
|
||||
*/
|
||||
public $operation;
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @var array
|
||||
*/
|
||||
public $variables;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $originalInput;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $readOnly;
|
||||
|
||||
/**
|
||||
* Creates an instance from given array
|
||||
*
|
||||
* @api
|
||||
* @param array $params
|
||||
* @param bool $readonly
|
||||
* @return OperationParams
|
||||
*/
|
||||
public static function create(array $params, $readonly = false)
|
||||
{
|
||||
$instance = new static();
|
||||
|
||||
$params = array_change_key_case($params, CASE_LOWER);
|
||||
$instance->originalInput = $params;
|
||||
|
||||
$params += [
|
||||
'query' => null,
|
||||
'queryid' => null,
|
||||
'documentid' => null, // alias to queryid
|
||||
'id' => null, // alias to queryid
|
||||
'operation' => null,
|
||||
'variables' => null
|
||||
];
|
||||
|
||||
if ($params['variables'] === "") {
|
||||
$params['variables'] = null;
|
||||
}
|
||||
|
||||
if (is_string($params['variables'])) {
|
||||
$tmp = json_decode($params['variables'], true);
|
||||
if (!json_last_error()) {
|
||||
$params['variables'] = $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
$instance->query = $params['query'];
|
||||
$instance->queryId = $params['queryid'] ?: $params['documentid'] ?: $params['id'];
|
||||
$instance->operation = $params['operation'];
|
||||
$instance->variables = $params['variables'];
|
||||
$instance->readOnly = (bool) $readonly;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function getOriginalInput($key)
|
||||
{
|
||||
return isset($this->originalInput[$key]) ? $this->originalInput[$key] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that operation is executed in read-only context
|
||||
* (e.g. via HTTP GET request)
|
||||
*
|
||||
* @api
|
||||
* @return bool
|
||||
*/
|
||||
public function isReadOnly()
|
||||
{
|
||||
return $this->readOnly;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace GraphQL\Server;
|
||||
|
||||
use GraphQL\Error\ClientAware;
|
||||
|
||||
class RequestError extends \Exception implements ClientAware
|
||||
{
|
||||
/**
|
||||
* Returns true when exception message is safe to be displayed to client
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isClientSafe()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns string describing error category. E.g. "validation" for your own validation errors.
|
||||
*
|
||||
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
return 'request';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
namespace GraphQL\Server;
|
||||
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Executor\Promise\PromiseAdapter;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Server configuration class.
|
||||
* Could be passed directly to server constructor. List of options accepted by **create** method is
|
||||
* [described in docs](executing-queries.md#server-configuration-options).
|
||||
*
|
||||
* Usage example:
|
||||
*
|
||||
* $config = GraphQL\Server\ServerConfig::create()
|
||||
* ->setSchema($mySchema)
|
||||
* ->setContext($myContext);
|
||||
*
|
||||
* $server = new GraphQL\Server\StandardServer($config);
|
||||
*/
|
||||
class ServerConfig
|
||||
{
|
||||
/**
|
||||
* Converts an array of options to instance of ServerConfig
|
||||
* (or just returns empty config when array is not passed).
|
||||
*
|
||||
* @api
|
||||
* @param array $config
|
||||
* @return ServerConfig
|
||||
*/
|
||||
public static function create(array $config = [])
|
||||
{
|
||||
$instance = new static();
|
||||
foreach ($config as $key => $value) {
|
||||
$method = 'set' . ucfirst($key);
|
||||
if (!method_exists($instance, $method)) {
|
||||
throw new InvariantViolation("Unknown server config option \"$key\"");
|
||||
}
|
||||
$instance->$method($value);
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var Schema
|
||||
*/
|
||||
private $schema;
|
||||
|
||||
/**
|
||||
* @var mixed|\Closure
|
||||
*/
|
||||
private $context;
|
||||
|
||||
/**
|
||||
* @var mixed|\Closure
|
||||
*/
|
||||
private $rootValue;
|
||||
|
||||
/**
|
||||
* @var callable|null
|
||||
*/
|
||||
private $errorFormatter;
|
||||
|
||||
/**
|
||||
* @var callable|null
|
||||
*/
|
||||
private $errorsHandler;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $debug = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $queryBatching = false;
|
||||
|
||||
/**
|
||||
* @var array|callable
|
||||
*/
|
||||
private $validationRules;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $fieldResolver;
|
||||
|
||||
/**
|
||||
* @var PromiseAdapter
|
||||
*/
|
||||
private $promiseAdapter;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $persistentQueryLoader;
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @param Schema $schema
|
||||
* @return $this
|
||||
*/
|
||||
public function setSchema(Schema $schema)
|
||||
{
|
||||
$this->schema = $schema;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @param mixed|\Closure $context
|
||||
* @return $this
|
||||
*/
|
||||
public function setContext($context)
|
||||
{
|
||||
$this->context = $context;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @param mixed|\Closure $rootValue
|
||||
* @return $this
|
||||
*/
|
||||
public function setRootValue($rootValue)
|
||||
{
|
||||
$this->rootValue = $rootValue;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects function(Throwable $e) : array
|
||||
*
|
||||
* @api
|
||||
* @param callable $errorFormatter
|
||||
* @return $this
|
||||
*/
|
||||
public function setErrorFormatter(callable $errorFormatter)
|
||||
{
|
||||
$this->errorFormatter = $errorFormatter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects function(array $errors, callable $formatter) : array
|
||||
*
|
||||
* @api
|
||||
* @param callable $handler
|
||||
* @return $this
|
||||
*/
|
||||
public function setErrorsHandler(callable $handler)
|
||||
{
|
||||
$this->errorsHandler = $handler;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set validation rules for this server.
|
||||
*
|
||||
* @api
|
||||
* @param array|callable
|
||||
* @return $this
|
||||
*/
|
||||
public function setValidationRules($validationRules)
|
||||
{
|
||||
if (!is_callable($validationRules) && !is_array($validationRules) && $validationRules !== null) {
|
||||
throw new InvariantViolation(
|
||||
'Server config expects array of validation rules or callable returning such array, but got ' .
|
||||
Utils::printSafe($validationRules)
|
||||
);
|
||||
}
|
||||
|
||||
$this->validationRules = $validationRules;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @param callable $fieldResolver
|
||||
* @return $this
|
||||
*/
|
||||
public function setFieldResolver(callable $fieldResolver)
|
||||
{
|
||||
$this->fieldResolver = $fieldResolver;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects function($queryId, OperationParams $params) : string|DocumentNode
|
||||
*
|
||||
* This function must return query string or valid DocumentNode.
|
||||
*
|
||||
* @api
|
||||
* @param callable $persistentQueryLoader
|
||||
* @return $this
|
||||
*/
|
||||
public function setPersistentQueryLoader(callable $persistentQueryLoader)
|
||||
{
|
||||
$this->persistentQueryLoader = $persistentQueryLoader;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set response debug flags. See GraphQL\Error\Debug class for a list of all available flags
|
||||
*
|
||||
* @api
|
||||
* @param bool|int $set
|
||||
* @return $this
|
||||
*/
|
||||
public function setDebug($set = true)
|
||||
{
|
||||
$this->debug = $set;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow batching queries (disabled by default)
|
||||
*
|
||||
* @api
|
||||
* @param bool $enableBatching
|
||||
* @return $this
|
||||
*/
|
||||
public function setQueryBatching($enableBatching)
|
||||
{
|
||||
$this->queryBatching = (bool) $enableBatching;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api
|
||||
* @param PromiseAdapter $promiseAdapter
|
||||
* @return $this
|
||||
*/
|
||||
public function setPromiseAdapter(PromiseAdapter $promiseAdapter)
|
||||
{
|
||||
$this->promiseAdapter = $promiseAdapter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed|callable
|
||||
*/
|
||||
public function getContext()
|
||||
{
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed|callable
|
||||
*/
|
||||
public function getRootValue()
|
||||
{
|
||||
return $this->rootValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Schema
|
||||
*/
|
||||
public function getSchema()
|
||||
{
|
||||
return $this->schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return callable|null
|
||||
*/
|
||||
public function getErrorFormatter()
|
||||
{
|
||||
return $this->errorFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return callable|null
|
||||
*/
|
||||
public function getErrorsHandler()
|
||||
{
|
||||
return $this->errorsHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PromiseAdapter
|
||||
*/
|
||||
public function getPromiseAdapter()
|
||||
{
|
||||
return $this->promiseAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|callable
|
||||
*/
|
||||
public function getValidationRules()
|
||||
{
|
||||
return $this->validationRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return callable
|
||||
*/
|
||||
public function getFieldResolver()
|
||||
{
|
||||
return $this->fieldResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return callable
|
||||
*/
|
||||
public function getPersistentQueryLoader()
|
||||
{
|
||||
return $this->persistentQueryLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getDebug()
|
||||
{
|
||||
return $this->debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getQueryBatching()
|
||||
{
|
||||
return $this->queryBatching;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
namespace GraphQL\Server;
|
||||
|
||||
use GraphQL\Error\FormattedError;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Executor\ExecutionResult;
|
||||
use GraphQL\Executor\Promise\Promise;
|
||||
use GraphQL\Utils;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/**
|
||||
* GraphQL server compatible with both: [express-graphql](https://github.com/graphql/express-graphql)
|
||||
* and [Apollo Server](https://github.com/apollographql/graphql-server).
|
||||
* Usage Example:
|
||||
*
|
||||
* $server = new StandardServer([
|
||||
* 'schema' => $mySchema
|
||||
* ]);
|
||||
* $server->handleRequest();
|
||||
*
|
||||
* Or using [ServerConfig](reference.md#graphqlserverserverconfig) instance:
|
||||
*
|
||||
* $config = GraphQL\Server\ServerConfig::create()
|
||||
* ->setSchema($mySchema)
|
||||
* ->setContext($myContext);
|
||||
*
|
||||
* $server = new GraphQL\Server\StandardServer($config);
|
||||
* $server->handleRequest();
|
||||
*
|
||||
* See [dedicated section in docs](executing-queries.md#using-server) for details.
|
||||
*
|
||||
*/
|
||||
class StandardServer
|
||||
{
|
||||
/**
|
||||
* @var ServerConfig
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* @var Helper
|
||||
*/
|
||||
private $helper;
|
||||
|
||||
/**
|
||||
* Converts and exception to error and sends spec-compliant HTTP 500 error.
|
||||
* Useful when an exception is thrown somewhere outside of server execution context
|
||||
* (e.g. during schema instantiation).
|
||||
*
|
||||
* @api
|
||||
* @param \Throwable $error
|
||||
* @param bool $debug
|
||||
* @param bool $exitWhenDone
|
||||
*/
|
||||
public static function send500Error($error, $debug = false, $exitWhenDone = false)
|
||||
{
|
||||
$response = [
|
||||
'errors' => [
|
||||
FormattedError::createFromException($error, $debug)
|
||||
]
|
||||
];
|
||||
$helper = new Helper();
|
||||
$helper->emitResponse($response, 500, $exitWhenDone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new instance of a standard GraphQL HTTP server
|
||||
*
|
||||
* @api
|
||||
* @param ServerConfig|array $config
|
||||
*/
|
||||
public function __construct($config)
|
||||
{
|
||||
if (is_array($config)) {
|
||||
$config = ServerConfig::create($config);
|
||||
}
|
||||
if (!$config instanceof ServerConfig) {
|
||||
throw new InvariantViolation("Expecting valid server config, but got " . Utils::printSafe($config));
|
||||
}
|
||||
$this->config = $config;
|
||||
$this->helper = new Helper();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses HTTP request, executes and emits response (using standard PHP `header` function and `echo`)
|
||||
*
|
||||
* By default (when $parsedBody is not set) it uses PHP globals to parse a request.
|
||||
* It is possible to implement request parsing elsewhere (e.g. using framework Request instance)
|
||||
* and then pass it to the server.
|
||||
*
|
||||
* See `executeRequest()` if you prefer to emit response yourself
|
||||
* (e.g. using Response object of some framework)
|
||||
*
|
||||
* @api
|
||||
* @param OperationParams|OperationParams[] $parsedBody
|
||||
* @param bool $exitWhenDone
|
||||
*/
|
||||
public function handleRequest($parsedBody = null, $exitWhenDone = false)
|
||||
{
|
||||
$result = $this->executeRequest($parsedBody);
|
||||
$this->helper->sendResponse($result, $exitWhenDone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes GraphQL operation and returns execution result
|
||||
* (or promise when promise adapter is different from SyncPromiseAdapter).
|
||||
*
|
||||
* By default (when $parsedBody is not set) it uses PHP globals to parse a request.
|
||||
* It is possible to implement request parsing elsewhere (e.g. using framework Request instance)
|
||||
* and then pass it to the server.
|
||||
*
|
||||
* PSR-7 compatible method executePsrRequest() does exactly this.
|
||||
*
|
||||
* @api
|
||||
* @param OperationParams|OperationParams[] $parsedBody
|
||||
* @return ExecutionResult|ExecutionResult[]|Promise
|
||||
* @throws InvariantViolation
|
||||
*/
|
||||
public function executeRequest($parsedBody = null)
|
||||
{
|
||||
if (null === $parsedBody) {
|
||||
$parsedBody = $this->helper->parseHttpRequest();
|
||||
}
|
||||
|
||||
if (is_array($parsedBody)) {
|
||||
return $this->helper->executeBatch($this->config, $parsedBody);
|
||||
} else {
|
||||
return $this->helper->executeOperation($this->config, $parsedBody);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes PSR-7 request and fulfills PSR-7 response.
|
||||
*
|
||||
* See `executePsrRequest()` if you prefer to create response yourself
|
||||
* (e.g. using specific JsonResponse instance of some framework).
|
||||
*
|
||||
* @api
|
||||
* @param ServerRequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param StreamInterface $writableBodyStream
|
||||
* @return ResponseInterface|Promise
|
||||
*/
|
||||
public function processPsrRequest(
|
||||
ServerRequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
StreamInterface $writableBodyStream
|
||||
)
|
||||
{
|
||||
$result = $this->executePsrRequest($request);
|
||||
return $this->helper->toPsrResponse($result, $response, $writableBodyStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes GraphQL operation and returns execution result
|
||||
* (or promise when promise adapter is different from SyncPromiseAdapter)
|
||||
*
|
||||
* @api
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ExecutionResult|ExecutionResult[]|Promise
|
||||
*/
|
||||
public function executePsrRequest(ServerRequestInterface $request)
|
||||
{
|
||||
$parsedBody = $this->helper->parsePsrRequest($request);
|
||||
return $this->executeRequest($parsedBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of Server helper, which contains most of the actual logic for
|
||||
* parsing / validating / executing request (which could be re-used by other server implementations)
|
||||
*
|
||||
* @api
|
||||
* @return Helper
|
||||
*/
|
||||
public function getHelper()
|
||||
{
|
||||
return $this->helper;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
/*
|
||||
export type GraphQLAbstractType =
|
||||
GraphQLInterfaceType |
|
||||
GraphQLUnionType;
|
||||
*/
|
||||
interface AbstractType
|
||||
{
|
||||
/**
|
||||
* Resolves concrete ObjectType for given object value
|
||||
*
|
||||
* @param $objectValue
|
||||
* @param $context
|
||||
* @param ResolveInfo $info
|
||||
* @return mixed
|
||||
*/
|
||||
public function resolveType($objectValue, $context, ResolveInfo $info);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Language\AST\BooleanValueNode;
|
||||
|
||||
/**
|
||||
* Class BooleanType
|
||||
* @package GraphQL\Type\Definition
|
||||
*/
|
||||
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 !!$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function parseValue($value)
|
||||
{
|
||||
return is_bool($value) ? $value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $ast
|
||||
* @return bool|null
|
||||
*/
|
||||
public function parseLiteral($ast)
|
||||
{
|
||||
if ($ast instanceof BooleanValueNode) {
|
||||
return (bool) $ast->value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
/*
|
||||
export type GraphQLCompositeType =
|
||||
GraphQLObjectType |
|
||||
GraphQLInterfaceType |
|
||||
GraphQLUnionType;
|
||||
*/
|
||||
interface CompositeType
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Error\Warning;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Class Config
|
||||
* @package GraphQL\Type\Definition
|
||||
* @deprecated See https://github.com/webonyx/graphql-php/issues/148 for alternatives
|
||||
*/
|
||||
class Config
|
||||
{
|
||||
const BOOLEAN = 1;
|
||||
const STRING = 2;
|
||||
const INT = 4;
|
||||
const FLOAT = 8;
|
||||
const NUMERIC = 16;
|
||||
const SCALAR = 32;
|
||||
const CALLBACK = 64;
|
||||
const ANY = 128;
|
||||
const NAME = 256;
|
||||
|
||||
const OUTPUT_TYPE = 2048;
|
||||
const INPUT_TYPE = 4096;
|
||||
const INTERFACE_TYPE = 8192;
|
||||
const OBJECT_TYPE = 16384;
|
||||
|
||||
const REQUIRED = 65536;
|
||||
const KEY_AS_NAME = 131072;
|
||||
const MAYBE_THUNK = 262144;
|
||||
const MAYBE_TYPE = 524288;
|
||||
const MAYBE_NAME = 1048576;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private static $enableValidation = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private static $allowCustomOptions = true;
|
||||
|
||||
/**
|
||||
*
|
||||
* Disables config validation
|
||||
*
|
||||
* @deprecated See https://github.com/webonyx/graphql-php/issues/148 for alternatives
|
||||
*/
|
||||
public static function disableValidation()
|
||||
{
|
||||
self::$enableValidation = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable deep config validation (disabled by default because it creates significant performance overhead).
|
||||
* Useful only at development to catch type definition errors quickly.
|
||||
*
|
||||
* @deprecated See https://github.com/webonyx/graphql-php/issues/148 for alternatives
|
||||
*/
|
||||
public static function enableValidation($allowCustomOptions = true)
|
||||
{
|
||||
Warning::warnOnce(
|
||||
'GraphQL\Type\Defintion\Config is deprecated and will be removed in the next version. ' .
|
||||
'See https://github.com/webonyx/graphql-php/issues/148 for alternatives',
|
||||
Warning::WARNING_CONFIG_DEPRECATION,
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
|
||||
self::$enableValidation = true;
|
||||
self::$allowCustomOptions = $allowCustomOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public static function isValidationEnabled()
|
||||
{
|
||||
return self::$enableValidation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $config
|
||||
* @param array $definition
|
||||
*/
|
||||
public static function validate(array $config, array $definition)
|
||||
{
|
||||
if (self::$enableValidation) {
|
||||
$name = isset($config['name']) ? $config['name'] : '(Unnamed Type)';
|
||||
self::validateMap($name, $config, $definition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $typeName
|
||||
* @param array $config
|
||||
* @param array $definition
|
||||
*/
|
||||
public static function validateField($typeName, array $config, array $definition)
|
||||
{
|
||||
if (self::$enableValidation) {
|
||||
if (!isset($config['name'])) {
|
||||
$pathStr = isset($config['type'])
|
||||
? '(Unknown Field of type: ' . Utils::printSafe($config['type']) . ')'
|
||||
: '(Unknown Field)';
|
||||
} else {
|
||||
$pathStr = '';
|
||||
}
|
||||
self::validateMap($typeName ?: '(Unnamed Type)', $config, $definition, $pathStr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|int $definition
|
||||
* @param int $flags
|
||||
* @return \stdClass
|
||||
*/
|
||||
public static function arrayOf($definition, $flags = 0)
|
||||
{
|
||||
$tmp = new \stdClass();
|
||||
$tmp->isArray = true;
|
||||
$tmp->definition = $definition;
|
||||
$tmp->flags = (int) $flags;
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $typeName
|
||||
* @param array $map
|
||||
* @param array $definitions
|
||||
* @param null $pathStr
|
||||
*/
|
||||
private static function validateMap($typeName, array $map, array $definitions, $pathStr = null)
|
||||
{
|
||||
$suffix = $pathStr ? " at $pathStr" : '';
|
||||
|
||||
// Make sure there are no unexpected keys in map
|
||||
$unexpectedKeys = array_keys(array_diff_key($map, $definitions));
|
||||
|
||||
if (!empty($unexpectedKeys)) {
|
||||
if (!self::$allowCustomOptions) {
|
||||
Warning::warnOnce(
|
||||
sprintf('Error in "%s" type definition: Non-standard keys "%s" ' . $suffix, $typeName, implode(', ', $unexpectedKeys)),
|
||||
Warning::WARNING_CONFIG
|
||||
);
|
||||
}
|
||||
$map = array_intersect_key($map, $definitions);
|
||||
}
|
||||
|
||||
// Make sure that all required keys are present in map
|
||||
$requiredKeys = array_filter($definitions, function($def) {return (self::getFlags($def) & self::REQUIRED) > 0;});
|
||||
$missingKeys = array_keys(array_diff_key($requiredKeys, $map));
|
||||
Utils::invariant(
|
||||
empty($missingKeys),
|
||||
'Error in "' . $typeName . '" type definition: Required keys missing: "%s" %s', implode(', ', $missingKeys), $suffix
|
||||
);
|
||||
|
||||
// Make sure that every map value is valid given the definition
|
||||
foreach ($map as $key => $value) {
|
||||
self::validateEntry($typeName, $key, $value, $definitions[$key], $pathStr ? "$pathStr:$key" : $key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $typeName
|
||||
* @param $key
|
||||
* @param $value
|
||||
* @param $def
|
||||
* @param $pathStr
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function validateEntry($typeName, $key, $value, $def, $pathStr)
|
||||
{
|
||||
$type = Utils::getVariableType($value);
|
||||
$err = 'Error in "'.$typeName.'" type definition: expecting "%s" at "' . $pathStr . '", but got "' . $type . '"';
|
||||
|
||||
if ($def instanceof \stdClass) {
|
||||
if (($def->flags & self::REQUIRED) === 0 && $value === null) {
|
||||
return ;
|
||||
}
|
||||
if (($def->flags & self::MAYBE_THUNK) > 0) {
|
||||
// TODO: consider wrapping thunk with other function to force validation of value returned by thunk
|
||||
Utils::invariant(is_array($value) || is_callable($value), $err, 'array or callable');
|
||||
} else {
|
||||
Utils::invariant(is_array($value), $err, 'array');
|
||||
}
|
||||
|
||||
if (!empty($def->isArray)) {
|
||||
|
||||
if ($def->flags & self::REQUIRED) {
|
||||
Utils::invariant(!empty($value), 'Error in "'.$typeName.'" type definition: ' . "Value at '$pathStr' cannot be empty array");
|
||||
}
|
||||
|
||||
$err = 'Error in "'.$typeName.'" type definition: ' . "Each entry at '$pathStr' must be an array, but entry at '%s' is '%s'";
|
||||
|
||||
foreach ($value as $arrKey => $arrValue) {
|
||||
if (is_array($def->definition)) {
|
||||
if ($def->flags & self::MAYBE_TYPE && $arrValue instanceof Type) {
|
||||
$arrValue = ['type' => $arrValue];
|
||||
}
|
||||
if ($def->flags & self::MAYBE_NAME && is_string($arrValue)) {
|
||||
$arrValue = ['name' => $arrValue];
|
||||
}
|
||||
|
||||
if (!$arrValue instanceof FieldDefinition) {
|
||||
Utils::invariant(is_array($arrValue), $err, $arrKey, Utils::getVariableType($arrValue));
|
||||
|
||||
if ($def->flags & self::KEY_AS_NAME && is_string($arrKey)) {
|
||||
$arrValue += ['name' => $arrKey];
|
||||
}
|
||||
self::validateMap($typeName, $arrValue, $def->definition, "$pathStr:$arrKey");
|
||||
}
|
||||
} else {
|
||||
self::validateEntry($typeName, $arrKey, $arrValue, $def->definition, "$pathStr:$arrKey");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new InvariantViolation('Error in "'.$typeName.'" type definition: ' . "unexpected definition: " . print_r($def, true));
|
||||
}
|
||||
} else {
|
||||
Utils::invariant(is_int($def), 'Error in "'.$typeName.'" type definition: ' . "Definition for '$pathStr' is expected to be single integer value");
|
||||
|
||||
if ($def & self::REQUIRED) {
|
||||
Utils::invariant($value !== null, 'Error in "'.$typeName.'" type definition: ' . 'Value at "%s" can not be null', $pathStr);
|
||||
}
|
||||
|
||||
if (null === $value) {
|
||||
return ; // Allow nulls for non-required fields
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case $def & self::ANY:
|
||||
break;
|
||||
case $def & self::BOOLEAN:
|
||||
Utils::invariant(is_bool($value), $err, 'boolean');
|
||||
break;
|
||||
case $def & self::STRING:
|
||||
Utils::invariant(is_string($value), $err, 'string');
|
||||
break;
|
||||
case $def & self::NUMERIC:
|
||||
Utils::invariant(is_numeric($value), $err, 'numeric');
|
||||
break;
|
||||
case $def & self::FLOAT:
|
||||
Utils::invariant(is_float($value) || is_int($value), $err, 'float');
|
||||
break;
|
||||
case $def & self::INT:
|
||||
Utils::invariant(is_int($value), $err, 'int');
|
||||
break;
|
||||
case $def & self::CALLBACK:
|
||||
Utils::invariant(is_callable($value), $err, 'callable');
|
||||
break;
|
||||
case $def & self::SCALAR:
|
||||
Utils::invariant(is_scalar($value), $err, 'scalar');
|
||||
break;
|
||||
case $def & self::NAME:
|
||||
Utils::invariant(is_string($value), $err, 'name');
|
||||
Utils::invariant(
|
||||
preg_match('~^[_a-zA-Z][_a-zA-Z0-9]*$~', $value),
|
||||
'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "%s" does not.',
|
||||
$value
|
||||
);
|
||||
break;
|
||||
case $def & self::INPUT_TYPE:
|
||||
Utils::invariant(
|
||||
is_callable($value) || $value instanceof InputType,
|
||||
$err,
|
||||
'InputType definition'
|
||||
);
|
||||
break;
|
||||
case $def & self::OUTPUT_TYPE:
|
||||
Utils::invariant(
|
||||
is_callable($value) || $value instanceof OutputType,
|
||||
$err,
|
||||
'OutputType definition'
|
||||
);
|
||||
break;
|
||||
case $def & self::INTERFACE_TYPE:
|
||||
Utils::invariant(
|
||||
is_callable($value) || $value instanceof InterfaceType,
|
||||
$err,
|
||||
'InterfaceType definition'
|
||||
);
|
||||
break;
|
||||
case $def & self::OBJECT_TYPE:
|
||||
Utils::invariant(
|
||||
is_callable($value) || $value instanceof ObjectType,
|
||||
$err,
|
||||
'ObjectType definition'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new InvariantViolation("Unexpected validation rule: " . $def);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $def
|
||||
* @return mixed
|
||||
*/
|
||||
private static function getFlags($def)
|
||||
{
|
||||
return is_object($def) ? $def->flags : $def;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Class CustomScalarType
|
||||
* @package GraphQL\Type\Definition
|
||||
*/
|
||||
class CustomScalarType extends ScalarType
|
||||
{
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function serialize($value)
|
||||
{
|
||||
return call_user_func($this->config['serialize'], $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function parseValue($value)
|
||||
{
|
||||
if (isset($this->config['parseValue'])) {
|
||||
return call_user_func($this->config['parseValue'], $value);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $valueNode
|
||||
* @return mixed
|
||||
*/
|
||||
public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode)
|
||||
{
|
||||
if (isset($this->config['parseLiteral'])) {
|
||||
return call_user_func($this->config['parseLiteral'], $valueNode);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function assertValid()
|
||||
{
|
||||
parent::assertValid();
|
||||
|
||||
Utils::invariant(
|
||||
isset($this->config['serialize']) && is_callable($this->config['serialize']),
|
||||
"{$this->name} must provide \"serialize\" function. If this custom Scalar " .
|
||||
'is also used as an input type, ensure "parseValue" and "parseLiteral" ' .
|
||||
'functions are also provided.'
|
||||
);
|
||||
if (isset($this->config['parseValue']) || isset($this->config['parseLiteral'])) {
|
||||
Utils::invariant(
|
||||
isset($this->config['parseValue']) && isset($this->config['parseLiteral']) &&
|
||||
is_callable($this->config['parseValue']) && is_callable($this->config['parseLiteral']),
|
||||
"{$this->name} must provide both \"parseValue\" and \"parseLiteral\" functions."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Language\AST\DirectiveDefinitionNode;
|
||||
|
||||
/**
|
||||
* Class Directive
|
||||
* @package GraphQL\Type\Definition
|
||||
*/
|
||||
class Directive
|
||||
{
|
||||
const DEFAULT_DEPRECATION_REASON = 'No longer supported';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public static $internalDirectives;
|
||||
|
||||
// Schema Definitions
|
||||
|
||||
|
||||
/**
|
||||
* @var array
|
||||
* @deprecated as of 8.0 (use DirectiveLocation constants directly)
|
||||
*/
|
||||
public static $directiveLocations = [
|
||||
// Operations:
|
||||
DirectiveLocation::QUERY => DirectiveLocation::QUERY,
|
||||
DirectiveLocation::MUTATION => DirectiveLocation::MUTATION,
|
||||
DirectiveLocation::SUBSCRIPTION => DirectiveLocation::SUBSCRIPTION,
|
||||
DirectiveLocation::FIELD => DirectiveLocation::FIELD,
|
||||
DirectiveLocation::FRAGMENT_DEFINITION => DirectiveLocation::FRAGMENT_DEFINITION,
|
||||
DirectiveLocation::FRAGMENT_SPREAD => DirectiveLocation::FRAGMENT_SPREAD,
|
||||
DirectiveLocation::INLINE_FRAGMENT => DirectiveLocation::INLINE_FRAGMENT,
|
||||
|
||||
// Schema Definitions
|
||||
DirectiveLocation::SCHEMA => DirectiveLocation::SCHEMA,
|
||||
DirectiveLocation::SCALAR => DirectiveLocation::SCALAR,
|
||||
DirectiveLocation::OBJECT => DirectiveLocation::OBJECT,
|
||||
DirectiveLocation::FIELD_DEFINITION => DirectiveLocation::FIELD_DEFINITION,
|
||||
DirectiveLocation::ARGUMENT_DEFINITION => DirectiveLocation::ARGUMENT_DEFINITION,
|
||||
DirectiveLocation::IFACE => DirectiveLocation::IFACE,
|
||||
DirectiveLocation::UNION => DirectiveLocation::UNION,
|
||||
DirectiveLocation::ENUM => DirectiveLocation::ENUM,
|
||||
DirectiveLocation::ENUM_VALUE => DirectiveLocation::ENUM_VALUE,
|
||||
DirectiveLocation::INPUT_OBJECT => DirectiveLocation::INPUT_OBJECT,
|
||||
DirectiveLocation::INPUT_FIELD_DEFINITION => DirectiveLocation::INPUT_FIELD_DEFINITION
|
||||
];
|
||||
|
||||
/**
|
||||
* @return Directive
|
||||
*/
|
||||
public static function includeDirective()
|
||||
{
|
||||
$internal = self::getInternalDirectives();
|
||||
return $internal['include'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Directive
|
||||
*/
|
||||
public static function skipDirective()
|
||||
{
|
||||
$internal = self::getInternalDirectives();
|
||||
return $internal['skip'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Directive
|
||||
*/
|
||||
public static function deprecatedDirective()
|
||||
{
|
||||
$internal = self::getInternalDirectives();
|
||||
return $internal['deprecated'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getInternalDirectives()
|
||||
{
|
||||
if (!self::$internalDirectives) {
|
||||
self::$internalDirectives = [
|
||||
'include' => new self([
|
||||
'name' => 'include',
|
||||
'description' => 'Directs the executor to include this field or fragment only when the `if` argument is true.',
|
||||
'locations' => [
|
||||
DirectiveLocation::FIELD,
|
||||
DirectiveLocation::FRAGMENT_SPREAD,
|
||||
DirectiveLocation::INLINE_FRAGMENT,
|
||||
],
|
||||
'args' => [
|
||||
new FieldArgument([
|
||||
'name' => 'if',
|
||||
'type' => Type::nonNull(Type::boolean()),
|
||||
'description' => 'Included when true.'
|
||||
])
|
||||
],
|
||||
]),
|
||||
'skip' => new self([
|
||||
'name' => 'skip',
|
||||
'description' => 'Directs the executor to skip this field or fragment when the `if` argument is true.',
|
||||
'locations' => [
|
||||
DirectiveLocation::FIELD,
|
||||
DirectiveLocation::FRAGMENT_SPREAD,
|
||||
DirectiveLocation::INLINE_FRAGMENT
|
||||
],
|
||||
'args' => [
|
||||
new FieldArgument([
|
||||
'name' => 'if',
|
||||
'type' => Type::nonNull(Type::boolean()),
|
||||
'description' => 'Skipped when true.'
|
||||
])
|
||||
]
|
||||
]),
|
||||
'deprecated' => new self([
|
||||
'name' => 'deprecated',
|
||||
'description' => 'Marks an element of a GraphQL schema as no longer supported.',
|
||||
'locations' => [
|
||||
DirectiveLocation::FIELD_DEFINITION,
|
||||
DirectiveLocation::ENUM_VALUE
|
||||
],
|
||||
'args' => [
|
||||
new FieldArgument([
|
||||
'name' => 'reason',
|
||||
'type' => Type::string(),
|
||||
'description' =>
|
||||
'Explains why this element was deprecated, usually also including a ' .
|
||||
'suggestion for how to access supported similar data. Formatted ' .
|
||||
'in [Markdown](https://daringfireball.net/projects/markdown/).',
|
||||
'defaultValue' => self::DEFAULT_DEPRECATION_REASON
|
||||
])
|
||||
]
|
||||
])
|
||||
];
|
||||
}
|
||||
return self::$internalDirectives;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* Values from self::$locationMap
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $locations;
|
||||
|
||||
/**
|
||||
* @var FieldArgument[]
|
||||
*/
|
||||
public $args;
|
||||
|
||||
/**
|
||||
* @var DirectiveDefinitionNode|null
|
||||
*/
|
||||
public $astNode;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $config;
|
||||
|
||||
/**
|
||||
* Directive constructor.
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(array $config)
|
||||
{
|
||||
foreach ($config as $key => $value) {
|
||||
$this->{$key} = $value;
|
||||
}
|
||||
$this->config = $config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
/**
|
||||
* List of available directive locations
|
||||
*/
|
||||
class DirectiveLocation
|
||||
{
|
||||
const IFACE = 'INTERFACE';
|
||||
const SUBSCRIPTION = 'SUBSCRIPTION';
|
||||
const FRAGMENT_SPREAD = 'FRAGMENT_SPREAD';
|
||||
const QUERY = 'QUERY';
|
||||
const MUTATION = 'MUTATION';
|
||||
const FRAGMENT_DEFINITION = 'FRAGMENT_DEFINITION';
|
||||
const INPUT_OBJECT = 'INPUT_OBJECT';
|
||||
const INLINE_FRAGMENT = 'INLINE_FRAGMENT';
|
||||
const UNION = 'UNION';
|
||||
const SCALAR = 'SCALAR';
|
||||
const FIELD_DEFINITION = 'FIELD_DEFINITION';
|
||||
const ARGUMENT_DEFINITION = 'ARGUMENT_DEFINITION';
|
||||
const ENUM = 'ENUM';
|
||||
const OBJECT = 'OBJECT';
|
||||
const ENUM_VALUE = 'ENUM_VALUE';
|
||||
const FIELD = 'FIELD';
|
||||
const SCHEMA = 'SCHEMA';
|
||||
const INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION';
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Language\AST\EnumTypeDefinitionNode;
|
||||
use GraphQL\Language\AST\EnumValueNode;
|
||||
use GraphQL\Utils\MixedStore;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Class EnumType
|
||||
* @package GraphQL\Type\Definition
|
||||
*/
|
||||
class EnumType extends Type implements InputType, OutputType, LeafType
|
||||
{
|
||||
/**
|
||||
* @var EnumTypeDefinitionNode|null
|
||||
*/
|
||||
public $astNode;
|
||||
|
||||
/**
|
||||
* @var EnumValueDefinition[]
|
||||
*/
|
||||
private $values;
|
||||
|
||||
/**
|
||||
* @var MixedStore<mixed, EnumValueDefinition>
|
||||
*/
|
||||
private $valueLookup;
|
||||
|
||||
/**
|
||||
* @var \ArrayObject<string, EnumValueDefinition>
|
||||
*/
|
||||
private $nameLookup;
|
||||
|
||||
public function __construct($config)
|
||||
{
|
||||
if (!isset($config['name'])) {
|
||||
$config['name'] = $this->tryInferName();
|
||||
}
|
||||
|
||||
Utils::assertValidName($config['name'], !empty($config['isIntrospection']));
|
||||
|
||||
Config::validate($config, [
|
||||
'name' => Config::NAME | Config::REQUIRED,
|
||||
'values' => Config::arrayOf([
|
||||
'name' => Config::NAME | Config::REQUIRED,
|
||||
'value' => Config::ANY,
|
||||
'deprecationReason' => Config::STRING,
|
||||
'description' => Config::STRING
|
||||
], Config::KEY_AS_NAME | Config::MAYBE_NAME),
|
||||
'description' => Config::STRING
|
||||
]);
|
||||
|
||||
$this->name = $config['name'];
|
||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||
$this->astNode = isset($config['astNode']) ? $config['astNode'] : null;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EnumValueDefinition[]
|
||||
*/
|
||||
public function getValues()
|
||||
{
|
||||
if ($this->values === null) {
|
||||
$this->values = [];
|
||||
$config = $this->config;
|
||||
|
||||
if (isset($config['values'])) {
|
||||
if (!is_array($config['values'])) {
|
||||
throw new InvariantViolation("{$this->name} values must be an array");
|
||||
}
|
||||
foreach ($config['values'] as $name => $value) {
|
||||
if (is_string($name)) {
|
||||
if (!is_array($value)) {
|
||||
$value = ['name' => $name, 'value' => $value];
|
||||
} else {
|
||||
$value += ['name' => $name, 'value' => $name];
|
||||
}
|
||||
} else if (is_int($name) && is_string($value)) {
|
||||
$value = ['name' => $value, 'value' => $value];
|
||||
} else {
|
||||
throw new InvariantViolation("{$this->name} values must be an array with value names as keys.");
|
||||
}
|
||||
$this->values[] = new EnumValueDefinition($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* @return EnumValueDefinition|null
|
||||
*/
|
||||
public function getValue($name)
|
||||
{
|
||||
$lookup = $this->getNameLookup();
|
||||
return is_scalar($name) && isset($lookup[$name]) ? $lookup[$name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @return null
|
||||
*/
|
||||
public function serialize($value)
|
||||
{
|
||||
$lookup = $this->getValueLookup();
|
||||
return isset($lookup[$value]) ? $lookup[$value]->name : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @return bool
|
||||
*/
|
||||
public function isValidValue($value)
|
||||
{
|
||||
return is_string($value) && $this->getNameLookup()->offsetExists($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $valueNode
|
||||
* @return bool
|
||||
*/
|
||||
public function isValidLiteral($valueNode)
|
||||
{
|
||||
return $valueNode instanceof EnumValueNode && $this->getNameLookup()->offsetExists($valueNode->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @return null
|
||||
*/
|
||||
public function parseValue($value)
|
||||
{
|
||||
$lookup = $this->getNameLookup();
|
||||
return isset($lookup[$value]) ? $lookup[$value]->value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @return null
|
||||
*/
|
||||
public function parseLiteral($value)
|
||||
{
|
||||
if ($value instanceof EnumValueNode) {
|
||||
$lookup = $this->getNameLookup();
|
||||
if (isset($lookup[$value->value])) {
|
||||
$enumValue = $lookup[$value->value];
|
||||
if ($enumValue) {
|
||||
return $enumValue->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MixedStore<mixed, EnumValueDefinition>
|
||||
*/
|
||||
private function getValueLookup()
|
||||
{
|
||||
if (null === $this->valueLookup) {
|
||||
$this->valueLookup = new MixedStore();
|
||||
|
||||
foreach ($this->getValues() as $valueName => $value) {
|
||||
$this->valueLookup->offsetSet($value->value, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->valueLookup;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \ArrayObject<string, EnumValueDefinition>
|
||||
*/
|
||||
private function getNameLookup()
|
||||
{
|
||||
if (!$this->nameLookup) {
|
||||
$lookup = new \ArrayObject();
|
||||
foreach ($this->getValues() as $value) {
|
||||
$lookup[$value->name] = $value;
|
||||
}
|
||||
$this->nameLookup = $lookup;
|
||||
}
|
||||
return $this->nameLookup;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvariantViolation
|
||||
*/
|
||||
public function assertValid()
|
||||
{
|
||||
parent::assertValid();
|
||||
|
||||
Utils::invariant(
|
||||
isset($this->config['values']),
|
||||
"{$this->name} values must be an array."
|
||||
);
|
||||
|
||||
$values = $this->getValues();
|
||||
|
||||
Utils::invariant(
|
||||
!empty($values),
|
||||
"{$this->name} values must be not empty."
|
||||
);
|
||||
foreach ($values as $value) {
|
||||
try {
|
||||
Utils::assertValidName($value->name);
|
||||
} catch (InvariantViolation $e) {
|
||||
throw new InvariantViolation(
|
||||
"{$this->name} has value with invalid name: " .
|
||||
Utils::printSafe($value->name) . " ({$e->getMessage()})"
|
||||
);
|
||||
}
|
||||
Utils::invariant(
|
||||
!in_array($value->name, ['true', 'false', 'null']),
|
||||
"{$this->name}: \"{$value->name}\" can not be used as an Enum value."
|
||||
);
|
||||
Utils::invariant(
|
||||
!isset($value->config['isDeprecated']),
|
||||
"{$this->name}.{$value->name} should provide \"deprecationReason\" instead of \"isDeprecated\"."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
use GraphQL\Language\AST\EnumValueDefinitionNode;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Class EnumValueDefinition
|
||||
* @package GraphQL\Type\Definition
|
||||
*/
|
||||
class EnumValueDefinition
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $deprecationReason;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* @var EnumValueDefinitionNode|null
|
||||
*/
|
||||
public $astNode;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $config;
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
$this->name = isset($config['name']) ? $config['name'] : null;
|
||||
$this->value = isset($config['value']) ? $config['value'] : null;
|
||||
$this->deprecationReason = isset($config['deprecationReason']) ? $config['deprecationReason'] : null;
|
||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||
$this->astNode = isset($config['astNode']) ? $config['astNode'] : null;
|
||||
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isDeprecated()
|
||||
{
|
||||
return !!$this->deprecationReason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Language\AST\ArgumentNode;
|
||||
use GraphQL\Language\AST\InputValueDefinitionNode;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
|
||||
/**
|
||||
* Class FieldArgument
|
||||
*
|
||||
* @package GraphQL\Type\Definition
|
||||
*/
|
||||
class FieldArgument
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
public $defaultValue;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* @var InputValueDefinitionNode|null
|
||||
*/
|
||||
public $astNode;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $config;
|
||||
|
||||
/**
|
||||
* @var InputType
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $defaultValueExists = false;
|
||||
|
||||
/**
|
||||
* @param array $config
|
||||
* @return array
|
||||
*/
|
||||
public static function createMap(array $config)
|
||||
{
|
||||
$map = [];
|
||||
foreach ($config as $name => $argConfig) {
|
||||
if (!is_array($argConfig)) {
|
||||
$argConfig = ['type' => $argConfig];
|
||||
}
|
||||
$map[] = new self($argConfig + ['name' => $name]);
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* FieldArgument constructor.
|
||||
* @param array $def
|
||||
*/
|
||||
public function __construct(array $def)
|
||||
{
|
||||
foreach ($def as $key => $value) {
|
||||
switch ($key) {
|
||||
case 'type':
|
||||
$this->type = $value;
|
||||
break;
|
||||
case 'name':
|
||||
$this->name = $value;
|
||||
break;
|
||||
case 'defaultValue':
|
||||
$this->defaultValue = $value;
|
||||
$this->defaultValueExists = true;
|
||||
break;
|
||||
case 'description':
|
||||
$this->description = $value;
|
||||
break;
|
||||
case 'astNode':
|
||||
$this->astNode = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->config = $def;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InputType
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function defaultValueExists()
|
||||
{
|
||||
return $this->defaultValueExists;
|
||||
}
|
||||
|
||||
public function assertValid(FieldDefinition $parentField, Type $parentType)
|
||||
{
|
||||
try {
|
||||
Utils::assertValidName($this->name);
|
||||
} catch (InvariantViolation $e) {
|
||||
throw new InvariantViolation(
|
||||
"{$parentType->name}.{$parentField->name}({$this->name}:) {$e->getMessage()}")
|
||||
;
|
||||
}
|
||||
$type = $this->type;
|
||||
if ($type instanceof WrappingType) {
|
||||
$type = $type->getWrappedType(true);
|
||||
}
|
||||
Utils::invariant(
|
||||
$type instanceof InputType,
|
||||
"{$parentType->name}.{$parentField->name}({$this->name}): argument type must be " .
|
||||
"Input Type but got: " . Utils::printSafe($this->type)
|
||||
);
|
||||
Utils::invariant(
|
||||
$this->description === null || is_string($this->description),
|
||||
"{$parentType->name}.{$parentField->name}({$this->name}): argument description type must be " .
|
||||
"string but got: " . Utils::printSafe($this->description)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||
use GraphQL\Language\AST\TypeDefinitionNode;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Class FieldDefinition
|
||||
* @package GraphQL\Type\Definition
|
||||
* @todo Move complexity-related code to it's own place
|
||||
*/
|
||||
class FieldDefinition
|
||||
{
|
||||
const DEFAULT_COMPLEXITY_FN = 'GraphQL\Type\Definition\FieldDefinition::defaultComplexity';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var FieldArgument[]
|
||||
*/
|
||||
public $args;
|
||||
|
||||
/**
|
||||
* Callback for resolving field value given parent value.
|
||||
* Mutually exclusive with `map`
|
||||
*
|
||||
* @var callable
|
||||
*/
|
||||
public $resolveFn;
|
||||
|
||||
/**
|
||||
* Callback for mapping list of parent values to list of field values.
|
||||
* Mutually exclusive with `resolve`
|
||||
*
|
||||
* @var callable
|
||||
*/
|
||||
public $mapFn;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $deprecationReason;
|
||||
|
||||
/**
|
||||
* @var FieldDefinitionNode|null
|
||||
*/
|
||||
public $astNode;
|
||||
|
||||
/**
|
||||
* Original field definition config
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $config;
|
||||
|
||||
/**
|
||||
* @var OutputType
|
||||
*/
|
||||
private $type;
|
||||
|
||||
private static $def;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getDefinition()
|
||||
{
|
||||
return self::$def ?: (self::$def = [
|
||||
'name' => Config::NAME | Config::REQUIRED,
|
||||
'type' => Config::OUTPUT_TYPE | Config::REQUIRED,
|
||||
'args' => Config::arrayOf([
|
||||
'name' => Config::NAME | Config::REQUIRED,
|
||||
'type' => Config::INPUT_TYPE | Config::REQUIRED,
|
||||
'description' => Config::STRING,
|
||||
'defaultValue' => Config::ANY
|
||||
], Config::KEY_AS_NAME | Config::MAYBE_TYPE),
|
||||
'resolve' => Config::CALLBACK,
|
||||
'map' => Config::CALLBACK,
|
||||
'description' => Config::STRING,
|
||||
'deprecationReason' => Config::STRING,
|
||||
'complexity' => Config::CALLBACK,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function defineFieldMap(Type $type, $fields)
|
||||
{
|
||||
if (is_callable($fields)) {
|
||||
$fields = $fields();
|
||||
}
|
||||
if (!is_array($fields)) {
|
||||
throw new InvariantViolation(
|
||||
"{$type->name} fields must be an array or a callable which returns such an array."
|
||||
);
|
||||
}
|
||||
$map = [];
|
||||
foreach ($fields as $name => $field) {
|
||||
if (is_array($field)) {
|
||||
if (!isset($field['name']) && is_string($name)) {
|
||||
$field['name'] = $name;
|
||||
}
|
||||
if (isset($field['args']) && !is_array($field['args'])) {
|
||||
throw new InvariantViolation(
|
||||
"{$type->name}.{$name} args must be an array."
|
||||
);
|
||||
}
|
||||
$fieldDef = self::create($field);
|
||||
} else if ($field instanceof FieldDefinition) {
|
||||
$fieldDef = $field;
|
||||
} else {
|
||||
if (is_string($name) && $field) {
|
||||
$fieldDef = self::create(['name' => $name, 'type' => $field]);
|
||||
} else {
|
||||
throw new InvariantViolation(
|
||||
"{$type->name}.$name field config must be an array, but got: " . Utils::printSafe($field)
|
||||
);
|
||||
}
|
||||
}
|
||||
$map[$fieldDef->name] = $fieldDef;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $fields
|
||||
* @param string $parentTypeName
|
||||
* @deprecated use defineFieldMap instead
|
||||
* @return array
|
||||
*/
|
||||
public static function createMap(array $fields, $parentTypeName = null)
|
||||
{
|
||||
trigger_error(
|
||||
__METHOD__ . ' is deprecated, use ' . __CLASS__ . '::defineFieldMap() instead',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
|
||||
$map = [];
|
||||
foreach ($fields as $name => $field) {
|
||||
if (is_array($field)) {
|
||||
if (!isset($field['name']) && is_string($name)) {
|
||||
$field['name'] = $name;
|
||||
}
|
||||
$fieldDef = self::create($field);
|
||||
} else if ($field instanceof FieldDefinition) {
|
||||
$fieldDef = $field;
|
||||
} else {
|
||||
if (is_string($name)) {
|
||||
$fieldDef = self::create(['name' => $name, 'type' => $field]);
|
||||
} else {
|
||||
throw new InvariantViolation(
|
||||
"Unexpected field definition for type $parentTypeName at field $name: " . Utils::printSafe($field)
|
||||
);
|
||||
}
|
||||
}
|
||||
$map[$fieldDef->name] = $fieldDef;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|Config $field
|
||||
* @param string $typeName
|
||||
* @return FieldDefinition
|
||||
*/
|
||||
public static function create($field, $typeName = null)
|
||||
{
|
||||
if ($typeName) {
|
||||
Config::validateField($typeName, $field, self::getDefinition());
|
||||
}
|
||||
return new self($field);
|
||||
}
|
||||
|
||||
/**
|
||||
* FieldDefinition constructor.
|
||||
* @param array $config
|
||||
*/
|
||||
protected function __construct(array $config)
|
||||
{
|
||||
$this->name = $config['name'];
|
||||
$this->type = $config['type'];
|
||||
$this->resolveFn = isset($config['resolve']) ? $config['resolve'] : null;
|
||||
$this->mapFn = isset($config['map']) ? $config['map'] : null;
|
||||
$this->args = isset($config['args']) ? FieldArgument::createMap($config['args']) : [];
|
||||
|
||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||
$this->deprecationReason = isset($config['deprecationReason']) ? $config['deprecationReason'] : null;
|
||||
$this->astNode = isset($config['astNode']) ? $config['astNode'] : null;
|
||||
|
||||
$this->config = $config;
|
||||
|
||||
$this->complexityFn = isset($config['complexity']) ? $config['complexity'] : static::DEFAULT_COMPLEXITY_FN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* @return FieldArgument|null
|
||||
*/
|
||||
public function getArg($name)
|
||||
{
|
||||
foreach ($this->args ?: [] as $arg) {
|
||||
/** @var FieldArgument $arg */
|
||||
if ($arg->name === $name) {
|
||||
return $arg;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Type
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isDeprecated()
|
||||
{
|
||||
return !!$this->deprecationReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return callable|\Closure
|
||||
*/
|
||||
public function getComplexityFn()
|
||||
{
|
||||
return $this->complexityFn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Type $parentType
|
||||
* @throws InvariantViolation
|
||||
*/
|
||||
public function assertValid(Type $parentType)
|
||||
{
|
||||
try {
|
||||
Utils::assertValidName($this->name);
|
||||
} catch (InvariantViolation $e) {
|
||||
throw new InvariantViolation("{$parentType->name}.{$this->name}: {$e->getMessage()}");
|
||||
}
|
||||
Utils::invariant(
|
||||
!isset($this->config['isDeprecated']),
|
||||
"{$parentType->name}.{$this->name} should provide \"deprecationReason\" instead of \"isDeprecated\"."
|
||||
);
|
||||
|
||||
$type = $this->type;
|
||||
if ($type instanceof WrappingType) {
|
||||
$type = $type->getWrappedType(true);
|
||||
}
|
||||
Utils::invariant(
|
||||
$type instanceof OutputType,
|
||||
"{$parentType->name}.{$this->name} field type must be Output Type but got: " . Utils::printSafe($this->type)
|
||||
);
|
||||
Utils::invariant(
|
||||
$this->resolveFn === null || is_callable($this->resolveFn),
|
||||
"{$parentType->name}.{$this->name} field resolver must be a function if provided, but got: %s",
|
||||
Utils::printSafe($this->resolveFn)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $childrenComplexity
|
||||
* @return mixed
|
||||
*/
|
||||
public static function defaultComplexity($childrenComplexity)
|
||||
{
|
||||
return $childrenComplexity + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Language\AST\FloatValueNode;
|
||||
use GraphQL\Language\AST\IntValueNode;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Class FloatType
|
||||
* @package GraphQL\Type\Definition
|
||||
*/
|
||||
class FloatType extends ScalarType
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name = Type::FLOAT;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $description =
|
||||
'The `Float` scalar type represents signed double-precision fractional
|
||||
values as specified by
|
||||
[IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). ';
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return float|null
|
||||
*/
|
||||
public function serialize($value)
|
||||
{
|
||||
if (is_numeric($value) || $value === true || $value === false) {
|
||||
return (float) $value;
|
||||
}
|
||||
|
||||
if ($value === '') {
|
||||
$err = 'Float cannot represent non numeric value: (empty string)';
|
||||
} else {
|
||||
$err = sprintf('Float cannot represent non numeric value: %s', Utils::printSafe($value));
|
||||
}
|
||||
throw new InvariantViolation($err);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return float|null
|
||||
*/
|
||||
public function parseValue($value)
|
||||
{
|
||||
return (is_numeric($value) && !is_string($value)) ? (float) $value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $ast
|
||||
* @return float|null
|
||||
*/
|
||||
public function parseLiteral($ast)
|
||||
{
|
||||
if ($ast instanceof FloatValueNode || $ast instanceof IntValueNode) {
|
||||
return (float) $ast->value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Language\AST\IntValueNode;
|
||||
use GraphQL\Language\AST\StringValueNode;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Class IDType
|
||||
* @package GraphQL\Type\Definition
|
||||
*/
|
||||
class IDType extends ScalarType
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name = 'ID';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $description =
|
||||
'The `ID` scalar type represents a unique identifier, often used to
|
||||
refetch an object or as key for a cache. The ID type appears in a JSON
|
||||
response as a String; however, it is not intended to be human-readable.
|
||||
When expected as an input type, any string (such as `"4"`) or integer
|
||||
(such as `4`) input value will be accepted as an ID.';
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return string
|
||||
*/
|
||||
public function serialize($value)
|
||||
{
|
||||
if ($value === true) {
|
||||
return 'true';
|
||||
}
|
||||
if ($value === false) {
|
||||
return 'false';
|
||||
}
|
||||
if ($value === null) {
|
||||
return 'null';
|
||||
}
|
||||
if (!is_scalar($value) && (!is_object($value) || !method_exists($value, '__toString'))) {
|
||||
throw new InvariantViolation("ID type cannot represent non scalar value: " . Utils::printSafe($value));
|
||||
}
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return string
|
||||
*/
|
||||
public function parseValue($value)
|
||||
{
|
||||
return (is_string($value) || is_int($value)) ? (string) $value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $ast
|
||||
* @return null|string
|
||||
*/
|
||||
public function parseLiteral($ast)
|
||||
{
|
||||
if ($ast instanceof StringValueNode || $ast instanceof IntValueNode) {
|
||||
return $ast->value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
use GraphQL\Language\AST\InputValueDefinitionNode;
|
||||
|
||||
/**
|
||||
* Class InputObjectField
|
||||
* @package GraphQL\Type\Definition
|
||||
*/
|
||||
class InputObjectField
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var mixed|null
|
||||
*/
|
||||
public $defaultValue;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* @var callback|InputType
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* @var InputValueDefinitionNode|null
|
||||
*/
|
||||
public $astNode;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $config;
|
||||
|
||||
/**
|
||||
* Helps to differentiate when `defaultValue` is `null` and when it was not even set initially
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $defaultValueExists = false;
|
||||
|
||||
/**
|
||||
* InputObjectField constructor.
|
||||
* @param array $opts
|
||||
*/
|
||||
public function __construct(array $opts)
|
||||
{
|
||||
foreach ($opts as $k => $v) {
|
||||
switch ($k) {
|
||||
case 'defaultValue':
|
||||
$this->defaultValue = $v;
|
||||
$this->defaultValueExists = true;
|
||||
break;
|
||||
case 'defaultValueExists':
|
||||
break;
|
||||
default:
|
||||
$this->{$k} = $v;
|
||||
}
|
||||
}
|
||||
$this->config = $opts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function defaultValueExists()
|
||||
{
|
||||
return $this->defaultValueExists;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Class InputObjectType
|
||||
* @package GraphQL\Type\Definition
|
||||
*/
|
||||
class InputObjectType extends Type implements InputType
|
||||
{
|
||||
/**
|
||||
* @var InputObjectField[]
|
||||
*/
|
||||
private $fields;
|
||||
|
||||
/**
|
||||
* @var InputObjectTypeDefinitionNode|null
|
||||
*/
|
||||
public $astNode;
|
||||
|
||||
/**
|
||||
* InputObjectType constructor.
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(array $config)
|
||||
{
|
||||
if (!isset($config['name'])) {
|
||||
$config['name'] = $this->tryInferName();
|
||||
}
|
||||
|
||||
Utils::assertValidName($config['name']);
|
||||
|
||||
Config::validate($config, [
|
||||
'name' => Config::NAME | Config::REQUIRED,
|
||||
'fields' => Config::arrayOf([
|
||||
'name' => Config::NAME | Config::REQUIRED,
|
||||
'type' => Config::INPUT_TYPE | Config::REQUIRED,
|
||||
'defaultValue' => Config::ANY,
|
||||
'description' => Config::STRING
|
||||
], Config::KEY_AS_NAME | Config::MAYBE_THUNK | Config::MAYBE_TYPE),
|
||||
'description' => Config::STRING
|
||||
]);
|
||||
|
||||
$this->config = $config;
|
||||
$this->name = $config['name'];
|
||||
$this->astNode = isset($config['astNode']) ? $config['astNode'] : null;
|
||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InputObjectField[]
|
||||
*/
|
||||
public function getFields()
|
||||
{
|
||||
if (null === $this->fields) {
|
||||
$this->fields = [];
|
||||
$fields = isset($this->config['fields']) ? $this->config['fields'] : [];
|
||||
$fields = is_callable($fields) ? call_user_func($fields) : $fields;
|
||||
|
||||
if (!is_array($fields)) {
|
||||
throw new InvariantViolation(
|
||||
"{$this->name} fields must be an array or a callable which returns such an array."
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($fields as $name => $field) {
|
||||
if ($field instanceof Type) {
|
||||
$field = ['type' => $field];
|
||||
}
|
||||
$field = new InputObjectField($field + ['name' => $name]);
|
||||
$this->fields[$field->name] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return InputObjectField
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getField($name)
|
||||
{
|
||||
if (null === $this->fields) {
|
||||
$this->getFields();
|
||||
}
|
||||
Utils::invariant(isset($this->fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name);
|
||||
return $this->fields[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvariantViolation
|
||||
*/
|
||||
public function assertValid()
|
||||
{
|
||||
parent::assertValid();
|
||||
|
||||
$fields = $this->getFields();
|
||||
|
||||
Utils::invariant(
|
||||
!empty($fields),
|
||||
"{$this->name} fields must not be empty"
|
||||
);
|
||||
|
||||
foreach ($fields as $field) {
|
||||
try {
|
||||
Utils::assertValidName($field->name);
|
||||
} catch (InvariantViolation $e) {
|
||||
throw new InvariantViolation("{$this->name}.{$field->name}: {$e->getMessage()}");
|
||||
}
|
||||
|
||||
$fieldType = $field->type;
|
||||
if ($fieldType instanceof WrappingType) {
|
||||
$fieldType = $fieldType->getWrappedType(true);
|
||||
}
|
||||
Utils::invariant(
|
||||
$fieldType instanceof InputType,
|
||||
"{$this->name}.{$field->name} field type must be Input Type but got: %s.",
|
||||
Utils::printSafe($field->type)
|
||||
);
|
||||
Utils::invariant(
|
||||
!isset($field->config['resolve']),
|
||||
"{$this->name}.{$field->name} field type has a resolve property, but Input Types cannot define resolvers."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
/*
|
||||
export type GraphQLInputType =
|
||||
GraphQLScalarType |
|
||||
GraphQLEnumType |
|
||||
GraphQLInputObjectType |
|
||||
GraphQLList |
|
||||
GraphQLNonNull;
|
||||
*/
|
||||
interface InputType
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Language\AST\IntValueNode;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Class IntType
|
||||
* @package GraphQL\Type\Definition
|
||||
*/
|
||||
class IntType extends ScalarType
|
||||
{
|
||||
// As per the GraphQL Spec, Integers are only treated as valid when a valid
|
||||
// 32-bit signed integer, providing the broadest support across platforms.
|
||||
//
|
||||
// n.b. JavaScript's integers are safe between -(2^53 - 1) and 2^53 - 1 because
|
||||
// they are internally represented as IEEE 754 doubles.
|
||||
const MAX_INT = 2147483647;
|
||||
const MIN_INT = -2147483648;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name = Type::INT;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $description =
|
||||
'The `Int` scalar type represents non-fractional signed whole numeric
|
||||
values. Int can represent values between -(2^31) and 2^31 - 1. ';
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return int|null
|
||||
*/
|
||||
public function serialize($value)
|
||||
{
|
||||
if ($value === '') {
|
||||
throw new InvariantViolation('Int cannot represent non 32-bit signed integer value: (empty string)');
|
||||
}
|
||||
if (false === $value || true === $value) {
|
||||
return (int) $value;
|
||||
}
|
||||
if (!is_numeric($value) || $value > self::MAX_INT || $value < self::MIN_INT) {
|
||||
throw new InvariantViolation(sprintf(
|
||||
'Int cannot represent non 32-bit signed integer value: %s',
|
||||
Utils::printSafe($value)
|
||||
));
|
||||
}
|
||||
$num = (float) $value;
|
||||
|
||||
// The GraphQL specification does not allow serializing non-integer values
|
||||
// as Int to avoid accidental data loss.
|
||||
// Examples: 1.0 == 1; 1.1 != 1, etc
|
||||
if ($num != (int) $value) {
|
||||
// Additionally account for scientific notation (i.e. 1e3), because (float)'1e3' is 1000, but (int)'1e3' is 1
|
||||
$trimmed = floor($num);
|
||||
if ($trimmed !== $num) {
|
||||
throw new InvariantViolation(sprintf(
|
||||
'Int cannot represent non-integer value: %s',
|
||||
Utils::printSafe($value)
|
||||
));
|
||||
}
|
||||
}
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return int|null
|
||||
*/
|
||||
public function parseValue($value)
|
||||
{
|
||||
// Below is a fix against PHP bug where (in some combinations of OSs and versions)
|
||||
// boundary values are treated as "double" vs "integer" and failing is_int() check
|
||||
$isInt = is_int($value) || $value === self::MIN_INT || $value === self::MAX_INT;
|
||||
return $isInt && $value <= self::MAX_INT && $value >= self::MIN_INT ? $value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $ast
|
||||
* @return int|null
|
||||
*/
|
||||
public function parseLiteral($ast)
|
||||
{
|
||||
if ($ast instanceof IntValueNode) {
|
||||
$val = (int) $ast->value;
|
||||
if ($ast->value === (string) $val && self::MIN_INT <= $val && $val <= self::MAX_INT) {
|
||||
return $val;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
/**
|
||||
* Class InterfaceType
|
||||
* @package GraphQL\Type\Definition
|
||||
*/
|
||||
class InterfaceType extends Type implements AbstractType, OutputType, CompositeType
|
||||
{
|
||||
/**
|
||||
* @var FieldDefinition[]
|
||||
*/
|
||||
private $fields;
|
||||
|
||||
/**
|
||||
* @var InterfaceTypeDefinitionNode|null
|
||||
*/
|
||||
public $astNode;
|
||||
|
||||
/**
|
||||
* InterfaceType constructor.
|
||||
* @param array $config
|
||||
*/
|
||||
public function __construct(array $config)
|
||||
{
|
||||
if (!isset($config['name'])) {
|
||||
$config['name'] = $this->tryInferName();
|
||||
}
|
||||
|
||||
Utils::assertValidName($config['name']);
|
||||
|
||||
Config::validate($config, [
|
||||
'name' => Config::NAME,
|
||||
'fields' => Config::arrayOf(
|
||||
FieldDefinition::getDefinition(),
|
||||
Config::KEY_AS_NAME | Config::MAYBE_THUNK | Config::MAYBE_TYPE
|
||||
),
|
||||
'resolveType' => Config::CALLBACK, // function($value, $context, ResolveInfo $info) => ObjectType
|
||||
'description' => Config::STRING
|
||||
]);
|
||||
|
||||
$this->name = $config['name'];
|
||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||
$this->astNode = isset($config['astNode']) ? $config['astNode'] : null;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FieldDefinition[]
|
||||
*/
|
||||
public function getFields()
|
||||
{
|
||||
if (null === $this->fields) {
|
||||
$fields = isset($this->config['fields']) ? $this->config['fields'] : [];
|
||||
$this->fields = FieldDefinition::defineFieldMap($this, $fields);
|
||||
}
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* @return FieldDefinition
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getField($name)
|
||||
{
|
||||
if (null === $this->fields) {
|
||||
$this->getFields();
|
||||
}
|
||||
Utils::invariant(isset($this->fields[$name]), 'Field "%s" is not defined for type "%s"', $name, $this->name);
|
||||
return $this->fields[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves concrete ObjectType for given object value
|
||||
*
|
||||
* @param $objectValue
|
||||
* @param $context
|
||||
* @param ResolveInfo $info
|
||||
* @return callable|null
|
||||
*/
|
||||
public function resolveType($objectValue, $context, ResolveInfo $info)
|
||||
{
|
||||
if (isset($this->config['resolveType'])) {
|
||||
$fn = $this->config['resolveType'];
|
||||
return $fn($objectValue, $context, $info);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvariantViolation
|
||||
*/
|
||||
public function assertValid()
|
||||
{
|
||||
parent::assertValid();
|
||||
|
||||
$fields = $this->getFields();
|
||||
|
||||
Utils::invariant(
|
||||
!isset($this->config['resolveType']) || is_callable($this->config['resolveType']),
|
||||
"{$this->name} must provide \"resolveType\" as a function."
|
||||
);
|
||||
|
||||
Utils::invariant(
|
||||
!empty($fields),
|
||||
"{$this->name} fields must not be empty"
|
||||
);
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$field->assertValid($this);
|
||||
foreach ($field->args as $arg) {
|
||||
$arg->assertValid($field, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 3791 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user