This commit is contained in:
cutemeli
2025-12-22 10:35:30 +00:00
parent 0bfc6c8425
commit 5ce7ca2c5d
38927 changed files with 0 additions and 4594700 deletions

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2015-present, Webonyx, LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,69 +0,0 @@
# graphql-php
[![Test](https://github.com/webonyx/graphql-php/workflows/Test/badge.svg)](https://github.com/webonyx/graphql-php/actions?query=workflow:Test+branch:master)
[![Static Analysis](https://github.com/webonyx/graphql-php/workflows/Static%20Analysis/badge.svg)](https://github.com/webonyx/graphql-php/actions?query=workflow:%22Static%20Analysis%22+branch:master)
[![Coverage Status](https://codecov.io/gh/webonyx/graphql-php/branch/master/graph/badge.svg)](https://codecov.io/gh/webonyx/graphql-php/branch/master)
[![Latest Stable Version](https://poser.pugx.org/webonyx/graphql-php/version)](https://packagist.org/packages/webonyx/graphql-php)
This is a PHP implementation of the [GraphQL](https://graphql.org) [specification](https://github.com/graphql/graphql-spec)
based on the [reference implementation in JavaScript](https://github.com/graphql/graphql-js).
## Installation
Via composer:
```sh
composer require webonyx/graphql-php
```
## Documentation
Full documentation is available at [https://webonyx.github.io/graphql-php](https://webonyx.github.io/graphql-php)
or in the [docs](docs) directory.
## Examples
There are several ready examples in the [examples](examples) directory,
with a specific README file per example.
## Versioning
This project follows [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).
Elements that belong to the public API of this package are marked with the `@api` PHPDoc tag.
Those elements are thus guaranteed to be stable within major versions. All other elements are
not part of this backwards compatibility guarantee and may change between minor or patch versions.
The most recent version is actively developed on [`master`](https://github.com/webonyx/graphql-php/tree/master).
Older versions are generally no longer supported, although exceptions may be made for sponsors.
## Security
For security related issues, email [benedikt@franke.tech](benedikt@franke.tech) instead of opening a GitHub issue.
## Contributors
This project exists thanks to [all the people](https://github.com/webonyx/graphql-php/graphs/contributors) who [contribute](CONTRIBUTING.md).
## Backers
<a href="https://opencollective.com/webonyx-graphql-php#backers" target="_blank"><img src="https://opencollective.com/webonyx-graphql-php/backers.svg?width=890"></a>
## Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/webonyx-graphql-php#sponsor).
<a href="https://opencollective.com/webonyx-graphql-php/sponsor/0/website" target="_blank"><img src="https://opencollective.com/webonyx-graphql-php/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/webonyx-graphql-php/sponsor/1/website" target="_blank"><img src="https://opencollective.com/webonyx-graphql-php/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/webonyx-graphql-php/sponsor/2/website" target="_blank"><img src="https://opencollective.com/webonyx-graphql-php/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/webonyx-graphql-php/sponsor/3/website" target="_blank"><img src="https://opencollective.com/webonyx-graphql-php/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/webonyx-graphql-php/sponsor/4/website" target="_blank"><img src="https://opencollective.com/webonyx-graphql-php/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/webonyx-graphql-php/sponsor/5/website" target="_blank"><img src="https://opencollective.com/webonyx-graphql-php/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/webonyx-graphql-php/sponsor/6/website" target="_blank"><img src="https://opencollective.com/webonyx-graphql-php/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/webonyx-graphql-php/sponsor/7/website" target="_blank"><img src="https://opencollective.com/webonyx-graphql-php/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/webonyx-graphql-php/sponsor/8/website" target="_blank"><img src="https://opencollective.com/webonyx-graphql-php/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/webonyx-graphql-php/sponsor/9/website" target="_blank"><img src="https://opencollective.com/webonyx-graphql-php/sponsor/9/avatar.svg"></a>
## License
See [LICENSE](LICENSE).

View File

@@ -1,83 +0,0 @@
{
"name": "webonyx/graphql-php",
"description": "A PHP port of GraphQL reference implementation",
"license": "MIT",
"type": "library",
"keywords": [
"graphql",
"API"
],
"homepage": "https://github.com/webonyx/graphql-php",
"require": {
"php": "^7.4 || ^8",
"ext-json": "*",
"ext-mbstring": "*"
},
"require-dev": {
"amphp/amp": "^2.6",
"amphp/http-server": "^2.1",
"dms/phpunit-arraysubset-asserts": "dev-master",
"ergebnis/composer-normalize": "^2.28",
"friendsofphp/php-cs-fixer": "3.89.1",
"mll-lab/php-cs-fixer-config": "5.11.0",
"nyholm/psr7": "^1.5",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "2.1.31",
"phpstan/phpstan-phpunit": "2.0.7",
"phpstan/phpstan-strict-rules": "2.0.7",
"phpunit/phpunit": "^9.5 || ^10.5.21 || ^11",
"psr/http-message": "^1 || ^2",
"react/http": "^1.6",
"react/promise": "^2.0 || ^3.0",
"rector/rector": "^2.0",
"symfony/polyfill-php81": "^1.23",
"symfony/var-exporter": "^5 || ^6 || ^7",
"thecodingmachine/safe": "^1.3 || ^2 || ^3",
"ticketswap/phpstan-error-formatter": "1.2.3"
},
"suggest": {
"amphp/http-server": "To leverage async resolving with webserver on AMPHP platform",
"psr/http-message": "To use standard GraphQL server",
"react/promise": "To leverage async resolving on React PHP platform"
},
"autoload": {
"psr-4": {
"GraphQL\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"GraphQL\\Benchmarks\\": "benchmarks/",
"GraphQL\\Examples\\Blog\\": "examples/01-blog/Blog/",
"GraphQL\\Tests\\": "tests/"
}
},
"config": {
"allow-plugins": {
"composer/package-versions-deprecated": true,
"ergebnis/composer-normalize": true,
"phpstan/extension-installer": true
},
"preferred-install": "dist",
"sort-packages": true
},
"scripts": {
"baseline": "phpstan --generate-baseline",
"bench": "phpbench run",
"check": [
"@fix",
"@stan",
"@test"
],
"docs": "php generate-class-reference.php",
"fix": [
"@rector",
"@php-cs-fixer"
],
"php-cs-fixer": "php-cs-fixer fix",
"rector": "rector process",
"stan": "phpstan --verbose",
"test": "php -d zend.exception_ignore_args=Off -d zend.assertions=On -d assert.active=On -d assert.exception=On vendor/bin/phpunit"
}
}

View File

@@ -1,23 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL;
use GraphQL\Executor\Promise\Adapter\SyncPromise;
/**
* @phpstan-import-type Executor from SyncPromise
*/
class Deferred extends SyncPromise
{
/** @param Executor $executor */
public static function create(callable $executor): self
{
return new self($executor);
}
/** @param Executor $executor */
public function __construct(callable $executor)
{
parent::__construct($executor);
}
}

View File

@@ -1,21 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Error;
/**
* Implementing ClientAware allows graphql-php to decide if this error is safe to be shown to clients.
*
* Only errors that both implement this interface and return true from `isClientSafe()`
* will retain their original error message during formatting.
*
* All other errors will have their message replaced with "Internal server error".
*/
interface ClientAware
{
/**
* Is it safe to show the error message to clients?
*
* @api
*/
public function isClientSafe(): bool;
}

View File

@@ -1,57 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Error;
use GraphQL\Utils\Utils;
/**
* @phpstan-type InputPath list<string|int>
*/
class CoercionError extends Error
{
/** @var InputPath|null */
public ?array $inputPath;
/** @var mixed whatever invalid value was passed */
public $invalidValue;
/**
* @param InputPath|null $inputPath
* @param mixed $invalidValue whatever invalid value was passed
*
* @return static
*/
public static function make(
string $message,
?array $inputPath,
$invalidValue,
?\Throwable $previous = null
): self {
$instance = new static($message, null, null, [], null, $previous);
$instance->inputPath = $inputPath;
$instance->invalidValue = $invalidValue;
return $instance;
}
public function printInputPath(): ?string
{
if ($this->inputPath === null) {
return null;
}
$path = '';
foreach ($this->inputPath as $segment) {
$path .= is_int($segment)
? "[{$segment}]"
: ".{$segment}";
}
return $path;
}
public function printInvalidValue(): string
{
return Utils::printSafeJson($this->invalidValue);
}
}

View File

@@ -1,15 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Error;
/**
* Collection of flags for [error debugging](error-handling.md#debugging-tools).
*/
final class DebugFlag
{
public const NONE = 0;
public const INCLUDE_DEBUG_MESSAGE = 1;
public const INCLUDE_TRACE = 2;
public const RETHROW_INTERNAL_EXCEPTIONS = 4;
public const RETHROW_UNSAFE_EXCEPTIONS = 8;
}

View File

@@ -1,319 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Error;
use GraphQL\Language\AST\Node;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
/**
* 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.
*
* @see \GraphQL\Tests\Error\ErrorTest
*/
class Error extends \Exception implements \JsonSerializable, ClientAware, ProvidesExtensions
{
/**
* Lazily initialized.
*
* @var array<int, SourceLocation>
*/
private array $locations;
/**
* An array describing the JSON-path into the execution response which
* corresponds to this error. Only included for errors during execution.
* When fields are aliased, the path includes aliases.
*
* @var list<int|string>|null
*/
public ?array $path;
/**
* An array describing the JSON-path into the execution response which
* corresponds to this error. Only included for errors during execution.
* This will never include aliases.
*
* @var list<int|string>|null
*/
public ?array $unaliasedPath;
/**
* An array of GraphQL AST Nodes corresponding to this error.
*
* @var array<Node>|null
*/
public ?array $nodes;
/**
* The source GraphQL document for the first location of this error.
*
* Note that if this Error represents more than one node, the source may not
* represent nodes after the first node.
*/
private ?Source $source;
/** @var array<int, int>|null */
private ?array $positions;
private bool $isClientSafe;
/** @var array<string, mixed>|null */
protected ?array $extensions;
/**
* @param iterable<array-key, Node|null>|Node|null $nodes
* @param array<int, int>|null $positions
* @param list<int|string>|null $path
* @param array<string, mixed>|null $extensions
* @param list<int|string>|null $unaliasedPath
*/
public function __construct(
string $message = '',
$nodes = null,
?Source $source = null,
?array $positions = null,
?array $path = null,
?\Throwable $previous = null,
?array $extensions = null,
?array $unaliasedPath = null
) {
parent::__construct($message, 0, $previous);
// Compute list of blame nodes.
if ($nodes instanceof \Traversable) {
/** @phpstan-ignore arrayFilter.strict */
$this->nodes = array_filter(iterator_to_array($nodes));
} elseif (is_array($nodes)) {
$this->nodes = array_filter($nodes);
} elseif ($nodes !== null) {
$this->nodes = [$nodes];
} else {
$this->nodes = null;
}
$this->source = $source;
$this->positions = $positions;
$this->path = $path;
$this->unaliasedPath = $unaliasedPath;
if (is_array($extensions) && $extensions !== []) {
$this->extensions = $extensions;
} elseif ($previous instanceof ProvidesExtensions) {
$this->extensions = $previous->getExtensions();
} else {
$this->extensions = null;
}
$this->isClientSafe = $previous instanceof ClientAware
? $previous->isClientSafe()
: $previous === null;
}
/**
* Given an arbitrary Error, presumably thrown while attempting to execute a
* GraphQL operation, produce a new GraphQLError aware of the location in the
* document responsible for the original Error.
*
* @param mixed $error
* @param iterable<Node>|Node|null $nodes
* @param list<int|string>|null $path
* @param list<int|string>|null $unaliasedPath
*/
public static function createLocatedError($error, $nodes = null, ?array $path = null, ?array $unaliasedPath = null): Error
{
if ($error instanceof self) {
if ($error->isLocated()) {
return $error;
}
$nodes ??= $error->getNodes();
$path ??= $error->getPath();
$unaliasedPath ??= $error->getUnaliasedPath();
}
$source = null;
$originalError = null;
$positions = [];
$extensions = [];
if ($error instanceof self) {
$message = $error->getMessage();
$originalError = $error;
$source = $error->getSource();
$positions = $error->getPositions();
$extensions = $error->getExtensions();
} elseif ($error instanceof InvariantViolation) {
$message = $error->getMessage();
$originalError = $error->getPrevious() ?? $error;
} elseif ($error instanceof \Throwable) {
$message = $error->getMessage();
$originalError = $error;
} else {
$message = (string) $error;
}
$nonEmptyMessage = $message === ''
? 'An unknown error occurred.'
: $message;
return new static(
$nonEmptyMessage,
$nodes,
$source,
$positions,
$path,
$originalError,
$extensions,
$unaliasedPath
);
}
protected function isLocated(): bool
{
$path = $this->getPath();
$nodes = $this->getNodes();
return $path !== null
&& $path !== []
&& $nodes !== null
&& $nodes !== [];
}
public function isClientSafe(): bool
{
return $this->isClientSafe;
}
public function getSource(): ?Source
{
return $this->source
??= $this->nodes[0]->loc->source
?? null;
}
/** @return array<int, int> */
public function getPositions(): array
{
if (! isset($this->positions)) {
$this->positions = [];
if (isset($this->nodes)) {
foreach ($this->nodes as $node) {
if (isset($node->loc->start)) {
$this->positions[] = $node->loc->start;
}
}
}
}
return $this->positions;
}
/**
* An array of locations within the source GraphQL document which correspond to this error.
*
* Each entry has information about `line` and `column` within source GraphQL document:
* $location->line;
* $location->column;
*
* Errors during validation often contain multiple locations, for example to
* point out to field mentioned in multiple fragments. Errors during execution include a
* single location, the field which produced the error.
*
* @return array<int, SourceLocation>
*
* @api
*/
public function getLocations(): array
{
if (! isset($this->locations)) {
$positions = $this->getPositions();
$source = $this->getSource();
$nodes = $this->getNodes();
$this->locations = [];
if ($source !== null && $positions !== []) {
foreach ($positions as $position) {
$this->locations[] = $source->getLocation($position);
}
} elseif ($nodes !== null && $nodes !== []) {
foreach ($nodes as $node) {
if (isset($node->loc->source)) {
$this->locations[] = $node->loc->source->getLocation($node->loc->start);
}
}
}
}
return $this->locations;
}
/** @return array<Node>|null */
public function getNodes(): ?array
{
return $this->nodes;
}
/**
* Returns an array describing the path from the root value to the field which produced this error.
* Only included for execution errors. When fields are aliased, the path includes aliases.
*
* @return list<int|string>|null
*
* @api
*/
public function getPath(): ?array
{
return $this->path;
}
/**
* Returns an array describing the path from the root value to the field which produced this error.
* Only included for execution errors. This will never include aliases.
*
* @return list<int|string>|null
*
* @api
*/
public function getUnaliasedPath(): ?array
{
return $this->unaliasedPath;
}
/** @return array<string, mixed>|null */
public function getExtensions(): ?array
{
return $this->extensions;
}
/**
* Specify data which should be serialized to JSON.
*
* @see http://php.net/manual/en/jsonserializable.jsonserialize.php
*
* @return array<string, mixed> data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource
*/
#[\ReturnTypeWillChange]
public function jsonSerialize(): array
{
return FormattedError::createFromException($this);
}
public function __toString(): string
{
return FormattedError::printError($this);
}
}

View File

@@ -1,337 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Error;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\Utils;
use PHPUnit\Framework\Test;
/**
* 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.
*
* @see ExecutionResult
*
* @phpstan-import-type SerializableError from ExecutionResult
* @phpstan-import-type ErrorFormatter from ExecutionResult
*
* @see \GraphQL\Tests\Error\FormattedErrorTest
*/
class FormattedError
{
private static string $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
*/
public static function setInternalErrorMessage(string $msg): void
{
self::$internalErrorMessage = $msg;
}
/**
* Prints a GraphQLError to a string, representing useful location information
* about the error's position in the source.
*/
public static function printError(Error $error): string
{
$printedLocations = [];
$nodes = $error->nodes;
if (isset($nodes) && $nodes !== []) {
foreach ($nodes as $node) {
$location = $node->loc;
if (isset($location)) {
$source = $location->source;
if (isset($source)) {
$printedLocations[] = self::highlightSourceAtLocation(
$source,
$source->getLocation($location->start)
);
}
}
}
} elseif ($error->getSource() !== null && $error->getLocations() !== []) {
$source = $error->getSource();
foreach ($error->getLocations() as $location) {
$printedLocations[] = self::highlightSourceAtLocation($source, $location);
}
}
return $printedLocations === []
? $error->getMessage()
: implode("\n\n", array_merge([$error->getMessage()], $printedLocations)) . "\n";
}
/**
* Render a helpful description of the location of the error in the GraphQL
* Source document.
*/
private static function highlightSourceAtLocation(Source $source, SourceLocation $location): string
{
$line = $location->line;
$lineOffset = $source->locationOffset->line - 1;
$columnOffset = self::getColumnOffset($source, $location);
$contextLine = $line + $lineOffset;
$contextColumn = $location->column + $columnOffset;
$prevLineNum = (string) ($contextLine - 1);
$lineNum = (string) $contextLine;
$nextLineNum = (string) ($contextLine + 1);
$padLen = strlen($nextLineNum);
$lines = Utils::splitLines($source->body);
$lines[0] = self::spaces($source->locationOffset->column - 1) . $lines[0];
$outputLines = [
"{$source->name} ({$contextLine}:{$contextColumn})",
$line >= 2 ? (self::leftPad($padLen, $prevLineNum) . ': ' . $lines[$line - 2]) : null,
self::leftPad($padLen, $lineNum) . ': ' . $lines[$line - 1],
self::spaces(2 + $padLen + $contextColumn - 1) . '^',
$line < count($lines) ? self::leftPad($padLen, $nextLineNum) . ': ' . $lines[$line] : null,
];
return implode("\n", array_filter($outputLines));
}
private static function getColumnOffset(Source $source, SourceLocation $location): int
{
return $location->line === 1
? $source->locationOffset->column - 1
: 0;
}
private static function spaces(int $length): string
{
return str_repeat(' ', $length);
}
private static function leftPad(int $length, string $str): string
{
return self::spaces($length - mb_strlen($str)) . $str;
}
/**
* Convert any exception to a GraphQL spec compliant array.
*
* This method only exposes the exception message when the given exception
* implements the ClientAware interface, or when debug flags are passed.
*
* For a list of available debug flags @see \GraphQL\Error\DebugFlag constants.
*
* @return SerializableError
*
* @api
*/
public static function createFromException(\Throwable $exception, int $debugFlag = DebugFlag::NONE, ?string $internalErrorMessage = null): array
{
$internalErrorMessage ??= self::$internalErrorMessage;
$message = $exception instanceof ClientAware && $exception->isClientSafe()
? $exception->getMessage()
: $internalErrorMessage;
$formattedError = ['message' => $message];
if ($exception instanceof Error) {
$locations = array_map(
static fn (SourceLocation $loc): array => $loc->toSerializableArray(),
$exception->getLocations()
);
if ($locations !== []) {
$formattedError['locations'] = $locations;
}
if ($exception->path !== null && $exception->path !== []) {
$formattedError['path'] = $exception->path;
}
}
if ($exception instanceof ProvidesExtensions) {
$extensions = $exception->getExtensions();
if (is_array($extensions) && $extensions !== []) {
$formattedError['extensions'] = $extensions;
}
}
if ($debugFlag !== DebugFlag::NONE) {
$formattedError = self::addDebugEntries($formattedError, $exception, $debugFlag);
}
return $formattedError;
}
/**
* Decorates spec-compliant $formattedError with debug entries according to $debug flags.
*
* @param SerializableError $formattedError
* @param int $debugFlag For available flags @see \GraphQL\Error\DebugFlag
*
* @throws \Throwable
*
* @return SerializableError
*/
public static function addDebugEntries(array $formattedError, \Throwable $e, int $debugFlag): array
{
if ($debugFlag === DebugFlag::NONE) {
return $formattedError;
}
if (($debugFlag & DebugFlag::RETHROW_INTERNAL_EXCEPTIONS) !== 0) {
if (! $e instanceof Error) {
throw $e;
}
if ($e->getPrevious() !== null) {
throw $e->getPrevious();
}
}
$isUnsafe = ! $e instanceof ClientAware || ! $e->isClientSafe();
if (($debugFlag & DebugFlag::RETHROW_UNSAFE_EXCEPTIONS) !== 0 && $isUnsafe && $e->getPrevious() !== null) {
throw $e->getPrevious();
}
if (($debugFlag & DebugFlag::INCLUDE_DEBUG_MESSAGE) !== 0 && $isUnsafe) {
$formattedError['extensions']['debugMessage'] = $e->getMessage();
}
if (($debugFlag & DebugFlag::INCLUDE_TRACE) !== 0) {
$actualError = $e->getPrevious() ?? $e;
if ($e instanceof \ErrorException || $e instanceof \Error) {
$formattedError['extensions']['file'] = $e->getFile();
$formattedError['extensions']['line'] = $e->getLine();
} else {
$formattedError['extensions']['file'] = $actualError->getFile();
$formattedError['extensions']['line'] = $actualError->getLine();
}
$isTrivial = $e instanceof Error && $e->getPrevious() === null;
if (! $isTrivial) {
$formattedError['extensions']['trace'] = static::toSafeTrace($actualError);
}
}
return $formattedError;
}
/**
* Prepares final error formatter taking in account $debug flags.
*
* If initial formatter is not set, FormattedError::createFromException is used.
*
* @phpstan-param ErrorFormatter|null $formatter
*/
public static function prepareFormatter(?callable $formatter, int $debug): callable
{
return $formatter === null
? static fn (\Throwable $e): array => static::createFromException($e, $debug)
: static fn (\Throwable $e): array => static::addDebugEntries($formatter($e), $e, $debug);
}
/**
* Returns error trace as serializable array.
*
* @return array<int, array{
* file?: string,
* line?: int,
* function?: string,
* call?: string,
* }>
*
* @api
*/
public static function toSafeTrace(\Throwable $error): array
{
$trace = $error->getTrace();
if (
isset($trace[0]['function']) && isset($trace[0]['class'])
// Remove invariant entries as they don't provide much value:
&& ($trace[0]['class'] . '::' . $trace[0]['function'] === 'GraphQL\Utils\Utils::invariant')
) {
array_shift($trace);
} elseif (! isset($trace[0]['file'])) {
// Remove root call as it's likely error handler trace:
array_shift($trace);
}
$formatted = [];
foreach ($trace as $err) {
$safeErr = [];
if (isset($err['file'])) {
$safeErr['file'] = $err['file'];
}
if (isset($err['line'])) {
$safeErr['line'] = $err['line'];
}
$func = $err['function'];
$args = array_map([self::class, 'printVar'], $err['args'] ?? []);
$funcStr = $func . '(' . implode(', ', $args) . ')';
if (isset($err['class'])) {
$safeErr['call'] = $err['class'] . '::' . $funcStr;
} else {
$safeErr['function'] = $funcStr;
}
$formatted[] = $safeErr;
}
return $formatted;
}
/** @param mixed $var */
public static function printVar($var): string
{
if ($var instanceof Type) {
return 'GraphQLType: ' . $var->toString();
}
if (is_object($var)) {
// Calling `count` on instances of `PHPUnit\Framework\Test` triggers an unintended side effect - see https://github.com/sebastianbergmann/phpunit/issues/5866#issuecomment-2172429263
$count = ! $var instanceof Test && $var instanceof \Countable
? '(' . count($var) . ')'
: '';
return 'instance of ' . get_class($var) . $count;
}
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 (string) $var;
}
if ($var === null) {
return 'null';
}
return gettype($var);
}
}

View File

@@ -1,10 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Error;
/**
* Note:
* This exception should not inherit base Error exception as it is raised when there is an error somewhere in
* user-land code.
*/
class InvariantViolation extends \LogicException {}

View File

@@ -1,16 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Error;
/**
* Implementing HasExtensions allows this error to provide additional data to clients.
*/
interface ProvidesExtensions
{
/**
* Data to include within the "extensions" key of the formatted error.
*
* @return array<string, mixed>|null
*/
public function getExtensions(): ?array;
}

View File

@@ -1,11 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Error;
/**
* Thrown when failing to serialize a leaf value.
*
* Not generally safe for clients, as the wrong given value could
* be something not intended to ever be seen by clients.
*/
class SerializationError extends \Exception {}

View File

@@ -1,18 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Error;
use GraphQL\Language\Source;
class SyntaxError extends Error
{
public function __construct(Source $source, int $position, string $description)
{
parent::__construct(
"Syntax Error: {$description}",
null,
$source,
[$position]
);
}
}

View File

@@ -1,14 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Error;
/**
* Caused by GraphQL clients and can safely be displayed.
*/
class UserError extends \RuntimeException implements ClientAware
{
public function isClientSafe(): bool
{
return true;
}
}

View File

@@ -1,122 +0,0 @@
<?php declare(strict_types=1);
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).
*
* @phpstan-type WarningHandler callable(string $errorMessage, int $warningId, ?int $messageLevel): void
*/
final class Warning
{
public const NONE = 0;
public const WARNING_ASSIGN = 2;
public const WARNING_CONFIG = 4;
public const WARNING_FULL_SCHEMA_SCAN = 8;
public const WARNING_CONFIG_DEPRECATION = 16;
public const WARNING_NOT_A_TYPE = 32;
public const ALL = 63;
private static int $enableWarnings = self::ALL;
/** @var array<int, true> */
private static array $warned = [];
/**
* @var callable|null
*
* @phpstan-var WarningHandler|null
*/
private static $warningHandler;
/**
* Sets warning handler which can intercept all system warnings.
* When not set, trigger_error() is used to notify about warnings.
*
* @phpstan-param WarningHandler|null $warningHandler
*
* @api
*/
public static function setWarningHandler(?callable $warningHandler = null): void
{
self::$warningHandler = $warningHandler;
}
/**
* Suppress warning by id (has no effect when custom warning handler is set).
*
* @param bool|int $suppress
*
* @example Warning::suppress(Warning::WARNING_NOT_A_TYPE) suppress a specific warning
* @example Warning::suppress(true) suppresses all warnings
* @example Warning::suppress(false) enables all warnings
*
* @api
*/
public static function suppress($suppress = true): void
{
if ($suppress === true) {
self::$enableWarnings = 0;
} elseif ($suppress === false) {
self::$enableWarnings = self::ALL;
// @phpstan-ignore-next-line necessary until we can use proper unions
} elseif (is_int($suppress)) {
self::$enableWarnings &= ~$suppress;
} else {
$type = gettype($suppress);
throw new \InvalidArgumentException("Expected type bool|int, got {$type}.");
}
}
/**
* Re-enable previously suppressed warning by id (has no effect when custom warning handler is set).
*
* @param bool|int $enable
*
* @example Warning::suppress(Warning::WARNING_NOT_A_TYPE) re-enables a specific warning
* @example Warning::suppress(true) re-enables all warnings
* @example Warning::suppress(false) suppresses all warnings
*
* @api
*/
public static function enable($enable = true): void
{
if ($enable === true) {
self::$enableWarnings = self::ALL;
} elseif ($enable === false) {
self::$enableWarnings = 0;
// @phpstan-ignore-next-line necessary until we can use proper unions
} elseif (is_int($enable)) {
self::$enableWarnings |= $enable;
} else {
$type = gettype($enable);
throw new \InvalidArgumentException("Expected type bool|int, got {$type}.");
}
}
public static function warnOnce(string $errorMessage, int $warningId, ?int $messageLevel = null): void
{
$messageLevel ??= \E_USER_WARNING;
if (self::$warningHandler !== null) {
(self::$warningHandler)($errorMessage, $warningId, $messageLevel);
} elseif ((self::$enableWarnings & $warningId) > 0 && ! isset(self::$warned[$warningId])) {
self::$warned[$warningId] = true;
trigger_error($errorMessage, $messageLevel);
}
}
public static function warn(string $errorMessage, int $warningId, ?int $messageLevel = null): void
{
$messageLevel ??= \E_USER_WARNING;
if (self::$warningHandler !== null) {
(self::$warningHandler)($errorMessage, $warningId, $messageLevel);
} elseif ((self::$enableWarnings & $warningId) > 0) {
trigger_error($errorMessage, $messageLevel);
}
}
}

View File

@@ -1,94 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Executor;
use GraphQL\Error\Error;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Type\Schema;
/**
* Data that must be available at all points during query execution.
*
* Namely, schema of the type system that is currently executing,
* and the fragments defined in the query document.
*
* @phpstan-import-type FieldResolver from Executor
* @phpstan-import-type ArgsMapper from Executor
*/
class ExecutionContext
{
public Schema $schema;
/** @var array<string, FragmentDefinitionNode> */
public array $fragments;
/** @var mixed */
public $rootValue;
/** @var mixed */
public $contextValue;
public OperationDefinitionNode $operation;
/** @var array<string, mixed> */
public array $variableValues;
/**
* @var callable
*
* @phpstan-var FieldResolver
*/
public $fieldResolver;
/**
* @var callable
*
* @phpstan-var ArgsMapper
*/
public $argsMapper;
/** @var list<Error> */
public array $errors;
public PromiseAdapter $promiseAdapter;
/**
* @param array<string, FragmentDefinitionNode> $fragments
* @param mixed $rootValue
* @param mixed $contextValue
* @param array<string, mixed> $variableValues
* @param list<Error> $errors
*
* @phpstan-param FieldResolver $fieldResolver
*/
public function __construct(
Schema $schema,
array $fragments,
$rootValue,
$contextValue,
OperationDefinitionNode $operation,
array $variableValues,
array $errors,
callable $fieldResolver,
callable $argsMapper,
PromiseAdapter $promiseAdapter
) {
$this->schema = $schema;
$this->fragments = $fragments;
$this->rootValue = $rootValue;
$this->contextValue = $contextValue;
$this->operation = $operation;
$this->variableValues = $variableValues;
$this->errors = $errors;
$this->fieldResolver = $fieldResolver;
$this->argsMapper = $argsMapper;
$this->promiseAdapter = $promiseAdapter;
}
public function addError(Error $error): void
{
$this->errors[] = $error;
}
}

View File

@@ -1,186 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Executor;
use GraphQL\Error\DebugFlag;
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()`.
*
* @phpstan-type SerializableError array{
* message: string,
* locations?: array<int, array{line: int, column: int}>,
* path?: array<int, int|string>,
* extensions?: array<string, mixed>
* }
* @phpstan-type SerializableErrors list<SerializableError>
* @phpstan-type SerializableResult array{
* data?: array<string, mixed>,
* errors?: SerializableErrors,
* extensions?: array<string, mixed>
* }
* @phpstan-type ErrorFormatter callable(\Throwable): SerializableError
* @phpstan-type ErrorsHandler callable(list<Error> $errors, ErrorFormatter $formatter): SerializableErrors
*
* @see \GraphQL\Tests\Executor\ExecutionResultTest
*/
class ExecutionResult implements \JsonSerializable
{
/**
* Data collected from resolvers during query execution.
*
* @api
*
* @var array<string, mixed>|null
*/
public ?array $data = null;
/**
* Errors registered during query execution.
*
* If an error was caused by exception thrown in resolver, $error->getPrevious() would
* contain original exception.
*
* @api
*
* @var list<Error>
*/
public array $errors = [];
/**
* User-defined serializable array of extensions included in serialized result.
*
* @api
*
* @var array<string, mixed>|null
*/
public ?array $extensions = null;
/**
* @var callable|null
*
* @phpstan-var ErrorFormatter|null
*/
private $errorFormatter;
/**
* @var callable|null
*
* @phpstan-var ErrorsHandler|null
*/
private $errorsHandler;
/**
* @param array<string, mixed>|null $data
* @param list<Error> $errors
* @param array<string, mixed> $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
* );
*
* @phpstan-param ErrorFormatter|null $errorFormatter
*
* @api
*/
public function setErrorFormatter(?callable $errorFormatter): self
{
$this->errorFormatter = $errorFormatter;
return $this;
}
/**
* Define custom logic for error handling (filtering, logging, etc).
*
* Expected handler signature is:
* fn (array $errors, callable $formatter): array
*
* Default handler is:
* fn (array $errors, callable $formatter): array => array_map($formatter, $errors)
*
* @phpstan-param ErrorsHandler|null $errorsHandler
*
* @api
*/
public function setErrorsHandler(?callable $errorsHandler): self
{
$this->errorsHandler = $errorsHandler;
return $this;
}
/** @phpstan-return SerializableResult */
#[\ReturnTypeWillChange]
public function jsonSerialize(): array
{
return $this->toArray();
}
/**
* Converts GraphQL query result to spec-compliant serializable array using provided
* errors handler and formatter.
*
* If debug argument is passed, output of error formatter is enriched which debugging information
* ("debugMessage", "trace" keys depending on flags).
*
* $debug argument must sum of flags from @see \GraphQL\Error\DebugFlag
*
* @phpstan-return SerializableResult
*
* @api
*/
public function toArray(int $debug = DebugFlag::NONE): array
{
$result = [];
if ($this->errors !== []) {
$errorsHandler = $this->errorsHandler
?? static fn (array $errors, callable $formatter): array => array_map($formatter, $errors);
/** @phpstan-var SerializableErrors */
$handledErrors = $errorsHandler(
$this->errors,
FormattedError::prepareFormatter($this->errorFormatter, $debug)
);
// While we know that there were errors initially, they might have been discarded
if ($handledErrors !== []) {
$result['errors'] = $handledErrors;
}
}
if ($this->data !== null) {
$result['data'] = $this->data;
}
if ($this->extensions !== null && $this->extensions !== []) {
$result['extensions'] = $this->extensions;
}
return $result;
}
}

View File

@@ -1,219 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Executor;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Schema;
use GraphQL\Utils\Utils;
/**
* Implements the "Evaluating requests" section of the GraphQL specification.
*
* @phpstan-type ArgsMapper callable(array<string, mixed>, FieldDefinition, FieldNode, mixed): mixed
* @phpstan-type FieldResolver callable(mixed, array<string, mixed>, mixed, ResolveInfo): mixed
* @phpstan-type ImplementationFactory callable(PromiseAdapter, Schema, DocumentNode, mixed, mixed, array<mixed>, ?string, callable, callable): ExecutorImplementation
*
* @see \GraphQL\Tests\Executor\ExecutorTest
*/
class Executor
{
/**
* @var callable
*
* @phpstan-var FieldResolver
*/
private static $defaultFieldResolver = [self::class, 'defaultFieldResolver'];
/**
* @var callable
*
* @phpstan-var ArgsMapper
*/
private static $defaultArgsMapper = [self::class, 'defaultArgsMapper'];
private static ?PromiseAdapter $defaultPromiseAdapter;
/**
* @var callable
*
* @phpstan-var ImplementationFactory
*/
private static $implementationFactory = [ReferenceExecutor::class, 'create'];
/** @phpstan-return FieldResolver */
public static function getDefaultFieldResolver(): callable
{
return self::$defaultFieldResolver;
}
/**
* Set a custom default resolve function.
*
* @phpstan-param FieldResolver $fieldResolver
*/
public static function setDefaultFieldResolver(callable $fieldResolver): void
{
self::$defaultFieldResolver = $fieldResolver;
}
/** @phpstan-return ArgsMapper */
public static function getDefaultArgsMapper(): callable
{
return self::$defaultArgsMapper;
}
/** @phpstan-param ArgsMapper $argsMapper */
public static function setDefaultArgsMapper(callable $argsMapper): void
{
self::$defaultArgsMapper = $argsMapper;
}
public static function getDefaultPromiseAdapter(): PromiseAdapter
{
return self::$defaultPromiseAdapter ??= new SyncPromiseAdapter();
}
/** Set a custom default promise adapter. */
public static function setDefaultPromiseAdapter(?PromiseAdapter $defaultPromiseAdapter = null): void
{
self::$defaultPromiseAdapter = $defaultPromiseAdapter;
}
/** @phpstan-return ImplementationFactory */
public static function getImplementationFactory(): callable
{
return self::$implementationFactory;
}
/**
* Set a custom executor implementation factory.
*
* @phpstan-param ImplementationFactory $implementationFactory
*/
public static function setImplementationFactory(callable $implementationFactory): void
{
self::$implementationFactory = $implementationFactory;
}
/**
* Executes DocumentNode against given $schema.
*
* Always returns ExecutionResult and never throws.
* All errors which occur during operation execution are collected in `$result->errors`.
*
* @param mixed $rootValue
* @param mixed $contextValue
* @param array<string, mixed>|null $variableValues
*
* @phpstan-param FieldResolver|null $fieldResolver
*
* @api
*
* @throws InvariantViolation
*/
public static function execute(
Schema $schema,
DocumentNode $documentNode,
$rootValue = null,
$contextValue = null,
?array $variableValues = null,
?string $operationName = null,
?callable $fieldResolver = null
): ExecutionResult {
$promiseAdapter = new SyncPromiseAdapter();
$result = static::promiseToExecute(
$promiseAdapter,
$schema,
$documentNode,
$rootValue,
$contextValue,
$variableValues,
$operationName,
$fieldResolver
);
return $promiseAdapter->wait($result);
}
/**
* Same as execute(), but requires promise adapter and returns a promise which is always
* fulfilled with an instance of ExecutionResult and never rejected.
*
* Useful for async PHP platforms.
*
* @param mixed $rootValue
* @param mixed $contextValue
* @param array<string, mixed>|null $variableValues
*
* @phpstan-param FieldResolver|null $fieldResolver
* @phpstan-param ArgsMapper|null $argsMapper
*
* @api
*/
public static function promiseToExecute(
PromiseAdapter $promiseAdapter,
Schema $schema,
DocumentNode $documentNode,
$rootValue = null,
$contextValue = null,
?array $variableValues = null,
?string $operationName = null,
?callable $fieldResolver = null,
?callable $argsMapper = null
): Promise {
$executor = (self::$implementationFactory)(
$promiseAdapter,
$schema,
$documentNode,
$rootValue,
$contextValue,
$variableValues ?? [],
$operationName,
$fieldResolver ?? self::$defaultFieldResolver,
$argsMapper ?? self::$defaultArgsMapper,
);
return $executor->doExecute();
}
/**
* If a resolve function is not given, then a default resolve behavior is used
* which takes the property of the root value of the same name as the field
* and returns it as the result, or if it's a function, returns the result
* of calling that function while passing along args and context.
*
* @param mixed $objectLikeValue
* @param array<string, mixed> $args
* @param mixed $contextValue
*
* @return mixed
*/
public static function defaultFieldResolver($objectLikeValue, array $args, $contextValue, ResolveInfo $info)
{
$property = Utils::extractKey($objectLikeValue, $info->fieldName);
return $property instanceof \Closure
? $property($objectLikeValue, $args, $contextValue, $info)
: $property;
}
/**
* @template T of array<string, mixed>
*
* @param T $args
*
* @return T
*/
public static function defaultArgsMapper(array $args): array
{
return $args;
}
}

View File

@@ -1,11 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Executor;
use GraphQL\Executor\Promise\Promise;
interface ExecutorImplementation
{
/** Returns promise of {@link ExecutionResult}. Promise should always resolve, never reject. */
public function doExecute(): Promise;
}

View File

@@ -1,152 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Executor\Promise\Adapter;
use Amp\Deferred;
use Amp\Failure;
use Amp\Promise as AmpPromise;
use Amp\Success;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Executor\Promise\PromiseAdapter;
use function Amp\Promise\all;
class AmpPromiseAdapter implements PromiseAdapter
{
public function isThenable($value): bool
{
return $value instanceof AmpPromise;
}
/** @throws InvariantViolation */
public function convertThenable($thenable): Promise
{
return new Promise($thenable, $this);
}
/** @throws InvariantViolation */
public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise
{
$deferred = new Deferred();
$onResolve = static function (?\Throwable $reason, $value) use ($onFulfilled, $onRejected, $deferred): void {
if ($reason === null && $onFulfilled !== null) {
self::resolveWithCallable($deferred, $onFulfilled, $value);
} elseif ($reason === null) {
$deferred->resolve($value);
} elseif ($onRejected !== null) {
self::resolveWithCallable($deferred, $onRejected, $reason);
} else {
$deferred->fail($reason);
}
};
$adoptedPromise = $promise->adoptedPromise;
assert($adoptedPromise instanceof AmpPromise);
$adoptedPromise->onResolve($onResolve);
return new Promise($deferred->promise(), $this);
}
/** @throws InvariantViolation */
public function create(callable $resolver): Promise
{
$deferred = new Deferred();
$resolver(
static function ($value) use ($deferred): void {
$deferred->resolve($value);
},
static function (\Throwable $exception) use ($deferred): void {
$deferred->fail($exception);
}
);
return new Promise($deferred->promise(), $this);
}
/**
* @throws \Error
* @throws InvariantViolation
*/
public function createFulfilled($value = null): Promise
{
$promise = new Success($value);
return new Promise($promise, $this);
}
/** @throws InvariantViolation */
public function createRejected(\Throwable $reason): Promise
{
$promise = new Failure($reason);
return new Promise($promise, $this);
}
/**
* @throws \Error
* @throws InvariantViolation
*/
public function all(iterable $promisesOrValues): Promise
{
/** @var array<AmpPromise<mixed>> $promises */
$promises = [];
foreach ($promisesOrValues as $key => $item) {
if ($item instanceof Promise) {
$ampPromise = $item->adoptedPromise;
assert($ampPromise instanceof AmpPromise);
$promises[$key] = $ampPromise;
} elseif ($item instanceof AmpPromise) {
$promises[$key] = $item;
}
}
$deferred = new Deferred();
all($promises)->onResolve(static function (?\Throwable $reason, ?array $values) use ($promisesOrValues, $deferred): void {
if ($reason === null) {
assert(is_array($values), 'Either $reason or $values must be passed');
$promisesOrValuesArray = is_array($promisesOrValues)
? $promisesOrValues
: iterator_to_array($promisesOrValues);
$resolvedValues = array_replace($promisesOrValuesArray, $values);
$deferred->resolve($resolvedValues);
return;
}
$deferred->fail($reason);
});
return new Promise($deferred->promise(), $this);
}
/**
* @template TArgument
* @template TResult of AmpPromise<mixed>
*
* @param Deferred<TResult> $deferred
* @param callable(TArgument): TResult $callback
* @param TArgument $argument
*/
private static function resolveWithCallable(Deferred $deferred, callable $callback, $argument): void
{
try {
$result = $callback($argument);
} catch (\Throwable $exception) {
$deferred->fail($exception);
return;
}
if ($result instanceof Promise) {
/** @var TResult $result */
$result = $result->adoptedPromise;
}
$deferred->resolve($result);
}
}

View File

@@ -1,80 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Executor\Promise\Adapter;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Executor\Promise\PromiseAdapter;
use React\Promise\Promise as ReactPromise;
use React\Promise\PromiseInterface as ReactPromiseInterface;
use function React\Promise\all;
use function React\Promise\reject;
use function React\Promise\resolve;
class ReactPromiseAdapter implements PromiseAdapter
{
public function isThenable($value): bool
{
return $value instanceof ReactPromiseInterface;
}
/** @throws InvariantViolation */
public function convertThenable($thenable): Promise
{
return new Promise($thenable, $this);
}
/** @throws InvariantViolation */
public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise
{
$adoptedPromise = $promise->adoptedPromise;
assert($adoptedPromise instanceof ReactPromiseInterface);
return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this);
}
/** @throws InvariantViolation */
public function create(callable $resolver): Promise
{
$promise = new ReactPromise($resolver);
return new Promise($promise, $this);
}
/** @throws InvariantViolation */
public function createFulfilled($value = null): Promise
{
$promise = resolve($value);
return new Promise($promise, $this);
}
/** @throws InvariantViolation */
public function createRejected(\Throwable $reason): Promise
{
$promise = reject($reason);
return new Promise($promise, $this);
}
/** @throws InvariantViolation */
public function all(iterable $promisesOrValues): Promise
{
foreach ($promisesOrValues as &$promiseOrValue) {
if ($promiseOrValue instanceof Promise) {
$promiseOrValue = $promiseOrValue->adoptedPromise;
}
}
$promisesOrValuesArray = is_array($promisesOrValues)
? $promisesOrValues
: iterator_to_array($promisesOrValues);
$promise = all($promisesOrValuesArray)->then(static fn ($values): array => array_map(
static fn ($key) => $values[$key],
array_keys($promisesOrValuesArray),
));
return new Promise($promise, $this);
}
}

View File

@@ -1,216 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Executor\Promise\Adapter;
use GraphQL\Error\InvariantViolation;
/**
* Simplistic (yet full-featured) implementation of Promises A+ spec for regular PHP `sync` mode
* (using queue to defer promises execution).
*
* Library users are not supposed to use SyncPromise class in their resolvers.
* Instead, they should use @see \GraphQL\Deferred which enforces `$executor` callback in the constructor.
*
* Root SyncPromise without explicit `$executor` will never resolve (actually throw while trying).
* The whole point of Deferred is to ensure it never happens and that any resolver creates
* at least one $executor to start the promise chain.
*
* @phpstan-type Executor callable(): mixed
*/
class SyncPromise
{
public const PENDING = 'pending';
public const FULFILLED = 'fulfilled';
public const REJECTED = 'rejected';
public string $state = self::PENDING;
/** @var mixed */
public $result;
/**
* Promises created in `then` method of this promise and awaiting resolution of this promise.
*
* @var array<
* int,
* array{
* self,
* (callable(mixed): mixed)|null,
* (callable(\Throwable): mixed)|null
* }
* >
*/
protected array $waiting = [];
public static function runQueue(): void
{
$q = self::getQueue();
while (! $q->isEmpty()) {
$task = $q->dequeue();
$task();
}
}
/** @param Executor|null $executor */
public function __construct(?callable $executor = null)
{
if ($executor === null) {
return;
}
self::getQueue()->enqueue(function () use ($executor): void {
try {
$this->resolve($executor());
} catch (\Throwable $e) {
$this->reject($e);
}
});
}
/**
* @param mixed $value
*
* @throws \Exception
*/
public function resolve($value): self
{
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): void {
$this->resolve($resolvedValue);
},
function ($reason): void {
$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;
}
/**
* @throws \Exception
*
* @return $this
*/
public function reject(\Throwable $reason): self
{
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;
}
/** @throws InvariantViolation */
private function enqueueWaitingPromises(): void
{
if ($this->state === self::PENDING) {
throw new InvariantViolation('Cannot enqueue derived promises when parent is still pending');
}
foreach ($this->waiting as $descriptor) {
self::getQueue()->enqueue(function () use ($descriptor): void {
[$promise, $onFulfilled, $onRejected] = $descriptor;
if ($this->state === self::FULFILLED) {
try {
$promise->resolve($onFulfilled === null ? $this->result : $onFulfilled($this->result));
} catch (\Throwable $e) {
$promise->reject($e);
}
} elseif ($this->state === self::REJECTED) {
try {
if ($onRejected === null) {
$promise->reject($this->result);
} else {
$promise->resolve($onRejected($this->result));
}
} catch (\Throwable $e) {
$promise->reject($e);
}
}
});
}
$this->waiting = [];
}
/** @return \SplQueue<callable(): void> */
public static function getQueue(): \SplQueue
{
static $queue;
return $queue ??= new \SplQueue();
}
/**
* @param (callable(mixed): mixed)|null $onFulfilled
* @param (callable(\Throwable): mixed)|null $onRejected
*
* @throws InvariantViolation
*/
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): self
{
if ($this->state === self::REJECTED && $onRejected === null) {
return $this;
}
if ($this->state === self::FULFILLED && $onFulfilled === null) {
return $this;
}
$tmp = new self();
$this->waiting[] = [$tmp, $onFulfilled, $onRejected];
if ($this->state !== self::PENDING) {
$this->enqueueWaitingPromises();
}
return $tmp;
}
/**
* @param callable(\Throwable): mixed $onRejected
*
* @throws InvariantViolation
*/
public function catch(callable $onRejected): self
{
return $this->then(null, $onRejected);
}
}

View File

@@ -1,167 +0,0 @@
<?php declare(strict_types=1);
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;
/**
* Allows changing order of field resolution even in sync environments
* (by leveraging queue of deferreds and promises).
*/
class SyncPromiseAdapter implements PromiseAdapter
{
public function isThenable($value): bool
{
return $value instanceof SyncPromise;
}
/** @throws InvariantViolation */
public function convertThenable($thenable): Promise
{
if (! $thenable instanceof SyncPromise) {
// End-users should always use Deferred (and don't use SyncPromise directly)
$deferred = Deferred::class;
$safeThenable = Utils::printSafe($thenable);
throw new InvariantViolation("Expected instance of {$deferred}, got {$safeThenable}");
}
return new Promise($thenable, $this);
}
/** @throws InvariantViolation */
public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise
{
$adoptedPromise = $promise->adoptedPromise;
assert($adoptedPromise instanceof SyncPromise);
return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this);
}
/**
* @throws \Exception
* @throws InvariantViolation
*/
public function create(callable $resolver): Promise
{
$promise = new SyncPromise();
try {
$resolver(
[$promise, 'resolve'],
[$promise, 'reject']
);
} catch (\Throwable $e) {
$promise->reject($e);
}
return new Promise($promise, $this);
}
/**
* @throws \Exception
* @throws InvariantViolation
*/
public function createFulfilled($value = null): Promise
{
$promise = new SyncPromise();
return new Promise($promise->resolve($value), $this);
}
/**
* @throws \Exception
* @throws InvariantViolation
*/
public function createRejected(\Throwable $reason): Promise
{
$promise = new SyncPromise();
return new Promise($promise->reject($reason), $this);
}
/**
* @throws \Exception
* @throws InvariantViolation
*/
public function all(iterable $promisesOrValues): Promise
{
$all = new SyncPromise();
$total = is_array($promisesOrValues)
? count($promisesOrValues)
: iterator_count($promisesOrValues);
$count = 0;
$result = [];
$resolveAllWhenFinished = function () use (&$count, &$total, $all, &$result): void {
if ($count === $total) {
$all->resolve($result);
}
};
foreach ($promisesOrValues as $index => $promiseOrValue) {
if ($promiseOrValue instanceof Promise) {
$result[$index] = null;
$promiseOrValue->then(
static function ($value) use (&$result, $index, &$count, &$resolveAllWhenFinished): void {
$result[$index] = $value;
++$count;
$resolveAllWhenFinished();
},
[$all, 'reject']
);
} else {
$result[$index] = $promiseOrValue;
++$count;
}
}
$resolveAllWhenFinished();
return new Promise($all, $this);
}
/**
* Synchronously wait when promise completes.
*
* @throws InvariantViolation
*
* @return mixed
*/
public function wait(Promise $promise)
{
$this->beforeWait($promise);
$taskQueue = SyncPromise::getQueue();
$syncPromise = $promise->adoptedPromise;
assert($syncPromise instanceof SyncPromise);
while (
$syncPromise->state === SyncPromise::PENDING
&& ! $taskQueue->isEmpty()
) {
SyncPromise::runQueue();
$this->onWait($promise);
}
if ($syncPromise->state === SyncPromise::FULFILLED) {
return $syncPromise->result;
}
if ($syncPromise->state === SyncPromise::REJECTED) {
throw $syncPromise->result;
}
throw new InvariantViolation('Could not resolve promise');
}
/** Execute just before starting to run promise completion. */
protected function beforeWait(Promise $promise): void {}
/** Execute while running promise completion. */
protected function onWait(Promise $promise): void {}
}

View File

@@ -1,39 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Executor\Promise;
use Amp\Promise as AmpPromise;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\Promise\Adapter\SyncPromise;
use React\Promise\PromiseInterface as ReactPromise;
/**
* Convenience wrapper for promises represented by Promise Adapter.
*/
class Promise
{
/** @var SyncPromise|ReactPromise<mixed>|AmpPromise<mixed> */
public $adoptedPromise;
private PromiseAdapter $adapter;
/**
* @param mixed $adoptedPromise
*
* @throws InvariantViolation
*/
public function __construct($adoptedPromise, PromiseAdapter $adapter)
{
if ($adoptedPromise instanceof self) {
throw new InvariantViolation('Expecting promise from adapted system, got ' . self::class);
}
$this->adoptedPromise = $adoptedPromise;
$this->adapter = $adapter;
}
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): Promise
{
return $this->adapter->then($this, $onFulfilled, $onRejected);
}
}

View File

@@ -1,72 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Executor\Promise;
/**
* Provides a means for integration of async PHP platforms ([related docs](data-fetching.md#async-php)).
*/
interface PromiseAdapter
{
/**
* Is the value a promise or a deferred of the underlying platform?
*
* @param mixed $value
*
* @api
*/
public function isThenable($value): bool;
/**
* Converts thenable of the underlying platform into GraphQL\Executor\Promise\Promise instance.
*
* @param mixed $thenable
*
* @api
*/
public function convertThenable($thenable): Promise;
/**
* 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
*/
public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise;
/**
* Creates a Promise from the given resolver callable.
*
* @param callable(callable $resolve, callable $reject): void $resolver
*
* @api
*/
public function create(callable $resolver): Promise;
/**
* Creates a fulfilled Promise for a value if the value is not a promise.
*
* @param mixed $value
*
* @api
*/
public function createFulfilled($value = null): Promise;
/**
* 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
*/
public function createRejected(\Throwable $reason): Promise;
/**
* Given an iterable of promises (or values), returns a promise that is fulfilled when all the
* items in the iterable are fulfilled.
*
* @param iterable<Promise|mixed> $promisesOrValues
*
* @api
*/
public function all(iterable $promisesOrValues): Promise;
}

View File

@@ -1,20 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Executor;
use GraphQL\Executor\Promise\Promise;
class PromiseExecutor implements ExecutorImplementation
{
private Promise $result;
public function __construct(Promise $result)
{
$this->result = $result;
}
public function doExecute(): Promise
{
return $this->result;
}
}

View File

@@ -1,13 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Executor;
/**
* When the object passed as `$contextValue` to GraphQL execution implements this,
* its `clone()` method will be called before passing the context down to a field.
* This allows passing information to child fields in the query tree without affecting sibling or parent fields.
*/
interface ScopedContext
{
public function clone(): self;
}

View File

@@ -1,279 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Executor;
use GraphQL\Error\Error;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\EnumTypeDefinitionNode;
use GraphQL\Language\AST\EnumTypeExtensionNode;
use GraphQL\Language\AST\EnumValueDefinitionNode;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeExtensionNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\NullValueNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\AST\ObjectTypeExtensionNode;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
use GraphQL\Language\AST\ScalarTypeExtensionNode;
use GraphQL\Language\AST\SchemaExtensionNode;
use GraphQL\Language\AST\UnionTypeDefinitionNode;
use GraphQL\Language\AST\UnionTypeExtensionNode;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Language\AST\VariableNode;
use GraphQL\Language\Printer;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use GraphQL\Utils\AST;
use GraphQL\Utils\Utils;
use GraphQL\Utils\Value;
/**
* @see ArgumentNode - force IDE import
*
* @phpstan-import-type ArgumentNodeValue from ArgumentNode
*
* @see \GraphQL\Tests\Executor\ValuesTest
*/
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, an Error will be thrown.
*
* @param NodeList<VariableDefinitionNode> $varDefNodes
* @param array<string, mixed> $rawVariableValues
*
* @throws \Exception
*
* @return array{array<int, Error>, null}|array{null, array<string, mixed>}
*/
public static function getVariableValues(Schema $schema, NodeList $varDefNodes, array $rawVariableValues): array
{
$errors = [];
$coercedValues = [];
foreach ($varDefNodes as $varDefNode) {
$varName = $varDefNode->variable->name->value;
$varType = AST::typeFromAST([$schema, 'getType'], $varDefNode->type);
if (! Type::isInputType($varType)) {
// Must use input types for variables. This should be caught during
// validation, however is checked again here for safety.
$typeStr = Printer::doPrint($varDefNode->type);
$errors[] = new Error(
"Variable \"\${$varName}\" expected value of type \"{$typeStr}\" which cannot be used as an input type.",
[$varDefNode->type]
);
} else {
$hasValue = array_key_exists($varName, $rawVariableValues);
$value = $hasValue
? $rawVariableValues[$varName]
: Utils::undefined();
if (! $hasValue && ($varDefNode->defaultValue !== null)) {
// If no value was provided to a variable with a default value,
// use the default value.
$coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType);
} elseif ((! $hasValue || $value === null) && ($varType instanceof NonNull)) {
// If no value or a nullish value was provided to a variable with a
// non-null type (required), produce an error.
$safeVarType = Utils::printSafe($varType);
$message = $hasValue
? "Variable \"\${$varName}\" of non-null type \"{$safeVarType}\" must not be null."
: "Variable \"\${$varName}\" of required type \"{$safeVarType}\" was not provided.";
$errors[] = new Error($message, [$varDefNode]);
} elseif ($hasValue) {
if ($value === null) {
// If the explicit value `null` was provided, an entry in the coerced
// values must exist as the value `null`.
$coercedValues[$varName] = null;
} else {
// Otherwise, a non-null value was provided, coerce it to the expected
// type or report an error if coercion fails.
$coerced = Value::coerceInputValue($value, $varType);
$coercionErrors = $coerced['errors'];
if ($coercionErrors !== null) {
foreach ($coercionErrors as $coercionError) {
$invalidValue = $coercionError->printInvalidValue();
$inputPath = $coercionError->printInputPath();
$pathMessage = $inputPath !== null
? " at \"{$varName}{$inputPath}\""
: '';
$errors[] = new Error(
"Variable \"\${$varName}\" got invalid value {$invalidValue}{$pathMessage}; {$coercionError->getMessage()}",
$varDefNode,
$coercionError->getSource(),
$coercionError->getPositions(),
$coercionError->getPath(),
$coercionError,
$coercionError->getExtensions()
);
}
} else {
$coercedValues[$varName] = $coerced['value'];
}
}
}
}
}
return $errors === []
? [null, $coercedValues]
: [$errors, null];
}
/**
* Prepares an object map of argument values given a directive definition
* and an 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 EnumTypeDefinitionNode|EnumTypeExtensionNode|EnumValueDefinitionNode|FieldDefinitionNode|FieldNode|FragmentDefinitionNode|FragmentSpreadNode|InlineFragmentNode|InputObjectTypeDefinitionNode|InputObjectTypeExtensionNode|InputValueDefinitionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode|ObjectTypeDefinitionNode|ObjectTypeExtensionNode|OperationDefinitionNode|ScalarTypeDefinitionNode|ScalarTypeExtensionNode|SchemaExtensionNode|UnionTypeDefinitionNode|UnionTypeExtensionNode|VariableDefinitionNode $node
* @param array<string, mixed>|null $variableValues
*
* @throws \Exception
* @throws Error
*
* @return array<string, mixed>|null
*/
public static function getDirectiveValues(Directive $directiveDef, Node $node, ?array $variableValues = null): ?array
{
$directiveDefName = $directiveDef->name;
foreach ($node->directives as $directive) {
if ($directive->name->value === $directiveDefName) {
return self::getArgumentValues($directiveDef, $directive, $variableValues);
}
}
return null;
}
/**
* Prepares an object map of argument values given a list of argument
* definitions and list of argument AST nodes.
*
* @param FieldDefinition|Directive $def
* @param FieldNode|DirectiveNode $node
* @param array<string, mixed>|null $variableValues
*
* @throws \Exception
* @throws Error
*
* @return array<string, mixed>
*/
public static function getArgumentValues($def, Node $node, ?array $variableValues = null): array
{
if ($def->args === []) {
return [];
}
/** @var array<string, ArgumentNodeValue> $argumentValueMap */
$argumentValueMap = [];
// Might not be defined when an AST from JS is used
if (isset($node->arguments)) {
foreach ($node->arguments as $argumentNode) {
$argumentValueMap[$argumentNode->name->value] = $argumentNode->value;
}
}
return static::getArgumentValuesForMap($def, $argumentValueMap, $variableValues, $node);
}
/**
* @param FieldDefinition|Directive $def
* @param array<string, ArgumentNodeValue> $argumentValueMap
* @param array<string, mixed>|null $variableValues
*
* @throws \Exception
* @throws Error
*
* @return array<string, mixed>
*/
public static function getArgumentValuesForMap($def, array $argumentValueMap, ?array $variableValues = null, ?Node $referenceNode = null): array
{
/** @var array<string, mixed> $coercedValues */
$coercedValues = [];
foreach ($def->args as $argumentDefinition) {
$name = $argumentDefinition->name;
$argType = $argumentDefinition->getType();
$argumentValueNode = $argumentValueMap[$name] ?? null;
if ($argumentValueNode instanceof VariableNode) {
$variableName = $argumentValueNode->name->value;
$hasValue = $variableValues !== null && array_key_exists($variableName, $variableValues);
$isNull = $hasValue && $variableValues[$variableName] === null;
} else {
$hasValue = $argumentValueNode !== null;
$isNull = $argumentValueNode instanceof NullValueNode;
}
if (! $hasValue && $argumentDefinition->defaultValueExists()) {
// If no argument was provided where the definition has a default value,
// use the default value.
$coercedValues[$name] = $argumentDefinition->defaultValue;
} elseif ((! $hasValue || $isNull) && ($argType instanceof NonNull)) {
// If no argument or a null value was provided to an argument with a
// non-null type (required), produce a field error.
$safeArgType = Utils::printSafe($argType);
if ($isNull) {
throw new Error("Argument \"{$name}\" of non-null type \"{$safeArgType}\" must not be null.", $referenceNode);
}
if ($argumentValueNode instanceof VariableNode) {
throw new Error("Argument \"{$name}\" of required type \"{$safeArgType}\" was provided the variable \"\${$argumentValueNode->name->value}\" which was not provided a runtime value.", [$argumentValueNode]);
}
throw new Error("Argument \"{$name}\" of required type \"{$safeArgType}\" was not provided.", $referenceNode);
} elseif ($hasValue) {
assert($argumentValueNode instanceof Node);
if ($argumentValueNode instanceof NullValueNode) {
// If the explicit value `null` was provided, an entry in the coerced
// values must exist as the value `null`.
$coercedValues[$name] = null;
} elseif ($argumentValueNode instanceof VariableNode) {
$variableName = $argumentValueNode->name->value;
// Note: This does no further checking that this variable is correct.
// This assumes that this query has been validated and the variable
// usage here is of the correct type.
$coercedValues[$name] = $variableValues[$variableName] ?? null;
} else {
$coercedValue = AST::valueFromAST($argumentValueNode, $argType, $variableValues);
if (Utils::undefined() === $coercedValue) {
// Note: ValuesOfCorrectType validation should catch this before
// execution. This is a runtime check to ensure execution does not
// continue with an invalid argument value.
$invalidValue = Printer::doPrint($argumentValueNode);
throw new Error("Argument \"{$name}\" has invalid value {$invalidValue}.", [$argumentValueNode]);
}
$coercedValues[$name] = $coercedValue;
}
}
}
return $coercedValues;
}
}

View File

@@ -1,259 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL;
use GraphQL\Error\Error;
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\Language\AST\DocumentNode;
use GraphQL\Language\Parser;
use GraphQL\Language\Source;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema as SchemaType;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\ValidationRule;
/**
* This is the primary facade for fulfilling GraphQL operations.
* See [related documentation](executing-queries.md).
*
* @phpstan-import-type ArgsMapper from Executor
* @phpstan-import-type FieldResolver from Executor
*
* @see \GraphQL\Tests\GraphQLTest
*/
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).
* contextValue:
* The context value is provided as an argument to resolver functions after
* field arguments. It is used to pass shared information useful at any point
* during executing this query, for example the currently logged in user and
* connections to databases or other services.
* If the passed object implements the `ScopedContext` interface,
* its `clone()` method will be called before passing the context down to a field.
* This allows passing information to child fields in the query tree without affecting sibling or parent fields.
* variableValues:
* A mapping of variable name to runtime value to use for all variables
* defined in the requestString.
* operationName:
* The name of the operation to use if requestString contains multiple
* possible operations. Can be omitted if requestString contains only
* one operation.
* fieldResolver:
* A resolver function to use when one is not provided by the schema.
* If not provided, the default field resolver is used (which looks for a
* value on the source value with the field's name).
* validationRules:
* A set of rules for query validation step. Default value is all available rules.
* Empty array would allow to skip query validation (may be convenient for persisted
* queries which are validated before persisting and assumed valid during execution)
*
* @param string|DocumentNode $source
* @param mixed $rootValue
* @param mixed $contextValue
* @param array<string, mixed>|null $variableValues
* @param array<ValidationRule>|null $validationRules
*
* @api
*
* @throws \Exception
* @throws InvariantViolation
*/
public static function executeQuery(
SchemaType $schema,
$source,
$rootValue = null,
$contextValue = null,
?array $variableValues = null,
?string $operationName = null,
?callable $fieldResolver = null,
?array $validationRules = null
): ExecutionResult {
$promiseAdapter = new SyncPromiseAdapter();
$promise = self::promiseToExecute(
$promiseAdapter,
$schema,
$source,
$rootValue,
$contextValue,
$variableValues,
$operationName,
$fieldResolver,
$validationRules
);
return $promiseAdapter->wait($promise);
}
/**
* Same as executeQuery(), but requires PromiseAdapter and always returns a Promise.
* Useful for Async PHP platforms.
*
* @param string|DocumentNode $source
* @param mixed $rootValue
* @param mixed $context
* @param array<string, mixed>|null $variableValues
* @param array<ValidationRule>|null $validationRules Defaults to using all available rules
*
* @api
*
* @throws \Exception
*/
public static function promiseToExecute(
PromiseAdapter $promiseAdapter,
SchemaType $schema,
$source,
$rootValue = null,
$context = null,
?array $variableValues = null,
?string $operationName = null,
?callable $fieldResolver = null,
?array $validationRules = null
): Promise {
try {
$documentNode = $source instanceof DocumentNode
? $source
: Parser::parse(new Source($source, 'GraphQL'));
if ($validationRules === null) {
$queryComplexity = DocumentValidator::getRule(QueryComplexity::class);
assert($queryComplexity instanceof QueryComplexity, 'should not register a different rule for QueryComplexity');
$queryComplexity->setRawVariableValues($variableValues);
} else {
foreach ($validationRules as $rule) {
if ($rule instanceof QueryComplexity) {
$rule->setRawVariableValues($variableValues);
}
}
}
$validationErrors = DocumentValidator::validate($schema, $documentNode, $validationRules);
if ($validationErrors !== []) {
return $promiseAdapter->createFulfilled(
new ExecutionResult(null, $validationErrors)
);
}
return Executor::promiseToExecute(
$promiseAdapter,
$schema,
$documentNode,
$rootValue,
$context,
$variableValues,
$operationName,
$fieldResolver
);
} catch (Error $e) {
return $promiseAdapter->createFulfilled(
new ExecutionResult(null, [$e])
);
}
}
/**
* Returns directives defined in GraphQL spec.
*
* @throws InvariantViolation
*
* @return array<string, Directive>
*
* @api
*/
public static function getStandardDirectives(): array
{
return Directive::getInternalDirectives();
}
/**
* Returns types defined in GraphQL spec.
*
* @throws InvariantViolation
*
* @return array<string, ScalarType>
*
* @api
*/
public static function getStandardTypes(): array
{
return Type::getStandardTypes();
}
/**
* Replaces standard types with types from this list (matching by name).
*
* Standard types not listed here remain untouched.
*
* @param array<string, ScalarType> $types
*
* @api
*
* @throws InvariantViolation
*/
public static function overrideStandardTypes(array $types): void
{
Type::overrideStandardTypes($types);
}
/**
* Returns standard validation rules implementing GraphQL spec.
*
* @return array<class-string<ValidationRule>, ValidationRule>
*
* @api
*/
public static function getStandardValidationRules(): array
{
return DocumentValidator::defaultRules();
}
/**
* Set default resolver implementation.
*
* @phpstan-param FieldResolver $fn
*
* @api
*/
public static function setDefaultFieldResolver(callable $fn): void
{
Executor::setDefaultFieldResolver($fn);
}
/**
* Set default args mapper implementation.
*
* @phpstan-param ArgsMapper $fn
*
* @api
*/
public static function setDefaultArgsMapper(callable $fn): void
{
Executor::setDefaultArgsMapper($fn);
}
}

View File

@@ -1,16 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
/**
* @phpstan-type ArgumentNodeValue VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode
*/
class ArgumentNode extends Node
{
public string $kind = NodeKind::ARGUMENT;
/** @phpstan-var ArgumentNodeValue */
public ValueNode $value;
public NameNode $name;
}

View File

@@ -1,10 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class BooleanValueNode extends Node implements ValueNode
{
public string $kind = NodeKind::BOOLEAN;
public bool $value;
}

View File

@@ -1,11 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
/**
* export type DefinitionNode =
* | ExecutableDefinitionNode
* | TypeSystemDefinitionNode
* | TypeSystemExtensionNode;.
*/
interface DefinitionNode {}

View File

@@ -1,26 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class DirectiveDefinitionNode extends Node implements TypeSystemDefinitionNode
{
public string $kind = NodeKind::DIRECTIVE_DEFINITION;
public NameNode $name;
public ?StringValueNode $description = null;
/** @var NodeList<InputValueDefinitionNode> */
public NodeList $arguments;
public bool $repeatable;
/** @var NodeList<NameNode> */
public NodeList $locations;
public function __construct(array $vars)
{
parent::__construct($vars);
$this->arguments ??= new NodeList([]);
}
}

View File

@@ -1,19 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class DirectiveNode extends Node
{
public string $kind = NodeKind::DIRECTIVE;
public NameNode $name;
/** @var NodeList<ArgumentNode> */
public NodeList $arguments;
public function __construct(array $vars)
{
parent::__construct($vars);
$this->arguments ??= new NodeList([]);
}
}

View File

@@ -1,11 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class DocumentNode extends Node
{
public string $kind = NodeKind::DOCUMENT;
/** @var NodeList<DefinitionNode&Node> */
public NodeList $definitions;
}

View File

@@ -1,29 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class EnumTypeDefinitionNode extends Node implements TypeDefinitionNode
{
public string $kind = NodeKind::ENUM_TYPE_DEFINITION;
public NameNode $name;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
/** @var NodeList<EnumValueDefinitionNode> */
public NodeList $values;
public ?StringValueNode $description = null;
public function getName(): NameNode
{
return $this->name;
}
public function __construct(array $vars)
{
parent::__construct($vars);
$this->directives ??= new NodeList([]);
}
}

View File

@@ -1,27 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class EnumTypeExtensionNode extends Node implements TypeExtensionNode
{
public string $kind = NodeKind::ENUM_TYPE_EXTENSION;
public NameNode $name;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
/** @var NodeList<EnumValueDefinitionNode> */
public NodeList $values;
public function __construct(array $vars)
{
parent::__construct($vars);
$this->directives ??= new NodeList([]);
}
public function getName(): NameNode
{
return $this->name;
}
}

View File

@@ -1,15 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class EnumValueDefinitionNode extends Node
{
public string $kind = NodeKind::ENUM_VALUE_DEFINITION;
public NameNode $name;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
public ?StringValueNode $description = null;
}

View File

@@ -1,10 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class EnumValueNode extends Node implements ValueNode
{
public string $kind = NodeKind::ENUM;
public string $value;
}

View File

@@ -1,10 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
/**
* export type ExecutableDefinitionNode =
* | OperationDefinitionNode
* | FragmentDefinitionNode;.
*/
interface ExecutableDefinitionNode extends DefinitionNode {}

View File

@@ -1,21 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class FieldDefinitionNode extends Node
{
public string $kind = NodeKind::FIELD_DEFINITION;
public NameNode $name;
/** @var NodeList<InputValueDefinitionNode> */
public NodeList $arguments;
/** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */
public TypeNode $type;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
public ?StringValueNode $description = null;
}

View File

@@ -1,27 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class FieldNode extends Node implements SelectionNode
{
public string $kind = NodeKind::FIELD;
public NameNode $name;
public ?NameNode $alias = null;
/** @var NodeList<ArgumentNode> */
public NodeList $arguments;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
public ?SelectionSetNode $selectionSet = null;
public function __construct(array $vars)
{
parent::__construct($vars);
$this->directives ??= new NodeList([]);
$this->arguments ??= new NodeList([]);
}
}

View File

@@ -1,10 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class FloatValueNode extends Node implements ValueNode
{
public string $kind = NodeKind::FLOAT;
public string $value;
}

View File

@@ -1,38 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class FragmentDefinitionNode extends Node implements ExecutableDefinitionNode, HasSelectionSet
{
public string $kind = NodeKind::FRAGMENT_DEFINITION;
public NameNode $name;
/**
* Note: fragment variable definitions are experimental and may be changed
* or removed in the future.
*
* Thus, this property is the single exception where this is not always a NodeList but may be null.
*
* @var NodeList<VariableDefinitionNode>|null
*/
public ?NodeList $variableDefinitions = null;
public NamedTypeNode $typeCondition;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
public SelectionSetNode $selectionSet;
public function __construct(array $vars)
{
parent::__construct($vars);
$this->directives ??= new NodeList([]);
}
public function getSelectionSet(): SelectionSetNode
{
return $this->selectionSet;
}
}

View File

@@ -1,19 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class FragmentSpreadNode extends Node implements SelectionNode
{
public string $kind = NodeKind::FRAGMENT_SPREAD;
public NameNode $name;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
public function __construct(array $vars)
{
parent::__construct($vars);
$this->directives ??= new NodeList([]);
}
}

View File

@@ -1,12 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
/**
* export type DefinitionNode = OperationDefinitionNode
* | FragmentDefinitionNode.
*/
interface HasSelectionSet
{
public function getSelectionSet(): SelectionSetNode;
}

View File

@@ -1,21 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class InlineFragmentNode extends Node implements SelectionNode
{
public string $kind = NodeKind::INLINE_FRAGMENT;
public ?NamedTypeNode $typeCondition = null;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
public SelectionSetNode $selectionSet;
public function __construct(array $vars)
{
parent::__construct($vars);
$this->directives ??= new NodeList([]);
}
}

View File

@@ -1,29 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class InputObjectTypeDefinitionNode extends Node implements TypeDefinitionNode
{
public string $kind = NodeKind::INPUT_OBJECT_TYPE_DEFINITION;
public NameNode $name;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
/** @var NodeList<InputValueDefinitionNode> */
public NodeList $fields;
public ?StringValueNode $description = null;
public function getName(): NameNode
{
return $this->name;
}
public function __construct(array $vars)
{
parent::__construct($vars);
$this->directives ??= new NodeList([]);
}
}

View File

@@ -1,21 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class InputObjectTypeExtensionNode extends Node implements TypeExtensionNode
{
public string $kind = NodeKind::INPUT_OBJECT_TYPE_EXTENSION;
public NameNode $name;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
/** @var NodeList<InputValueDefinitionNode> */
public NodeList $fields;
public function getName(): NameNode
{
return $this->name;
}
}

View File

@@ -1,21 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class InputValueDefinitionNode extends Node
{
public string $kind = NodeKind::INPUT_VALUE_DEFINITION;
public NameNode $name;
/** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */
public TypeNode $type;
/** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null */
public ?ValueNode $defaultValue = null;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
public ?StringValueNode $description = null;
}

View File

@@ -1,10 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class IntValueNode extends Node implements ValueNode
{
public string $kind = NodeKind::INT;
public string $value;
}

View File

@@ -1,26 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class InterfaceTypeDefinitionNode extends Node implements TypeDefinitionNode
{
public string $kind = NodeKind::INTERFACE_TYPE_DEFINITION;
public NameNode $name;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
/** @var NodeList<NamedTypeNode> */
public NodeList $interfaces;
/** @var NodeList<FieldDefinitionNode> */
public NodeList $fields;
public ?StringValueNode $description = null;
public function getName(): NameNode
{
return $this->name;
}
}

View File

@@ -1,24 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class InterfaceTypeExtensionNode extends Node implements TypeExtensionNode
{
public string $kind = NodeKind::INTERFACE_TYPE_EXTENSION;
public NameNode $name;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
/** @var NodeList<NamedTypeNode> */
public NodeList $interfaces;
/** @var NodeList<FieldDefinitionNode> */
public NodeList $fields;
public function getName(): NameNode
{
return $this->name;
}
}

View File

@@ -1,11 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class ListTypeNode extends Node implements TypeNode
{
public string $kind = NodeKind::LIST_TYPE;
/** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */
public TypeNode $type;
}

View File

@@ -1,11 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class ListValueNode extends Node implements ValueNode
{
public string $kind = NodeKind::LST;
/** @var NodeList<ValueNode&Node> */
public NodeList $values;
}

View File

@@ -1,63 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
use GraphQL\Language\Source;
use GraphQL\Language\Token;
/**
* Contains a range of UTF-8 character offsets and token references that
* identify the region of the source from which the AST derived.
*
* @phpstan-type LocationArray array{start: int, end: int}
*/
class Location
{
/** The character offset at which this Node begins. */
public int $start;
/** The character offset at which this Node ends. */
public int $end;
/** The Token at which this Node begins. */
public ?Token $startToken = null;
/** The Token at which this Node ends. */
public ?Token $endToken = null;
/** The Source document the AST represents. */
public ?Source $source = null;
public static function create(int $start, int $end): self
{
$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 === null || $endToken === null) {
return;
}
$this->start = $startToken->start;
$this->end = $endToken->end;
}
/** @return LocationArray */
public function toArray(): array
{
return [
'start' => $this->start,
'end' => $this->end,
];
}
}

View File

@@ -1,10 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class NameNode extends Node implements TypeNode
{
public string $kind = NodeKind::NAME;
public string $value;
}

View File

@@ -1,10 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class NamedTypeNode extends Node implements TypeNode
{
public string $kind = NodeKind::NAMED_TYPE;
public NameNode $name;
}

View File

@@ -1,145 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils;
/**
* type Node = NameNode
* | DocumentNode
* | OperationDefinitionNode
* | VariableDefinitionNode
* | VariableNode
* | SelectionSetNode
* | FieldNode
* | ArgumentNode
* | FragmentSpreadNode
* | InlineFragmentNode
* | FragmentDefinitionNode
* | IntValueNode
* | FloatValueNode
* | StringValueNode
* | BooleanValueNode
* | EnumValueNode
* | ListValueNode
* | ObjectValueNode
* | ObjectFieldNode
* | DirectiveNode
* | ListTypeNode
* | NonNullTypeNode.
*
* @see \GraphQL\Tests\Language\AST\NodeTest
*/
abstract class Node implements \JsonSerializable
{
public ?Location $loc = null;
public string $kind;
/** @param array<string, mixed> $vars */
public function __construct(array $vars)
{
Utils::assign($this, $vars);
}
/**
* Returns a clone of this instance and all its children, except Location $loc.
*
* @throws \JsonException
* @throws InvariantViolation
*
* @return static
*/
public function cloneDeep(): self
{
return static::cloneValue($this);
}
/**
* @template TNode of Node
* @template TCloneable of TNode|NodeList<TNode>|Location|string
*
* @phpstan-param TCloneable $value
*
* @throws \JsonException
* @throws InvariantViolation
*
* @phpstan-return TCloneable
*/
protected static function cloneValue($value)
{
if ($value instanceof self) {
$cloned = clone $value;
foreach (get_object_vars($cloned) as $prop => $propValue) {
$cloned->{$prop} = static::cloneValue($propValue); // @phpstan-ignore argument.templateType
}
return $cloned;
}
if ($value instanceof NodeList) {
/**
* @phpstan-var TCloneable
*
* @phpstan-ignore varTag.nativeType (PHPStan is strict about template types and sees NodeList<TNode> as potentially different from TCloneable)
*/
return $value->cloneDeep();
}
return $value;
}
/** @throws \JsonException */
public function __toString(): string
{
return json_encode($this, JSON_THROW_ON_ERROR);
}
/**
* Improves upon the default serialization by:
* - excluding null values
* - excluding large reference values such as @see Location::$source.
*
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
/** @return array<string, mixed> */
public function toArray(): array
{
return self::recursiveToArray($this);
}
/** @return array<string, mixed> */
private static function recursiveToArray(Node $node): array
{
$result = [];
foreach (get_object_vars($node) as $prop => $propValue) {
if ($propValue === null) {
continue;
}
if ($propValue instanceof NodeList) {
$converted = [];
foreach ($propValue as $item) {
$converted[] = self::recursiveToArray($item);
}
} elseif ($propValue instanceof Node) {
$converted = self::recursiveToArray($propValue);
} elseif ($propValue instanceof Location) {
$converted = $propValue->toArray();
} else {
$converted = $propValue;
}
$result[$prop] = $converted;
}
return $result;
}
}

View File

@@ -1,138 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
/**
* Holds constants of possible AST nodes.
*/
class NodeKind
{
// constants from language/kinds.js:
public const NAME = 'Name';
// Document
public const DOCUMENT = 'Document';
public const OPERATION_DEFINITION = 'OperationDefinition';
public const VARIABLE_DEFINITION = 'VariableDefinition';
public const VARIABLE = 'Variable';
public const SELECTION_SET = 'SelectionSet';
public const FIELD = 'Field';
public const ARGUMENT = 'Argument';
// Fragments
public const FRAGMENT_SPREAD = 'FragmentSpread';
public const INLINE_FRAGMENT = 'InlineFragment';
public const FRAGMENT_DEFINITION = 'FragmentDefinition';
// Values
public const INT = 'IntValue';
public const FLOAT = 'FloatValue';
public const STRING = 'StringValue';
public const BOOLEAN = 'BooleanValue';
public const ENUM = 'EnumValue';
public const NULL = 'NullValue';
public const LST = 'ListValue';
public const OBJECT = 'ObjectValue';
public const OBJECT_FIELD = 'ObjectField';
// Directives
public const DIRECTIVE = 'Directive';
// Types
public const NAMED_TYPE = 'NamedType';
public const LIST_TYPE = 'ListType';
public const NON_NULL_TYPE = 'NonNullType';
// Type System Definitions
public const SCHEMA_DEFINITION = 'SchemaDefinition';
public const OPERATION_TYPE_DEFINITION = 'OperationTypeDefinition';
// Type Definitions
public const SCALAR_TYPE_DEFINITION = 'ScalarTypeDefinition';
public const OBJECT_TYPE_DEFINITION = 'ObjectTypeDefinition';
public const FIELD_DEFINITION = 'FieldDefinition';
public const INPUT_VALUE_DEFINITION = 'InputValueDefinition';
public const INTERFACE_TYPE_DEFINITION = 'InterfaceTypeDefinition';
public const UNION_TYPE_DEFINITION = 'UnionTypeDefinition';
public const ENUM_TYPE_DEFINITION = 'EnumTypeDefinition';
public const ENUM_VALUE_DEFINITION = 'EnumValueDefinition';
public const INPUT_OBJECT_TYPE_DEFINITION = 'InputObjectTypeDefinition';
// Type Extensions
public const SCALAR_TYPE_EXTENSION = 'ScalarTypeExtension';
public const OBJECT_TYPE_EXTENSION = 'ObjectTypeExtension';
public const INTERFACE_TYPE_EXTENSION = 'InterfaceTypeExtension';
public const UNION_TYPE_EXTENSION = 'UnionTypeExtension';
public const ENUM_TYPE_EXTENSION = 'EnumTypeExtension';
public const INPUT_OBJECT_TYPE_EXTENSION = 'InputObjectTypeExtension';
// Directive Definitions
public const DIRECTIVE_DEFINITION = 'DirectiveDefinition';
// Type System Extensions
public const SCHEMA_EXTENSION = 'SchemaExtension';
public const CLASS_MAP = [
self::NAME => NameNode::class,
// Document
self::DOCUMENT => DocumentNode::class,
self::OPERATION_DEFINITION => OperationDefinitionNode::class,
self::VARIABLE_DEFINITION => VariableDefinitionNode::class,
self::VARIABLE => VariableNode::class,
self::SELECTION_SET => SelectionSetNode::class,
self::FIELD => FieldNode::class,
self::ARGUMENT => ArgumentNode::class,
// Fragments
self::FRAGMENT_SPREAD => FragmentSpreadNode::class,
self::INLINE_FRAGMENT => InlineFragmentNode::class,
self::FRAGMENT_DEFINITION => FragmentDefinitionNode::class,
// Values
self::INT => IntValueNode::class,
self::FLOAT => FloatValueNode::class,
self::STRING => StringValueNode::class,
self::BOOLEAN => BooleanValueNode::class,
self::ENUM => EnumValueNode::class,
self::NULL => NullValueNode::class,
self::LST => ListValueNode::class,
self::OBJECT => ObjectValueNode::class,
self::OBJECT_FIELD => ObjectFieldNode::class,
// Directives
self::DIRECTIVE => DirectiveNode::class,
// Types
self::NAMED_TYPE => NamedTypeNode::class,
self::LIST_TYPE => ListTypeNode::class,
self::NON_NULL_TYPE => NonNullTypeNode::class,
// Type System Definitions
self::SCHEMA_DEFINITION => SchemaDefinitionNode::class,
self::OPERATION_TYPE_DEFINITION => OperationTypeDefinitionNode::class,
// Type Definitions
self::SCALAR_TYPE_DEFINITION => ScalarTypeDefinitionNode::class,
self::OBJECT_TYPE_DEFINITION => ObjectTypeDefinitionNode::class,
self::FIELD_DEFINITION => FieldDefinitionNode::class,
self::INPUT_VALUE_DEFINITION => InputValueDefinitionNode::class,
self::INTERFACE_TYPE_DEFINITION => InterfaceTypeDefinitionNode::class,
self::UNION_TYPE_DEFINITION => UnionTypeDefinitionNode::class,
self::ENUM_TYPE_DEFINITION => EnumTypeDefinitionNode::class,
self::ENUM_VALUE_DEFINITION => EnumValueDefinitionNode::class,
self::INPUT_OBJECT_TYPE_DEFINITION => InputObjectTypeDefinitionNode::class,
// Type Extensions
self::SCALAR_TYPE_EXTENSION => ScalarTypeExtensionNode::class,
self::OBJECT_TYPE_EXTENSION => ObjectTypeExtensionNode::class,
self::INTERFACE_TYPE_EXTENSION => InterfaceTypeExtensionNode::class,
self::UNION_TYPE_EXTENSION => UnionTypeExtensionNode::class,
self::ENUM_TYPE_EXTENSION => EnumTypeExtensionNode::class,
self::INPUT_OBJECT_TYPE_EXTENSION => InputObjectTypeExtensionNode::class,
// Directive Definitions
self::DIRECTIVE_DEFINITION => DirectiveDefinitionNode::class,
];
}

View File

@@ -1,161 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\AST;
/**
* @template T of Node
*
* @phpstan-implements \ArrayAccess<array-key, T>
* @phpstan-implements \IteratorAggregate<array-key, T>
*/
class NodeList implements \ArrayAccess, \IteratorAggregate, \Countable
{
/**
* @var array<Node|array>
*
* @phpstan-var array<T|array<string, mixed>>
*/
private array $nodes;
/**
* @param array<Node|array> $nodes
*
* @phpstan-param array<T|array<string, mixed>> $nodes
*/
public function __construct(array $nodes)
{
$this->nodes = $nodes;
}
/** @param int|string $offset */
#[\ReturnTypeWillChange]
public function offsetExists($offset): bool
{
return isset($this->nodes[$offset]);
}
/**
* @param int|string $offset
*
* @phpstan-return T
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset): Node
{
$item = $this->nodes[$offset];
if (is_array($item)) {
// @phpstan-ignore-next-line not really possible to express the correctness of this in PHP
return $this->nodes[$offset] = AST::fromArray($item);
}
return $item;
}
/**
* @param int|string|null $offset
* @param Node|array<string, mixed> $value
*
* @phpstan-param T|array<string, mixed> $value
*
* @throws \JsonException
* @throws InvariantViolation
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value): void
{
if (is_array($value)) {
/** @phpstan-var T $value */
$value = AST::fromArray($value);
}
// Happens when a Node is pushed via []=
if ($offset === null) {
$this->nodes[] = $value;
return;
}
$this->nodes[$offset] = $value;
}
/** @param int|string $offset */
#[\ReturnTypeWillChange]
public function offsetUnset($offset): void
{
unset($this->nodes[$offset]);
}
public function getIterator(): \Traversable
{
foreach ($this->nodes as $key => $_) {
yield $key => $this->offsetGet($key);
}
}
public function count(): int
{
return count($this->nodes);
}
/**
* Remove a portion of the NodeList and replace it with something else.
*
* @param T|iterable<T>|null $replacement
*
* @phpstan-return NodeList<T> the NodeList with the extracted elements
*/
public function splice(int $offset, int $length, $replacement = null): NodeList
{
if (is_iterable($replacement) && ! is_array($replacement)) {
$replacement = iterator_to_array($replacement);
}
return new NodeList(
array_splice($this->nodes, $offset, $length, $replacement)
);
}
/**
* @phpstan-param iterable<array-key, T> $list
*
* @phpstan-return NodeList<T>
*/
public function merge(iterable $list): NodeList
{
if (! is_array($list)) {
$list = iterator_to_array($list);
}
return new NodeList(array_merge($this->nodes, $list));
}
/** Resets the keys of the stored nodes to contiguous numeric indexes. */
public function reindex(): void
{
$this->nodes = array_values($this->nodes);
}
/**
* Returns a clone of this instance and all its children, except Location $loc.
*
* @throws \JsonException
* @throws InvariantViolation
*
* @return static<T>
*/
public function cloneDeep(): self
{
/** @var array<T> $empty */
$empty = [];
$cloned = new static($empty);
foreach ($this->getIterator() as $key => $node) {
$cloned[$key] = $node->cloneDeep();
}
return $cloned;
}
}

View File

@@ -1,11 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class NonNullTypeNode extends Node implements TypeNode
{
public string $kind = NodeKind::NON_NULL_TYPE;
/** @var NamedTypeNode|ListTypeNode */
public TypeNode $type;
}

View File

@@ -1,8 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class NullValueNode extends Node implements ValueNode
{
public string $kind = NodeKind::NULL;
}

View File

@@ -1,13 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class ObjectFieldNode extends Node
{
public string $kind = NodeKind::OBJECT_FIELD;
public NameNode $name;
/** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode */
public ValueNode $value;
}

View File

@@ -1,26 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class ObjectTypeDefinitionNode extends Node implements TypeDefinitionNode
{
public string $kind = NodeKind::OBJECT_TYPE_DEFINITION;
public NameNode $name;
/** @var NodeList<NamedTypeNode> */
public NodeList $interfaces;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
/** @var NodeList<FieldDefinitionNode> */
public NodeList $fields;
public ?StringValueNode $description = null;
public function getName(): NameNode
{
return $this->name;
}
}

View File

@@ -1,24 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class ObjectTypeExtensionNode extends Node implements TypeExtensionNode
{
public string $kind = NodeKind::OBJECT_TYPE_EXTENSION;
public NameNode $name;
/** @var NodeList<NamedTypeNode> */
public NodeList $interfaces;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
/** @var NodeList<FieldDefinitionNode> */
public NodeList $fields;
public function getName(): NameNode
{
return $this->name;
}
}

View File

@@ -1,11 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class ObjectValueNode extends Node implements ValueNode
{
public string $kind = NodeKind::OBJECT;
/** @var NodeList<ObjectFieldNode> */
public NodeList $fields;
}

View File

@@ -1,36 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
/**
* @phpstan-type OperationType 'query'|'mutation'|'subscription'
*/
class OperationDefinitionNode extends Node implements ExecutableDefinitionNode, HasSelectionSet
{
public string $kind = NodeKind::OPERATION_DEFINITION;
public ?NameNode $name = null;
/** @var OperationType */
public string $operation;
/** @var NodeList<VariableDefinitionNode> */
public NodeList $variableDefinitions;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
public SelectionSetNode $selectionSet;
public function __construct(array $vars)
{
parent::__construct($vars);
$this->directives ??= new NodeList([]);
$this->variableDefinitions ??= new NodeList([]);
}
public function getSelectionSet(): SelectionSetNode
{
return $this->selectionSet;
}
}

View File

@@ -1,16 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
/**
* @phpstan-import-type OperationType from OperationDefinitionNode
*/
class OperationTypeDefinitionNode extends Node
{
public string $kind = NodeKind::OPERATION_TYPE_DEFINITION;
/** @var OperationType */
public string $operation;
public NamedTypeNode $type;
}

View File

@@ -1,20 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class ScalarTypeDefinitionNode extends Node implements TypeDefinitionNode
{
public string $kind = NodeKind::SCALAR_TYPE_DEFINITION;
public NameNode $name;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
public ?StringValueNode $description = null;
public function getName(): NameNode
{
return $this->name;
}
}

View File

@@ -1,18 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class ScalarTypeExtensionNode extends Node implements TypeExtensionNode
{
public string $kind = NodeKind::SCALAR_TYPE_EXTENSION;
public NameNode $name;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
public function getName(): NameNode
{
return $this->name;
}
}

View File

@@ -1,14 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class SchemaDefinitionNode extends Node implements TypeSystemDefinitionNode
{
public string $kind = NodeKind::SCHEMA_DEFINITION;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
/** @var NodeList<OperationTypeDefinitionNode> */
public NodeList $operationTypes;
}

View File

@@ -1,14 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class SchemaExtensionNode extends Node implements TypeSystemExtensionNode
{
public string $kind = NodeKind::SCHEMA_EXTENSION;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
/** @var NodeList<OperationTypeDefinitionNode> */
public NodeList $operationTypes;
}

View File

@@ -1,8 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
/**
* export type SelectionNode = FieldNode | FragmentSpreadNode | InlineFragmentNode.
*/
interface SelectionNode {}

View File

@@ -1,11 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class SelectionSetNode extends Node
{
public string $kind = NodeKind::SELECTION_SET;
/** @var NodeList<SelectionNode&Node> */
public NodeList $selections;
}

View File

@@ -1,12 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class StringValueNode extends Node implements ValueNode
{
public string $kind = NodeKind::STRING;
public string $value;
public bool $block = false;
}

View File

@@ -1,16 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
/**
* export type TypeDefinitionNode = ScalarTypeDefinitionNode
* | ObjectTypeDefinitionNode
* | InterfaceTypeDefinitionNode
* | UnionTypeDefinitionNode
* | EnumTypeDefinitionNode
* | InputObjectTypeDefinitionNode.
*/
interface TypeDefinitionNode extends TypeSystemDefinitionNode
{
public function getName(): NameNode;
}

View File

@@ -1,17 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
/**
* export type TypeExtensionNode =
* | ScalarTypeExtensionNode
* | ObjectTypeExtensionNode
* | InterfaceTypeExtensionNode
* | UnionTypeExtensionNode
* | EnumTypeExtensionNode
* | InputObjectTypeExtensionNode;.
*/
interface TypeExtensionNode extends TypeSystemExtensionNode
{
public function getName(): NameNode;
}

View File

@@ -1,10 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
/**
* export type TypeNode = NamedTypeNode
* | ListTypeNode
* | NonNullTypeNode.
*/
interface TypeNode {}

View File

@@ -1,11 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
/**
* export type TypeSystemDefinitionNode =
* | SchemaDefinitionNode
* | TypeDefinitionNode
* | DirectiveDefinitionNode.
*/
interface TypeSystemDefinitionNode extends DefinitionNode {}

View File

@@ -1,8 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
/**
* export type TypeSystemExtensionNode = SchemaExtensionNode | TypeExtensionNode;.
*/
interface TypeSystemExtensionNode extends DefinitionNode {}

View File

@@ -1,23 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class UnionTypeDefinitionNode extends Node implements TypeDefinitionNode
{
public string $kind = NodeKind::UNION_TYPE_DEFINITION;
public NameNode $name;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
/** @var NodeList<NamedTypeNode> */
public NodeList $types;
public ?StringValueNode $description = null;
public function getName(): NameNode
{
return $this->name;
}
}

View File

@@ -1,21 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class UnionTypeExtensionNode extends Node implements TypeExtensionNode
{
public string $kind = NodeKind::UNION_TYPE_EXTENSION;
public NameNode $name;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
/** @var NodeList<NamedTypeNode> */
public NodeList $types;
public function getName(): NameNode
{
return $this->name;
}
}

View File

@@ -1,16 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
/**
* export type ValueNode = VariableNode
* | NullValueNode
* | IntValueNode
* | FloatValueNode
* | StringValueNode
* | BooleanValueNode
* | EnumValueNode
* | ListValueNode
* | ObjectValueNode.
*/
interface ValueNode {}

View File

@@ -1,25 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class VariableDefinitionNode extends Node implements DefinitionNode
{
public string $kind = NodeKind::VARIABLE_DEFINITION;
public VariableNode $variable;
/** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */
public TypeNode $type;
/** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null */
public ?ValueNode $defaultValue = null;
/** @var NodeList<DirectiveNode> */
public NodeList $directives;
public function __construct(array $vars)
{
parent::__construct($vars);
$this->directives ??= new NodeList([]);
}
}

View File

@@ -1,10 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language\AST;
class VariableNode extends Node implements ValueNode
{
public string $kind = NodeKind::VARIABLE;
public NameNode $name;
}

View File

@@ -1,155 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language;
use GraphQL\Utils\Utils;
/**
* @see \GraphQL\Tests\Language\BlockStringTest
*/
class BlockString
{
/**
* Produces the value of a block string from its parsed raw value, similar to
* CoffeeScript's block string, Python's docstring trim or Ruby's strip_heredoc.
*
* This implements the GraphQL spec's BlockStringValue() static algorithm.
*/
public static function dedentBlockStringLines(string $rawString): string
{
$lines = Utils::splitLines($rawString);
// Remove common indentation from all lines but first.
$commonIndent = self::getIndentation($rawString);
$linesLength = count($lines);
if ($commonIndent > 0) {
for ($i = 1; $i < $linesLength; ++$i) {
$lines[$i] = mb_substr($lines[$i], $commonIndent);
}
}
// Remove leading and trailing blank lines.
$startLine = 0;
while ($startLine < $linesLength && self::isBlank($lines[$startLine])) {
++$startLine;
}
$endLine = $linesLength;
while ($endLine > $startLine && self::isBlank($lines[$endLine - 1])) {
--$endLine;
}
// Return a string of the lines joined with U+000A.
return implode("\n", array_slice($lines, $startLine, $endLine - $startLine));
}
private static function isBlank(string $str): bool
{
$strLength = mb_strlen($str);
for ($i = 0; $i < $strLength; ++$i) {
if ($str[$i] !== ' ' && $str[$i] !== '\t') {
return false;
}
}
return true;
}
public static function getIndentation(string $value): int
{
$isFirstLine = true;
$isEmptyLine = true;
$indent = 0;
$commonIndent = null;
$valueLength = mb_strlen($value);
for ($i = 0; $i < $valueLength; ++$i) {
switch (Utils::charCodeAt($value, $i)) {
case 13: // \r
if (Utils::charCodeAt($value, $i + 1) === 10) {
++$i; // skip \r\n as one symbol
}
// falls through
// no break
case 10: // \n
$isFirstLine = false;
$isEmptyLine = true;
$indent = 0;
break;
case 9: // \t
case 32: // <space>
++$indent;
break;
default:
if (
$isEmptyLine
&& ! $isFirstLine
&& ($commonIndent === null || $indent < $commonIndent)
) {
$commonIndent = $indent;
}
$isEmptyLine = false;
}
}
return $commonIndent ?? 0;
}
/**
* Print a block string in the indented block form by adding a leading and
* trailing blank line. However, if a block string starts with whitespace and is
* a single-line, adding a leading blank line would strip that whitespace.
*/
public static function print(string $value): string
{
$escapedValue = str_replace('"""', '\\"""', $value);
// Expand a block string's raw value into independent lines.
$lines = Utils::splitLines($escapedValue);
$isSingleLine = count($lines) === 1;
// If common indentation is found we can fix some of those cases by adding leading new line
$forceLeadingNewLine = count($lines) > 1;
foreach ($lines as $i => $line) {
if ($i === 0) {
continue;
}
if ($line !== '' && preg_match('/^\s/', $line) !== 1) {
$forceLeadingNewLine = false;
}
}
// Trailing triple quotes just looks confusing but doesn't force trailing new line
$hasTrailingTripleQuotes = preg_match('/\\\\"""$/', $escapedValue) === 1;
// Trailing quote (single or double) or slash forces trailing new line
$hasTrailingQuote = preg_match('/"$/', $value) === 1 && ! $hasTrailingTripleQuotes;
$hasTrailingSlash = preg_match('/\\\\$/', $value) === 1;
$forceTrailingNewline = $hasTrailingQuote || $hasTrailingSlash;
// add leading and trailing new lines only if it improves readability
$printAsMultipleLines = ! $isSingleLine
|| mb_strlen($value) > 70
|| $forceTrailingNewline
|| $forceLeadingNewLine
|| $hasTrailingTripleQuotes;
$result = '';
// Format a multi-line block quote to account for leading space.
$skipLeadingNewLine = $isSingleLine && preg_match('/^\s/', $value) === 1;
if (($printAsMultipleLines && ! $skipLeadingNewLine) || $forceLeadingNewLine) {
$result .= "\n";
}
$result .= $escapedValue;
if ($printAsMultipleLines) {
$result .= "\n";
}
return '"""' . $result . '"""';
}
}

View File

@@ -1,62 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language;
/**
* Enumeration of available directive locations.
*/
class DirectiveLocation
{
public const QUERY = 'QUERY';
public const MUTATION = 'MUTATION';
public const SUBSCRIPTION = 'SUBSCRIPTION';
public const FIELD = 'FIELD';
public const FRAGMENT_DEFINITION = 'FRAGMENT_DEFINITION';
public const FRAGMENT_SPREAD = 'FRAGMENT_SPREAD';
public const INLINE_FRAGMENT = 'INLINE_FRAGMENT';
public const VARIABLE_DEFINITION = 'VARIABLE_DEFINITION';
public const EXECUTABLE_LOCATIONS = [
self::QUERY => self::QUERY,
self::MUTATION => self::MUTATION,
self::SUBSCRIPTION => self::SUBSCRIPTION,
self::FIELD => self::FIELD,
self::FRAGMENT_DEFINITION => self::FRAGMENT_DEFINITION,
self::FRAGMENT_SPREAD => self::FRAGMENT_SPREAD,
self::INLINE_FRAGMENT => self::INLINE_FRAGMENT,
self::VARIABLE_DEFINITION => self::VARIABLE_DEFINITION,
];
public const SCHEMA = 'SCHEMA';
public const SCALAR = 'SCALAR';
public const OBJECT = 'OBJECT';
public const FIELD_DEFINITION = 'FIELD_DEFINITION';
public const ARGUMENT_DEFINITION = 'ARGUMENT_DEFINITION';
public const IFACE = 'INTERFACE';
public const UNION = 'UNION';
public const ENUM = 'ENUM';
public const ENUM_VALUE = 'ENUM_VALUE';
public const INPUT_OBJECT = 'INPUT_OBJECT';
public const INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION';
public const TYPE_SYSTEM_LOCATIONS = [
self::SCHEMA => self::SCHEMA,
self::SCALAR => self::SCALAR,
self::OBJECT => self::OBJECT,
self::FIELD_DEFINITION => self::FIELD_DEFINITION,
self::ARGUMENT_DEFINITION => self::ARGUMENT_DEFINITION,
self::IFACE => self::IFACE,
self::UNION => self::UNION,
self::ENUM => self::ENUM,
self::ENUM_VALUE => self::ENUM_VALUE,
self::INPUT_OBJECT => self::INPUT_OBJECT,
self::INPUT_FIELD_DEFINITION => self::INPUT_FIELD_DEFINITION,
];
public const LOCATIONS = self::EXECUTABLE_LOCATIONS + self::TYPE_SYSTEM_LOCATIONS;
public static function has(string $name): bool
{
return isset(self::LOCATIONS[$name]);
}
}

View File

@@ -1,756 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language;
use GraphQL\Error\SyntaxError;
use GraphQL\Utils\Utils;
/**
* A lexer is a stateful stream generator, it returns the next token in the Source when advanced.
* Assuming the source is valid, the final returned token will be EOF,
* after which the lexer will repeatedly return the same EOF token whenever called.
*
* Algorithm is O(N) both on memory and time.
*
* @phpstan-import-type ParserOptions from Parser
*
* @see \GraphQL\Tests\Language\LexerTest
*/
class Lexer
{
// https://spec.graphql.org/October2021/#sec-Punctuators
private const TOKEN_BANG = 33;
private const TOKEN_DOLLAR = 36;
private const TOKEN_AMP = 38;
private const TOKEN_PAREN_L = 40;
private const TOKEN_PAREN_R = 41;
private const TOKEN_DOT = 46;
private const TOKEN_COLON = 58;
private const TOKEN_EQUALS = 61;
private const TOKEN_AT = 64;
private const TOKEN_BRACKET_L = 91;
private const TOKEN_BRACKET_R = 93;
private const TOKEN_BRACE_L = 123;
private const TOKEN_PIPE = 124;
private const TOKEN_BRACE_R = 125;
public Source $source;
/** @phpstan-var ParserOptions */
public array $options;
/** The previously focused non-ignored token. */
public Token $lastToken;
/** The currently focused non-ignored token. */
public Token $token;
/** The (1-indexed) line containing the current token. */
public int $line = 1;
/** The character offset at which the current line begins. */
public int $lineStart = 0;
/** Current cursor position for UTF8 encoding of the source. */
private int $position = 0;
/** Current cursor position for ASCII representation of the source. */
private int $byteStreamPosition = 0;
/** @phpstan-param ParserOptions $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;
}
/**
* @throws \JsonException
* @throws SyntaxError
*/
public function advance(): Token
{
$this->lastToken = $this->token;
return $this->token = $this->lookahead();
}
/**
* @throws \JsonException
* @throws SyntaxError
*/
public function lookahead(): Token
{
$token = $this->token;
if ($token->kind !== Token::EOF) {
do {
$token = $token->next ?? ($token->next = $this->readToken($token));
} while ($token->kind === Token::COMMENT);
}
return $token;
}
/**
* @throws \JsonException
* @throws SyntaxError
*/
private function readToken(Token $prev): Token
{
$bodyLength = $this->source->length;
$this->positionAfterWhitespace();
$position = $this->position;
$line = $this->line;
$col = 1 + $position - $this->lineStart;
if ($position >= $bodyLength) {
return new Token(Token::EOF, $bodyLength, $bodyLength, $line, $col, $prev);
}
// Read next char and advance string cursor:
[, $code, $bytes] = $this->readChar(true);
switch ($code) {
case self::TOKEN_BANG: // !
return new Token(Token::BANG, $position, $position + 1, $line, $col, $prev);
case 35: // #
$this->moveStringCursor(-1, -1 * $bytes);
return $this->readComment($line, $col, $prev);
case self::TOKEN_DOLLAR: // $
return new Token(Token::DOLLAR, $position, $position + 1, $line, $col, $prev);
case self::TOKEN_AMP: // &
return new Token(Token::AMP, $position, $position + 1, $line, $col, $prev);
case self::TOKEN_PAREN_L: // (
return new Token(Token::PAREN_L, $position, $position + 1, $line, $col, $prev);
case self::TOKEN_PAREN_R: // )
return new Token(Token::PAREN_R, $position, $position + 1, $line, $col, $prev);
case self::TOKEN_DOT: // .
[, $charCode1] = $this->readChar(true);
[, $charCode2] = $this->readChar(true);
if ($charCode1 === self::TOKEN_DOT && $charCode2 === self::TOKEN_DOT) {
return new Token(Token::SPREAD, $position, $position + 3, $line, $col, $prev);
}
break;
case self::TOKEN_COLON: // :
return new Token(Token::COLON, $position, $position + 1, $line, $col, $prev);
case self::TOKEN_EQUALS: // =
return new Token(Token::EQUALS, $position, $position + 1, $line, $col, $prev);
case self::TOKEN_AT: // @
return new Token(Token::AT, $position, $position + 1, $line, $col, $prev);
case self::TOKEN_BRACKET_L: // [
return new Token(Token::BRACKET_L, $position, $position + 1, $line, $col, $prev);
case self::TOKEN_BRACKET_R: // ]
return new Token(Token::BRACKET_R, $position, $position + 1, $line, $col, $prev);
case self::TOKEN_BRACE_L: // {
return new Token(Token::BRACE_L, $position, $position + 1, $line, $col, $prev);
case self::TOKEN_PIPE: // |
return new Token(Token::PIPE, $position, $position + 1, $line, $col, $prev);
case self::TOKEN_BRACE_R: // }
return new Token(Token::BRACE_R, $position, $position + 1, $line, $col, $prev);
// A-Z
case 65:
case 66:
case 67:
case 68:
case 69:
case 70:
case 71:
case 72:
case 73:
case 74:
case 75:
case 76:
case 77:
case 78:
case 79:
case 80:
case 81:
case 82:
case 83:
case 84:
case 85:
case 86:
case 87:
case 88:
case 89:
case 90:
// _
case 95:
// a-z
case 97:
case 98:
case 99:
case 100:
case 101:
case 102:
case 103:
case 104:
case 105:
case 106:
case 107:
case 108:
case 109:
case 110:
case 111:
case 112:
case 113:
case 114:
case 115:
case 116:
case 117:
case 118:
case 119:
case 120:
case 121:
case 122:
return $this->moveStringCursor(-1, -1 * $bytes)
->readName($line, $col, $prev);
// -
case 45:
// 0-9
case 48:
case 49:
case 50:
case 51:
case 52:
case 53:
case 54:
case 55:
case 56:
case 57:
return $this->moveStringCursor(-1, -1 * $bytes)
->readNumber($line, $col, $prev);
// "
case 34:
[, $nextCode] = $this->readChar();
[, $nextNextCode] = $this->moveStringCursor(1, 1)
->readChar();
if ($nextCode === 34 && $nextNextCode === 34) {
return $this->moveStringCursor(-2, (-1 * $bytes) - 1)
->readBlockString($line, $col, $prev);
}
return $this->moveStringCursor(-2, (-1 * $bytes) - 1)
->readString($line, $col, $prev);
}
throw new SyntaxError($this->source, $position, $this->unexpectedCharacterMessage($code));
}
/** @throws \JsonException */
private function unexpectedCharacterMessage(?int $code): string
{
// SourceCharacter
if ($code < 0x0020 && $code !== 0x0009 && $code !== 0x000A && $code !== 0x000D) {
return 'Cannot contain the invalid character ' . Utils::printCharCode($code);
}
if ($code === 39) {
return 'Unexpected single quote character (\'), did you mean to use a double quote (")?';
}
return 'Cannot parse the unexpected character ' . Utils::printCharCode($code) . '.';
}
/**
* Reads an alphanumeric + underscore name from the source.
*
* [_A-Za-z][_0-9A-Za-z]*
*/
private function readName(int $line, int $col, Token $prev): Token
{
$value = '';
$start = $this->position;
[$char, $code] = $this->readChar();
while (
$code !== null && (
$code === 95 // _
|| ($code >= 48 && $code <= 57) // 0-9
|| ($code >= 65 && $code <= 90) // A-Z
|| ($code >= 97 && $code <= 122) // a-z
)
) {
$value .= $char;
[$char, $code] = $this->moveStringCursor(1, 1)->readChar();
}
return new Token(
Token::NAME,
$start,
$this->position,
$line,
$col,
$prev,
$value
);
}
/**
* Reads a number token from the source file, either a float
* or an int depending on whether a decimal point appears.
*
* Int: -?(0|[1-9][0-9]*)
* Float: -?(0|[1-9][0-9]*)(\.[0-9]+)?((E|e)(+|-)?[0-9]+)?
*
* @throws \JsonException
* @throws SyntaxError
*/
private function readNumber(int $line, int $col, Token $prev): Token
{
$value = '';
$start = $this->position;
[$char, $code] = $this->readChar();
$isFloat = false;
if ($code === 45) { // -
$value .= $char;
[$char, $code] = $this->moveStringCursor(1, 1)->readChar();
}
// guard against leading zero's
if ($code === 48) { // 0
$value .= $char;
[$char, $code] = $this->moveStringCursor(1, 1)->readChar();
if ($code >= 48 && $code <= 57) {
throw new SyntaxError($this->source, $this->position, 'Invalid number, unexpected digit after 0: ' . Utils::printCharCode($code));
}
} else {
$value .= $this->readDigits();
[$char, $code] = $this->readChar();
}
if ($code === 46) { // .
$isFloat = true;
$this->moveStringCursor(1, 1);
$value .= $char;
$value .= $this->readDigits();
[$char, $code] = $this->readChar();
}
if ($code === 69 || $code === 101) { // E e
$isFloat = true;
$value .= $char;
[$char, $code] = $this->moveStringCursor(1, 1)->readChar();
if ($code === 43 || $code === 45) { // + -
$value .= $char;
$this->moveStringCursor(1, 1);
}
$value .= $this->readDigits();
}
return new Token(
$isFloat ? Token::FLOAT : Token::INT,
$start,
$this->position,
$line,
$col,
$prev,
$value
);
}
/**
* Returns string with all digits + changes current string cursor position to point to the first char after digits.
*
* @throws \JsonException
* @throws SyntaxError
*/
private function readDigits(): string
{
[$char, $code] = $this->readChar();
if ($code >= 48 && $code <= 57) { // 0 - 9
$value = '';
do {
$value .= $char;
[$char, $code] = $this->moveStringCursor(1, 1)->readChar();
} while ($code >= 48 && $code <= 57); // 0 - 9
return $value;
}
if ($this->position > $this->source->length - 1) {
$code = null;
}
throw new SyntaxError($this->source, $this->position, 'Invalid number, expected digit but got: ' . Utils::printCharCode($code));
}
/**
* @throws \JsonException
* @throws SyntaxError
*/
private function readString(int $line, int $col, Token $prev): Token
{
$start = $this->position;
// Skip leading quote and read first string char:
[$char, $code, $bytes] = $this->moveStringCursor(1, 1)
->readChar();
$chunk = '';
$value = '';
while (
$code !== null
&& $code !== 10 && $code !== 13 // not LineTerminator
) {
if ($code === 34) { // Closing Quote (")
$value .= $chunk;
// Skip quote
$this->moveStringCursor(1, 1);
return new Token(
Token::STRING,
$start,
$this->position,
$line,
$col,
$prev,
$value
);
}
$this->assertValidStringCharacterCode($code, $this->position);
$this->moveStringCursor(1, $bytes);
if ($code === 92) { // \
$value .= $chunk;
[, $code] = $this->readChar(true);
switch ($code) {
case 34:
$value .= '"';
break;
case 47:
$value .= '/';
break;
case 92:
$value .= '\\';
break;
case 98:
$value .= chr(8); // \b (backspace)
break;
case 102:
$value .= "\f";
break;
case 110:
$value .= "\n";
break;
case 114:
$value .= "\r";
break;
case 116:
$value .= "\t";
break;
case 117:
$position = $this->position;
[$hex] = $this->readChars(4);
if (preg_match('/[0-9a-fA-F]{4}/', $hex) !== 1) {
throw new SyntaxError($this->source, $position - 1, "Invalid character escape sequence: \\u{$hex}");
}
$code = hexdec($hex);
assert(is_int($code), 'Since only a single char is read');
// UTF-16 surrogate pair detection and handling.
$highOrderByte = $code >> 8;
if ($highOrderByte >= 0xD8 && $highOrderByte <= 0xDF) {
[$utf16Continuation] = $this->readChars(6);
if (preg_match('/^\\\u[0-9a-fA-F]{4}$/', $utf16Continuation) !== 1) {
throw new SyntaxError($this->source, $this->position - 5, 'Invalid UTF-16 trailing surrogate: ' . $utf16Continuation);
}
$surrogatePairHex = $hex . substr($utf16Continuation, 2, 4);
$value .= mb_convert_encoding(pack('H*', $surrogatePairHex), 'UTF-8', 'UTF-16');
break;
}
$this->assertValidStringCharacterCode($code, $position - 2);
$value .= Utils::chr($code);
break;
// null means EOF, will delegate to general handling of unterminated strings
case null:
continue 2;
default:
$chr = Utils::chr($code);
throw new SyntaxError($this->source, $this->position - 1, "Invalid character escape sequence: \\{$chr}");
}
$chunk = '';
} else {
$chunk .= $char;
}
[$char, $code, $bytes] = $this->readChar();
}
throw new SyntaxError($this->source, $this->position, 'Unterminated string.');
}
/**
* Reads a block string token from the source file.
*
* """("?"?(\\"""|\\(?!=""")|[^"\\]))*"""
*
* @throws \JsonException
* @throws SyntaxError
*/
private function readBlockString(int $line, int $col, Token $prev): Token
{
$start = $this->position;
// Skip leading quotes and read first string char:
[$char, $code, $bytes] = $this->moveStringCursor(3, 3)->readChar();
$chunk = '';
$value = '';
while ($code !== null) {
// Closing Triple-Quote (""")
if ($code === 34) {
// Move 2 quotes
[, $nextCode] = $this->moveStringCursor(1, 1)->readChar();
[, $nextNextCode] = $this->moveStringCursor(1, 1)->readChar();
if ($nextCode === 34 && $nextNextCode === 34) {
$value .= $chunk;
$this->moveStringCursor(1, 1);
return new Token(
Token::BLOCK_STRING,
$start,
$this->position,
$line,
$col,
$prev,
BlockString::dedentBlockStringLines($value)
);
}
// move cursor back to before the first quote
$this->moveStringCursor(-2, -2);
}
$this->assertValidBlockStringCharacterCode($code, $this->position);
$this->moveStringCursor(1, $bytes);
[, $nextCode] = $this->readChar();
[, $nextNextCode] = $this->moveStringCursor(1, 1)->readChar();
[, $nextNextNextCode] = $this->moveStringCursor(1, 1)->readChar();
// Escape Triple-Quote (\""")
if (
$code === 92
&& $nextCode === 34
&& $nextNextCode === 34
&& $nextNextNextCode === 34
) {
$this->moveStringCursor(1, 1);
$value .= $chunk . '"""';
$chunk = '';
} else {
// move cursor back to before the first quote
$this->moveStringCursor(-2, -2);
if ($code === 10) { // new line
++$this->line;
$this->lineStart = $this->position;
}
$chunk .= $char;
}
[$char, $code, $bytes] = $this->readChar();
}
throw new SyntaxError($this->source, $this->position, 'Unterminated string.');
}
/**
* @throws \JsonException
* @throws SyntaxError
*/
private function assertValidStringCharacterCode(int $code, int $position): void
{
// SourceCharacter
if ($code < 0x0020 && $code !== 0x0009) {
$char = Utils::printCharCode($code);
throw new SyntaxError($this->source, $position, "Invalid character within String: {$char}");
}
}
/**
* @throws \JsonException
* @throws SyntaxError
*/
private function assertValidBlockStringCharacterCode(int $code, int $position): void
{
// SourceCharacter
if ($code < 0x0020 && $code !== 0x0009 && $code !== 0x000A && $code !== 0x000D) {
$char = Utils::printCharCode($code);
throw new SyntaxError($this->source, $position, "Invalid character within String: {$char}");
}
}
/**
* 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(): void
{
while ($this->position < $this->source->length) {
[, $code, $bytes] = $this->readChar();
// Skip whitespace
// tab | space | comma | BOM
if (in_array($code, [9, 32, 44, 0xFEFF], true)) {
$this->moveStringCursor(1, $bytes);
} elseif ($code === 10) { // new line
$this->moveStringCursor(1, $bytes);
++$this->line;
$this->lineStart = $this->position;
} elseif ($code === 13) { // carriage return
[, $nextCode, $nextBytes] = $this->moveStringCursor(1, $bytes)->readChar();
if ($nextCode === 10) { // lf after cr
$this->moveStringCursor(1, $nextBytes);
}
++$this->line;
$this->lineStart = $this->position;
} else {
break;
}
}
}
/**
* Reads a comment token from the source file.
*
* #[\u0009\u0020-\uFFFF]*
*/
private function readComment(int $line, int $col, Token $prev): Token
{
$start = $this->position;
$value = '';
$bytes = 1;
do {
[$char, $code, $bytes] = $this->moveStringCursor(1, $bytes)->readChar();
$value .= $char;
} while (
$code !== null
// 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.
*
* @return array{string, int|null, int}
*/
private function readChar(bool $advance = false, ?int $byteStreamPosition = null): array
{
if ($byteStreamPosition === null) {
$byteStreamPosition = $this->byteStreamPosition;
}
$code = null;
$utf8char = '';
$bytes = 0;
$positionOffset = 0;
if (isset($this->source->body[$byteStreamPosition])) {
$ord = ord($this->source->body[$byteStreamPosition]);
if ($ord < 128) {
$bytes = 1;
} elseif ($ord < 224) {
$bytes = 2;
} elseif ($ord < 240) {
$bytes = 3;
} else {
$bytes = 4;
}
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.
*
* @return array{string, int}
*/
private function readChars(int $charCount): array
{
$result = '';
$totalBytes = 0;
$byteOffset = $this->byteStreamPosition;
for ($i = 0; $i < $charCount; ++$i) {
[$char, $code, $bytes] = $this->readChar(false, $byteOffset);
$totalBytes += $bytes;
$byteOffset += $bytes;
$result .= $char;
}
$this->moveStringCursor($charCount, $totalBytes);
return [$result, $totalBytes];
}
/** Moves internal string cursor position. */
private function moveStringCursor(int $positionOffset, int $byteStreamOffset): self
{
$this->position += $positionOffset;
$this->byteStreamPosition += $byteStreamOffset;
return $this;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,525 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\BooleanValueNode;
use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\EnumTypeDefinitionNode;
use GraphQL\Language\AST\EnumTypeExtensionNode;
use GraphQL\Language\AST\EnumValueDefinitionNode;
use GraphQL\Language\AST\EnumValueNode;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FloatValueNode;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeExtensionNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
use GraphQL\Language\AST\IntValueNode;
use GraphQL\Language\AST\ListTypeNode;
use GraphQL\Language\AST\ListValueNode;
use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\NonNullTypeNode;
use GraphQL\Language\AST\NullValueNode;
use GraphQL\Language\AST\ObjectFieldNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\AST\ObjectTypeExtensionNode;
use GraphQL\Language\AST\ObjectValueNode;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\OperationTypeDefinitionNode;
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
use GraphQL\Language\AST\ScalarTypeExtensionNode;
use GraphQL\Language\AST\SchemaDefinitionNode;
use GraphQL\Language\AST\SchemaExtensionNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Language\AST\UnionTypeDefinitionNode;
use GraphQL\Language\AST\UnionTypeExtensionNode;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Language\AST\VariableNode;
/**
* 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);
* ```
*
* @see \GraphQL\Tests\Language\PrinterTest
*/
class Printer
{
/**
* Converts the AST of a GraphQL node to a string.
*
* Handles both executable definitions and schema definitions.
*
* @throws \JsonException
*
* @api
*/
public static function doPrint(Node $ast): string
{
return static::p($ast);
}
/** @throws \JsonException */
protected static function p(?Node $node): string
{
if ($node === null) {
return '';
}
switch (true) {
case $node instanceof ArgumentNode:
case $node instanceof ObjectFieldNode:
return static::p($node->name) . ': ' . static::p($node->value);
case $node instanceof BooleanValueNode:
return $node->value
? 'true'
: 'false';
case $node instanceof DirectiveDefinitionNode:
$argStrings = [];
foreach ($node->arguments as $arg) {
$argStrings[] = static::p($arg);
}
$noIndent = true;
foreach ($argStrings as $argString) {
if (strpos($argString, "\n") !== false) {
$noIndent = false;
break;
}
}
return static::addDescription($node->description, 'directive @'
. static::p($node->name)
. ($noIndent
? static::wrap('(', static::join($argStrings, ', '), ')')
: static::wrap("(\n", static::indent(static::join($argStrings, "\n")), "\n"))
. ($node->repeatable
? ' repeatable'
: '')
. ' on ' . static::printList($node->locations, ' | '));
case $node instanceof DirectiveNode:
return '@' . static::p($node->name) . static::wrap('(', static::printList($node->arguments, ', '), ')');
case $node instanceof DocumentNode:
return static::printList($node->definitions, "\n\n") . "\n";
case $node instanceof EnumTypeDefinitionNode:
return static::addDescription($node->description, static::join(
[
'enum',
static::p($node->name),
static::printList($node->directives, ' '),
static::printListBlock($node->values),
],
' '
));
case $node instanceof EnumTypeExtensionNode:
return static::join(
[
'extend enum',
static::p($node->name),
static::printList($node->directives, ' '),
static::printListBlock($node->values),
],
' '
);
case $node instanceof EnumValueDefinitionNode:
return static::addDescription(
$node->description,
static::join([static::p($node->name), static::printList($node->directives, ' ')], ' ')
);
case $node instanceof EnumValueNode:
case $node instanceof FloatValueNode:
case $node instanceof IntValueNode:
case $node instanceof NameNode:
return $node->value;
case $node instanceof FieldDefinitionNode:
$argStrings = [];
foreach ($node->arguments as $item) {
$argStrings[] = static::p($item);
}
$noIndent = true;
foreach ($argStrings as $argString) {
if (strpos($argString, "\n") !== false) {
$noIndent = false;
break;
}
}
return static::addDescription(
$node->description,
static::p($node->name)
. ($noIndent
? static::wrap('(', static::join($argStrings, ', '), ')')
: static::wrap("(\n", static::indent(static::join($argStrings, "\n")), "\n)"))
. ': ' . static::p($node->type)
. static::wrap(' ', static::printList($node->directives, ' '))
);
case $node instanceof FieldNode:
$prefix = static::wrap('', $node->alias->value ?? null, ': ') . static::p($node->name);
$argsLine = $prefix . static::wrap(
'(',
static::printList($node->arguments, ', '),
')'
);
if (strlen($argsLine) > 80) {
$argsLine = $prefix . static::wrap(
"(\n",
static::indent(
static::printList($node->arguments, "\n")
),
"\n)"
);
}
return static::join(
[
$argsLine,
static::printList($node->directives, ' '),
static::p($node->selectionSet),
],
' '
);
case $node instanceof FragmentDefinitionNode:
// Note: fragment variable definitions are experimental and may be changed or removed in the future.
return 'fragment ' . static::p($node->name)
. static::wrap(
'(',
static::printList($node->variableDefinitions ?? new NodeList([]), ', '),
')'
)
. ' on ' . static::p($node->typeCondition->name) . ' '
. static::wrap(
'',
static::printList($node->directives, ' '),
' '
)
. static::p($node->selectionSet);
case $node instanceof FragmentSpreadNode:
return '...'
. static::p($node->name)
. static::wrap(' ', static::printList($node->directives, ' '));
case $node instanceof InlineFragmentNode:
return static::join(
[
'...',
static::wrap('on ', static::p($node->typeCondition->name ?? null)),
static::printList($node->directives, ' '),
static::p($node->selectionSet),
],
' '
);
case $node instanceof InputObjectTypeDefinitionNode:
return static::addDescription($node->description, static::join(
[
'input',
static::p($node->name),
static::printList($node->directives, ' '),
static::printListBlock($node->fields),
],
' '
));
case $node instanceof InputObjectTypeExtensionNode:
return static::join(
[
'extend input',
static::p($node->name),
static::printList($node->directives, ' '),
static::printListBlock($node->fields),
],
' '
);
case $node instanceof InputValueDefinitionNode:
return static::addDescription($node->description, static::join(
[
static::p($node->name) . ': ' . static::p($node->type),
static::wrap('= ', static::p($node->defaultValue)),
static::printList($node->directives, ' '),
],
' '
));
case $node instanceof InterfaceTypeDefinitionNode:
return static::addDescription($node->description, static::join(
[
'interface',
static::p($node->name),
static::wrap('implements ', static::printList($node->interfaces, ' & ')),
static::printList($node->directives, ' '),
static::printListBlock($node->fields),
],
' '
));
case $node instanceof InterfaceTypeExtensionNode:
return static::join(
[
'extend interface',
static::p($node->name),
static::wrap('implements ', static::printList($node->interfaces, ' & ')),
static::printList($node->directives, ' '),
static::printListBlock($node->fields),
],
' '
);
case $node instanceof ListTypeNode:
return '[' . static::p($node->type) . ']';
case $node instanceof ListValueNode:
return '[' . static::printList($node->values, ', ') . ']';
case $node instanceof NamedTypeNode:
return static::p($node->name);
case $node instanceof NonNullTypeNode:
return static::p($node->type) . '!';
case $node instanceof NullValueNode:
return 'null';
case $node instanceof ObjectTypeDefinitionNode:
return static::addDescription($node->description, static::join(
[
'type',
static::p($node->name),
static::wrap('implements ', static::printList($node->interfaces, ' & ')),
static::printList($node->directives, ' '),
static::printListBlock($node->fields),
],
' '
));
case $node instanceof ObjectTypeExtensionNode:
return static::join(
[
'extend type',
static::p($node->name),
static::wrap('implements ', static::printList($node->interfaces, ' & ')),
static::printList($node->directives, ' '),
static::printListBlock($node->fields),
],
' '
);
case $node instanceof ObjectValueNode:
return '{ '
. static::printList($node->fields, ', ')
. ' }';
case $node instanceof OperationDefinitionNode:
$op = $node->operation;
$name = static::p($node->name);
$varDefs = static::wrap('(', static::printList($node->variableDefinitions, ', '), ')');
$directives = static::printList($node->directives, ' ');
$selectionSet = static::p($node->selectionSet);
// Anonymous queries with no directives or variable definitions can use
// the query short form.
return $name === '' && $directives === '' && $varDefs === '' && $op === 'query'
? $selectionSet
: static::join([$op, static::join([$name, $varDefs]), $directives, $selectionSet], ' ');
case $node instanceof OperationTypeDefinitionNode:
return $node->operation . ': ' . static::p($node->type);
case $node instanceof ScalarTypeDefinitionNode:
return static::addDescription($node->description, static::join([
'scalar',
static::p($node->name),
static::printList($node->directives, ' '),
], ' '));
case $node instanceof ScalarTypeExtensionNode:
return static::join(
[
'extend scalar',
static::p($node->name),
static::printList($node->directives, ' '),
],
' '
);
case $node instanceof SchemaDefinitionNode:
return static::join(
[
'schema',
static::printList($node->directives, ' '),
static::printListBlock($node->operationTypes),
],
' '
);
case $node instanceof SchemaExtensionNode:
return static::join(
[
'extend schema',
static::printList($node->directives, ' '),
static::printListBlock($node->operationTypes),
],
' '
);
case $node instanceof SelectionSetNode:
return static::printListBlock($node->selections);
case $node instanceof StringValueNode:
if ($node->block) {
return BlockString::print($node->value);
}
return json_encode($node->value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
case $node instanceof UnionTypeDefinitionNode:
$typesStr = static::printList($node->types, ' | ');
return static::addDescription($node->description, static::join(
[
'union',
static::p($node->name),
static::printList($node->directives, ' '),
$typesStr !== ''
? "= {$typesStr}"
: '',
],
' '
));
case $node instanceof UnionTypeExtensionNode:
$typesStr = static::printList($node->types, ' | ');
return static::join(
[
'extend union',
static::p($node->name),
static::printList($node->directives, ' '),
$typesStr !== ''
? "= {$typesStr}"
: '',
],
' '
);
case $node instanceof VariableDefinitionNode:
return '$' . static::p($node->variable->name)
. ': '
. static::p($node->type)
. static::wrap(' = ', static::p($node->defaultValue))
. static::wrap(' ', static::printList($node->directives, ' '));
case $node instanceof VariableNode:
return '$' . static::p($node->name);
}
return '';
}
/**
* @template TNode of Node
*
* @param NodeList<TNode> $list
*
* @throws \JsonException
*/
protected static function printList(NodeList $list, string $separator = ''): string
{
$parts = [];
foreach ($list as $item) {
$parts[] = static::p($item);
}
return static::join($parts, $separator);
}
/**
* Print each item on its own line, wrapped in an indented "{ }" block.
*
* @template TNode of Node
*
* @param NodeList<TNode> $list
*
* @throws \JsonException
*/
protected static function printListBlock(NodeList $list): string
{
if (count($list) === 0) {
return '';
}
$parts = [];
foreach ($list as $item) {
$parts[] = static::p($item);
}
return "{\n" . static::indent(static::join($parts, "\n")) . "\n}";
}
/** @throws \JsonException */
protected static function addDescription(?StringValueNode $description, string $body): string
{
return static::join([static::p($description), $body], "\n");
}
/**
* If maybeString is not null or empty, then wrap with start and end, otherwise
* print an empty string.
*/
protected static function wrap(string $start, ?string $maybeString, string $end = ''): string
{
if ($maybeString === null || $maybeString === '') {
return '';
}
return $start . $maybeString . $end;
}
protected static function indent(string $string): string
{
if ($string === '') {
return '';
}
return ' ' . str_replace("\n", "\n ", $string);
}
/** @param array<string|null> $parts */
protected static function join(array $parts, string $separator = ''): string
{
return implode($separator, array_filter($parts, static fn (?string $part) => $part !== '' && $part !== null));
}
}

View File

@@ -1,52 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language;
class Source
{
public string $body;
public int $length;
public string $name;
public SourceLocation $locationOffset;
/**
* A representation of source input to GraphQL.
*
* `name` and `locationOffset` are optional. They are useful for clients who
* store GraphQL documents in source files; for example, if the GraphQL input
* starts at line 40 in a file named Foo.graphql, it might be useful for name to
* be "Foo.graphql" and location to be `{ line: 40, column: 0 }`.
* line and column in locationOffset are 1-indexed
*/
public function __construct(string $body, ?string $name = null, ?SourceLocation $location = null)
{
$this->body = $body;
$this->length = mb_strlen($body, 'UTF-8');
$this->name = $name === '' || $name === null
? 'GraphQL request'
: $name;
$this->locationOffset = $location ?? new SourceLocation(1, 1);
}
public function getLocation(int $position): SourceLocation
{
$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 $match) {
++$line;
$column = $position + 1 - ($match[1] + mb_strlen($match[0], 'UTF-8'));
}
return new SourceLocation($line, $column);
}
}

View File

@@ -1,38 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language;
class SourceLocation implements \JsonSerializable
{
public int $line;
public int $column;
public function __construct(int $line, int $col)
{
$this->line = $line;
$this->column = $col;
}
/** @return array{line: int, column: int} */
public function toArray(): array
{
return [
'line' => $this->line,
'column' => $this->column,
];
}
/** @return array{line: int, column: int} */
public function toSerializableArray(): array
{
return $this->toArray();
}
/** @return array{line: int, column: int} */
#[\ReturnTypeWillChange]
public function jsonSerialize(): array
{
return $this->toArray();
}
}

View File

@@ -1,99 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language;
/**
* Represents a range of characters represented by a lexical token
* within a Source.
*
* @see \GraphQL\Tests\Language\TokenTest
*/
class Token
{
// Each kind of token.
public const SOF = '<SOF>';
public const EOF = '<EOF>';
public const BANG = '!';
public const DOLLAR = '$';
public const AMP = '&';
public const PAREN_L = '(';
public const PAREN_R = ')';
public const SPREAD = '...';
public const COLON = ':';
public const EQUALS = '=';
public const AT = '@';
public const BRACKET_L = '[';
public const BRACKET_R = ']';
public const BRACE_L = '{';
public const PIPE = '|';
public const BRACE_R = '}';
public const NAME = 'Name';
public const INT = 'Int';
public const FLOAT = 'Float';
public const STRING = 'String';
public const BLOCK_STRING = 'BlockString';
public const COMMENT = 'Comment';
/** The kind of Token (see one of constants above). */
public string $kind;
/** The character offset at which this Node begins. */
public int $start;
/** The character offset at which this Node ends. */
public int $end;
/** The 1-indexed line number on which this Token appears. */
public int $line;
/** The 1-indexed column number at which this Token begins. */
public int $column;
public ?string $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.
*/
public ?Token $prev;
public ?Token $next = null;
public function __construct(string $kind, int $start, int $end, int $line, int $column, ?Token $previous = null, ?string $value = null)
{
$this->kind = $kind;
$this->start = $start;
$this->end = $end;
$this->line = $line;
$this->column = $column;
$this->prev = $previous;
$this->value = $value;
}
public function getDescription(): string
{
return $this->kind
. ($this->value === null
? ''
: " \"{$this->value}\"");
}
/**
* @return array{
* kind: string,
* value: string|null,
* line: int,
* column: int,
* }
*/
public function toArray(): array
{
return [
'kind' => $this->kind,
'value' => $this->value,
'line' => $this->line,
'column' => $this->column,
];
}
}

View File

@@ -1,510 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NodeList;
use GraphQL\Utils\TypeInfo;
use GraphQL\Utils\Utils;
/**
* 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 its child nodes.
*
* By returning different values from the `enter` and `leave` functions, the
* behavior of the visitor can be altered.
* - no return (`void`) or return `null`: no action
* - `Visitor::skipNode()`: skips over the subtree at the current node of the AST
* - `Visitor::stop()`: stop the Visitor completely
* - `Visitor::removeNode()`: remove the current node
* - return any other value: replace this node with the returned value
*
* 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 $node, $key, $parent, array $path, array $ancestors) {
* // ...
* },
* 'leave' => function (Node $node, $key, $parent, array $path, array $ancestors) {
* // ...
* }
* ]);
*
* Alternatively to providing `enter` and `leave` functions, a visitor can
* instead provide functions named the same as the [kinds of AST nodes](class-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, [
* NodeKind::OBJECT_TYPE_DEFINITION => function (ObjectTypeDefinitionNode $node) {
* // enter the "ObjectTypeDefinition" node
* }
* ]);
*
* 2. Named visitors that trigger upon entering and leaving a node of
* a specific kind.
*
* Visitor::visit($ast, [
* NodeKind::OBJECT_TYPE_DEFINITION => [
* 'enter' => function (ObjectTypeDefinitionNode $node) {
* // enter the "ObjectTypeDefinition" node
* }
* 'leave' => function (ObjectTypeDefinitionNode $node) {
* // leave the "ObjectTypeDefinition" node
* }
* ]
* ]);
*
* 3. Generic visitors that trigger upon entering and leaving any node.
*
* Visitor::visit($ast, [
* 'enter' => function (Node $node) {
* // enter any node
* },
* 'leave' => function (Node $node) {
* // leave any node
* }
* ]);
*
* 4. Parallel visitors for entering and leaving nodes of a specific kind.
*
* Visitor::visit($ast, [
* 'enter' => [
* NodeKind::OBJECT_TYPE_DEFINITION => function (ObjectTypeDefinitionNode $node) {
* // enter the "ObjectTypeDefinition" node
* }
* },
* 'leave' => [
* NodeKind::OBJECT_TYPE_DEFINITION => function (ObjectTypeDefinitionNode $node) {
* // leave the "ObjectTypeDefinition" node
* }
* ]
* ]);
*
* @phpstan-type NodeVisitor callable(Node): (VisitorOperation|Node|NodeList<Node>|null|false|void)
* @phpstan-type VisitorArray array<string, NodeVisitor>|array<string, array<string, NodeVisitor>>
*
* @see \GraphQL\Tests\Language\VisitorTest
*/
class Visitor
{
public const VISITOR_KEYS = [
NodeKind::NAME => [],
NodeKind::DOCUMENT => ['definitions'],
NodeKind::OPERATION_DEFINITION => ['name', 'variableDefinitions', 'directives', 'selectionSet'],
NodeKind::VARIABLE_DEFINITION => ['variable', 'type', 'defaultValue', 'directives'],
NodeKind::VARIABLE => ['name'],
NodeKind::SELECTION_SET => ['selections'],
NodeKind::FIELD => ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
NodeKind::ARGUMENT => ['name', 'value'],
NodeKind::FRAGMENT_SPREAD => ['name', 'directives'],
NodeKind::INLINE_FRAGMENT => ['typeCondition', 'directives', 'selectionSet'],
NodeKind::FRAGMENT_DEFINITION => [
'name',
// Note: fragment variable definitions are experimental and may be changed
// or removed in the future.
'variableDefinitions',
'typeCondition',
'directives',
'selectionSet',
],
NodeKind::INT => [],
NodeKind::FLOAT => [],
NodeKind::STRING => [],
NodeKind::BOOLEAN => [],
NodeKind::NULL => [],
NodeKind::ENUM => [],
NodeKind::LST => ['values'],
NodeKind::OBJECT => ['fields'],
NodeKind::OBJECT_FIELD => ['name', 'value'],
NodeKind::DIRECTIVE => ['name', 'arguments'],
NodeKind::NAMED_TYPE => ['name'],
NodeKind::LIST_TYPE => ['type'],
NodeKind::NON_NULL_TYPE => ['type'],
NodeKind::SCHEMA_DEFINITION => ['directives', 'operationTypes'],
NodeKind::OPERATION_TYPE_DEFINITION => ['type'],
NodeKind::SCALAR_TYPE_DEFINITION => ['description', 'name', 'directives'],
NodeKind::OBJECT_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'],
NodeKind::FIELD_DEFINITION => ['description', 'name', 'arguments', 'type', 'directives'],
NodeKind::INPUT_VALUE_DEFINITION => ['description', 'name', 'type', 'defaultValue', 'directives'],
NodeKind::INTERFACE_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'],
NodeKind::UNION_TYPE_DEFINITION => ['description', 'name', 'directives', 'types'],
NodeKind::ENUM_TYPE_DEFINITION => ['description', 'name', 'directives', 'values'],
NodeKind::ENUM_VALUE_DEFINITION => ['description', 'name', 'directives'],
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'],
NodeKind::SCALAR_TYPE_EXTENSION => ['name', 'directives'],
NodeKind::OBJECT_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'],
NodeKind::INTERFACE_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'],
NodeKind::UNION_TYPE_EXTENSION => ['name', 'directives', 'types'],
NodeKind::ENUM_TYPE_EXTENSION => ['name', 'directives', 'values'],
NodeKind::INPUT_OBJECT_TYPE_EXTENSION => ['name', 'directives', 'fields'],
NodeKind::DIRECTIVE_DEFINITION => ['description', 'name', 'arguments', 'locations'],
NodeKind::SCHEMA_EXTENSION => ['directives', 'operationTypes'],
];
/**
* Visit the AST (see class description for details).
*
* @param NodeList<Node>|Node $root
* @param VisitorArray $visitor
* @param array<string, mixed>|null $keyMap
*
* @throws \Exception
*
* @return mixed
*
* @api
*/
public static function visit(object $root, array $visitor, ?array $keyMap = null)
{
$visitorKeys = $keyMap ?? self::VISITOR_KEYS;
/**
* @var list<array{
* inList: bool,
* index: int,
* keys: Node|NodeList|mixed,
* edits: array<int, array{mixed, mixed}>,
* }> $stack */
$stack = [];
$inList = $root instanceof NodeList;
$keys = [$root];
$index = -1;
$edits = [];
$parent = null;
$path = [];
$ancestors = [];
do {
++$index;
$isLeaving = $index === count($keys);
$key = null;
$node = null;
$isEdited = $isLeaving && $edits !== [];
if ($isLeaving) {
$key = $ancestors === []
? null
: $path[count($path) - 1];
$node = $parent;
$parent = array_pop($ancestors);
if ($isEdited) {
if ($node instanceof Node || $node instanceof NodeList) {
$node = $node->cloneDeep();
}
$editOffset = 0;
foreach ($edits as [$editKey, $editValue]) {
if ($inList) {
$editKey -= $editOffset;
}
if ($inList && $editValue === null) {
assert($node instanceof NodeList, 'Follows from $inList');
$node->splice($editKey, 1);
++$editOffset;
} elseif ($node instanceof NodeList) {
if ($editValue instanceof NodeList) {
$node->splice($editKey, 1, $editValue);
$editOffset -= count($editValue) - 1;
} elseif ($editValue instanceof Node) {
$node[$editKey] = $editValue;
} else {
$notNodeOrNodeList = Utils::printSafe($editValue);
throw new \Exception("Can only add Node or NodeList to NodeList, got: {$notNodeOrNodeList}.");
}
} else {
$node->{$editKey} = $editValue;
}
}
}
// @phpstan-ignore-next-line the stack is guaranteed to be non-empty at this point
[
'index' => $index,
'keys' => $keys,
'edits' => $edits,
'inList' => $inList,
] = array_pop($stack);
} elseif ($parent === null) {
$node = $root;
} else {
$key = $inList
? $index
: $keys[$index];
$node = $parent instanceof NodeList
? $parent[$key]
: $parent->{$key};
if ($node === null) {
continue;
}
$path[] = $key;
}
$result = null;
if (! $node instanceof NodeList) {
if (! ($node instanceof Node)) {
$notNode = Utils::printSafe($node);
throw new \Exception("Invalid AST Node: {$notNode}.");
}
$visitFn = self::extractVisitFn($visitor, $node->kind, $isLeaving);
if ($visitFn !== null) {
$result = $visitFn($node, $key, $parent, $path, $ancestors);
if ($result !== null) {
if ($result instanceof VisitorStop) {
break;
}
if ($result instanceof VisitorSkipNode) {
if (! $isLeaving) {
array_pop($path);
}
continue;
}
$editValue = $result instanceof VisitorRemoveNode
? null
: $result;
$edits[] = [$key, $editValue];
if (! $isLeaving) {
if (! ($editValue instanceof Node)) {
array_pop($path);
continue;
}
$node = $editValue;
}
}
}
}
if ($result === null && $isEdited) {
$edits[] = [$key, $node];
}
if ($isLeaving) {
array_pop($path);
} else {
$stack[] = [
'inList' => $inList,
'index' => $index,
'keys' => $keys,
'edits' => $edits,
];
$inList = $node instanceof NodeList;
$keys = ($inList ? $node : $visitorKeys[$node->kind]) ?? [];
$index = -1;
$edits = [];
if ($parent !== null) {
$ancestors[] = $parent;
}
$parent = $node;
}
} while ($stack !== []);
return $edits === []
? $root
: $edits[0][1];
}
/**
* Returns marker for stopping.
*
* @api
*/
public static function stop(): VisitorStop
{
static $stop;
return $stop ??= new VisitorStop();
}
/**
* Returns marker for skipping the subtree at the current node.
*
* @api
*/
public static function skipNode(): VisitorSkipNode
{
static $skipNode;
return $skipNode ??= new VisitorSkipNode();
}
/**
* Returns marker for removing the current node.
*
* @api
*/
public static function removeNode(): VisitorRemoveNode
{
static $removeNode;
return $removeNode ??= new VisitorRemoveNode();
}
/**
* Combines the given visitors to run in parallel.
*
* @phpstan-param array<int, VisitorArray> $visitors
*
* @return VisitorArray
*/
public static function visitInParallel(array $visitors): array
{
$visitorsCount = count($visitors);
$skipping = new \SplFixedArray($visitorsCount);
return [
'enter' => static function (Node $node) use ($visitors, $skipping, $visitorsCount) {
for ($i = 0; $i < $visitorsCount; ++$i) {
if ($skipping[$i] !== null) {
continue;
}
$fn = self::extractVisitFn(
$visitors[$i],
$node->kind,
false
);
if ($fn === null) {
continue;
}
$result = $fn(...func_get_args());
if ($result instanceof VisitorSkipNode) {
$skipping[$i] = $node;
} elseif ($result instanceof VisitorStop) {
$skipping[$i] = $result;
} elseif ($result instanceof VisitorRemoveNode) {
return $result;
} elseif ($result !== null) {
return $result;
}
}
return null;
},
'leave' => static function (Node $node) use ($visitors, $skipping, $visitorsCount) {
for ($i = 0; $i < $visitorsCount; ++$i) {
if ($skipping[$i] === null) {
$fn = self::extractVisitFn(
$visitors[$i],
$node->kind,
true
);
if ($fn !== null) {
$result = $fn(...func_get_args());
if ($result instanceof VisitorStop) {
$skipping[$i] = $result;
} elseif ($result instanceof VisitorRemoveNode) {
return $result;
} elseif ($result !== null) {
return $result;
}
}
} elseif ($skipping[$i] === $node) {
$skipping[$i] = null;
}
}
return null;
},
];
}
/**
* Creates a new visitor that updates TypeInfo and delegates to the given visitor.
*
* @phpstan-param VisitorArray $visitor
*
* @phpstan-return VisitorArray
*/
public static function visitWithTypeInfo(TypeInfo $typeInfo, array $visitor): array
{
return [
'enter' => static function (Node $node) use ($typeInfo, $visitor) {
$typeInfo->enter($node);
$fn = self::extractVisitFn($visitor, $node->kind, false);
if ($fn === null) {
return null;
}
$result = $fn(...func_get_args());
if ($result === null) {
return null;
}
$typeInfo->leave($node);
if ($result instanceof Node) {
$typeInfo->enter($result);
}
return $result;
},
'leave' => static function (Node $node) use ($typeInfo, $visitor) {
$fn = self::extractVisitFn($visitor, $node->kind, true);
$result = $fn !== null
? $fn(...func_get_args())
: null;
$typeInfo->leave($node);
return $result;
},
];
}
/**
* @phpstan-param VisitorArray $visitor
*
* @return (callable(Node $node, string|int|null $key, Node|NodeList<Node>|null $parent, array<int, int|string> $path, array<int, Node|NodeList<Node>> $ancestors): (VisitorOperation|Node|null))|(callable(Node): (VisitorOperation|Node|NodeList<Node>|void|false|null))|null
*/
protected static function extractVisitFn(array $visitor, string $kind, bool $isLeaving): ?callable
{
$kindVisitor = $visitor[$kind] ?? null;
if (is_array($kindVisitor)) {
return $isLeaving
? $kindVisitor['leave'] ?? null
: $kindVisitor['enter'] ?? null;
}
if ($kindVisitor !== null && ! $isLeaving) {
return $kindVisitor;
}
$specificVisitor = $isLeaving
? $visitor['leave'] ?? null
: $visitor['enter'] ?? null;
if (is_array($specificVisitor)) {
return $specificVisitor[$kind] ?? null;
}
return $specificVisitor;
}
}

View File

@@ -1,5 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language;
abstract class VisitorOperation {}

View File

@@ -1,5 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language;
final class VisitorRemoveNode extends VisitorOperation {}

View File

@@ -1,5 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language;
final class VisitorSkipNode extends VisitorOperation {}

View File

@@ -1,5 +0,0 @@
<?php declare(strict_types=1);
namespace GraphQL\Language;
final class VisitorStop extends VisitorOperation {}

Some files were not shown because too many files have changed in this diff Show More