Version 4.5.0

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

No files matched your search

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