Compare commits

..

No commits in common. "master" and "v0.10.2" have entirely different histories.

403 changed files with 30699 additions and 50686 deletions

11
.gitattributes vendored
View file

@ -1,13 +1,2 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text eol=lf
/benchmarks export-ignore
/tests export-ignore
/examples export-ignore
/tools export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.travis.yml export-ignore
CONTRIBUTING.md export-ignore
mkdocs.yml export-ignore
phpbench.json export-ignore
phpunit.xml.dist export-ignore

8
.gitignore vendored
View file

@ -1,7 +1,5 @@
.phpcs-cache
composer.lock
.idea/
composer.phar
phpcs.xml
phpstan.neon
composer.lock
vendor/
/.idea
bin/

View file

@ -1,29 +0,0 @@
build:
nodes:
analysis:
environment:
php:
version: 7.1
cache:
disabled: false
directories:
- ~/.composer/cache
project_setup:
override: true
tests:
override:
- php-scrutinizer-run
dependencies:
override:
- composer install --ignore-platform-reqs --no-interaction
tools:
external_code_coverage:
timeout: 900
build_failure_conditions:
- 'elements.rating(<= C).new.exists' # No new classes/methods with a rating of C or worse allowed
- 'issues.label("coding-style").new.exists' # No new coding style issues allowed
- 'issues.severity(>= MAJOR).new.exists' # New issues of major or higher severity
- 'project.metric_change("scrutinizer.test_coverage", < 0)' # Code Coverage decreased from previous inspection

View file

@ -1,66 +1,34 @@
dist: trusty
language: php
# Required for HHVM, see https://github.com/travis-ci/travis-ci/issues/7712
dist: trusty
php:
- 7.1
- 7.2
- 7.3
- 7.4snapshot
- nightly
env:
matrix:
- EXECUTOR= DEPENDENCIES=--prefer-lowest
- EXECUTOR=coroutine DEPENDENCIES=--prefer-lowest
- EXECUTOR=
- EXECUTOR=coroutine
- 5.5
- 5.6
- 7.0
- 7.1
- hhvm
- nightly
matrix:
allow_failures:
- php: nightly
cache:
directories:
- $HOME/.composer/cache
directories:
- $HOME/.composer/cache
before_install:
- mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{,.disabled} || echo "xdebug not available"
- travis_retry composer self-update
- if [[ "$TRAVIS_PHP_VERSION" != "5.6" && "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then phpenv config-rm xdebug.ini || true; fi
- composer selfupdate
install: travis_retry composer update --prefer-dist
install:
- composer install --dev --prefer-dist
- composer require react/promise:2.*
- composer require psr/http-message:1.*
script: ./vendor/bin/phpunit --group default,ReactPromise
jobs:
allow_failures:
- php: 7.4snapshot
- php: nightly
include:
- stage: Test
install:
- travis_retry composer update --prefer-dist {$DEPENDENCIES}
- stage: Test
env: COVERAGE
before_script:
- mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{.disabled,}
- if [[ ! $(php -m | grep -si xdebug) ]]; then echo "xdebug required for coverage"; exit 1; fi
script:
- ./vendor/bin/phpunit --coverage-php /tmp/coverage/clover_executor.cov
- EXECUTOR=coroutine ./vendor/bin/phpunit --coverage-php /tmp/coverage/clover_executor-coroutine.cov
after_script:
- ./vendor/bin/phpcov merge /tmp/coverage --clover /tmp/clover.xml
- wget https://github.com/scrutinizer-ci/ocular/releases/download/1.5.2/ocular.phar
- php ocular.phar code-coverage:upload --format=php-clover /tmp/clover.xml
- stage: Code Quality
php: 7.1
env: CODING_STANDARD
install: travis_retry composer install --prefer-dist
script:
- ./vendor/bin/phpcs
- stage: Code Quality
php: 7.1
env: STATIC_ANALYSIS
install: travis_retry composer install --prefer-dist
script: composer static-analysis
script: if [ "$TRAVIS_PHP_VERSION" == "5.6" ]; then bin/phpunit --coverage-clover build/logs/clover.xml --group default,ReactPromise; else bin/phpunit --group default,ReactPromise; fi
after_success:
- if [ "$TRAVIS_PHP_VERSION" == "5.6" ]; then composer require "satooshi/php-coveralls:^1.0" && travis_retry php bin/coveralls -v; fi

View file

@ -1,108 +1,5 @@
# Changelog
## Unreleased
- Add schema validation: Input Objects must not contain non-nullable circular references (#492)
#### v0.13.5
- Fix coroutine executor when using with promise (#486)
#### v0.13.4
- Force int when setting max query depth (#477)
#### v0.13.3
- Reverted minor possible breaking change (#476)
#### v0.13.2
- Added QueryPlan support (#436)
- Fixed an issue with NodeList iteration over missing keys (#475)
#### v0.13.1
- Better validation of field/directive arguments
- Support for apollo client/server persisted queries
- Minor tweaks and fixes
## v0.13.0
This release brings several breaking changes. Please refer to [UPGRADE](UPGRADE.md) document for details.
New features and notable changes:
- PHP version required: 7.1+
- Spec compliance: error `category` and extensions are displayed under `extensions` key when using default formatting (#389)
- New experimental executor with improved performance (#314).<br>
It is a one-line switch: `GraphQL::useExperimentalExecutor()`.<br>
<br>
**Please try it and post your feedback at https://github.com/webonyx/graphql-php/issues/397**
(as it may become the default one in future)
<br>
<br>
- Ported `extendSchema` from the reference implementation under `GraphQL\Utils\SchemaExtender` (#362)
- Added ability to override standard types via `GraphQL::overrideStandardTypes(array $types)` (#401)
- Added flag `Debug::RETHROW_UNSAFE_EXCEPTIONS` which would only rethrow app-specific exceptions (#337)
- Several classes were renamed (see [UPGRADE.md](UPGRADE.md))
- Schema Validation improvements
#### v0.12.6
- Bugfix: Call to a member function getLocation() on null (#336)
- Fixed several errors discovered by static analysis (#329)
#### v0.12.5
- Execution performance optimization for lists
#### v0.12.4
- Allow stringeable objects to be serialized by StringType (#303)
#### v0.12.3
- StandardServer: add support for the multipart/form-data content type (#300)
#### v0.12.2
- SchemaPrinter: Use multi-line block for trailing quote (#294)
#### v0.12.1
- Fixed bug in validation rule OverlappingFieldsCanBeMerged (#292)
- Added one more breaking change note in UPGRADE.md (#291)
- Spec compliance: remove `data` entry from response on top-level error (#281)
## v0.12.0
- RFC: Block String (multi-line strings via triple-quote """string""")
- GraphQL Schema SDL: Descriptions as strings (including multi-line)
- Changed minimum required PHP version to 5.6
Improvements:
- Allow extending GraphQL errors with additional properties
- Fixed parsing of default values in Schema SDL
- Handling several more cases in findBreakingChanges
- StandardServer: expect `operationName` (instead of `operation`) in input
#### v0.11.5
- Allow objects with __toString in IDType
#### v0.11.4
- findBreakingChanges utility (see #199)
#### v0.11.3
- StandardServer: Support non pre-parsed PSR-7 request body (see #202)
#### v0.11.2
- Bugfix: provide descriptions to custom scalars (see #181)
#### v0.11.1
- Ability to override internal types via `types` option of the schema (see #174).
## v0.11.0
This release brings little changes but there are two reasons why it is released as major version:
1. To follow reference implementation versions (it matches 0.11.x series of graphql-js)
2. It may break existing applications because scalar input coercion rules are stricter now:<br>
In previous versions sloppy client input could leak through with unexpected results.
For example string `"false"` accidentally sent in variables was converted to boolean `true`
and passed to field arguments. In the new version, such input will produce an error
(which is a spec-compliant behavior).
Improvements:
- Stricter input coercion (see #171)
- Types built with `BuildSchema` now have reference to AST node with corresponding AST definition (in $astNode property)
- Account for query offset for error locations (e.g. when query is stored in `.graphql` file)
#### v0.10.2
- StandardServer improvement: do not raise an error when variables are passed as empty string (see #156)
@ -197,4 +94,4 @@ Improvements:
- New docs and examples
## Older versions
Look at [GitHub Releases Page](https://github.com/webonyx/graphql-php/releases).
Look at [Github Releases Page](https://github.com/webonyx/graphql-php/releases).

View file

@ -1,8 +1,9 @@
# Contributing to GraphQL PHP
## Workflow
If your contribution requires significant or breaking changes, or if you plan to propose a major new feature,
we recommend you to create an issue on the [GitHub](https://github.com/webonyx/graphql-php/issues) with
we recommend you to create an issue on the [Github](https://github.com/webonyx/graphql-php/issues) with
a brief proposal and discuss it with us first.
For smaller contributions just use this workflow:
@ -10,46 +11,17 @@ For smaller contributions just use this workflow:
* Fork the project.
* Add your features and or bug fixes.
* Add tests. Tests are important for us.
* Check your changes using `composer check-all`.
* Add an entry to the [Changelog's Unreleases section](CHANGELOG.md#unreleased).
* Send a pull request.
* Send a pull request
## Setup the Development Environment
First, copy the URL of your fork and `git clone` it to your local machine.
```sh
cd graphql-php
composer install
## Using GraphQL PHP from a Git checkout
```
$ git clone https://github.com/webonyx/graphql-php.git
$ cd graphql-php
$ composer install
```
## Running tests
```sh
./vendor/bin/phpunit
From the project root:
```
Some tests have annotation `@see it('<description>')`. It is used for reference to same tests in [graphql-js implementation](https://github.com/graphql/graphql-js) with the same description.
## Coding Standard
The coding standard of this project is based on [Doctrine CS](https://github.com/doctrine/coding-standard).
Run the inspections:
```sh
./vendor/bin/phpcs
```
Apply automatic code style fixes:
```sh
./vendor/bin/phpcbf
```
## Static analysis
Based on [PHPStan](https://github.com/phpstan/phpstan).
```sh
./vendor/bin/phpstan analyse --ansi --memory-limit 256M
```
## Running benchmarks
Benchmarks are run via [PHPBench](https://github.com/phpbench/phpbench).
```sh
./vendor/bin/phpbench run .
$ ./bin/phpunit
```

41
LICENSE
View file

@ -1,21 +1,28 @@
MIT License
Copyright (c) 2015, webonyx
All rights reserved.
Copyright (c) 2015-present, Webonyx, LLC.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
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:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of graphql-php nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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,6 +1,6 @@
# graphql-php
[![Build Status](https://travis-ci.org/webonyx/graphql-php.svg?branch=master)](https://travis-ci.org/webonyx/graphql-php)
[![Code Coverage](https://scrutinizer-ci.com/g/webonyx/graphql-php/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/webonyx/graphql-php)
[![Coverage Status](https://coveralls.io/repos/github/webonyx/graphql-php/badge.svg)](https://coveralls.io/github/webonyx/graphql-php)
[![Latest Stable Version](https://poser.pugx.org/webonyx/graphql-php/version)](https://packagist.org/packages/webonyx/graphql-php)
[![License](https://poser.pugx.org/webonyx/graphql-php/license)](https://packagist.org/packages/webonyx/graphql-php)
@ -14,7 +14,7 @@ composer require webonyx/graphql-php
```
## Documentation
Full documentation is available on the [Documentation site](https://webonyx.github.io/graphql-php/) as well
Full documentation is available on the [Documentation site](http://webonyx.github.io/graphql-php/) as well
as in the [docs](docs/) folder of the distribution.
If you don't know what GraphQL is, visit this [official website](http://graphql.org)
@ -24,29 +24,11 @@ by the Facebook engineering team.
There are several ready examples in the [examples](examples/) folder of the distribution with specific
README file per example.
## Contributors
## Contribute
Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) for information on how to contribute.
This project exists thanks to [all the people](https://github.com/webonyx/graphql-php/graphs/contributors) who contribute. [[Contribute](CONTRIBUTING.md)].
## Old README.md
Here is a [link to the old README.md](https://github.com/webonyx/graphql-php/blob/v0.9.14/README.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).
Keep in mind that it relates to the version 0.9.x. It may contain outdated information for
newer versions (even though we try to preserve backwards compatibility).

View file

@ -1,222 +1,3 @@
## Master
### Breaking (major): dropped deprecations
- dropped deprecated `GraphQL\Schema`. Use `GraphQL\Type\Schema`.
## Upgrade v0.12.x > v0.13.x
### Breaking (major): minimum supported version of PHP
New minimum required version of PHP is **7.1+**
### Breaking (major): default errors formatting changed according to spec
**Category** and extensions assigned to errors are shown under `extensions` key
```php
$e = new Error(
'msg',
null,
null,
null,
null,
null,
['foo' => 'bar']
);
```
Formatting before the change:
```
'errors' => [
[
'message' => 'msg',
'category' => 'graphql',
'foo' => 'bar'
]
]
```
After the change:
```
'errors' => [
[
'message' => 'msg',
'extensions' => [
'category' => 'graphql',
'foo' => 'bar',
],
]
]
```
Note: if error extensions contain `category` key - it has a priority over default category.
You can always switch to [custom error formatting](https://webonyx.github.io/graphql-php/error-handling/#custom-error-handling-and-formatting) to revert to the old format.
### Try it: Experimental Executor with improved performance
It is disabled by default. To enable it, do the following
```php
<?php
use GraphQL\Executor\Executor;
use GraphQL\Experimental\Executor\CoroutineExecutor;
Executor::setImplementationFactory([CoroutineExecutor::class, 'create']);
```
**Please post your feedback about new executor at https://github.com/webonyx/graphql-php/issues/397
Especially if you had issues (because it may become the default in one of the next releases)**
### Breaking: multiple interfaces separated with & in SDL
Before the change:
```graphql
type Foo implements Bar, Baz { field: Type }
```
After the change:
```graphql
type Foo implements Bar & Baz { field: Type }
```
To allow for an adaptive migration, use `allowLegacySDLImplementsInterfaces` option of parser:
```php
Parser::parse($source, [ 'allowLegacySDLImplementsInterfaces' => true])
```
### Breaking: several classes renamed
- `AbstractValidationRule` renamed to `ValidationRule` (NS `GraphQL\Validator\Rules`)
- `AbstractQuerySecurity` renamed to `QuerySecurityRule` (NS `GraphQL\Validator\Rules`)
- `FindBreakingChanges` renamed to `BreakingChangesFinder` (NS `GraphQL\Utils`)
### Breaking: new constructors
`GraphQL\Type\Definition\ResolveInfo` now takes 10 arguments instead of one array.
## Upgrade v0.11.x > v0.12.x
### Breaking: Minimum supported version is PHP5.6
Dropped support for PHP 5.5. This release still supports PHP 5.6 and PHP 7.0
**But the next major release will require PHP7.1+**
### Breaking: Custom scalar types need to throw on invalid value
As null might be a valid value custom types need to throw an
Exception inside `parseLiteral()`, `parseValue()` and `serialize()`.
Returning null from any of these methods will now be treated as valid result.
### Breaking: Custom scalar types parseLiteral() declaration changed
A new parameter was added to `parseLiteral()`, which also needs to be added to any custom scalar type extending from `ScalarType`
Before:
```php
class MyType extends ScalarType {
...
public function parseLiteral($valueNode) {
//custom implementation
}
}
```
After:
```php
class MyType extends ScalarType {
...
public function parseLiteral($valueNode, array $variables = null) {
//custom implementation
}
}
```
### Breaking: Descriptions in comments are not used as descriptions by default anymore
Descriptions now need to be inside Strings or BlockStrings in order to be picked up as
description. If you want to keep the old behaviour you can supply the option `commentDescriptions`
to BuildSchema::buildAST(), BuildSchema::build() or Printer::doPrint().
Here is the official way now to define descriptions in the graphQL language:
Old:
```graphql
# Description
type Dog {
...
}
```
New:
```graphql
"Description"
type Dog {
...
}
"""
Long Description
"""
type Dog {
...
}
```
### Breaking: Cached AST of version 0.11.x is not compatible with 0.12.x.
That's because description in AST is now a separate node, not just a string.
Make sure to renew caches.
### Breaking: Most of previously deprecated classes and methods were removed
See deprecation notices for previous versions in details.
### Breaking: Standard server expects `operationName` vs `operation` for multi-op queries
Before the change:
```json
{
"queryId": "persisted-query-id",
"operation": "QueryFromPersistedDocument",
"variables": {}
}
```
After the change:
```json
{
"queryId": "persisted-query-id",
"operationName": "QueryFromPersistedDocument",
"variables": {}
}
```
This naming is aligned with graphql-express version.
### Possibly Breaking: AST to array serialization excludes nulls
Most users won't be affected. It *may* affect you only if you do your own manipulations
with exported AST.
Example of json-serialized AST before the change:
```json
{
"kind": "Field",
"loc": null,
"name": {
"kind": "Name",
"loc": null,
"value": "id"
},
"alias": null,
"arguments": [],
"directives": [],
"selectionSet": null
}
```
After the change:
```json
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "id"
},
"arguments": [],
"directives": []
}
```
## Upgrade v0.8.x, v0.9.x > v0.10.x
### Breaking: changed minimum PHP version from 5.4 to 5.5

View file

@ -1,11 +1,11 @@
<?php
namespace GraphQL\Benchmarks;
use GraphQL\GraphQL;
use GraphQL\Schema;
use GraphQL\Benchmarks\Utils\QueryGenerator;
use GraphQL\Benchmarks\Utils\SchemaGenerator;
use GraphQL\GraphQL;
use GraphQL\Type\Schema;
use GraphQL\Type\SchemaConfig;
use GraphQL\Type\LazyResolution;
/**
* @BeforeMethods({"setUp"})
@ -16,12 +16,18 @@ use GraphQL\Type\SchemaConfig;
*/
class HugeSchemaBench
{
/** @var SchemaGenerator */
/**
* @var SchemaGenerator
*/
private $schemaBuilder;
private $schema;
/** @var string */
private $lazySchema;
/**
* @var string
*/
private $smallQuery;
public function setUp()
@ -30,12 +36,12 @@ class HugeSchemaBench
'totalTypes' => 600,
'fieldsPerType' => 8,
'listFieldsPerType' => 2,
'nestingLevel' => 10,
'nestingLevel' => 10
]);
$this->schema = $this->schemaBuilder->buildSchema();
$queryBuilder = new QueryGenerator($this->schema, 0.05);
$queryBuilder = new QueryGenerator($this->schema, 0.05);
$this->smallQuery = $queryBuilder->buildQuery();
}
@ -43,7 +49,8 @@ class HugeSchemaBench
{
$this->schemaBuilder
->buildSchema()
->getTypeMap();
->getTypeMap()
;
}
public function benchSchemaLazy()
@ -53,21 +60,21 @@ class HugeSchemaBench
public function benchSmallQuery()
{
$result = GraphQL::executeQuery($this->schema, $this->smallQuery);
$result = GraphQL::execute($this->schema, $this->smallQuery);
}
public function benchSmallQueryLazy()
{
$schema = $this->createLazySchema();
$result = GraphQL::executeQuery($schema, $this->smallQuery);
$result = GraphQL::execute($schema, $this->smallQuery);
}
private function createLazySchema()
{
return new Schema(
SchemaConfig::create()
\GraphQL\Type\SchemaConfig::create()
->setQuery($this->schemaBuilder->buildQueryType())
->setTypeLoader(function ($name) {
->setTypeLoader(function($name) {
return $this->schemaBuilder->loadType($name);
})
);

View file

@ -1,6 +1,5 @@
<?php
namespace GraphQL\Benchmarks;
use GraphQL\GraphQL;
use GraphQL\Tests\StarWarsSchema;
use GraphQL\Type\Introspection;
@ -36,7 +35,7 @@ class StarWarsBench
}
';
GraphQL::executeQuery(
GraphQL::execute(
StarWarsSchema::build(),
$q
);
@ -58,7 +57,7 @@ class StarWarsBench
}
}
';
GraphQL::executeQuery(
GraphQL::execute(
StarWarsSchema::build(),
$q
);
@ -82,7 +81,7 @@ class StarWarsBench
}
';
GraphQL::executeQuery(
GraphQL::execute(
StarWarsSchema::build(),
$q
);
@ -90,7 +89,7 @@ class StarWarsBench
public function benchStarWarsIntrospectionQuery()
{
GraphQL::executeQuery(
GraphQL::execute(
StarWarsSchema::build(),
$this->introQuery
);

View file

@ -7,15 +7,12 @@ use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\Printer;
use GraphQL\Schema;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\WrappingType;
use GraphQL\Type\Schema;
use GraphQL\Utils\Utils;
use function count;
use function max;
use function round;
class QueryGenerator
{
@ -33,14 +30,12 @@ class QueryGenerator
$totalFields = 0;
foreach ($schema->getTypeMap() as $type) {
if (! ($type instanceof ObjectType)) {
continue;
if ($type instanceof ObjectType) {
$totalFields += count($type->getFields());
}
$totalFields += count($type->getFields());
}
$this->maxLeafFields = max(1, round($totalFields * $percentOfLeafFields));
$this->maxLeafFields = max(1, round($totalFields * $percentOfLeafFields));
$this->currentLeafFields = 0;
}
@ -49,12 +44,13 @@ class QueryGenerator
$qtype = $this->schema->getQueryType();
$ast = new DocumentNode([
'definitions' => [new OperationDefinitionNode([
'name' => new NameNode(['value' => 'TestQuery']),
'operation' => 'query',
'selectionSet' => $this->buildSelectionSet($qtype->getFields()),
]),
],
'definitions' => [
new OperationDefinitionNode([
'name' => new NameNode(['value' => 'TestQuery']),
'operation' => 'query',
'selectionSet' => $this->buildSelectionSet($qtype->getFields())
])
]
]);
return Printer::doPrint($ast);
@ -62,13 +58,12 @@ class QueryGenerator
/**
* @param FieldDefinition[] $fields
*
* @return SelectionSetNode
*/
public function buildSelectionSet($fields)
{
$selections[] = new FieldNode([
'name' => new NameNode(['value' => '__typename']),
'name' => new NameNode(['value' => '__typename'])
]);
$this->currentLeafFields++;
@ -92,12 +87,12 @@ class QueryGenerator
$selections[] = new FieldNode([
'name' => new NameNode(['value' => $field->name]),
'selectionSet' => $selectionSet,
'selectionSet' => $selectionSet
]);
}
$selectionSet = new SelectionSetNode([
'selections' => $selections,
'selections' => $selections
]);
return $selectionSet;

View file

@ -1,11 +1,11 @@
<?php
namespace GraphQL\Benchmarks\Utils;
use GraphQL\Schema;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
class SchemaGenerator
{
@ -152,7 +152,7 @@ class SchemaGenerator
];
}
public function resolveField($objectValue, $args, $context, $resolveInfo)
public function resolveField($value, $args, $context, $resolveInfo)
{
return $resolveInfo->fieldName . '-value';
}

View file

@ -2,33 +2,25 @@
"name": "webonyx/graphql-php",
"description": "A PHP port of GraphQL reference implementation",
"type": "library",
"license": "MIT",
"license": "BSD",
"homepage": "https://github.com/webonyx/graphql-php",
"keywords": [
"graphql",
"API"
],
"require": {
"php": "^7.1||^8.0",
"ext-json": "*",
"php": ">=5.5,<8.0-DEV",
"ext-mbstring": "*"
},
"require-dev": {
"doctrine/coding-standard": "^6.0",
"phpbench/phpbench": "^0.14.0",
"phpstan/phpstan": "^0.11.12",
"phpstan/phpstan-phpunit": "^0.11.2",
"phpstan/phpstan-strict-rules": "^0.11.1",
"phpunit/phpcov": "^5.0",
"phpunit/phpunit": "^7.2",
"psr/http-message": "^1.0",
"react/promise": "2.*"
"phpunit/phpunit": "^4.8",
"psr/http-message": "^1.0"
},
"config": {
"preferred-install": "dist",
"sort-packages": true
"bin-dir": "bin"
},
"autoload": {
"files": ["src/deprecated.php"],
"psr-4": {
"GraphQL\\": "src/"
}
@ -43,14 +35,5 @@
"suggest": {
"react/promise": "To leverage async resolving on React PHP platform",
"psr/http-message": "To use standard GraphQL server"
},
"scripts": {
"api-docs": "php tools/gendocs.php",
"bench": "phpbench run .",
"test": "phpunit",
"lint" : "phpcs",
"fix-style" : "phpcbf",
"static-analysis": "phpstan analyse --ansi --memory-limit 256M",
"check-all": "composer lint && composer static-analysis && composer test"
}
}

View file

@ -1,26 +1,14 @@
# Integrations
* [Standard Server](executing-queries.md/#using-server) Out of the box integration with any PSR-7 compatible framework (like [Slim](http://slimframework.com) or [Zend Expressive](http://zendframework.github.io/zend-expressive/)).
* [Relay Library for graphql-php](https://github.com/ivome/graphql-relay-php) Helps construct Relay related schema definitions.
* [Lighthouse](https://github.com/nuwave/lighthouse) Laravel based, uses Schema Definition Language
* [Laravel GraphQL](https://github.com/rebing/graphql-laravel) - Laravel wrapper for Facebook's GraphQL
* [OverblogGraphQLBundle](https://github.com/overblog/GraphQLBundle) Bundle for Symfony
* [WP-GraphQL](https://github.com/wp-graphql/wp-graphql) - GraphQL API for WordPress
- [Integration with Relay](https://github.com/ivome/graphql-relay-php)
- [Integration with Laravel 5](https://github.com/Folkloreatelier/laravel-graphql) + [Relay Helpers for Laravel](https://github.com/nuwave/laravel-graphql-relay)
- [Symfony Bundle](https://github.com/overblog/GraphQLBundle) by Overblog
- Define types with Doctrine ORM annotations ([for PHP7.1](https://github.com/Ecodev/graphql-doctrine), for [earlier PHP versions](https://github.com/rahuljayaraman/doctrine-graphql))
- Out of the box integration with any PSR-7 compatible framework (like [Slim](http://slimframework.com) or [Zend Expressive](http://zendframework.github.io/zend-expressive/)) via [Standard Server](executing-queries.md/#using-server)
# GraphQL PHP Tools
* [GraphQLite](https://graphqlite.thecodingmachine.io) Define your complete schema with annotations
* [GraphQL Doctrine](https://github.com/Ecodev/graphql-doctrine) Define types with Doctrine ORM annotations
* [DataLoaderPHP](https://github.com/overblog/dataloader-php) as a ready implementation for [deferred resolvers](data-fetching.md#solving-n1-problem)
* [GraphQL Uploads](https://github.com/Ecodev/graphql-upload) A PSR-15 middleware to support file uploads in GraphQL.
* [GraphQL Batch Processor](https://github.com/vasily-kartashov/graphql-batch-processing) Provides a builder interface for defining collection, querying, filtering, and post-processing logic of batched data fetches.
* [GraphQL Utils](https://github.com/simPod/GraphQL-Utils) Objective schema definition builders (no need for arrays anymore) and `DateTime` scalar
* [PSR 15 compliant middleware](https://github.com/phps-cans/psr7-middleware-graphql) for the Standard Server _(experimental)_
# General GraphQL Tools
* [GraphQL Playground](https://github.com/prismagraphql/graphql-playground) GraphQL IDE for better development workflows (GraphQL Subscriptions, interactive docs & collaboration).
* [GraphiQL](https://github.com/graphql/graphiql) An in-browser IDE for exploring GraphQL
* [ChromeiQL](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij)
or [GraphiQL Feen](https://chrome.google.com/webstore/detail/graphiql-feen/mcbfdonlkfpbfdpimkjilhdneikhfklp)
# Tools
- [GraphiQL](https://github.com/graphql/graphiql) - An in-browser IDE for exploring GraphQL
- [ChromeiQL](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij)
or [GraphiQL Feen](https://chrome.google.com/webstore/detail/graphiql-feen/mcbfdonlkfpbfdpimkjilhdneikhfklp) -
GraphiQL as Google Chrome extension
- [DataLoader PHP](https://github.com/overblog/dataloader-php) - as a ready implementation for [deferred resolvers](data-fetching.md#solving-n1-problem)

View file

@ -103,25 +103,23 @@ for a field you simply override this default resolver.
**graphql-php** provides following default field resolver:
```php
<?php
function defaultFieldResolver($objectValue, $args, $context, \GraphQL\Type\Definition\ResolveInfo $info)
{
$fieldName = $info->fieldName;
$property = null;
function defaultFieldResolver($source, $args, $context, \GraphQL\Type\Definition\ResolveInfo $info)
{
$fieldName = $info->fieldName;
$property = null;
if (is_array($objectValue) || $objectValue instanceof \ArrayAccess) {
if (isset($objectValue[$fieldName])) {
$property = $objectValue[$fieldName];
}
} elseif (is_object($objectValue)) {
if (isset($objectValue->{$fieldName})) {
$property = $objectValue->{$fieldName};
}
if (is_array($source) || $source instanceof \ArrayAccess) {
if (isset($source[$fieldName])) {
$property = $source[$fieldName];
}
} else if (is_object($source)) {
if (isset($source->{$fieldName})) {
$property = $source->{$fieldName};
}
return $property instanceof Closure
? $property($objectValue, $args, $context, $info)
: $property;
}
return $property instanceof \Closure ? $property($source, $args, $context) : $property;
}
```
As you see it returns value by key (for arrays) or property (for objects).
@ -163,6 +161,7 @@ $userType = new ObjectType([
Keep in mind that **field resolver** has precedence over **default field resolver per type** which in turn
has precedence over **default field resolver**.
# Solving N+1 Problem
Since: 0.9.0
@ -271,4 +270,4 @@ Where **$promiseAdapter** is an instance of:
* Other platforms: write your own class implementing interface: <br>
[`GraphQL\Executor\Promise\PromiseAdapter`](reference.md#graphqlexecutorpromisepromiseadapter).
Then your **resolve** functions should return promises of your platform instead of `GraphQL\Deferred`s.
Then your **resolve** functions should return promises of your platform instead of `GraphQL\Deferred`s.

View file

@ -84,7 +84,7 @@ To change default **"Internal server error"** message to something else, use:
GraphQL\Error\FormattedError::setInternalErrorMessage("Unexpected error");
```
# Debugging tools
#Debugging tools
During development or debugging use `$result->toArray(true)` to add **debugMessage** key to
each formatted error entry. If you also want to add exception trace - pass flags instead:
@ -116,7 +116,7 @@ This will make each error entry to look like this:
];
```
If you prefer the first resolver exception to be re-thrown, use following flags:
If you prefer first resolver exception to be re-thrown, use following flags:
```php
<?php
use GraphQL\GraphQL;
@ -127,9 +127,6 @@ $debug = Debug::INCLUDE_DEBUG_MESSAGE | Debug::RETHROW_INTERNAL_EXCEPTIONS;
$result = GraphQL::executeQuery(/*args*/)->toArray($debug);
```
If you only want to re-throw Exceptions that are not marked as safe through the `ClientAware` interface, use
the flag `Debug::RETHROW_UNSAFE_EXCEPTIONS`.
# Custom Error Handling and Formatting
It is possible to define custom **formatter** and **handler** for result errors.
@ -154,8 +151,8 @@ $myErrorHandler = function(array $errors, callable $formatter) {
$result = GraphQL::executeQuery(/* $args */)
->setErrorFormatter($myErrorFormatter)
->setErrorsHandler($myErrorHandler)
->toArray();
->setErrorHandler($myErrorHandler)
->toArray();
```
Note that when you pass [debug flags](#debugging-tools) to **toArray()** your custom formatter will still be

View file

@ -87,7 +87,7 @@ $psrResponse = new SomePsr7ResponseImplementation(json_encode($result));
PSR-7 is useful when you want to integrate the server into existing framework:
- [PSR-7 for Laravel](https://laravel.com/docs/5.1/requests#psr7-requests)
- [Symfony PSR-7 Bridge](https://symfony.com/doc/current/components/psr7.html)
- [Symfony PSR-7 Bridge](https://symfony.com/doc/current/request/psr7.html)
- [Slim](https://www.slimframework.com/docs/concepts/value-objects.html)
- [Zend Expressive](http://zendframework.github.io/zend-expressive/)
@ -136,13 +136,13 @@ So for example following batch will require single DB request (if user field is
```json
[
{
"query": "{user(id: 1) { id }}"
"query": "{user(id: 1)} { id }"
},
{
"query": "{user(id: 2) { id }}"
"query": "{user(id: 2)} { id }"
},
{
"query": "{user(id: 3) { id }}"
"query": "{user(id: 3)} { id }"
}
]
```

View file

@ -4,7 +4,7 @@ first learn about GraphQL on [the official website](http://graphql.org/learn/).
# Installation
Using [composer](https://getcomposer.org/doc/00-intro.md), run:
Using [composer](https://getcomposer.org/doc/00-intro.md), simply run:
```sh
composer require webonyx/graphql-php
@ -54,8 +54,8 @@ $queryType = new ObjectType([
'args' => [
'message' => Type::nonNull(Type::string()),
],
'resolve' => function ($rootValue, $args) {
return $rootValue['prefix'] . $args['message'];
'resolve' => function ($root, $args) {
return $root['prefix'] . $args['message'];
}
],
],
@ -106,7 +106,7 @@ echo json_encode($output);
Our example is finished. Try it by running:
```sh
php -S localhost:8080 graphql.php
php -S localhost:8000 graphql.php
curl http://localhost:8080 -d '{"query": "query { echo(message: \"Hello World\") }" }'
```

View file

@ -7,7 +7,7 @@
# About GraphQL
GraphQL is a modern way to build HTTP APIs consumed by the web and mobile clients.
It is intended to be an alternative to REST and SOAP APIs (even for **existing applications**).
It is intended to be a replacement for REST and SOAP APIs (even for **existing applications**).
GraphQL itself is a [specification](https://github.com/facebook/graphql) designed by Facebook
engineers. Various implementations of this specification were written
@ -34,9 +34,9 @@ Library features include:
- Parsing, validating and [executing GraphQL queries](executing-queries.md) against this Type System
- Rich [error reporting](error-handling.md), including query validation and execution errors
- Optional tools for [parsing GraphQL Type language](type-system/type-language.md)
- Tools for [batching requests](data-fetching.md#solving-n1-problem) to backend storage
- [Async PHP platforms support](data-fetching.md#async-php) via promises
- [Standard HTTP server](executing-queries.md#using-server)
- Tools for [batching requests](data-fetching.md/#solving-n1-problem) to backend storage
- [Async PHP platforms support](data-fetching.md/#async-php) via promises
- [Standard HTTP server](executing-queries.md/#using-server)
Also, several [complementary tools](complementary-tools.md) are available which provide integrations with
existing PHP frameworks, add support for Relay, etc.
@ -44,12 +44,12 @@ existing PHP frameworks, add support for Relay, etc.
## Current Status
The first version of this library (v0.1) was released on August 10th 2015.
The current version supports all features described by GraphQL specification
as well as some experimental features like
The current version (v0.10) supports all features described by GraphQL specification
(including April 2016 add-ons) as well as some experimental features like
[Schema Language parser](type-system/type-language.md) and
[Schema printer](reference.md#graphqlutilsschemaprinter).
Ready for real-world usage.
## GitHub
## Github
Project source code is [hosted on GitHub](https://github.com/webonyx/graphql-php).

File diff suppressed because it is too large Load diff

View file

@ -35,9 +35,9 @@ In **graphql-php** custom directive is an instance of `GraphQL\Type\Definition\D
```php
<?php
use GraphQL\Language\DirectiveLocation;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\DirectiveLocation;
use GraphQL\Type\Definition\FieldArgument;
$trackDirective = new Directive([
@ -58,4 +58,4 @@ $trackDirective = new Directive([
```
See possible directive locations in
[`GraphQL\Language\DirectiveLocation`](../reference.md#graphqllanguagedirectivelocation).
[`GraphQL\Type\Definition\DirectiveLocation`](../reference.md#graphqltypedefinitiondirectivelocation).

View file

@ -158,7 +158,7 @@ $heroType = new ObjectType([
'args' => [
'episode' => Type::nonNull($enumType)
],
'resolve' => function($hero, $args) {
'resolve' => function($_value, $args) {
return $args['episode'] === 5 ? true : false;
}
]

View file

@ -114,7 +114,7 @@ Option | Type | Notes
name | `string` | **Required.** Name of the input field. When not set - inferred from **fields** array key
type | `Type` | **Required.** Instance of one of [Input Types](input-types.md) (**Scalar**, **Enum**, **InputObjectType** + any combination of those with **nonNull** and **listOf** modifiers)
description | `string` | Plain-text description of this input field for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation)
defaultValue | `scalar` | Default value of this input field. Use the internal value if specifying a default for an **enum** type
defaultValue | `scalar` | Default value of this input field
# Using Input Object Type
In the example above we defined our InputObjectType. Now let's use it in one of field arguments:
@ -131,7 +131,7 @@ $queryType = new ObjectType([
'type' => Type::listOf($storyType),
'args' => [
'filters' => [
'type' => $filters,
'type' => Type::nonNull($filters),
'defaultValue' => [
'popular' => true
]

View file

@ -80,7 +80,7 @@ Option | Type | Notes
name | `string` | **Required.** Name of the field. When not set - inferred from **fields** array key (read about [shorthand field definition](#shorthand-field-definitions) below)
type | `Type` | **Required.** An instance of internal or custom type. Note: type must be represented by a single instance within one schema (see also [Type Registry](index.md#type-registry))
args | `array` | An array of possible type arguments. Each entry is expected to be an array with keys: **name**, **type**, **description**, **defaultValue**. See [Field Arguments](#field-arguments) section below.
resolve | `callable` | **function($objectValue, $args, $context, [ResolveInfo](../reference.md#graphqltypedefinitionresolveinfo) $info)**<br> Given the **$objectValue** of this type, it is expected to return actual value of the current field. See section on [Data Fetching](../data-fetching.md) for details
resolve | `callable` | **function($value, $args, $context, [ResolveInfo](../reference.md#graphqltypedefinitionresolveinfo) $info)**<br> Given the **$value** of this type, it is expected to return actual value of the current field. See section on [Data Fetching](../data-fetching.md) for details
complexity | `callable` | **function($childrenComplexity, $args)**<br> Used to restrict query complexity. The feature is disabled by default, read about [Security](../security.md#query-complexity-analysis) to use it.
description | `string` | Plain-text description of this field for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation)
deprecationReason | `string` | Text describing why this field is deprecated. When not empty - field will not be returned by introspection queries (unless forced)
@ -94,7 +94,7 @@ Option | Type | Notes
name | `string` | **Required.** Name of the argument. When not set - inferred from **args** array key
type | `Type` | **Required.** Instance of one of [Input Types](input-types.md) (**scalar**, **enum**, **InputObjectType** + any combination of those with **nonNull** and **listOf** modifiers)
description | `string` | Plain-text description of this argument for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation)
defaultValue | `scalar` | Default value for this argument. Use the internal value if specifying a default for an **enum** type
defaultValue | `scalar` | Default value for this argument
# Shorthand field definitions
Fields can be also defined in **shorthand** notation (with only **name** and **type** options):

View file

@ -96,11 +96,10 @@ class EmailType extends ScalarType
* }
*
* @param \GraphQL\Language\AST\Node $valueNode
* @param array|null $variables
* @return string
* @throws Error
*/
public function parseLiteral($valueNode, array $variables = null)
public function parseLiteral($valueNode)
{
// Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL
// error location in query:
@ -125,6 +124,6 @@ $emailType = new CustomScalarType([
'name' => 'Email',
'serialize' => function($value) {/* See function body above */},
'parseValue' => function($value) {/* See function body above */},
'parseLiteral' => function($valueNode, array $variables = null) {/* See function body above */},
'parseLiteral' => function($valueNode) {/* See function body above */},
]);
```

View file

@ -62,7 +62,7 @@ $mutationType = new ObjectType([
'episode' => $episodeEnum,
'review' => $reviewInputObject
],
'resolve' => function($rootValue, $args) {
'resolve' => function($val, $args) {
// TODOC
}
]

View file

@ -1,5 +1,4 @@
# Defining your schema
Since 0.9.0
[Type language](http://graphql.org/learn/schema/#type-language) is a convenient way to define your schema,
especially with IDE autocompletion and syntax validation.
@ -33,19 +32,18 @@ $contents = file_get_contents('schema.graphql');
$schema = BuildSchema::build($contents);
```
By default, such schema is created without any resolvers.
By default, such schema is created without any resolvers. As a result, it doesn't support **Interfaces** and **Unions**
because it is impossible to resolve actual implementations during execution.
We have to rely on [default field resolver](../data-fetching.md#default-field-resolver) and **root value** in
Also, we have to rely on [default field resolver](../data-fetching.md#default-field-resolver) and **root value** in
order to execute a query against this schema.
# Defining resolvers
Since 0.10.0
In order to enable **Interfaces**, **Unions** and custom field resolvers you can pass the second argument:
**type config decorator** to schema builder.
In order to enable **Interfaces**, **Unions** and custom field resolvers you can pass the second argument:
**type config decorator** to schema builder.
It accepts default type config produced by the builder and is expected to add missing options like
[**resolveType**](interfaces.md#configuration-options) for interface types or
It accepts default type config produced by the builder and is expected to add missing options like
[**resolveType**](interfaces.md#configuration-options) for interface types or
[**resolveField**](object-types.md#configuration-options) for object types.
```php
@ -63,8 +61,6 @@ $schema = BuildSchema::build($contents, $typeConfigDecorator);
```
# Performance considerations
Since 0.10.0
Method **build()** produces a [lazy schema](schema.md#lazy-loading-of-types)
automatically, so it works efficiently even with very large schemas.
@ -81,11 +77,11 @@ $cacheFilename = 'cached_schema.php';
if (!file_exists($cacheFilename)) {
$document = Parser::parse(file_get_contents('./schema.graphql'));
file_put_contents($cacheFilename, "<?php\nreturn " . var_export(AST::toArray($document), true) . ";\n");
file_put_contents($cacheFilename, "<?php\nreturn " . var_export($document->toArray(), true));
} else {
$document = AST::fromArray(require $cacheFilename); // fromArray() is a lazy operation as well
}
$typeConfigDecorator = function () {};
$schema = BuildSchema::build($document, $typeConfigDecorator);
```
```

View file

@ -19,8 +19,8 @@ try {
'args' => [
'message' => ['type' => Type::string()],
],
'resolve' => function ($rootValue, $args) {
return $rootValue['prefix'] . $args['message'];
'resolve' => function ($root, $args) {
return $root['prefix'] . $args['message'];
}
],
],
@ -35,7 +35,7 @@ try {
'x' => ['type' => Type::int()],
'y' => ['type' => Type::int()],
],
'resolve' => function ($calc, $args) {
'resolve' => function ($root, $args) {
return $args['x'] + $args['y'];
},
],

View file

@ -35,12 +35,12 @@ class CommentType extends ObjectType
Types::htmlField('body')
];
},
'resolveField' => function($comment, $args, $context, ResolveInfo $info) {
'resolveField' => function($value, $args, $context, ResolveInfo $info) {
$method = 'resolve' . ucfirst($info->fieldName);
if (method_exists($this, $method)) {
return $this->{$method}($comment, $args, $context, $info);
return $this->{$method}($value, $args, $context, $info);
} else {
return $comment->{$info->fieldName};
return $value->{$info->fieldName};
}
}
];

View file

@ -57,8 +57,8 @@ class QueryType extends ObjectType
],
'hello' => Type::string()
],
'resolveField' => function($rootValue, $args, $context, ResolveInfo $info) {
return $this->{$info->fieldName}($rootValue, $args, $context, $info);
'resolveField' => function($val, $args, $context, ResolveInfo $info) {
return $this->{$info->fieldName}($val, $args, $context, $info);
}
];
parent::__construct($config);

View file

@ -30,12 +30,11 @@ class UrlType extends ScalarType
*
* @param mixed $value
* @return mixed
* @throws Error
*/
public function parseValue($value)
{
if (!is_string($value) || !filter_var($value, FILTER_VALIDATE_URL)) { // quite naive, but after all this is example
throw new Error("Cannot represent value as URL: " . Utils::printSafe($value));
throw new \UnexpectedValueException("Cannot represent value as URL: " . Utils::printSafe($value));
}
return $value;
}
@ -43,21 +42,20 @@ class UrlType extends ScalarType
/**
* Parses an externally provided literal value to use as an input (e.g. in Query AST)
*
* @param Node $valueNode
* @param array|null $variables
* @param $ast Node
* @return null|string
* @throws Error
*/
public function parseLiteral($valueNode, array $variables = null)
public function parseLiteral($ast)
{
// Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL
// error location in query:
if (!($valueNode instanceof StringValueNode)) {
throw new Error('Query error: Can only parse strings got: ' . $valueNode->kind, [$valueNode]);
if (!($ast instanceof StringValueNode)) {
throw new Error('Query error: Can only parse strings got: ' . $ast->kind, [$ast]);
}
if (!is_string($valueNode->value) || !filter_var($valueNode->value, FILTER_VALIDATE_URL)) {
throw new Error('Query error: Not a valid URL', [$valueNode]);
if (!is_string($ast->value) || !filter_var($ast->value, FILTER_VALIDATE_URL)) {
throw new Error('Query error: Not a valid URL', [$ast]);
}
return $valueNode->value;
return $ast->value;
}
}

View file

@ -75,12 +75,12 @@ class StoryType extends ObjectType
'interfaces' => [
Types::node()
],
'resolveField' => function($story, $args, $context, ResolveInfo $info) {
'resolveField' => function($value, $args, $context, ResolveInfo $info) {
$method = 'resolve' . ucfirst($info->fieldName);
if (method_exists($this, $method)) {
return $this->{$method}($story, $args, $context, $info);
return $this->{$method}($value, $args, $context, $info);
} else {
return $story->{$info->fieldName};
return $value->{$info->fieldName};
}
}
];

View file

@ -44,12 +44,12 @@ class UserType extends ObjectType
'interfaces' => [
Types::node()
],
'resolveField' => function($user, $args, $context, ResolveInfo $info) {
'resolveField' => function($value, $args, $context, ResolveInfo $info) {
$method = 'resolve' . ucfirst($info->fieldName);
if (method_exists($this, $method)) {
return $this->{$method}($user, $args, $context, $info);
return $this->{$method}($value, $args, $context, $info);
} else {
return $user->{$info->fieldName};
return $value->{$info->fieldName};
}
}
];

View file

@ -35,11 +35,10 @@ try {
// Parse incoming query and variables
if (isset($_SERVER['CONTENT_TYPE']) && strpos($_SERVER['CONTENT_TYPE'], 'application/json') !== false) {
$raw = file_get_contents('php://input') ?: '';
$data = json_decode($raw, true) ?: [];
$data = json_decode($raw, true);
} else {
$data = $_REQUEST;
}
$data += ['query' => null, 'variables' => null];
if (null === $data['query']) {

View file

@ -18,7 +18,7 @@ try {
$query = $input['query'];
$variableValues = isset($input['variables']) ? $input['variables'] : null;
$result = GraphQL::executeQuery($schema, $query, $rootValue, null, $variableValues);
$result = GraphQL::execute($schema, $query, $rootValue, null, $variableValues);
} catch (\Exception $e) {
$result = [
'error' => [

View file

@ -1,12 +1,12 @@
<?php
interface Resolver {
public function resolve($rootValue, $args, $context);
public function resolve($root, $args, $context);
}
class Addition implements Resolver
{
public function resolve($rootValue, $args, $context)
public function resolve($root, $args, $context)
{
return $args['x'] + $args['y'];
}
@ -14,22 +14,22 @@ class Addition implements Resolver
class Echoer implements Resolver
{
public function resolve($rootValue, $args, $context)
public function resolve($root, $args, $context)
{
return $rootValue['prefix'].$args['message'];
return $root['prefix'].$args['message'];
}
}
return [
'sum' => function($rootValue, $args, $context) {
'sum' => function($root, $args, $context) {
$sum = new Addition();
return $sum->resolve($rootValue, $args, $context);
return $sum->resolve($root, $args, $context);
},
'echo' => function($rootValue, $args, $context) {
'echo' => function($root, $args, $context) {
$echo = new Echoer();
return $echo->resolve($rootValue, $args, $context);
return $echo->resolve($root, $args, $context);
},
'prefix' => 'You said: ',
];

View file

@ -10,10 +10,10 @@ php -S localhost:8080 ./graphql.php
### Try query
```
curl -d '{"query": "query { echo(message: \"Hello World\") }" }' -H "Content-Type: application/json" http://localhost:8080
curl http://localhost:8080 -d '{"query": "query { echo(message: \"Hello World\") }" }'
```
### Try mutation
```
curl -d '{"query": "mutation { sum(x: 2, y: 2) }" }' -H "Content-Type: application/json" http://localhost:8080
curl http://localhost:8080 -d '{"query": "mutation { sum(x: 2, y: 2) }" }'
```

View file

@ -19,8 +19,8 @@ try {
'args' => [
'message' => ['type' => Type::string()],
],
'resolve' => function ($rootValue, $args) {
return $rootValue['prefix'] . $args['message'];
'resolve' => function ($root, $args) {
return $root['prefix'] . $args['message'];
}
],
],
@ -35,7 +35,7 @@ try {
'x' => ['type' => Type::int()],
'y' => ['type' => Type::int()],
],
'resolve' => function ($calc, $args) {
'resolve' => function ($root, $args) {
return $args['x'] + $args['y'];
},
],

View file

@ -1,102 +0,0 @@
<?xml version="1.0"?>
<ruleset>
<arg name="basepath" value="." />
<arg name="extensions" value="php" />
<arg name="parallel" value="80" />
<arg name="cache" value=".phpcs-cache" />
<arg name="colors" />
<!-- Ignore warnings, show progress of the run and show sniff names -->
<arg value="nps" />
<file>src</file>
<file>tests</file>
<rule ref="Doctrine">
<!-- Disable PHP7+ features that might cause BC breaks for now -->
<exclude name="SlevomatCodingStandard.Classes.ClassConstantVisibility.MissingConstantVisibility" />
<exclude name="SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint" />
<exclude name="SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingReturnTypeHint" />
<!-- Enable when Slevomat starts supporting variadics, see https://github.com/slevomat/coding-standard/issues/251 -->
<exclude name="SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse" />
</rule>
<!--@api annotation is required for now -->
<rule ref="SlevomatCodingStandard.TypeHints.TypeHintDeclaration">
<properties>
<property
name="usefulAnnotations"
type="array"
value="
@after,
@afterClass,
@AfterMethods,
@api,
@Attribute,
@Attributes,
@before,
@beforeClass,
@BeforeMethods,
@covers,
@coversDefaultClass,
@coversNothing,
@dataProvider,
@depends,
@deprecated,
@doesNotPerformAssertions,
@Enum,
@expectedDeprecation,
@expectedException,
@expectedExceptionCode,
@expectedExceptionMessage,
@expectedExceptionMessageRegExp,
@group,
@Groups,
@IgnoreAnnotation,
@internal,
@Iterations,
@link,
@ODM\,
@ORM\,
@requires,
@Required,
@Revs,
@runInSeparateProcess,
@runTestsInSeparateProcesses,
@see,
@Target,
@test,
@throws,
@uses
"
/>
</properties>
</rule>
<rule ref="SlevomatCodingStandard.Commenting.ForbiddenAnnotations">
<properties>
<property
name="forbiddenAnnotations"
type="array"
value="
@author,
@category,
@copyright,
@created,
@license,
@package,
@since,
@subpackage,
@version
"
/>
</properties>
</rule>
<!-- IDEs sort by PSR12, Slevomat coding standard uses old sorting for BC -->
<rule ref="SlevomatCodingStandard.Namespaces.AlphabeticallySortedUses">
<properties>
<property name="psr12Compatible" type="bool" value="true" />
</properties>
</rule>
</ruleset>

View file

@ -1,17 +0,0 @@
parameters:
level: 1
paths:
- %currentWorkingDirectory%/src
- %currentWorkingDirectory%/tests
ignoreErrors:
- "~Construct empty\\(\\) is not allowed\\. Use more strict comparison~"
- "~(Method|Property) .+::.+(\\(\\))? (has parameter \\$\\w+ with no|has no return|has no) typehint specified~"
- "~Variable property access on .+~"
- "~Variable method call on static\\(GraphQL\\\\Server\\\\ServerConfig\\)~" # TODO get rid of
includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
- vendor/phpstan/phpstan-phpunit/rules.neon
- vendor/phpstan/phpstan-strict-rules/rules.neon

View file

@ -1,13 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="tests/bootstrap.php"
>
<php>
<ini name="error_reporting" value="E_ALL"/>
</php>
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="vendor/autoload.php"
>
<testsuites>
<testsuite name="webonyx/graphql-php Test Suite">
<directory>./tests/</directory>
@ -23,6 +26,21 @@
<filter>
<whitelist>
<directory suffix=".php">./src</directory>
<exclude>
<directory>./bin</directory>
<directory>./docs</directory>
<directory>./build</directory>
<directory>./tests</directory>
<directory>./vendor</directory>
<directory>./examples</directory>
<directory>./benchmarks</directory>
<file>./src/deprecated.php</file>
</exclude>
</whitelist>
</filter>
<php>
<ini name="error_reporting" value="E_ALL"/>
</php>
</phpunit>

View file

@ -1,64 +1,60 @@
<?php
declare(strict_types=1);
namespace GraphQL;
use Exception;
use GraphQL\Executor\Promise\Adapter\SyncPromise;
use SplQueue;
use Throwable;
class Deferred
{
/** @var SplQueue|null */
/**
* @var \SplQueue
*/
private static $queue;
/** @var callable */
/**
* @var callable
*/
private $callback;
/** @var SyncPromise */
/**
* @var SyncPromise
*/
public $promise;
public static function getQueue()
{
return self::$queue ?: self::$queue = new \SplQueue();
}
public static function runQueue()
{
$q = self::$queue;
while ($q && !$q->isEmpty()) {
/** @var self $dfd */
$dfd = $q->dequeue();
$dfd->run();
}
}
public function __construct(callable $callback)
{
$this->callback = $callback;
$this->promise = new SyncPromise();
$this->promise = new SyncPromise();
self::getQueue()->enqueue($this);
}
public static function getQueue() : SplQueue
{
if (self::$queue === null) {
self::$queue = new SplQueue();
}
return self::$queue;
}
public static function runQueue() : void
{
$queue = self::getQueue();
while (! $queue->isEmpty()) {
/** @var self $dequeuedNodeValue */
$dequeuedNodeValue = $queue->dequeue();
$dequeuedNodeValue->run();
}
}
public function then($onFulfilled = null, $onRejected = null)
{
return $this->promise->then($onFulfilled, $onRejected);
}
public function run() : void
private function run()
{
try {
$cb = $this->callback;
$this->promise->resolve($cb());
} catch (Exception $e) {
} catch (\Exception $e) {
$this->promise->reject($e);
} catch (Throwable $e) {
} catch (\Throwable $e) {
$this->promise->reject($e);
}
}

18
src/Error.php Normal file
View file

@ -0,0 +1,18 @@
<?php
namespace GraphQL;
trigger_error(
'GraphQL\Error was moved to GraphQL\Error\Error and will be deleted on next release',
E_USER_DEPRECATED
);
/**
* Class Error
*
* @deprecated as of 8.0 in favor of GraphQL\Error\Error
* @package GraphQL
*/
class Error extends \GraphQL\Error\Error
{
}

View file

@ -1,7 +1,4 @@
<?php
declare(strict_types=1);
namespace GraphQL\Error;
/**
@ -17,9 +14,8 @@ interface ClientAware
/**
* Returns true when exception message is safe to be displayed to a client.
*
* @return bool
*
* @api
* @return bool
*/
public function isClientSafe();
@ -28,9 +24,8 @@ interface ClientAware
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*
* @return string
*
* @api
* @return string
*/
public function getCategory();
}

View file

@ -1,7 +1,4 @@
<?php
declare(strict_types=1);
namespace GraphQL\Error;
/**
@ -9,8 +6,7 @@ namespace GraphQL\Error;
*/
class Debug
{
const INCLUDE_DEBUG_MESSAGE = 1;
const INCLUDE_TRACE = 2;
const INCLUDE_DEBUG_MESSAGE = 1;
const INCLUDE_TRACE = 2;
const RETHROW_INTERNAL_EXCEPTIONS = 4;
const RETHROW_UNSAFE_EXCEPTIONS = 8;
}

View file

@ -1,22 +1,9 @@
<?php
declare(strict_types=1);
namespace GraphQL\Error;
use Exception;
use GraphQL\Language\AST\Node;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
use GraphQL\Utils\Utils;
use JsonSerializable;
use Throwable;
use Traversable;
use function array_filter;
use function array_map;
use function array_values;
use function is_array;
use function iterator_to_array;
/**
* Describes an Error found during the parse, validate, or
@ -32,9 +19,9 @@ use function iterator_to_array;
* Class extends standard PHP `\Exception`, so all standard methods of base `\Exception` class
* are available in addition to those listed below.
*/
class Error extends Exception implements JsonSerializable, ClientAware
class Error extends \Exception implements \JsonSerializable, ClientAware
{
const CATEGORY_GRAPHQL = 'graphql';
const CATEGORY_GRAPHQL = 'graphql';
const CATEGORY_INTERNAL = 'internal';
/**
@ -44,103 +31,56 @@ class Error extends Exception implements JsonSerializable, ClientAware
*/
public $message;
/** @var SourceLocation[] */
/**
* @var SourceLocation[]
*/
private $locations;
/**
* An array describing the JSON-path into the execution response which
* corresponds to this error. Only included for errors during execution.
*
* @var mixed[]|null
* @var array
*/
public $path;
/**
* An array of GraphQL AST Nodes corresponding to this error.
*
* @var Node[]|null
* @var array
*/
public $nodes;
/**
* The source GraphQL document for the first location of this error.
*
* Note that if this Error represents more than one node, the source may not
* represent nodes after the first node.
* The source GraphQL document corresponding to this error.
*
* @var Source|null
*/
private $source;
/** @var int[]|null */
/**
* @var array
*/
private $positions;
/** @var bool */
/**
* @var bool
*/
private $isClientSafe;
/** @var string */
protected $category;
/** @var mixed[]|null */
protected $extensions;
/**
* @param string $message
* @param Node|Node[]|Traversable|null $nodes
* @param mixed[]|null $positions
* @param mixed[]|null $path
* @param Throwable $previous
* @param mixed[] $extensions
* @var string
*/
public function __construct(
$message,
$nodes = null,
?Source $source = null,
$positions = null,
$path = null,
$previous = null,
array $extensions = []
) {
parent::__construct($message, 0, $previous);
// Compute list of blame nodes.
if ($nodes instanceof Traversable) {
$nodes = iterator_to_array($nodes);
} elseif ($nodes && ! is_array($nodes)) {
$nodes = [$nodes];
}
$this->nodes = $nodes;
$this->source = $source;
$this->positions = $positions;
$this->path = $path;
$this->extensions = $extensions ?: (
$previous && $previous instanceof self
? $previous->extensions
: []
);
if ($previous instanceof ClientAware) {
$this->isClientSafe = $previous->isClientSafe();
$this->category = $previous->getCategory() ?: self::CATEGORY_INTERNAL;
} elseif ($previous) {
$this->isClientSafe = false;
$this->category = self::CATEGORY_INTERNAL;
} else {
$this->isClientSafe = true;
$this->category = self::CATEGORY_GRAPHQL;
}
}
protected $category;
/**
* Given an arbitrary Error, presumably thrown while attempting to execute a
* GraphQL operation, produce a new GraphQLError aware of the location in the
* document responsible for the original Error.
*
* @param mixed $error
* @param Node[]|null $nodes
* @param mixed[]|null $path
*
* @param $error
* @param array|null $nodes
* @param array|null $path
* @return Error
*/
public static function createLocatedError($error, $nodes = null, $path = null)
@ -148,24 +88,22 @@ class Error extends Exception implements JsonSerializable, ClientAware
if ($error instanceof self) {
if ($error->path && $error->nodes) {
return $error;
} else {
$nodes = $nodes ?: $error->nodes;
$path = $path ?: $error->path;
}
$nodes = $nodes ?: $error->nodes;
$path = $path ?: $error->path;
}
$source = $positions = $originalError = null;
$extensions = [];
$source = $positions = $originalError = null;
if ($error instanceof self) {
$message = $error->getMessage();
$message = $error->getMessage();
$originalError = $error;
$nodes = $error->nodes ?: $nodes;
$source = $error->source;
$positions = $error->positions;
$extensions = $error->extensions;
} elseif ($error instanceof Exception || $error instanceof Throwable) {
$message = $error->getMessage();
$nodes = $error->nodes ?: $nodes;
$source = $error->source;
$positions = $error->positions;
} else if ($error instanceof \Exception || $error instanceof \Throwable) {
$message = $error->getMessage();
$originalError = $error;
} else {
$message = (string) $error;
@ -177,19 +115,60 @@ class Error extends Exception implements JsonSerializable, ClientAware
$source,
$positions,
$path,
$originalError,
$extensions
$originalError
);
}
/**
* @return mixed[]
* @param Error $error
* @return array
*/
public static function formatError(Error $error)
{
return $error->toSerializableArray();
}
/**
* @param string $message
* @param array|null $nodes
* @param Source $source
* @param array|null $positions
* @param array|null $path
* @param \Throwable $previous
*/
public function __construct(
$message,
$nodes = null,
Source $source = null,
$positions = null,
$path = null,
$previous = null
)
{
parent::__construct($message, 0, $previous);
if ($nodes instanceof \Traversable) {
$nodes = iterator_to_array($nodes);
}
$this->nodes = $nodes;
$this->source = $source;
$this->positions = $positions;
$this->path = $path;
if ($previous instanceof ClientAware) {
$this->isClientSafe = $previous->isClientSafe();
$this->category = $previous->getCategory() ?: static::CATEGORY_INTERNAL;
} else if ($previous) {
$this->isClientSafe = false;
$this->category = static::CATEGORY_INTERNAL;
} else {
$this->isClientSafe = true;
$this->category = static::CATEGORY_GRAPHQL;
}
}
/**
* @inheritdoc
*/
@ -211,38 +190,29 @@ class Error extends Exception implements JsonSerializable, ClientAware
*/
public function getSource()
{
if ($this->source === null) {
if (! empty($this->nodes[0]) && ! empty($this->nodes[0]->loc)) {
if (null === $this->source) {
if (!empty($this->nodes[0]) && !empty($this->nodes[0]->loc)) {
$this->source = $this->nodes[0]->loc->source;
}
}
return $this->source;
}
/**
* @return int[]
* @return array
*/
public function getPositions()
{
if ($this->positions === null && ! empty($this->nodes)) {
$positions = array_map(
static function ($node) {
if (null === $this->positions) {
if (!empty($this->nodes)) {
$positions = array_map(function($node) {
return isset($node->loc) ? $node->loc->start : null;
},
$this->nodes
);
$positions = array_filter(
$positions,
static function ($p) {
}, $this->nodes);
$this->positions = array_filter($positions, function($p) {
return $p !== null;
}
);
$this->positions = array_values($positions);
});
}
}
return $this->positions;
}
@ -257,36 +227,19 @@ class Error extends Exception implements JsonSerializable, ClientAware
* point out to field mentioned in multiple fragments. Errors during execution include a
* single location, the field which produced the error.
*
* @return SourceLocation[]
*
* @api
* @return SourceLocation[]
*/
public function getLocations()
{
if ($this->locations === null) {
if (null === $this->locations) {
$positions = $this->getPositions();
$source = $this->getSource();
$nodes = $this->nodes;
$source = $this->getSource();
if ($positions && $source) {
$this->locations = array_map(
static function ($pos) use ($source) {
return $source->getLocation($pos);
},
$positions
);
} elseif ($nodes) {
$locations = array_filter(
array_map(
static function ($node) {
if ($node->loc && $node->loc->source) {
return $node->loc->source->getLocation($node->loc->start);
}
},
$nodes
)
);
$this->locations = array_values($locations);
$this->locations = array_map(function ($pos) use ($source) {
return $source->getLocation($pos);
}, $positions);
} else {
$this->locations = [];
}
@ -295,86 +248,53 @@ class Error extends Exception implements JsonSerializable, ClientAware
return $this->locations;
}
/**
* @return Node[]|null
*/
public function getNodes()
{
return $this->nodes;
}
/**
* Returns an array describing the path from the root value to the field which produced this error.
* Only included for execution errors.
*
* @return mixed[]|null
*
* @api
* @return array|null
*/
public function getPath()
{
return $this->path;
}
/**
* @return mixed[]
*/
public function getExtensions()
{
return $this->extensions;
}
/**
* Returns array representation of error suitable for serialization
*
* @deprecated Use FormattedError::createFromException() instead
*
* @return mixed[]
* @return array
*/
public function toSerializableArray()
{
$arr = [
'message' => $this->getMessage(),
'message' => $this->getMessage()
];
$locations = Utils::map(
$this->getLocations(),
static function (SourceLocation $loc) {
return $loc->toSerializableArray();
}
);
$locations = Utils::map($this->getLocations(), function(SourceLocation $loc) {
return $loc->toSerializableArray();
});
if (! empty($locations)) {
if (!empty($locations)) {
$arr['locations'] = $locations;
}
if (! empty($this->path)) {
if (!empty($this->path)) {
$arr['path'] = $this->path;
}
if (! empty($this->extensions)) {
$arr['extensions'] = $this->extensions;
}
return $arr;
}
/**
* Specify data which should be serialized to JSON
*
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
*
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize()
function jsonSerialize()
{
return $this->toSerializableArray();
}
/**
* @return string
*/
public function __toString()
{
return FormattedError::printError($this);
}
}

View file

@ -1,39 +1,10 @@
<?php
declare(strict_types=1);
namespace GraphQL\Error;
use Countable;
use ErrorException;
use Exception;
use GraphQL\Language\AST\Node;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\WrappingType;
use GraphQL\Utils\Utils;
use Throwable;
use function addcslashes;
use function array_filter;
use function array_intersect_key;
use function array_map;
use function array_merge;
use function array_shift;
use function count;
use function get_class;
use function gettype;
use function implode;
use function is_array;
use function is_bool;
use function is_object;
use function is_scalar;
use function is_string;
use function mb_strlen;
use function preg_split;
use function sprintf;
use function str_repeat;
use function strlen;
/**
* This class is used for [default error formatting](error-handling.md).
@ -42,119 +13,20 @@ use function strlen;
*/
class FormattedError
{
/** @var string */
private static $internalErrorMessage = 'Internal server error';
/**
* Set default error message for internal errors formatted using createFormattedError().
* This value can be overridden by passing 3rd argument to `createFormattedError()`.
*
* @param string $msg
*
* @api
* @param string $msg
*/
public static function setInternalErrorMessage($msg)
{
self::$internalErrorMessage = $msg;
}
/**
* Prints a GraphQLError to a string, representing useful location information
* about the error's position in the source.
*
* @return string
*/
public static function printError(Error $error)
{
$printedLocations = [];
if ($error->nodes) {
/** @var Node $node */
foreach ($error->nodes as $node) {
if (! $node->loc) {
continue;
}
if ($node->loc->source === null) {
continue;
}
$printedLocations[] = self::highlightSourceAtLocation(
$node->loc->source,
$node->loc->source->getLocation($node->loc->start)
);
}
} elseif ($error->getSource() && $error->getLocations()) {
$source = $error->getSource();
foreach ($error->getLocations() as $location) {
$printedLocations[] = self::highlightSourceAtLocation($source, $location);
}
}
return ! $printedLocations
? $error->getMessage()
: implode("\n\n", array_merge([$error->getMessage()], $printedLocations)) . "\n";
}
/**
* Render a helpful description of the location of the error in the GraphQL
* Source document.
*
* @return string
*/
private static function highlightSourceAtLocation(Source $source, SourceLocation $location)
{
$line = $location->line;
$lineOffset = $source->locationOffset->line - 1;
$columnOffset = self::getColumnOffset($source, $location);
$contextLine = $line + $lineOffset;
$contextColumn = $location->column + $columnOffset;
$prevLineNum = (string) ($contextLine - 1);
$lineNum = (string) $contextLine;
$nextLineNum = (string) ($contextLine + 1);
$padLen = strlen($nextLineNum);
$lines = preg_split('/\r\n|[\n\r]/', $source->body);
$lines[0] = self::whitespace($source->locationOffset->column - 1) . $lines[0];
$outputLines = [
sprintf('%s (%s:%s)', $source->name, $contextLine, $contextColumn),
$line >= 2 ? (self::lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2]) : null,
self::lpad($padLen, $lineNum) . ': ' . $lines[$line - 1],
self::whitespace(2 + $padLen + $contextColumn - 1) . '^',
$line < count($lines) ? self::lpad($padLen, $nextLineNum) . ': ' . $lines[$line] : null,
];
return implode("\n", array_filter($outputLines));
}
/**
* @return int
*/
private static function getColumnOffset(Source $source, SourceLocation $location)
{
return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
}
/**
* @param int $len
*
* @return string
*/
private static function whitespace($len)
{
return str_repeat(' ', $len);
}
/**
* @param int $len
*
* @return string
*/
private static function lpad($len, $str)
{
return self::whitespace($len - mb_strlen($str)) . $str;
}
/**
* Standard GraphQL error formatter. Converts any exception to array
* conforming to GraphQL spec.
@ -164,21 +36,18 @@ class FormattedError
*
* For a list of available debug flags see GraphQL\Error\Debug constants.
*
* @param Throwable $e
* @param bool|int $debug
* @param string $internalErrorMessage
*
* @return mixed[]
*
* @throws Throwable
*
* @api
* @param \Throwable $e
* @param bool|int $debug
* @param string $internalErrorMessage
* @return array
* @throws \Throwable
*/
public static function createFromException($e, $debug = false, $internalErrorMessage = null)
{
Utils::invariant(
$e instanceof Exception || $e instanceof Throwable,
'Expected exception, got %s',
$e instanceof \Exception || $e instanceof \Throwable,
"Expected exception, got %s",
Utils::getVariableType($e)
);
@ -186,36 +55,27 @@ class FormattedError
if ($e instanceof ClientAware) {
$formattedError = [
'message' => $e->isClientSafe() ? $e->getMessage() : $internalErrorMessage,
'extensions' => [
'category' => $e->getCategory(),
],
'message' => $e->isClientSafe() ? $e->getMessage() : $internalErrorMessage,
'category' => $e->getCategory()
];
} else {
$formattedError = [
'message' => $internalErrorMessage,
'extensions' => [
'category' => Error::CATEGORY_INTERNAL,
],
'message' => $internalErrorMessage,
'category' => Error::CATEGORY_INTERNAL
];
}
if ($e instanceof Error) {
$locations = Utils::map(
$e->getLocations(),
static function (SourceLocation $loc) {
return $loc->toSerializableArray();
}
);
if (! empty($locations)) {
$locations = Utils::map($e->getLocations(), function(SourceLocation $loc) {
return $loc->toSerializableArray();
});
if (!empty($locations)) {
$formattedError['locations'] = $locations;
}
if (! empty($e->path)) {
if (!empty($e->path)) {
$formattedError['path'] = $e->path;
}
if (! empty($e->getExtensions())) {
$formattedError['extensions'] = $e->getExtensions() + $formattedError['extensions'];
}
}
if ($debug) {
@ -229,67 +89,56 @@ class FormattedError
* Decorates spec-compliant $formattedError with debug entries according to $debug flags
* (see GraphQL\Error\Debug for available flags)
*
* @param mixed[] $formattedError
* @param Throwable $e
* @param bool|int $debug
*
* @return mixed[]
*
* @throws Throwable
* @param array $formattedError
* @param \Throwable $e
* @param bool $debug
* @return array
* @throws \Throwable
*/
public static function addDebugEntries(array $formattedError, $e, $debug)
{
if (! $debug) {
if (!$debug) {
return $formattedError;
}
Utils::invariant(
$e instanceof Exception || $e instanceof Throwable,
'Expected exception, got %s',
$e instanceof \Exception || $e instanceof \Throwable,
"Expected exception, got %s",
Utils::getVariableType($e)
);
$debug = (int) $debug;
if ($debug & Debug::RETHROW_INTERNAL_EXCEPTIONS) {
if (! $e instanceof Error) {
if (!$e instanceof Error) {
throw $e;
}
if ($e->getPrevious()) {
} else if ($e->getPrevious()) {
throw $e->getPrevious();
}
}
$isUnsafe = ! $e instanceof ClientAware || ! $e->isClientSafe();
$isInternal = !$e instanceof ClientAware || !$e->isClientSafe();
if (($debug & Debug::RETHROW_UNSAFE_EXCEPTIONS) && $isUnsafe) {
if ($e->getPrevious()) {
throw $e->getPrevious();
}
}
if (($debug & Debug::INCLUDE_DEBUG_MESSAGE) && $isUnsafe) {
if (($debug & Debug::INCLUDE_DEBUG_MESSAGE) && $isInternal) {
// Displaying debugMessage as a first entry:
$formattedError = ['debugMessage' => $e->getMessage()] + $formattedError;
}
if ($debug & Debug::INCLUDE_TRACE) {
if ($e instanceof ErrorException || $e instanceof \Error) {
if ($e instanceof \ErrorException || $e instanceof \Error) {
$formattedError += [
'file' => $e->getFile(),
'line' => $e->getLine(),
];
}
$isTrivial = $e instanceof Error && ! $e->getPrevious();
$isTrivial = $e instanceof Error && !$e->getPrevious();
if (! $isTrivial) {
$debugging = $e->getPrevious() ?: $e;
if (!$isTrivial) {
$debugging = $e->getPrevious() ?: $e;
$formattedError['trace'] = static::toSafeTrace($debugging);
}
}
return $formattedError;
}
@ -297,71 +146,67 @@ class FormattedError
* Prepares final error formatter taking in account $debug flags.
* If initial formatter is not set, FormattedError::createFromException is used
*
* @param bool|int $debug
*
* @return callable|callable
* @param callable|null $formatter
* @param $debug
* @return callable|\Closure
*/
public static function prepareFormatter(?callable $formatter = null, $debug)
public static function prepareFormatter(callable $formatter = null, $debug)
{
$formatter = $formatter ?: static function ($e) {
$formatter = $formatter ?: function($e) {
return FormattedError::createFromException($e);
};
if ($debug) {
$formatter = static function ($e) use ($formatter, $debug) {
$formatter = function($e) use ($formatter, $debug) {
return FormattedError::addDebugEntries($formatter($e), $e, $debug);
};
}
return $formatter;
}
/**
* Returns error trace as serializable array
*
* @param Throwable $error
*
* @return mixed[]
*
* @api
* @param \Throwable $error
* @return array
*/
public static function toSafeTrace($error)
{
$trace = $error->getTrace();
if (isset($trace[0]['function']) && isset($trace[0]['class']) &&
// Remove invariant entries as they don't provide much value:
($trace[0]['class'] . '::' . $trace[0]['function'] === 'GraphQL\Utils\Utils::invariant')) {
array_shift($trace);
} elseif (! isset($trace[0]['file'])) {
// Remove root call as it's likely error handler trace:
// Remove invariant entries as they don't provide much value:
if (
isset($trace[0]['function']) && isset($trace[0]['class']) &&
('GraphQL\Utils\Utils::invariant' === $trace[0]['class'].'::'.$trace[0]['function'])) {
array_shift($trace);
}
return array_map(
static function ($err) {
$safeErr = array_intersect_key($err, ['file' => true, 'line' => true]);
// Remove root call as it's likely error handler trace:
else if (!isset($trace[0]['file'])) {
array_shift($trace);
}
if (isset($err['function'])) {
$func = $err['function'];
$args = ! empty($err['args']) ? array_map([self::class, 'printVar'], $err['args']) : [];
$funcStr = $func . '(' . implode(', ', $args) . ')';
return array_map(function($err) {
$safeErr = array_intersect_key($err, ['file' => true, 'line' => true]);
if (isset($err['class'])) {
$safeErr['call'] = $err['class'] . '::' . $funcStr;
} else {
$safeErr['function'] = $funcStr;
}
if (isset($err['function'])) {
$func = $err['function'];
$args = !empty($err['args']) ? array_map([__CLASS__, 'printVar'], $err['args']) : [];
$funcStr = $func . '(' . implode(", ", $args) . ')';
if (isset($err['class'])) {
$safeErr['call'] = $err['class'] . '::' . $funcStr;
} else {
$safeErr['function'] = $funcStr;
}
}
return $safeErr;
},
$trace
);
return $safeErr;
}, $trace);
}
/**
* @param mixed $var
*
* @param $var
* @return string
*/
public static function printVar($var)
@ -371,17 +216,16 @@ class FormattedError
if ($var instanceof WrappingType) {
$var = $var->getWrappedType(true);
}
return 'GraphQLType: ' . $var->name;
}
if (is_object($var)) {
return 'instance of ' . get_class($var) . ($var instanceof Countable ? '(' . count($var) . ')' : '');
return 'instance of ' . get_class($var) . ($var instanceof \Countable ? '(' . count($var) . ')' : '');
}
if (is_array($var)) {
return 'array(' . count($var) . ')';
}
if ($var === '') {
if ('' === $var) {
return '(empty string)';
}
if (is_string($var)) {
@ -393,48 +237,42 @@ class FormattedError
if (is_scalar($var)) {
return $var;
}
if ($var === null) {
if (null === $var) {
return 'null';
}
return gettype($var);
}
/**
* @deprecated as of v0.8.0
*
* @param string $error
* @param $error
* @param SourceLocation[] $locations
*
* @return mixed[]
* @return array
*/
public static function create($error, array $locations = [])
{
$formatted = ['message' => $error];
$formatted = [
'message' => $error
];
if (! empty($locations)) {
$formatted['locations'] = array_map(
static function ($loc) {
return $loc->toArray();
},
$locations
);
if (!empty($locations)) {
$formatted['locations'] = array_map(function($loc) { return $loc->toArray();}, $locations);
}
return $formatted;
}
/**
* @param \ErrorException $e
* @deprecated as of v0.10.0, use general purpose method createFromException() instead
*
* @return mixed[]
* @return array
*/
public static function createFromPHPError(ErrorException $e)
public static function createFromPHPError(\ErrorException $e)
{
return [
'message' => $e->getMessage(),
'message' => $e->getMessage(),
'severity' => $e->getSeverity(),
'trace' => self::toSafeTrace($e),
'trace' => self::toSafeTrace($e)
];
}
}

View file

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

View file

@ -1,25 +1,50 @@
<?php
declare(strict_types=1);
namespace GraphQL\Error;
use GraphQL\Language\Source;
use function sprintf;
use GraphQL\Language\SourceLocation;
class SyntaxError extends Error
{
/**
* @param int $position
* @param Source $source
* @param int $position
* @param string $description
*/
public function __construct(Source $source, $position, $description)
{
parent::__construct(
sprintf('Syntax Error: %s', $description),
null,
$source,
[$position]
);
$location = $source->getLocation($position);
$syntaxError =
"Syntax Error {$source->name} ({$location->line}:{$location->column}) $description\n\n" .
self::highlightSourceAtLocation($source, $location);
parent::__construct($syntaxError, null, $source, [$position]);
}
/**
* @param Source $source
* @param SourceLocation $location
* @return string
*/
public static function highlightSourceAtLocation(Source $source, SourceLocation $location)
{
$line = $location->line;
$prevLineNum = (string) ($line - 1);
$lineNum = (string) $line;
$nextLineNum = (string) ($line + 1);
$padLen = mb_strlen($nextLineNum, 'UTF-8');
$unicodeChars = json_decode('"\u2028\u2029"'); // Quick hack to get js-compatible representation of these chars
$lines = preg_split('/\r\n|[\n\r' . $unicodeChars . ']/su', $source->body);
$lpad = function($len, $str) {
return str_pad($str, $len - mb_strlen($str, 'UTF-8') + 1, ' ', STR_PAD_LEFT);
};
return
($line >= 2 ? $lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2] . "\n" : '') .
($lpad($padLen, $lineNum) . ': ' . $lines[$line - 1] . "\n") .
(str_repeat(' ', 1 + $padLen + $location->column) . "^\n") .
($line < count($lines) ? $lpad($padLen, $nextLineNum) . ': ' . $lines[$line] . "\n" : '');
}
}

View file

@ -1,15 +1,14 @@
<?php
declare(strict_types=1);
namespace GraphQL\Error;
use RuntimeException;
/**
* Class UserError
*
* Error caused by actions of GraphQL clients. Can be safely displayed to a client...
*
* @package GraphQL\Error
*/
class UserError extends RuntimeException implements ClientAware
class UserError extends \RuntimeException implements ClientAware
{
/**
* @return bool

View file

@ -1,14 +1,6 @@
<?php
declare(strict_types=1);
namespace GraphQL\Error;
use GraphQL\Exception\InvalidArgument;
use function is_int;
use function trigger_error;
use const E_USER_WARNING;
/**
* Encapsulates warnings produced by the library.
*
@ -17,29 +9,28 @@ use const E_USER_WARNING;
*/
final class Warning
{
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;
const WARNING_NAME = 1;
const WARNING_ASSIGN = 2;
const WARNING_CONFIG = 4;
const WARNING_FULL_SCHEMA_SCAN = 8;
const WARNING_CONFIG_DEPRECATION = 16;
const WARNING_NOT_A_TYPE = 32;
const ALL = 63;
/** @var int */
private static $enableWarnings = self::ALL;
static $enableWarnings = self::ALL;
/** @var mixed[] */
private static $warned = [];
static $warned = [];
/** @var callable|null */
private static $warningHandler;
static private $warningHandler;
/**
* Sets warning handler which can intercept all system warnings.
* When not set, trigger_error() is used to notify about warnings.
*
* @api
* @param callable|null $warningHandler
*/
public static function setWarningHandler(?callable $warningHandler = null) : void
public static function setWarningHandler(callable $warningHandler = null)
{
self::$warningHandler = $warningHandler;
}
@ -52,20 +43,18 @@ final class Warning
*
* When passing true - suppresses all warnings.
*
* @param bool|int $suppress
*
* @api
* @param bool|int $suppress
*/
public static function suppress($suppress = true) : void
static function suppress($suppress = true)
{
if ($suppress === true) {
if (true === $suppress) {
self::$enableWarnings = 0;
} elseif ($suppress === false) {
} else if (false === $suppress) {
self::$enableWarnings = self::ALL;
} elseif (is_int($suppress)) {
self::$enableWarnings &= ~$suppress;
} else {
throw InvalidArgument::fromExpectedTypeAndArgument('bool|int', $suppress);
$suppress = (int) $suppress;
self::$enableWarnings &= ~$suppress;
}
}
@ -77,40 +66,38 @@ final class Warning
*
* When passing true - re-enables all warnings.
*
* @param bool|int $enable
*
* @api
* @param bool|int $enable
*/
public static function enable($enable = true) : void
public static function enable($enable = true)
{
if ($enable === true) {
if (true === $enable) {
self::$enableWarnings = self::ALL;
} elseif ($enable === false) {
} else if (false === $enable) {
self::$enableWarnings = 0;
} elseif (is_int($enable)) {
self::$enableWarnings |= $enable;
} else {
throw InvalidArgument::fromExpectedTypeAndArgument('bool|int', $enable);
$enable = (int) $enable;
self::$enableWarnings |= $enable;
}
}
public static function warnOnce(string $errorMessage, int $warningId, ?int $messageLevel = null) : void
static function warnOnce($errorMessage, $warningId, $messageLevel = null)
{
if (self::$warningHandler !== null) {
if (self::$warningHandler) {
$fn = self::$warningHandler;
$fn($errorMessage, $warningId);
} elseif ((self::$enableWarnings & $warningId) > 0 && ! isset(self::$warned[$warningId])) {
} else if ((self::$enableWarnings & $warningId) > 0 && !isset(self::$warned[$warningId])) {
self::$warned[$warningId] = true;
trigger_error($errorMessage, $messageLevel ?: E_USER_WARNING);
}
}
public static function warn(string $errorMessage, int $warningId, ?int $messageLevel = null) : void
static function warn($errorMessage, $warningId, $messageLevel = null)
{
if (self::$warningHandler !== null) {
if (self::$warningHandler) {
$fn = self::$warningHandler;
$fn($errorMessage, $warningId);
} elseif ((self::$enableWarnings & $warningId) > 0) {
} else if ((self::$enableWarnings & $warningId) > 0) {
trigger_error($errorMessage, $messageLevel ?: E_USER_WARNING);
}
}

View file

@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace GraphQL\Exception;
use InvalidArgumentException;
use function gettype;
use function sprintf;
final class InvalidArgument extends InvalidArgumentException
{
/**
* @param mixed $argument
*/
public static function fromExpectedTypeAndArgument(string $expectedType, $argument) : self
{
return new self(sprintf('Expected type "%s", got "%s"', $expectedType, gettype($argument)));
}
}

View file

@ -1,11 +1,7 @@
<?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;
@ -14,65 +10,78 @@ 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.
* and the fragments defined in the query document
*
* @internal
*/
class ExecutionContext
{
/** @var Schema */
/**
* @var Schema
*/
public $schema;
/** @var FragmentDefinitionNode[] */
/**
* @var FragmentDefinitionNode[]
*/
public $fragments;
/** @var mixed */
/**
* @var mixed
*/
public $rootValue;
/** @var mixed */
/**
* @var mixed
*/
public $contextValue;
/** @var OperationDefinitionNode */
/**
* @var OperationDefinitionNode
*/
public $operation;
/** @var mixed[] */
/**
* @var array
*/
public $variableValues;
/** @var callable */
/**
* @var callable
*/
public $fieldResolver;
/** @var Error[] */
/**
* @var array
*/
public $errors;
/** @var PromiseAdapter */
public $promiseAdapter;
public function __construct(
$schema,
$fragments,
$rootValue,
$root,
$contextValue,
$operation,
$variableValues,
$variables,
$errors,
$fieldResolver,
$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->promiseAdapter = $promiseAdapter;
)
{
$this->schema = $schema;
$this->fragments = $fragments;
$this->rootValue = $root;
$this->contextValue = $contextValue;
$this->operation = $operation;
$this->variableValues = $variables;
$this->errors = $errors ?: [];
$this->fieldResolver = $fieldResolver;
$this->promises = $promiseAdapter;
}
public function addError(Error $error)
{
$this->errors[] = $error;
return $this;
}
}

View file

@ -1,13 +1,8 @@
<?php
declare(strict_types=1);
namespace GraphQL\Executor;
use GraphQL\Error\Error;
use GraphQL\Error\FormattedError;
use JsonSerializable;
use function array_map;
/**
* Returned after [query execution](executing-queries.md).
@ -17,13 +12,13 @@ use function array_map;
* Could be converted to [spec-compliant](https://facebook.github.io/graphql/#sec-Response-Format)
* serializable array using `toArray()`
*/
class ExecutionResult implements JsonSerializable
class ExecutionResult implements \JsonSerializable
{
/**
* Data collected from resolvers during query execution
*
* @api
* @var mixed[]
* @var array
*/
public $data;
@ -34,34 +29,38 @@ class ExecutionResult implements JsonSerializable
* contain original exception.
*
* @api
* @var Error[]
* @var \GraphQL\Error\Error[]
*/
public $errors;
/**
* User-defined serializable array of extensions included in serialized result.
* Conforms to
*
* @api
* @var mixed[]
* @var array
*/
public $extensions;
/** @var callable */
/**
* @var callable
*/
private $errorFormatter;
/** @var callable */
/**
* @var callable
*/
private $errorsHandler;
/**
* @param mixed[] $data
* @param Error[] $errors
* @param mixed[] $extensions
* @param array $data
* @param array $errors
* @param array $extensions
*/
public function __construct($data = null, array $errors = [], array $extensions = [])
public function __construct(array $data = null, array $errors = [], array $extensions = [])
{
$this->data = $data;
$this->errors = $errors;
$this->data = $data;
$this->errors = $errors;
$this->extensions = $extensions;
}
@ -78,14 +77,13 @@ class ExecutionResult implements JsonSerializable
* // ... other keys
* );
*
* @return self
*
* @api
* @param callable $errorFormatter
* @return $this
*/
public function setErrorFormatter(callable $errorFormatter)
{
$this->errorFormatter = $errorFormatter;
return $this;
}
@ -99,25 +97,16 @@ class ExecutionResult implements JsonSerializable
* return array_map($formatter, $errors);
* }
*
* @return self
*
* @api
* @param callable $handler
* @return $this
*/
public function setErrorsHandler(callable $handler)
{
$this->errorsHandler = $handler;
return $this;
}
/**
* @return mixed[]
*/
public function jsonSerialize()
{
return $this->toArray();
}
/**
* Converts GraphQL query result to spec-compliant serializable array using provided
* errors handler and formatter.
@ -128,35 +117,42 @@ class ExecutionResult implements JsonSerializable
* $debug argument must be either bool (only adds "debugMessage" to result) or sum of flags from
* GraphQL\Error\Debug
*
* @param bool|int $debug
*
* @return mixed[]
*
* @api
* @param bool|int $debug
* @return array
*/
public function toArray($debug = false)
{
$result = [];
if (! empty($this->errors)) {
$errorsHandler = $this->errorsHandler ?: static function (array $errors, callable $formatter) {
if (!empty($this->errors)) {
$errorsHandler = $this->errorsHandler ?: function(array $errors, callable $formatter) {
return array_map($formatter, $errors);
};
$result['errors'] = $errorsHandler(
$this->errors,
FormattedError::prepareFormatter($this->errorFormatter, $debug)
);
}
if ($this->data !== null) {
if (null !== $this->data) {
$result['data'] = $this->data;
}
if (! empty($this->extensions)) {
$result['extensions'] = $this->extensions;
if (!empty($this->extensions)) {
$result['extensions'] = (array) $this->extensions;
}
return $result;
}
/**
* Part of \JsonSerializable interface
*
* @return array
*/
public function jsonSerialize()
{
return $this->toArray();
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,15 +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,7 +1,4 @@
<?php
declare(strict_types=1);
namespace GraphQL\Executor\Promise\Adapter;
use GraphQL\Executor\Promise\Promise;
@ -9,9 +6,6 @@ use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Utils\Utils;
use React\Promise\Promise as ReactPromise;
use React\Promise\PromiseInterface as ReactPromiseInterface;
use function React\Promise\all;
use function React\Promise\reject;
use function React\Promise\resolve;
class ReactPromiseAdapter implements PromiseAdapter
{
@ -34,11 +28,10 @@ class ReactPromiseAdapter implements PromiseAdapter
/**
* @inheritdoc
*/
public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null)
public function then(Promise $promise, callable $onFulfilled = null, callable $onRejected = null)
{
/** @var ReactPromiseInterface $adoptedPromise */
/** @var $adoptedPromise ReactPromiseInterface */
$adoptedPromise = $promise->adoptedPromise;
return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this);
}
@ -48,7 +41,6 @@ class ReactPromiseAdapter implements PromiseAdapter
public function create(callable $resolver)
{
$promise = new ReactPromise($resolver);
return new Promise($promise, $this);
}
@ -57,8 +49,7 @@ class ReactPromiseAdapter implements PromiseAdapter
*/
public function createFulfilled($value = null)
{
$promise = resolve($value);
$promise = \React\Promise\resolve($value);
return new Promise($promise, $this);
}
@ -67,8 +58,7 @@ class ReactPromiseAdapter implements PromiseAdapter
*/
public function createRejected($reason)
{
$promise = reject($reason);
$promise = \React\Promise\reject($reason);
return new Promise($promise, $this);
}
@ -78,14 +68,11 @@ class ReactPromiseAdapter implements PromiseAdapter
public function all(array $promisesOrValues)
{
// TODO: rework with generators when PHP minimum required version is changed to 5.5+
$promisesOrValues = Utils::map(
$promisesOrValues,
static function ($item) {
return $item instanceof Promise ? $item->adoptedPromise : $item;
}
);
$promisesOrValues = Utils::map($promisesOrValues, function ($item) {
return $item instanceof Promise ? $item->adoptedPromise : $item;
});
$promise = all($promisesOrValues)->then(static function ($values) use ($promisesOrValues) {
$promise = \React\Promise\all($promisesOrValues)->then(function($values) use ($promisesOrValues) {
$orderedResults = [];
foreach ($promisesOrValues as $key => $value) {
@ -94,7 +81,6 @@ class ReactPromiseAdapter implements PromiseAdapter
return $orderedResults;
});
return new Promise($promise, $this);
}
}

View file

@ -1,164 +1,117 @@
<?php
declare(strict_types=1);
namespace GraphQL\Executor\Promise\Adapter;
use Exception;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Utils\Utils;
use SplQueue;
use Throwable;
use function is_object;
use function method_exists;
/**
* Class SyncPromise
*
* Simplistic (yet full-featured) implementation of Promises A+ spec for regular PHP `sync` mode
* (using queue to defer promises execution)
*
* @package GraphQL\Executor\Promise\Adapter
*/
class SyncPromise
{
const PENDING = 'pending';
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
/** @var SplQueue */
public static $queue;
/** @var string */
public $state = self::PENDING;
/** @var ExecutionResult|Throwable */
public $result;
const REJECTED = 'rejected';
/**
* Promises created in `then` method of this promise and awaiting for resolution of this promise
*
* @var mixed[][]
* @var \SplQueue
*/
private $waiting = [];
public static $queue;
public static function runQueue() : void
public static function getQueue()
{
return self::$queue ?: self::$queue = new \SplQueue();
}
public static function runQueue()
{
$q = self::$queue;
while ($q !== null && ! $q->isEmpty()) {
while ($q && !$q->isEmpty()) {
$task = $q->dequeue();
$task();
}
}
public function resolve($value) : self
public $state = self::PENDING;
public $result;
/**
* Promises created in `then` method of this promise and awaiting for resolution of this promise
* @var array
*/
private $waiting = [];
public function reject($reason)
{
switch ($this->state) {
case self::PENDING:
if ($value === $this) {
throw new Exception('Cannot resolve promise with self');
}
if (is_object($value) && method_exists($value, 'then')) {
$value->then(
function ($resolvedValue) {
$this->resolve($resolvedValue);
},
function ($reason) {
$this->reject($reason);
}
);
return $this;
}
$this->state = self::FULFILLED;
$this->result = $value;
$this->enqueueWaitingPromises();
break;
case self::FULFILLED:
if ($this->result !== $value) {
throw new Exception('Cannot change value of fulfilled promise');
}
break;
case self::REJECTED:
throw new Exception('Cannot resolve rejected promise');
}
return $this;
}
public function reject($reason) : self
{
if (! $reason instanceof Exception && ! $reason instanceof Throwable) {
throw new Exception('SyncPromise::reject() has to be called with an instance of \Throwable');
if (!$reason instanceof \Exception && !$reason instanceof \Throwable) {
throw new \Exception('SyncPromise::reject() has to be called with an instance of \Throwable');
}
switch ($this->state) {
case self::PENDING:
$this->state = self::REJECTED;
$this->state = self::REJECTED;
$this->result = $reason;
$this->enqueueWaitingPromises();
break;
case self::REJECTED:
if ($reason !== $this->result) {
throw new Exception('Cannot change rejection reason');
throw new \Exception("Cannot change rejection reason");
}
break;
case self::FULFILLED:
throw new Exception('Cannot reject fulfilled promise');
throw new \Exception("Cannot reject fulfilled promise");
}
return $this;
}
private function enqueueWaitingPromises() : void
public function resolve($value)
{
Utils::invariant(
$this->state !== self::PENDING,
'Cannot enqueue derived promises when parent is still pending'
);
foreach ($this->waiting as $descriptor) {
self::getQueue()->enqueue(function () use ($descriptor) {
/** @var self $promise */
[$promise, $onFulfilled, $onRejected] = $descriptor;
if ($this->state === self::FULFILLED) {
try {
$promise->resolve($onFulfilled === null ? $this->result : $onFulfilled($this->result));
} catch (Exception $e) {
$promise->reject($e);
} catch (Throwable $e) {
$promise->reject($e);
}
} elseif ($this->state === self::REJECTED) {
try {
if ($onRejected === null) {
$promise->reject($this->result);
} else {
$promise->resolve($onRejected($this->result));
}
} catch (Exception $e) {
$promise->reject($e);
} catch (Throwable $e) {
$promise->reject($e);
}
switch ($this->state) {
case self::PENDING:
if ($value === $this) {
throw new \Exception("Cannot resolve promise with self");
}
});
if (is_object($value) && method_exists($value, 'then')) {
$value->then(
function($resolvedValue) {
$this->resolve($resolvedValue);
},
function($reason) {
$this->reject($reason);
}
);
return $this;
}
$this->state = self::FULFILLED;
$this->result = $value;
$this->enqueueWaitingPromises();
break;
case self::FULFILLED:
if ($this->result !== $value) {
throw new \Exception("Cannot change value of fulfilled promise");
}
break;
case self::REJECTED:
throw new \Exception("Cannot resolve rejected promise");
}
$this->waiting = [];
return $this;
}
public static function getQueue() : SplQueue
public function then(callable $onFulfilled = null, callable $onRejected = null)
{
return self::$queue ?: self::$queue = new SplQueue();
}
public function then(?callable $onFulfilled = null, ?callable $onRejected = null)
{
if ($this->state === self::REJECTED && $onRejected === null) {
if ($this->state === self::REJECTED && !$onRejected) {
return $this;
}
if ($this->state === self::FULFILLED && $onFulfilled === null) {
if ($this->state === self::FULFILLED && !$onFulfilled) {
return $this;
}
$tmp = new self();
$tmp = new self();
$this->waiting[] = [$tmp, $onFulfilled, $onRejected];
if ($this->state !== self::PENDING) {
@ -167,4 +120,39 @@ class SyncPromise
return $tmp;
}
private function enqueueWaitingPromises()
{
Utils::invariant($this->state !== self::PENDING, 'Cannot enqueue derived promises when parent is still pending');
foreach ($this->waiting as $descriptor) {
self::getQueue()->enqueue(function () use ($descriptor) {
/** @var $promise self */
list($promise, $onFulfilled, $onRejected) = $descriptor;
if ($this->state === self::FULFILLED) {
try {
$promise->resolve($onFulfilled ? $onFulfilled($this->result) : $this->result);
} catch (\Exception $e) {
$promise->reject($e);
} catch (\Throwable $e) {
$promise->reject($e);
}
} else if ($this->state === self::REJECTED) {
try {
if ($onRejected) {
$promise->resolve($onRejected($this->result));
} else {
$promise->reject($this->result);
}
} catch (\Exception $e) {
$promise->reject($e);
} catch (\Throwable $e) {
$promise->reject($e);
}
}
});
}
$this->waiting = [];
}
}

View file

@ -1,22 +1,19 @@
<?php
declare(strict_types=1);
namespace GraphQL\Executor\Promise\Adapter;
use Exception;
use GraphQL\Deferred;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Utils\Utils;
use Throwable;
use function count;
/**
* Class SyncPromiseAdapter
*
* Allows changing order of field resolution even in sync environments
* (by leveraging queue of deferreds and promises)
*
* @package GraphQL\Executor\Promise\Adapter
*/
class SyncPromiseAdapter implements PromiseAdapter
{
@ -33,22 +30,20 @@ class SyncPromiseAdapter implements PromiseAdapter
*/
public function convertThenable($thenable)
{
if (! $thenable instanceof Deferred) {
if (!$thenable instanceof Deferred) {
throw new InvariantViolation('Expected instance of GraphQL\Deferred, got ' . Utils::printSafe($thenable));
}
return new Promise($thenable->promise, $this);
}
/**
* @inheritdoc
*/
public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null)
public function then(Promise $promise, callable $onFulfilled = null, callable $onRejected = null)
{
/** @var SyncPromise $adoptedPromise */
$adoptedPromise = $promise->adoptedPromise;
return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this);
/** @var SyncPromise $promise */
$promise = $promise->adoptedPromise;
return new Promise($promise->then($onFulfilled, $onRejected), $this);
}
/**
@ -60,18 +55,12 @@ class SyncPromiseAdapter implements PromiseAdapter
try {
$resolver(
[
$promise,
'resolve',
],
[
$promise,
'reject',
]
[$promise, 'resolve'],
[$promise, 'reject']
);
} catch (Exception $e) {
} catch (\Exception $e) {
$promise->reject($e);
} catch (Throwable $e) {
} catch (\Throwable $e) {
$promise->reject($e);
}
@ -84,7 +73,6 @@ class SyncPromiseAdapter implements PromiseAdapter
public function createFulfilled($value = null)
{
$promise = new SyncPromise();
return new Promise($promise->resolve($value), $this);
}
@ -94,7 +82,6 @@ class SyncPromiseAdapter implements PromiseAdapter
public function createRejected($reason)
{
$promise = new SyncPromise();
return new Promise($promise->reject($reason), $this);
}
@ -105,22 +92,20 @@ class SyncPromiseAdapter implements PromiseAdapter
{
$all = new SyncPromise();
$total = count($promisesOrValues);
$count = 0;
$total = count($promisesOrValues);
$count = 0;
$result = [];
foreach ($promisesOrValues as $index => $promiseOrValue) {
if ($promiseOrValue instanceof Promise) {
$result[$index] = null;
$promiseOrValue->then(
static function ($value) use ($index, &$count, $total, &$result, $all) {
function($value) use ($index, &$count, $total, &$result, $all) {
$result[$index] = $value;
$count++;
if ($count < $total) {
return;
if ($count >= $total) {
$all->resolve($result);
}
$all->resolve($result);
},
[$all, 'reject']
);
@ -132,23 +117,24 @@ class SyncPromiseAdapter implements PromiseAdapter
if ($count === $total) {
$all->resolve($result);
}
return new Promise($all, $this);
}
/**
* Synchronously wait when promise completes
*
* @return ExecutionResult
* @param Promise $promise
* @return mixed
*/
public function wait(Promise $promise)
{
$this->beforeWait($promise);
$dfdQueue = Deferred::getQueue();
$dfdQueue = Deferred::getQueue();
$promiseQueue = SyncPromise::getQueue();
while ($promise->adoptedPromise->state === SyncPromise::PENDING &&
! ($dfdQueue->isEmpty() && $promiseQueue->isEmpty())
while (
$promise->adoptedPromise->state === SyncPromise::PENDING &&
!($dfdQueue->isEmpty() && $promiseQueue->isEmpty())
) {
Deferred::runQueue();
SyncPromise::runQueue();
@ -160,17 +146,17 @@ class SyncPromiseAdapter implements PromiseAdapter
if ($syncPromise->state === SyncPromise::FULFILLED) {
return $syncPromise->result;
}
if ($syncPromise->state === SyncPromise::REJECTED) {
} else if ($syncPromise->state === SyncPromise::REJECTED) {
throw $syncPromise->result;
}
throw new InvariantViolation('Could not resolve promise');
throw new InvariantViolation("Could not resolve promise");
}
/**
* Execute just before starting to run promise completion
*
* @param Promise $promise
*/
protected function beforeWait(Promise $promise)
{
@ -178,6 +164,8 @@ class SyncPromiseAdapter implements PromiseAdapter
/**
* Execute while running promise completion
*
* @param Promise $promise
*/
protected function onWait(Promise $promise)
{

View file

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

View file

@ -1,11 +1,6 @@
<?php
declare(strict_types=1);
namespace GraphQL\Executor\Promise;
use Throwable;
/**
* Provides a means for integration of async PHP platforms ([related docs](data-fetching.md#async-php))
*/
@ -14,22 +9,18 @@ interface PromiseAdapter
/**
* Return true if the value is a promise or a deferred of the underlying platform
*
* @param mixed $value
*
* @return bool
*
* @api
* @param mixed $value
* @return bool
*/
public function isThenable($value);
/**
* Converts thenable of the underlying platform into GraphQL\Executor\Promise\Promise instance
*
* @param object $thenable
*
* @return Promise
*
* @api
* @param object $thenable
* @return Promise
*/
public function convertThenable($thenable);
@ -37,11 +28,14 @@ interface PromiseAdapter
* Accepts our Promise wrapper, extracts adopted promise out of it and executes actual `then` logic described
* in Promises/A+ specs. Then returns new wrapped instance of GraphQL\Executor\Promise\Promise.
*
* @return Promise
*
* @api
* @param Promise $promise
* @param callable|null $onFulfilled
* @param callable|null $onRejected
*
* @return Promise
*/
public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null);
public function then(Promise $promise, callable $onFulfilled = null, callable $onRejected = null);
/**
* Creates a Promise
@ -49,20 +43,18 @@ interface PromiseAdapter
* Expected resolver signature:
* function(callable $resolve, callable $reject)
*
* @return Promise
*
* @api
* @param callable $resolver
* @return Promise
*/
public function create(callable $resolver);
/**
* Creates a fulfilled Promise for a value if the value is not a promise.
*
* @param mixed $value
*
* @return Promise
*
* @api
* @param mixed $value
* @return Promise
*/
public function createFulfilled($value = null);
@ -70,11 +62,9 @@ interface PromiseAdapter
* Creates a rejected promise for a reason if the reason is not a promise. If
* the provided reason is a promise, then it is returned as-is.
*
* @param Throwable $reason
*
* @return Promise
*
* @api
* @param \Throwable $reason
* @return Promise
*/
public function createRejected($reason);
@ -82,11 +72,9 @@ interface PromiseAdapter
* Given an array of promises (or values), returns a promise that is fulfilled when all the
* items in the array are fulfilled.
*
* @param Promise[]|mixed[] $promisesOrValues Promises or values.
*
* @return Promise
*
* @api
* @param array $promisesOrValues Promises or values.
* @return Promise
*/
public function all(array $promisesOrValues);
}

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,9 @@
<?php
declare(strict_types=1);
namespace GraphQL\Executor;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\EnumValueDefinitionNode;
@ -12,27 +11,23 @@ use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Language\AST\VariableNode;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Language\Printer;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\LeafType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use GraphQL\Utils\AST;
use GraphQL\Utils\TypeInfo;
use GraphQL\Utils\Utils;
use GraphQL\Utils\Value;
use stdClass;
use Throwable;
use function array_key_exists;
use function array_map;
use function sprintf;
use GraphQL\Validator\DocumentValidator;
class Values
{
@ -41,78 +36,134 @@ class Values
* variable definitions and arbitrary input. If the input cannot be coerced
* to match the variable definitions, a Error will be thrown.
*
* @param VariableDefinitionNode[] $varDefNodes
* @param mixed[] $inputs
*
* @return mixed[]
* @param Schema $schema
* @param VariableDefinitionNode[] $definitionNodes
* @param array $inputs
* @return array
* @throws Error
*/
public static function getVariableValues(Schema $schema, $varDefNodes, array $inputs)
public static function getVariableValues(Schema $schema, $definitionNodes, array $inputs)
{
$errors = [];
$coercedValues = [];
foreach ($varDefNodes as $varDefNode) {
$varName = $varDefNode->variable->name->value;
/** @var InputType|Type $varType */
$varType = TypeInfo::typeFromAST($schema, $varDefNode->type);
foreach ($definitionNodes as $definitionNode) {
$varName = $definitionNode->variable->name->value;
$varType = TypeInfo::typeFromAST($schema, $definitionNode->type);
if (Type::isInputType($varType)) {
if (array_key_exists($varName, $inputs)) {
$value = $inputs[$varName];
$coerced = Value::coerceValue($value, $varType, $varDefNode);
/** @var Error[] $coercionErrors */
$coercionErrors = $coerced['errors'];
if (empty($coercionErrors)) {
$coercedValues[$varName] = $coerced['value'];
} else {
$messagePrelude = sprintf(
'Variable "$%s" got invalid value %s; ',
$varName,
Utils::printSafeJson($value)
);
foreach ($coercionErrors as $error) {
$errors[] = new Error(
$messagePrelude . $error->getMessage(),
$error->getNodes(),
$error->getSource(),
$error->getPositions(),
$error->getPath(),
$error,
$error->getExtensions()
);
}
}
} else {
if ($varType instanceof NonNull) {
$errors[] = new Error(
sprintf(
'Variable "$%s" of required type "%s" was not provided.',
$varName,
$varType
),
[$varDefNode]
);
} elseif ($varDefNode->defaultValue !== null) {
$coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType);
}
}
} else {
$errors[] = new Error(
sprintf(
'Variable "$%s" expected value of type "%s" which cannot be used as an input type.',
$varName,
Printer::doPrint($varDefNode->type)
),
[$varDefNode->type]
if (!Type::isInputType($varType)) {
throw new Error(
'Variable "$'.$varName.'" expected value of type ' .
'"' . Printer::doPrint($definitionNode->type) . '" which cannot be used as an input type.',
[$definitionNode->type]
);
}
if (!array_key_exists($varName, $inputs)) {
$defaultValue = $definitionNode->defaultValue;
if ($defaultValue) {
$coercedValues[$varName] = AST::valueFromAST($defaultValue, $varType);
}
if ($varType instanceof NonNull) {
throw new Error(
'Variable "$'.$varName .'" of required type ' .
'"'. Utils::printSafe($varType) . '" was not provided.',
[$definitionNode]
);
}
} else {
$value = $inputs[$varName];
$errors = self::isValidPHPValue($value, $varType);
if (!empty($errors)) {
$message = "\n" . implode("\n", $errors);
throw new Error(
'Variable "$' . $varName . '" got invalid value ' .
json_encode($value) . '.' . $message,
[$definitionNode]
);
}
$coercedValue = self::coerceValue($varType, $value);
Utils::invariant($coercedValue !== Utils::undefined(), 'Should have reported error.');
$coercedValues[$varName] = $coercedValue;
}
}
return $coercedValues;
}
/**
* Prepares an object map of argument values given a list of argument
* definitions and list of argument AST nodes.
*
* @param FieldDefinition|Directive $def
* @param FieldNode|\GraphQL\Language\AST\DirectiveNode $node
* @param $variableValues
* @return array
* @throws Error
*/
public static function getArgumentValues($def, $node, $variableValues = null)
{
$argDefs = $def->args;
$argNodes = $node->arguments;
if (!$argDefs || null === $argNodes) {
return [];
}
if (! empty($errors)) {
return [$errors, null];
}
$coercedValues = [];
$undefined = Utils::undefined();
return [null, $coercedValues];
/** @var ArgumentNode[] $argNodeMap */
$argNodeMap = $argNodes ? Utils::keyMap($argNodes, function (ArgumentNode $arg) {
return $arg->name->value;
}) : [];
foreach ($argDefs as $argDef) {
$name = $argDef->name;
$argType = $argDef->getType();
$argumentNode = isset($argNodeMap[$name]) ? $argNodeMap[$name] : null;
if (!$argumentNode) {
if ($argDef->defaultValueExists()) {
$coercedValues[$name] = $argDef->defaultValue;
} else if ($argType instanceof NonNull) {
throw new Error(
'Argument "' . $name . '" of required type ' .
'"' . Utils::printSafe($argType) . '" was not provided.',
[$node]
);
}
} else if ($argumentNode->value instanceof VariableNode) {
$variableName = $argumentNode->value->name->value;
if ($variableValues && array_key_exists($variableName, $variableValues)) {
// Note: this does not check that this variable value is correct.
// This assumes that this query has been validated and the variable
// usage here is of the correct type.
$coercedValues[$name] = $variableValues[$variableName];
} else if ($argDef->defaultValueExists()) {
$coercedValues[$name] = $argDef->defaultValue;
} else if ($argType instanceof NonNull) {
throw new Error(
'Argument "' . $name . '" of required type "' . Utils::printSafe($argType) . '" was ' .
'provided the variable "$' . $variableName . '" which was not provided ' .
'a runtime value.',
[ $argumentNode->value ]
);
}
} else {
$valueNode = $argumentNode->value;
$coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues);
if ($coercedValue === $undefined) {
$errors = DocumentValidator::isValidLiteralValue($argType, $valueNode);
$message = !empty($errors) ? ("\n" . implode("\n", $errors)) : '';
throw new Error(
'Argument "' . $name . '" got invalid value ' . Printer::doPrint($valueNode) . '.' . $message,
[ $argumentNode->value ]
);
}
$coercedValues[$name] = $coercedValue;
}
}
return $coercedValues;
}
/**
@ -122,158 +173,213 @@ class Values
*
* If the directive does not exist on the node, returns undefined.
*
* @param FragmentSpreadNode|FieldNode|InlineFragmentNode|EnumValueDefinitionNode|FieldDefinitionNode $node
* @param mixed[]|null $variableValues
* @param Directive $directiveDef
* @param FragmentSpreadNode | FieldNode | InlineFragmentNode | EnumValueDefinitionNode | FieldDefinitionNode $node
* @param array|null $variableValues
*
* @return mixed[]|null
* @return array|null
*/
public static function getDirectiveValues(Directive $directiveDef, $node, $variableValues = null)
{
if (isset($node->directives) && $node->directives instanceof NodeList) {
$directiveNode = Utils::find(
$node->directives,
static function (DirectiveNode $directive) use ($directiveDef) {
return $directive->name->value === $directiveDef->name;
}
);
$directiveNode = Utils::find($node->directives, function(DirectiveNode $directive) use ($directiveDef) {
return $directive->name->value === $directiveDef->name;
});
if ($directiveNode !== null) {
if ($directiveNode) {
return self::getArgumentValues($directiveDef, $directiveNode, $variableValues);
}
}
return null;
}
/**
* Prepares an object map of argument values given a list of argument
* definitions and list of argument AST nodes.
*
* @param FieldDefinition|Directive $def
* @param FieldNode|DirectiveNode $node
* @param mixed[] $variableValues
*
* @return mixed[]
*
* @throws Error
*/
public static function getArgumentValues($def, $node, $variableValues = null)
{
if (empty($def->args)) {
return [];
}
$argumentNodes = $node->arguments;
if (empty($argumentNodes)) {
return [];
}
$argumentValueMap = [];
foreach ($argumentNodes as $argumentNode) {
$argumentValueMap[$argumentNode->name->value] = $argumentNode->value;
}
return static::getArgumentValuesForMap($def, $argumentValueMap, $variableValues, $node);
}
/**
* @param FieldDefinition|Directive $fieldDefinition
* @param ArgumentNode[] $argumentValueMap
* @param mixed[] $variableValues
* @param Node|null $referenceNode
*
* @return mixed[]
*
* @throws Error
*/
public static function getArgumentValuesForMap($fieldDefinition, $argumentValueMap, $variableValues = null, $referenceNode = null)
{
$argumentDefinitions = $fieldDefinition->args;
$coercedValues = [];
foreach ($argumentDefinitions as $argumentDefinition) {
$name = $argumentDefinition->name;
$argType = $argumentDefinition->getType();
$argumentValueNode = $argumentValueMap[$name] ?? null;
if ($argumentValueNode === null) {
if ($argumentDefinition->defaultValueExists()) {
$coercedValues[$name] = $argumentDefinition->defaultValue;
} elseif ($argType instanceof NonNull) {
throw new Error(
'Argument "' . $name . '" of required type ' .
'"' . Utils::printSafe($argType) . '" was not provided.',
$referenceNode
);
}
} elseif ($argumentValueNode instanceof VariableNode) {
$variableName = $argumentValueNode->name->value;
if ($variableValues !== null && array_key_exists($variableName, $variableValues)) {
// Note: this does not check that this variable value is correct.
// This assumes that this query has been validated and the variable
// usage here is of the correct type.
$coercedValues[$name] = $variableValues[$variableName];
} elseif ($argumentDefinition->defaultValueExists()) {
$coercedValues[$name] = $argumentDefinition->defaultValue;
} elseif ($argType instanceof NonNull) {
throw new Error(
'Argument "' . $name . '" of required type "' . Utils::printSafe($argType) . '" was ' .
'provided the variable "$' . $variableName . '" which was not provided ' .
'a runtime value.',
[$argumentValueNode]
);
}
} else {
$valueNode = $argumentValueNode;
$coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues);
if (Utils::isInvalid($coercedValue)) {
// Note: ValuesOfCorrectType validation should catch this before
// execution. This is a runtime check to ensure execution does not
// continue with an invalid argument value.
throw new Error(
'Argument "' . $name . '" has invalid value ' . Printer::doPrint($valueNode) . '.',
[$argumentValueNode]
);
}
$coercedValues[$name] = $coercedValue;
}
}
return $coercedValues;
}
/**
* @deprecated as of 8.0 (Moved to \GraphQL\Utils\AST::valueFromAST)
*
* @param ValueNode $valueNode
* @param mixed[]|null $variables
*
* @return mixed[]|stdClass|null
* @param $valueNode
* @param InputType $type
* @param null $variables
* @return array|null|\stdClass
*/
public static function valueFromAST($valueNode, InputType $type, ?array $variables = null)
public static function valueFromAST($valueNode, InputType $type, $variables = null)
{
return AST::valueFromAST($valueNode, $type, $variables);
}
/**
* @deprecated as of 0.12 (Use coerceValue() directly for richer information)
* Given a PHP value and a GraphQL type, determine if the value will be
* accepted for that type. This is primarily useful for validating the
* runtime values of query variables.
*
* @param mixed[] $value
*
* @return string[]
* @param $value
* @param InputType $type
* @return array
*/
public static function isValidPHPValue($value, InputType $type)
{
$errors = Value::coerceValue($value, $type)['errors'];
// A value must be provided if the type is non-null.
if ($type instanceof NonNull) {
if (null === $value) {
return ['Expected "' . Utils::printSafe($type) . '", found null.'];
}
return self::isValidPHPValue($value, $type->getWrappedType());
}
return $errors
? array_map(
static function (Throwable $error) {
return $error->getMessage();
},
$errors
)
: [];
if (null === $value) {
return [];
}
// Lists accept a non-list value as a list of one.
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if (is_array($value)) {
$tmp = [];
foreach ($value as $index => $item) {
$errors = self::isValidPHPValue($item, $itemType);
$tmp = array_merge($tmp, Utils::map($errors, function ($error) use ($index) {
return "In element #$index: $error";
}));
}
return $tmp;
}
return self::isValidPHPValue($value, $itemType);
}
// Input objects check each defined field.
if ($type instanceof InputObjectType) {
if (!is_object($value) && !is_array($value)) {
return ["Expected \"{$type->name}\", found not an object."];
}
$fields = $type->getFields();
$errors = [];
// Ensure every provided field is defined.
$props = is_object($value) ? get_object_vars($value) : $value;
foreach ($props as $providedField => $tmp) {
if (!isset($fields[$providedField])) {
$errors[] = "In field \"{$providedField}\": Unknown field.";
}
}
// Ensure every defined field is valid.
foreach ($fields as $fieldName => $tmp) {
$newErrors = self::isValidPHPValue(isset($value[$fieldName]) ? $value[$fieldName] : null, $fields[$fieldName]->getType());
$errors = array_merge(
$errors,
Utils::map($newErrors, function ($error) use ($fieldName) {
return "In field \"{$fieldName}\": {$error}";
})
);
}
return $errors;
}
if ($type instanceof LeafType) {
try {
// Scalar/Enum input checks to ensure the type can parse the value to
// a non-null value.
$parseResult = $type->parseValue($value);
if (null === $parseResult && !$type->isValidValue($value)) {
$v = Utils::printSafe($value);
return [
"Expected type \"{$type->name}\", found $v."
];
}
return [];
} catch (\Exception $e) {
return [
"Expected type \"{$type->name}\", found " . Utils::printSafe($value) . ': ' .
$e->getMessage()
];
} catch (\Throwable $e) {
return [
"Expected type \"{$type->name}\", found " . Utils::printSafe($value) . ': ' .
$e->getMessage()
];
}
}
throw new InvariantViolation('Must be input type');
}
/**
* Given a type and any value, return a runtime value coerced to match the type.
*/
private static function coerceValue(Type $type, $value)
{
$undefined = Utils::undefined();
if ($value === $undefined) {
return $undefined;
}
if ($type instanceof NonNull) {
if ($value === null) {
// Intentionally return no value.
return $undefined;
}
return self::coerceValue($type->getWrappedType(), $value);
}
if (null === $value) {
return null;
}
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if (is_array($value) || $value instanceof \Traversable) {
$coercedValues = [];
foreach ($value as $item) {
$itemValue = self::coerceValue($itemType, $item);
if ($undefined === $itemValue) {
// Intentionally return no value.
return $undefined;
}
$coercedValues[] = $itemValue;
}
return $coercedValues;
} else {
$coercedValue = self::coerceValue($itemType, $value);
if ($coercedValue === $undefined) {
// Intentionally return no value.
return $undefined;
}
return [$coercedValue];
}
}
if ($type instanceof InputObjectType) {
$coercedObj = [];
$fields = $type->getFields();
foreach ($fields as $fieldName => $field) {
if (!array_key_exists($fieldName, $value)) {
if ($field->defaultValueExists()) {
$coercedObj[$fieldName] = $field->defaultValue;
} else if ($field->getType() instanceof NonNull) {
// Intentionally return no value.
return $undefined;
}
continue;
}
$fieldValue = self::coerceValue($field->getType(), $value[$fieldName]);
if ($fieldValue === $undefined) {
// Intentionally return no value.
return $undefined;
}
$coercedObj[$fieldName] = $fieldValue;
}
return $coercedObj;
}
if ($type instanceof LeafType) {
$parsed = $type->parseValue($value);
if (null === $parsed) {
// null or invalid values represent a failure to parse correctly,
// in which case no value is returned.
return $undefined;
}
return $parsed;
}
throw new InvariantViolation('Must be input type');
}
}

View file

@ -1,283 +0,0 @@
<?php
declare(strict_types=1);
namespace GraphQL\Experimental\Executor;
use Generator;
use GraphQL\Error\Error;
use GraphQL\Language\AST\DefinitionNode;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Introspection;
use GraphQL\Type\Schema;
use function sprintf;
/**
* @internal
*/
class Collector
{
/** @var Schema */
private $schema;
/** @var Runtime */
private $runtime;
/** @var OperationDefinitionNode|null */
public $operation = null;
/** @var FragmentDefinitionNode[] */
public $fragments = [];
/** @var ObjectType|null */
public $rootType;
/** @var FieldNode[][] */
private $fields;
/** @var string[] */
private $visitedFragments;
public function __construct(Schema $schema, Runtime $runtime)
{
$this->schema = $schema;
$this->runtime = $runtime;
}
public function initialize(DocumentNode $documentNode, ?string $operationName = null)
{
$hasMultipleAssumedOperations = false;
foreach ($documentNode->definitions as $definitionNode) {
/** @var DefinitionNode|Node $definitionNode */
if ($definitionNode instanceof OperationDefinitionNode) {
/** @var OperationDefinitionNode $definitionNode */
if ($operationName === null && $this->operation !== null) {
$hasMultipleAssumedOperations = true;
}
if ($operationName === null ||
(isset($definitionNode->name) && $definitionNode->name->value === $operationName)
) {
$this->operation = $definitionNode;
}
} elseif ($definitionNode instanceof FragmentDefinitionNode) {
/** @var FragmentDefinitionNode $definitionNode */
$this->fragments[$definitionNode->name->value] = $definitionNode;
}
}
if ($this->operation === null) {
if ($operationName !== null) {
$this->runtime->addError(new Error(sprintf('Unknown operation named "%s".', $operationName)));
} else {
$this->runtime->addError(new Error('Must provide an operation.'));
}
return;
}
if ($hasMultipleAssumedOperations) {
$this->runtime->addError(new Error('Must provide operation name if query contains multiple operations.'));
return;
}
if ($this->operation->operation === 'query') {
$this->rootType = $this->schema->getQueryType();
} elseif ($this->operation->operation === 'mutation') {
$this->rootType = $this->schema->getMutationType();
} elseif ($this->operation->operation === 'subscription') {
$this->rootType = $this->schema->getSubscriptionType();
} else {
$this->runtime->addError(new Error(sprintf('Cannot initialize collector with operation type "%s".', $this->operation->operation)));
}
}
/**
* @return Generator
*/
public function collectFields(ObjectType $runtimeType, ?SelectionSetNode $selectionSet)
{
$this->fields = [];
$this->visitedFragments = [];
$this->doCollectFields($runtimeType, $selectionSet);
foreach ($this->fields as $resultName => $fieldNodes) {
$fieldNode = $fieldNodes[0];
$fieldName = $fieldNode->name->value;
$argumentValueMap = null;
if (! empty($fieldNode->arguments)) {
foreach ($fieldNode->arguments as $argumentNode) {
$argumentValueMap = $argumentValueMap ?? [];
$argumentValueMap[$argumentNode->name->value] = $argumentNode->value;
}
}
if ($fieldName !== Introspection::TYPE_NAME_FIELD_NAME &&
! ($runtimeType === $this->schema->getQueryType() && ($fieldName === Introspection::SCHEMA_FIELD_NAME || $fieldName === Introspection::TYPE_FIELD_NAME)) &&
! $runtimeType->hasField($fieldName)
) {
// do not emit error
continue;
}
yield new CoroutineContextShared($fieldNodes, $fieldName, $resultName, $argumentValueMap);
}
}
private function doCollectFields(ObjectType $runtimeType, ?SelectionSetNode $selectionSet)
{
if ($selectionSet === null) {
return;
}
foreach ($selectionSet->selections as $selection) {
/** @var FieldNode|FragmentSpreadNode|InlineFragmentNode $selection */
if (! empty($selection->directives)) {
foreach ($selection->directives as $directiveNode) {
if ($directiveNode->name->value === Directive::SKIP_NAME) {
/** @var ValueNode|null $condition */
$condition = null;
foreach ($directiveNode->arguments as $argumentNode) {
if ($argumentNode->name->value === Directive::IF_ARGUMENT_NAME) {
$condition = $argumentNode->value;
break;
}
}
if ($condition === null) {
$this->runtime->addError(new Error(
sprintf('@%s directive is missing "%s" argument.', Directive::SKIP_NAME, Directive::IF_ARGUMENT_NAME),
$selection
));
} else {
if ($this->runtime->evaluate($condition, Type::boolean()) === true) {
continue 2; // !!! advances outer loop
}
}
} elseif ($directiveNode->name->value === Directive::INCLUDE_NAME) {
/** @var ValueNode|null $condition */
$condition = null;
foreach ($directiveNode->arguments as $argumentNode) {
if ($argumentNode->name->value === Directive::IF_ARGUMENT_NAME) {
$condition = $argumentNode->value;
break;
}
}
if ($condition === null) {
$this->runtime->addError(new Error(
sprintf('@%s directive is missing "%s" argument.', Directive::INCLUDE_NAME, Directive::IF_ARGUMENT_NAME),
$selection
));
} else {
if ($this->runtime->evaluate($condition, Type::boolean()) !== true) {
continue 2; // !!! advances outer loop
}
}
}
}
}
if ($selection instanceof FieldNode) {
/** @var FieldNode $selection */
$resultName = $selection->alias === null ? $selection->name->value : $selection->alias->value;
if (! isset($this->fields[$resultName])) {
$this->fields[$resultName] = [];
}
$this->fields[$resultName][] = $selection;
} elseif ($selection instanceof FragmentSpreadNode) {
/** @var FragmentSpreadNode $selection */
$fragmentName = $selection->name->value;
if (isset($this->visitedFragments[$fragmentName])) {
continue;
}
if (! isset($this->fragments[$fragmentName])) {
$this->runtime->addError(new Error(
sprintf('Fragment "%s" does not exist.', $fragmentName),
$selection
));
continue;
}
$this->visitedFragments[$fragmentName] = true;
$fragmentDefinition = $this->fragments[$fragmentName];
$conditionTypeName = $fragmentDefinition->typeCondition->name->value;
if (! $this->schema->hasType($conditionTypeName)) {
$this->runtime->addError(new Error(
sprintf('Cannot spread fragment "%s", type "%s" does not exist.', $fragmentName, $conditionTypeName),
$selection
));
continue;
}
$conditionType = $this->schema->getType($conditionTypeName);
if ($conditionType instanceof ObjectType) {
if ($runtimeType->name !== $conditionType->name) {
continue;
}
} elseif ($conditionType instanceof AbstractType) {
if (! $this->schema->isPossibleType($conditionType, $runtimeType)) {
continue;
}
}
$this->doCollectFields($runtimeType, $fragmentDefinition->selectionSet);
} elseif ($selection instanceof InlineFragmentNode) {
/** @var InlineFragmentNode $selection */
if ($selection->typeCondition !== null) {
$conditionTypeName = $selection->typeCondition->name->value;
if (! $this->schema->hasType($conditionTypeName)) {
$this->runtime->addError(new Error(
sprintf('Cannot spread inline fragment, type "%s" does not exist.', $conditionTypeName),
$selection
));
continue;
}
$conditionType = $this->schema->getType($conditionTypeName);
if ($conditionType instanceof ObjectType) {
if ($runtimeType->name !== $conditionType->name) {
continue;
}
} elseif ($conditionType instanceof AbstractType) {
if (! $this->schema->isPossibleType($conditionType, $runtimeType)) {
continue;
}
}
}
$this->doCollectFields($runtimeType, $selection->selectionSet);
}
}
}
}

View file

@ -1,57 +0,0 @@
<?php
declare(strict_types=1);
namespace GraphQL\Experimental\Executor;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
/**
* @internal
*/
class CoroutineContext
{
/** @var CoroutineContextShared */
public $shared;
/** @var ObjectType */
public $type;
/** @var mixed */
public $value;
/** @var object */
public $result;
/** @var string[] */
public $path;
/** @var ResolveInfo|null */
public $resolveInfo;
/** @var string[]|null */
public $nullFence;
/**
* @param mixed $value
* @param object $result
* @param string[] $path
* @param string[]|null $nullFence
*/
public function __construct(
CoroutineContextShared $shared,
ObjectType $type,
$value,
$result,
array $path,
?array $nullFence = null
) {
$this->shared = $shared;
$this->type = $type;
$this->value = $value;
$this->result = $result;
$this->path = $path;
$this->nullFence = $nullFence;
}
}

View file

@ -1,62 +0,0 @@
<?php
declare(strict_types=1);
namespace GraphQL\Experimental\Executor;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
/**
* @internal
*/
class CoroutineContextShared
{
/** @var FieldNode[] */
public $fieldNodes;
/** @var string */
public $fieldName;
/** @var string */
public $resultName;
/** @var ValueNode[]|null */
public $argumentValueMap;
/** @var SelectionSetNode|null */
public $mergedSelectionSet;
/** @var ObjectType|null */
public $typeGuard1;
/** @var callable|null */
public $resolveIfType1;
/** @var mixed */
public $argumentsIfType1;
/** @var ResolveInfo|null */
public $resolveInfoIfType1;
/** @var ObjectType|null */
public $typeGuard2;
/** @var CoroutineContext[]|null */
public $childContextsIfType2;
/**
* @param FieldNode[] $fieldNodes
* @param mixed[]|null $argumentValueMap
*/
public function __construct(array $fieldNodes, string $fieldName, string $resultName, ?array $argumentValueMap)
{
$this->fieldNodes = $fieldNodes;
$this->fieldName = $fieldName;
$this->resultName = $resultName;
$this->argumentValueMap = $argumentValueMap;
}
}

View file

@ -1,952 +0,0 @@
<?php
declare(strict_types=1);
namespace GraphQL\Experimental\Executor;
use Generator;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Error\Warning;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\ExecutorImplementation;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Executor\Values;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\CompositeType;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\LeafType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Introspection;
use GraphQL\Type\Schema;
use GraphQL\Utils\AST;
use GraphQL\Utils\Utils;
use SplQueue;
use stdClass;
use Throwable;
use function is_array;
use function is_string;
use function sprintf;
class CoroutineExecutor implements Runtime, ExecutorImplementation
{
/** @var object */
private static $undefined;
/** @var Schema */
private $schema;
/** @var callable */
private $fieldResolver;
/** @var PromiseAdapter */
private $promiseAdapter;
/** @var mixed|null */
private $rootValue;
/** @var mixed|null */
private $contextValue;
/** @var mixed|null */
private $rawVariableValues;
/** @var mixed|null */
private $variableValues;
/** @var DocumentNode */
private $documentNode;
/** @var string|null */
private $operationName;
/** @var Collector */
private $collector;
/** @var Error[] */
private $errors;
/** @var SplQueue */
private $queue;
/** @var SplQueue */
private $schedule;
/** @var stdClass */
private $rootResult;
/** @var int */
private $pending;
/** @var callable */
private $doResolve;
public function __construct(
PromiseAdapter $promiseAdapter,
Schema $schema,
DocumentNode $documentNode,
$rootValue,
$contextValue,
$rawVariableValues,
?string $operationName,
callable $fieldResolver
) {
if (self::$undefined === null) {
self::$undefined = Utils::undefined();
}
$this->schema = $schema;
$this->fieldResolver = $fieldResolver;
$this->promiseAdapter = $promiseAdapter;
$this->rootValue = $rootValue;
$this->contextValue = $contextValue;
$this->rawVariableValues = $rawVariableValues;
$this->documentNode = $documentNode;
$this->operationName = $operationName;
}
public static function create(
PromiseAdapter $promiseAdapter,
Schema $schema,
DocumentNode $documentNode,
$rootValue,
$contextValue,
$variableValues,
?string $operationName,
callable $fieldResolver
) {
return new static(
$promiseAdapter,
$schema,
$documentNode,
$rootValue,
$contextValue,
$variableValues,
$operationName,
$fieldResolver
);
}
private static function resultToArray($value, $emptyObjectAsStdClass = true)
{
if ($value instanceof stdClass) {
$array = [];
foreach ($value as $propertyName => $propertyValue) {
$array[$propertyName] = self::resultToArray($propertyValue);
}
if ($emptyObjectAsStdClass && empty($array)) {
return new stdClass();
}
return $array;
}
if (is_array($value)) {
$array = [];
foreach ($value as $key => $item) {
$array[$key] = self::resultToArray($item);
}
return $array;
}
return $value;
}
public function doExecute() : Promise
{
$this->rootResult = new stdClass();
$this->errors = [];
$this->queue = new SplQueue();
$this->schedule = new SplQueue();
$this->pending = 0;
$this->collector = new Collector($this->schema, $this);
$this->collector->initialize($this->documentNode, $this->operationName);
if (! empty($this->errors)) {
return $this->promiseAdapter->createFulfilled($this->finishExecute(null, $this->errors));
}
[$errors, $coercedVariableValues] = Values::getVariableValues(
$this->schema,
$this->collector->operation->variableDefinitions ?: [],
$this->rawVariableValues ?: []
);
if (! empty($errors)) {
return $this->promiseAdapter->createFulfilled($this->finishExecute(null, $errors));
}
$this->variableValues = $coercedVariableValues;
foreach ($this->collector->collectFields($this->collector->rootType, $this->collector->operation->selectionSet) as $shared) {
/** @var CoroutineContextShared $shared */
// !!! assign to keep object keys sorted
$this->rootResult->{$shared->resultName} = null;
$ctx = new CoroutineContext(
$shared,
$this->collector->rootType,
$this->rootValue,
$this->rootResult,
[$shared->resultName]
);
$fieldDefinition = $this->findFieldDefinition($ctx);
if (! $fieldDefinition->getType() instanceof NonNull) {
$ctx->nullFence = [$shared->resultName];
}
if ($this->collector->operation->operation === 'mutation' && ! $this->queue->isEmpty()) {
$this->schedule->enqueue($ctx);
} else {
$this->queue->enqueue(new Strand($this->spawn($ctx)));
}
}
$this->run();
if ($this->pending > 0) {
return $this->promiseAdapter->create(function (callable $resolve) {
$this->doResolve = $resolve;
});
}
return $this->promiseAdapter->createFulfilled($this->finishExecute($this->rootResult, $this->errors));
}
/**
* @param object|null $value
* @param Error[] $errors
*/
private function finishExecute($value, array $errors) : ExecutionResult
{
$this->rootResult = null;
$this->errors = null;
$this->queue = null;
$this->schedule = null;
$this->pending = null;
$this->collector = null;
$this->variableValues = null;
if ($value !== null) {
$value = self::resultToArray($value, false);
}
return new ExecutionResult($value, $errors);
}
/**
* @internal
*/
public function evaluate(ValueNode $valueNode, InputType $type)
{
return AST::valueFromAST($valueNode, $type, $this->variableValues);
}
/**
* @internal
*/
public function addError($error)
{
$this->errors[] = $error;
}
private function run()
{
RUN:
while (! $this->queue->isEmpty()) {
/** @var Strand $strand */
$strand = $this->queue->dequeue();
try {
if ($strand->success !== null) {
RESUME:
if ($strand->success) {
$strand->current->send($strand->value);
} else {
$strand->current->throw($strand->value);
}
$strand->success = null;
$strand->value = null;
}
START:
if ($strand->current->valid()) {
$value = $strand->current->current();
if ($value instanceof Generator) {
$strand->stack[$strand->depth++] = $strand->current;
$strand->current = $value;
goto START;
} elseif ($this->isPromise($value)) {
// !!! increment pending before calling ->then() as it may invoke the callback right away
++$this->pending;
if (! $value instanceof Promise) {
$value = $this->promiseAdapter->convertThenable($value);
}
$this->promiseAdapter
->then(
$value,
function ($value) use ($strand) {
$strand->success = true;
$strand->value = $value;
$this->queue->enqueue($strand);
$this->done();
},
function (Throwable $throwable) use ($strand) {
$strand->success = false;
$strand->value = $throwable;
$this->queue->enqueue($strand);
$this->done();
}
);
continue;
} else {
$strand->success = true;
$strand->value = $value;
goto RESUME;
}
}
$strand->success = true;
$strand->value = $strand->current->getReturn();
} catch (Throwable $reason) {
$strand->success = false;
$strand->value = $reason;
}
if ($strand->depth <= 0) {
continue;
}
$current = &$strand->stack[--$strand->depth];
$strand->current = $current;
$current = null;
goto RESUME;
}
if ($this->pending > 0 || $this->schedule->isEmpty()) {
return;
}
/** @var CoroutineContext $ctx */
$ctx = $this->schedule->dequeue();
$this->queue->enqueue(new Strand($this->spawn($ctx)));
goto RUN;
}
private function done()
{
--$this->pending;
$this->run();
if ($this->pending > 0) {
return;
}
$doResolve = $this->doResolve;
$doResolve($this->finishExecute($this->rootResult, $this->errors));
}
private function spawn(CoroutineContext $ctx)
{
// short-circuit evaluation for __typename
if ($ctx->shared->fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
$ctx->result->{$ctx->shared->resultName} = $ctx->type->name;
return;
}
try {
if ($ctx->shared->typeGuard1 === $ctx->type) {
$resolve = $ctx->shared->resolveIfType1;
$ctx->resolveInfo = clone $ctx->shared->resolveInfoIfType1;
$ctx->resolveInfo->path = $ctx->path;
$arguments = $ctx->shared->argumentsIfType1;
$returnType = $ctx->resolveInfo->returnType;
} else {
$fieldDefinition = $this->findFieldDefinition($ctx);
if ($fieldDefinition->resolveFn !== null) {
$resolve = $fieldDefinition->resolveFn;
} elseif ($ctx->type->resolveFieldFn !== null) {
$resolve = $ctx->type->resolveFieldFn;
} else {
$resolve = $this->fieldResolver;
}
$returnType = $fieldDefinition->getType();
$ctx->resolveInfo = new ResolveInfo(
$ctx->shared->fieldName,
$ctx->shared->fieldNodes,
$returnType,
$ctx->type,
$ctx->path,
$this->schema,
$this->collector->fragments,
$this->rootValue,
$this->collector->operation,
$this->variableValues
);
$arguments = Values::getArgumentValuesForMap(
$fieldDefinition,
$ctx->shared->argumentValueMap,
$this->variableValues
);
// !!! assign only in batch when no exception can be thrown in-between
$ctx->shared->typeGuard1 = $ctx->type;
$ctx->shared->resolveIfType1 = $resolve;
$ctx->shared->argumentsIfType1 = $arguments;
$ctx->shared->resolveInfoIfType1 = $ctx->resolveInfo;
}
$value = $resolve($ctx->value, $arguments, $this->contextValue, $ctx->resolveInfo);
if (! $this->completeValueFast($ctx, $returnType, $value, $ctx->path, $returnValue)) {
$returnValue = yield $this->completeValue(
$ctx,
$returnType,
$value,
$ctx->path,
$ctx->nullFence
);
}
} catch (Throwable $reason) {
$this->addError(Error::createLocatedError(
$reason,
$ctx->shared->fieldNodes,
$ctx->path
));
$returnValue = self::$undefined;
}
if ($returnValue !== self::$undefined) {
$ctx->result->{$ctx->shared->resultName} = $returnValue;
} elseif ($ctx->resolveInfo !== null && $ctx->resolveInfo->returnType instanceof NonNull) { // !!! $ctx->resolveInfo might not have been initialized yet
$result =& $this->rootResult;
foreach ($ctx->nullFence ?? [] as $key) {
if (is_string($key)) {
$result =& $result->{$key};
} else {
$result =& $result[$key];
}
}
$result = null;
}
}
private function findFieldDefinition(CoroutineContext $ctx)
{
if ($ctx->shared->fieldName === Introspection::SCHEMA_FIELD_NAME && $ctx->type === $this->schema->getQueryType()) {
return Introspection::schemaMetaFieldDef();
}
if ($ctx->shared->fieldName === Introspection::TYPE_FIELD_NAME && $ctx->type === $this->schema->getQueryType()) {
return Introspection::typeMetaFieldDef();
}
if ($ctx->shared->fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
return Introspection::typeNameMetaFieldDef();
}
return $ctx->type->getField($ctx->shared->fieldName);
}
/**
* @param mixed $value
* @param string[] $path
* @param mixed $returnValue
*/
private function completeValueFast(CoroutineContext $ctx, Type $type, $value, array $path, &$returnValue) : bool
{
// special handling of Throwable inherited from JS reference implementation, but makes no sense in this PHP
if ($this->isPromise($value) || $value instanceof Throwable) {
return false;
}
$nonNull = false;
if ($type instanceof NonNull) {
$nonNull = true;
$type = $type->getWrappedType();
}
if (! $type instanceof LeafType) {
return false;
}
if ($type !== $this->schema->getType($type->name)) {
$hint = '';
if ($this->schema->getConfig()->typeLoader !== null) {
$hint = sprintf(
'Make sure that type loader returns the same instance as defined in %s.%s',
$ctx->type,
$ctx->shared->fieldName
);
}
$this->addError(Error::createLocatedError(
new InvariantViolation(
sprintf(
'Schema must contain unique named types but contains multiple types named "%s". %s ' .
'(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
$type->name,
$hint
)
),
$ctx->shared->fieldNodes,
$path
));
$value = null;
}
if ($value === null) {
$returnValue = null;
} else {
try {
$returnValue = $type->serialize($value);
} catch (Throwable $error) {
$this->addError(Error::createLocatedError(
new InvariantViolation(
'Expected a value of type "' . Utils::printSafe($type) . '" but received: ' . Utils::printSafe($value),
0,
$error
),
$ctx->shared->fieldNodes,
$path
));
$returnValue = null;
}
}
if ($nonNull && $returnValue === null) {
$this->addError(Error::createLocatedError(
new InvariantViolation(sprintf(
'Cannot return null for non-nullable field %s.%s.',
$ctx->type->name,
$ctx->shared->fieldName
)),
$ctx->shared->fieldNodes,
$path
));
$returnValue = self::$undefined;
}
return true;
}
/**
* @param mixed $value
* @param string[] $path
* @param string[]|null $nullFence
*
* @return mixed
*/
private function completeValue(CoroutineContext $ctx, Type $type, $value, array $path, ?array $nullFence)
{
$nonNull = false;
$returnValue = null;
if ($type instanceof NonNull) {
$nonNull = true;
$type = $type->getWrappedType();
} else {
$nullFence = $path;
}
// !!! $value might be promise, yield to resolve
try {
if ($this->isPromise($value)) {
$value = yield $value;
}
} catch (Throwable $reason) {
$this->addError(Error::createLocatedError(
$reason,
$ctx->shared->fieldNodes,
$path
));
if ($nonNull) {
$returnValue = self::$undefined;
} else {
$returnValue = null;
}
goto CHECKED_RETURN;
}
if ($value === null) {
$returnValue = $value;
goto CHECKED_RETURN;
} elseif ($value instanceof Throwable) {
// special handling of Throwable inherited from JS reference implementation, but makes no sense in this PHP
$this->addError(Error::createLocatedError(
$value,
$ctx->shared->fieldNodes,
$path
));
if ($nonNull) {
$returnValue = self::$undefined;
} else {
$returnValue = null;
}
goto CHECKED_RETURN;
}
if ($type instanceof ListOfType) {
$returnValue = [];
$index = -1;
$itemType = $type->getWrappedType();
foreach ($value as $itemValue) {
++$index;
$itemPath = $path;
$itemPath[] = $index; // !!! use arrays COW semantics
$ctx->resolveInfo->path = $itemPath;
try {
if (! $this->completeValueFast($ctx, $itemType, $itemValue, $itemPath, $itemReturnValue)) {
$itemReturnValue = yield $this->completeValue($ctx, $itemType, $itemValue, $itemPath, $nullFence);
}
} catch (Throwable $reason) {
$this->addError(Error::createLocatedError(
$reason,
$ctx->shared->fieldNodes,
$itemPath
));
$itemReturnValue = null;
}
if ($itemReturnValue === self::$undefined) {
$returnValue = self::$undefined;
goto CHECKED_RETURN;
}
$returnValue[$index] = $itemReturnValue;
}
goto CHECKED_RETURN;
} else {
if ($type !== $this->schema->getType($type->name)) {
$hint = '';
if ($this->schema->getConfig()->typeLoader !== null) {
$hint = sprintf(
'Make sure that type loader returns the same instance as defined in %s.%s',
$ctx->type,
$ctx->shared->fieldName
);
}
$this->addError(Error::createLocatedError(
new InvariantViolation(
sprintf(
'Schema must contain unique named types but contains multiple types named "%s". %s ' .
'(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
$type->name,
$hint
)
),
$ctx->shared->fieldNodes,
$path
));
$returnValue = null;
goto CHECKED_RETURN;
}
if ($type instanceof LeafType) {
try {
$returnValue = $type->serialize($value);
} catch (Throwable $error) {
$this->addError(Error::createLocatedError(
new InvariantViolation(
'Expected a value of type "' . Utils::printSafe($type) . '" but received: ' . Utils::printSafe($value),
0,
$error
),
$ctx->shared->fieldNodes,
$path
));
$returnValue = null;
}
goto CHECKED_RETURN;
} elseif ($type instanceof CompositeType) {
/** @var ObjectType|null $objectType */
$objectType = null;
if ($type instanceof InterfaceType || $type instanceof UnionType) {
$objectType = $type->resolveType($value, $this->contextValue, $ctx->resolveInfo);
if ($objectType === null) {
$objectType = yield $this->resolveTypeSlow($ctx, $value, $type);
}
// !!! $objectType->resolveType() might return promise, yield to resolve
$objectType = yield $objectType;
if (is_string($objectType)) {
$objectType = $this->schema->getType($objectType);
}
if ($objectType === null) {
$this->addError(Error::createLocatedError(
sprintf(
'Composite type "%s" did not resolve concrete object type for value: %s.',
$type->name,
Utils::printSafe($value)
),
$ctx->shared->fieldNodes,
$path
));
$returnValue = self::$undefined;
goto CHECKED_RETURN;
} elseif (! $objectType instanceof ObjectType) {
$this->addError(Error::createLocatedError(
new InvariantViolation(sprintf(
'Abstract type %s must resolve to an Object type at ' .
'runtime for field %s.%s with value "%s", received "%s". ' .
'Either the %s type should provide a "resolveType" ' .
'function or each possible type should provide an "isTypeOf" function.',
$type,
$ctx->resolveInfo->parentType,
$ctx->resolveInfo->fieldName,
Utils::printSafe($value),
Utils::printSafe($objectType),
$type
)),
$ctx->shared->fieldNodes,
$path
));
$returnValue = null;
goto CHECKED_RETURN;
} elseif (! $this->schema->isPossibleType($type, $objectType)) {
$this->addError(Error::createLocatedError(
new InvariantViolation(sprintf(
'Runtime Object type "%s" is not a possible type for "%s".',
$objectType,
$type
)),
$ctx->shared->fieldNodes,
$path
));
$returnValue = null;
goto CHECKED_RETURN;
} elseif ($objectType !== $this->schema->getType($objectType->name)) {
$this->addError(Error::createLocatedError(
new InvariantViolation(
sprintf(
'Schema must contain unique named types but contains multiple types named "%s". ' .
'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
'type instance as referenced anywhere else within the schema ' .
'(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
$objectType,
$type
)
),
$ctx->shared->fieldNodes,
$path
));
$returnValue = null;
goto CHECKED_RETURN;
}
} elseif ($type instanceof ObjectType) {
$objectType = $type;
} else {
$this->addError(Error::createLocatedError(
sprintf(
'Unexpected field type "%s".',
Utils::printSafe($type)
),
$ctx->shared->fieldNodes,
$path
));
$returnValue = self::$undefined;
goto CHECKED_RETURN;
}
$typeCheck = $objectType->isTypeOf($value, $this->contextValue, $ctx->resolveInfo);
if ($typeCheck !== null) {
// !!! $objectType->isTypeOf() might return promise, yield to resolve
$typeCheck = yield $typeCheck;
if (! $typeCheck) {
$this->addError(Error::createLocatedError(
sprintf('Expected value of type "%s" but got: %s.', $type->name, Utils::printSafe($value)),
$ctx->shared->fieldNodes,
$path
));
$returnValue = null;
goto CHECKED_RETURN;
}
}
$returnValue = new stdClass();
if ($ctx->shared->typeGuard2 === $objectType) {
foreach ($ctx->shared->childContextsIfType2 as $childCtx) {
$childCtx = clone $childCtx;
$childCtx->type = $objectType;
$childCtx->value = $value;
$childCtx->result = $returnValue;
$childCtx->path = $path;
$childCtx->path[] = $childCtx->shared->resultName; // !!! uses array COW semantics
$childCtx->nullFence = $nullFence;
$childCtx->resolveInfo = null;
$this->queue->enqueue(new Strand($this->spawn($childCtx)));
// !!! assign null to keep object keys sorted
$returnValue->{$childCtx->shared->resultName} = null;
}
} else {
$childContexts = [];
foreach ($this->collector->collectFields(
$objectType,
$ctx->shared->mergedSelectionSet ?? $this->mergeSelectionSets($ctx)
) as $childShared) {
/** @var CoroutineContextShared $childShared */
$childPath = $path;
$childPath[] = $childShared->resultName; // !!! uses array COW semantics
$childCtx = new CoroutineContext(
$childShared,
$objectType,
$value,
$returnValue,
$childPath,
$nullFence
);
$childContexts[] = $childCtx;
$this->queue->enqueue(new Strand($this->spawn($childCtx)));
// !!! assign null to keep object keys sorted
$returnValue->{$childShared->resultName} = null;
}
$ctx->shared->typeGuard2 = $objectType;
$ctx->shared->childContextsIfType2 = $childContexts;
}
goto CHECKED_RETURN;
} else {
$this->addError(Error::createLocatedError(
sprintf('Unhandled type "%s".', Utils::printSafe($type)),
$ctx->shared->fieldNodes,
$path
));
$returnValue = null;
goto CHECKED_RETURN;
}
}
CHECKED_RETURN:
if ($nonNull && $returnValue === null) {
$this->addError(Error::createLocatedError(
new InvariantViolation(sprintf(
'Cannot return null for non-nullable field %s.%s.',
$ctx->type->name,
$ctx->shared->fieldName
)),
$ctx->shared->fieldNodes,
$path
));
return self::$undefined;
}
return $returnValue;
}
private function mergeSelectionSets(CoroutineContext $ctx)
{
$selections = [];
foreach ($ctx->shared->fieldNodes as $fieldNode) {
if ($fieldNode->selectionSet === null) {
continue;
}
foreach ($fieldNode->selectionSet->selections as $selection) {
$selections[] = $selection;
}
}
return $ctx->shared->mergedSelectionSet = new SelectionSetNode(['selections' => $selections]);
}
private function resolveTypeSlow(CoroutineContext $ctx, $value, AbstractType $abstractType)
{
if ($value !== null &&
is_array($value) &&
isset($value['__typename']) &&
is_string($value['__typename'])
) {
return $this->schema->getType($value['__typename']);
}
if ($abstractType instanceof InterfaceType && $this->schema->getConfig()->typeLoader !== null) {
Warning::warnOnce(
sprintf(
'GraphQL Interface Type `%s` returned `null` from its `resolveType` function ' .
'for value: %s. Switching to slow resolution method using `isTypeOf` ' .
'of all possible implementations. It requires full schema scan and degrades query performance significantly. ' .
' Make sure your `resolveType` always returns valid implementation or throws.',
$abstractType->name,
Utils::printSafe($value)
),
Warning::WARNING_FULL_SCHEMA_SCAN
);
}
$possibleTypes = $this->schema->getPossibleTypes($abstractType);
// to be backward-compatible with old executor, ->isTypeOf() is called for all possible types,
// it cannot short-circuit when the match is found
$selectedType = null;
foreach ($possibleTypes as $type) {
$typeCheck = yield $type->isTypeOf($value, $this->contextValue, $ctx->resolveInfo);
if ($selectedType !== null || $typeCheck !== true) {
continue;
}
$selectedType = $type;
}
return $selectedType;
}
/**
* @param mixed $value
*
* @return bool
*/
private function isPromise($value)
{
return $value instanceof Promise || $this->promiseAdapter->isThenable($value);
}
}

View file

@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
namespace GraphQL\Experimental\Executor;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Type\Definition\InputType;
/**
* @internal
*/
interface Runtime
{
public function evaluate(ValueNode $valueNode, InputType $type);
public function addError($error);
}

View file

@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace GraphQL\Experimental\Executor;
use Generator;
/**
* @internal
*/
class Strand
{
/** @var Generator */
public $current;
/** @var Generator[] */
public $stack;
/** @var int */
public $depth;
/** @var bool|null */
public $success;
/** @var mixed */
public $value;
public function __construct(Generator $coroutine)
{
$this->current = $coroutine;
$this->stack = [];
$this->depth = 0;
}
}

View file

@ -1,7 +1,4 @@
<?php
declare(strict_types=1);
namespace GraphQL;
use GraphQL\Error\Error;
@ -9,21 +6,15 @@ use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Executor;
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Executor\ReferenceExecutor;
use GraphQL\Experimental\Executor\CoroutineExecutor;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\Parser;
use GraphQL\Language\Source;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema as SchemaType;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\Rules\AbstractValidationRule;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\ValidationRule;
use function array_values;
use function trigger_error;
use const E_USER_DEPRECATED;
/**
* This is the primary facade for fulfilling GraphQL operations.
@ -66,37 +57,33 @@ class GraphQL
* Empty array would allow to skip query validation (may be convenient for persisted
* queries which are validated before persisting and assumed valid during execution)
*
* @param string|DocumentNode $source
* @param mixed $rootValue
* @param mixed $context
* @param mixed[]|null $variableValues
* @param ValidationRule[] $validationRules
*
* @api
* @param \GraphQL\Type\Schema $schema
* @param string|DocumentNode $source
* @param mixed $rootValue
* @param mixed $context
* @param array|null $variableValues
* @param string|null $operationName
* @param callable $fieldResolver
* @param array $validationRules
*
* @return ExecutionResult
*/
public static function executeQuery(
SchemaType $schema,
\GraphQL\Type\Schema $schema,
$source,
$rootValue = null,
$context = null,
$variableValues = null,
?string $operationName = null,
?callable $fieldResolver = null,
?array $validationRules = null
) : ExecutionResult {
$operationName = null,
callable $fieldResolver = null,
array $validationRules = null
)
{
$promiseAdapter = new SyncPromiseAdapter();
$promise = self::promiseToExecute(
$promiseAdapter,
$schema,
$source,
$rootValue,
$context,
$variableValues,
$operationName,
$fieldResolver,
$validationRules
);
$promise = self::promiseToExecute($promiseAdapter, $schema, $source, $rootValue, $context,
$variableValues, $operationName, $fieldResolver, $validationRules);
return $promiseAdapter->wait($promise);
}
@ -105,25 +92,31 @@ class GraphQL
* Same as executeQuery(), but requires PromiseAdapter and always returns a Promise.
* Useful for Async PHP platforms.
*
* @param string|DocumentNode $source
* @param mixed $rootValue
* @param mixed $context
* @param mixed[]|null $variableValues
* @param ValidationRule[]|null $validationRules
*
* @api
* @param PromiseAdapter $promiseAdapter
* @param \GraphQL\Type\Schema $schema
* @param string|DocumentNode $source
* @param mixed $rootValue
* @param mixed $context
* @param array|null $variableValues
* @param string|null $operationName
* @param callable $fieldResolver
* @param array $validationRules
*
* @return Promise
*/
public static function promiseToExecute(
PromiseAdapter $promiseAdapter,
SchemaType $schema,
\GraphQL\Type\Schema $schema,
$source,
$rootValue = null,
$context = null,
$variableValues = null,
?string $operationName = null,
?callable $fieldResolver = null,
?array $validationRules = null
) : Promise {
$operationName = null,
callable $fieldResolver = null,
array $validationRules = null
)
{
try {
if ($source instanceof DocumentNode) {
$documentNode = $source;
@ -132,38 +125,36 @@ class GraphQL
}
// FIXME
if (empty($validationRules)) {
if (!empty($validationRules)) {
foreach ($validationRules as $rule) {
if ($rule instanceof QueryComplexity) {
$rule->setRawVariableValues($variableValues);
}
}
} else {
/** @var QueryComplexity $queryComplexity */
$queryComplexity = DocumentValidator::getRule(QueryComplexity::class);
$queryComplexity->setRawVariableValues($variableValues);
} else {
foreach ($validationRules as $rule) {
if (! ($rule instanceof QueryComplexity)) {
continue;
}
$rule->setRawVariableValues($variableValues);
}
}
$validationErrors = DocumentValidator::validate($schema, $documentNode, $validationRules);
if (! empty($validationErrors)) {
if (!empty($validationErrors)) {
return $promiseAdapter->createFulfilled(
new ExecutionResult(null, $validationErrors)
);
} else {
return Executor::promiseToExecute(
$promiseAdapter,
$schema,
$documentNode,
$rootValue,
$context,
$variableValues,
$operationName,
$fieldResolver
);
}
return Executor::promiseToExecute(
$promiseAdapter,
$schema,
$documentNode,
$rootValue,
$context,
$variableValues,
$operationName,
$fieldResolver
);
} catch (Error $e) {
return $promiseAdapter->createFulfilled(
new ExecutionResult(null, [$e])
@ -174,29 +165,25 @@ class GraphQL
/**
* @deprecated Use executeQuery()->toArray() instead
*
* @param \GraphQL\Type\Schema $schema
* @param string|DocumentNode $source
* @param mixed $rootValue
* @param mixed $contextValue
* @param mixed[]|null $variableValues
*
* @return Promise|mixed[]
* @param mixed $rootValue
* @param mixed $contextValue
* @param array|null $variableValues
* @param string|null $operationName
* @return Promise|array
*/
public static function execute(
SchemaType $schema,
\GraphQL\Type\Schema $schema,
$source,
$rootValue = null,
$contextValue = null,
$variableValues = null,
?string $operationName = null
) {
trigger_error(
__METHOD__ . ' is deprecated, use GraphQL::executeQuery()->toArray() as a quick replacement',
E_USER_DEPRECATED
);
$promiseAdapter = Executor::getPromiseAdapter();
$result = self::promiseToExecute(
$promiseAdapter,
$operationName = null
)
{
$result = self::promiseToExecute(
$promiseAdapter = Executor::getPromiseAdapter(),
$schema,
$source,
$rootValue,
@ -208,40 +195,36 @@ class GraphQL
if ($promiseAdapter instanceof SyncPromiseAdapter) {
$result = $promiseAdapter->wait($result)->toArray();
} else {
$result = $result->then(static function (ExecutionResult $r) {
$result = $result->then(function(ExecutionResult $r) {
return $r->toArray();
});
}
return $result;
}
/**
* @deprecated renamed to executeQuery()
*
* @param \GraphQL\Type\Schema $schema
* @param string|DocumentNode $source
* @param mixed $rootValue
* @param mixed $contextValue
* @param mixed[]|null $variableValues
* @param mixed $rootValue
* @param mixed $contextValue
* @param array|null $variableValues
* @param string|null $operationName
*
* @return ExecutionResult|Promise
*/
public static function executeAndReturnResult(
SchemaType $schema,
\GraphQL\Type\Schema $schema,
$source,
$rootValue = null,
$contextValue = null,
$variableValues = null,
?string $operationName = null
) {
trigger_error(
__METHOD__ . ' is deprecated, use GraphQL::executeQuery() as a quick replacement',
E_USER_DEPRECATED
);
$promiseAdapter = Executor::getPromiseAdapter();
$result = self::promiseToExecute(
$promiseAdapter,
$operationName = null
)
{
$result = self::promiseToExecute(
$promiseAdapter = Executor::getPromiseAdapter(),
$schema,
$source,
$rootValue,
@ -249,22 +232,19 @@ class GraphQL
$variableValues,
$operationName
);
if ($promiseAdapter instanceof SyncPromiseAdapter) {
$result = $promiseAdapter->wait($result);
}
return $result;
}
/**
* Returns directives defined in GraphQL spec
*
* @return Directive[]
*
* @api
* @return Directive[]
*/
public static function getStandardDirectives() : array
public static function getStandardDirectives()
{
return array_values(Directive::getInternalDirectives());
}
@ -272,79 +252,48 @@ class GraphQL
/**
* Returns types defined in GraphQL spec
*
* @api
* @return Type[]
*
* @api
*/
public static function getStandardTypes() : array
public static function getStandardTypes()
{
return array_values(Type::getStandardTypes());
}
/**
* Replaces standard types with types from this list (matching by name)
* Standard types not listed here remain untouched.
*
* @param Type[] $types
*
* @api
*/
public static function overrideStandardTypes(array $types)
{
Type::overrideStandardTypes($types);
return array_values(Type::getInternalTypes());
}
/**
* Returns standard validation rules implementing GraphQL spec
*
* @return ValidationRule[]
*
* @api
* @return AbstractValidationRule[]
*/
public static function getStandardValidationRules() : array
public static function getStandardValidationRules()
{
return array_values(DocumentValidator::defaultRules());
}
/**
* Set default resolver implementation
*
* @api
* @param callable $fn
*/
public static function setDefaultFieldResolver(callable $fn) : void
public static function setDefaultFieldResolver(callable $fn)
{
Executor::setDefaultFieldResolver($fn);
}
public static function setPromiseAdapter(?PromiseAdapter $promiseAdapter = null) : void
/**
* @param PromiseAdapter|null $promiseAdapter
*/
public static function setPromiseAdapter(PromiseAdapter $promiseAdapter = null)
{
Executor::setPromiseAdapter($promiseAdapter);
}
/**
* Experimental: Switch to the new executor
*/
public static function useExperimentalExecutor()
{
Executor::setImplementationFactory([CoroutineExecutor::class, 'create']);
}
/**
* Experimental: Switch back to the default executor
*/
public static function useReferenceExecutor()
{
Executor::setImplementationFactory([ReferenceExecutor::class, 'create']);
}
/**
* Returns directives defined in GraphQL spec
*
* @deprecated Renamed to getStandardDirectives
*
* @return Directive[]
*/
public static function getInternalDirectives() : array
public static function getInternalDirectives()
{
return self::getStandardDirectives();
}

View file

@ -1,17 +1,17 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
class ArgumentNode extends Node
{
/** @var string */
public $kind = NodeKind::ARGUMENT;
/** @var ValueNode */
/**
* @var ValueNode
*/
public $value;
/** @var NameNode */
/**
* @var NameNode
*/
public $name;
}

View file

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

View file

@ -1,14 +1,11 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
/**
* export type DefinitionNode =
* | ExecutableDefinitionNode
* | TypeSystemDefinitionNode; // experimental non-spec addition.
*/
interface DefinitionNode
{
/**
* export type DefinitionNode = OperationDefinitionNode
* | FragmentDefinitionNode
* | TypeSystemDefinitionNode // experimental non-spec addition.
*/
}

View file

@ -1,23 +1,25 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
class DirectiveDefinitionNode extends Node implements TypeSystemDefinitionNode
{
/** @var string */
/**
* @var string
*/
public $kind = NodeKind::DIRECTIVE_DEFINITION;
/** @var NameNode */
/**
* @var NameNode
*/
public $name;
/** @var ArgumentNode[] */
/**
* @var ArgumentNode[]
*/
public $arguments;
/** @var NameNode[] */
/**
* @var NameNode[]
*/
public $locations;
/** @var StringValueNode|null */
public $description;
}

View file

@ -1,17 +1,17 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
class DirectiveNode extends Node
{
/** @var string */
public $kind = NodeKind::DIRECTIVE;
/** @var NameNode */
/**
* @var NameNode
*/
public $name;
/** @var ArgumentNode[] */
/**
* @var ArgumentNode[]
*/
public $arguments;
}

View file

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

View file

@ -1,23 +1,30 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
class EnumTypeDefinitionNode extends Node implements TypeDefinitionNode
{
/** @var string */
/**
* @var string
*/
public $kind = NodeKind::ENUM_TYPE_DEFINITION;
/** @var NameNode */
/**
* @var NameNode
*/
public $name;
/** @var DirectiveNode[] */
/**
* @var DirectiveNode[]
*/
public $directives;
/** @var EnumValueDefinitionNode[]|NodeList|null */
/**
* @var EnumValueDefinitionNode[]
*/
public $values;
/** @var StringValueNode|null */
/**
* @var string
*/
public $description;
}

View file

@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
class EnumTypeExtensionNode extends Node implements TypeExtensionNode
{
/** @var string */
public $kind = NodeKind::ENUM_TYPE_EXTENSION;
/** @var NameNode */
public $name;
/** @var DirectiveNode[]|null */
public $directives;
/** @var EnumValueDefinitionNode[]|null */
public $values;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,26 +1,32 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
class FieldNode extends Node implements SelectionNode
{
/** @var string */
public $kind = NodeKind::FIELD;
/** @var NameNode */
/**
* @var NameNode
*/
public $name;
/** @var NameNode|null */
/**
* @var NameNode|null
*/
public $alias;
/** @var ArgumentNode[]|null */
/**
* @var ArgumentNode[]|null
*/
public $arguments;
/** @var DirectiveNode[]|null */
/**
* @var DirectiveNode[]|null
*/
public $directives;
/** @var SelectionSetNode|null */
/**
* @var SelectionSetNode|null
*/
public $selectionSet;
}

View file

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

View file

@ -1,31 +1,27 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
class FragmentDefinitionNode extends Node implements ExecutableDefinitionNode, HasSelectionSet
class FragmentDefinitionNode extends Node implements DefinitionNode, HasSelectionSet
{
/** @var string */
public $kind = NodeKind::FRAGMENT_DEFINITION;
/** @var NameNode */
/**
* @var NameNode
*/
public $name;
/**
* Note: fragment variable definitions are experimental and may be changed
* or removed in the future.
*
* @var VariableDefinitionNode[]|NodeList
* @var NamedTypeNode
*/
public $variableDefinitions;
/** @var NamedTypeNode */
public $typeCondition;
/** @var DirectiveNode[]|NodeList */
/**
* @var DirectiveNode[]
*/
public $directives;
/** @var SelectionSetNode */
/**
* @var SelectionSetNode
*/
public $selectionSet;
}

View file

@ -1,17 +1,17 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
class FragmentSpreadNode extends Node implements SelectionNode
{
/** @var string */
public $kind = NodeKind::FRAGMENT_SPREAD;
/** @var NameNode */
/**
* @var NameNode
*/
public $name;
/** @var DirectiveNode[] */
/**
* @var DirectiveNode[]
*/
public $directives;
}

View file

@ -1,7 +1,4 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
interface HasSelectionSet

View file

@ -1,20 +1,22 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
class InlineFragmentNode extends Node implements SelectionNode
{
/** @var string */
public $kind = NodeKind::INLINE_FRAGMENT;
/** @var NamedTypeNode */
/**
* @var NamedTypeNode
*/
public $typeCondition;
/** @var DirectiveNode[]|null */
/**
* @var DirectiveNode[]|null
*/
public $directives;
/** @var SelectionSetNode */
/**
* @var SelectionSetNode
*/
public $selectionSet;
}

View file

@ -1,23 +1,30 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
class InputObjectTypeDefinitionNode extends Node implements TypeDefinitionNode
{
/** @var string */
/**
* @var string
*/
public $kind = NodeKind::INPUT_OBJECT_TYPE_DEFINITION;
/** @var NameNode */
/**
* @var NameNode
*/
public $name;
/** @var DirectiveNode[]|null */
/**
* @var DirectiveNode[]
*/
public $directives;
/** @var InputValueDefinitionNode[]|null */
/**
* @var InputValueDefinitionNode[]
*/
public $fields;
/** @var StringValueNode|null */
/**
* @var string
*/
public $description;
}

View file

@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
class InputObjectTypeExtensionNode extends Node implements TypeExtensionNode
{
/** @var string */
public $kind = NodeKind::INPUT_OBJECT_TYPE_EXTENSION;
/** @var NameNode */
public $name;
/** @var DirectiveNode[]|null */
public $directives;
/** @var InputValueDefinitionNode[]|null */
public $fields;
}

View file

@ -1,26 +1,35 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
class InputValueDefinitionNode extends Node
{
/** @var string */
/**
* @var string
*/
public $kind = NodeKind::INPUT_VALUE_DEFINITION;
/** @var NameNode */
/**
* @var NameNode
*/
public $name;
/** @var TypeNode */
/**
* @var TypeNode
*/
public $type;
/** @var ValueNode */
/**
* @var ValueNode
*/
public $defaultValue;
/** @var DirectiveNode[] */
/**
* @var DirectiveNode[]
*/
public $directives;
/** @var StringValueNode|null */
/**
* @var string
*/
public $description;
}

View file

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

View file

@ -1,23 +1,30 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
class InterfaceTypeDefinitionNode extends Node implements TypeDefinitionNode
{
/** @var string */
/**
* @var string
*/
public $kind = NodeKind::INTERFACE_TYPE_DEFINITION;
/** @var NameNode */
/**
* @var NameNode
*/
public $name;
/** @var DirectiveNode[]|null */
/**
* @var DirectiveNode[]
*/
public $directives;
/** @var FieldDefinitionNode[]|null */
public $fields;
/**
* @var FieldDefinitionNode[]
*/
public $fields = [];
/** @var StringValueNode|null */
/**
* @var string
*/
public $description;
}

View file

@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
class InterfaceTypeExtensionNode extends Node implements TypeExtensionNode
{
/** @var string */
public $kind = NodeKind::INTERFACE_TYPE_EXTENSION;
/** @var NameNode */
public $name;
/** @var DirectiveNode[]|null */
public $directives;
/** @var FieldDefinitionNode[]|null */
public $fields;
}

View file

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

View file

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

View file

@ -1,7 +1,4 @@
<?php
declare(strict_types=1);
namespace GraphQL\Language\AST;
use GraphQL\Language\Source;
@ -49,31 +46,27 @@ class Location
public $source;
/**
* @param int $start
* @param int $end
*
* @param $start
* @param $end
* @return static
*/
public static function create($start, $end)
{
$tmp = new static();
$tmp = new static();
$tmp->start = $start;
$tmp->end = $end;
$tmp->end = $end;
return $tmp;
}
public function __construct(?Token $startToken = null, ?Token $endToken = null, ?Source $source = null)
public function __construct(Token $startToken = null, Token $endToken = null, Source $source = null)
{
$this->startToken = $startToken;
$this->endToken = $endToken;
$this->source = $source;
$this->endToken = $endToken;
$this->source = $source;
if ($startToken === null || $endToken === null) {
return;
if ($startToken && $endToken) {
$this->start = $startToken->start;
$this->end = $endToken->end;
}
$this->start = $startToken->start;
$this->end = $endToken->end;
}
}

View file

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

View file

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

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