mirror of
https://github.com/retailcrm/graphql-php.git
synced 2025-04-04 05:03:31 +03:00
Compare commits
560 commits
Author | SHA1 | Date | |
---|---|---|---|
|
b72ba3c93a | ||
|
1ac5af1d8b | ||
|
a01b089058 | ||
|
4401f4dd18 | ||
|
005b1a38c5 | ||
|
974258b352 | ||
|
a502c33254 | ||
|
22a0da9b98 | ||
|
8c4e7b178d | ||
|
3b33167c87 | ||
|
d037ab7ec3 | ||
|
54064b37b3 | ||
|
f7443b6f0c | ||
|
9704baf422 | ||
|
ed1d835bd5 | ||
|
3b27abafca | ||
|
c069d20ca7 | ||
|
19a37609f4 | ||
|
99453076b5 | ||
|
24f236403a | ||
|
8da3043702 | ||
|
a222cc9137 | ||
|
e704f8cc5c | ||
|
a34bb68d65 | ||
|
218e02a88c | ||
|
9ca7bb6ea1 | ||
|
65e4488ce8 | ||
|
bc66034f40 | ||
|
65a3a8d13e | ||
|
8381f67bd8 | ||
|
91b72f145d | ||
|
6e91e2181c | ||
|
03c33c9dc2 | ||
|
93ccd7351d | ||
|
ed1746e800 | ||
|
84a52c6c76 | ||
|
261f8f5ebd | ||
|
d1d4455eaa | ||
|
d5fbf1b29f | ||
|
c336d01bd2 | ||
|
752010b341 | ||
|
368a9ee2f7 | ||
|
6086792824 | ||
|
d259a303d3 | ||
|
0a71f9fba9 | ||
|
61453a4f0b | ||
|
6a5325a448 | ||
|
e87460880c | ||
|
173a4297d9 | ||
|
6f6a39468c | ||
|
a22a083220 | ||
|
acc0442152 | ||
|
21592f8f28 | ||
|
e17f578842 | ||
|
6d9275e6bc | ||
|
747cb49ae3 | ||
|
cdcf5b4473 | ||
|
692d10c127 | ||
|
6c82b85e79 | ||
|
e5528d14ab | ||
|
c9faa3489b | ||
|
bd02ccd47e | ||
|
08d9493b2c | ||
|
0b4b1485e0 | ||
|
387f416984 | ||
|
cf90a8d338 | ||
|
2fd21dd231 | ||
|
2173bb9696 | ||
|
e55c7d72cb | ||
|
ef9a24b01f | ||
|
40ac5ed269 | ||
|
27539d5af0 | ||
|
58d86abaf7 | ||
|
c803c455b4 | ||
|
9fc4d11425 | ||
|
d0ab4dc8d8 | ||
|
231919fbb2 | ||
|
22e3b0e981 | ||
|
019ed04a51 | ||
|
e5b955ecc8 | ||
|
e94db8a045 | ||
|
49b7aced87 | ||
|
0bd13d1828 | ||
|
179944495e | ||
|
0308cf0c0c | ||
|
11d32d4568 | ||
|
db915d8812 | ||
|
5821caa249 | ||
|
8435c3111e | ||
|
edec095055 | ||
|
06529e1924 | ||
|
ddebd9a414 | ||
|
1acddf4e22 | ||
|
569945cd37 | ||
|
b088720d40 | ||
|
b3bb316224 | ||
|
23ece09407 | ||
|
1864facda8 | ||
|
d15a9405cd | ||
|
d4742a76e5 | ||
|
f107cc2076 | ||
|
0226b08429 | ||
|
255ecbd709 | ||
|
8c66fa8d1e | ||
|
7405ddc852 | ||
|
0cbc1c9c07 | ||
|
c628fa39a1 | ||
|
a0f214a9f9 | ||
|
153f6f862e | ||
|
610979555d | ||
|
ababa18157 | ||
|
f52dfcfaef | ||
|
8b8ea0d4a3 | ||
|
42b20e7651 | ||
|
08992de960 | ||
|
1d8f526d91 | ||
|
fda73f3212 | ||
|
22cee49747 | ||
|
59c128c54a | ||
|
a116127436 | ||
|
9ada606919 | ||
|
1e778d259e | ||
|
b005803bf6 | ||
|
21dc3fe664 | ||
|
16d42dead3 | ||
|
bf471838ae | ||
|
29eba82093 | ||
|
e6e9d9ea22 | ||
|
f24e00f4b4 | ||
|
4a39dadd0d | ||
|
616fc10837 | ||
|
2f73bbe96c | ||
|
b667d8b3a3 | ||
|
441d70c7e5 | ||
|
972532cf6c | ||
|
e467f80149 | ||
|
d97fac6ab0 | ||
|
dfefdf24cb | ||
|
20e98aefa4 | ||
|
ff16350aa1 | ||
|
d20a6a9d56 | ||
|
77448ba623 | ||
|
bc637414e5 | ||
|
7d59811c4f | ||
|
31bbc416a5 | ||
|
b1ab1820b6 | ||
|
5b3f44c7a3 | ||
|
1dc291b073 | ||
|
edb5268583 | ||
|
6544197ef8 | ||
|
5ac3eeab18 | ||
|
a80d38747f | ||
|
9c1a89710e | ||
|
fd7465521a | ||
|
828a9fb002 | ||
|
00490d289c | ||
|
2295b96a49 | ||
|
31d89acfae | ||
|
244ec66ecc | ||
|
f96bd2740d | ||
|
ead704022c | ||
|
9609d2ac84 | ||
|
d5fddfd504 | ||
|
62b0036437 | ||
|
9a0dbff26b | ||
|
e22b400373 | ||
|
f644c1a837 | ||
|
c33e41f2bf | ||
|
63b4e3f0a4 | ||
|
039577d9eb | ||
|
6610f4e2da | ||
|
19d9c523c5 | ||
|
99f93939db | ||
|
8338db0480 | ||
|
f95d1e81ea | ||
|
33e3c9c338 | ||
|
376e927505 | ||
|
ead1b864bc | ||
|
b2cea8b538 | ||
|
012082d1d9 | ||
|
9bb8e73277 | ||
|
627cc786a5 | ||
|
c45fa1a4b1 | ||
|
54263d50c0 | ||
|
e1b4d438db | ||
|
e344c8a441 | ||
|
90b51e1a5d | ||
|
396d3370dc | ||
|
d591eccd9f | ||
|
bdbb30c604 | ||
|
89fa0c3e67 | ||
|
9cb6b4e6f3 | ||
|
d8d032f0f6 | ||
|
c0ae3ccdaf | ||
|
7c19777dff | ||
|
779774b162 | ||
|
b4be42acdf | ||
|
25cfebbd37 | ||
|
0f3ebaa20b | ||
|
21e0c830a6 | ||
|
4811cd198f | ||
|
7249e2611a | ||
|
b5d3341995 | ||
|
98807286f7 | ||
|
f39f0f5517 | ||
|
23169603fc | ||
|
6f8aed800e | ||
|
1a1bf17c6a | ||
|
cd38568aaa | ||
|
9ee62b014e | ||
|
4a096b14ec | ||
|
427ac1b5d5 | ||
|
2bddfe2225 | ||
|
0d954b6ecc | ||
|
644f97634b | ||
|
6cce6742eb | ||
|
e32bbb726d | ||
|
5fb970b3f1 | ||
|
0ea7ffa601 | ||
|
4bc9dfc6f8 | ||
|
c7fcd4eb48 | ||
|
90d0156291 | ||
|
0070cb4039 | ||
|
e4c743cf8c | ||
|
e7de069bd5 | ||
|
7ff3e9399f | ||
|
1417a43697 | ||
|
dd4a5076f6 | ||
|
e664c4455e | ||
|
18a5639cb7 | ||
|
7ba98ce773 | ||
|
a95d2ad140 | ||
|
bfebcd7bee | ||
|
0063bd6c15 | ||
|
73e75b6314 | ||
|
07c070d795 | ||
|
af31ca7ad8 | ||
|
849c15dbf8 | ||
|
84f13650a2 | ||
|
aed406eade | ||
|
5f5c7d89be | ||
|
5a1caf0549 | ||
|
a3d050ff6b | ||
|
0262f59a3f | ||
|
b65aa4e657 | ||
|
b1cd086177 | ||
|
0c33cfa88f | ||
|
1dc2b939cb | ||
|
b0ee36feb4 | ||
|
1d15ae7f3e | ||
|
68bfde953d | ||
|
c4e06ba528 | ||
|
4d4282b60f | ||
|
450a1932e5 | ||
|
6bdead3fe3 | ||
|
85f4c774a6 | ||
|
e2e6d70ea8 | ||
|
0ddb7519bb | ||
|
1ec4927f69 | ||
|
c4fe304efe | ||
|
e7513e356a | ||
|
326e0b4719 | ||
|
86503e2e35 | ||
|
a400f27dce | ||
|
48c6f56640 | ||
|
eed9cc7f1b | ||
|
2f63387864 | ||
|
be12d6f273 | ||
|
21a7611820 | ||
|
ff0733d013 | ||
|
7428cb8a31 | ||
|
62de403f27 | ||
|
eb7ff7048d | ||
|
b5b27c95b1 | ||
|
6a4c815b6d | ||
|
da70134c38 | ||
|
6866779d26 | ||
|
18954ea655 | ||
|
05dbc0fb96 | ||
|
31601d710b | ||
|
d8f41e854f | ||
|
6c40fec35a | ||
|
c7d8cf4ea2 | ||
|
7dbd72cebf | ||
|
a4edb34deb | ||
|
bfff27ef34 | ||
|
ec43a2e01a | ||
|
d70c8e87e5 | ||
|
4e43a2cbcd | ||
|
ce0272b447 | ||
|
a7af4663b8 | ||
|
7f99bf478f | ||
|
9327e75a16 | ||
|
d398e59ced | ||
|
737da333fb | ||
|
caa50d6db9 | ||
|
b886742968 | ||
|
046bd02d6c | ||
|
76e1c33b68 | ||
|
bd8722652a | ||
|
33cb80335e | ||
|
d1f49bedbd | ||
|
b02d25e62c | ||
|
503ac4619a | ||
|
ec54d6152b | ||
|
715146cdd1 | ||
|
573a77ce0c | ||
|
a30b3104a0 | ||
|
cf4cefc2bc | ||
|
ef93557a5d | ||
|
3170951620 | ||
|
00d547dc06 | ||
|
8817e8e010 | ||
|
ea6a21a13b | ||
|
672ff0b7d6 | ||
|
804daa188e | ||
|
f123e5c954 | ||
|
227f0b867d | ||
|
d87c1aba5c | ||
|
6e7cf27579 | ||
|
90623f68d7 | ||
|
00caed811b | ||
|
9ae8b9f26e | ||
|
7d326c44d8 | ||
|
49f34d3243 | ||
|
d44ec9e809 | ||
|
a3ef1be1ab | ||
|
ad8693cb8a | ||
|
dbeaf46631 | ||
|
0caaa1fa3b | ||
|
23fce6385f | ||
|
189d474633 | ||
|
86fa8b1301 | ||
|
cc39b3ecbf | ||
|
64c463e889 | ||
|
4c327a6c16 | ||
|
0d93d190f8 | ||
|
56e91d008e | ||
|
fcb9c24bb5 | ||
|
49ec89b28f | ||
|
03d7d1851c | ||
|
d9aee43129 | ||
|
ccb9486d21 | ||
|
a19fc3d208 | ||
|
8e02fdc537 | ||
|
ea13c9edbf | ||
|
c3d69c7c2b | ||
|
4227404aee | ||
|
f4008f0fb2 | ||
|
39df711eac | ||
|
942d4995c5 | ||
|
392b567f23 | ||
|
48b44fbdc5 | ||
|
c962afc566 | ||
|
2f2b54a3d6 | ||
|
24b6b736b2 | ||
|
bedbd32fb7 | ||
|
34500bd8b2 | ||
|
0a6eaa1173 | ||
|
c02c218c71 | ||
|
1b42de0658 | ||
|
ec2ff0d4bf | ||
|
54456b1160 | ||
|
c1a62fdb05 | ||
|
e515964a73 | ||
|
15672ab66c | ||
|
a59b2803be | ||
|
dbafdf849e | ||
|
750ce383ec | ||
|
ec77f439fb | ||
|
1b6fb4c29c | ||
|
cd1cc911e7 | ||
|
ddc3a01f09 | ||
|
6e64983f82 | ||
|
a032367e26 | ||
|
5a90e9bd64 | ||
|
89369fd8ec | ||
|
6195029215 | ||
|
8ba146071d | ||
|
3a4f520da7 | ||
|
300b58093b | ||
|
f44ff2cfe7 | ||
|
4bc3b7885c | ||
|
fe5c3bdee5 | ||
|
e31947a452 | ||
|
f0c2f12222 | ||
|
4e9ad1fd75 | ||
|
82b1dbd836 | ||
|
9452655fcd | ||
|
01e084a9ee | ||
|
9201df1b39 | ||
|
1b22f95a86 | ||
|
2580750d4c | ||
|
72e8607e47 | ||
|
06490cae8b | ||
|
c7f114d90b | ||
|
62748279d4 | ||
|
f140149127 | ||
|
ec0985619f | ||
|
f265320c3c | ||
|
63299157d8 | ||
|
98b4355b94 | ||
|
d6b16ba0ec | ||
|
afbc327ea7 | ||
|
6c55f20f43 | ||
|
4418f4f975 | ||
|
b3791378fa | ||
|
568cae584b | ||
|
ccbc91a97f | ||
|
66108bec84 | ||
|
87729589e0 | ||
|
9b94ac2f06 | ||
|
f2678b4a10 | ||
|
bcf396868a | ||
|
5c57d3b379 | ||
|
b141ed2d72 | ||
|
7762430bc3 | ||
|
68eb325d18 | ||
|
a1b1436f7d | ||
|
2913f07050 | ||
|
45baa5f185 | ||
|
3e067cc60f | ||
|
2a4c0a111a | ||
|
8aa6dc17a5 | ||
|
f9a366e69a | ||
|
61fe317faf | ||
|
5e7cf2aacb | ||
|
dc6e814de3 | ||
|
d92a2dab21 | ||
|
48c5e64a08 | ||
|
d71b45d60e | ||
|
ddfeee314c | ||
|
58e0c7a178 | ||
|
17520876d8 | ||
|
949b853678 | ||
|
fde7df534d | ||
|
97e8a9e200 | ||
|
6d08c342c9 | ||
|
50cbfb4a44 | ||
|
9387548aa1 | ||
|
60df83f47e | ||
|
6d45a22ba4 | ||
|
cf276340a4 | ||
|
06c6c4bd97 | ||
|
15374a31dd | ||
|
f661f38215 | ||
|
481cdc9a82 | ||
|
0c984a83bb | ||
|
b5106a06c9 | ||
|
74854d55a0 | ||
|
ff63e07b05 | ||
|
6e358eb26c | ||
|
7b05673d8d | ||
|
58453c31f7 | ||
|
d70a9a5e53 | ||
|
0c32982171 | ||
|
d6add77540 | ||
|
1da3801614 | ||
|
c4f11a577e | ||
|
2cbccb87db | ||
|
48c33302a8 | ||
|
27ce24b5fe | ||
|
2123946dbd | ||
|
17a8c26fc9 | ||
|
1fdb3da7fb | ||
|
98e397ce44 | ||
|
4e26de3588 | ||
|
7705e50e44 | ||
|
022c490011 | ||
|
e65638f6f4 | ||
|
8747ff8954 | ||
|
46816a7cda | ||
|
eb9ac66af8 | ||
|
94525c0025 | ||
|
944ccebc08 | ||
|
ccecc3ce1b | ||
|
dca2091351 | ||
|
7310b25730 | ||
|
8cd154776e | ||
|
4f223ba11d | ||
|
918bbff2bd | ||
|
9944a689bf | ||
|
8b17953fe5 | ||
|
61f6ccfe76 | ||
|
90978ea78d | ||
|
7c3737609f | ||
|
b534bfbbf1 | ||
|
88db2f8a21 | ||
|
5cbaf973e1 | ||
|
5c6f69c254 | ||
|
178b179db3 | ||
|
a3f18b51f7 | ||
|
f6c3fe3758 | ||
|
b97cad0f4a | ||
|
bb75586aa5 | ||
|
9e2c1dae87 | ||
|
25e341e9d9 | ||
|
3536280fac | ||
|
9c563d5c00 | ||
|
ea05c92723 | ||
|
0af2fe79f2 | ||
|
9d37f4c0d9 | ||
|
11c9429fab | ||
|
b18dfd670f | ||
|
533b8b8b5f | ||
|
e0a63ec792 | ||
|
c4ae03454a | ||
|
3c0ed787ba | ||
|
dbccf9b196 | ||
|
0bb689d340 | ||
|
90f35f26a2 | ||
|
0fd5abc833 | ||
|
42d8ac07f9 | ||
|
4ea6cbe839 | ||
|
fc9c5e85aa | ||
|
cac011246e | ||
|
98ce1ccc69 | ||
|
dde2747918 | ||
|
cf4cccf4d6 | ||
|
68dbcc9ca3 | ||
|
b2b5d6f080 | ||
|
d9ce567cc8 | ||
|
4207adc098 | ||
|
6bdb7b7f80 | ||
|
af60f1ee4d | ||
|
a1325eeb3f | ||
|
55f6d6cf47 | ||
|
6e95b81aee | ||
|
e649ef307a | ||
|
3811181f49 | ||
|
7aebf2dbf7 | ||
|
eaadae4a5b | ||
|
9b449745ab | ||
|
7f54b1f7e3 | ||
|
cf3ca86246 | ||
|
57f5ee3783 | ||
|
b17b1c3336 | ||
|
1487741f37 | ||
|
0c3a657800 | ||
|
440d38d3bc | ||
|
fb0ca607e2 | ||
|
cb40e111a3 | ||
|
f7248dec76 | ||
|
d46ad09108 | ||
|
c5efd1d65b | ||
|
1e34982bda | ||
|
a1e06b2e61 | ||
|
6050af4e67 | ||
|
39f378ece7 | ||
|
5f5c8118c0 | ||
|
2023b427ae | ||
|
c97438cd7d | ||
|
537dbabe8f | ||
|
d22385cc93 | ||
|
79ebc54538 | ||
|
7cc863df37 | ||
|
6ff427d241 | ||
|
46477c75c4 | ||
|
524a01a3a4 | ||
|
2ccc631ff3 |
403 changed files with 50854 additions and 31368 deletions
11
.gitattributes
vendored
11
.gitattributes
vendored
|
@ -1,2 +1,13 @@
|
|||
# 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
8
.gitignore
vendored
|
@ -1,5 +1,7 @@
|
|||
.idea/
|
||||
composer.phar
|
||||
.phpcs-cache
|
||||
composer.lock
|
||||
composer.phar
|
||||
phpcs.xml
|
||||
phpstan.neon
|
||||
vendor/
|
||||
bin/
|
||||
/.idea
|
||||
|
|
29
.scrutinizer.yml
Normal file
29
.scrutinizer.yml
Normal file
|
@ -0,0 +1,29 @@
|
|||
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
|
78
.travis.yml
78
.travis.yml
|
@ -1,34 +1,66 @@
|
|||
dist: trusty
|
||||
language: php
|
||||
|
||||
# Required for HHVM, see https://github.com/travis-ci/travis-ci/issues/7712
|
||||
dist: trusty
|
||||
|
||||
php:
|
||||
- 5.5
|
||||
- 5.6
|
||||
- 7.0
|
||||
- 7.1
|
||||
- hhvm
|
||||
- nightly
|
||||
- 7.1
|
||||
- 7.2
|
||||
- 7.3
|
||||
- 7.4snapshot
|
||||
- nightly
|
||||
|
||||
env:
|
||||
matrix:
|
||||
- EXECUTOR= DEPENDENCIES=--prefer-lowest
|
||||
- EXECUTOR=coroutine DEPENDENCIES=--prefer-lowest
|
||||
- EXECUTOR=
|
||||
- EXECUTOR=coroutine
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- php: nightly
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_PHP_VERSION" != "5.6" && "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then phpenv config-rm xdebug.ini || true; fi
|
||||
- composer selfupdate
|
||||
- mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{,.disabled} || echo "xdebug not available"
|
||||
- travis_retry composer self-update
|
||||
|
||||
install:
|
||||
- composer install --dev --prefer-dist
|
||||
- composer require react/promise:2.*
|
||||
- composer require psr/http-message:1.*
|
||||
install: travis_retry composer update --prefer-dist
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
after_success:
|
||||
- if [ "$TRAVIS_PHP_VERSION" == "5.6" ]; then composer require "satooshi/php-coveralls:^1.0" && travis_retry php bin/coveralls -v; fi
|
||||
|
|
108
CHANGELOG.md
108
CHANGELOG.md
|
@ -1,5 +1,111 @@
|
|||
# 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)
|
||||
|
||||
#### v0.10.1
|
||||
- Fixed infinite loop in the server (see #153)
|
||||
|
||||
|
@ -91,4 +197,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).
|
||||
|
|
55
CONTRIBUTING.md
Normal file
55
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,55 @@
|
|||
# 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
|
||||
a brief proposal and discuss it with us first.
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## Running tests
|
||||
```sh
|
||||
./vendor/bin/phpunit
|
||||
```
|
||||
|
||||
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 .
|
||||
```
|
41
LICENSE
41
LICENSE
|
@ -1,28 +1,21 @@
|
|||
Copyright (c) 2015, webonyx
|
||||
All rights reserved.
|
||||
MIT License
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
Copyright (c) 2015-present, Webonyx, LLC.
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
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 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 above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
603
README.md
603
README.md
|
@ -1,589 +1,52 @@
|
|||
# graphql-php
|
||||
|
||||
This is a PHP port of GraphQL reference implementation based on the [specification](https://github.com/facebook/graphql)
|
||||
and the [reference implementation in JavaScript](https://github.com/graphql/graphql-js).
|
||||
|
||||
This implementation will follow JavaScript version as close as possible until GraphQL itself stabilizes.
|
||||
|
||||
**Current status**: version 0.4+ supports all features described by specification.
|
||||
|
||||
[](https://travis-ci.org/webonyx/graphql-php)
|
||||
[](https://coveralls.io/github/webonyx/graphql-php)
|
||||
[](https://scrutinizer-ci.com/g/webonyx/graphql-php)
|
||||
[](https://packagist.org/packages/webonyx/graphql-php)
|
||||
[](https://packagist.org/packages/webonyx/graphql-php)
|
||||
|
||||
Work is in progress on new [Documentation site](http://webonyx.github.io/graphql-php/). It already
|
||||
contains more information than this Readme, so try it first.
|
||||
This is a PHP implementation of the GraphQL [specification](https://github.com/facebook/graphql)
|
||||
based on the [reference implementation in JavaScript](https://github.com/graphql/graphql-js).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Installation](#installing-graphql-php)
|
||||
- [Learn by example](#learn-by-example)
|
||||
- [Type System](#type-system)
|
||||
- [Internal Types](#internal-types)
|
||||
- [Enums](#enums)
|
||||
- [Interfaces](#interfaces)
|
||||
- [Objects](#objects)
|
||||
- Unions (TODOC)
|
||||
- [Fields](#fields)
|
||||
- [Schema definition](#schema)
|
||||
- [Query Resolution and Data Fetching](#query-resolution)
|
||||
- [HTTP endpoint example](#http-endpoint)
|
||||
- [More Examples](#more-examples)
|
||||
- [Complementary Tools](#complementary-tools)
|
||||
|
||||
## Overview
|
||||
GraphQL is intended to be a replacement for REST APIs. [Read more](http://facebook.github.io/react/blog/2015/05/01/graphql-introduction.html) about rationale behind it.
|
||||
|
||||
Example usage:
|
||||
```php
|
||||
$result = GraphQL::execute(
|
||||
StarWarsSchema::build(),
|
||||
'query HeroNameAndFriendsQuery {
|
||||
hero {
|
||||
id
|
||||
name
|
||||
friends {
|
||||
name
|
||||
}
|
||||
}
|
||||
}'
|
||||
)
|
||||
## Installation
|
||||
Via composer:
|
||||
```
|
||||
composer require webonyx/graphql-php
|
||||
```
|
||||
|
||||
Result returned:
|
||||
```php
|
||||
[
|
||||
'hero' => [
|
||||
'id' => '2001',
|
||||
'name' => 'R2-D2',
|
||||
'friends' => [
|
||||
['name' => 'Luke Skywalker'],
|
||||
['name' => 'Han Solo'],
|
||||
['name' => 'Leia Organa'],
|
||||
]
|
||||
]
|
||||
]
|
||||
```
|
||||
(see also [schema definition](https://github.com/webonyx/graphql-php/blob/master/tests/StarWarsSchema.php#L22) for type system of this example).
|
||||
## Documentation
|
||||
Full documentation is available on the [Documentation site](https://webonyx.github.io/graphql-php/) as well
|
||||
as in the [docs](docs/) folder of the distribution.
|
||||
|
||||
This PHP implementation is a thin wrapper around your existing data layer and business logic. It doesn't dictate how these layers are implemented or which storage engines are used. Instead it provides tools for creating API for your existing app. These tools include:
|
||||
- Type system
|
||||
- Schema validation and introspection
|
||||
- Ability to parse and execute GraphQL queries against type system
|
||||
If you don't know what GraphQL is, visit this [official website](http://graphql.org)
|
||||
by the Facebook engineering team.
|
||||
|
||||
Actual data fetching has to be implemented on the user land.
|
||||
## Examples
|
||||
There are several ready examples in the [examples](examples/) folder of the distribution with specific
|
||||
README file per example.
|
||||
|
||||
Check out single-file [hello world](https://gist.github.com/leocavalcante/9e61ca6065130e37737f24892d81fa40) example for quick introduction.
|
||||
## Contributors
|
||||
|
||||
## Installing graphql-php
|
||||
```
|
||||
$> curl -sS https://getcomposer.org/installer | php
|
||||
$> php composer.phar require webonyx/graphql-php="^0.9"
|
||||
```
|
||||
This project exists thanks to [all the people](https://github.com/webonyx/graphql-php/graphs/contributors) who contribute. [[Contribute](CONTRIBUTING.md)].
|
||||
|
||||
If you are upgrading, see [upgrade instructions](UPGRADE.md)
|
||||
## Backers
|
||||
|
||||
## Requirements
|
||||
PHP >=5.4
|
||||
<a href="https://opencollective.com/webonyx-graphql-php#backers" target="_blank"><img src="https://opencollective.com/webonyx-graphql-php/backers.svg?width=890"></a>
|
||||
|
||||
## Learn by example
|
||||
It is often easier to start with full-featured example and then get back to documentation
|
||||
for your own work.
|
||||
## Sponsors
|
||||
|
||||
Check out full-featured [example of GraphQL API](https://github.com/webonyx/graphql-php/tree/master/examples/01-blog).
|
||||
Follow instructions and try it yourself in ~10 minutes.
|
||||
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)]
|
||||
|
||||
## Getting Started
|
||||
First, make sure to read [Getting Started](https://github.com/facebook/graphql#getting-started) section of GraphQL documentation.
|
||||
Examples below implement the type system described in this document.
|
||||
<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>
|
||||
|
||||
### Type System
|
||||
To start using GraphQL you are expected to implement a Type system.
|
||||
## License
|
||||
|
||||
GraphQL PHP provides several *kinds* of types to build a hierarchical type system:
|
||||
`scalar`, `enum`, `object`, `interface`, `union`, `listOf`, `nonNull`.
|
||||
|
||||
#### Internal types
|
||||
Only several `scalar` types are implemented out of the box:
|
||||
`ID`, `String`, `Int`, `Float`, `Boolean`
|
||||
|
||||
As well as two internal modifier types: `ListOf` and `NonNull`.
|
||||
|
||||
All internal types are exposed as static methods of `GraphQL\Type\Definition\Type` class:
|
||||
|
||||
```php
|
||||
use GraphQL\Type\Definition\Type;
|
||||
|
||||
// Internal Scalar types:
|
||||
Type::string(); // String type
|
||||
Type::int(); // Int type
|
||||
Type::float(); // Float type
|
||||
Type::boolean(); // Boolean type
|
||||
Type::id(); // ID type
|
||||
|
||||
// Internal wrapping types:
|
||||
Type::nonNull(Type::string()) // String! type
|
||||
Type::listOf(Type::string()) // String[] type
|
||||
```
|
||||
|
||||
Other types must be implemented by your application. Most often you will work with `enum`, `object`, `interface` and `union` type *kinds* to build a type system.
|
||||
|
||||
#### Enums
|
||||
Enum types represent a set of allowed values for an object field. Let's define `enum` type describing the set of episodes of original Star Wars trilogy:
|
||||
|
||||
```php
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
|
||||
/**
|
||||
* The original trilogy consists of three movies.
|
||||
*
|
||||
* This implements the following type system shorthand:
|
||||
* enum Episode { NEWHOPE, EMPIRE, JEDI }
|
||||
*/
|
||||
$episodeEnum = new EnumType([
|
||||
'name' => 'Episode',
|
||||
'description' => 'One of the films in the Star Wars Trilogy',
|
||||
'values' => [
|
||||
'NEWHOPE' => [
|
||||
'value' => 4,
|
||||
'description' => 'Released in 1977.'
|
||||
],
|
||||
'EMPIRE' => [
|
||||
'value' => 5,
|
||||
'description' => 'Released in 1980.'
|
||||
],
|
||||
'JEDI' => [
|
||||
'value' => 6,
|
||||
'description' => 'Released in 1983.'
|
||||
],
|
||||
]
|
||||
]);
|
||||
```
|
||||
|
||||
#### Interfaces
|
||||
Next, let's define a `Character` interface, describing characters of original Star Wars trilogy:
|
||||
|
||||
```php
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
|
||||
// Implementor types (will be defined in next examples):
|
||||
$humanType = null;
|
||||
$droidType = null;
|
||||
|
||||
/**
|
||||
* Characters in the Star Wars trilogy are either humans or droids.
|
||||
*
|
||||
* This implements the following type system shorthand:
|
||||
* interface Character {
|
||||
* id: String!
|
||||
* name: String
|
||||
* friends: [Character]
|
||||
* appearsIn: [Episode]
|
||||
* }
|
||||
*/
|
||||
$characterInterface = new InterfaceType([
|
||||
'name' => 'Character',
|
||||
'description' => 'A character in the Star Wars Trilogy',
|
||||
'fields' => [
|
||||
'id' => [
|
||||
'type' => Type::nonNull(Type::string()),
|
||||
'description' => 'The id of the character.',
|
||||
],
|
||||
'name' => [
|
||||
'type' => Type::string(),
|
||||
'description' => 'The name of the character.'
|
||||
],
|
||||
'friends' => [
|
||||
'type' => function () use (&$characterInterface) {
|
||||
return Type::listOf($characterInterface);
|
||||
},
|
||||
'description' => 'The friends of the character.',
|
||||
],
|
||||
'appearsIn' => [
|
||||
'type' => Type::listOf($episodeEnum),
|
||||
'description' => 'Which movies they appear in.'
|
||||
]
|
||||
],
|
||||
'resolveType' => function ($obj) use (&$humanType, &$droidType) {
|
||||
$humans = StarWarsData::humans();
|
||||
$droids = StarWarsData::droids();
|
||||
if (isset($humans[$obj['id']])) {
|
||||
return $humanType;
|
||||
}
|
||||
if (isset($droids[$obj['id']])) {
|
||||
return $droidType;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
As you can see `type` may be optionally defined as `callback` that returns actual type at runtime. (see [Fields](#fields) section for details)
|
||||
|
||||
In this example field `friends` represents a list of `characterInterface`. Since at the moment of type definition `characterInterface` is still not defined, we pass in `closure` that will return this type at runtime.
|
||||
|
||||
**Interface definition options:**
|
||||
|
||||
Option | Type | Notes
|
||||
------ | ---- | -----
|
||||
name | `string` | Required. Unique name of this interface type within Schema
|
||||
fields | `array` | Required. List of fields required to be defined by interface implementors. See [Fields](#fields) section for available options.
|
||||
description | `string` | Textual description of this interface for clients
|
||||
resolveType | `callback($value, $context, ResolveInfo $info) => objectType` | Any `callable` that receives data from data layer of your application and returns concrete interface implementor for that data.
|
||||
|
||||
|
||||
**Notes**:
|
||||
|
||||
1. If `resolveType` option is omitted, GraphQL PHP will loop through all interface implementers and use their `isTypeOf()` method to pick the first suitable one. This is obviously less efficient than single `resolveType` call. So it is recommended to define `resolveType` when possible.
|
||||
|
||||
2. Interface types do not participate in data fetching. They just resolve actual `object` type which will be asked for data when GraphQL query is executed.
|
||||
|
||||
|
||||
#### Objects
|
||||
Now let's define `Human` type that implements `CharacterInterface` from example above:
|
||||
|
||||
```php
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
|
||||
/**
|
||||
* We define our human type, which implements the character interface.
|
||||
*
|
||||
* This implements the following type system shorthand:
|
||||
* type Human : Character {
|
||||
* id: String!
|
||||
* name: String
|
||||
* friends: [Character]
|
||||
* appearsIn: [Episode]
|
||||
* }
|
||||
*/
|
||||
$humanType = new ObjectType([
|
||||
'name' => 'Human',
|
||||
'description' => 'A humanoid creature in the Star Wars universe.',
|
||||
'fields' => [
|
||||
'id' => [
|
||||
'type' => Type::nonNull(Type::string()),
|
||||
'description' => 'The id of the human.',
|
||||
],
|
||||
'name' => [
|
||||
'type' => Type::string(),
|
||||
'description' => 'The name of the human.',
|
||||
],
|
||||
'friends' => [
|
||||
'type' => Type::listOf($characterInterface),
|
||||
'description' => 'The friends of the human',
|
||||
'resolve' => function ($human) {
|
||||
return StarWarsData::getFriends($human);
|
||||
},
|
||||
],
|
||||
'appearsIn' => [
|
||||
'type' => Type::listOf($episodeEnum),
|
||||
'description' => 'Which movies they appear in.'
|
||||
],
|
||||
'homePlanet' => [
|
||||
'type' => Type::string(),
|
||||
'description' => 'The home planet of the human, or null if unknown.'
|
||||
],
|
||||
],
|
||||
'interfaces' => [$characterInterface]
|
||||
]);
|
||||
```
|
||||
|
||||
**Object definition options**
|
||||
|
||||
Option | Type | Notes
|
||||
------ | ---- | -----
|
||||
name | `string` | Required. Unique name of this object type within Schema
|
||||
fields | `array` | Required. List of fields describing object properties. See [Fields](#fields) section for available options.
|
||||
description | `string` | Textual description of this type for clients
|
||||
interfaces | `array` or `callback() => ObjectType[]` | List of interfaces implemented by this type (or callback returning list of interfaces)
|
||||
isTypeOf | `callback($value, $context, GraphQL\Type\Definition\ResolveInfo $info)` | Callback that takes `$value` provided by your data layer and returns true if that `$value` qualifies for this type
|
||||
|
||||
**Notes:**
|
||||
|
||||
1. Both `object` types and `interface` types define set of fields which can have their own types. That's how type composition is implemented.
|
||||
|
||||
2. Object types are responsible for data fetching. Each of their fields may have optional `resolve` callback option. This callback takes `$value` that corresponds to instance of this type and returns `data` accepted by type of given field.
|
||||
If `resolve` option is not set, GraphQL will try to get `data` from `$value[$fieldName]`.
|
||||
|
||||
3. `resolve` callback is a place where you can use your existing data fetching logic. `$context` is defined by your application on the top level of query execution (useful for storing current user, environment details, etc)
|
||||
|
||||
4. Other `ObjectType` mentioned in examples is `Droid`. Check out tests for this type: https://github.com/webonyx/graphql-php/blob/master/tests/StarWarsSchema.php
|
||||
|
||||
|
||||
#### Unions
|
||||
TODOC
|
||||
|
||||
#### Fields
|
||||
|
||||
Fields are parts of [Object](#objects) and [Interface](#interfaces) type definitions.
|
||||
|
||||
Allowed Field definition options:
|
||||
|
||||
Option | Type | Notes
|
||||
------ | ---- | -----
|
||||
name | `string` | Required. Name of the field. If not set - GraphQL will look use `key` of fields array on type definition.
|
||||
type | `Type` or `callback() => Type` | Required. One of internal or custom types. Alternatively - callback that returns `type`.
|
||||
args | `array` | Array of possible type arguments. Each entry is expected to be an array with following keys: **name** (`string`), **type** (`Type` or `callback() => Type`), **defaultValue** (`any`)
|
||||
resolve | `callback($value, $args, $context, ResolveInfo $info) => $fieldValue` | Function that receives `$value` of parent type and returns value for this field. `$context` is also defined by your application in the root call to `GraphQL::execute()`
|
||||
description | `string` | Field description for clients
|
||||
deprecationReason | `string` | Text describing why this field is deprecated. When not empty - field will not be returned by introspection queries (unless forced)
|
||||
|
||||
|
||||
### Schema
|
||||
After all of your types are defined, you must define schema. Schema consists of two special root-level types: `Query` and `Mutation`
|
||||
|
||||
`Query` type is a surface of your *read* API. `Mutation` type exposes *write* API by declaring all possible mutations in your app.
|
||||
|
||||
Example schema:
|
||||
```php
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Schema;
|
||||
|
||||
/**
|
||||
* This is the type that will be the root of our query, and the
|
||||
* entry point into our schema. It gives us the ability to fetch
|
||||
* objects by their IDs, as well as to fetch the undisputed hero
|
||||
* of the Star Wars trilogy, R2-D2, directly.
|
||||
*
|
||||
* This implements the following type system shorthand:
|
||||
* type Query {
|
||||
* hero(episode: Episode): Character
|
||||
* human(id: String!): Human
|
||||
* droid(id: String!): Droid
|
||||
* }
|
||||
*
|
||||
*/
|
||||
$queryType = new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => [
|
||||
'hero' => [
|
||||
'type' => $characterInterface,
|
||||
'args' => [
|
||||
'episode' => [
|
||||
'description' => 'If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode.',
|
||||
'type' => $episodeEnum
|
||||
]
|
||||
],
|
||||
'resolve' => function ($root, $args) {
|
||||
return StarWarsData::getHero(isset($args['episode']) ? $args['episode'] : null);
|
||||
},
|
||||
],
|
||||
'human' => [
|
||||
'type' => $humanType,
|
||||
'args' => [
|
||||
'id' => [
|
||||
'name' => 'id',
|
||||
'description' => 'id of the human',
|
||||
'type' => Type::nonNull(Type::string())
|
||||
]
|
||||
],
|
||||
'resolve' => function ($root, $args) {
|
||||
$humans = StarWarsData::humans();
|
||||
return isset($humans[$args['id']]) ? $humans[$args['id']] : null;
|
||||
}
|
||||
],
|
||||
'droid' => [
|
||||
'type' => $droidType,
|
||||
'args' => [
|
||||
'id' => [
|
||||
'name' => 'id',
|
||||
'description' => 'id of the droid',
|
||||
'type' => Type::nonNull(Type::string())
|
||||
]
|
||||
],
|
||||
'resolve' => function ($root, $args) {
|
||||
$droids = StarWarsData::droids();
|
||||
return isset($droids[$args['id']]) ? $droids[$args['id']] : null;
|
||||
}
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
// TODOC
|
||||
$mutationType = null;
|
||||
|
||||
$schema = new Schema([
|
||||
'query' => $queryType,
|
||||
'mutation' => $mutationType,
|
||||
|
||||
// We need to pass the types that implement interfaces in case the types are only created on demand.
|
||||
// This ensures that they are available during query validation phase for interfaces.
|
||||
'types' => [
|
||||
$humanType,
|
||||
$droidType
|
||||
]
|
||||
]);
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
|
||||
1. `Query` is a regular `object` type.
|
||||
|
||||
2. Fields of this type represent all possible root-level queries to your API.
|
||||
|
||||
3. Fields can have `args`, so that your queries could be dynamic (see [Fields](#fields) section).
|
||||
|
||||
|
||||
### Query Resolution
|
||||
Resolution is a cascading process that starts from root `Query` type.
|
||||
|
||||
In our example `Query` type exposes field `human` that expects `id` argument. Say we receive following GraphQL query that requests data for Luke Skywalker:
|
||||
```
|
||||
query FetchLukeQuery {
|
||||
human(id: "1000") {
|
||||
name
|
||||
friends {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And that's how our data for Luke looks like (in some internal storage):
|
||||
```
|
||||
$lukeData = [
|
||||
'id' => '1000',
|
||||
'name' => 'Luke Skywalker',
|
||||
'friends' => ['1002', '1003', '2000', '2001'],
|
||||
'appearsIn' => [4, 5, 6],
|
||||
'homePlanet' => 'Tatooine',
|
||||
]
|
||||
```
|
||||
|
||||
What happens:
|
||||
|
||||
1. GraphQL query is parsed and validated against schema (it happens in `GraphQL\GraphQL::execute()` method)
|
||||
2. GraphQL executor detects that field `human` of `Human` type is requested at root `Query` level
|
||||
3. It calls `resolve(null, ['id' => 1000])` on this field (note first argument is null at the root level)
|
||||
4. `resolve` callback of `human` field fetches our data by id and returns it
|
||||
5. Since field `human` is expected to return type `Human` GraphQL traverses all requested fields of type `Human` and matches them against `$lukeData`
|
||||
6. Requested field `name` on `Human` type does not provide any `resolve` callback, so GraphQL simply resolves it as `$lukeData['name']`
|
||||
7. Requested field `friend` has `resolve` callback, so it is called: `resolve($lukeData, /*args*/ [], ResolveInfo $info)`
|
||||
8. Callback fetches data for all `$lukeData['friends']` and returns `[$friend1002, $friend1003, ...]` where each entry contains array with same structure as `$lukeData`
|
||||
9. GraphQL executor repeats these steps until all requested leaf fields are reached
|
||||
10. Final result is composed and returned:
|
||||
```
|
||||
[
|
||||
'human' => [
|
||||
'name' => 'Luke Skywalker',
|
||||
'friends' => [
|
||||
['name' => 'Han Solo'],
|
||||
['name' => 'Leia Organa'],
|
||||
['name' => 'C-3PO'],
|
||||
['name' => 'R2-D2'],
|
||||
]
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
### HTTP endpoint
|
||||
Specification for GraphQL HTTP endpoint is still under development.
|
||||
But you can use following naive example to build your own custom HTTP endpoint that is ready to accept GraphQL queries:
|
||||
|
||||
```php
|
||||
use GraphQL\GraphQL;
|
||||
use \Exception;
|
||||
|
||||
if (isset($_SERVER['CONTENT_TYPE']) && $_SERVER['CONTENT_TYPE'] === 'application/json') {
|
||||
$rawBody = file_get_contents('php://input');
|
||||
$data = json_decode($rawBody ?: '', true);
|
||||
} else {
|
||||
$data = $_POST;
|
||||
}
|
||||
|
||||
$requestString = isset($data['query']) ? $data['query'] : null;
|
||||
$operationName = isset($data['operation']) ? $data['operation'] : null;
|
||||
$variableValues = isset($data['variables']) ? $data['variables'] : null;
|
||||
|
||||
try {
|
||||
// Define your schema:
|
||||
$schema = MyApp\Schema::build();
|
||||
$result = GraphQL::execute(
|
||||
$schema,
|
||||
$requestString,
|
||||
/* $rootValue */ null,
|
||||
/* $context */ null, // A custom context that can be used to pass current User object etc to all resolvers.
|
||||
$variableValues,
|
||||
$operationName
|
||||
);
|
||||
} catch (Exception $exception) {
|
||||
$result = [
|
||||
'errors' => [
|
||||
['message' => $exception->getMessage()]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($result);
|
||||
```
|
||||
|
||||
### Security
|
||||
|
||||
#### Query Complexity Analysis
|
||||
|
||||
This is a PHP port of [Query Complexity Analysis](http://sangria-graphql.org/learn/#query-complexity-analysis) in Sangria implementation.
|
||||
Introspection query with description max complexity is **109**.
|
||||
|
||||
This document validator rule is disabled by default. Here an example to enabled it:
|
||||
|
||||
```php
|
||||
use GraphQL\GraphQL;
|
||||
|
||||
/** @var \GraphQL\Validator\Rules\QueryComplexity $queryComplexity */
|
||||
$queryComplexity = DocumentValidator::getRule('QueryComplexity');
|
||||
$queryComplexity->setMaxQueryComplexity($maxQueryComplexity = 110);
|
||||
|
||||
GraphQL::execute(/*...*/);
|
||||
```
|
||||
|
||||
#### Limiting Query Depth
|
||||
|
||||
This is a PHP port of [Limiting Query Depth](http://sangria-graphql.org/learn/#limiting-query-depth) in Sangria implementation.
|
||||
Introspection query with description max depth is **7**.
|
||||
|
||||
This document validator rule is disabled by default. Here an example to enabled it:
|
||||
|
||||
```php
|
||||
use GraphQL\GraphQL;
|
||||
|
||||
/** @var \GraphQL\Validator\Rules\QueryDepth $queryDepth */
|
||||
$queryDepth = DocumentValidator::getRule('QueryDepth');
|
||||
$queryDepth->setMaxQueryDepth($maxQueryDepth = 10);
|
||||
|
||||
GraphQL::execute(/*...*/);
|
||||
```
|
||||
|
||||
#### Disabling Introspection
|
||||
|
||||
This is a PHP port of [graphql-disable-introspection](https://github.com/helfer/graphql-disable-introspection).
|
||||
The rule prohibits queries that contain `__type` or `__schema` fields.
|
||||
|
||||
This document validator rule is disabled by default. Here an example to enable it:
|
||||
|
||||
```php
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Validator\Rules\DisableIntrospection;
|
||||
|
||||
/** @var \GraphQL\Validator\Rules\DisableIntrospection $disableIntrospection */
|
||||
$disableIntrospection = DocumentValidator::getRule('DisableIntrospection');
|
||||
$disableIntrospection->setEnabled(DisableIntrospection::ENABLED);
|
||||
|
||||
GraphQL::execute(/*...*/);
|
||||
```
|
||||
|
||||
### More Examples
|
||||
Make sure to check [tests](https://github.com/webonyx/graphql-php/tree/master/tests) for more usage examples.
|
||||
|
||||
### Complementary Tools
|
||||
- [Integration with Relay](https://github.com/ivome/graphql-relay-php)
|
||||
- [Use GraphQL with Laravel 5](https://github.com/Folkloreatelier/laravel-graphql)
|
||||
- [Relay helpers for laravel-graphql](https://github.com/nuwave/laravel-graphql-relay)
|
||||
- [GraphQL and Relay with Symfony2](https://github.com/overblog/GraphQLBundle)
|
||||
|
||||
Also check [Awesome GraphQL](https://github.com/chentsulin/awesome-graphql) for full picture of GraphQL ecosystem.
|
||||
See [LICENSE](LICENSE).
|
||||
|
|
219
UPGRADE.md
219
UPGRADE.md
|
@ -1,3 +1,222 @@
|
|||
## 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
|
||||
|
|
|
@ -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\Type\LazyResolution;
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Type\SchemaConfig;
|
||||
|
||||
/**
|
||||
* @BeforeMethods({"setUp"})
|
||||
|
@ -16,18 +16,12 @@ use GraphQL\Type\LazyResolution;
|
|||
*/
|
||||
class HugeSchemaBench
|
||||
{
|
||||
/**
|
||||
* @var SchemaGenerator
|
||||
*/
|
||||
/** @var SchemaGenerator */
|
||||
private $schemaBuilder;
|
||||
|
||||
private $schema;
|
||||
|
||||
private $lazySchema;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
private $smallQuery;
|
||||
|
||||
public function setUp()
|
||||
|
@ -36,12 +30,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();
|
||||
}
|
||||
|
||||
|
@ -49,8 +43,7 @@ class HugeSchemaBench
|
|||
{
|
||||
$this->schemaBuilder
|
||||
->buildSchema()
|
||||
->getTypeMap()
|
||||
;
|
||||
->getTypeMap();
|
||||
}
|
||||
|
||||
public function benchSchemaLazy()
|
||||
|
@ -60,21 +53,21 @@ class HugeSchemaBench
|
|||
|
||||
public function benchSmallQuery()
|
||||
{
|
||||
$result = GraphQL::execute($this->schema, $this->smallQuery);
|
||||
$result = GraphQL::executeQuery($this->schema, $this->smallQuery);
|
||||
}
|
||||
|
||||
public function benchSmallQueryLazy()
|
||||
{
|
||||
$schema = $this->createLazySchema();
|
||||
$result = GraphQL::execute($schema, $this->smallQuery);
|
||||
$result = GraphQL::executeQuery($schema, $this->smallQuery);
|
||||
}
|
||||
|
||||
private function createLazySchema()
|
||||
{
|
||||
return new Schema(
|
||||
\GraphQL\Type\SchemaConfig::create()
|
||||
SchemaConfig::create()
|
||||
->setQuery($this->schemaBuilder->buildQueryType())
|
||||
->setTypeLoader(function($name) {
|
||||
->setTypeLoader(function ($name) {
|
||||
return $this->schemaBuilder->loadType($name);
|
||||
})
|
||||
);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
namespace GraphQL\Benchmarks;
|
||||
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Tests\StarWarsSchema;
|
||||
use GraphQL\Type\Introspection;
|
||||
|
@ -35,7 +36,7 @@ class StarWarsBench
|
|||
}
|
||||
';
|
||||
|
||||
GraphQL::execute(
|
||||
GraphQL::executeQuery(
|
||||
StarWarsSchema::build(),
|
||||
$q
|
||||
);
|
||||
|
@ -57,7 +58,7 @@ class StarWarsBench
|
|||
}
|
||||
}
|
||||
';
|
||||
GraphQL::execute(
|
||||
GraphQL::executeQuery(
|
||||
StarWarsSchema::build(),
|
||||
$q
|
||||
);
|
||||
|
@ -81,7 +82,7 @@ class StarWarsBench
|
|||
}
|
||||
';
|
||||
|
||||
GraphQL::execute(
|
||||
GraphQL::executeQuery(
|
||||
StarWarsSchema::build(),
|
||||
$q
|
||||
);
|
||||
|
@ -89,7 +90,7 @@ class StarWarsBench
|
|||
|
||||
public function benchStarWarsIntrospectionQuery()
|
||||
{
|
||||
GraphQL::execute(
|
||||
GraphQL::executeQuery(
|
||||
StarWarsSchema::build(),
|
||||
$this->introQuery
|
||||
);
|
||||
|
|
|
@ -7,12 +7,15 @@ 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
|
||||
{
|
||||
|
@ -30,12 +33,14 @@ class QueryGenerator
|
|||
|
||||
$totalFields = 0;
|
||||
foreach ($schema->getTypeMap() as $type) {
|
||||
if ($type instanceof ObjectType) {
|
||||
$totalFields += count($type->getFields());
|
||||
if (! ($type instanceof ObjectType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$totalFields += count($type->getFields());
|
||||
}
|
||||
|
||||
$this->maxLeafFields = max(1, round($totalFields * $percentOfLeafFields));
|
||||
$this->maxLeafFields = max(1, round($totalFields * $percentOfLeafFields));
|
||||
$this->currentLeafFields = 0;
|
||||
}
|
||||
|
||||
|
@ -44,13 +49,12 @@ 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);
|
||||
|
@ -58,12 +62,13 @@ 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++;
|
||||
|
||||
|
@ -87,12 +92,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;
|
||||
|
|
|
@ -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($value, $args, $context, $resolveInfo)
|
||||
public function resolveField($objectValue, $args, $context, $resolveInfo)
|
||||
{
|
||||
return $resolveInfo->fieldName . '-value';
|
||||
}
|
||||
|
|
|
@ -2,25 +2,33 @@
|
|||
"name": "webonyx/graphql-php",
|
||||
"description": "A PHP port of GraphQL reference implementation",
|
||||
"type": "library",
|
||||
"license": "BSD",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/webonyx/graphql-php",
|
||||
"keywords": [
|
||||
"graphql",
|
||||
"API"
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.5,<8.0-DEV",
|
||||
"php": "^7.1||^8.0",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8",
|
||||
"psr/http-message": "^1.0"
|
||||
"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.*"
|
||||
},
|
||||
"config": {
|
||||
"bin-dir": "bin"
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true
|
||||
},
|
||||
"autoload": {
|
||||
"files": ["src/deprecated.php"],
|
||||
"psr-4": {
|
||||
"GraphQL\\": "src/"
|
||||
}
|
||||
|
@ -35,5 +43,14 @@
|
|||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,26 @@
|
|||
# Integrations
|
||||
|
||||
- [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)
|
||||
* [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
|
||||
|
||||
# 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) -
|
||||
# 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) –
|
||||
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)
|
|
@ -103,23 +103,25 @@ for a field you simply override this default resolver.
|
|||
**graphql-php** provides following default field resolver:
|
||||
```php
|
||||
<?php
|
||||
function defaultFieldResolver($source, $args, $context, \GraphQL\Type\Definition\ResolveInfo $info)
|
||||
{
|
||||
$fieldName = $info->fieldName;
|
||||
$property = null;
|
||||
function defaultFieldResolver($objectValue, $args, $context, \GraphQL\Type\Definition\ResolveInfo $info)
|
||||
{
|
||||
$fieldName = $info->fieldName;
|
||||
$property = null;
|
||||
|
||||
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};
|
||||
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};
|
||||
}
|
||||
}
|
||||
|
||||
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).
|
||||
|
@ -161,7 +163,6 @@ $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
|
||||
|
||||
|
@ -270,4 +271,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.
|
||||
|
|
|
@ -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 first resolver exception to be re-thrown, use following flags:
|
||||
If you prefer the first resolver exception to be re-thrown, use following flags:
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\GraphQL;
|
||||
|
@ -127,6 +127,9 @@ $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.
|
||||
|
||||
|
@ -151,8 +154,8 @@ $myErrorHandler = function(array $errors, callable $formatter) {
|
|||
|
||||
$result = GraphQL::executeQuery(/* $args */)
|
||||
->setErrorFormatter($myErrorFormatter)
|
||||
->setErrorHandler($myErrorHandler)
|
||||
->toArray();
|
||||
->setErrorsHandler($myErrorHandler)
|
||||
->toArray();
|
||||
```
|
||||
|
||||
Note that when you pass [debug flags](#debugging-tools) to **toArray()** your custom formatter will still be
|
||||
|
|
|
@ -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/request/psr7.html)
|
||||
- [Symfony PSR-7 Bridge](https://symfony.com/doc/current/components/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 }}"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
|
|
@ -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), simply run:
|
||||
Using [composer](https://getcomposer.org/doc/00-intro.md), run:
|
||||
|
||||
```sh
|
||||
composer require webonyx/graphql-php
|
||||
|
@ -54,8 +54,8 @@ $queryType = new ObjectType([
|
|||
'args' => [
|
||||
'message' => Type::nonNull(Type::string()),
|
||||
],
|
||||
'resolve' => function ($root, $args) {
|
||||
return $root['prefix'] . $args['message'];
|
||||
'resolve' => function ($rootValue, $args) {
|
||||
return $rootValue['prefix'] . $args['message'];
|
||||
}
|
||||
],
|
||||
],
|
||||
|
@ -106,7 +106,7 @@ echo json_encode($output);
|
|||
|
||||
Our example is finished. Try it by running:
|
||||
```sh
|
||||
php -S localhost:8000 graphql.php
|
||||
php -S localhost:8080 graphql.php
|
||||
curl http://localhost:8080 -d '{"query": "query { echo(message: \"Hello World\") }" }'
|
||||
```
|
||||
|
||||
|
|
|
@ -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 a replacement for REST and SOAP APIs (even for **existing applications**).
|
||||
It is intended to be an alternative to 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 (v0.10) supports all features described by GraphQL specification
|
||||
(including April 2016 add-ons) as well as some experimental features like
|
||||
The current version supports all features described by GraphQL specification
|
||||
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).
|
||||
|
|
1010
docs/reference.md
1010
docs/reference.md
File diff suppressed because it is too large
Load diff
|
@ -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\Type\Definition\DirectiveLocation`](../reference.md#graphqltypedefinitiondirectivelocation).
|
||||
[`GraphQL\Language\DirectiveLocation`](../reference.md#graphqllanguagedirectivelocation).
|
||||
|
|
|
@ -158,7 +158,7 @@ $heroType = new ObjectType([
|
|||
'args' => [
|
||||
'episode' => Type::nonNull($enumType)
|
||||
],
|
||||
'resolve' => function($_value, $args) {
|
||||
'resolve' => function($hero, $args) {
|
||||
return $args['episode'] === 5 ? true : false;
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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
|
||||
defaultValue | `scalar` | Default value of this input field. Use the internal value if specifying a default for an **enum** type
|
||||
|
||||
# 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' => Type::nonNull($filters),
|
||||
'type' => $filters,
|
||||
'defaultValue' => [
|
||||
'popular' => true
|
||||
]
|
||||
|
|
|
@ -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($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
|
||||
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
|
||||
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
|
||||
defaultValue | `scalar` | Default value for this argument. Use the internal value if specifying a default for an **enum** type
|
||||
|
||||
# Shorthand field definitions
|
||||
Fields can be also defined in **shorthand** notation (with only **name** and **type** options):
|
||||
|
|
|
@ -96,10 +96,11 @@ class EmailType extends ScalarType
|
|||
* }
|
||||
*
|
||||
* @param \GraphQL\Language\AST\Node $valueNode
|
||||
* @param array|null $variables
|
||||
* @return string
|
||||
* @throws Error
|
||||
*/
|
||||
public function parseLiteral($valueNode)
|
||||
public function parseLiteral($valueNode, array $variables = null)
|
||||
{
|
||||
// Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL
|
||||
// error location in query:
|
||||
|
@ -124,6 +125,6 @@ $emailType = new CustomScalarType([
|
|||
'name' => 'Email',
|
||||
'serialize' => function($value) {/* See function body above */},
|
||||
'parseValue' => function($value) {/* See function body above */},
|
||||
'parseLiteral' => function($valueNode) {/* See function body above */},
|
||||
'parseLiteral' => function($valueNode, array $variables = null) {/* See function body above */},
|
||||
]);
|
||||
```
|
||||
|
|
|
@ -62,7 +62,7 @@ $mutationType = new ObjectType([
|
|||
'episode' => $episodeEnum,
|
||||
'review' => $reviewInputObject
|
||||
],
|
||||
'resolve' => function($val, $args) {
|
||||
'resolve' => function($rootValue, $args) {
|
||||
// TODOC
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# 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.
|
||||
|
@ -32,18 +33,19 @@ $contents = file_get_contents('schema.graphql');
|
|||
$schema = BuildSchema::build($contents);
|
||||
```
|
||||
|
||||
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.
|
||||
By default, such schema is created without any resolvers.
|
||||
|
||||
Also, we have to rely on [default field resolver](../data-fetching.md#default-field-resolver) and **root value** in
|
||||
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
|
||||
In order to enable **Interfaces**, **Unions** and custom field resolvers you can pass the second argument:
|
||||
**type config decorator** to schema builder.
|
||||
Since 0.10.0
|
||||
|
||||
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
|
||||
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
|
||||
[**resolveField**](object-types.md#configuration-options) for object types.
|
||||
|
||||
```php
|
||||
|
@ -61,6 +63,8 @@ $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.
|
||||
|
||||
|
@ -77,11 +81,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($document->toArray(), true));
|
||||
file_put_contents($cacheFilename, "<?php\nreturn " . var_export(AST::toArray($document), true) . ";\n");
|
||||
} else {
|
||||
$document = AST::fromArray(require $cacheFilename); // fromArray() is a lazy operation as well
|
||||
}
|
||||
|
||||
$typeConfigDecorator = function () {};
|
||||
$schema = BuildSchema::build($document, $typeConfigDecorator);
|
||||
```
|
||||
```
|
||||
|
|
|
@ -19,8 +19,8 @@ try {
|
|||
'args' => [
|
||||
'message' => ['type' => Type::string()],
|
||||
],
|
||||
'resolve' => function ($root, $args) {
|
||||
return $root['prefix'] . $args['message'];
|
||||
'resolve' => function ($rootValue, $args) {
|
||||
return $rootValue['prefix'] . $args['message'];
|
||||
}
|
||||
],
|
||||
],
|
||||
|
@ -35,7 +35,7 @@ try {
|
|||
'x' => ['type' => Type::int()],
|
||||
'y' => ['type' => Type::int()],
|
||||
],
|
||||
'resolve' => function ($root, $args) {
|
||||
'resolve' => function ($calc, $args) {
|
||||
return $args['x'] + $args['y'];
|
||||
},
|
||||
],
|
||||
|
|
|
@ -35,12 +35,12 @@ class CommentType extends ObjectType
|
|||
Types::htmlField('body')
|
||||
];
|
||||
},
|
||||
'resolveField' => function($value, $args, $context, ResolveInfo $info) {
|
||||
'resolveField' => function($comment, $args, $context, ResolveInfo $info) {
|
||||
$method = 'resolve' . ucfirst($info->fieldName);
|
||||
if (method_exists($this, $method)) {
|
||||
return $this->{$method}($value, $args, $context, $info);
|
||||
return $this->{$method}($comment, $args, $context, $info);
|
||||
} else {
|
||||
return $value->{$info->fieldName};
|
||||
return $comment->{$info->fieldName};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
@ -57,8 +57,8 @@ class QueryType extends ObjectType
|
|||
],
|
||||
'hello' => Type::string()
|
||||
],
|
||||
'resolveField' => function($val, $args, $context, ResolveInfo $info) {
|
||||
return $this->{$info->fieldName}($val, $args, $context, $info);
|
||||
'resolveField' => function($rootValue, $args, $context, ResolveInfo $info) {
|
||||
return $this->{$info->fieldName}($rootValue, $args, $context, $info);
|
||||
}
|
||||
];
|
||||
parent::__construct($config);
|
||||
|
|
|
@ -30,11 +30,12 @@ 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 \UnexpectedValueException("Cannot represent value as URL: " . Utils::printSafe($value));
|
||||
throw new Error("Cannot represent value as URL: " . Utils::printSafe($value));
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
@ -42,20 +43,21 @@ class UrlType extends ScalarType
|
|||
/**
|
||||
* Parses an externally provided literal value to use as an input (e.g. in Query AST)
|
||||
*
|
||||
* @param $ast Node
|
||||
* @param Node $valueNode
|
||||
* @param array|null $variables
|
||||
* @return null|string
|
||||
* @throws Error
|
||||
*/
|
||||
public function parseLiteral($ast)
|
||||
public function parseLiteral($valueNode, array $variables = null)
|
||||
{
|
||||
// Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL
|
||||
// error location in query:
|
||||
if (!($ast instanceof StringValueNode)) {
|
||||
throw new Error('Query error: Can only parse strings got: ' . $ast->kind, [$ast]);
|
||||
if (!($valueNode instanceof StringValueNode)) {
|
||||
throw new Error('Query error: Can only parse strings got: ' . $valueNode->kind, [$valueNode]);
|
||||
}
|
||||
if (!is_string($ast->value) || !filter_var($ast->value, FILTER_VALIDATE_URL)) {
|
||||
throw new Error('Query error: Not a valid URL', [$ast]);
|
||||
if (!is_string($valueNode->value) || !filter_var($valueNode->value, FILTER_VALIDATE_URL)) {
|
||||
throw new Error('Query error: Not a valid URL', [$valueNode]);
|
||||
}
|
||||
return $ast->value;
|
||||
return $valueNode->value;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,12 +75,12 @@ class StoryType extends ObjectType
|
|||
'interfaces' => [
|
||||
Types::node()
|
||||
],
|
||||
'resolveField' => function($value, $args, $context, ResolveInfo $info) {
|
||||
'resolveField' => function($story, $args, $context, ResolveInfo $info) {
|
||||
$method = 'resolve' . ucfirst($info->fieldName);
|
||||
if (method_exists($this, $method)) {
|
||||
return $this->{$method}($value, $args, $context, $info);
|
||||
return $this->{$method}($story, $args, $context, $info);
|
||||
} else {
|
||||
return $value->{$info->fieldName};
|
||||
return $story->{$info->fieldName};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
@ -44,12 +44,12 @@ class UserType extends ObjectType
|
|||
'interfaces' => [
|
||||
Types::node()
|
||||
],
|
||||
'resolveField' => function($value, $args, $context, ResolveInfo $info) {
|
||||
'resolveField' => function($user, $args, $context, ResolveInfo $info) {
|
||||
$method = 'resolve' . ucfirst($info->fieldName);
|
||||
if (method_exists($this, $method)) {
|
||||
return $this->{$method}($value, $args, $context, $info);
|
||||
return $this->{$method}($user, $args, $context, $info);
|
||||
} else {
|
||||
return $value->{$info->fieldName};
|
||||
return $user->{$info->fieldName};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
@ -35,10 +35,11 @@ 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']) {
|
||||
|
|
|
@ -18,7 +18,7 @@ try {
|
|||
$query = $input['query'];
|
||||
$variableValues = isset($input['variables']) ? $input['variables'] : null;
|
||||
|
||||
$result = GraphQL::execute($schema, $query, $rootValue, null, $variableValues);
|
||||
$result = GraphQL::executeQuery($schema, $query, $rootValue, null, $variableValues);
|
||||
} catch (\Exception $e) {
|
||||
$result = [
|
||||
'error' => [
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<?php
|
||||
|
||||
interface Resolver {
|
||||
public function resolve($root, $args, $context);
|
||||
public function resolve($rootValue, $args, $context);
|
||||
}
|
||||
|
||||
class Addition implements Resolver
|
||||
{
|
||||
public function resolve($root, $args, $context)
|
||||
public function resolve($rootValue, $args, $context)
|
||||
{
|
||||
return $args['x'] + $args['y'];
|
||||
}
|
||||
|
@ -14,22 +14,22 @@ class Addition implements Resolver
|
|||
|
||||
class Echoer implements Resolver
|
||||
{
|
||||
public function resolve($root, $args, $context)
|
||||
public function resolve($rootValue, $args, $context)
|
||||
{
|
||||
return $root['prefix'].$args['message'];
|
||||
return $rootValue['prefix'].$args['message'];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'sum' => function($root, $args, $context) {
|
||||
'sum' => function($rootValue, $args, $context) {
|
||||
$sum = new Addition();
|
||||
|
||||
return $sum->resolve($root, $args, $context);
|
||||
return $sum->resolve($rootValue, $args, $context);
|
||||
},
|
||||
'echo' => function($root, $args, $context) {
|
||||
'echo' => function($rootValue, $args, $context) {
|
||||
$echo = new Echoer();
|
||||
|
||||
return $echo->resolve($root, $args, $context);
|
||||
return $echo->resolve($rootValue, $args, $context);
|
||||
},
|
||||
'prefix' => 'You said: ',
|
||||
];
|
||||
|
|
|
@ -10,10 +10,10 @@ php -S localhost:8080 ./graphql.php
|
|||
|
||||
### Try query
|
||||
```
|
||||
curl http://localhost:8080 -d '{"query": "query { echo(message: \"Hello World\") }" }'
|
||||
curl -d '{"query": "query { echo(message: \"Hello World\") }" }' -H "Content-Type: application/json" http://localhost:8080
|
||||
```
|
||||
|
||||
### Try mutation
|
||||
```
|
||||
curl http://localhost:8080 -d '{"query": "mutation { sum(x: 2, y: 2) }" }'
|
||||
curl -d '{"query": "mutation { sum(x: 2, y: 2) }" }' -H "Content-Type: application/json" http://localhost:8080
|
||||
```
|
||||
|
|
|
@ -19,8 +19,8 @@ try {
|
|||
'args' => [
|
||||
'message' => ['type' => Type::string()],
|
||||
],
|
||||
'resolve' => function ($root, $args) {
|
||||
return $root['prefix'] . $args['message'];
|
||||
'resolve' => function ($rootValue, $args) {
|
||||
return $rootValue['prefix'] . $args['message'];
|
||||
}
|
||||
],
|
||||
],
|
||||
|
@ -35,7 +35,7 @@ try {
|
|||
'x' => ['type' => Type::int()],
|
||||
'y' => ['type' => Type::int()],
|
||||
],
|
||||
'resolve' => function ($root, $args) {
|
||||
'resolve' => function ($calc, $args) {
|
||||
return $args['x'] + $args['y'];
|
||||
},
|
||||
],
|
||||
|
|
102
phpcs.xml.dist
Normal file
102
phpcs.xml.dist
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?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>
|
17
phpstan.neon.dist
Normal file
17
phpstan.neon.dist
Normal file
|
@ -0,0 +1,17 @@
|
|||
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
|
|
@ -1,16 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?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>
|
||||
|
||||
<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>
|
||||
|
@ -26,21 +23,6 @@
|
|||
<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>
|
||||
|
|
|
@ -1,60 +1,64 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL;
|
||||
|
||||
use Exception;
|
||||
use GraphQL\Executor\Promise\Adapter\SyncPromise;
|
||||
use SplQueue;
|
||||
use Throwable;
|
||||
|
||||
class Deferred
|
||||
{
|
||||
/**
|
||||
* @var \SplQueue
|
||||
*/
|
||||
/** @var SplQueue|null */
|
||||
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);
|
||||
}
|
||||
|
||||
private function run()
|
||||
public function run() : void
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
<?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
|
||||
{
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Error;
|
||||
|
||||
/**
|
||||
|
@ -14,8 +17,9 @@ interface ClientAware
|
|||
/**
|
||||
* Returns true when exception message is safe to be displayed to a client.
|
||||
*
|
||||
* @api
|
||||
* @return bool
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function isClientSafe();
|
||||
|
||||
|
@ -24,8 +28,9 @@ interface ClientAware
|
|||
*
|
||||
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
|
||||
*
|
||||
* @api
|
||||
* @return string
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getCategory();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Error;
|
||||
|
||||
/**
|
||||
|
@ -6,7 +9,8 @@ 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;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,22 @@
|
|||
<?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
|
||||
|
@ -19,9 +32,9 @@ use GraphQL\Utils\Utils;
|
|||
* 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';
|
||||
|
||||
/**
|
||||
|
@ -31,56 +44,103 @@ 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 array
|
||||
* @var mixed[]|null
|
||||
*/
|
||||
public $path;
|
||||
|
||||
/**
|
||||
* An array of GraphQL AST Nodes corresponding to this error.
|
||||
*
|
||||
* @var array
|
||||
* @var Node[]|null
|
||||
*/
|
||||
public $nodes;
|
||||
|
||||
/**
|
||||
* The source GraphQL document corresponding to this error.
|
||||
* The source GraphQL document for the first location of this error.
|
||||
*
|
||||
* Note that if this Error represents more than one node, the source may not
|
||||
* represent nodes after the first node.
|
||||
*
|
||||
* @var Source|null
|
||||
*/
|
||||
private $source;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
/** @var int[]|null */
|
||||
private $positions;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
/** @var bool */
|
||||
private $isClientSafe;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
protected $category;
|
||||
|
||||
/** @var mixed[]|null */
|
||||
protected $extensions;
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @param Node|Node[]|Traversable|null $nodes
|
||||
* @param mixed[]|null $positions
|
||||
* @param mixed[]|null $path
|
||||
* @param Throwable $previous
|
||||
* @param mixed[] $extensions
|
||||
*/
|
||||
public function __construct(
|
||||
$message,
|
||||
$nodes = null,
|
||||
?Source $source = null,
|
||||
$positions = null,
|
||||
$path = null,
|
||||
$previous = null,
|
||||
array $extensions = []
|
||||
) {
|
||||
parent::__construct($message, 0, $previous);
|
||||
|
||||
// Compute list of blame nodes.
|
||||
if ($nodes instanceof Traversable) {
|
||||
$nodes = iterator_to_array($nodes);
|
||||
} elseif ($nodes && ! is_array($nodes)) {
|
||||
$nodes = [$nodes];
|
||||
}
|
||||
|
||||
$this->nodes = $nodes;
|
||||
$this->source = $source;
|
||||
$this->positions = $positions;
|
||||
$this->path = $path;
|
||||
$this->extensions = $extensions ?: (
|
||||
$previous && $previous instanceof self
|
||||
? $previous->extensions
|
||||
: []
|
||||
);
|
||||
|
||||
if ($previous instanceof ClientAware) {
|
||||
$this->isClientSafe = $previous->isClientSafe();
|
||||
$this->category = $previous->getCategory() ?: self::CATEGORY_INTERNAL;
|
||||
} elseif ($previous) {
|
||||
$this->isClientSafe = false;
|
||||
$this->category = self::CATEGORY_INTERNAL;
|
||||
} else {
|
||||
$this->isClientSafe = true;
|
||||
$this->category = self::CATEGORY_GRAPHQL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an arbitrary Error, presumably thrown while attempting to execute a
|
||||
* GraphQL operation, produce a new GraphQLError aware of the location in the
|
||||
* document responsible for the original Error.
|
||||
*
|
||||
* @param $error
|
||||
* @param array|null $nodes
|
||||
* @param array|null $path
|
||||
* @param mixed $error
|
||||
* @param Node[]|null $nodes
|
||||
* @param mixed[]|null $path
|
||||
*
|
||||
* @return Error
|
||||
*/
|
||||
public static function createLocatedError($error, $nodes = null, $path = null)
|
||||
|
@ -88,22 +148,24 @@ 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;
|
||||
$source = $positions = $originalError = null;
|
||||
$extensions = [];
|
||||
|
||||
if ($error instanceof self) {
|
||||
$message = $error->getMessage();
|
||||
$message = $error->getMessage();
|
||||
$originalError = $error;
|
||||
$nodes = $error->nodes ?: $nodes;
|
||||
$source = $error->source;
|
||||
$positions = $error->positions;
|
||||
} else if ($error instanceof \Exception || $error instanceof \Throwable) {
|
||||
$message = $error->getMessage();
|
||||
$nodes = $error->nodes ?: $nodes;
|
||||
$source = $error->source;
|
||||
$positions = $error->positions;
|
||||
$extensions = $error->extensions;
|
||||
} elseif ($error instanceof Exception || $error instanceof Throwable) {
|
||||
$message = $error->getMessage();
|
||||
$originalError = $error;
|
||||
} else {
|
||||
$message = (string) $error;
|
||||
|
@ -115,60 +177,19 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
|
|||
$source,
|
||||
$positions,
|
||||
$path,
|
||||
$originalError
|
||||
$originalError,
|
||||
$extensions
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Error $error
|
||||
* @return array
|
||||
* @return mixed[]
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
@ -190,29 +211,38 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
|
|||
*/
|
||||
public function getSource()
|
||||
{
|
||||
if (null === $this->source) {
|
||||
if (!empty($this->nodes[0]) && !empty($this->nodes[0]->loc)) {
|
||||
if ($this->source === null) {
|
||||
if (! empty($this->nodes[0]) && ! empty($this->nodes[0]->loc)) {
|
||||
$this->source = $this->nodes[0]->loc->source;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @return int[]
|
||||
*/
|
||||
public function getPositions()
|
||||
{
|
||||
if (null === $this->positions) {
|
||||
if (!empty($this->nodes)) {
|
||||
$positions = array_map(function($node) {
|
||||
if ($this->positions === null && ! empty($this->nodes)) {
|
||||
$positions = array_map(
|
||||
static function ($node) {
|
||||
return isset($node->loc) ? $node->loc->start : null;
|
||||
}, $this->nodes);
|
||||
$this->positions = array_filter($positions, function($p) {
|
||||
},
|
||||
$this->nodes
|
||||
);
|
||||
|
||||
$positions = array_filter(
|
||||
$positions,
|
||||
static function ($p) {
|
||||
return $p !== null;
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$this->positions = array_values($positions);
|
||||
}
|
||||
|
||||
return $this->positions;
|
||||
}
|
||||
|
||||
|
@ -227,19 +257,36 @@ 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.
|
||||
*
|
||||
* @api
|
||||
* @return SourceLocation[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getLocations()
|
||||
{
|
||||
if (null === $this->locations) {
|
||||
if ($this->locations === null) {
|
||||
$positions = $this->getPositions();
|
||||
$source = $this->getSource();
|
||||
$source = $this->getSource();
|
||||
$nodes = $this->nodes;
|
||||
|
||||
if ($positions && $source) {
|
||||
$this->locations = array_map(function ($pos) use ($source) {
|
||||
return $source->getLocation($pos);
|
||||
}, $positions);
|
||||
$this->locations = array_map(
|
||||
static function ($pos) use ($source) {
|
||||
return $source->getLocation($pos);
|
||||
},
|
||||
$positions
|
||||
);
|
||||
} elseif ($nodes) {
|
||||
$locations = array_filter(
|
||||
array_map(
|
||||
static function ($node) {
|
||||
if ($node->loc && $node->loc->source) {
|
||||
return $node->loc->source->getLocation($node->loc->start);
|
||||
}
|
||||
},
|
||||
$nodes
|
||||
)
|
||||
);
|
||||
$this->locations = array_values($locations);
|
||||
} else {
|
||||
$this->locations = [];
|
||||
}
|
||||
|
@ -248,53 +295,86 @@ 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 array
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function toSerializableArray()
|
||||
{
|
||||
$arr = [
|
||||
'message' => $this->getMessage()
|
||||
'message' => $this->getMessage(),
|
||||
];
|
||||
|
||||
$locations = Utils::map($this->getLocations(), function(SourceLocation $loc) {
|
||||
return $loc->toSerializableArray();
|
||||
});
|
||||
$locations = Utils::map(
|
||||
$this->getLocations(),
|
||||
static 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
|
||||
*/
|
||||
function jsonSerialize()
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toSerializableArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return FormattedError::printError($this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,39 @@
|
|||
<?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).
|
||||
|
@ -13,20 +42,119 @@ use GraphQL\Utils\Utils;
|
|||
*/
|
||||
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()`.
|
||||
*
|
||||
* @api
|
||||
* @param string $msg
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function setInternalErrorMessage($msg)
|
||||
{
|
||||
self::$internalErrorMessage = $msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a GraphQLError to a string, representing useful location information
|
||||
* about the error's position in the source.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function printError(Error $error)
|
||||
{
|
||||
$printedLocations = [];
|
||||
if ($error->nodes) {
|
||||
/** @var Node $node */
|
||||
foreach ($error->nodes as $node) {
|
||||
if (! $node->loc) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($node->loc->source === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$printedLocations[] = self::highlightSourceAtLocation(
|
||||
$node->loc->source,
|
||||
$node->loc->source->getLocation($node->loc->start)
|
||||
);
|
||||
}
|
||||
} elseif ($error->getSource() && $error->getLocations()) {
|
||||
$source = $error->getSource();
|
||||
foreach ($error->getLocations() as $location) {
|
||||
$printedLocations[] = self::highlightSourceAtLocation($source, $location);
|
||||
}
|
||||
}
|
||||
|
||||
return ! $printedLocations
|
||||
? $error->getMessage()
|
||||
: implode("\n\n", array_merge([$error->getMessage()], $printedLocations)) . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a helpful description of the location of the error in the GraphQL
|
||||
* Source document.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function highlightSourceAtLocation(Source $source, SourceLocation $location)
|
||||
{
|
||||
$line = $location->line;
|
||||
$lineOffset = $source->locationOffset->line - 1;
|
||||
$columnOffset = self::getColumnOffset($source, $location);
|
||||
$contextLine = $line + $lineOffset;
|
||||
$contextColumn = $location->column + $columnOffset;
|
||||
$prevLineNum = (string) ($contextLine - 1);
|
||||
$lineNum = (string) $contextLine;
|
||||
$nextLineNum = (string) ($contextLine + 1);
|
||||
$padLen = strlen($nextLineNum);
|
||||
$lines = preg_split('/\r\n|[\n\r]/', $source->body);
|
||||
|
||||
$lines[0] = self::whitespace($source->locationOffset->column - 1) . $lines[0];
|
||||
|
||||
$outputLines = [
|
||||
sprintf('%s (%s:%s)', $source->name, $contextLine, $contextColumn),
|
||||
$line >= 2 ? (self::lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2]) : null,
|
||||
self::lpad($padLen, $lineNum) . ': ' . $lines[$line - 1],
|
||||
self::whitespace(2 + $padLen + $contextColumn - 1) . '^',
|
||||
$line < count($lines) ? self::lpad($padLen, $nextLineNum) . ': ' . $lines[$line] : null,
|
||||
];
|
||||
|
||||
return implode("\n", array_filter($outputLines));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
private static function getColumnOffset(Source $source, SourceLocation $location)
|
||||
{
|
||||
return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $len
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function whitespace($len)
|
||||
{
|
||||
return str_repeat(' ', $len);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $len
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function lpad($len, $str)
|
||||
{
|
||||
return self::whitespace($len - mb_strlen($str)) . $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard GraphQL error formatter. Converts any exception to array
|
||||
* conforming to GraphQL spec.
|
||||
|
@ -36,18 +164,21 @@ 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)
|
||||
);
|
||||
|
||||
|
@ -55,27 +186,36 @@ class FormattedError
|
|||
|
||||
if ($e instanceof ClientAware) {
|
||||
$formattedError = [
|
||||
'message' => $e->isClientSafe() ? $e->getMessage() : $internalErrorMessage,
|
||||
'category' => $e->getCategory()
|
||||
'message' => $e->isClientSafe() ? $e->getMessage() : $internalErrorMessage,
|
||||
'extensions' => [
|
||||
'category' => $e->getCategory(),
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$formattedError = [
|
||||
'message' => $internalErrorMessage,
|
||||
'category' => Error::CATEGORY_INTERNAL
|
||||
'message' => $internalErrorMessage,
|
||||
'extensions' => [
|
||||
'category' => Error::CATEGORY_INTERNAL,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if ($e instanceof Error) {
|
||||
$locations = Utils::map($e->getLocations(), function(SourceLocation $loc) {
|
||||
return $loc->toSerializableArray();
|
||||
});
|
||||
|
||||
if (!empty($locations)) {
|
||||
$locations = Utils::map(
|
||||
$e->getLocations(),
|
||||
static 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) {
|
||||
|
@ -89,56 +229,67 @@ class FormattedError
|
|||
* Decorates spec-compliant $formattedError with debug entries according to $debug flags
|
||||
* (see GraphQL\Error\Debug for available flags)
|
||||
*
|
||||
* @param array $formattedError
|
||||
* @param \Throwable $e
|
||||
* @param bool $debug
|
||||
* @return array
|
||||
* @throws \Throwable
|
||||
* @param mixed[] $formattedError
|
||||
* @param Throwable $e
|
||||
* @param bool|int $debug
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @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;
|
||||
} else if ($e->getPrevious()) {
|
||||
}
|
||||
|
||||
if ($e->getPrevious()) {
|
||||
throw $e->getPrevious();
|
||||
}
|
||||
}
|
||||
|
||||
$isInternal = !$e instanceof ClientAware || !$e->isClientSafe();
|
||||
$isUnsafe = ! $e instanceof ClientAware || ! $e->isClientSafe();
|
||||
|
||||
if (($debug & Debug::INCLUDE_DEBUG_MESSAGE) && $isInternal) {
|
||||
if (($debug & Debug::RETHROW_UNSAFE_EXCEPTIONS) && $isUnsafe) {
|
||||
if ($e->getPrevious()) {
|
||||
throw $e->getPrevious();
|
||||
}
|
||||
}
|
||||
|
||||
if (($debug & Debug::INCLUDE_DEBUG_MESSAGE) && $isUnsafe) {
|
||||
// Displaying debugMessage as a first entry:
|
||||
$formattedError = ['debugMessage' => $e->getMessage()] + $formattedError;
|
||||
}
|
||||
|
||||
if ($debug & Debug::INCLUDE_TRACE) {
|
||||
if ($e instanceof \ErrorException || $e instanceof \Error) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -146,67 +297,71 @@ class FormattedError
|
|||
* Prepares final error formatter taking in account $debug flags.
|
||||
* If initial formatter is not set, FormattedError::createFromException is used
|
||||
*
|
||||
* @param callable|null $formatter
|
||||
* @param $debug
|
||||
* @return callable|\Closure
|
||||
* @param bool|int $debug
|
||||
*
|
||||
* @return callable|callable
|
||||
*/
|
||||
public static function prepareFormatter(callable $formatter = null, $debug)
|
||||
public static function prepareFormatter(?callable $formatter = null, $debug)
|
||||
{
|
||||
$formatter = $formatter ?: function($e) {
|
||||
$formatter = $formatter ?: static function ($e) {
|
||||
return FormattedError::createFromException($e);
|
||||
};
|
||||
if ($debug) {
|
||||
$formatter = function($e) use ($formatter, $debug) {
|
||||
$formatter = static function ($e) use ($formatter, $debug) {
|
||||
return FormattedError::addDebugEntries($formatter($e), $e, $debug);
|
||||
};
|
||||
}
|
||||
|
||||
return $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns error trace as serializable array
|
||||
*
|
||||
* @param Throwable $error
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @api
|
||||
* @param \Throwable $error
|
||||
* @return array
|
||||
*/
|
||||
public static function toSafeTrace($error)
|
||||
{
|
||||
$trace = $error->getTrace();
|
||||
|
||||
// Remove invariant entries as they don't provide much value:
|
||||
if (
|
||||
isset($trace[0]['function']) && isset($trace[0]['class']) &&
|
||||
('GraphQL\Utils\Utils::invariant' === $trace[0]['class'].'::'.$trace[0]['function'])) {
|
||||
if (isset($trace[0]['function']) && isset($trace[0]['class']) &&
|
||||
// Remove invariant entries as they don't provide much value:
|
||||
($trace[0]['class'] . '::' . $trace[0]['function'] === 'GraphQL\Utils\Utils::invariant')) {
|
||||
array_shift($trace);
|
||||
} elseif (! isset($trace[0]['file'])) {
|
||||
// Remove root call as it's likely error handler trace:
|
||||
array_shift($trace);
|
||||
}
|
||||
|
||||
// Remove root call as it's likely error handler trace:
|
||||
else if (!isset($trace[0]['file'])) {
|
||||
array_shift($trace);
|
||||
}
|
||||
return array_map(
|
||||
static function ($err) {
|
||||
$safeErr = array_intersect_key($err, ['file' => true, 'line' => true]);
|
||||
|
||||
return array_map(function($err) {
|
||||
$safeErr = array_intersect_key($err, ['file' => true, 'line' => true]);
|
||||
if (isset($err['function'])) {
|
||||
$func = $err['function'];
|
||||
$args = ! empty($err['args']) ? array_map([self::class, 'printVar'], $err['args']) : [];
|
||||
$funcStr = $func . '(' . implode(', ', $args) . ')';
|
||||
|
||||
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;
|
||||
if (isset($err['class'])) {
|
||||
$safeErr['call'] = $err['class'] . '::' . $funcStr;
|
||||
} else {
|
||||
$safeErr['function'] = $funcStr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $safeErr;
|
||||
}, $trace);
|
||||
return $safeErr;
|
||||
},
|
||||
$trace
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $var
|
||||
* @param mixed $var
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function printVar($var)
|
||||
|
@ -216,16 +371,17 @@ 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)) {
|
||||
|
@ -237,42 +393,48 @@ class FormattedError
|
|||
if (is_scalar($var)) {
|
||||
return $var;
|
||||
}
|
||||
if (null === $var) {
|
||||
if ($var === null) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
return gettype($var);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated as of v0.8.0
|
||||
* @param $error
|
||||
*
|
||||
* @param string $error
|
||||
* @param SourceLocation[] $locations
|
||||
* @return array
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function create($error, array $locations = [])
|
||||
{
|
||||
$formatted = [
|
||||
'message' => $error
|
||||
];
|
||||
$formatted = ['message' => $error];
|
||||
|
||||
if (!empty($locations)) {
|
||||
$formatted['locations'] = array_map(function($loc) { return $loc->toArray();}, $locations);
|
||||
if (! empty($locations)) {
|
||||
$formatted['locations'] = array_map(
|
||||
static function ($loc) {
|
||||
return $loc->toArray();
|
||||
},
|
||||
$locations
|
||||
);
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ErrorException $e
|
||||
* @deprecated as of v0.10.0, use general purpose method createFromException() instead
|
||||
* @return array
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
<?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
|
||||
{
|
||||
}
|
||||
|
|
|
@ -1,50 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Error;
|
||||
|
||||
use GraphQL\Language\Source;
|
||||
use GraphQL\Language\SourceLocation;
|
||||
use function sprintf;
|
||||
|
||||
class SyntaxError extends Error
|
||||
{
|
||||
/**
|
||||
* @param Source $source
|
||||
* @param int $position
|
||||
* @param int $position
|
||||
* @param string $description
|
||||
*/
|
||||
public function __construct(Source $source, $position, $description)
|
||||
{
|
||||
$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" : '');
|
||||
parent::__construct(
|
||||
sprintf('Syntax Error: %s', $description),
|
||||
null,
|
||||
$source,
|
||||
[$position]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
<?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
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
<?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.
|
||||
*
|
||||
|
@ -9,28 +17,29 @@ namespace GraphQL\Error;
|
|||
*/
|
||||
final class Warning
|
||||
{
|
||||
const WARNING_NAME = 1;
|
||||
const WARNING_ASSIGN = 2;
|
||||
const WARNING_CONFIG = 4;
|
||||
const WARNING_FULL_SCHEMA_SCAN = 8;
|
||||
const WARNING_CONFIG_DEPRECATION = 16;
|
||||
const WARNING_NOT_A_TYPE = 32;
|
||||
const ALL = 63;
|
||||
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;
|
||||
|
||||
static $enableWarnings = self::ALL;
|
||||
/** @var int */
|
||||
private static $enableWarnings = self::ALL;
|
||||
|
||||
static $warned = [];
|
||||
/** @var mixed[] */
|
||||
private static $warned = [];
|
||||
|
||||
static private $warningHandler;
|
||||
/** @var callable|null */
|
||||
private static $warningHandler;
|
||||
|
||||
/**
|
||||
* Sets warning handler which can intercept all system warnings.
|
||||
* When not set, trigger_error() is used to notify about warnings.
|
||||
*
|
||||
* @api
|
||||
* @param callable|null $warningHandler
|
||||
*/
|
||||
public static function setWarningHandler(callable $warningHandler = null)
|
||||
public static function setWarningHandler(?callable $warningHandler = null) : void
|
||||
{
|
||||
self::$warningHandler = $warningHandler;
|
||||
}
|
||||
|
@ -43,18 +52,20 @@ final class Warning
|
|||
*
|
||||
* When passing true - suppresses all warnings.
|
||||
*
|
||||
* @api
|
||||
* @param bool|int $suppress
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
static function suppress($suppress = true)
|
||||
public static function suppress($suppress = true) : void
|
||||
{
|
||||
if (true === $suppress) {
|
||||
if ($suppress === true) {
|
||||
self::$enableWarnings = 0;
|
||||
} else if (false === $suppress) {
|
||||
} elseif ($suppress === false) {
|
||||
self::$enableWarnings = self::ALL;
|
||||
} else {
|
||||
$suppress = (int) $suppress;
|
||||
} elseif (is_int($suppress)) {
|
||||
self::$enableWarnings &= ~$suppress;
|
||||
} else {
|
||||
throw InvalidArgument::fromExpectedTypeAndArgument('bool|int', $suppress);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,38 +77,40 @@ final class Warning
|
|||
*
|
||||
* When passing true - re-enables all warnings.
|
||||
*
|
||||
* @api
|
||||
* @param bool|int $enable
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function enable($enable = true)
|
||||
public static function enable($enable = true) : void
|
||||
{
|
||||
if (true === $enable) {
|
||||
if ($enable === true) {
|
||||
self::$enableWarnings = self::ALL;
|
||||
} else if (false === $enable) {
|
||||
} elseif ($enable === false) {
|
||||
self::$enableWarnings = 0;
|
||||
} else {
|
||||
$enable = (int) $enable;
|
||||
} elseif (is_int($enable)) {
|
||||
self::$enableWarnings |= $enable;
|
||||
} else {
|
||||
throw InvalidArgument::fromExpectedTypeAndArgument('bool|int', $enable);
|
||||
}
|
||||
}
|
||||
|
||||
static function warnOnce($errorMessage, $warningId, $messageLevel = null)
|
||||
public static function warnOnce(string $errorMessage, int $warningId, ?int $messageLevel = null) : void
|
||||
{
|
||||
if (self::$warningHandler) {
|
||||
if (self::$warningHandler !== null) {
|
||||
$fn = self::$warningHandler;
|
||||
$fn($errorMessage, $warningId);
|
||||
} else if ((self::$enableWarnings & $warningId) > 0 && !isset(self::$warned[$warningId])) {
|
||||
} elseif ((self::$enableWarnings & $warningId) > 0 && ! isset(self::$warned[$warningId])) {
|
||||
self::$warned[$warningId] = true;
|
||||
trigger_error($errorMessage, $messageLevel ?: E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
static function warn($errorMessage, $warningId, $messageLevel = null)
|
||||
public static function warn(string $errorMessage, int $warningId, ?int $messageLevel = null) : void
|
||||
{
|
||||
if (self::$warningHandler) {
|
||||
if (self::$warningHandler !== null) {
|
||||
$fn = self::$warningHandler;
|
||||
$fn($errorMessage, $warningId);
|
||||
} else if ((self::$enableWarnings & $warningId) > 0) {
|
||||
} elseif ((self::$enableWarnings & $warningId) > 0) {
|
||||
trigger_error($errorMessage, $messageLevel ?: E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
|
|
20
src/Exception/InvalidArgument.php
Normal file
20
src/Exception/InvalidArgument.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?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)));
|
||||
}
|
||||
}
|
|
@ -1,7 +1,11 @@
|
|||
<?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;
|
||||
|
@ -10,78 +14,65 @@ 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 array
|
||||
*/
|
||||
/** @var mixed[] */
|
||||
public $variableValues;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
/** @var callable */
|
||||
public $fieldResolver;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
/** @var Error[] */
|
||||
public $errors;
|
||||
|
||||
/** @var PromiseAdapter */
|
||||
public $promiseAdapter;
|
||||
|
||||
public function __construct(
|
||||
$schema,
|
||||
$fragments,
|
||||
$root,
|
||||
$rootValue,
|
||||
$contextValue,
|
||||
$operation,
|
||||
$variables,
|
||||
$variableValues,
|
||||
$errors,
|
||||
$fieldResolver,
|
||||
$promiseAdapter
|
||||
)
|
||||
{
|
||||
$this->schema = $schema;
|
||||
$this->fragments = $fragments;
|
||||
$this->rootValue = $root;
|
||||
$this->contextValue = $contextValue;
|
||||
$this->operation = $operation;
|
||||
$this->variableValues = $variables;
|
||||
$this->errors = $errors ?: [];
|
||||
$this->fieldResolver = $fieldResolver;
|
||||
$this->promises = $promiseAdapter;
|
||||
) {
|
||||
$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;
|
||||
}
|
||||
|
||||
public function addError(Error $error)
|
||||
{
|
||||
$this->errors[] = $error;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
<?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).
|
||||
|
@ -12,13 +17,13 @@ use GraphQL\Error\FormattedError;
|
|||
* 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 array
|
||||
* @var mixed[]
|
||||
*/
|
||||
public $data;
|
||||
|
||||
|
@ -29,38 +34,34 @@ class ExecutionResult implements \JsonSerializable
|
|||
* contain original exception.
|
||||
*
|
||||
* @api
|
||||
* @var \GraphQL\Error\Error[]
|
||||
* @var Error[]
|
||||
*/
|
||||
public $errors;
|
||||
|
||||
|
||||
/**
|
||||
* User-defined serializable array of extensions included in serialized result.
|
||||
* Conforms to
|
||||
*
|
||||
* @api
|
||||
* @var array
|
||||
* @var mixed[]
|
||||
*/
|
||||
public $extensions;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
/** @var callable */
|
||||
private $errorFormatter;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
/** @var callable */
|
||||
private $errorsHandler;
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param array $errors
|
||||
* @param array $extensions
|
||||
* @param mixed[] $data
|
||||
* @param Error[] $errors
|
||||
* @param mixed[] $extensions
|
||||
*/
|
||||
public function __construct(array $data = null, array $errors = [], array $extensions = [])
|
||||
public function __construct($data = null, array $errors = [], array $extensions = [])
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->errors = $errors;
|
||||
$this->data = $data;
|
||||
$this->errors = $errors;
|
||||
$this->extensions = $extensions;
|
||||
}
|
||||
|
||||
|
@ -77,13 +78,14 @@ class ExecutionResult implements \JsonSerializable
|
|||
* // ... other keys
|
||||
* );
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
* @api
|
||||
* @param callable $errorFormatter
|
||||
* @return $this
|
||||
*/
|
||||
public function setErrorFormatter(callable $errorFormatter)
|
||||
{
|
||||
$this->errorFormatter = $errorFormatter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -97,16 +99,25 @@ 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.
|
||||
|
@ -117,42 +128,35 @@ class ExecutionResult implements \JsonSerializable
|
|||
* $debug argument must be either bool (only adds "debugMessage" to result) or sum of flags from
|
||||
* GraphQL\Error\Debug
|
||||
*
|
||||
* @api
|
||||
* @param bool|int $debug
|
||||
* @return array
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function toArray($debug = false)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
if (!empty($this->errors)) {
|
||||
$errorsHandler = $this->errorsHandler ?: function(array $errors, callable $formatter) {
|
||||
if (! empty($this->errors)) {
|
||||
$errorsHandler = $this->errorsHandler ?: static function (array $errors, callable $formatter) {
|
||||
return array_map($formatter, $errors);
|
||||
};
|
||||
|
||||
$result['errors'] = $errorsHandler(
|
||||
$this->errors,
|
||||
FormattedError::prepareFormatter($this->errorFormatter, $debug)
|
||||
);
|
||||
}
|
||||
|
||||
if (null !== $this->data) {
|
||||
if ($this->data !== null) {
|
||||
$result['data'] = $this->data;
|
||||
}
|
||||
|
||||
if (!empty($this->extensions)) {
|
||||
$result['extensions'] = (array) $this->extensions;
|
||||
|
||||
if (! empty($this->extensions)) {
|
||||
$result['extensions'] = $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
15
src/Executor/ExecutorImplementation.php
Normal file
15
src/Executor/ExecutorImplementation.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Executor;
|
||||
|
||||
use GraphQL\Executor\Promise\Promise;
|
||||
|
||||
interface ExecutorImplementation
|
||||
{
|
||||
/**
|
||||
* Returns promise of {@link ExecutionResult}. Promise should always resolve, never reject.
|
||||
*/
|
||||
public function doExecute() : Promise;
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Executor\Promise\Adapter;
|
||||
|
||||
use GraphQL\Executor\Promise\Promise;
|
||||
|
@ -6,6 +9,9 @@ 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
|
||||
{
|
||||
|
@ -28,10 +34,11 @@ 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 $adoptedPromise ReactPromiseInterface */
|
||||
/** @var ReactPromiseInterface $adoptedPromise */
|
||||
$adoptedPromise = $promise->adoptedPromise;
|
||||
|
||||
return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this);
|
||||
}
|
||||
|
||||
|
@ -41,6 +48,7 @@ class ReactPromiseAdapter implements PromiseAdapter
|
|||
public function create(callable $resolver)
|
||||
{
|
||||
$promise = new ReactPromise($resolver);
|
||||
|
||||
return new Promise($promise, $this);
|
||||
}
|
||||
|
||||
|
@ -49,7 +57,8 @@ class ReactPromiseAdapter implements PromiseAdapter
|
|||
*/
|
||||
public function createFulfilled($value = null)
|
||||
{
|
||||
$promise = \React\Promise\resolve($value);
|
||||
$promise = resolve($value);
|
||||
|
||||
return new Promise($promise, $this);
|
||||
}
|
||||
|
||||
|
@ -58,7 +67,8 @@ class ReactPromiseAdapter implements PromiseAdapter
|
|||
*/
|
||||
public function createRejected($reason)
|
||||
{
|
||||
$promise = \React\Promise\reject($reason);
|
||||
$promise = reject($reason);
|
||||
|
||||
return new Promise($promise, $this);
|
||||
}
|
||||
|
||||
|
@ -68,11 +78,14 @@ 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, function ($item) {
|
||||
return $item instanceof Promise ? $item->adoptedPromise : $item;
|
||||
});
|
||||
$promisesOrValues = Utils::map(
|
||||
$promisesOrValues,
|
||||
static function ($item) {
|
||||
return $item instanceof Promise ? $item->adoptedPromise : $item;
|
||||
}
|
||||
);
|
||||
|
||||
$promise = \React\Promise\all($promisesOrValues)->then(function($values) use ($promisesOrValues) {
|
||||
$promise = all($promisesOrValues)->then(static function ($values) use ($promisesOrValues) {
|
||||
$orderedResults = [];
|
||||
|
||||
foreach ($promisesOrValues as $key => $value) {
|
||||
|
@ -81,6 +94,7 @@ class ReactPromiseAdapter implements PromiseAdapter
|
|||
|
||||
return $orderedResults;
|
||||
});
|
||||
|
||||
return new Promise($promise, $this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,117 +1,164 @@
|
|||
<?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';
|
||||
const REJECTED = 'rejected';
|
||||
|
||||
/**
|
||||
* @var \SplQueue
|
||||
*/
|
||||
/** @var SplQueue */
|
||||
public static $queue;
|
||||
|
||||
public static function getQueue()
|
||||
{
|
||||
return self::$queue ?: self::$queue = new \SplQueue();
|
||||
}
|
||||
/** @var string */
|
||||
public $state = self::PENDING;
|
||||
|
||||
public static function runQueue()
|
||||
/** @var ExecutionResult|Throwable */
|
||||
public $result;
|
||||
|
||||
/**
|
||||
* Promises created in `then` method of this promise and awaiting for resolution of this promise
|
||||
*
|
||||
* @var mixed[][]
|
||||
*/
|
||||
private $waiting = [];
|
||||
|
||||
public static function runQueue() : void
|
||||
{
|
||||
$q = self::$queue;
|
||||
while ($q && !$q->isEmpty()) {
|
||||
while ($q !== null && ! $q->isEmpty()) {
|
||||
$task = $q->dequeue();
|
||||
$task();
|
||||
}
|
||||
}
|
||||
|
||||
public $state = self::PENDING;
|
||||
|
||||
public $result;
|
||||
|
||||
/**
|
||||
* Promises created in `then` method of this promise and awaiting for resolution of this promise
|
||||
* @var array
|
||||
*/
|
||||
private $waiting = [];
|
||||
|
||||
public function reject($reason)
|
||||
{
|
||||
if (!$reason instanceof \Exception && !$reason instanceof \Throwable) {
|
||||
throw new \Exception('SyncPromise::reject() has to be called with an instance of \Throwable');
|
||||
}
|
||||
|
||||
switch ($this->state) {
|
||||
case self::PENDING:
|
||||
$this->state = self::REJECTED;
|
||||
$this->result = $reason;
|
||||
$this->enqueueWaitingPromises();
|
||||
break;
|
||||
case self::REJECTED:
|
||||
if ($reason !== $this->result) {
|
||||
throw new \Exception("Cannot change rejection reason");
|
||||
}
|
||||
break;
|
||||
case self::FULFILLED:
|
||||
throw new \Exception("Cannot reject fulfilled promise");
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function resolve($value)
|
||||
public function resolve($value) : self
|
||||
{
|
||||
switch ($this->state) {
|
||||
case self::PENDING:
|
||||
if ($value === $this) {
|
||||
throw new \Exception("Cannot resolve promise with self");
|
||||
throw new Exception('Cannot resolve promise with self');
|
||||
}
|
||||
if (is_object($value) && method_exists($value, 'then')) {
|
||||
$value->then(
|
||||
function($resolvedValue) {
|
||||
function ($resolvedValue) {
|
||||
$this->resolve($resolvedValue);
|
||||
},
|
||||
function($reason) {
|
||||
function ($reason) {
|
||||
$this->reject($reason);
|
||||
}
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->state = self::FULFILLED;
|
||||
$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");
|
||||
throw new Exception('Cannot change value of fulfilled promise');
|
||||
}
|
||||
break;
|
||||
case self::REJECTED:
|
||||
throw new \Exception("Cannot resolve rejected promise");
|
||||
throw new Exception('Cannot resolve rejected promise');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function then(callable $onFulfilled = null, callable $onRejected = null)
|
||||
public function reject($reason) : self
|
||||
{
|
||||
if ($this->state === self::REJECTED && !$onRejected) {
|
||||
if (! $reason instanceof Exception && ! $reason instanceof Throwable) {
|
||||
throw new Exception('SyncPromise::reject() has to be called with an instance of \Throwable');
|
||||
}
|
||||
|
||||
switch ($this->state) {
|
||||
case self::PENDING:
|
||||
$this->state = self::REJECTED;
|
||||
$this->result = $reason;
|
||||
$this->enqueueWaitingPromises();
|
||||
break;
|
||||
case self::REJECTED:
|
||||
if ($reason !== $this->result) {
|
||||
throw new Exception('Cannot change rejection reason');
|
||||
}
|
||||
break;
|
||||
case self::FULFILLED:
|
||||
throw new Exception('Cannot reject fulfilled promise');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function enqueueWaitingPromises() : void
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
$this->waiting = [];
|
||||
}
|
||||
|
||||
public static function getQueue() : SplQueue
|
||||
{
|
||||
return self::$queue ?: self::$queue = new SplQueue();
|
||||
}
|
||||
|
||||
public function then(?callable $onFulfilled = null, ?callable $onRejected = null)
|
||||
{
|
||||
if ($this->state === self::REJECTED && $onRejected === null) {
|
||||
return $this;
|
||||
}
|
||||
if ($this->state === self::FULFILLED && !$onFulfilled) {
|
||||
if ($this->state === self::FULFILLED && $onFulfilled === null) {
|
||||
return $this;
|
||||
}
|
||||
$tmp = new self();
|
||||
$tmp = new self();
|
||||
$this->waiting[] = [$tmp, $onFulfilled, $onRejected];
|
||||
|
||||
if ($this->state !== self::PENDING) {
|
||||
|
@ -120,39 +167,4 @@ 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 = [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
<?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
|
||||
{
|
||||
|
@ -30,20 +33,22 @@ 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 $promise */
|
||||
$promise = $promise->adoptedPromise;
|
||||
return new Promise($promise->then($onFulfilled, $onRejected), $this);
|
||||
/** @var SyncPromise $adoptedPromise */
|
||||
$adoptedPromise = $promise->adoptedPromise;
|
||||
|
||||
return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -55,12 +60,18 @@ 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);
|
||||
}
|
||||
|
||||
|
@ -73,6 +84,7 @@ class SyncPromiseAdapter implements PromiseAdapter
|
|||
public function createFulfilled($value = null)
|
||||
{
|
||||
$promise = new SyncPromise();
|
||||
|
||||
return new Promise($promise->resolve($value), $this);
|
||||
}
|
||||
|
||||
|
@ -82,6 +94,7 @@ class SyncPromiseAdapter implements PromiseAdapter
|
|||
public function createRejected($reason)
|
||||
{
|
||||
$promise = new SyncPromise();
|
||||
|
||||
return new Promise($promise->reject($reason), $this);
|
||||
}
|
||||
|
||||
|
@ -92,20 +105,22 @@ 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(
|
||||
function($value) use ($index, &$count, $total, &$result, $all) {
|
||||
static function ($value) use ($index, &$count, $total, &$result, $all) {
|
||||
$result[$index] = $value;
|
||||
$count++;
|
||||
if ($count >= $total) {
|
||||
$all->resolve($result);
|
||||
if ($count < $total) {
|
||||
return;
|
||||
}
|
||||
|
||||
$all->resolve($result);
|
||||
},
|
||||
[$all, 'reject']
|
||||
);
|
||||
|
@ -117,24 +132,23 @@ class SyncPromiseAdapter implements PromiseAdapter
|
|||
if ($count === $total) {
|
||||
$all->resolve($result);
|
||||
}
|
||||
|
||||
return new Promise($all, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously wait when promise completes
|
||||
*
|
||||
* @param Promise $promise
|
||||
* @return mixed
|
||||
* @return ExecutionResult
|
||||
*/
|
||||
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();
|
||||
|
@ -146,17 +160,17 @@ class SyncPromiseAdapter implements PromiseAdapter
|
|||
|
||||
if ($syncPromise->state === SyncPromise::FULFILLED) {
|
||||
return $syncPromise->result;
|
||||
} else if ($syncPromise->state === SyncPromise::REJECTED) {
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -164,8 +178,6 @@ class SyncPromiseAdapter implements PromiseAdapter
|
|||
|
||||
/**
|
||||
* Execute while running promise completion
|
||||
*
|
||||
* @param Promise $promise
|
||||
*/
|
||||
protected function onWait(Promise $promise)
|
||||
{
|
||||
|
|
|
@ -1,38 +1,39 @@
|
|||
<?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
|
||||
{
|
||||
private $adapter;
|
||||
|
||||
/** @var SyncPromise|ReactPromise */
|
||||
public $adoptedPromise;
|
||||
|
||||
/** @var PromiseAdapter */
|
||||
private $adapter;
|
||||
|
||||
/**
|
||||
* Promise constructor.
|
||||
*
|
||||
* @param mixed $adoptedPromise
|
||||
* @param PromiseAdapter $adapter
|
||||
*/
|
||||
public function __construct($adoptedPromise, PromiseAdapter $adapter)
|
||||
{
|
||||
Utils::invariant(!$adoptedPromise instanceof self, 'Expecting promise from adapted system, got ' . __CLASS__);
|
||||
Utils::invariant(! $adoptedPromise instanceof self, 'Expecting promise from adapted system, got ' . self::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);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
<?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))
|
||||
*/
|
||||
|
@ -9,18 +14,22 @@ interface PromiseAdapter
|
|||
/**
|
||||
* Return true if the value is a promise or a deferred of the underlying platform
|
||||
*
|
||||
* @api
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function isThenable($value);
|
||||
|
||||
/**
|
||||
* Converts thenable of the underlying platform into GraphQL\Executor\Promise\Promise instance
|
||||
*
|
||||
* @api
|
||||
* @param object $thenable
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function convertThenable($thenable);
|
||||
|
||||
|
@ -28,14 +37,11 @@ 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.
|
||||
*
|
||||
* @api
|
||||
* @param Promise $promise
|
||||
* @param callable|null $onFulfilled
|
||||
* @param callable|null $onRejected
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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
|
||||
|
@ -43,18 +49,20 @@ interface PromiseAdapter
|
|||
* Expected resolver signature:
|
||||
* function(callable $resolve, callable $reject)
|
||||
*
|
||||
* @api
|
||||
* @param callable $resolver
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function create(callable $resolver);
|
||||
|
||||
/**
|
||||
* Creates a fulfilled Promise for a value if the value is not a promise.
|
||||
*
|
||||
* @api
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function createFulfilled($value = null);
|
||||
|
||||
|
@ -62,9 +70,11 @@ 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.
|
||||
*
|
||||
* @api
|
||||
* @param \Throwable $reason
|
||||
* @param Throwable $reason
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function createRejected($reason);
|
||||
|
||||
|
@ -72,9 +82,11 @@ interface PromiseAdapter
|
|||
* Given an array of promises (or values), returns a promise that is fulfilled when all the
|
||||
* items in the array are fulfilled.
|
||||
*
|
||||
* @api
|
||||
* @param array $promisesOrValues Promises or values.
|
||||
* @param Promise[]|mixed[] $promisesOrValues Promises or values.
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function all(array $promisesOrValues);
|
||||
}
|
||||
|
|
1361
src/Executor/ReferenceExecutor.php
Normal file
1361
src/Executor/ReferenceExecutor.php
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,9 +1,10 @@
|
|||
<?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;
|
||||
|
@ -11,23 +12,27 @@ 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\VariableNode;
|
||||
use GraphQL\Language\AST\ValueNode;
|
||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||
use GraphQL\Language\AST\VariableNode;
|
||||
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\Validator\DocumentValidator;
|
||||
use GraphQL\Utils\Value;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
use function array_key_exists;
|
||||
use function array_map;
|
||||
use function sprintf;
|
||||
|
||||
class Values
|
||||
{
|
||||
|
@ -36,134 +41,78 @@ class Values
|
|||
* variable definitions and arbitrary input. If the input cannot be coerced
|
||||
* to match the variable definitions, a Error will be thrown.
|
||||
*
|
||||
* @param Schema $schema
|
||||
* @param VariableDefinitionNode[] $definitionNodes
|
||||
* @param array $inputs
|
||||
* @return array
|
||||
* @throws Error
|
||||
* @param VariableDefinitionNode[] $varDefNodes
|
||||
* @param mixed[] $inputs
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function getVariableValues(Schema $schema, $definitionNodes, array $inputs)
|
||||
public static function getVariableValues(Schema $schema, $varDefNodes, array $inputs)
|
||||
{
|
||||
$errors = [];
|
||||
$coercedValues = [];
|
||||
foreach ($definitionNodes as $definitionNode) {
|
||||
$varName = $definitionNode->variable->name->value;
|
||||
$varType = TypeInfo::typeFromAST($schema, $definitionNode->type);
|
||||
foreach ($varDefNodes as $varDefNode) {
|
||||
$varName = $varDefNode->variable->name->value;
|
||||
/** @var InputType|Type $varType */
|
||||
$varType = TypeInfo::typeFromAST($schema, $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 (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 (!array_key_exists($varName, $inputs)) {
|
||||
$defaultValue = $definitionNode->defaultValue;
|
||||
if ($defaultValue) {
|
||||
$coercedValues[$varName] = AST::valueFromAST($defaultValue, $varType);
|
||||
}
|
||||
if ($varType instanceof NonNull) {
|
||||
throw new Error(
|
||||
'Variable "$'.$varName .'" of required type ' .
|
||||
'"'. Utils::printSafe($varType) . '" was not provided.',
|
||||
[$definitionNode]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$value = $inputs[$varName];
|
||||
$errors = self::isValidPHPValue($value, $varType);
|
||||
if (!empty($errors)) {
|
||||
$message = "\n" . implode("\n", $errors);
|
||||
throw new Error(
|
||||
'Variable "$' . $varName . '" got invalid value ' .
|
||||
json_encode($value) . '.' . $message,
|
||||
[$definitionNode]
|
||||
);
|
||||
}
|
||||
|
||||
$coercedValue = self::coerceValue($varType, $value);
|
||||
Utils::invariant($coercedValue !== Utils::undefined(), 'Should have reported error.');
|
||||
$coercedValues[$varName] = $coercedValue;
|
||||
}
|
||||
}
|
||||
return $coercedValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares an object map of argument values given a list of argument
|
||||
* definitions and list of argument AST nodes.
|
||||
*
|
||||
* @param FieldDefinition|Directive $def
|
||||
* @param FieldNode|\GraphQL\Language\AST\DirectiveNode $node
|
||||
* @param $variableValues
|
||||
* @return array
|
||||
* @throws Error
|
||||
*/
|
||||
public static function getArgumentValues($def, $node, $variableValues = null)
|
||||
{
|
||||
$argDefs = $def->args;
|
||||
$argNodes = $node->arguments;
|
||||
|
||||
if (!$argDefs || null === $argNodes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$coercedValues = [];
|
||||
$undefined = Utils::undefined();
|
||||
|
||||
/** @var ArgumentNode[] $argNodeMap */
|
||||
$argNodeMap = $argNodes ? Utils::keyMap($argNodes, function (ArgumentNode $arg) {
|
||||
return $arg->name->value;
|
||||
}) : [];
|
||||
|
||||
foreach ($argDefs as $argDef) {
|
||||
$name = $argDef->name;
|
||||
$argType = $argDef->getType();
|
||||
$argumentNode = isset($argNodeMap[$name]) ? $argNodeMap[$name] : null;
|
||||
|
||||
if (!$argumentNode) {
|
||||
if ($argDef->defaultValueExists()) {
|
||||
$coercedValues[$name] = $argDef->defaultValue;
|
||||
} else if ($argType instanceof NonNull) {
|
||||
throw new Error(
|
||||
'Argument "' . $name . '" of required type ' .
|
||||
'"' . Utils::printSafe($argType) . '" was not provided.',
|
||||
[$node]
|
||||
);
|
||||
}
|
||||
} else if ($argumentNode->value instanceof VariableNode) {
|
||||
$variableName = $argumentNode->value->name->value;
|
||||
|
||||
if ($variableValues && array_key_exists($variableName, $variableValues)) {
|
||||
// Note: this does not check that this variable value is correct.
|
||||
// This assumes that this query has been validated and the variable
|
||||
// usage here is of the correct type.
|
||||
$coercedValues[$name] = $variableValues[$variableName];
|
||||
} else if ($argDef->defaultValueExists()) {
|
||||
$coercedValues[$name] = $argDef->defaultValue;
|
||||
} else if ($argType instanceof NonNull) {
|
||||
throw new Error(
|
||||
'Argument "' . $name . '" of required type "' . Utils::printSafe($argType) . '" was ' .
|
||||
'provided the variable "$' . $variableName . '" which was not provided ' .
|
||||
'a runtime value.',
|
||||
[ $argumentNode->value ]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$valueNode = $argumentNode->value;
|
||||
$coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues);
|
||||
if ($coercedValue === $undefined) {
|
||||
$errors = DocumentValidator::isValidLiteralValue($argType, $valueNode);
|
||||
$message = !empty($errors) ? ("\n" . implode("\n", $errors)) : '';
|
||||
throw new Error(
|
||||
'Argument "' . $name . '" got invalid value ' . Printer::doPrint($valueNode) . '.' . $message,
|
||||
[ $argumentNode->value ]
|
||||
);
|
||||
}
|
||||
$coercedValues[$name] = $coercedValue;
|
||||
}
|
||||
if (! empty($errors)) {
|
||||
return [$errors, null];
|
||||
}
|
||||
return $coercedValues;
|
||||
|
||||
return [null, $coercedValues];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -173,213 +122,158 @@ class Values
|
|||
*
|
||||
* If the directive does not exist on the node, returns undefined.
|
||||
*
|
||||
* @param Directive $directiveDef
|
||||
* @param FragmentSpreadNode | FieldNode | InlineFragmentNode | EnumValueDefinitionNode | FieldDefinitionNode $node
|
||||
* @param array|null $variableValues
|
||||
* @param FragmentSpreadNode|FieldNode|InlineFragmentNode|EnumValueDefinitionNode|FieldDefinitionNode $node
|
||||
* @param mixed[]|null $variableValues
|
||||
*
|
||||
* @return array|null
|
||||
* @return mixed[]|null
|
||||
*/
|
||||
public static function getDirectiveValues(Directive $directiveDef, $node, $variableValues = null)
|
||||
{
|
||||
if (isset($node->directives) && $node->directives instanceof NodeList) {
|
||||
$directiveNode = Utils::find($node->directives, function(DirectiveNode $directive) use ($directiveDef) {
|
||||
return $directive->name->value === $directiveDef->name;
|
||||
});
|
||||
$directiveNode = Utils::find(
|
||||
$node->directives,
|
||||
static function (DirectiveNode $directive) use ($directiveDef) {
|
||||
return $directive->name->value === $directiveDef->name;
|
||||
}
|
||||
);
|
||||
|
||||
if ($directiveNode) {
|
||||
if ($directiveNode !== null) {
|
||||
return self::getArgumentValues($directiveDef, $directiveNode, $variableValues);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares an object map of argument values given a list of argument
|
||||
* definitions and list of argument AST nodes.
|
||||
*
|
||||
* @param FieldDefinition|Directive $def
|
||||
* @param FieldNode|DirectiveNode $node
|
||||
* @param mixed[] $variableValues
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public static function getArgumentValues($def, $node, $variableValues = null)
|
||||
{
|
||||
if (empty($def->args)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$argumentNodes = $node->arguments;
|
||||
if (empty($argumentNodes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$argumentValueMap = [];
|
||||
foreach ($argumentNodes as $argumentNode) {
|
||||
$argumentValueMap[$argumentNode->name->value] = $argumentNode->value;
|
||||
}
|
||||
|
||||
return static::getArgumentValuesForMap($def, $argumentValueMap, $variableValues, $node);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FieldDefinition|Directive $fieldDefinition
|
||||
* @param ArgumentNode[] $argumentValueMap
|
||||
* @param mixed[] $variableValues
|
||||
* @param Node|null $referenceNode
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public static function getArgumentValuesForMap($fieldDefinition, $argumentValueMap, $variableValues = null, $referenceNode = null)
|
||||
{
|
||||
$argumentDefinitions = $fieldDefinition->args;
|
||||
$coercedValues = [];
|
||||
|
||||
foreach ($argumentDefinitions as $argumentDefinition) {
|
||||
$name = $argumentDefinition->name;
|
||||
$argType = $argumentDefinition->getType();
|
||||
$argumentValueNode = $argumentValueMap[$name] ?? null;
|
||||
|
||||
if ($argumentValueNode === 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
|
||||
* @param InputType $type
|
||||
* @param null $variables
|
||||
* @return array|null|\stdClass
|
||||
* @param ValueNode $valueNode
|
||||
* @param mixed[]|null $variables
|
||||
*
|
||||
* @return mixed[]|stdClass|null
|
||||
*/
|
||||
public static function valueFromAST($valueNode, InputType $type, $variables = null)
|
||||
public static function valueFromAST($valueNode, InputType $type, ?array $variables = null)
|
||||
{
|
||||
return AST::valueFromAST($valueNode, $type, $variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a PHP value and a GraphQL type, determine if the value will be
|
||||
* accepted for that type. This is primarily useful for validating the
|
||||
* runtime values of query variables.
|
||||
* @deprecated as of 0.12 (Use coerceValue() directly for richer information)
|
||||
*
|
||||
* @param $value
|
||||
* @param InputType $type
|
||||
* @return array
|
||||
* @param mixed[] $value
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function isValidPHPValue($value, InputType $type)
|
||||
{
|
||||
// A value must be provided if the type is non-null.
|
||||
if ($type instanceof NonNull) {
|
||||
if (null === $value) {
|
||||
return ['Expected "' . Utils::printSafe($type) . '", found null.'];
|
||||
}
|
||||
return self::isValidPHPValue($value, $type->getWrappedType());
|
||||
}
|
||||
$errors = Value::coerceValue($value, $type)['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');
|
||||
return $errors
|
||||
? array_map(
|
||||
static function (Throwable $error) {
|
||||
return $error->getMessage();
|
||||
},
|
||||
$errors
|
||||
)
|
||||
: [];
|
||||
}
|
||||
}
|
||||
|
|
283
src/Experimental/Executor/Collector.php
Normal file
283
src/Experimental/Executor/Collector.php
Normal file
|
@ -0,0 +1,283 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Experimental\Executor;
|
||||
|
||||
use Generator;
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\DefinitionNode;
|
||||
use GraphQL\Language\AST\DocumentNode;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\InlineFragmentNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeKind;
|
||||
use GraphQL\Language\AST\OperationDefinitionNode;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Language\AST\ValueNode;
|
||||
use GraphQL\Type\Definition\AbstractType;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Introspection;
|
||||
use GraphQL\Type\Schema;
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Collector
|
||||
{
|
||||
/** @var Schema */
|
||||
private $schema;
|
||||
|
||||
/** @var Runtime */
|
||||
private $runtime;
|
||||
|
||||
/** @var OperationDefinitionNode|null */
|
||||
public $operation = null;
|
||||
|
||||
/** @var FragmentDefinitionNode[] */
|
||||
public $fragments = [];
|
||||
|
||||
/** @var ObjectType|null */
|
||||
public $rootType;
|
||||
|
||||
/** @var FieldNode[][] */
|
||||
private $fields;
|
||||
|
||||
/** @var string[] */
|
||||
private $visitedFragments;
|
||||
|
||||
public function __construct(Schema $schema, Runtime $runtime)
|
||||
{
|
||||
$this->schema = $schema;
|
||||
$this->runtime = $runtime;
|
||||
}
|
||||
|
||||
public function initialize(DocumentNode $documentNode, ?string $operationName = null)
|
||||
{
|
||||
$hasMultipleAssumedOperations = false;
|
||||
|
||||
foreach ($documentNode->definitions as $definitionNode) {
|
||||
/** @var DefinitionNode|Node $definitionNode */
|
||||
|
||||
if ($definitionNode 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
src/Experimental/Executor/CoroutineContext.php
Normal file
57
src/Experimental/Executor/CoroutineContext.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Experimental\Executor;
|
||||
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CoroutineContext
|
||||
{
|
||||
/** @var CoroutineContextShared */
|
||||
public $shared;
|
||||
|
||||
/** @var ObjectType */
|
||||
public $type;
|
||||
|
||||
/** @var mixed */
|
||||
public $value;
|
||||
|
||||
/** @var object */
|
||||
public $result;
|
||||
|
||||
/** @var string[] */
|
||||
public $path;
|
||||
|
||||
/** @var ResolveInfo|null */
|
||||
public $resolveInfo;
|
||||
|
||||
/** @var string[]|null */
|
||||
public $nullFence;
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param object $result
|
||||
* @param string[] $path
|
||||
* @param string[]|null $nullFence
|
||||
*/
|
||||
public function __construct(
|
||||
CoroutineContextShared $shared,
|
||||
ObjectType $type,
|
||||
$value,
|
||||
$result,
|
||||
array $path,
|
||||
?array $nullFence = null
|
||||
) {
|
||||
$this->shared = $shared;
|
||||
$this->type = $type;
|
||||
$this->value = $value;
|
||||
$this->result = $result;
|
||||
$this->path = $path;
|
||||
$this->nullFence = $nullFence;
|
||||
}
|
||||
}
|
62
src/Experimental/Executor/CoroutineContextShared.php
Normal file
62
src/Experimental/Executor/CoroutineContextShared.php
Normal file
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Experimental\Executor;
|
||||
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Language\AST\ValueNode;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CoroutineContextShared
|
||||
{
|
||||
/** @var FieldNode[] */
|
||||
public $fieldNodes;
|
||||
|
||||
/** @var string */
|
||||
public $fieldName;
|
||||
|
||||
/** @var string */
|
||||
public $resultName;
|
||||
|
||||
/** @var ValueNode[]|null */
|
||||
public $argumentValueMap;
|
||||
|
||||
/** @var SelectionSetNode|null */
|
||||
public $mergedSelectionSet;
|
||||
|
||||
/** @var ObjectType|null */
|
||||
public $typeGuard1;
|
||||
|
||||
/** @var callable|null */
|
||||
public $resolveIfType1;
|
||||
|
||||
/** @var mixed */
|
||||
public $argumentsIfType1;
|
||||
|
||||
/** @var ResolveInfo|null */
|
||||
public $resolveInfoIfType1;
|
||||
|
||||
/** @var ObjectType|null */
|
||||
public $typeGuard2;
|
||||
|
||||
/** @var CoroutineContext[]|null */
|
||||
public $childContextsIfType2;
|
||||
|
||||
/**
|
||||
* @param FieldNode[] $fieldNodes
|
||||
* @param mixed[]|null $argumentValueMap
|
||||
*/
|
||||
public function __construct(array $fieldNodes, string $fieldName, string $resultName, ?array $argumentValueMap)
|
||||
{
|
||||
$this->fieldNodes = $fieldNodes;
|
||||
$this->fieldName = $fieldName;
|
||||
$this->resultName = $resultName;
|
||||
$this->argumentValueMap = $argumentValueMap;
|
||||
}
|
||||
}
|
952
src/Experimental/Executor/CoroutineExecutor.php
Normal file
952
src/Experimental/Executor/CoroutineExecutor.php
Normal file
|
@ -0,0 +1,952 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
18
src/Experimental/Executor/Runtime.php
Normal file
18
src/Experimental/Executor/Runtime.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Experimental\Executor;
|
||||
|
||||
use GraphQL\Language\AST\ValueNode;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
interface Runtime
|
||||
{
|
||||
public function evaluate(ValueNode $valueNode, InputType $type);
|
||||
|
||||
public function addError($error);
|
||||
}
|
35
src/Experimental/Executor/Strand.php
Normal file
35
src/Experimental/Executor/Strand.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Experimental\Executor;
|
||||
|
||||
use Generator;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Strand
|
||||
{
|
||||
/** @var Generator */
|
||||
public $current;
|
||||
|
||||
/** @var Generator[] */
|
||||
public $stack;
|
||||
|
||||
/** @var int */
|
||||
public $depth;
|
||||
|
||||
/** @var bool|null */
|
||||
public $success;
|
||||
|
||||
/** @var mixed */
|
||||
public $value;
|
||||
|
||||
public function __construct(Generator $coroutine)
|
||||
{
|
||||
$this->current = $coroutine;
|
||||
$this->stack = [];
|
||||
$this->depth = 0;
|
||||
}
|
||||
}
|
237
src/GraphQL.php
237
src/GraphQL.php
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
|
@ -6,15 +9,21 @@ 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.
|
||||
|
@ -57,33 +66,37 @@ 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)
|
||||
*
|
||||
* @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
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $context
|
||||
* @param mixed[]|null $variableValues
|
||||
* @param ValidationRule[] $validationRules
|
||||
*
|
||||
* @return ExecutionResult
|
||||
* @api
|
||||
*/
|
||||
public static function executeQuery(
|
||||
\GraphQL\Type\Schema $schema,
|
||||
SchemaType $schema,
|
||||
$source,
|
||||
$rootValue = null,
|
||||
$context = null,
|
||||
$variableValues = null,
|
||||
$operationName = null,
|
||||
callable $fieldResolver = null,
|
||||
array $validationRules = null
|
||||
)
|
||||
{
|
||||
?string $operationName = null,
|
||||
?callable $fieldResolver = null,
|
||||
?array $validationRules = null
|
||||
) : ExecutionResult {
|
||||
$promiseAdapter = new SyncPromiseAdapter();
|
||||
|
||||
$promise = self::promiseToExecute($promiseAdapter, $schema, $source, $rootValue, $context,
|
||||
$variableValues, $operationName, $fieldResolver, $validationRules);
|
||||
$promise = self::promiseToExecute(
|
||||
$promiseAdapter,
|
||||
$schema,
|
||||
$source,
|
||||
$rootValue,
|
||||
$context,
|
||||
$variableValues,
|
||||
$operationName,
|
||||
$fieldResolver,
|
||||
$validationRules
|
||||
);
|
||||
|
||||
return $promiseAdapter->wait($promise);
|
||||
}
|
||||
|
@ -92,31 +105,25 @@ class GraphQL
|
|||
* Same as executeQuery(), but requires PromiseAdapter and always returns a Promise.
|
||||
* Useful for Async PHP platforms.
|
||||
*
|
||||
* @api
|
||||
* @param PromiseAdapter $promiseAdapter
|
||||
* @param \GraphQL\Type\Schema $schema
|
||||
* @param string|DocumentNode $source
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $context
|
||||
* @param array|null $variableValues
|
||||
* @param string|null $operationName
|
||||
* @param callable $fieldResolver
|
||||
* @param array $validationRules
|
||||
* @param string|DocumentNode $source
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $context
|
||||
* @param mixed[]|null $variableValues
|
||||
* @param ValidationRule[]|null $validationRules
|
||||
*
|
||||
* @return Promise
|
||||
* @api
|
||||
*/
|
||||
public static function promiseToExecute(
|
||||
PromiseAdapter $promiseAdapter,
|
||||
\GraphQL\Type\Schema $schema,
|
||||
SchemaType $schema,
|
||||
$source,
|
||||
$rootValue = null,
|
||||
$context = null,
|
||||
$variableValues = null,
|
||||
$operationName = null,
|
||||
callable $fieldResolver = null,
|
||||
array $validationRules = null
|
||||
)
|
||||
{
|
||||
?string $operationName = null,
|
||||
?callable $fieldResolver = null,
|
||||
?array $validationRules = null
|
||||
) : Promise {
|
||||
try {
|
||||
if ($source instanceof DocumentNode) {
|
||||
$documentNode = $source;
|
||||
|
@ -125,36 +132,38 @@ class GraphQL
|
|||
}
|
||||
|
||||
// FIXME
|
||||
if (!empty($validationRules)) {
|
||||
foreach ($validationRules as $rule) {
|
||||
if ($rule instanceof QueryComplexity) {
|
||||
$rule->setRawVariableValues($variableValues);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (empty($validationRules)) {
|
||||
/** @var QueryComplexity $queryComplexity */
|
||||
$queryComplexity = DocumentValidator::getRule(QueryComplexity::class);
|
||||
$queryComplexity->setRawVariableValues($variableValues);
|
||||
} else {
|
||||
foreach ($validationRules as $rule) {
|
||||
if (! ($rule instanceof QueryComplexity)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rule->setRawVariableValues($variableValues);
|
||||
}
|
||||
}
|
||||
|
||||
$validationErrors = DocumentValidator::validate($schema, $documentNode, $validationRules);
|
||||
|
||||
if (!empty($validationErrors)) {
|
||||
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])
|
||||
|
@ -165,25 +174,29 @@ class GraphQL
|
|||
/**
|
||||
* @deprecated Use executeQuery()->toArray() instead
|
||||
*
|
||||
* @param \GraphQL\Type\Schema $schema
|
||||
* @param string|DocumentNode $source
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $contextValue
|
||||
* @param array|null $variableValues
|
||||
* @param string|null $operationName
|
||||
* @return Promise|array
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $contextValue
|
||||
* @param mixed[]|null $variableValues
|
||||
*
|
||||
* @return Promise|mixed[]
|
||||
*/
|
||||
public static function execute(
|
||||
\GraphQL\Type\Schema $schema,
|
||||
SchemaType $schema,
|
||||
$source,
|
||||
$rootValue = null,
|
||||
$contextValue = null,
|
||||
$variableValues = null,
|
||||
$operationName = null
|
||||
)
|
||||
{
|
||||
$result = self::promiseToExecute(
|
||||
$promiseAdapter = Executor::getPromiseAdapter(),
|
||||
?string $operationName = null
|
||||
) {
|
||||
trigger_error(
|
||||
__METHOD__ . ' is deprecated, use GraphQL::executeQuery()->toArray() as a quick replacement',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
|
||||
$promiseAdapter = Executor::getPromiseAdapter();
|
||||
$result = self::promiseToExecute(
|
||||
$promiseAdapter,
|
||||
$schema,
|
||||
$source,
|
||||
$rootValue,
|
||||
|
@ -195,36 +208,40 @@ class GraphQL
|
|||
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
||||
$result = $promiseAdapter->wait($result)->toArray();
|
||||
} else {
|
||||
$result = $result->then(function(ExecutionResult $r) {
|
||||
$result = $result->then(static function (ExecutionResult $r) {
|
||||
return $r->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated renamed to executeQuery()
|
||||
*
|
||||
* @param \GraphQL\Type\Schema $schema
|
||||
* @param string|DocumentNode $source
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $contextValue
|
||||
* @param array|null $variableValues
|
||||
* @param string|null $operationName
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $contextValue
|
||||
* @param mixed[]|null $variableValues
|
||||
*
|
||||
* @return ExecutionResult|Promise
|
||||
*/
|
||||
public static function executeAndReturnResult(
|
||||
\GraphQL\Type\Schema $schema,
|
||||
SchemaType $schema,
|
||||
$source,
|
||||
$rootValue = null,
|
||||
$contextValue = null,
|
||||
$variableValues = null,
|
||||
$operationName = null
|
||||
)
|
||||
{
|
||||
$result = self::promiseToExecute(
|
||||
$promiseAdapter = Executor::getPromiseAdapter(),
|
||||
?string $operationName = null
|
||||
) {
|
||||
trigger_error(
|
||||
__METHOD__ . ' is deprecated, use GraphQL::executeQuery() as a quick replacement',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
|
||||
$promiseAdapter = Executor::getPromiseAdapter();
|
||||
$result = self::promiseToExecute(
|
||||
$promiseAdapter,
|
||||
$schema,
|
||||
$source,
|
||||
$rootValue,
|
||||
|
@ -232,19 +249,22 @@ class GraphQL
|
|||
$variableValues,
|
||||
$operationName
|
||||
);
|
||||
|
||||
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
||||
$result = $promiseAdapter->wait($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns directives defined in GraphQL spec
|
||||
*
|
||||
* @api
|
||||
* @return Directive[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function getStandardDirectives()
|
||||
public static function getStandardDirectives() : array
|
||||
{
|
||||
return array_values(Directive::getInternalDirectives());
|
||||
}
|
||||
|
@ -252,48 +272,79 @@ class GraphQL
|
|||
/**
|
||||
* Returns types defined in GraphQL spec
|
||||
*
|
||||
* @api
|
||||
* @return Type[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function getStandardTypes()
|
||||
public static function getStandardTypes() : array
|
||||
{
|
||||
return array_values(Type::getInternalTypes());
|
||||
return array_values(Type::getStandardTypes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces standard types with types from this list (matching by name)
|
||||
* Standard types not listed here remain untouched.
|
||||
*
|
||||
* @param Type[] $types
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function overrideStandardTypes(array $types)
|
||||
{
|
||||
Type::overrideStandardTypes($types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns standard validation rules implementing GraphQL spec
|
||||
*
|
||||
* @return ValidationRule[]
|
||||
*
|
||||
* @api
|
||||
* @return AbstractValidationRule[]
|
||||
*/
|
||||
public static function getStandardValidationRules()
|
||||
public static function getStandardValidationRules() : array
|
||||
{
|
||||
return array_values(DocumentValidator::defaultRules());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $fn
|
||||
* Set default resolver implementation
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function setDefaultFieldResolver(callable $fn)
|
||||
public static function setDefaultFieldResolver(callable $fn) : void
|
||||
{
|
||||
Executor::setDefaultFieldResolver($fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PromiseAdapter|null $promiseAdapter
|
||||
*/
|
||||
public static function setPromiseAdapter(PromiseAdapter $promiseAdapter = null)
|
||||
public static function setPromiseAdapter(?PromiseAdapter $promiseAdapter = null) : void
|
||||
{
|
||||
Executor::setPromiseAdapter($promiseAdapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Experimental: Switch to the new executor
|
||||
*/
|
||||
public static function useExperimentalExecutor()
|
||||
{
|
||||
Executor::setImplementationFactory([CoroutineExecutor::class, 'create']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Experimental: Switch back to the default executor
|
||||
*/
|
||||
public static function useReferenceExecutor()
|
||||
{
|
||||
Executor::setImplementationFactory([ReferenceExecutor::class, 'create']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns directives defined in GraphQL spec
|
||||
*
|
||||
* @deprecated Renamed to getStandardDirectives
|
||||
*
|
||||
* @return Directive[]
|
||||
*/
|
||||
public static function getInternalDirectives()
|
||||
public static function getInternalDirectives() : array
|
||||
{
|
||||
return self::getStandardDirectives();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
<?php
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class BooleanValueNode extends Node implements ValueNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::BOOLEAN;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var bool */
|
||||
public $value;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<?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.
|
||||
*/
|
||||
}
|
||||
|
|
|
@ -1,25 +1,23 @@
|
|||
<?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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class DocumentNode extends Node
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::DOCUMENT;
|
||||
|
||||
/**
|
||||
* @var DefinitionNode[]
|
||||
*/
|
||||
/** @var NodeList|DefinitionNode[] */
|
||||
public $definitions;
|
||||
}
|
||||
|
|
|
@ -1,30 +1,23 @@
|
|||
<?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[]
|
||||
*/
|
||||
/** @var EnumValueDefinitionNode[]|NodeList|null */
|
||||
public $values;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var StringValueNode|null */
|
||||
public $description;
|
||||
}
|
||||
|
|
20
src/Language/AST/EnumTypeExtensionNode.php
Normal file
20
src/Language/AST/EnumTypeExtensionNode.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class EnumTypeExtensionNode extends Node implements TypeExtensionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::ENUM_TYPE_EXTENSION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
|
||||
/** @var EnumValueDefinitionNode[]|null */
|
||||
public $values;
|
||||
}
|
|
@ -1,25 +1,20 @@
|
|||
<?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 string
|
||||
*/
|
||||
/** @var StringValueNode|null */
|
||||
public $description;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class EnumValueNode extends Node implements ValueNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::ENUM;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
public $value;
|
||||
}
|
||||
|
|
14
src/Language/AST/ExecutableDefinitionNode.php
Normal file
14
src/Language/AST/ExecutableDefinitionNode.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
/**
|
||||
* export type ExecutableDefinitionNode =
|
||||
* | OperationDefinitionNode
|
||||
* | FragmentDefinitionNode;
|
||||
*/
|
||||
interface ExecutableDefinitionNode extends DefinitionNode
|
||||
{
|
||||
}
|
|
@ -1,35 +1,26 @@
|
|||
<?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[]
|
||||
*/
|
||||
/** @var InputValueDefinitionNode[]|NodeList */
|
||||
public $arguments;
|
||||
|
||||
/**
|
||||
* @var TypeNode
|
||||
*/
|
||||
/** @var TypeNode */
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* @var DirectiveNode[]
|
||||
*/
|
||||
/** @var DirectiveNode[]|NodeList */
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var StringValueNode|null */
|
||||
public $description;
|
||||
}
|
||||
|
|
|
@ -1,32 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class FieldNode extends Node implements SelectionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::FIELD;
|
||||
|
||||
/**
|
||||
* @var NameNode
|
||||
*/
|
||||
/** @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;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class FloatValueNode extends Node implements ValueNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::FLOAT;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
public $value;
|
||||
}
|
||||
|
|
|
@ -1,27 +1,31 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class FragmentDefinitionNode extends Node implements DefinitionNode, HasSelectionSet
|
||||
class FragmentDefinitionNode extends Node implements ExecutableDefinitionNode, HasSelectionSet
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::FRAGMENT_DEFINITION;
|
||||
|
||||
/**
|
||||
* @var NameNode
|
||||
*/
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var NamedTypeNode
|
||||
* Note: fragment variable definitions are experimental and may be changed
|
||||
* or removed in the future.
|
||||
*
|
||||
* @var VariableDefinitionNode[]|NodeList
|
||||
*/
|
||||
public $variableDefinitions;
|
||||
|
||||
/** @var NamedTypeNode */
|
||||
public $typeCondition;
|
||||
|
||||
/**
|
||||
* @var DirectiveNode[]
|
||||
*/
|
||||
/** @var DirectiveNode[]|NodeList */
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* @var SelectionSetNode
|
||||
*/
|
||||
/** @var SelectionSetNode */
|
||||
public $selectionSet;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
interface HasSelectionSet
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class InlineFragmentNode extends Node implements SelectionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::INLINE_FRAGMENT;
|
||||
|
||||
/**
|
||||
* @var NamedTypeNode
|
||||
*/
|
||||
/** @var NamedTypeNode */
|
||||
public $typeCondition;
|
||||
|
||||
/**
|
||||
* @var DirectiveNode[]|null
|
||||
*/
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* @var SelectionSetNode
|
||||
*/
|
||||
/** @var SelectionSetNode */
|
||||
public $selectionSet;
|
||||
}
|
||||
|
|
|
@ -1,30 +1,23 @@
|
|||
<?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[]
|
||||
*/
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* @var InputValueDefinitionNode[]
|
||||
*/
|
||||
/** @var InputValueDefinitionNode[]|null */
|
||||
public $fields;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var StringValueNode|null */
|
||||
public $description;
|
||||
}
|
||||
|
|
20
src/Language/AST/InputObjectTypeExtensionNode.php
Normal file
20
src/Language/AST/InputObjectTypeExtensionNode.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class InputObjectTypeExtensionNode extends Node implements TypeExtensionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::INPUT_OBJECT_TYPE_EXTENSION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
|
||||
/** @var InputValueDefinitionNode[]|null */
|
||||
public $fields;
|
||||
}
|
|
@ -1,35 +1,26 @@
|
|||
<?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 string
|
||||
*/
|
||||
/** @var StringValueNode|null */
|
||||
public $description;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class IntValueNode extends Node implements ValueNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::INT;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
public $value;
|
||||
}
|
||||
|
|
|
@ -1,30 +1,23 @@
|
|||
<?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[]
|
||||
*/
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
|
||||
/**
|
||||
* @var FieldDefinitionNode[]
|
||||
*/
|
||||
public $fields = [];
|
||||
/** @var FieldDefinitionNode[]|null */
|
||||
public $fields;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var StringValueNode|null */
|
||||
public $description;
|
||||
}
|
||||
|
|
20
src/Language/AST/InterfaceTypeExtensionNode.php
Normal file
20
src/Language/AST/InterfaceTypeExtensionNode.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class InterfaceTypeExtensionNode extends Node implements TypeExtensionNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::INTERFACE_TYPE_EXTENSION;
|
||||
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
|
||||
/** @var DirectiveNode[]|null */
|
||||
public $directives;
|
||||
|
||||
/** @var FieldDefinitionNode[]|null */
|
||||
public $fields;
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ListTypeNode extends Node implements TypeNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::LIST_TYPE;
|
||||
|
||||
/**
|
||||
* @var Node
|
||||
*/
|
||||
/** @var TypeNode */
|
||||
public $type;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class ListValueNode extends Node implements ValueNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::LST;
|
||||
|
||||
/**
|
||||
* @var ValueNode[]
|
||||
*/
|
||||
/** @var ValueNode[]|NodeList */
|
||||
public $values;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
use GraphQL\Language\Source;
|
||||
|
@ -46,27 +49,31 @@ class Location
|
|||
public $source;
|
||||
|
||||
/**
|
||||
* @param $start
|
||||
* @param $end
|
||||
* @param int $start
|
||||
* @param int $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 && $endToken) {
|
||||
$this->start = $startToken->start;
|
||||
$this->end = $endToken->end;
|
||||
if ($startToken === null || $endToken === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->start = $startToken->start;
|
||||
$this->end = $endToken->end;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class NameNode extends Node implements TypeNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::NAME;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
public $value;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Language\AST;
|
||||
|
||||
class NamedTypeNode extends Node implements TypeNode
|
||||
{
|
||||
/** @var string */
|
||||
public $kind = NodeKind::NAMED_TYPE;
|
||||
|
||||
/**
|
||||
* @var NameNode
|
||||
*/
|
||||
/** @var NameNode */
|
||||
public $name;
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue