mirror of
https://github.com/retailcrm/graphql-php.git
synced 2025-04-04 05:03:31 +03:00
Compare commits
709 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 | ||
|
1eb2ccac76 | ||
|
d95fb461ee | ||
|
a1652468f0 | ||
|
df1b575469 | ||
|
6fdcfd9bb0 | ||
|
cbc744ea08 | ||
|
1f68909eb0 | ||
|
a3b6974249 | ||
|
90602b31ba | ||
|
7f346d5658 | ||
|
de791536ce | ||
|
d5e3d08d85 | ||
|
085516bdda | ||
|
bd444752f8 | ||
|
199caf3080 | ||
|
71343f2f62 | ||
|
8098b2b886 | ||
|
009cdecb94 | ||
|
1b4f983f3f | ||
|
3ef2d2827b | ||
|
0af1fb2793 | ||
|
ed66291308 | ||
|
1c143360ca | ||
|
99356f7faf | ||
|
4f374bca83 | ||
|
203fddfe4e | ||
|
9499e5ae8e | ||
|
637156fe65 | ||
|
2537a62ec2 | ||
|
2bfce65484 | ||
|
83cc9132a0 | ||
|
d578b8a22f | ||
|
c04d037fb1 | ||
|
c65d8d8624 | ||
|
e52fe8c384 | ||
|
03629c1e3c | ||
|
1d38643538 | ||
|
a2be92937e | ||
|
b294329a40 | ||
|
51e877bfba | ||
|
b4d767bad6 | ||
|
a50c9a4c1f | ||
|
4634f214ea | ||
|
b56083b7de | ||
|
1ee226465b | ||
|
f369d4e2d4 | ||
|
88c959edad | ||
|
3971001f6d | ||
|
828c6b0fc3 | ||
|
9931cde6d4 | ||
|
e4813c3a05 | ||
|
8817d54e83 | ||
|
b9d3a11785 | ||
|
f47db61907 | ||
|
884a8967f3 | ||
|
20f8cab943 | ||
|
f9eb14869f | ||
|
6845b28a35 | ||
|
ed3591c1a9 | ||
|
34eae0b891 | ||
|
9d150c7702 | ||
|
658816180a | ||
|
d3580e959e | ||
|
2c8c7baa87 | ||
|
f911fac7b1 | ||
|
fa17d6c461 | ||
|
09070485c1 | ||
|
3a8301f6c6 | ||
|
7afd6d3f9a | ||
|
6a20483b87 | ||
|
e7838d2253 | ||
|
3e6f2c9e83 | ||
|
1af902865b | ||
|
e04d3300a7 | ||
|
b2ec265d4f | ||
|
87c812b221 | ||
|
81986145fe | ||
|
08a68d4857 | ||
|
e6e531b88b | ||
|
90088c7bde | ||
|
38922dbbed | ||
|
fbcd20814a | ||
|
8e3d1eb29b | ||
|
8fe26a1a21 | ||
|
49208d758d | ||
|
919cf80240 | ||
|
24ffd605f4 | ||
|
0e2ac57515 | ||
|
f8c3195e54 | ||
|
d2cbb0c354 | ||
|
794d3672ef | ||
|
a3b40db0fb | ||
|
3f04d29628 | ||
|
37a42ededd | ||
|
65d9472b0b | ||
|
bf0a7a8e2b | ||
|
aaa5b7af41 | ||
|
5e6acb60a6 | ||
|
81376e7c34 | ||
|
296544089c | ||
|
ed28deda81 | ||
|
9551569ffe | ||
|
463d995d95 | ||
|
3beeb06340 | ||
|
24bcc65314 | ||
|
ea94ee7515 | ||
|
a79a51d445 | ||
|
30632050a5 | ||
|
0b7d55c30d | ||
|
9b9a74c1d1 | ||
|
678cf5d0bf | ||
|
78d9ba0d5e | ||
|
9f4980ce49 | ||
|
b47c87f793 | ||
|
f569c6de2d | ||
|
90e1ea4d22 | ||
|
88b85c9761 | ||
|
1c41fb27ed | ||
|
189877c173 | ||
|
a53b798f29 | ||
|
fc9ad7e37a | ||
|
76e182e616 | ||
|
c3db8de9e7 | ||
|
3e1fc1a922 | ||
|
32376dd6ee | ||
|
4c96193027 | ||
|
c5484ae6f9 | ||
|
34bd378c7e | ||
|
29c1132554 | ||
|
14ef8ef835 | ||
|
d64c352262 | ||
|
445f579f09 | ||
|
e30f2a99cf | ||
|
faf81ef18a | ||
|
7937b15855 | ||
|
b471938f16 | ||
|
b147b528e2 | ||
|
21e3445754 | ||
|
1657f0e9bd | ||
|
d18cb84ec4 | ||
|
65ef159ddc | ||
|
61368c59f2 | ||
|
a0657b7847 | ||
|
ffc4542cd0 | ||
|
8a5f337469 | ||
|
e07c86bd5e | ||
|
ce9bf33f20 | ||
|
d5866e194a | ||
|
416733b4db |
417 changed files with 62184 additions and 25949 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.4
|
||||
- 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.*
|
||||
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
|
||||
|
|
200
CHANGELOG.md
Normal file
200
CHANGELOG.md
Normal file
|
@ -0,0 +1,200 @@
|
|||
# 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)
|
||||
|
||||
## v0.10.0
|
||||
This release brings several breaking changes. Please refer to [UPGRADE](UPGRADE.md) document for details.
|
||||
|
||||
New features and notable changes:
|
||||
- Changed minimum PHP version from 5.4 to 5.5
|
||||
- Lazy loading of types without separate build step (see #69, see [docs](http://webonyx.github.io/graphql-php/type-system/schema/#lazy-loading-of-types))
|
||||
- PSR-7 compliant Standard Server (see [docs](http://webonyx.github.io/graphql-php/executing-queries/#using-server))
|
||||
- New default error formatting, which does not expose sensitive data (see [docs](http://webonyx.github.io/graphql-php/error-handling/))
|
||||
- Ability to define custom error handler to filter/log/re-throw exceptions after execution (see [docs](http://webonyx.github.io/graphql-php/error-handling/#custom-error-handling-and-formatting))
|
||||
- Allow defining schema configuration using objects with fluent setters vs array (see [docs](http://webonyx.github.io/graphql-php/type-system/schema/#using-config-class))
|
||||
- Allow serializing AST to array and re-creating AST from array lazily (see [docs](http://webonyx.github.io/graphql-php/reference/#graphqlutilsast))
|
||||
- [Apollo-style](https://dev-blog.apollodata.com/query-batching-in-apollo-63acfd859862) query batching support via server (see [docs](http://webonyx.github.io/graphql-php/executing-queries/#query-batching))
|
||||
- Schema validation, including validation of interface implementations (see [docs](http://webonyx.github.io/graphql-php/type-system/schema/#schema-validation))
|
||||
- Ability to pass custom config formatter when defining schema using [GraphQL type language](http://graphql.org/learn/schema/#type-language) (see [docs](http://webonyx.github.io/graphql-php/type-system/type-language/))
|
||||
|
||||
Improvements:
|
||||
- Significantly improved parser performance (see #137 and #128)
|
||||
- Support for PHP7 exceptions everywhere (see #127)
|
||||
- Improved [documentation](http://webonyx.github.io/graphql-php/) and docblock comments
|
||||
|
||||
Deprecations and breaking changes - see [UPGRADE](UPGRADE.md) document.
|
||||
|
||||
#### v0.9.14
|
||||
- Minor change to assist DataLoader project in fixing #150
|
||||
|
||||
#### v0.9.13
|
||||
- Fixed PHP notice and invalid conversion when non-scalar value is passed as ID or String type (see #121)
|
||||
|
||||
#### v0.9.12
|
||||
- Fixed bug occurring when enum `value` is bool, null or float (see #141)
|
||||
|
||||
#### v0.9.11
|
||||
- Ability to disable introspection (see #131)
|
||||
|
||||
#### v0.9.10
|
||||
- Fixed issue with query complexity throwing on invalid queries (see #125)
|
||||
- Fixed "Out of memory" error when `resolveType` returns unexpected result (see #119)
|
||||
|
||||
#### v0.9.9
|
||||
- Bugfix: throw UserError vs InvariantViolationError for errors caused by client (see #123)
|
||||
|
||||
#### v0.9.8
|
||||
- Bugfix: use directives when calculating query complexity (see #113)
|
||||
- Bugfix: `AST\Node::__toString()` will convert node to array recursively to encode to json without errors
|
||||
|
||||
#### v0.9.7
|
||||
- Bugfix: `ResolveInfo::getFieldSelection()` now correctly merges fragment selections (see #98)
|
||||
|
||||
#### v0.9.6
|
||||
- Bugfix: `ResolveInfo::getFieldSelection()` now respects inline fragments
|
||||
|
||||
#### v0.9.5
|
||||
- Fixed SyncPromiseAdapter::all() to not change the order of arrays (see #92)
|
||||
|
||||
#### v0.9.4
|
||||
- Tools to help building schema out of Schema definition language as well as printing existing
|
||||
schema in Schema definition language (see #91)
|
||||
|
||||
#### v0.9.3
|
||||
- Fixed Utils::assign() bug related to detecting missing required keys (see #89)
|
||||
|
||||
#### v0.9.2
|
||||
- Schema Definition Language: element descriptions can be set through comments (see #88)
|
||||
|
||||
#### v0.9.1
|
||||
- Fixed: `GraphQL\Server` now properly sets promise adapter before executing query
|
||||
|
||||
## v0.9.0
|
||||
- Deferred resolvers (see #66, see [docs](docs/data-fetching.md#solving-n1-problem))
|
||||
- New Facade class with fluid interface: `GraphQL\Server` (see #82)
|
||||
- Experimental: ability to load types in Schema lazily via custom `TypeResolutionStrategy` (see #69)
|
||||
|
||||
|
||||
## v0.8.0
|
||||
This release brings several minor breaking changes. Please refer to [UPGRADE](UPGRADE.md) document for details.
|
||||
|
||||
New features:
|
||||
- Support for `null` value (as required by latest GraphQL spec)
|
||||
- Shorthand definitions for field and argument types (see #47)
|
||||
- `path` entry in errors produced by resolvers for better debugging
|
||||
- `resolveType` for interface/union is now allowed to return string name of type
|
||||
- Ability to omit name when extending type class (vs defining inline)
|
||||
|
||||
Improvements:
|
||||
- Spec compliance improvements
|
||||
- New docs and examples
|
||||
|
||||
## Older versions
|
||||
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 implementors 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).
|
||||
|
|
375
UPGRADE.md
375
UPGRADE.md
|
@ -1,4 +1,377 @@
|
|||
# Upgrade
|
||||
## 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
|
||||
It allows us to leverage `::class` constant, `generators` and other features of newer PHP versions.
|
||||
|
||||
### Breaking: default error formatting
|
||||
By default exceptions thrown in resolvers will be reported with generic message `"Internal server error"`.
|
||||
Only exceptions implementing interface `GraphQL\Error\ClientAware` and claiming themselves as `safe` will
|
||||
be reported with full error message.
|
||||
|
||||
This breaking change is done to avoid information leak in production when unhandled
|
||||
exceptions were reported to clients (e.g. database connection errors, file access errors, etc).
|
||||
|
||||
Also every error reported to client now has new `category` key which is either `graphql` or `internal`.
|
||||
Exceptions implementing `ClientAware` interface may define their own custom categories.
|
||||
|
||||
During development or debugging use `$executionResult->toArray(true)`. It will add `debugMessage` key to
|
||||
each error entry in result. If you also want to add `trace` for each error - pass flags instead:
|
||||
|
||||
```
|
||||
use GraphQL\Error\FormattedError;
|
||||
$debug = FormattedError::INCLUDE_DEBUG_MESSAGE | FormattedError::INCLUDE_TRACE;
|
||||
$result = GraphQL::executeAndReturnResult(/*args*/)->toArray($debug);
|
||||
```
|
||||
|
||||
To change default `"Internal server error"` message to something else, use:
|
||||
```
|
||||
GraphQL\Error\FormattedError::setInternalErrorMessage("Unexpected error");
|
||||
```
|
||||
|
||||
**This change only affects default error reporting mechanism. If you set your own error formatter using
|
||||
`$executionResult->setErrorFormatter($myFormatter)` you won't be affected by this change.**
|
||||
|
||||
If you need to revert to old behavior temporary, use:
|
||||
|
||||
```php
|
||||
GraphQL::executeAndReturnResult(/**/)
|
||||
->setErrorFormatter('\GraphQL\Error\Error::formatError')
|
||||
->toArray();
|
||||
```
|
||||
But note that this is deprecated format and will be removed in future versions.
|
||||
|
||||
In general, if new default formatting doesn't work for you - just set [your own error
|
||||
formatter](http://webonyx.github.io/graphql-php/error-handling/#custom-error-handling-and-formatting).
|
||||
|
||||
### Breaking: Validation rules now have abstract base class
|
||||
Previously any callable was accepted by DocumentValidator as validation rule. Now only instances of
|
||||
`GraphQL\Validator\Rules\AbstractValidationRule` are allowed.
|
||||
|
||||
If you were using custom validation rules, just wrap them with
|
||||
`GraphQL\Validator\Rules\CustomValidationRule` (created for backwards compatibility).
|
||||
|
||||
Before:
|
||||
```php
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
|
||||
$myRule = function(ValidationContext $context) {};
|
||||
DocumentValidator::validate($schema, $ast, [$myRule]);
|
||||
```
|
||||
|
||||
After:
|
||||
```php
|
||||
use GraphQL\Validator\Rules\CustomValidationRule;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
|
||||
$myRule = new CustomValidationRule('MyRule', function(ValidationContext $context) {});
|
||||
DocumentValidator::validate($schema, $ast, [$myRule]);
|
||||
```
|
||||
|
||||
Also `DocumentValidator::addRule()` signature changed.
|
||||
|
||||
Before the change:
|
||||
```php
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
|
||||
$myRule = function(ValidationContext $context) {};
|
||||
DocumentValidator::addRule('MyRuleName', $myRule);
|
||||
```
|
||||
|
||||
After the change:
|
||||
```php
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
|
||||
$myRule = new CustomValidationRulefunction('MyRule', ValidationContext $context) {});
|
||||
DocumentValidator::addRule($myRule);
|
||||
```
|
||||
|
||||
|
||||
### Breaking: AST now uses `NodeList` vs array for lists of nodes
|
||||
It helps us unserialize AST from array lazily. This change affects you only if you use `array_`
|
||||
functions with AST or mutate AST directly.
|
||||
|
||||
Before the change:
|
||||
```php
|
||||
new GraphQL\Language\AST\DocumentNode([
|
||||
'definitions' => array(/*...*/)
|
||||
]);
|
||||
```
|
||||
After the change:
|
||||
```
|
||||
new GraphQL\Language\AST\DocumentNode([
|
||||
'definitions' => new NodeList([/*...*/])
|
||||
]);
|
||||
```
|
||||
|
||||
|
||||
### Breaking: scalar types now throw different exceptions when parsing and serializing
|
||||
On invalid client input (`parseValue` and `parseLiteral`) they throw standard `GraphQL\Error\Error`
|
||||
but when they encounter invalid output (in `serialize`) they throw `GraphQL\Error\InvariantViolation`.
|
||||
|
||||
Previously they were throwing `GraphQL\Error\UserError`. This exception is no longer used so make sure
|
||||
to adjust if you were checking for this error in your custom error formatters.
|
||||
|
||||
### Breaking: removed previously deprecated ability to define type as callable
|
||||
See https://github.com/webonyx/graphql-php/issues/35
|
||||
|
||||
### Deprecated: `GraphQL\GraphQL::executeAndReturnResult`
|
||||
Method is renamed to `GraphQL\GraphQL::executeQuery`. Old method name is still available,
|
||||
but will trigger deprecation warning in the next version.
|
||||
|
||||
### Deprecated: `GraphQL\GraphQL::execute`
|
||||
Use `GraphQL\GraphQL::executeQuery()->toArray()` instead.
|
||||
Old method still exists, but will trigger deprecation warning in next version.
|
||||
|
||||
### Deprecated: `GraphQL\Schema` moved to `GraphQL\Type\Schema`
|
||||
Old class still exists, but will trigger deprecation warning in next version.
|
||||
|
||||
### Deprecated: `GraphQL\Utils` moved to `GraphQL\Utils\Utils`
|
||||
Old class still exists, but triggers deprecation warning when referenced.
|
||||
|
||||
### Deprecated: `GraphQL\Type\Definition\Config`
|
||||
If you were using config validation in previous versions, replace:
|
||||
```php
|
||||
GraphQL\Type\Definition\Config::enableValidation();
|
||||
```
|
||||
with:
|
||||
```php
|
||||
$schema->assertValid();
|
||||
```
|
||||
See https://github.com/webonyx/graphql-php/issues/148
|
||||
|
||||
### Deprecated: experimental `GraphQL\Server`
|
||||
Use [new PSR-7 compliant implementation](docs/executing-queries.md#using-server) instead.
|
||||
|
||||
### Deprecated: experimental `GraphQL\Type\Resolution` interface and implementations
|
||||
Use schema [**typeLoader** option](docs/type-system/schema.md#lazy-loading-of-types) instead.
|
||||
|
||||
### Non-breaking: usage on async platforms
|
||||
When using the library on async platforms use separate method `GraphQL::promiseToExecute()`.
|
||||
It requires promise adapter in it's first argument and always returns a `Promise`.
|
||||
|
||||
Old methods `GraphQL::execute` and `GraphQL::executeAndReturnResult` still work in backwards-compatible manner,
|
||||
but they are deprecated and will be removed eventually.
|
||||
|
||||
Same applies to Executor: use `Executor::promiseToExecute()` vs `Executor::execute()`.
|
||||
|
||||
## Upgrade v0.7.x > v0.8.x
|
||||
All of those changes apply to those who extends various parts of this library.
|
||||
|
|
|
@ -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,23 +16,12 @@ use GraphQL\Type\LazyResolution;
|
|||
*/
|
||||
class HugeSchemaBench
|
||||
{
|
||||
/**
|
||||
* @var SchemaGenerator
|
||||
*/
|
||||
/** @var SchemaGenerator */
|
||||
private $schemaBuilder;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $descriptor;
|
||||
|
||||
private $schema;
|
||||
|
||||
private $lazySchema;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
private $smallQuery;
|
||||
|
||||
public function setUp()
|
||||
|
@ -41,20 +30,20 @@ class HugeSchemaBench
|
|||
'totalTypes' => 600,
|
||||
'fieldsPerType' => 8,
|
||||
'listFieldsPerType' => 2,
|
||||
'nestingLevel' => 10
|
||||
'nestingLevel' => 10,
|
||||
]);
|
||||
|
||||
$this->schema = $this->schemaBuilder->buildSchema();
|
||||
|
||||
$queryBuilder = new QueryGenerator($this->schema, 0.05);
|
||||
$this->descriptor = $this->schema->getDescriptor();
|
||||
$queryBuilder = new QueryGenerator($this->schema, 0.05);
|
||||
$this->smallQuery = $queryBuilder->buildQuery();
|
||||
}
|
||||
|
||||
public function benchSchema()
|
||||
{
|
||||
$this->schemaBuilder
|
||||
->buildSchema();
|
||||
->buildSchema()
|
||||
->getTypeMap();
|
||||
}
|
||||
|
||||
public function benchSchemaLazy()
|
||||
|
@ -64,27 +53,23 @@ 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()
|
||||
{
|
||||
$strategy = new LazyResolution(
|
||||
$this->descriptor,
|
||||
function($name) {
|
||||
return $this->schemaBuilder->loadType($name);
|
||||
}
|
||||
return new Schema(
|
||||
SchemaConfig::create()
|
||||
->setQuery($this->schemaBuilder->buildQueryType())
|
||||
->setTypeLoader(function ($name) {
|
||||
return $this->schemaBuilder->loadType($name);
|
||||
})
|
||||
);
|
||||
|
||||
return new Schema([
|
||||
'query' => $this->schemaBuilder->buildQueryType(),
|
||||
'typeResolution' => $strategy,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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\Utils;
|
||||
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,24 +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.4,<8.0-DEV",
|
||||
"php": "^7.1||^8.0",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8"
|
||||
"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/"
|
||||
}
|
||||
|
@ -32,6 +41,16 @@
|
|||
}
|
||||
},
|
||||
"suggest": {
|
||||
"react/promise": "To use ReactPHP promise adapter"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ but make sure to restrict it to debug/development mode only.
|
|||
**graphql-php** expects that each type in Schema is presented by single instance. Therefore
|
||||
if you define your types as separate PHP classes you need to ensure that each type is referenced only once.
|
||||
|
||||
Technically you can create several instances of your type (for example for tests), but `GraphQL\Schema`
|
||||
Technically you can create several instances of your type (for example for tests), but `GraphQL\Type\Schema`
|
||||
will throw on attempt to add different instances with the same name.
|
||||
|
||||
There are several ways to achieve this depending on your preferences. We provide reference
|
||||
|
|
|
@ -1,11 +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
|
||||
* [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
|
||||
|
|
|
@ -3,7 +3,8 @@ GraphQL is data-centric. On the very top level it is built around three major co
|
|||
**Schema**, **Query** and **Mutation**.
|
||||
|
||||
You are expected to express your application as **Schema** (aka Type System) and expose it
|
||||
with single [HTTP endpoint](http-endpoint/). Application clients (e.g. web or mobile clients) send **Queries**
|
||||
with single HTTP endpoint (e.g. using our [standard server](executing-queries.md#using-server)).
|
||||
Application clients (e.g. web or mobile clients) send **Queries**
|
||||
to this endpoint to request structured data and **Mutations** to perform changes (usually with HTTP POST method).
|
||||
|
||||
## Queries
|
||||
|
@ -34,9 +35,9 @@ It was designed to mirror the structure of expected response:
|
|||
}
|
||||
```
|
||||
**graphql-php** runtime parses Queries, makes sure that they are valid for given Type System
|
||||
and executes using [data fetching tools](type-system/object-types/#data-fetching) provided by you
|
||||
and executes using [data fetching tools](data-fetching.md) provided by you
|
||||
as a part of integration. Queries are supposed to be idempotent.
|
||||
|
||||
|
||||
## Mutations
|
||||
Mutations use advanced features of the very same query language (like arguments and variables)
|
||||
and have only semantic difference from Queries:
|
||||
|
@ -135,4 +136,4 @@ $blogPostType = new ObjectType([
|
|||
# Further Reading
|
||||
To get deeper understanding of GraphQL concepts - [read the docs on official GraphQL website](http://graphql.org/learn/)
|
||||
|
||||
To get started with **graphql-php** - continue to next section ["Getting Started"](getting-started/)
|
||||
To get started with **graphql-php** - continue to next section ["Getting Started"](getting-started.md)
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
GraphQL is data-storage agnostic. You can use any underlying data storage engine, including SQL or NoSQL database,
|
||||
plain files or in-memory data structures.
|
||||
|
||||
In order to convert GraphQL query to PHP array **graphql-php** traverses query fields (using depth-first algorithm) and
|
||||
runs special `resolve` function on each field. This `resolve` function is provided by you as a part of
|
||||
[field definition](type-system/object-types/#field-configuration-options).
|
||||
In order to convert the GraphQL query to PHP array, **graphql-php** traverses query fields (using depth-first algorithm) and
|
||||
runs special **resolve** function on each field. This **resolve** function is provided by you as a part of
|
||||
[field definition](type-system/object-types.md#field-configuration-options) or [query execution call](executing-queries.md#overview).
|
||||
|
||||
Result returned by `resolve` function is directly included in response (for scalars and enums)
|
||||
Result returned by **resolve** function is directly included in the response (for scalars and enums)
|
||||
or passed down to nested fields (for objects).
|
||||
|
||||
Let's walk through an example. Consider following GraphQL query:
|
||||
|
@ -22,9 +22,12 @@ Let's walk through an example. Consider following GraphQL query:
|
|||
}
|
||||
```
|
||||
|
||||
We need Schema that can fulfill it. On the very top level Schema contains Query type:
|
||||
We need a Schema that can fulfill it. On the very top level the Schema contains Query type:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
|
||||
$queryType = new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => [
|
||||
|
@ -44,14 +47,18 @@ $queryType = new ObjectType([
|
|||
]);
|
||||
```
|
||||
|
||||
As we see field `lastStory` has `resolve` function that is responsible for fetching data.
|
||||
As we see field **lastStory** has **resolve** function that is responsible for fetching data.
|
||||
|
||||
In our example we simply return array value, but in real-world application you would query
|
||||
your database/cache/search index and return result.
|
||||
In our example, we simply return array value, but in the real-world application you would query
|
||||
your database/cache/search index and return the result.
|
||||
|
||||
Since `lastStory` is of complex type `BlogStory` this result is passed down to fields of this type:
|
||||
Since **lastStory** is of composite type **BlogStory** this result is passed down to fields of this type:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
|
||||
$blogStoryType = new ObjectType([
|
||||
'name' => 'BlogStory',
|
||||
'fields' => [
|
||||
|
@ -81,51 +88,57 @@ $blogStoryType = new ObjectType([
|
|||
]);
|
||||
```
|
||||
|
||||
Here `$blogStory` is the array returned by `lastStory` field above.
|
||||
Here **$blogStory** is the array returned by **lastStory** field above.
|
||||
|
||||
Again: in real-world applications you would fetch user data from datastore by `authorId` and return it.
|
||||
Also note that you don't have to return arrays. You can return any value, **graphql-php** will pass it untouched
|
||||
Again: in the real-world applications you would fetch user data from data store by **authorId** and return it.
|
||||
Also, note that you don't have to return arrays. You can return any value, **graphql-php** will pass it untouched
|
||||
to nested resolvers.
|
||||
|
||||
But then the question appears - field `title` has no `resolve` option. How is it resolved?
|
||||
But then the question appears - field **title** has no **resolve** option. How is it resolved?
|
||||
|
||||
The answer is: there is default resolver for all fields. When you define your own `resolve` function
|
||||
There is a default resolver for all fields. When you define your own **resolve** function
|
||||
for a field you simply override this default resolver.
|
||||
|
||||
# Default Field Resolver
|
||||
**graphql-php** provides following default field resolver:
|
||||
```php
|
||||
function defaultFieldResolver($source, $args, $context, ResolveInfo $info)
|
||||
{
|
||||
$fieldName = $info->fieldName;
|
||||
$property = null;
|
||||
<?php
|
||||
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). If value is not set - it returns `null`.
|
||||
As you see it returns value by key (for arrays) or property (for objects).
|
||||
If the value is not set - it returns **null**.
|
||||
|
||||
To override default resolver - use:
|
||||
```php
|
||||
GraphQL\GraphQL::setDefaultFieldResolver($myResolverCallback);
|
||||
```
|
||||
To override the default resolver, pass it as an argument of [executeQuery](executing-queries.md) call.
|
||||
|
||||
# Default Field Resolver per Type
|
||||
Sometimes it might be convenient to set default field resolver per type. You can do so by providing
|
||||
[resolveField option in type config](type-system/object-types/#configuration-options). For example:
|
||||
[resolveField option in type config](type-system/object-types.md#configuration-options). For example:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
|
||||
$userType = new ObjectType([
|
||||
'name' => 'User',
|
||||
'fields' => [
|
||||
|
@ -150,12 +163,11 @@ $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
|
||||
|
||||
One of the most annoying problems with data fetching is so-called [N+1 problem](https://secure.phabricator.com/book/phabcontrib/article/n_plus_one/).
|
||||
|
||||
One of the most annoying problems with data fetching is a so-called
|
||||
[N+1 problem](https://secure.phabricator.com/book/phabcontrib/article/n_plus_one/). <br>
|
||||
Consider following GraphQL query:
|
||||
```
|
||||
{
|
||||
|
@ -169,13 +181,14 @@ Consider following GraphQL query:
|
|||
}
|
||||
```
|
||||
|
||||
Naive field resolution process would require up to 10 calls to underlying data store to fetch authors for all 10 stories.
|
||||
Naive field resolution process would require up to 10 calls to the underlying data store to fetch authors for all 10 stories.
|
||||
|
||||
**graphql-php** provides tools to mitigate this problem: it allows you to defer actual field resolution to later stage
|
||||
**graphql-php** provides tools to mitigate this problem: it allows you to defer actual field resolution to a later stage
|
||||
when one batched query could be executed instead of 10 distinct queries.
|
||||
|
||||
Here is an example of `BlogStory` resolver for field `author` that uses deferring:
|
||||
Here is an example of **BlogStory** resolver for field **author** that uses deferring:
|
||||
```php
|
||||
<?php
|
||||
'resolve' => function($blogStory) {
|
||||
MyUserBuffer::add($blogStory['authorId']);
|
||||
|
||||
|
@ -186,18 +199,16 @@ Here is an example of `BlogStory` resolver for field `author` that uses deferrin
|
|||
}
|
||||
```
|
||||
|
||||
In this example we fill up buffer with 10 author ids first. Then **graphql-php** continues
|
||||
In this example, we fill up the buffer with 10 author ids first. Then **graphql-php** continues
|
||||
resolving other non-deferred fields until there are none of them left.
|
||||
|
||||
After that it calls `Closures` wrapped by `GraphQL\Deferred` which in turn load all buffered
|
||||
ids once (using SQL IN(?), Redis MGET or other similar tools) and return final field value.
|
||||
After that, it calls closures wrapped by `GraphQL\Deferred` which in turn load all buffered
|
||||
ids once (using SQL IN(?), Redis MGET or other similar tools) and returns final field value.
|
||||
|
||||
Originally this approach was advocated by Facebook in their [Dataloader](https://github.com/facebook/dataloader)
|
||||
project.
|
||||
project. This solution enables very interesting optimizations at no cost. Consider the following query:
|
||||
|
||||
This solution enables very interesting optimizations at no cost. Consider following query:
|
||||
|
||||
```
|
||||
```graphql
|
||||
{
|
||||
topStories(limit: 10) {
|
||||
author {
|
||||
|
@ -214,33 +225,50 @@ This solution enables very interesting optimizations at no cost. Consider follow
|
|||
}
|
||||
```
|
||||
|
||||
Even if `author` field is located on different levels of query - it can be buffered in the same buffer.
|
||||
In this example only one query will be executed for all story authors comparing to 20 queries
|
||||
in naive implementation.
|
||||
Even though **author** field is located on different levels of the query - it can be buffered in the same buffer.
|
||||
In this example, only one query will be executed for all story authors comparing to 20 queries
|
||||
in a naive implementation.
|
||||
|
||||
# Async PHP
|
||||
Since: 0.9.0
|
||||
Since: 0.10.0 (version 0.9.0 had slightly different API which still works, but is deprecated)
|
||||
|
||||
If your project runs in environment that supports async operations
|
||||
(like `HHVM`, `ReactPHP`, `Icicle.io`, `appserver.io` `PHP threads`, etc) you can leverage
|
||||
the power of your platform to resolve fields asynchronously.
|
||||
If your project runs in an environment that supports async operations
|
||||
(like HHVM, ReactPHP, Icicle.io, appserver.io, PHP threads, etc)
|
||||
you can leverage the power of your platform to resolve some fields asynchronously.
|
||||
|
||||
The only requirement: your platform must support the concept of Promises compatible with
|
||||
[Promises A+](https://promisesaplus.com/) specification.
|
||||
|
||||
To enable async support - set adapter for promises:
|
||||
```
|
||||
GraphQL\GraphQL::setPromiseAdapter($adapter);
|
||||
To start using this feature, switch facade method for query execution from
|
||||
**executeQuery** to **promiseToExecute**:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Executor\ExecutionResult;
|
||||
|
||||
$promise = GraphQL::promiseToExecute(
|
||||
$promiseAdapter,
|
||||
$schema,
|
||||
$queryString,
|
||||
$rootValue = null,
|
||||
$contextValue = null,
|
||||
$variableValues = null,
|
||||
$operationName = null,
|
||||
$fieldResolver = null,
|
||||
$validationRules = null
|
||||
);
|
||||
$promise->then(function(ExecutionResult $result) {
|
||||
return $result->toArray();
|
||||
});
|
||||
```
|
||||
|
||||
Where `$adapter` is an instance of class implementing `GraphQL\Executor\Promise\PromiseAdapter` interface.
|
||||
Where **$promiseAdapter** is an instance of:
|
||||
|
||||
Then in your `resolve` functions you should return `Promises` of your platform instead of
|
||||
`GraphQL\Deferred` instances.
|
||||
* For [ReactPHP](https://github.com/reactphp/react) (requires **react/promise** as composer dependency): <br>
|
||||
`GraphQL\Executor\Promise\Adapter\ReactPromiseAdapter`
|
||||
|
||||
Platforms supported out of the box:
|
||||
* Other platforms: write your own class implementing interface: <br>
|
||||
[`GraphQL\Executor\Promise\PromiseAdapter`](reference.md#graphqlexecutorpromisepromiseadapter).
|
||||
|
||||
* [ReactPHP](https://github.com/reactphp/react) (requires **react/promise** as composer dependency):
|
||||
`GraphQL\GraphQL::setPromiseAdapter(new GraphQL\Executor\Promise\Adapter\ReactPromiseAdapter());`
|
||||
|
||||
To integrate other platform - implement `GraphQL\Executor\Promise\PromiseAdapter` interface.
|
||||
Then your **resolve** functions should return promises of your platform instead of `GraphQL\Deferred`s.
|
||||
|
|
|
@ -1,137 +1,190 @@
|
|||
# Errors in GraphQL
|
||||
|
||||
Query execution process never throws exceptions. Instead all errors that occur during query execution
|
||||
are caught, collected and included in response.
|
||||
Query execution process never throws exceptions. Instead, all errors are caught and collected.
|
||||
After execution, they are available in **$errors** prop of
|
||||
[`GraphQL\Executor\ExecutionResult`](reference.md#graphqlexecutorexecutionresult).
|
||||
|
||||
There are 3 types of errors in GraphQL (Syntax, Validation and Execution errors):
|
||||
When the result is converted to a serializable array using its **toArray()** method, all errors are
|
||||
converted to arrays as well using default error formatting (see below).
|
||||
|
||||
Alternatively, you can apply [custom error filtering and formatting](#custom-error-handling-and-formatting)
|
||||
for your specific requirements.
|
||||
|
||||
# Default Error formatting
|
||||
By default, each error entry is converted to an associative array with following structure:
|
||||
|
||||
**Syntax** errors are returned in response when query has invalid syntax and could not be parsed.
|
||||
Example output for invalid query `{hello` (missing bracket):
|
||||
```php
|
||||
<?php
|
||||
[
|
||||
'errors' => [
|
||||
[
|
||||
'message' => "Syntax Error GraphQL request (1:7) Expected Name, found <EOF>\n\n1: {hello\n ^\n",
|
||||
'locations' => [
|
||||
['line' => 1, 'column' => 7]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
**Validation** errors - returned in response when query has semantic errors.
|
||||
Example output for invalid query `{unknownField}`:
|
||||
```php
|
||||
[
|
||||
'errors' => [
|
||||
[
|
||||
'message' => 'Cannot query field "unknownField" on type "Query".',
|
||||
'locations' => [
|
||||
['line' => 1, 'column' => 2]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
**Execution** errors - included in response when some field resolver throws
|
||||
(or returns unexpected value). Example output for query with exception thrown in
|
||||
field resolver `{fieldWithException}`:
|
||||
```php
|
||||
[
|
||||
'data' => [
|
||||
'fieldWithException' => null
|
||||
'message' => 'Error message',
|
||||
'category' => 'graphql',
|
||||
'locations' => [
|
||||
['line' => 1, 'column' => 2]
|
||||
],
|
||||
'errors' => [
|
||||
[
|
||||
'message' => 'Exception message thrown in field resolver',
|
||||
'locations' => [
|
||||
['line' => 1, 'column' => 2]
|
||||
],
|
||||
'path': [
|
||||
'fieldWithException'
|
||||
]
|
||||
]
|
||||
'path' => [
|
||||
'listField',
|
||||
0,
|
||||
'fieldWithException'
|
||||
]
|
||||
]
|
||||
];
|
||||
```
|
||||
Entry at key **locations** points to a character in query string which caused the error.
|
||||
In some cases (like deep fragment fields) locations will include several entries to track down
|
||||
the path to field with the error in query.
|
||||
|
||||
Entry at key **path** exists only for errors caused by exceptions thrown in resolvers.
|
||||
It contains a path from the very root field to actual field value producing an error
|
||||
(including indexes for list types and field names for composite types).
|
||||
|
||||
**Internal errors**
|
||||
|
||||
As of version **0.10.0**, all exceptions thrown in resolvers are reported with generic message **"Internal server error"**.
|
||||
This is done to avoid information leak in production environments (e.g. database connection errors, file access errors, etc).
|
||||
|
||||
Only exceptions implementing interface [`GraphQL\Error\ClientAware`](reference.md#graphqlerrorclientaware) and claiming themselves as **safe** will
|
||||
be reported with a full error message.
|
||||
|
||||
For example:
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Error\ClientAware;
|
||||
|
||||
class MySafeException extends \Exception implements ClientAware
|
||||
{
|
||||
public function isClientSafe()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getCategory()
|
||||
{
|
||||
return 'businessLogic';
|
||||
}
|
||||
}
|
||||
```
|
||||
When such exception is thrown it will be reported with a full error message:
|
||||
```php
|
||||
<?php
|
||||
[
|
||||
'message' => 'My reported error',
|
||||
'category' => 'businessLogic',
|
||||
'locations' => [
|
||||
['line' => 10, 'column' => 2]
|
||||
],
|
||||
'path' => [
|
||||
'path',
|
||||
'to',
|
||||
'fieldWithException'
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
Obviously when **Syntax** or **Validation** error is detected - process is interrupted and query is not
|
||||
executed. In such scenarios response only contains **errors**, but not **data**.
|
||||
|
||||
GraphQL is forgiving to **Execution** errors which occur in resolvers of nullable fields.
|
||||
If such field throws or returns unexpected value the value of the field in response will be simply
|
||||
replaced with `null` and error entry will be added to response.
|
||||
|
||||
If exception is thrown in non-null field - error bubbles up to first nullable field. This nullable field is
|
||||
replaced with `null` and error entry is added to response. If all fields up to the root are non-null -
|
||||
**data** entry will be removed from response and only **errors** key will be presented.
|
||||
To change default **"Internal server error"** message to something else, use:
|
||||
```
|
||||
GraphQL\Error\FormattedError::setInternalErrorMessage("Unexpected error");
|
||||
```
|
||||
|
||||
# Debugging tools
|
||||
|
||||
Each error entry contains pointer to line and column in original query string which caused
|
||||
the error:
|
||||
|
||||
```php
|
||||
'locations' => [
|
||||
['line' => 1, 'column' => 2]
|
||||
]
|
||||
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:
|
||||
|
||||
```
|
||||
|
||||
GraphQL clients like **Relay** or **GraphiQL** leverage this information to highlight
|
||||
actual piece of query containing error.
|
||||
|
||||
In some cases (like deep fragment fields) locations will include several entries to track down the
|
||||
path to field with error in query.
|
||||
|
||||
**Execution** errors also contain **path** from the very root field to actual field value producing
|
||||
an error (including indexes for array types and fieldNames for object types). So in complex situation
|
||||
this path could look like this:
|
||||
|
||||
```php
|
||||
'path' => [
|
||||
'lastStoryPosted',
|
||||
'author',
|
||||
'friends',
|
||||
3
|
||||
'fieldWithException'
|
||||
]
|
||||
use GraphQL\Error\Debug;
|
||||
$debug = Debug::INCLUDE_DEBUG_MESSAGE | Debug::INCLUDE_TRACE;
|
||||
$result = GraphQL::executeQuery(/*args*/)->toArray($debug);
|
||||
```
|
||||
|
||||
# Custom Error Formatting
|
||||
This will make each error entry to look like this:
|
||||
```php
|
||||
<?php
|
||||
[
|
||||
'debugMessage' => 'Actual exception message',
|
||||
'message' => 'Internal server error',
|
||||
'category' => 'internal',
|
||||
'locations' => [
|
||||
['line' => 10, 'column' => 2]
|
||||
],
|
||||
'path' => [
|
||||
'listField',
|
||||
0,
|
||||
'fieldWithException'
|
||||
],
|
||||
'trace' => [
|
||||
/* Formatted original exception trace */
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
If you want to apply custom formatting to errors - use **GraphQL::executeAndReturnResult()** instead
|
||||
of **GraphQL::execute()**.
|
||||
If you prefer the first resolver exception to be re-thrown, use following flags:
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Error\Debug;
|
||||
$debug = Debug::INCLUDE_DEBUG_MESSAGE | Debug::RETHROW_INTERNAL_EXCEPTIONS;
|
||||
|
||||
It has exactly the same [signature](executing-queries/), but instead of array it
|
||||
returns `GraphQL\Executor\ExecutionResult` instance which holds errors in public **$errors**
|
||||
property and data in **$data** property.
|
||||
// Following will throw if there was an exception in resolver during execution:
|
||||
$result = GraphQL::executeQuery(/*args*/)->toArray($debug);
|
||||
```
|
||||
|
||||
Each entry of **$errors** array contains instance of `GraphQL\Error\Error` which wraps original
|
||||
exceptions thrown by resolvers. To access original exceptions use `$error->getPrevious()` method.
|
||||
But note that previous exception is only available for **Execution** errors and will be `null`
|
||||
for **Syntax** or **Validation** errors.
|
||||
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.
|
||||
|
||||
**Formatter** is responsible for converting instances of [`GraphQL\Error\Error`](reference.md#graphqlerrorerror)
|
||||
to an array. **Handler** is useful for error filtering and logging.
|
||||
|
||||
For example, these are default formatter and handler:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\FormattedError;
|
||||
|
||||
$myErrorFormatter = function(Error $error) {
|
||||
return FormattedError::createFromException($error);
|
||||
};
|
||||
|
||||
$myErrorHandler = function(array $errors, callable $formatter) {
|
||||
return array_map($formatter, $errors);
|
||||
};
|
||||
|
||||
$result = GraphQL::executeQuery(/* $args */)
|
||||
->setErrorFormatter($myErrorFormatter)
|
||||
->setErrorsHandler($myErrorHandler)
|
||||
->toArray();
|
||||
```
|
||||
|
||||
Note that when you pass [debug flags](#debugging-tools) to **toArray()** your custom formatter will still be
|
||||
decorated with same debugging information mentioned above.
|
||||
|
||||
# Schema Errors
|
||||
So far we only covered errors which occur during query execution process. But schema definition can
|
||||
also throw if there is an error in one of type definitions.
|
||||
also throw `GraphQL\Error\InvariantViolation` if there is an error in one of type definitions.
|
||||
|
||||
Usually such errors mean that there is some logical error in your schema and it is the only case
|
||||
when it makes sense to return `500` error code for GraphQL endpoint:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Error\FormattedError;
|
||||
|
||||
try {
|
||||
$schema = new Schema([
|
||||
// ...
|
||||
]);
|
||||
|
||||
$body = GraphQL::execute($schema, $query);
|
||||
$body = GraphQL::executeQuery($schema, $query);
|
||||
$status = 200;
|
||||
} catch(\Exception $e) {
|
||||
$body = json_encode([
|
||||
'message' => 'Unexpected error'
|
||||
]);
|
||||
$body = [
|
||||
'errors' => [FormattedError::createFromException($e)]
|
||||
];
|
||||
$status = 500;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,48 +1,208 @@
|
|||
# Overview
|
||||
# Using Facade Method
|
||||
Query execution is a complex process involving multiple steps, including query **parsing**,
|
||||
**validating** and finally **executing** against your [schema](type-system/schema/).
|
||||
**validating** and finally **executing** against your [schema](type-system/schema.md).
|
||||
|
||||
**graphql-php** provides convenient facade for this process in class `GraphQL\GraphQL`:
|
||||
**graphql-php** provides a convenient facade for this process in class
|
||||
[`GraphQL\GraphQL`](reference.md#graphqlgraphql):
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\GraphQL;
|
||||
|
||||
$result = GraphQL::execute(
|
||||
$result = GraphQL::executeQuery(
|
||||
$schema,
|
||||
$queryString,
|
||||
$rootValue = null,
|
||||
$contextValue = null,
|
||||
$context = null,
|
||||
$variableValues = null,
|
||||
$operationName = null
|
||||
$operationName = null,
|
||||
$fieldResolver = null,
|
||||
$validationRules = null
|
||||
);
|
||||
```
|
||||
|
||||
Method returns `array` with **data** and **errors** keys, as described by
|
||||
[GraphQL specs](http://facebook.github.io/graphql/#sec-Response-Format).
|
||||
This array is suitable for further serialization (e.g. using `json_encode`).
|
||||
See also section on [error handling](error-handling/).
|
||||
It returns an instance of [`GraphQL\Executor\ExecutionResult`](reference.md#graphqlexecutorexecutionresult)
|
||||
which can be easily converted to array:
|
||||
|
||||
```php
|
||||
$serializableResult = $result->toArray();
|
||||
```
|
||||
|
||||
Description of method arguments:
|
||||
Returned array contains **data** and **errors** keys, as described by the
|
||||
[GraphQL spec](http://facebook.github.io/graphql/#sec-Response-Format).
|
||||
This array is suitable for further serialization (e.g. using **json_encode**).
|
||||
See also the section on [error handling and formatting](error-handling.md).
|
||||
|
||||
Description of **executeQuery** method arguments:
|
||||
|
||||
Argument | Type | Notes
|
||||
------------ | -------- | -----
|
||||
schema | `GraphQL\Schema` | **Required.** Instance of your application [Schema](type-system/schema/)
|
||||
queryString | `string` or `GraphQL\Language\AST\DocumentNode` | **Required.** Actual GraphQL query string to be parsed, validated and executed. If you parse query elsewhere before executing - pass corresponding ast document here to avoid new parsing.
|
||||
rootValue | `mixed` | Any value that represents a root of your data graph. It is passed as 1st argument to field resolvers of [Query type](type-system/schema/#query-and-mutation-types). Can be omitted or set to null if actual root values are fetched by Query type itself.
|
||||
contextValue | `mixed` | Any value that holds information shared between all field resolvers. Most often they use it to pass currently logged in user, locale details, etc.<br><br>It will be available as 3rd argument in all field resolvers. (see section on [Field Definitions](type-system/object-types/#field-configuration-options) for reference) **graphql-php** never modifies this value and passes it *as is* to all underlying resolvers.
|
||||
schema | [`GraphQL\Type\Schema`](#) | **Required.** Instance of your application [Schema](type-system/schema.md)
|
||||
queryString | `string` or `GraphQL\Language\AST\DocumentNode` | **Required.** Actual GraphQL query string to be parsed, validated and executed. If you parse query elsewhere before executing - pass corresponding AST document here to avoid new parsing.
|
||||
rootValue | `mixed` | Any value that represents a root of your data graph. It is passed as the 1st argument to field resolvers of [Query type](type-system/schema.md#query-and-mutation-types). Can be omitted or set to null if actual root values are fetched by Query type itself.
|
||||
context | `mixed` | Any value that holds information shared between all field resolvers. Most often they use it to pass currently logged in user, locale details, etc.<br><br>It will be available as the 3rd argument in all field resolvers. (see section on [Field Definitions](type-system/object-types.md#field-configuration-options) for reference) **graphql-php** never modifies this value and passes it *as is* to all underlying resolvers.
|
||||
variableValues | `array` | Map of variable values passed along with query string. See section on [query variables on official GraphQL website](http://graphql.org/learn/queries/#variables)
|
||||
operationName | `string` | Allows the caller to specify which operation in queryString will be run, in cases where queryString contains multiple top-level operations.
|
||||
fieldResolver | `callable` | A resolver function to use when one is not provided by the schema. If not provided, the [default field resolver is used](data-fetching.md#default-field-resolver).
|
||||
validationRules | `array` | A set of rules for query validation step. The default value is all available rules. Empty array would allow skipping query validation (may be convenient for persisted queries which are validated before persisting and assumed valid during execution)
|
||||
|
||||
# Parsing
|
||||
Following reading describes implementation details of query execution process. It may clarify some
|
||||
internals of GraphQL but is not required in order to use it. Feel free to skip to next section
|
||||
on [Error Handling](error-handling/) for essentials.
|
||||
# Using Server
|
||||
If you are building HTTP GraphQL API, you may prefer our Standard Server
|
||||
(compatible with [express-graphql](https://github.com/graphql/express-graphql)).
|
||||
It supports more features out of the box, including parsing HTTP requests, producing a spec-compliant response; [batched queries](#query-batching); persisted queries.
|
||||
|
||||
TODOC
|
||||
Usage example (with plain PHP):
|
||||
|
||||
# Validating
|
||||
TODOC
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Server\StandardServer;
|
||||
|
||||
# Executing
|
||||
TODOC
|
||||
$server = new StandardServer([/* server options, see below */]);
|
||||
$server->handleRequest(); // parses PHP globals and emits response
|
||||
```
|
||||
|
||||
Server also supports [PSR-7 request/response interfaces](http://www.php-fig.org/psr/psr-7/):
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Server\StandardServer;
|
||||
use GraphQL\Executor\ExecutionResult;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
|
||||
/** @var ServerRequestInterface $psrRequest */
|
||||
/** @var ResponseInterface $psrResponse */
|
||||
/** @var StreamInterface $psrBodyStream */
|
||||
$server = new StandardServer([/* server options, see below */]);
|
||||
$psrResponse = $server->processPsrRequest($psrRequest, $psrResponse, $psrBodyStream);
|
||||
|
||||
|
||||
// Alternatively create PSR-7 response yourself:
|
||||
|
||||
/** @var ExecutionResult|ExecutionResult[] $result */
|
||||
$result = $server->executePsrRequest($psrRequest);
|
||||
$psrResponse = new SomePsr7ResponseImplementation(json_encode($result));
|
||||
```
|
||||
|
||||
PSR-7 is useful when you want to integrate the server into existing framework:
|
||||
|
||||
- [PSR-7 for Laravel](https://laravel.com/docs/5.1/requests#psr7-requests)
|
||||
- [Symfony PSR-7 Bridge](https://symfony.com/doc/current/components/psr7.html)
|
||||
- [Slim](https://www.slimframework.com/docs/concepts/value-objects.html)
|
||||
- [Zend Expressive](http://zendframework.github.io/zend-expressive/)
|
||||
|
||||
## Server configuration options
|
||||
|
||||
Argument | Type | Notes
|
||||
------------ | -------- | -----
|
||||
schema | [`Schema`](reference.md#graphqltypeschema) | **Required.** Instance of your application [Schema](type-system/schema/)
|
||||
rootValue | `mixed` | Any value that represents a root of your data graph. It is passed as the 1st argument to field resolvers of [Query type](type-system/schema.md#query-and-mutation-types). Can be omitted or set to null if actual root values are fetched by Query type itself.
|
||||
context | `mixed` | Any value that holds information shared between all field resolvers. Most often they use it to pass currently logged in user, locale details, etc.<br><br>It will be available as the 3rd argument in all field resolvers. (see section on [Field Definitions](type-system/object-types.md#field-configuration-options) for reference) **graphql-php** never modifies this value and passes it *as is* to all underlying resolvers.
|
||||
fieldResolver | `callable` | A resolver function to use when one is not provided by the schema. If not provided, the [default field resolver is used](data-fetching.md#default-field-resolver).
|
||||
validationRules | `array` or `callable` | A set of rules for query validation step. The default value is all available rules. The empty array would allow skipping query validation (may be convenient for persisted queries which are validated before persisting and assumed valid during execution).<br><br>Pass `callable` to return different validation rules for different queries (e.g. empty array for persisted query and a full list of rules for regular queries). When passed, it is expected to have the following signature: <br><br> **function ([OperationParams](reference.md#graphqlserveroperationparams) $params, DocumentNode $node, $operationType): array**
|
||||
queryBatching | `bool` | Flag indicating whether this server supports query batching ([apollo-style](https://dev-blog.apollodata.com/query-batching-in-apollo-63acfd859862)).<br><br> Defaults to **false**
|
||||
debug | `int` | Debug flags. See [docs on error debugging](error-handling.md#debugging-tools) (flag values are the same).
|
||||
persistentQueryLoader | `callable` | A function which is called to fetch actual query when server encounters **queryId** in request vs **query**.<br><br> The server does not implement persistence part (which you will have to build on your own), but it allows you to execute queries which were persisted previously.<br><br> Expected function signature:<br> **function ($queryId, [OperationParams](reference.md#graphqlserveroperationparams) $params)** <br><br>Function is expected to return query **string** or parsed **DocumentNode** <br><br> [Read more about persisted queries](https://dev-blog.apollodata.com/persisted-graphql-queries-with-apollo-client-119fd7e6bba5).
|
||||
errorFormatter | `callable` | Custom error formatter. See [error handling docs](error-handling.md#custom-error-handling-and-formatting).
|
||||
errorsHandler | `callable` | Custom errors handler. See [error handling docs](error-handling.md#custom-error-handling-and-formatting).
|
||||
promiseAdapter | [`PromiseAdapter`](reference.md#graphqlexecutorpromisepromiseadapter) | Required for [Async PHP](data-fetching/#async-php) only.
|
||||
|
||||
**Server config instance**
|
||||
|
||||
If you prefer fluid interface for config with autocomplete in IDE and static time validation,
|
||||
use [`GraphQL\Server\ServerConfig`](reference.md#graphqlserverserverconfig) instead of an array:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Server\ServerConfig;
|
||||
use GraphQL\Server\StandardServer;
|
||||
|
||||
$config = ServerConfig::create()
|
||||
->setSchema($schema)
|
||||
->setErrorFormatter($myFormatter)
|
||||
->setDebug($debug)
|
||||
;
|
||||
|
||||
$server = new StandardServer($config);
|
||||
```
|
||||
|
||||
## Query batching
|
||||
Standard Server supports query batching ([apollo-style](https://dev-blog.apollodata.com/query-batching-in-apollo-63acfd859862)).
|
||||
|
||||
One of the major benefits of Server over a sequence of **executeQuery()** calls is that
|
||||
[Deferred resolvers](data-fetching.md#solving-n1-problem) won't be isolated in queries.
|
||||
So for example following batch will require single DB request (if user field is deferred):
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"query": "{user(id: 1) { id }}"
|
||||
},
|
||||
{
|
||||
"query": "{user(id: 2) { id }}"
|
||||
},
|
||||
{
|
||||
"query": "{user(id: 3) { id }}"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
To enable query batching, pass **queryBatching** option in server config:
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Server\StandardServer;
|
||||
|
||||
$server = new StandardServer([
|
||||
'queryBatching' => true
|
||||
]);
|
||||
```
|
||||
|
||||
# Custom Validation Rules
|
||||
Before execution, a query is validated using a set of standard rules defined by the GraphQL spec.
|
||||
It is possible to override standard set of rules globally or per execution.
|
||||
|
||||
Add rules globally:
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Validator\Rules;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
|
||||
// Add to standard set of rules globally:
|
||||
DocumentValidator::addRule(new Rules\DisableIntrospection());
|
||||
```
|
||||
|
||||
Custom rules per execution:
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Validator\Rules;
|
||||
|
||||
$myValiationRules = array_merge(
|
||||
GraphQL::getStandardValidationRules(),
|
||||
[
|
||||
new Rules\QueryComplexity(100),
|
||||
new Rules\DisableIntrospection()
|
||||
]
|
||||
);
|
||||
|
||||
$result = GraphQL::executeQuery(
|
||||
$schema,
|
||||
$queryString,
|
||||
$rootValue = null,
|
||||
$context = null,
|
||||
$variableValues = null,
|
||||
$operationName = null,
|
||||
$fieldResolver = null,
|
||||
$myValiationRules // <-- this will override global validation rules for this request
|
||||
);
|
||||
```
|
||||
|
||||
Or with a standard server:
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Server\StandardServer;
|
||||
|
||||
$server = new StandardServer([
|
||||
'validationRules' => $myValiationRules
|
||||
]);
|
||||
```
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# Prerequisites
|
||||
This documentation assumes your familiarity with GraphQL concepts. If it is not the case -
|
||||
first learn about GraphQL on [official website](http://graphql.org/learn/).
|
||||
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
|
||||
|
@ -17,7 +17,7 @@ they are explained in [upgrade instructions](https://github.com/webonyx/graphql-
|
|||
# Install Tools (optional)
|
||||
While it is possible to communicate with GraphQL API using regular HTTP tools it is way
|
||||
more convenient for humans to use [GraphiQL](https://github.com/graphql/graphiql) - an in-browser
|
||||
ide for exploring GraphQL APIs.
|
||||
IDE for exploring GraphQL APIs.
|
||||
|
||||
It provides syntax-highlighting, auto-completion and auto-generated documentation for
|
||||
GraphQL API.
|
||||
|
@ -27,12 +27,12 @@ The easiest way to use it is to install one of the existing Google Chrome extens
|
|||
- [ChromeiQL](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij)
|
||||
- [GraphiQL Feen](https://chrome.google.com/webstore/detail/graphiql-feen/mcbfdonlkfpbfdpimkjilhdneikhfklp)
|
||||
|
||||
Alternatively you can follow instructions on [GraphiQL](https://github.com/graphql/graphiql)
|
||||
Alternatively, you can follow instructions on [the GraphiQL](https://github.com/graphql/graphiql)
|
||||
page and install it locally.
|
||||
|
||||
|
||||
# Hello World
|
||||
Let's create type system that will be capable to process following simple query:
|
||||
Let's create a type system that will be capable to process following simple query:
|
||||
```
|
||||
query {
|
||||
echo(message: "Hello World")
|
||||
|
@ -54,28 +54,29 @@ $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'];
|
||||
}
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
```
|
||||
|
||||
(Note: type definition can be expressed in [different styles](type-system/#type-definition-styles),
|
||||
(Note: type definition can be expressed in [different styles](type-system/index.md#type-definition-styles),
|
||||
but this example uses **inline** style for simplicity)
|
||||
|
||||
The interesting piece here is `resolve` option of field definition. It is responsible for retuning
|
||||
value of our field. Values of **scalar** fields will be directly included in response while values of
|
||||
**complex** fields (objects, interfaces, unions) will be passed down to nested field resolvers
|
||||
The interesting piece here is **resolve** option of field definition. It is responsible for returning
|
||||
a value of our field. Values of **scalar** fields will be directly included in response while values of
|
||||
**composite** fields (objects, interfaces, unions) will be passed down to nested field resolvers
|
||||
(not in this example though).
|
||||
|
||||
Now when our type is ready, let's create GraphQL endpoint for it `graphql.php`:
|
||||
Now when our type is ready, let's create GraphQL endpoint file for it **graphql.php**:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Schema;
|
||||
|
||||
$schema = new Schema([
|
||||
'query' => $queryType
|
||||
|
@ -88,32 +89,36 @@ $variableValues = isset($input['variables']) ? $input['variables'] : null;
|
|||
|
||||
try {
|
||||
$rootValue = ['prefix' => 'You said: '];
|
||||
$result = GraphQL::execute($schema, $query, $rootValue, null, $variableValues);
|
||||
$result = GraphQL::executeQuery($schema, $query, $rootValue, null, $variableValues);
|
||||
$output = $result->toArray();
|
||||
} catch (\Exception $e) {
|
||||
$result = [
|
||||
'error' => [
|
||||
'message' => $e->getMessage()
|
||||
$output = [
|
||||
'errors' => [
|
||||
[
|
||||
'message' => $e->getMessage()
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
echo json_encode($result);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($output);
|
||||
```
|
||||
|
||||
Our example is ready. Try it by running:
|
||||
Our example is finished. Try it by running:
|
||||
```sh
|
||||
php -S localhost:8000 graphql.php
|
||||
curl http://localhost:8000 -d '{"query": "query { echo(message: \"Hello World\") }" }'
|
||||
php -S localhost:8080 graphql.php
|
||||
curl http://localhost:8080 -d '{"query": "query { echo(message: \"Hello World\") }" }'
|
||||
```
|
||||
|
||||
Check out the full [source code](https://github.com/webonyx/graphql-php/blob/master/examples/00-hello-world) of this example.
|
||||
Check out the full [source code](https://github.com/webonyx/graphql-php/blob/master/examples/00-hello-world) of this example
|
||||
which also includes simple mutation.
|
||||
|
||||
Obviously hello world only scratches the surface of what is possible.
|
||||
So check out next example, which is closer to real-world apps.
|
||||
Or keep reading about [schema definition](type-system/).
|
||||
Or keep reading about [schema definition](type-system/index.md).
|
||||
|
||||
# Blog example
|
||||
It is often easier to start with full-featured example and then get back to documentation
|
||||
It is often easier to start with a full-featured example and then get back to documentation
|
||||
for your own work.
|
||||
|
||||
Check out [Blog example of GraphQL API](https://github.com/webonyx/graphql-php/tree/master/examples/01-blog).
|
||||
|
|
35
docs/how-it-works.md
Normal file
35
docs/how-it-works.md
Normal file
|
@ -0,0 +1,35 @@
|
|||
# Overview
|
||||
Following reading describes implementation details of query execution process. It may clarify some
|
||||
internals of GraphQL runtime but is not required to use it.
|
||||
|
||||
# Parsing
|
||||
|
||||
TODOC
|
||||
|
||||
# Validating
|
||||
TODOC
|
||||
|
||||
# Executing
|
||||
TODOC
|
||||
|
||||
# Errors explained
|
||||
There are 3 types of errors in GraphQL:
|
||||
|
||||
- **Syntax**: query has invalid syntax and could not be parsed;
|
||||
- **Validation**: query is incompatible with type system (e.g. unknown field is requested);
|
||||
- **Execution**: occurs when some field resolver throws (or returns unexpected value).
|
||||
|
||||
Obviously, when **Syntax** or **Validation** error is detected - the process is interrupted and
|
||||
the query is not executed.
|
||||
|
||||
Execution process never throws exceptions. Instead, all errors are caught and collected in
|
||||
execution result.
|
||||
|
||||
GraphQL is forgiving to **Execution** errors which occur in resolvers of nullable fields.
|
||||
If such field throws or returns unexpected value the value of the field in response will be simply
|
||||
replaced with **null** and error entry will be registered.
|
||||
|
||||
If an exception is thrown in the non-null field - error bubbles up to the first nullable field.
|
||||
This nullable field is replaced with **null** and error entry is added to the result.
|
||||
If all fields up to the root are non-null - **data** entry will be removed from the result
|
||||
and only **errors** key will be presented.
|
|
@ -6,46 +6,50 @@
|
|||
|
||||
# About GraphQL
|
||||
|
||||
GraphQL is a modern way to build HTTP APIs consumed by web and mobile clients.
|
||||
It is intended to be a replacement for REST and SOAP APIs (even for **existing applications**).
|
||||
GraphQL is a modern way to build HTTP APIs consumed by the web and mobile clients.
|
||||
It is intended to be an alternative to REST and SOAP APIs (even for **existing applications**).
|
||||
|
||||
GraphQL itself is a [specification](https://github.com/facebook/graphql) designed by Facebook
|
||||
engineers. Various implementations of this specification were written
|
||||
[for different languages and environments](http://graphql.org/code/).
|
||||
[in different languages and environments](http://graphql.org/code/).
|
||||
|
||||
Great overview of GraphQL features and benefits is presented on [official website](http://graphql.org/).
|
||||
Great overview of GraphQL features and benefits is presented on [the official website](http://graphql.org/).
|
||||
All of them equally apply to this PHP implementation.
|
||||
|
||||
|
||||
# About graphql-php
|
||||
|
||||
**graphql-php** is a feature-complete implementation of GraphQL specification in PHP (5.4+, 7.0+).
|
||||
**graphql-php** is a feature-complete implementation of GraphQL specification in PHP (5.5+, 7.0+).
|
||||
It was originally inspired by [reference JavaScript implementation](https://github.com/graphql/graphql-js)
|
||||
published by Facebook.
|
||||
|
||||
This library 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 rich API for your existing app.
|
||||
are used. Instead, it provides tools for creating rich API for your existing app.
|
||||
|
||||
Library features include:
|
||||
|
||||
These tools include:
|
||||
- Primitives to express your app as a [Type System](type-system/index.md)
|
||||
- Validation and introspection of this Type System (for compatibility with tools like [GraphiQL](complementary-tools.md#tools))
|
||||
- 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)
|
||||
|
||||
- Primitives to express your app as a Type System
|
||||
- Tools for validation and introspection of this Type System (for compatibility with tools like [GraphiQL](complementary-tools/#tools))
|
||||
- Tools for parsing, validating and executing GraphQL queries against this Type System
|
||||
- Rich error reporting, including query validation and execution errors
|
||||
- Optional tools for parsing GraphQL Schema Definition language
|
||||
|
||||
Also several [complementary tools](complementary-tools/) are available which provide integrations with
|
||||
Also, several [complementary tools](complementary-tools.md) are available which provide integrations with
|
||||
existing PHP frameworks, add support for Relay, etc.
|
||||
|
||||
## Current Status
|
||||
First version of this library (v0.1) was released on August 10th 2015.
|
||||
The first version of this library (v0.1) was released on August 10th 2015.
|
||||
|
||||
Current version (v0.9) supports all features described by GraphQL specification
|
||||
(including April 2016 add-ons) as well as some experimental features like
|
||||
Schema Language parser.
|
||||
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
|
||||
Project source code is [hosted on GitHub](https://github.com/webonyx/graphql-php).
|
||||
## GitHub
|
||||
Project source code is [hosted on GitHub](https://github.com/webonyx/graphql-php).
|
||||
|
|
2336
docs/reference.md
Normal file
2336
docs/reference.md
Normal file
File diff suppressed because it is too large
Load diff
94
docs/security.md
Normal file
94
docs/security.md
Normal file
|
@ -0,0 +1,94 @@
|
|||
# Query Complexity Analysis
|
||||
|
||||
This is a PHP port of [Query Complexity Analysis](http://sangria-graphql.org/learn/#query-complexity-analysis) in Sangria implementation.
|
||||
|
||||
Complexity analysis is a separate validation rule which calculates query complexity score before execution.
|
||||
Every field in the query gets a default score 1 (including ObjectType nodes). Total complexity of the
|
||||
query is the sum of all field scores. For example, the complexity of introspection query is **109**.
|
||||
|
||||
If this score exceeds a threshold, a query is not executed and an error is returned instead.
|
||||
|
||||
Complexity analysis is disabled by default. To enabled it, add validation rule:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Validator\Rules\QueryComplexity;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
|
||||
$rule = new QueryComplexity($maxQueryComplexity = 100);
|
||||
DocumentValidator::addRule($rule);
|
||||
|
||||
GraphQL::executeQuery(/*...*/);
|
||||
```
|
||||
This will set the rule globally. Alternatively, you can provide validation rules [per execution](executing-queries.md#custom-validation-rules).
|
||||
|
||||
To customize field score add **complexity** function to field definition:
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
|
||||
$type = new ObjectType([
|
||||
'name' => 'MyType',
|
||||
'fields' => [
|
||||
'someList' => [
|
||||
'type' => Type::listOf(Type::string()),
|
||||
'args' => [
|
||||
'limit' => [
|
||||
'type' => Type::int(),
|
||||
'defaultValue' => 10
|
||||
]
|
||||
],
|
||||
'complexity' => function($childrenComplexity, $args) {
|
||||
return $childrenComplexity * $args['limit'];
|
||||
}
|
||||
]
|
||||
]
|
||||
]);
|
||||
```
|
||||
|
||||
# Limiting Query Depth
|
||||
|
||||
This is a PHP port of [Limiting Query Depth](http://sangria-graphql.org/learn/#limiting-query-depth) in Sangria implementation.
|
||||
For example, max depth of the introspection query is **7**.
|
||||
|
||||
It is disabled by default. To enable it, add following validation rule:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Validator\Rules\QueryDepth;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
|
||||
$rule = new QueryDepth($maxDepth = 10);
|
||||
DocumentValidator::addRule($rule);
|
||||
|
||||
GraphQL::executeQuery(/*...*/);
|
||||
```
|
||||
|
||||
This will set the rule globally. Alternatively, you can provide validation rules [per execution](executing-queries.md#custom-validation-rules).
|
||||
|
||||
# Disabling Introspection
|
||||
[Introspection](http://graphql.org/learn/introspection/) is a mechanism for fetching schema structure.
|
||||
It is used by tools like GraphiQL for auto-completion, query validation, etc.
|
||||
|
||||
Introspection is enabled by default. It means that anybody can get a full description of your schema by
|
||||
sending a special query containing meta fields **__type** and **__schema** .
|
||||
|
||||
If you are not planning to expose your API to the general public, it makes sense to disable this feature.
|
||||
|
||||
GraphQL PHP provides you separate validation rule which prohibits queries that contain
|
||||
**__type** or **__schema** fields. To disable introspection, add following rule:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Validator\Rules\DisableIntrospection;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
|
||||
DocumentValidator::addRule(new DisableIntrospection());
|
||||
|
||||
GraphQL::executeQuery(/*...*/);
|
||||
```
|
||||
This will set the rule globally. Alternatively, you can provide validation rules [per execution](executing-queries.md#custom-validation-rules).
|
|
@ -1,12 +1,12 @@
|
|||
# Built-in directives
|
||||
Directive is a way for client to give GraphQL server additional context and hints on how to execute
|
||||
the query. Directive can be attached to a field or fragment inclusion, and can affect execution of the
|
||||
The directive is a way for a client to give GraphQL server additional context and hints on how to execute
|
||||
the query. The directive can be attached to a field or fragment and can affect the execution of the
|
||||
query in any way the server desires.
|
||||
|
||||
GraphQL specification includes two built-in directives:
|
||||
|
||||
* `@include(if: Boolean)` Only include this field or fragment in the result if the argument is `true`
|
||||
* `@skip(if: Boolean)` Skip this field or fragment if the argument is `true`
|
||||
* **@include(if: Boolean)** Only include this field or fragment in the result if the argument is **true**
|
||||
* **@skip(if: Boolean)** Skip this field or fragment if the argument is **true**
|
||||
|
||||
For example:
|
||||
```graphql
|
||||
|
@ -19,47 +19,43 @@ query Hero($episode: Episode, $withFriends: Boolean!) {
|
|||
}
|
||||
}
|
||||
```
|
||||
Here if `$withFriends` variable is set to `false` - friends section will be ignored and excluded
|
||||
from response. Important implementation detail: those fields will never be executed
|
||||
Here if **$withFriends** variable is set to **false** - friends section will be ignored and excluded
|
||||
from the response. Important implementation detail: those fields will never be executed
|
||||
(not just removed from response after execution).
|
||||
|
||||
# Custom directives
|
||||
**graphql-php** supports custom directives even though their presence does not affect execution of fields.
|
||||
But you can use `GraphQL\Type\Definition\ResolveInfo` in field resolvers to modify the output depending
|
||||
on those directives or perform statistics collection.
|
||||
**graphql-php** supports custom directives even though their presence does not affect the execution of fields.
|
||||
But you can use [`GraphQL\Type\Definition\ResolveInfo`](../reference.md#graphqltypedefinitionresolveinfo)
|
||||
in field resolvers to modify the output depending on those directives or perform statistics collection.
|
||||
|
||||
Other use case is your own query validation rules relying on custom directives.
|
||||
|
||||
In **graphql-php** custom directive is an instance of `GraphQL\Type\Definition\Directive`
|
||||
(or one of it subclasses) which accepts an array with following options:
|
||||
(or one of its subclasses) which accepts an array of following options:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Language\DirectiveLocation;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
|
||||
$trackDirective = new Directive([
|
||||
'name' => 'track',
|
||||
'description' => 'Instruction to record usage of the field by client'
|
||||
'description' => 'Instruction to record usage of the field by client',
|
||||
'locations' => [
|
||||
Directive::LOCATION_FIELD,
|
||||
DirectiveLocation::FIELD,
|
||||
],
|
||||
'args' => [
|
||||
new FieldArgument([
|
||||
'name' => 'details',
|
||||
'type' => Type::string(),
|
||||
'description' => 'String with additional details of field usage scenario'
|
||||
'description' => 'String with additional details of field usage scenario',
|
||||
'defaultValue' => ''
|
||||
])
|
||||
]
|
||||
]);
|
||||
```
|
||||
|
||||
Directive location can be one of the following values:
|
||||
|
||||
* `Directive::LOCATION_QUERY`
|
||||
* `Directive::LOCATION_MUTATION`
|
||||
* `Directive::LOCATION_SUBSCRIPTION`
|
||||
* `Directive::LOCATION_FIELD`
|
||||
* `Directive::LOCATION_FRAGMENT_DEFINITION`
|
||||
* `Directive::LOCATION_FRAGMENT_SPREAD`
|
||||
* `Directive::LOCATION_INLINE_FRAGMENT`
|
||||
See possible directive locations in
|
||||
[`GraphQL\Language\DirectiveLocation`](../reference.md#graphqllanguagedirectivelocation).
|
||||
|
|
|
@ -3,9 +3,10 @@ Enumeration types are a special kind of scalar that is restricted to a particula
|
|||
of allowed values.
|
||||
|
||||
In **graphql-php** enum type is an instance of `GraphQL\Type\Definition\EnumType`
|
||||
(or one of it subclasses) which accepts configuration array in constructor:
|
||||
which accepts configuration array in constructor:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
|
||||
$episodeEnum = new EnumType([
|
||||
|
@ -28,15 +29,15 @@ $episodeEnum = new EnumType([
|
|||
]);
|
||||
```
|
||||
|
||||
This example uses **inline** style for Enum Type definition, but you can also use
|
||||
[inheritance](/type-system/#type-definition-styles).
|
||||
This example uses an **inline** style for Enum Type definition, but you can also use
|
||||
[inheritance or type language](index.md#type-definition-styles).
|
||||
|
||||
# Configuration options
|
||||
Enum Type constructor accepts array with following options:
|
||||
Enum Type constructor accepts an array with following options:
|
||||
|
||||
Option | Type | Notes
|
||||
------ | ---- | -----
|
||||
name | `string` | **Required.** Name of the type. When not set - inferred from array key (read about [shorthand field definition](#) below)
|
||||
name | `string` | **Required.** Name of the type. When not set - inferred from array key (read about [shorthand field definition](#shorthand-definitions) below)
|
||||
description | `string` | Plain-text description of the type for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation)
|
||||
values | `array` | List of enumerated items, see below for expected structure of each entry
|
||||
|
||||
|
@ -44,7 +45,7 @@ Each entry of **values** array in turn accepts following options:
|
|||
|
||||
Option | Type | Notes
|
||||
------ | ---- | -----
|
||||
name | `string` | **Required.** Name of the item. When not set - inferred from array key (read about [shorthand field definition](#) below)
|
||||
name | `string` | **Required.** Name of the item. When not set - inferred from array key (read about [shorthand field definition](#shorthand-definitions) below)
|
||||
value | `mixed` | Internal representation of enum item in your application (could be any value, including complex objects or callbacks)
|
||||
description | `string` | Plain-text description of enum value for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation)
|
||||
deprecationReason | `string` | Text describing why this enum value is deprecated. When not empty - item will not be returned by introspection queries (unless forced)
|
||||
|
@ -55,6 +56,9 @@ If internal representation of enumerated item is the same as item name, then you
|
|||
following shorthand for definition:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
|
||||
$episodeEnum = new EnumType([
|
||||
'name' => 'Episode',
|
||||
'description' => 'One of the films in the Star Wars Trilogy',
|
||||
|
@ -64,19 +68,9 @@ $episodeEnum = new EnumType([
|
|||
|
||||
which is equivalent of:
|
||||
```php
|
||||
$episodeEnum = new EnumType([
|
||||
'name' => 'Episode',
|
||||
'description' => 'One of the films in the Star Wars Trilogy',
|
||||
'values' => [
|
||||
'NEWHOPE' => 'NEWHOPE',
|
||||
'EMPIRE' => 'EMPIRE',
|
||||
'JEDI' => 'JEDI'
|
||||
]
|
||||
]);
|
||||
```
|
||||
<?php
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
|
||||
which is in turn equivalent of:
|
||||
```php
|
||||
$episodeEnum = new EnumType([
|
||||
'name' => 'Episode',
|
||||
'description' => 'One of the films in the Star Wars Trilogy',
|
||||
|
@ -88,9 +82,12 @@ $episodeEnum = new EnumType([
|
|||
]);
|
||||
```
|
||||
|
||||
which is in turn equivalent of full form:
|
||||
which is in turn equivalent of the full form:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
|
||||
$episodeEnum = new EnumType([
|
||||
'name' => 'Episode',
|
||||
'description' => 'One of the films in the Star Wars Trilogy',
|
||||
|
@ -103,11 +100,12 @@ $episodeEnum = new EnumType([
|
|||
```
|
||||
|
||||
# Field Resolution
|
||||
When object field is of Enum Type, field resolver is expected to return internal
|
||||
When object field is of Enum Type, field resolver is expected to return an internal
|
||||
representation of corresponding Enum item (**value** in config). **graphql-php** will
|
||||
then serialize this **value** to **name** to include in response:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
|
||||
|
@ -140,14 +138,18 @@ $heroType = new ObjectType([
|
|||
}
|
||||
]
|
||||
]
|
||||
])
|
||||
]);
|
||||
```
|
||||
|
||||
Reverse is true when enum is used as input type (e.g. as field argument).
|
||||
The Reverse is true when the enum is used as input type (e.g. as field argument).
|
||||
GraphQL will treat enum input as **name** and convert it into **value** before passing to your app.
|
||||
|
||||
For example, given object type definition:
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
|
||||
$heroType = new ObjectType([
|
||||
'name' => 'Hero',
|
||||
'fields' => [
|
||||
|
@ -155,17 +157,17 @@ $heroType = new ObjectType([
|
|||
'type' => Type::boolean(),
|
||||
'args' => [
|
||||
'episode' => Type::nonNull($enumType)
|
||||
]
|
||||
'resolve' => function($_value, $args) {
|
||||
],
|
||||
'resolve' => function($hero, $args) {
|
||||
return $args['episode'] === 5 ? true : false;
|
||||
}
|
||||
]
|
||||
]
|
||||
])
|
||||
]);
|
||||
```
|
||||
|
||||
Then following query:
|
||||
```
|
||||
```graphql
|
||||
fragment on Hero {
|
||||
appearsInNewHope: appearsIn(NEWHOPE)
|
||||
appearsInEmpire: appearsIn(EMPIRE)
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
# Type System
|
||||
To start using GraphQL you are expected to implement a type hierarchy and expose it as [Schema](type-system/schema/).
|
||||
To start using GraphQL you are expected to implement a type hierarchy and expose it as [Schema](schema.md).
|
||||
|
||||
In **graphql-php** `type` is an instance of internal class from
|
||||
`GraphQL\Type\Definition` namespace: `ScalarType`, `ObjectType`, `InterfaceType`,
|
||||
`UnionType`, `InputObjectType` (or one of it's subclasses).
|
||||
In graphql-php **type** is an instance of internal class from
|
||||
`GraphQL\Type\Definition` namespace: [`ObjectType`](object-types.md),
|
||||
[`InterfaceType`](interfaces.md), [`UnionType`](unions.md), [`InputObjectType`](input-types.md),
|
||||
[`ScalarType`](scalar-types.md), [`EnumType`](enum-types.md) (or one of subclasses).
|
||||
|
||||
But most of the types in your schema will be [object types](/type-system/object-types/).
|
||||
But most of the types in your schema will be [object types](object-types.md).
|
||||
|
||||
# Type Definition Styles
|
||||
Several styles of type definitions are supported depending on your preferences.
|
||||
|
@ -50,42 +51,34 @@ class MyType extends ObjectType
|
|||
}
|
||||
```
|
||||
|
||||
You can also mix-and-match styles for convenience. For example:
|
||||
```php
|
||||
<?php
|
||||
namespace MyApp;
|
||||
Using [GraphQL Type language](http://graphql.org/learn/schema/#type-language):
|
||||
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
```graphql
|
||||
schema {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
class BlogPostType extends ObjectType
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$config = [
|
||||
'fields' => [
|
||||
'body' => new ObjectType([
|
||||
'name' => 'BlogPostBody',
|
||||
'fields' => [
|
||||
'html' => Type::string(),
|
||||
'text' => Type::string(),
|
||||
]
|
||||
])
|
||||
]
|
||||
];
|
||||
parent::__construct($config);
|
||||
}
|
||||
type Query {
|
||||
greetings(input: HelloInput!): String!
|
||||
}
|
||||
|
||||
input HelloInput {
|
||||
firstName: String!
|
||||
lastName: String
|
||||
}
|
||||
```
|
||||
|
||||
Read more about type language definitions in a [dedicated docs section](type-language.md).
|
||||
|
||||
# Type Registry
|
||||
Every type must be presented in Schema by single instance (**graphql-php**
|
||||
throws when it discovers several instances with the same `name` in schema).
|
||||
Every type must be presented in Schema by a single instance (**graphql-php**
|
||||
throws when it discovers several instances with the same **name** in the schema).
|
||||
|
||||
Therefore if you define your type as separate PHP class you must ensure that only one
|
||||
instance of that class is added to schema.
|
||||
instance of that class is added to the schema.
|
||||
|
||||
Typical way to do this is to create registry of your types:
|
||||
The typical way to do this is to create a registry of your types:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
@ -126,8 +119,9 @@ class MyAType extends ObjectType
|
|||
}
|
||||
}
|
||||
```
|
||||
Obviously you can automate this registry as you wish to reduce boilerplate or even
|
||||
Obviously, you can automate this registry as you wish to reduce boilerplate or even
|
||||
introduce Dependency Injection Container if your types have other dependencies.
|
||||
|
||||
Alternatively all methods of registry could be static if you prefer - then there is no need
|
||||
to pass it in constructor - instead just use use `TypeRegistry::myAType()` in your type definitions.
|
||||
Alternatively, all methods of the registry could be static - then there is no need
|
||||
to pass it in constructor - instead just use use **TypeRegistry::myAType()** in your
|
||||
type definitions.
|
||||
|
|
|
@ -1,33 +1,82 @@
|
|||
# Mutations
|
||||
Mutation is just a field of a regular [Object Type](object-types.md) with arguments.
|
||||
For GraphQL PHP runtime there is no difference between query fields with arguments and mutations.
|
||||
They are executed [almost](http://facebook.github.io/graphql/#sec-Mutation) identically.
|
||||
To some extent, Mutation is just a convention described in the GraphQL spec.
|
||||
|
||||
Here is an example of a mutation operation:
|
||||
```graphql
|
||||
mutation CreateReviewForEpisode($ep: EpisodeInput!, $review: ReviewInput!) {
|
||||
createReview(episode: $ep, review: $review) {
|
||||
stars
|
||||
commentary
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To execute such a mutation, you need **Mutation** type [at the root of your schema](schema.md):
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
|
||||
$myMutationType = new ObjectType([
|
||||
'name' => 'Mutation',
|
||||
'fields' => [
|
||||
// List of mutations:
|
||||
'createReview' => [
|
||||
'args' => [
|
||||
'episode' => Type::nonNull($episodeInputType),
|
||||
'review' => Type::nonNull($reviewInputType)
|
||||
],
|
||||
'type' => new ObjectType([
|
||||
'name' => 'CreateReviewOutput',
|
||||
'fields' => [
|
||||
'stars' => ['type' => Type::int()],
|
||||
'commentary' => ['type' => Type::string()]
|
||||
]
|
||||
]),
|
||||
],
|
||||
// ... other mutations
|
||||
]
|
||||
]);
|
||||
```
|
||||
As you can see, the only difference from regular object type is the semantics of field names
|
||||
(verbs vs nouns).
|
||||
|
||||
Also as we see arguments can be of complex types. To leverage the full power of mutations
|
||||
(and field arguments in general) you must learn how to create complex input types.
|
||||
|
||||
|
||||
# About Input and Output Types
|
||||
GraphQL receives data from clients via [Field Arguments](object-types/#field-arguments).
|
||||
All types in GraphQL are of two categories: **input** and **output**.
|
||||
|
||||
Both - fields and arguments require **type** option in definition. But expected value of this option
|
||||
is different for fields and arguments, as in GraphQL argument is conceptually input while field is conceptually
|
||||
output.
|
||||
* **Output** types (or field types) are: [Scalar](scalar-types.md), [Enum](enum-types.md), [Object](object-types.md),
|
||||
[Interface](interfaces.md), [Union](unions.md)
|
||||
|
||||
Consequentially all types in GraphQL are of two categories: **input** and **output**.
|
||||
* **Input** types (or argument types) are: [Scalar](scalar-types.md), [Enum](enum-types.md), InputObject
|
||||
|
||||
* **Output** types (or field types) are: [Scalar](scalar-types/), [Enum](enum-types/), [Object](object-types/),
|
||||
[Interface](interfaces/), [Union](unions/)
|
||||
|
||||
* **Input** types (or argument types) are: [Scalar](scalar-types/), [Enum](enum-types/), InputObject
|
||||
|
||||
Obviously [NonNull and List](lists-and-nonnulls/) types belong to both categories depending on their
|
||||
Obviously, [NonNull and List](lists-and-nonnulls.md) types belong to both categories depending on their
|
||||
inner type.
|
||||
|
||||
Until now all examples of field **arguments** in this documentation were of [Scalar](scalar-types/) or
|
||||
[Enum](enum-types/) types. But you can also easily pass complex objects.
|
||||
Until now all examples of field **arguments** in this documentation were of [Scalar](scalar-types.md) or
|
||||
[Enum](enum-types.md) types. But you can also pass complex objects.
|
||||
|
||||
This is particularly valuable in the case of mutations, where input data might be rather complex.
|
||||
This is particularly valuable in case of mutations, where input data might be rather complex.
|
||||
|
||||
# Input Object Type
|
||||
GraphQL specification defines Input Object Type for complex inputs. It is similar to ObjectType
|
||||
except that it's fields have no **args** or **resolve** options and their **type** must be input type.
|
||||
|
||||
In **graphql-php** Input Object Type is an instance of `GraphQL\Type\Definition\InputObjectType`
|
||||
In graphql-php **Input Object Type** is an instance of `GraphQL\Type\Definition\InputObjectType`
|
||||
(or one of it subclasses) which accepts configuration array in constructor:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
|
||||
$filters = new InputObjectType([
|
||||
'name' => 'StoryFiltersInput',
|
||||
'fields' => [
|
||||
|
@ -50,12 +99,12 @@ $filters = new InputObjectType([
|
|||
Every field may be of other InputObjectType (thus complex hierarchies of inputs are possible)
|
||||
|
||||
# Configuration options
|
||||
Constructor of InputObjectType accepts array with only 3 options:
|
||||
The constructor of InputObjectType accepts array with only 3 options:
|
||||
|
||||
Option | Type | Notes
|
||||
------------ | -------- | -----
|
||||
name | `string` | **Required.** Unique name of this object type within Schema
|
||||
fields | `array` or `callback` returning `array` | **Required**. Array describing object fields (see below).
|
||||
fields | `array` or `callable` | **Required**. An array describing object fields or callable returning such an array (see below).
|
||||
description | `string` | Plain-text description of this type for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation)
|
||||
|
||||
Every field is an array with following entries:
|
||||
|
@ -63,14 +112,18 @@ Every field is an array with following entries:
|
|||
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/) (`Scalar`, `Enum`, `InputObjectType` + any combination of those with `NonNull` and `List` modifiers)
|
||||
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:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
|
||||
$queryType = new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => [
|
||||
|
@ -78,7 +131,7 @@ $queryType = new ObjectType([
|
|||
'type' => Type::listOf($storyType),
|
||||
'args' => [
|
||||
'filters' => [
|
||||
'type' => Type::nonNull($filters),
|
||||
'type' => $filters,
|
||||
'defaultValue' => [
|
||||
'popular' => true
|
||||
]
|
||||
|
|
|
@ -3,9 +3,10 @@ An Interface is an abstract type that includes a certain set of fields that a
|
|||
type must include to implement the interface.
|
||||
|
||||
In **graphql-php** interface type is an instance of `GraphQL\Type\Definition\InterfaceType`
|
||||
(or one of it subclasses) which accepts configuration array in constructor:
|
||||
(or one of its subclasses) which accepts configuration array in a constructor:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
|
||||
|
@ -32,21 +33,25 @@ $character = new InterfaceType([
|
|||
]);
|
||||
```
|
||||
This example uses **inline** style for Interface definition, but you can also use
|
||||
[inheritance](/type-system/#type-definition-styles).
|
||||
[inheritance or type language](index.md#type-definition-styles).
|
||||
|
||||
# Configuration options
|
||||
Constructor of InterfaceType accepts an array. Below is a full list of allowed options:
|
||||
The constructor of InterfaceType accepts an array. Below is a full list of allowed 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. Same as [Fields for Object Type](#)
|
||||
fields | `array` | **Required.** List of fields required to be defined by interface implementors. Same as [Fields for Object Type](object-types.md#field-configuration-options)
|
||||
description | `string` | Plain-text description of this type for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation)
|
||||
resolveType | `callback` returning instance of `ObjectType` | **function($value, $context, GraphQL\Type\Definition\ResolveInfo $info)** Any `callable` that receives `$value` from resolver of the parent field and returns concrete interface implementor for that `$value`.
|
||||
resolveType | `callback` | **function($value, $context, [ResolveInfo](../reference.md#graphqltypedefinitionresolveinfo) $info)**<br> Receives **$value** from resolver of the parent field and returns concrete interface implementor for this **$value**.
|
||||
|
||||
# Implementing interface
|
||||
To implement the Interface simply add it to **interfaces** array of Object Type definition:
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
|
||||
$humanType = new ObjectType([
|
||||
'name' => 'Human',
|
||||
'fields' => [
|
||||
|
@ -71,7 +76,7 @@ The only exception is when object's field type is more specific than the type of
|
|||
(see [Covariant return types for interface fields](#covariant-return-types-for-interface-fields) below)
|
||||
|
||||
# Covariant return types for interface fields
|
||||
Object types implementing interface may change field type to more specific.
|
||||
Object types implementing interface may change the field type to more specific.
|
||||
Example:
|
||||
|
||||
```
|
||||
|
@ -86,9 +91,13 @@ type B implements A {
|
|||
|
||||
# Sharing Interface fields
|
||||
Since every Object Type implementing an Interface must have the same set of fields - it often makes
|
||||
sense to re-use field definitions of Interface in Object Types:
|
||||
sense to reuse field definitions of Interface in Object Types:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
|
||||
$humanType = new ObjectType([
|
||||
'name' => 'Human',
|
||||
'interfaces' => [
|
||||
|
@ -102,26 +111,26 @@ $humanType = new ObjectType([
|
|||
]);
|
||||
```
|
||||
|
||||
In this case field definitions are created only once (as a part of Interface Type) and then
|
||||
re-used by all interface implementors. It can save several microseconds and kilobytes + ensures that
|
||||
In this case, field definitions are created only once (as a part of Interface Type) and then
|
||||
reused by all interface implementors. It can save several microseconds and kilobytes + ensures that
|
||||
field definitions of Interface and implementors are always in sync.
|
||||
|
||||
Yet it creates a problem with resolution of such fields. There are two ways how shared fields could
|
||||
Yet it creates a problem with the resolution of such fields. There are two ways how shared fields could
|
||||
be resolved:
|
||||
|
||||
1. If field resolution algorithm is the same for all Interface implementors - you can simply add
|
||||
**resolve** option to field definition in Interface itself.
|
||||
|
||||
2. If field resolution varies from implementor to implementor - you can specify **resolveField**
|
||||
option in [Object Type config](/type-system/object-types/#configuration-options) and handle field
|
||||
2. If field resolution varies for different implementations - you can specify **resolveField**
|
||||
option in [Object Type config](object-types.md#configuration-options) and handle field
|
||||
resolutions there
|
||||
(Note: **resolve** option in field definition has precedence over **resolveField** option in object type definition)
|
||||
|
||||
# Interface role in data fetching
|
||||
The only responsibility of interface in Data Fetching process is to return concrete Object Type
|
||||
for given `$value` in **resolveType**. Then resolution of fields is delegated to resolvers of this
|
||||
for given **$value** in **resolveType**. Then resolution of fields is delegated to resolvers of this
|
||||
concrete Object Type.
|
||||
|
||||
If **resolveType** option is omitted, **graphql-php** will loop through all interface implementors and
|
||||
If a **resolveType** option is omitted, graphql-php will loop through all interface implementors and
|
||||
use their **isTypeOf** callback to pick the first suitable one. This is obviously less efficient
|
||||
than single **resolveType** call. So it is recommended to define **resolveType** whenever possible.
|
||||
|
|
|
@ -21,17 +21,18 @@ $userType = new ObjectType([
|
|||
]);
|
||||
```
|
||||
|
||||
Resolvers for such fields are expected to return `array` or instance of PHP internal `Traversable`
|
||||
interface (`null` is allowed by default too).
|
||||
Resolvers for such fields are expected to return **array** or instance of PHP's built-in **Traversable**
|
||||
interface (**null** is allowed by default too).
|
||||
|
||||
If returned value is not of one of these types - **graphql-php** will add an error to result
|
||||
and set field value to `null` (only if field is nullable, see below for non-null fields).
|
||||
and set the field value to **null** (only if the field is nullable, see below for non-null fields).
|
||||
|
||||
# Non-Null fields
|
||||
By default in GraphQL every field can have `null` value. To indicate that some field always
|
||||
returns `non-null` value - use `GraphQL\Type\Definition\Type::nonNull()` modifier:
|
||||
By default in GraphQL, every field can have a **null** value. To indicate that some field always
|
||||
returns **non-null** value - use `GraphQL\Type\Definition\Type::nonNull()` modifier:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
|
||||
|
@ -54,8 +55,8 @@ $humanType = new ObjectType([
|
|||
]);
|
||||
```
|
||||
|
||||
If resolver of non-null field returns `null`, **graphql-php** will add an error to
|
||||
result and exclude whole object from output (error will bubble to first nullable parent
|
||||
field which will be set to `null`).
|
||||
If resolver of non-null field returns **null**, graphql-php will add an error to
|
||||
result and exclude the whole object from the output (an error will bubble to first
|
||||
nullable parent field which will be set to **null**).
|
||||
|
||||
Read section on [Data Fetching](#) for details.
|
||||
Read the section on [Data Fetching](../data-fetching.md) for details.
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Object Type Definition
|
||||
Object Type is the most frequently used primitive in typical GraphQL application.
|
||||
Object Type is the most frequently used primitive in a typical GraphQL application.
|
||||
|
||||
Conceptually Object Type is a collection of Fields. Each field in turn
|
||||
has it's own type which allows to build complex hierarchies.
|
||||
Conceptually Object Type is a collection of Fields. Each field, in turn,
|
||||
has its own type which allows building complex hierarchies.
|
||||
|
||||
In **graphql-php** object type is an instance of `GraphQL\Type\Definition\ObjectType`
|
||||
(or one of it subclasses) which accepts configuration array in constructor:
|
||||
|
@ -57,7 +57,7 @@ $blogStory = new ObjectType([
|
|||
]);
|
||||
```
|
||||
This example uses **inline** style for Object Type definitions, but you can also use
|
||||
[inheritance](/type-system/#type-definition-styles).
|
||||
[inheritance or type language](index.md#type-definition-styles).
|
||||
|
||||
|
||||
# Configuration options
|
||||
|
@ -66,21 +66,22 @@ Object type constructor expects configuration array. Below is a full list of ava
|
|||
Option | Type | Notes
|
||||
------------ | -------- | -----
|
||||
name | `string` | **Required.** Unique name of this object type within Schema
|
||||
fields | `array` or `callback` returning `array` | **Required**. Array describing object fields. See [Fields](#field-definitions) section below for expected structure of each array entry. See also section on [Circular types](#) for explanation of when to use callback for this option.
|
||||
fields | `array` or `callable` | **Required**. An array describing object fields or callable returning such an array. See [Fields](#field-definitions) section below for expected structure of each array entry. See also the section on [Circular types](#recurring-and-circular-types) for an explanation of when to use callable for this option.
|
||||
description | `string` | Plain-text description of this type for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation)
|
||||
interfaces | `array` or `callback` returning `array` | List of interfaces implemented by this type. See [Interface Types](/type-system/interface-types) for details. See also section on [Circular types](#) for explanation of when to use callback for this option.
|
||||
isTypeOf | `callback` returning `boolean` | **function($value, $context, GraphQL\Type\Definition\ResolveInfo $info)** Expected to return `true` if `$value` qualifies for this type (see section about [Abstract Type Resolution](#) for explanation).
|
||||
resolveField | `callback` returning `mixed` | **function($value, $args, $context, GraphQL\Type\Definition\ResolveInfo $info)** Given the `$value` of this type it is expected to return value for field defined in `$info->fieldName`. Good place to define type-specific strategy for field resolution. See section on [Data Fetching](#) for details.
|
||||
interfaces | `array` or `callable` | List of interfaces implemented by this type or callable returning such a list. See [Interface Types](interfaces.md) for details. See also the section on [Circular types](#recurring-and-circular-types) for an explanation of when to use callable for this option.
|
||||
isTypeOf | `callable` | **function($value, $context, [ResolveInfo](../reference.md#graphqltypedefinitionresolveinfo) $info)**<br> Expected to return **true** if **$value** qualifies for this type (see section about [Abstract Type Resolution](interfaces.md#interface-role-in-data-fetching) for explanation).
|
||||
resolveField | `callable` | **function($value, $args, $context, [ResolveInfo](../reference.md#graphqltypedefinitionresolveinfo) $info)**<br> Given the **$value** of this type, it is expected to return value for a field defined in **$info->fieldName**. A good place to define a type-specific strategy for field resolution. See section on [Data Fetching](../data-fetching.md) for details.
|
||||
|
||||
# Field configuration options
|
||||
Below is a full list of available field configuration options:
|
||||
|
||||
Option | Type | Notes
|
||||
------ | ---- | -----
|
||||
name | `string` | **Required.** Name of the field. When not set - inferred from **fields** array key (read about [shorthand field definition](#) below)
|
||||
type | `Type` | **Required.** Instance of internal or custom type. Note: type must be represented by single instance within schema (see also [Type Registry](#))
|
||||
args | `array` | 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 | `callback` | **function($value, $args, $context, GraphQL\Type\Definition\ResolveInfo $info)** Given the `$value` of this type it is expected to return value for current field. See section on [Data Fetching](#) for details
|
||||
name | `string` | **Required.** Name of the field. When not set - inferred from **fields** array key (read about [shorthand field definition](#shorthand-field-definitions) below)
|
||||
type | `Type` | **Required.** An instance of internal or custom type. Note: type must be represented by a single instance within one schema (see also [Type Registry](index.md#type-registry))
|
||||
args | `array` | An array of possible type arguments. Each entry is expected to be an array with keys: **name**, **type**, **description**, **defaultValue**. See [Field Arguments](#field-arguments) section below.
|
||||
resolve | `callable` | **function($objectValue, $args, $context, [ResolveInfo](../reference.md#graphqltypedefinitionresolveinfo) $info)**<br> Given the **$objectValue** of this type, it is expected to return actual value of the current field. See section on [Data Fetching](../data-fetching.md) for details
|
||||
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)
|
||||
|
||||
|
@ -91,9 +92,9 @@ Each argument is an array with following options:
|
|||
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/) (`scalar`, `enum`, `InputObjectType` + any combination of those with `nonNull` and `listOf` modifiers)
|
||||
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):
|
||||
|
@ -110,7 +111,7 @@ which is equivalent of:
|
|||
'fieldName' => ['type' => $fieldName]
|
||||
]
|
||||
```
|
||||
which is in turn equivalent of full form:
|
||||
which is in turn equivalent of the full form:
|
||||
```php
|
||||
'fields' => [
|
||||
['name' => 'id', 'type' => Type::id()],
|
||||
|
@ -123,11 +124,15 @@ Same shorthand notation applies to field arguments as well.
|
|||
Almost all real-world applications contain recurring or circular types.
|
||||
Think user friends or nested comments for example.
|
||||
|
||||
**graphql-php** allows such types, but you have to use `callback` in
|
||||
**graphql-php** allows such types, but you have to use `callable` in
|
||||
option **fields** (and/or **interfaces**).
|
||||
|
||||
For example:
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
|
||||
$userType = null;
|
||||
|
||||
$userType = new ObjectType([
|
||||
|
@ -145,7 +150,7 @@ $userType = new ObjectType([
|
|||
]);
|
||||
```
|
||||
|
||||
Same example for [inheritance style of type definitions](#) using [TypeRegistry](#):
|
||||
Same example for [inheritance style of type definitions](index.md#type-definition-styles) using [TypeRegistry](index.md#type-registry):
|
||||
```php
|
||||
<?php
|
||||
namespace MyApp;
|
||||
|
@ -192,14 +197,14 @@ class MyTypes
|
|||
|
||||
# Field Resolution
|
||||
Field resolution is the primary mechanism in **graphql-php** for returning actual data for your fields.
|
||||
It is implemented using `resolveField` callback in type definition or `resolve`
|
||||
callback in field definition (which has precedence).
|
||||
It is implemented using **resolveField** callable in type definition or **resolve**
|
||||
callable in field definition (which has precedence).
|
||||
|
||||
Read section on [Data Fetching]() for complete description of this process.
|
||||
Read the section on [Data Fetching](../data-fetching.md) for a complete description of this process.
|
||||
|
||||
# Custom Metadata
|
||||
All types in **graphql-php** accept configuration array. In some cases you may be interested in
|
||||
All types in **graphql-php** accept configuration array. In some cases, you may be interested in
|
||||
passing your own metadata for type or field definition.
|
||||
|
||||
**graphql-php** preserves original configuration array in every type or field instance in
|
||||
public property `$config`. Use it to implement app-level mappings and definitions.
|
||||
public property **$config**. Use it to implement app-level mappings and definitions.
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
# Built-in Scalar Types
|
||||
GraphQL specification describes several built-in scalar types. In **graphql-php** they are
|
||||
exposed as static methods of `GraphQL\Type\Definition\Type` class:
|
||||
exposed as static methods of [`GraphQL\Type\Definition\Type`](../reference.md#graphqltypedefinitiontype) class:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\Type;
|
||||
|
||||
// Built-in Scalar types:
|
||||
|
@ -12,39 +13,40 @@ Type::float(); // Float type
|
|||
Type::boolean(); // Boolean type
|
||||
Type::id(); // ID type
|
||||
```
|
||||
Those methods return instances of `GraphQL\Type\Definition\ScalarType` (actually one of it subclasses).
|
||||
Use them directly in type definitions, or wrap in your [TypeRegistry](/type-system/#type-registry)
|
||||
Those methods return instances of `GraphQL\Type\Definition\ScalarType` (actually one of subclasses).
|
||||
Use them directly in type definitions, or wrap in your [TypeRegistry](index.md#type-registry)
|
||||
(if you use one).
|
||||
|
||||
# Writing Custom Scalar Types
|
||||
In addition to built-in scalars, you can define your own scalar types with additional validation.
|
||||
Typical examples of such types are: `Email`, `Date`, `Url`, etc.
|
||||
Typical examples of such types are **Email**, **Date**, **Url**, etc.
|
||||
|
||||
In order to implement your own type you must understand how scalars are presented in GraphQL.
|
||||
In order to implement your own type, you must understand how scalars are presented in GraphQL.
|
||||
GraphQL deals with scalars in following cases:
|
||||
|
||||
1. When converting **internal representation** of value returned by your app (e.g. stored in database
|
||||
or hardcoded in source code) to **serialized** representation included in response.
|
||||
1. When converting **internal representation** of value returned by your app (e.g. stored in a database
|
||||
or hardcoded in the source code) to **serialized** representation included in the response.
|
||||
|
||||
2. When converting **input value** passed by client in variables along with GraphQL query to
|
||||
2. When converting **input value** passed by a client in variables along with GraphQL query to
|
||||
**internal representation** of your app.
|
||||
|
||||
3. When converting **input literal value** hardcoded in GraphQL query (e.g. field argument value) to
|
||||
**internal representation** of your app.
|
||||
the **internal representation** of your app.
|
||||
|
||||
Those cases are covered by methods `serialize`, `parseValue` and `parseLiteral` of abstract `ScalarType`
|
||||
class respectively.
|
||||
|
||||
Here is an example of simple `Email` type:
|
||||
Here is an example of a simple **Email** type:
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace MyApp;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Language\AST\StringValueNode;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use GraphQL\Utils;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
class EmailType extends ScalarType
|
||||
{
|
||||
|
@ -62,9 +64,12 @@ class EmailType extends ScalarType
|
|||
{
|
||||
// Assuming internal representation of email is always correct:
|
||||
return $value;
|
||||
|
||||
// If it might be incorrect and you want to make sure that only correct values are included in response -
|
||||
// use following line instead:
|
||||
|
||||
// If it might be incorrect and you want to make sure that only correct values are included
|
||||
// in response - use following line instead:
|
||||
// if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
// throw new InvariantViolation("Could not serialize following value as email: " . Utils::printSafe($value));
|
||||
// }
|
||||
// return $this->parseValue($value);
|
||||
}
|
||||
|
||||
|
@ -77,7 +82,7 @@ class EmailType extends ScalarType
|
|||
public function parseValue($value)
|
||||
{
|
||||
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new \UnexpectedValueException("Cannot represent value as email: " . Utils::printSafe($value));
|
||||
throw new Error("Cannot represent following value as email: " . Utils::printSafeJson($value));
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
@ -91,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:
|
||||
|
@ -119,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 */},
|
||||
]);
|
||||
```
|
||||
|
|
|
@ -1,31 +1,34 @@
|
|||
# Schema Definition
|
||||
Schema is a container of your type hierarchy, which accepts root types in constructor and provides
|
||||
The schema is a container of your type hierarchy, which accepts root types in a constructor and provides
|
||||
methods for receiving information about your types to internal GrahpQL tools.
|
||||
|
||||
In **graphql-php** schema is an instance of `GraphQL\Schema` which accepts configuration array
|
||||
in constructor:
|
||||
In **graphql-php** schema is an instance of [`GraphQL\Type\Schema`](../reference.md#graphqltypeschema)
|
||||
which accepts configuration array in a constructor:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Schema;
|
||||
|
||||
$schema = new Schema([
|
||||
'query' => $queryType,
|
||||
'mutation' => $mutationType,
|
||||
]);
|
||||
```
|
||||
See possible constructor options [below](#configuration-options)
|
||||
See possible constructor options [below](#configuration-options).
|
||||
|
||||
# Query and Mutation types
|
||||
Schema consists of two root types:
|
||||
The schema consists of two root types:
|
||||
|
||||
* `Query` type is a surface of your read API
|
||||
* `Mutation` type (optional) exposes write API by declaring all possible mutations in your app.
|
||||
* **Query** type is a surface of your read API
|
||||
* **Mutation** type (optional) exposes write API by declaring all possible mutations in your app.
|
||||
|
||||
Query and Mutation types are regular [object types](object-types/) containing root-level fields
|
||||
Query and Mutation types are regular [object types](object-types.md) containing root-level fields
|
||||
of your API:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Schema;
|
||||
|
||||
$queryType = new ObjectType([
|
||||
'name' => 'Query',
|
||||
|
@ -35,7 +38,7 @@ $queryType = new ObjectType([
|
|||
'resolve' => function() {
|
||||
return 'Hello World!';
|
||||
}
|
||||
]
|
||||
],
|
||||
'hero' => [
|
||||
'type' => $characterInterface,
|
||||
'args' => [
|
||||
|
@ -53,13 +56,13 @@ $queryType = new ObjectType([
|
|||
$mutationType = new ObjectType([
|
||||
'name' => 'Mutation',
|
||||
'fields' => [
|
||||
'createReviewForEpisode' => [
|
||||
'type' => $createReviewForEpisodeMutation,
|
||||
'createReview' => [
|
||||
'type' => $createReviewOutput,
|
||||
'args' => [
|
||||
'episode' => $episodeEnum,
|
||||
'review' => $reviewInputObject
|
||||
],
|
||||
'resolve' => function($val, $args) {
|
||||
'resolve' => function($rootValue, $args) {
|
||||
// TODOC
|
||||
}
|
||||
]
|
||||
|
@ -67,21 +70,125 @@ $mutationType = new ObjectType([
|
|||
]);
|
||||
```
|
||||
|
||||
Keep in mind that other than the special meaning of declaring surface area of your API,
|
||||
those types are the same as any other [object type](object-types/), and their fields work
|
||||
Keep in mind that other than the special meaning of declaring a surface area of your API,
|
||||
those types are the same as any other [object type](object-types.md), and their fields work
|
||||
exactly the same way.
|
||||
|
||||
**Mutation** type is also just a regular object type. The difference is in semantics.
|
||||
Field names of Mutation type are usually verbs and they almost always have arguments - quite often
|
||||
with complex input values (see [Input Types](input-types/) for details).
|
||||
with complex input values (see [Mutations and Input Types](input-types.md) for details).
|
||||
|
||||
# Configuration Options
|
||||
Schema constructor expects an array with following options:
|
||||
Schema constructor expects an instance of [`GraphQL\Type\SchemaConfig`](../reference.md#graphqltypeschemaconfig)
|
||||
or an array with following options:
|
||||
|
||||
Option | Type | Notes
|
||||
------------ | -------- | -----
|
||||
query | `ObjectType` | **Required.** Object type (usually named "Query") containing root-level fields of your read API
|
||||
mutation | `ObjectType` | Object type (usually named "Mutation") containing root-level fields of your write API
|
||||
subscription | `ObjectType` | Reserved for future subscriptions implementation. Currently presented for compatibility with introspection query of **graphql-js**, used by various clients (like Relay or GraphiQL)
|
||||
directives | `Directive[]` | Full list of [directives](directives/) supported by your schema. By default contains built-in `@skip` and `@include` directives.<br><br> If you pass your own directives and still want to use built-in directives - add them explicitly. For example: `array_merge(GraphQL::getInternalDirectives(), [$myCustomDirective]`
|
||||
types | `ObjectType[]` | List of object types which cannot be detected by **graphql-php** during static schema analysis.<br><br>Most often it happens when object type is never referenced in fields directly, but is still a part of schema because it implements an interface which resolves to this object type in it's `resolveType` callback. <br><br> Note that you are not required to pass all of your types here - it is simply a workaround for concrete use-case.
|
||||
directives | `Directive[]` | A full list of [directives](directives.md) supported by your schema. By default, contains built-in **@skip** and **@include** directives.<br><br> If you pass your own directives and still want to use built-in directives - add them explicitly. For example:<br><br> *array_merge(GraphQL::getStandardDirectives(), [$myCustomDirective]);*
|
||||
types | `ObjectType[]` | List of object types which cannot be detected by **graphql-php** during static schema analysis.<br><br>Most often it happens when the object type is never referenced in fields directly but is still a part of a schema because it implements an interface which resolves to this object type in its **resolveType** callable. <br><br> Note that you are not required to pass all of your types here - it is simply a workaround for concrete use-case.
|
||||
typeLoader | `callable` | **function($name)** Expected to return type instance given the name. Must always return the same instance if called multiple times. See section below on lazy type loading.
|
||||
|
||||
# Using config class
|
||||
If you prefer fluid interface for config with auto-completion in IDE and static time validation,
|
||||
use [`GraphQL\Type\SchemaConfig`](../reference.md#graphqltypeschemaconfig) instead of an array:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\SchemaConfig;
|
||||
use GraphQL\Type\Schema;
|
||||
|
||||
$config = SchemaConfig::create()
|
||||
->setQuery($myQueryType)
|
||||
->setTypeLoader($myTypeLoader);
|
||||
|
||||
$schema = new Schema($config);
|
||||
```
|
||||
|
||||
|
||||
# Lazy loading of types
|
||||
By default, the schema will scan all of your type, field and argument definitions to serve GraphQL queries.
|
||||
It may cause performance overhead when there are many types in the schema.
|
||||
|
||||
In this case, it is recommended to pass **typeLoader** option to schema constructor and define all
|
||||
of your object **fields** as callbacks.
|
||||
|
||||
Type loading concept is very similar to PHP class loading, but keep in mind that **typeLoader** must
|
||||
always return the same instance of a type.
|
||||
|
||||
Usage example:
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Schema;
|
||||
|
||||
class Types
|
||||
{
|
||||
private $types = [];
|
||||
|
||||
public function get($name)
|
||||
{
|
||||
if (!isset($this->types[$name])) {
|
||||
$this->types[$name] = $this->{$name}();
|
||||
}
|
||||
return $this->types[$name];
|
||||
}
|
||||
|
||||
private function MyTypeA()
|
||||
{
|
||||
return new ObjectType([
|
||||
'name' => 'MyTypeA',
|
||||
'fields' => function() {
|
||||
return [
|
||||
'b' => ['type' => $this->get('MyTypeB')]
|
||||
];
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
private function MyTypeB()
|
||||
{
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
$registry = new Types();
|
||||
|
||||
$schema = new Schema([
|
||||
'query' => $registry->get('Query'),
|
||||
'typeLoader' => function($name) use ($registry) {
|
||||
return $registry->get($name);
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
|
||||
# Schema Validation
|
||||
By default, the schema is created with only shallow validation of type and field definitions
|
||||
(because validation requires full schema scan and is very costly on bigger schemas).
|
||||
|
||||
But there is a special method **assertValid()** on schema instance which throws
|
||||
`GraphQL\Error\InvariantViolation` exception when it encounters any error, like:
|
||||
|
||||
- Invalid types used for fields/arguments
|
||||
- Missing interface implementations
|
||||
- Invalid interface implementations
|
||||
- Other schema errors...
|
||||
|
||||
Schema validation is supposed to be used in CLI commands or during build step of your app.
|
||||
Don't call it in web requests in production.
|
||||
|
||||
Usage example:
|
||||
```php
|
||||
<?php
|
||||
try {
|
||||
$schema = new GraphQL\Type\Schema([
|
||||
'query' => $myQueryType
|
||||
]);
|
||||
$schema->assertValid();
|
||||
} catch (GraphQL\Error\InvariantViolation $e) {
|
||||
echo $e->getMessage();
|
||||
}
|
||||
```
|
||||
|
|
91
docs/type-system/type-language.md
Normal file
91
docs/type-system/type-language.md
Normal file
|
@ -0,0 +1,91 @@
|
|||
# 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.
|
||||
|
||||
Here is a simple schema defined in GraphQL type language (e.g. in a separate **schema.graphql** file):
|
||||
|
||||
```graphql
|
||||
schema {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
type Query {
|
||||
greetings(input: HelloInput!): String!
|
||||
}
|
||||
|
||||
input HelloInput {
|
||||
firstName: String!
|
||||
lastName: String
|
||||
}
|
||||
```
|
||||
|
||||
In order to create schema instance out of this file, use
|
||||
[`GraphQL\Utils\BuildSchema`](../reference.md#graphqlutilsbuildschema):
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Utils\BuildSchema;
|
||||
|
||||
$contents = file_get_contents('schema.graphql');
|
||||
$schema = BuildSchema::build($contents);
|
||||
```
|
||||
|
||||
By default, such schema is created without any resolvers.
|
||||
|
||||
We have to rely on [default field resolver](../data-fetching.md#default-field-resolver) and **root value** in
|
||||
order to execute a query against this schema.
|
||||
|
||||
# Defining resolvers
|
||||
Since 0.10.0
|
||||
|
||||
In order to enable **Interfaces**, **Unions** and custom field resolvers you can pass the second argument:
|
||||
**type config decorator** to schema builder.
|
||||
|
||||
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
|
||||
<?php
|
||||
use GraphQL\Utils\BuildSchema;
|
||||
|
||||
$typeConfigDecorator = function($typeConfig, $typeDefinitionNode) {
|
||||
$name = $typeConfig['name'];
|
||||
// ... add missing options to $typeConfig based on type $name
|
||||
return $typeConfig;
|
||||
};
|
||||
|
||||
$contents = file_get_contents('schema.graphql');
|
||||
$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.
|
||||
|
||||
But parsing type definition file on each request is suboptimal, so it is recommended to cache
|
||||
intermediate parsed representation of the schema for the production environment:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Utils\BuildSchema;
|
||||
use GraphQL\Utils\AST;
|
||||
|
||||
$cacheFilename = 'cached_schema.php';
|
||||
|
||||
if (!file_exists($cacheFilename)) {
|
||||
$document = Parser::parse(file_get_contents('./schema.graphql'));
|
||||
file_put_contents($cacheFilename, "<?php\nreturn " . var_export(AST::toArray($document), true) . ";\n");
|
||||
} else {
|
||||
$document = AST::fromArray(require $cacheFilename); // fromArray() is a lazy operation as well
|
||||
}
|
||||
|
||||
$typeConfigDecorator = function () {};
|
||||
$schema = BuildSchema::build($document, $typeConfigDecorator);
|
||||
```
|
|
@ -1,17 +1,20 @@
|
|||
# Union Type Definition
|
||||
A Union is an abstract type that simply enumerates other Object Types.
|
||||
Value of Union Type is actually a value of one of included Object Types.
|
||||
The value of Union Type is actually a value of one of included Object Types.
|
||||
|
||||
In **graphql-php** union type is an instance of `GraphQL\Type\Definition\UnionType`
|
||||
(or one of it subclasses) which accepts configuration array in constructor:
|
||||
(or one of its subclasses) which accepts configuration array in a constructor:
|
||||
|
||||
```php
|
||||
<?php
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
|
||||
$searchResultType = new UnionType([
|
||||
'name' => 'SearchResult',
|
||||
'types' => [
|
||||
MyTypes::story(),
|
||||
MyTypes::user()
|
||||
];
|
||||
],
|
||||
'resolveType' => function($value) {
|
||||
if ($value->type === 'story') {
|
||||
return MyTypes::story();
|
||||
|
@ -23,14 +26,14 @@ $searchResultType = new UnionType([
|
|||
```
|
||||
|
||||
This example uses **inline** style for Union definition, but you can also use
|
||||
[inheritance](/type-system/#type-definition-styles).
|
||||
[inheritance or type language](index.md#type-definition-styles).
|
||||
|
||||
# Configuration options
|
||||
Constructor of UnionType accepts an array. Below is a full list of allowed options:
|
||||
The constructor of UnionType accepts an array. Below is a full list of allowed options:
|
||||
|
||||
Option | Type | Notes
|
||||
------ | ---- | -----
|
||||
name | `string` | **Required.** Unique name of this interface type within Schema
|
||||
types | `array` | **Required.** List of Object Types included in this Union. Note that you can't create a Union type out of Interfaces or other Unions.
|
||||
description | `string` | Plain-text description of this type for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation)
|
||||
resolveType | `callback` returning instance of `ObjectType` | **function($value, $context, GraphQL\Type\Definition\ResolveInfo $info)** Any `callable` that receives `$value` from resolver of the parent field and returns Object Type for that `$value`.
|
||||
resolveType | `callback` | **function($value, $context, [ResolveInfo](../reference.md#graphqltypedefinitionresolveinfo) $info)**<br> Receives **$value** from resolver of the parent field and returns concrete Object Type for this **$value**.
|
||||
|
|
|
@ -7,7 +7,7 @@ require_once __DIR__ . '/../../vendor/autoload.php';
|
|||
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\GraphQL;
|
||||
|
||||
try {
|
||||
|
@ -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,13 +35,15 @@ try {
|
|||
'x' => ['type' => Type::int()],
|
||||
'y' => ['type' => Type::int()],
|
||||
],
|
||||
'resolve' => function ($root, $args) {
|
||||
'resolve' => function ($calc, $args) {
|
||||
return $args['x'] + $args['y'];
|
||||
},
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// See docs on schema options:
|
||||
// http://webonyx.github.io/graphql-php/type-system/schema/#configuration-options
|
||||
$schema = new Schema([
|
||||
'query' => $queryType,
|
||||
'mutation' => $mutationType,
|
||||
|
@ -53,14 +55,15 @@ try {
|
|||
$variableValues = isset($input['variables']) ? $input['variables'] : null;
|
||||
|
||||
$rootValue = ['prefix' => 'You said: '];
|
||||
$result = GraphQL::execute($schema, $query, $rootValue, null, $variableValues);
|
||||
$result = GraphQL::executeQuery($schema, $query, $rootValue, null, $variableValues);
|
||||
$output = $result->toArray();
|
||||
} catch (\Exception $e) {
|
||||
$result = [
|
||||
$output = [
|
||||
'error' => [
|
||||
'message' => $e->getMessage()
|
||||
]
|
||||
];
|
||||
}
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
echo json_encode($result);
|
||||
echo json_encode($output);
|
||||
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
<?php
|
||||
namespace GraphQL\Examples\Blog;
|
||||
|
||||
use GraphQL\Examples\Blog\Data\DataSource;
|
||||
use GraphQL\Examples\Blog\Data\User;
|
||||
use GraphQL\Utils;
|
||||
|
||||
/**
|
||||
* Class AppContext
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
namespace GraphQL\Examples\Blog\Data;
|
||||
|
||||
|
||||
use GraphQL\Utils;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
class Comment
|
||||
{
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
namespace GraphQL\Examples\Blog\Data;
|
||||
|
||||
use GraphQL\Utils;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
class Image
|
||||
{
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
namespace GraphQL\Examples\Blog\Data;
|
||||
|
||||
use GraphQL\Utils;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
class Story
|
||||
{
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
namespace GraphQL\Examples\Blog\Data;
|
||||
|
||||
use GraphQL\Utils;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
class User
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace GraphQL\Examples\Blog\Type\Scalar;
|
|||
use GraphQL\Error\Error;
|
||||
use GraphQL\Language\AST\StringValueNode;
|
||||
use GraphQL\Type\Definition\CustomScalarType;
|
||||
use GraphQL\Utils;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
class EmailType
|
||||
{
|
||||
|
|
|
@ -5,7 +5,7 @@ use GraphQL\Error\Error;
|
|||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\StringValueNode;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use GraphQL\Utils;
|
||||
use GraphQL\Utils\Utils;
|
||||
|
||||
class UrlType extends ScalarType
|
||||
{
|
||||
|
@ -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};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
@ -6,24 +6,20 @@ require_once __DIR__ . '/../../vendor/autoload.php';
|
|||
use \GraphQL\Examples\Blog\Types;
|
||||
use \GraphQL\Examples\Blog\AppContext;
|
||||
use \GraphQL\Examples\Blog\Data\DataSource;
|
||||
use \GraphQL\Schema;
|
||||
use \GraphQL\Type\Schema;
|
||||
use \GraphQL\GraphQL;
|
||||
use \GraphQL\Type\Definition\Config;
|
||||
use \GraphQL\Error\FormattedError;
|
||||
use \GraphQL\Error\Debug;
|
||||
|
||||
// Disable default PHP error reporting - we have better one for debug mode (see bellow)
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
$debug = false;
|
||||
if (!empty($_GET['debug'])) {
|
||||
// Enable additional validation of type configs
|
||||
// (disabled by default because it is costly)
|
||||
Config::enableValidation();
|
||||
|
||||
// Catch custom errors (to report them in query results if debugging is enabled)
|
||||
$phpErrors = [];
|
||||
set_error_handler(function($severity, $message, $file, $line) use (&$phpErrors) {
|
||||
$phpErrors[] = new ErrorException($message, 0, $severity, $file, $line);
|
||||
throw new ErrorException($message, 0, $severity, $file, $line);
|
||||
});
|
||||
$debug = Debug::INCLUDE_DEBUG_MESSAGE | Debug::INCLUDE_TRACE;
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -39,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']) {
|
||||
|
@ -54,30 +51,21 @@ try {
|
|||
'query' => Types::query()
|
||||
]);
|
||||
|
||||
$result = GraphQL::execute(
|
||||
$result = GraphQL::executeQuery(
|
||||
$schema,
|
||||
$data['query'],
|
||||
null,
|
||||
$appContext,
|
||||
(array) $data['variables']
|
||||
);
|
||||
|
||||
// Add reported PHP errors to result (if any)
|
||||
if (!empty($_GET['debug']) && !empty($phpErrors)) {
|
||||
$result['extensions']['phpErrors'] = array_map(
|
||||
['GraphQL\Error\FormattedError', 'createFromPHPError'],
|
||||
$phpErrors
|
||||
);
|
||||
}
|
||||
$output = $result->toArray($debug);
|
||||
$httpStatus = 200;
|
||||
} catch (\Exception $error) {
|
||||
$httpStatus = 500;
|
||||
if (!empty($_GET['debug'])) {
|
||||
$result['extensions']['exception'] = FormattedError::createFromException($error);
|
||||
} else {
|
||||
$result['errors'] = [FormattedError::create('Unexpected Error')];
|
||||
}
|
||||
$output['errors'] = [
|
||||
FormattedError::createFromException($error, $debug)
|
||||
];
|
||||
}
|
||||
|
||||
header('Content-Type: application/json', true, $httpStatus);
|
||||
echo json_encode($result);
|
||||
echo json_encode($output);
|
||||
|
|
19
examples/02-shorthand/README.md
Normal file
19
examples/02-shorthand/README.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Parsing GraphQL IDL shorthand
|
||||
|
||||
Same as the Hello world example but shows how to build GraphQL schema from shorthand
|
||||
and wire up some resolvers
|
||||
|
||||
### Run locally
|
||||
```
|
||||
php -S localhost:8080 ./graphql.php
|
||||
```
|
||||
|
||||
### Try query
|
||||
```
|
||||
curl http://localhost:8080 -d '{"query": "query { echo(message: \"Hello World\") }" }'
|
||||
```
|
||||
|
||||
### Try mutation
|
||||
```
|
||||
curl http://localhost:8080 -d '{"query": "mutation { sum(x: 2, y: 2) }" }'
|
||||
```
|
31
examples/02-shorthand/graphql.php
Normal file
31
examples/02-shorthand/graphql.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
// Test this using following command
|
||||
// php -S localhost:8080 ./graphql.php &
|
||||
// curl http://localhost:8080 -d '{"query": "query { echo(message: \"Hello World\") }" }'
|
||||
// curl http://localhost:8080 -d '{"query": "mutation { sum(x: 2, y: 2) }" }'
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Utils\BuildSchema;
|
||||
|
||||
try {
|
||||
|
||||
$schema = BuildSchema::build(file_get_contents(__DIR__ . '/schema.graphqls'));
|
||||
$rootValue = include __DIR__ . '/rootvalue.php';
|
||||
|
||||
$rawInput = file_get_contents('php://input');
|
||||
$input = json_decode($rawInput, true);
|
||||
$query = $input['query'];
|
||||
$variableValues = isset($input['variables']) ? $input['variables'] : null;
|
||||
|
||||
$result = GraphQL::executeQuery($schema, $query, $rootValue, null, $variableValues);
|
||||
} catch (\Exception $e) {
|
||||
$result = [
|
||||
'error' => [
|
||||
'message' => $e->getMessage()
|
||||
]
|
||||
];
|
||||
}
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
echo json_encode($result);
|
||||
|
35
examples/02-shorthand/rootvalue.php
Normal file
35
examples/02-shorthand/rootvalue.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
interface Resolver {
|
||||
public function resolve($rootValue, $args, $context);
|
||||
}
|
||||
|
||||
class Addition implements Resolver
|
||||
{
|
||||
public function resolve($rootValue, $args, $context)
|
||||
{
|
||||
return $args['x'] + $args['y'];
|
||||
}
|
||||
}
|
||||
|
||||
class Echoer implements Resolver
|
||||
{
|
||||
public function resolve($rootValue, $args, $context)
|
||||
{
|
||||
return $rootValue['prefix'].$args['message'];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'sum' => function($rootValue, $args, $context) {
|
||||
$sum = new Addition();
|
||||
|
||||
return $sum->resolve($rootValue, $args, $context);
|
||||
},
|
||||
'echo' => function($rootValue, $args, $context) {
|
||||
$echo = new Echoer();
|
||||
|
||||
return $echo->resolve($rootValue, $args, $context);
|
||||
},
|
||||
'prefix' => 'You said: ',
|
||||
];
|
13
examples/02-shorthand/schema.graphqls
Normal file
13
examples/02-shorthand/schema.graphqls
Normal file
|
@ -0,0 +1,13 @@
|
|||
schema {
|
||||
query: Query
|
||||
mutation: Calc
|
||||
}
|
||||
|
||||
type Calc {
|
||||
sum(x: Int, y: Int): Int
|
||||
}
|
||||
|
||||
type Query {
|
||||
echo(message: String): String
|
||||
}
|
||||
|
19
examples/03-server/README.md
Normal file
19
examples/03-server/README.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Hello world
|
||||
Same example as 01-hello-world, but uses
|
||||
[Standard Http Server](http://webonyx.github.io/graphql-php/executing-queries/#using-server)
|
||||
instead of manual parsing of incoming data.
|
||||
|
||||
### Run locally
|
||||
```
|
||||
php -S localhost:8080 ./graphql.php
|
||||
```
|
||||
|
||||
### Try query
|
||||
```
|
||||
curl -d '{"query": "query { echo(message: \"Hello World\") }" }' -H "Content-Type: application/json" http://localhost:8080
|
||||
```
|
||||
|
||||
### Try mutation
|
||||
```
|
||||
curl -d '{"query": "mutation { sum(x: 2, y: 2) }" }' -H "Content-Type: application/json" http://localhost:8080
|
||||
```
|
61
examples/03-server/graphql.php
Normal file
61
examples/03-server/graphql.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
// Test this using following command
|
||||
// php -S localhost:8080 ./graphql.php &
|
||||
// curl http://localhost:8080 -d '{"query": "query { echo(message: \"Hello World\") }" }'
|
||||
// curl http://localhost:8080 -d '{"query": "mutation { sum(x: 2, y: 2) }" }'
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Server\StandardServer;
|
||||
|
||||
try {
|
||||
$queryType = new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => [
|
||||
'echo' => [
|
||||
'type' => Type::string(),
|
||||
'args' => [
|
||||
'message' => ['type' => Type::string()],
|
||||
],
|
||||
'resolve' => function ($rootValue, $args) {
|
||||
return $rootValue['prefix'] . $args['message'];
|
||||
}
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$mutationType = new ObjectType([
|
||||
'name' => 'Calc',
|
||||
'fields' => [
|
||||
'sum' => [
|
||||
'type' => Type::int(),
|
||||
'args' => [
|
||||
'x' => ['type' => Type::int()],
|
||||
'y' => ['type' => Type::int()],
|
||||
],
|
||||
'resolve' => function ($calc, $args) {
|
||||
return $args['x'] + $args['y'];
|
||||
},
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// See docs on schema options:
|
||||
// http://webonyx.github.io/graphql-php/type-system/schema/#configuration-options
|
||||
$schema = new Schema([
|
||||
'query' => $queryType,
|
||||
'mutation' => $mutationType,
|
||||
]);
|
||||
|
||||
// See docs on server options:
|
||||
// http://webonyx.github.io/graphql-php/executing-queries/#server-configuration-options
|
||||
$server = new StandardServer([
|
||||
'schema' => $schema
|
||||
]);
|
||||
|
||||
$server->handleRequest();
|
||||
} catch (\Exception $e) {
|
||||
StandardServer::send500Error($e);
|
||||
}
|
13
mkdocs.yml
13
mkdocs.yml
|
@ -2,7 +2,8 @@ site_name: graphql-php
|
|||
pages:
|
||||
- About: index.md
|
||||
- Getting Started: getting-started.md
|
||||
- Type System:
|
||||
- Complementary Tools: complementary-tools.md
|
||||
- Type Definitions:
|
||||
- Introduction: type-system/index.md
|
||||
- Object Types: type-system/object-types.md
|
||||
- Scalar Types: type-system/scalar-types.md
|
||||
|
@ -10,12 +11,16 @@ pages:
|
|||
- Lists and Non-Null: type-system/lists-and-nonnulls.md
|
||||
- Interfaces: type-system/interfaces.md
|
||||
- Unions: type-system/unions.md
|
||||
- Input Types: type-system/input-types.md
|
||||
- Mutations and Input Types: type-system/input-types.md
|
||||
- Directives: type-system/directives.md
|
||||
- Schema: type-system/schema.md
|
||||
- Using Type Language: type-system/type-language.md
|
||||
- Executing Queries: executing-queries.md
|
||||
- Fetching Data: data-fetching.md
|
||||
- Handling Errors: error-handling.md
|
||||
# - Best Practices: best-practices.md
|
||||
- Complementary Tools: complementary-tools.md
|
||||
# - Mutations: mutations.md
|
||||
- Security: security.md
|
||||
# - Performance tips: performance.md
|
||||
- How it works: how-it-works.md
|
||||
- Class Reference: reference.md
|
||||
theme: readthedocs
|
||||
|
|
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,58 +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) {
|
||||
$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
|
||||
{
|
||||
}
|
36
src/Error/ClientAware.php
Normal file
36
src/Error/ClientAware.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Error;
|
||||
|
||||
/**
|
||||
* This interface is used for [default error formatting](error-handling.md).
|
||||
*
|
||||
* Only errors implementing this interface (and returning true from `isClientSafe()`)
|
||||
* will be formatted with original error message.
|
||||
*
|
||||
* All other errors will be formatted with generic "Internal server error".
|
||||
*/
|
||||
interface ClientAware
|
||||
{
|
||||
/**
|
||||
* Returns true when exception message is safe to be displayed to a client.
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function isClientSafe();
|
||||
|
||||
/**
|
||||
* Returns string describing a category of the error.
|
||||
*
|
||||
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getCategory();
|
||||
}
|
16
src/Error/Debug.php
Normal file
16
src/Error/Debug.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Error;
|
||||
|
||||
/**
|
||||
* Collection of flags for [error debugging](error-handling.md#debugging-tools).
|
||||
*/
|
||||
class Debug
|
||||
{
|
||||
const INCLUDE_DEBUG_MESSAGE = 1;
|
||||
const INCLUDE_TRACE = 2;
|
||||
const RETHROW_INTERNAL_EXCEPTIONS = 4;
|
||||
const RETHROW_UNSAFE_EXCEPTIONS = 8;
|
||||
}
|
|
@ -1,21 +1,42 @@
|
|||
<?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;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Class Error
|
||||
* A GraphQLError describes an Error found during the parse, validate, or
|
||||
* Describes an Error found during the parse, validate, or
|
||||
* execute phases of performing a GraphQL operation. In addition to a message
|
||||
* and stack trace, it also includes information about the locations in a
|
||||
* GraphQL document and/or execution result that correspond to the Error.
|
||||
*
|
||||
* @package GraphQL
|
||||
* When the error was caused by an exception thrown in resolver, original exception
|
||||
* is available via `getPrevious()`.
|
||||
*
|
||||
* Also read related docs on [error handling](error-handling.md)
|
||||
*
|
||||
* Class extends standard PHP `\Exception`, so all standard methods of base `\Exception` class
|
||||
* are available in addition to those listed below.
|
||||
*/
|
||||
class Error extends \Exception implements \JsonSerializable
|
||||
class Error extends Exception implements JsonSerializable, ClientAware
|
||||
{
|
||||
const CATEGORY_GRAPHQL = 'graphql';
|
||||
const CATEGORY_INTERNAL = 'internal';
|
||||
|
||||
/**
|
||||
* A message describing the Error for debugging purposes.
|
||||
*
|
||||
|
@ -23,74 +44,128 @@ class Error extends \Exception implements \JsonSerializable
|
|||
*/
|
||||
public $message;
|
||||
|
||||
/**
|
||||
* An array of [ line => x, column => y] locations within the source GraphQL document
|
||||
* which correspond to this error.
|
||||
*
|
||||
* Errors during validation often contain multiple locations, for example to
|
||||
* point out two things with the same name. Errors during execution include a
|
||||
* single location, the field which produced the error.
|
||||
*
|
||||
* @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 */
|
||||
private $isClientSafe;
|
||||
|
||||
/** @var string */
|
||||
protected $category;
|
||||
|
||||
/** @var mixed[]|null */
|
||||
protected $extensions;
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @param Node|Node[]|Traversable|null $nodes
|
||||
* @param mixed[]|null $positions
|
||||
* @param mixed[]|null $path
|
||||
* @param Throwable $previous
|
||||
* @param mixed[] $extensions
|
||||
*/
|
||||
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)
|
||||
{
|
||||
if ($error instanceof self && $error->path) {
|
||||
return $error;
|
||||
if ($error instanceof self) {
|
||||
if ($error->path && $error->nodes) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$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) {
|
||||
$message = $error->getMessage();
|
||||
$originalError = $error;
|
||||
} else if ($error instanceof \Error) {
|
||||
$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;
|
||||
|
@ -102,14 +177,13 @@ class Error extends \Exception implements \JsonSerializable
|
|||
$source,
|
||||
$positions,
|
||||
$path,
|
||||
$originalError
|
||||
$originalError,
|
||||
$extensions
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Error $error
|
||||
* @return array
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function formatError(Error $error)
|
||||
{
|
||||
|
@ -117,25 +191,19 @@ class Error extends \Exception implements \JsonSerializable
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @param array|null $nodes
|
||||
* @param Source $source
|
||||
* @param array|null $positions
|
||||
* @param array|null $path
|
||||
* @param \Exception|\Error $previous
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function __construct($message, $nodes = null, Source $source = null, $positions = null, $path = null, $previous = null)
|
||||
public function isClientSafe()
|
||||
{
|
||||
parent::__construct($message, 0, $previous);
|
||||
return $this->isClientSafe;
|
||||
}
|
||||
|
||||
if ($nodes instanceof \Traversable) {
|
||||
$nodes = iterator_to_array($nodes);
|
||||
}
|
||||
|
||||
$this->nodes = $nodes;
|
||||
$this->source = $source;
|
||||
$this->positions = $positions;
|
||||
$this->path = $path;
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -143,45 +211,82 @@ class Error extends \Exception implements \JsonSerializable
|
|||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of locations within the source GraphQL document which correspond to this error.
|
||||
*
|
||||
* Each entry has information about `line` and `column` within source GraphQL document:
|
||||
* $location->line;
|
||||
* $location->column;
|
||||
*
|
||||
* Errors during validation often contain multiple locations, for example to
|
||||
* point out to field mentioned in multiple fragments. Errors during execution include a
|
||||
* single location, the field which produced the error.
|
||||
*
|
||||
* @return 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 = [];
|
||||
}
|
||||
|
@ -190,10 +295,41 @@ class Error extends \Exception implements \JsonSerializable
|
|||
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
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getExtensions()
|
||||
{
|
||||
return $this->extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array representation of error suitable for serialization
|
||||
*
|
||||
* @return array
|
||||
* @deprecated Use FormattedError::createFromException() instead
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function toSerializableArray()
|
||||
{
|
||||
|
@ -201,29 +337,44 @@ class Error extends \Exception implements \JsonSerializable
|
|||
'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,103 +1,367 @@
|
|||
<?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;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Class FormattedError
|
||||
* @todo move this class to Utils/ErrorUtils
|
||||
* @package GraphQL\Error
|
||||
* This class is used for [default error formatting](error-handling.md).
|
||||
* It converts PHP exceptions to [spec-compliant errors](https://facebook.github.io/graphql/#sec-Errors)
|
||||
* and provides tools for error debugging.
|
||||
*/
|
||||
class FormattedError
|
||||
{
|
||||
/**
|
||||
* @deprecated as of 8.0
|
||||
* @param $error
|
||||
* @param SourceLocation[] $locations
|
||||
* @return array
|
||||
*/
|
||||
public static function create($error, array $locations = [])
|
||||
{
|
||||
$formatted = [
|
||||
'message' => $error
|
||||
];
|
||||
|
||||
if (!empty($locations)) {
|
||||
$formatted['locations'] = array_map(function($loc) { return $loc->toArray();}, $locations);
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
/** @var string */
|
||||
private static $internalErrorMessage = 'Internal server error';
|
||||
|
||||
/**
|
||||
* @param \ErrorException $e
|
||||
* @return array
|
||||
*/
|
||||
public static function createFromPHPError(\ErrorException $e)
|
||||
{
|
||||
return [
|
||||
'message' => $e->getMessage(),
|
||||
'severity' => $e->getSeverity(),
|
||||
'trace' => self::toSafeTrace($e->getTrace())
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Exception $e
|
||||
* @return array
|
||||
*/
|
||||
public static function createFromException(\Exception $e)
|
||||
{
|
||||
return [
|
||||
'message' => $e->getMessage(),
|
||||
'trace' => self::toSafeTrace($e->getTrace())
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts error trace to serializable array
|
||||
* Set default error message for internal errors formatted using createFormattedError().
|
||||
* This value can be overridden by passing 3rd argument to `createFormattedError()`.
|
||||
*
|
||||
* @param array $trace
|
||||
* @return array
|
||||
* @param string $msg
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
private static function toSafeTrace(array $trace)
|
||||
public static function setInternalErrorMessage($msg)
|
||||
{
|
||||
// Remove invariant entries as they don't provide much value:
|
||||
if (
|
||||
isset($trace[0]['function']) && isset($trace[0]['class']) &&
|
||||
('GraphQL\Utils::invariant' === $trace[0]['class'].'::'.$trace[0]['function'])) {
|
||||
array_shift($trace);
|
||||
}
|
||||
self::$internalErrorMessage = $msg;
|
||||
}
|
||||
|
||||
// Remove root call as it's likely error handler trace:
|
||||
else if (!isset($trace[0]['file'])) {
|
||||
array_shift($trace);
|
||||
}
|
||||
|
||||
return array_map(function($err) {
|
||||
$safeErr = array_intersect_key($err, ['file' => true, 'line' => true]);
|
||||
|
||||
if (isset($err['function'])) {
|
||||
$func = $err['function'];
|
||||
$args = !empty($err['args']) ? array_map([__CLASS__, 'printVar'], $err['args']) : [];
|
||||
$funcStr = $func . '(' . implode(", ", $args) . ')';
|
||||
|
||||
if (isset($err['class'])) {
|
||||
$safeErr['call'] = $err['class'] . '::' . $funcStr;
|
||||
} else {
|
||||
$safeErr['function'] = $funcStr;
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* This method only exposes exception message when exception implements ClientAware interface
|
||||
* (or when debug flags are passed).
|
||||
*
|
||||
* For a list of available debug flags see GraphQL\Error\Debug constants.
|
||||
*
|
||||
* @param Throwable $e
|
||||
* @param bool|int $debug
|
||||
* @param string $internalErrorMessage
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @throws Throwable
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function createFromException($e, $debug = false, $internalErrorMessage = null)
|
||||
{
|
||||
Utils::invariant(
|
||||
$e instanceof Exception || $e instanceof Throwable,
|
||||
'Expected exception, got %s',
|
||||
Utils::getVariableType($e)
|
||||
);
|
||||
|
||||
$internalErrorMessage = $internalErrorMessage ?: self::$internalErrorMessage;
|
||||
|
||||
if ($e instanceof ClientAware) {
|
||||
$formattedError = [
|
||||
'message' => $e->isClientSafe() ? $e->getMessage() : $internalErrorMessage,
|
||||
'extensions' => [
|
||||
'category' => $e->getCategory(),
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$formattedError = [
|
||||
'message' => $internalErrorMessage,
|
||||
'extensions' => [
|
||||
'category' => Error::CATEGORY_INTERNAL,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if ($e instanceof Error) {
|
||||
$locations = Utils::map(
|
||||
$e->getLocations(),
|
||||
static function (SourceLocation $loc) {
|
||||
return $loc->toSerializableArray();
|
||||
}
|
||||
);
|
||||
if (! empty($locations)) {
|
||||
$formattedError['locations'] = $locations;
|
||||
}
|
||||
if (! empty($e->path)) {
|
||||
$formattedError['path'] = $e->path;
|
||||
}
|
||||
if (! empty($e->getExtensions())) {
|
||||
$formattedError['extensions'] = $e->getExtensions() + $formattedError['extensions'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($debug) {
|
||||
$formattedError = self::addDebugEntries($formattedError, $e, $debug);
|
||||
}
|
||||
|
||||
return $formattedError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorates spec-compliant $formattedError with debug entries according to $debug flags
|
||||
* (see GraphQL\Error\Debug for available flags)
|
||||
*
|
||||
* @param mixed[] $formattedError
|
||||
* @param Throwable $e
|
||||
* @param bool|int $debug
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function addDebugEntries(array $formattedError, $e, $debug)
|
||||
{
|
||||
if (! $debug) {
|
||||
return $formattedError;
|
||||
}
|
||||
|
||||
Utils::invariant(
|
||||
$e instanceof Exception || $e instanceof Throwable,
|
||||
'Expected exception, got %s',
|
||||
Utils::getVariableType($e)
|
||||
);
|
||||
|
||||
$debug = (int) $debug;
|
||||
|
||||
if ($debug & Debug::RETHROW_INTERNAL_EXCEPTIONS) {
|
||||
if (! $e instanceof Error) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $safeErr;
|
||||
}, $trace);
|
||||
if ($e->getPrevious()) {
|
||||
throw $e->getPrevious();
|
||||
}
|
||||
}
|
||||
|
||||
$isUnsafe = ! $e instanceof ClientAware || ! $e->isClientSafe();
|
||||
|
||||
if (($debug & Debug::RETHROW_UNSAFE_EXCEPTIONS) && $isUnsafe) {
|
||||
if ($e->getPrevious()) {
|
||||
throw $e->getPrevious();
|
||||
}
|
||||
}
|
||||
|
||||
if (($debug & Debug::INCLUDE_DEBUG_MESSAGE) && $isUnsafe) {
|
||||
// Displaying debugMessage as a first entry:
|
||||
$formattedError = ['debugMessage' => $e->getMessage()] + $formattedError;
|
||||
}
|
||||
|
||||
if ($debug & Debug::INCLUDE_TRACE) {
|
||||
if ($e instanceof ErrorException || $e instanceof \Error) {
|
||||
$formattedError += [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
];
|
||||
}
|
||||
|
||||
$isTrivial = $e instanceof Error && ! $e->getPrevious();
|
||||
|
||||
if (! $isTrivial) {
|
||||
$debugging = $e->getPrevious() ?: $e;
|
||||
$formattedError['trace'] = static::toSafeTrace($debugging);
|
||||
}
|
||||
}
|
||||
|
||||
return $formattedError;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $var
|
||||
* Prepares final error formatter taking in account $debug flags.
|
||||
* If initial formatter is not set, FormattedError::createFromException is used
|
||||
*
|
||||
* @param bool|int $debug
|
||||
*
|
||||
* @return callable|callable
|
||||
*/
|
||||
public static function prepareFormatter(?callable $formatter = null, $debug)
|
||||
{
|
||||
$formatter = $formatter ?: static function ($e) {
|
||||
return FormattedError::createFromException($e);
|
||||
};
|
||||
if ($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
|
||||
*/
|
||||
public static function toSafeTrace($error)
|
||||
{
|
||||
$trace = $error->getTrace();
|
||||
|
||||
if (isset($trace[0]['function']) && isset($trace[0]['class']) &&
|
||||
// Remove invariant entries as they don't provide much value:
|
||||
($trace[0]['class'] . '::' . $trace[0]['function'] === 'GraphQL\Utils\Utils::invariant')) {
|
||||
array_shift($trace);
|
||||
} elseif (! isset($trace[0]['file'])) {
|
||||
// Remove root call as it's likely error handler trace:
|
||||
array_shift($trace);
|
||||
}
|
||||
|
||||
return array_map(
|
||||
static 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['class'])) {
|
||||
$safeErr['call'] = $err['class'] . '::' . $funcStr;
|
||||
} else {
|
||||
$safeErr['function'] = $funcStr;
|
||||
}
|
||||
}
|
||||
|
||||
return $safeErr;
|
||||
},
|
||||
$trace
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $var
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function printVar($var)
|
||||
|
@ -107,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)) {
|
||||
|
@ -128,9 +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 string $error
|
||||
* @param SourceLocation[] $locations
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function create($error, array $locations = [])
|
||||
{
|
||||
$formatted = ['message' => $error];
|
||||
|
||||
if (! empty($locations)) {
|
||||
$formatted['locations'] = array_map(
|
||||
static function ($loc) {
|
||||
return $loc->toArray();
|
||||
},
|
||||
$locations
|
||||
);
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated as of v0.10.0, use general purpose method createFromException() instead
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public static function createFromPHPError(ErrorException $e)
|
||||
{
|
||||
return [
|
||||
'message' => $e->getMessage(),
|
||||
'severity' => $e->getSeverity(),
|
||||
'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,29 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Error;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class UserError
|
||||
*
|
||||
* Note:
|
||||
* Error that can be display safely to client...
|
||||
*
|
||||
* @package GraphQL\Error
|
||||
* Error caused by actions of GraphQL clients. Can be safely displayed to a client...
|
||||
*/
|
||||
class UserError extends InvariantViolation
|
||||
class UserError extends RuntimeException implements ClientAware
|
||||
{
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isClientSafe()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
return 'user';
|
||||
}
|
||||
}
|
||||
|
|
117
src/Error/Warning.php
Normal file
117
src/Error/Warning.php
Normal file
|
@ -0,0 +1,117 @@
|
|||
<?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.
|
||||
*
|
||||
* Warnings can be suppressed (individually or all) if required.
|
||||
* Also it is possible to override warning handler (which is **trigger_error()** by default)
|
||||
*/
|
||||
final class Warning
|
||||
{
|
||||
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;
|
||||
|
||||
/** @var int */
|
||||
private static $enableWarnings = self::ALL;
|
||||
|
||||
/** @var mixed[] */
|
||||
private static $warned = [];
|
||||
|
||||
/** @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
|
||||
*/
|
||||
public static function setWarningHandler(?callable $warningHandler = null) : void
|
||||
{
|
||||
self::$warningHandler = $warningHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress warning by id (has no effect when custom warning handler is set)
|
||||
*
|
||||
* Usage example:
|
||||
* Warning::suppress(Warning::WARNING_NOT_A_TYPE)
|
||||
*
|
||||
* When passing true - suppresses all warnings.
|
||||
*
|
||||
* @param bool|int $suppress
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function suppress($suppress = true) : void
|
||||
{
|
||||
if ($suppress === true) {
|
||||
self::$enableWarnings = 0;
|
||||
} elseif ($suppress === false) {
|
||||
self::$enableWarnings = self::ALL;
|
||||
} elseif (is_int($suppress)) {
|
||||
self::$enableWarnings &= ~$suppress;
|
||||
} else {
|
||||
throw InvalidArgument::fromExpectedTypeAndArgument('bool|int', $suppress);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-enable previously suppressed warning by id
|
||||
*
|
||||
* Usage example:
|
||||
* Warning::suppress(Warning::WARNING_NOT_A_TYPE)
|
||||
*
|
||||
* When passing true - re-enables all warnings.
|
||||
*
|
||||
* @param bool|int $enable
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function enable($enable = true) : void
|
||||
{
|
||||
if ($enable === true) {
|
||||
self::$enableWarnings = self::ALL;
|
||||
} elseif ($enable === false) {
|
||||
self::$enableWarnings = 0;
|
||||
} elseif (is_int($enable)) {
|
||||
self::$enableWarnings |= $enable;
|
||||
} else {
|
||||
throw InvalidArgument::fromExpectedTypeAndArgument('bool|int', $enable);
|
||||
}
|
||||
}
|
||||
|
||||
public static function warnOnce(string $errorMessage, int $warningId, ?int $messageLevel = null) : void
|
||||
{
|
||||
if (self::$warningHandler !== null) {
|
||||
$fn = self::$warningHandler;
|
||||
$fn($errorMessage, $warningId);
|
||||
} elseif ((self::$enableWarnings & $warningId) > 0 && ! isset(self::$warned[$warningId])) {
|
||||
self::$warned[$warningId] = true;
|
||||
trigger_error($errorMessage, $messageLevel ?: E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
public static function warn(string $errorMessage, int $warningId, ?int $messageLevel = null) : void
|
||||
{
|
||||
if (self::$warningHandler !== null) {
|
||||
$fn = self::$warningHandler;
|
||||
$fn($errorMessage, $warningId);
|
||||
} 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,68 +1,78 @@
|
|||
<?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\Schema;
|
||||
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 array
|
||||
*/
|
||||
/** @var callable */
|
||||
public $fieldResolver;
|
||||
|
||||
/** @var Error[] */
|
||||
public $errors;
|
||||
|
||||
public function __construct($schema, $fragments, $root, $contextValue, $operation, $variables, $errors)
|
||||
{
|
||||
$this->schema = $schema;
|
||||
$this->fragments = $fragments;
|
||||
$this->rootValue = $root;
|
||||
$this->contextValue = $contextValue;
|
||||
$this->operation = $operation;
|
||||
$this->variableValues = $variables;
|
||||
$this->errors = $errors ?: [];
|
||||
/** @var PromiseAdapter */
|
||||
public $promiseAdapter;
|
||||
|
||||
public function __construct(
|
||||
$schema,
|
||||
$fragments,
|
||||
$rootValue,
|
||||
$contextValue,
|
||||
$operation,
|
||||
$variableValues,
|
||||
$errors,
|
||||
$fieldResolver,
|
||||
$promiseAdapter
|
||||
) {
|
||||
$this->schema = $schema;
|
||||
$this->fragments = $fragments;
|
||||
$this->rootValue = $rootValue;
|
||||
$this->contextValue = $contextValue;
|
||||
$this->operation = $operation;
|
||||
$this->variableValues = $variableValues;
|
||||
$this->errors = $errors ?: [];
|
||||
$this->fieldResolver = $fieldResolver;
|
||||
$this->promiseAdapter = $promiseAdapter;
|
||||
}
|
||||
|
||||
public function addError(Error $error)
|
||||
{
|
||||
$this->errors[] = $error;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,76 +1,162 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Executor;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\FormattedError;
|
||||
use JsonSerializable;
|
||||
use function array_map;
|
||||
|
||||
class ExecutionResult implements \JsonSerializable
|
||||
/**
|
||||
* Returned after [query execution](executing-queries.md).
|
||||
* Represents both - result of successful execution and of a failed one
|
||||
* (with errors collected in `errors` prop)
|
||||
*
|
||||
* Could be converted to [spec-compliant](https://facebook.github.io/graphql/#sec-Response-Format)
|
||||
* serializable array using `toArray()`
|
||||
*/
|
||||
class ExecutionResult implements JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
* Data collected from resolvers during query execution
|
||||
*
|
||||
* @api
|
||||
* @var mixed[]
|
||||
*/
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* Errors registered during query execution.
|
||||
*
|
||||
* If an error was caused by exception thrown in resolver, $error->getPrevious() would
|
||||
* contain original exception.
|
||||
*
|
||||
* @api
|
||||
* @var Error[]
|
||||
*/
|
||||
public $errors;
|
||||
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
* User-defined serializable array of extensions included in serialized result.
|
||||
* Conforms to
|
||||
*
|
||||
* @api
|
||||
* @var mixed[]
|
||||
*/
|
||||
public $extensions;
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
private $errorFormatter = ['GraphQL\Error\Error', 'formatError'];
|
||||
/** @var callable */
|
||||
private $errorFormatter;
|
||||
|
||||
/** @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $errorFormatter
|
||||
* @return $this
|
||||
* Define custom error formatting (must conform to http://facebook.github.io/graphql/#sec-Errors)
|
||||
*
|
||||
* Expected signature is: function (GraphQL\Error\Error $error): array
|
||||
*
|
||||
* Default formatter is "GraphQL\Error\FormattedError::createFromException"
|
||||
*
|
||||
* Expected returned value must be an array:
|
||||
* array(
|
||||
* 'message' => 'errorMessage',
|
||||
* // ... other keys
|
||||
* );
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function setErrorFormatter(callable $errorFormatter)
|
||||
{
|
||||
$this->errorFormatter = $errorFormatter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* Define custom logic for error handling (filtering, logging, etc).
|
||||
*
|
||||
* Expected handler signature is: function (array $errors, callable $formatter): array
|
||||
*
|
||||
* Default handler is:
|
||||
* function (array $errors, callable $formatter) {
|
||||
* return array_map($formatter, $errors);
|
||||
* }
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function toArray()
|
||||
public function setErrorsHandler(callable $handler)
|
||||
{
|
||||
$result = [];
|
||||
$this->errorsHandler = $handler;
|
||||
|
||||
if (null !== $this->data) {
|
||||
$result['data'] = $this->data;
|
||||
}
|
||||
|
||||
if (!empty($this->errors)) {
|
||||
$result['errors'] = array_map($this->errorFormatter, $this->errors);
|
||||
}
|
||||
|
||||
if (!empty($this->extensions)) {
|
||||
$result['extensions'] = (array) $this->extensions;
|
||||
}
|
||||
|
||||
return $result;
|
||||
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.
|
||||
*
|
||||
* If debug argument is passed, output of error formatter is enriched which debugging information
|
||||
* ("debugMessage", "trace" keys depending on flags).
|
||||
*
|
||||
* $debug argument must be either bool (only adds "debugMessage" to result) or sum of flags from
|
||||
* GraphQL\Error\Debug
|
||||
*
|
||||
* @param bool|int $debug
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function toArray($debug = false)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
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 ($this->data !== null) {
|
||||
$result['data'] = $this->data;
|
||||
}
|
||||
|
||||
if (! empty($this->extensions)) {
|
||||
$result['extensions'] = $this->extensions;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
|
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,11 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Executor\Promise\Adapter;
|
||||
|
||||
use GraphQL\Executor\Promise\Promise;
|
||||
use GraphQL\Executor\Promise\PromiseAdapter;
|
||||
use GraphQL\Utils;
|
||||
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,16 +57,18 @@ class ReactPromiseAdapter implements PromiseAdapter
|
|||
*/
|
||||
public function createFulfilled($value = null)
|
||||
{
|
||||
$promise = \React\Promise\resolve($value);
|
||||
$promise = resolve($value);
|
||||
|
||||
return new Promise($promise, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function createRejected(\Exception $reason)
|
||||
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,113 +1,164 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Executor\Promise\Adapter;
|
||||
|
||||
use GraphQL\Utils;
|
||||
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(\Exception $reason)
|
||||
{
|
||||
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) {
|
||||
|
@ -116,35 +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);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
$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;
|
||||
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,10 +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) {
|
||||
$promise->reject($e);
|
||||
}
|
||||
|
||||
|
@ -71,15 +84,17 @@ class SyncPromiseAdapter implements PromiseAdapter
|
|||
public function createFulfilled($value = null)
|
||||
{
|
||||
$promise = new SyncPromise();
|
||||
|
||||
return new Promise($promise->resolve($value), $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function createRejected(\Exception $reason)
|
||||
public function createRejected($reason)
|
||||
{
|
||||
$promise = new SyncPromise();
|
||||
|
||||
return new Promise($promise->reject($reason), $this);
|
||||
}
|
||||
|
||||
|
@ -90,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']
|
||||
);
|
||||
|
@ -115,26 +132,27 @@ 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)
|
||||
{
|
||||
$dfdQueue = Deferred::getQueue();
|
||||
$this->beforeWait($promise);
|
||||
$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();
|
||||
$this->onWait($promise);
|
||||
}
|
||||
|
||||
/** @var SyncPromise $syncPromise */
|
||||
|
@ -142,10 +160,26 @@ 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
|
||||
*/
|
||||
protected function beforeWait(Promise $promise)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute while running promise completion
|
||||
*/
|
||||
protected function onWait(Promise $promise)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,38 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Executor\Promise;
|
||||
|
||||
use GraphQL\Utils;
|
||||
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,42 +1,57 @@
|
|||
<?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))
|
||||
*/
|
||||
interface PromiseAdapter
|
||||
{
|
||||
/**
|
||||
* Return true if value is promise of underlying system
|
||||
* Return true if the value is a promise or a deferred of the underlying platform
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function isThenable($value);
|
||||
|
||||
/**
|
||||
* Converts thenable of underlying system into Promise instance
|
||||
* Converts thenable of the underlying platform into GraphQL\Executor\Promise\Promise instance
|
||||
*
|
||||
* @param object $thenable
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function convertThenable($thenable);
|
||||
|
||||
/**
|
||||
* Accepts our Promise wrapper, extracts adopted promise out of it and executes actual `then` logic described
|
||||
* in Promises/A+ specs. Then returns new wrapped Promise instance.
|
||||
*
|
||||
* @param Promise $promise
|
||||
* @param callable|null $onFulfilled
|
||||
* @param callable|null $onRejected
|
||||
* in Promises/A+ specs. Then returns new wrapped instance of GraphQL\Executor\Promise\Promise.
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @param callable $resolver
|
||||
|
||||
* Expected resolver signature:
|
||||
* function(callable $resolve, callable $reject)
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function create(callable $resolver);
|
||||
|
||||
|
@ -46,6 +61,8 @@ interface PromiseAdapter
|
|||
* @param mixed $value
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function createFulfilled($value = null);
|
||||
|
||||
|
@ -53,19 +70,23 @@ interface PromiseAdapter
|
|||
* Creates a rejected promise for a reason if the reason is not a promise. If
|
||||
* the provided reason is a promise, then it is returned as-is.
|
||||
*
|
||||
* @param mixed $reason
|
||||
* @param Throwable $reason
|
||||
*
|
||||
* @return Promise
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function createRejected(\Exception $reason);
|
||||
public function createRejected($reason);
|
||||
|
||||
/**
|
||||
* Given an array of promises (or values), returns a promise that is fulfilled when all the
|
||||
* items in the array are fulfilled.
|
||||
*
|
||||
* @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,28 +1,38 @@
|
|||
<?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;
|
||||
use GraphQL\Language\AST\FieldDefinitionNode;
|
||||
use GraphQL\Language\AST\FieldNode;
|
||||
use GraphQL\Language\AST\NullValueNode;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\InlineFragmentNode;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\NodeList;
|
||||
use GraphQL\Language\AST\ValueNode;
|
||||
use GraphQL\Language\AST\VariableNode;
|
||||
use GraphQL\Language\AST\VariableDefinitionNode;
|
||||
use GraphQL\Language\AST\VariableNode;
|
||||
use GraphQL\Language\Printer;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
use GraphQL\Type\Definition\LeafType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Utils;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
use GraphQL\Type\Schema;
|
||||
use GraphQL\Utils\AST;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Utils\Utils;
|
||||
use GraphQL\Utils\Value;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
use function array_key_exists;
|
||||
use function array_map;
|
||||
use function sprintf;
|
||||
|
||||
class Values
|
||||
{
|
||||
|
@ -31,57 +41,108 @@ 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 = Utils\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)
|
||||
);
|
||||
|
||||
if (!array_key_exists($varName, $inputs)) {
|
||||
$defaultValue = $definitionNode->defaultValue;
|
||||
if ($defaultValue) {
|
||||
$coercedValues[$varName] = Utils\AST::valueFromAST($defaultValue, $varType);
|
||||
}
|
||||
if ($varType instanceof NonNull) {
|
||||
throw new Error(
|
||||
'Variable "$'.$varName .'" of required type ' .
|
||||
'"'. Utils::printSafe($varType) . '" was not provided.',
|
||||
[$definitionNode]
|
||||
);
|
||||
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 {
|
||||
$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;
|
||||
$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]
|
||||
);
|
||||
}
|
||||
}
|
||||
return $coercedValues;
|
||||
|
||||
if (! empty($errors)) {
|
||||
return [$errors, null];
|
||||
}
|
||||
|
||||
return [null, $coercedValues];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares an object map of argument values given a directive definition
|
||||
* and a AST node which may contain directives. Optionally also accepts a map
|
||||
* of variable values.
|
||||
*
|
||||
* If the directive does not exist on the node, returns undefined.
|
||||
*
|
||||
* @param FragmentSpreadNode|FieldNode|InlineFragmentNode|EnumValueDefinitionNode|FieldDefinitionNode $node
|
||||
* @param mixed[]|null $variableValues
|
||||
*
|
||||
* @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,
|
||||
static function (DirectiveNode $directive) use ($directiveDef) {
|
||||
return $directive->name->value === $directiveDef->name;
|
||||
}
|
||||
);
|
||||
|
||||
if ($directiveNode !== null) {
|
||||
return self::getArgumentValues($directiveDef, $directiveNode, $variableValues);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -89,253 +150,130 @@ class Values
|
|||
* definitions and list of argument AST nodes.
|
||||
*
|
||||
* @param FieldDefinition|Directive $def
|
||||
* @param FieldNode|\GraphQL\Language\AST\DirectiveNode $node
|
||||
* @param $variableValues
|
||||
* @return array
|
||||
* @param FieldNode|DirectiveNode $node
|
||||
* @param mixed[] $variableValues
|
||||
*
|
||||
* @return mixed[]
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public static function getArgumentValues($def, $node, $variableValues = null)
|
||||
{
|
||||
$argDefs = $def->args;
|
||||
$argNodes = $node->arguments;
|
||||
|
||||
if (!$argDefs || null === $argNodes) {
|
||||
if (empty($def->args)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$coercedValues = [];
|
||||
$undefined = Utils::undefined();
|
||||
$argumentNodes = $node->arguments;
|
||||
if (empty($argumentNodes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var ArgumentNode[] $argNodeMap */
|
||||
$argNodeMap = $argNodes ? Utils::keyMap($argNodes, function (ArgumentNode $arg) {
|
||||
return $arg->name->value;
|
||||
}) : [];
|
||||
$argumentValueMap = [];
|
||||
foreach ($argumentNodes as $argumentNode) {
|
||||
$argumentValueMap[$argumentNode->name->value] = $argumentNode->value;
|
||||
}
|
||||
|
||||
foreach ($argDefs as $argDef) {
|
||||
$name = $argDef->name;
|
||||
$argType = $argDef->getType();
|
||||
$argumentNode = isset($argNodeMap[$name]) ? $argNodeMap[$name] : null;
|
||||
return static::getArgumentValuesForMap($def, $argumentValueMap, $variableValues, $node);
|
||||
}
|
||||
|
||||
if (!$argumentNode) {
|
||||
if ($argDef->defaultValueExists()) {
|
||||
$coercedValues[$name] = $argDef->defaultValue;
|
||||
} else if ($argType instanceof NonNull) {
|
||||
/**
|
||||
* @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.',
|
||||
[$node]
|
||||
$referenceNode
|
||||
);
|
||||
}
|
||||
} else if ($argumentNode->value instanceof VariableNode) {
|
||||
$variableName = $argumentNode->value->name->value;
|
||||
} elseif ($argumentValueNode instanceof VariableNode) {
|
||||
$variableName = $argumentValueNode->name->value;
|
||||
|
||||
if ($variableValues && array_key_exists($variableName, $variableValues)) {
|
||||
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];
|
||||
} else if ($argDef->defaultValueExists()) {
|
||||
$coercedValues[$name] = $argDef->defaultValue;
|
||||
} else if ($argType instanceof NonNull) {
|
||||
} 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.',
|
||||
[ $argumentNode->value ]
|
||||
[$argumentValueNode]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$valueNode = $argumentNode->value;
|
||||
$coercedValue = Utils\AST::valueFromAST($valueNode, $argType, $variableValues);
|
||||
if ($coercedValue === $undefined) {
|
||||
$errors = DocumentValidator::isValidLiteralValue($argType, $valueNode);
|
||||
$message = !empty($errors) ? ("\n" . implode("\n", $errors)) : '';
|
||||
$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 . '" got invalid value ' . Printer::doPrint($valueNode) . '.' . $message,
|
||||
[ $argumentNode->value ]
|
||||
'Argument "' . $name . '" has invalid value ' . Printer::doPrint($valueNode) . '.',
|
||||
[$argumentValueNode]
|
||||
);
|
||||
}
|
||||
$coercedValues[$name] = $coercedValue;
|
||||
}
|
||||
}
|
||||
|
||||
return $coercedValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated as of 8.0 (Moved to Utils\AST::valueFromAST)
|
||||
* @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 Utils\AST::valueFromAST($valueNode, $type, $variables);
|
||||
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[]
|
||||
*/
|
||||
private static function isValidPHPValue($value, InputType $type)
|
||||
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) {
|
||||
// Scalar/Enum input checks to ensure the type can parse the value to
|
||||
// a non-null value.
|
||||
$parseResult = $type->parseValue($value);
|
||||
if (null === $parseResult) {
|
||||
$v = json_encode($value);
|
||||
return [
|
||||
"Expected type \"{$type->name}\", found $v."
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
354
src/GraphQL.php
354
src/GraphQL.php
|
@ -1,99 +1,351 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL;
|
||||
|
||||
use GraphQL\Error\Error;
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use GraphQL\Executor\ExecutionResult;
|
||||
use GraphQL\Executor\Executor;
|
||||
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
|
||||
use GraphQL\Executor\Promise\Promise;
|
||||
use GraphQL\Executor\Promise\PromiseAdapter;
|
||||
use GraphQL\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\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.
|
||||
* See [related documentation](executing-queries.md).
|
||||
*/
|
||||
class GraphQL
|
||||
{
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @param string|DocumentNode $requestString
|
||||
* @param mixed $rootValue
|
||||
* @param array|null $variableValues
|
||||
* @param string|null $operationName
|
||||
* @return Promise|array
|
||||
* Executes graphql query.
|
||||
*
|
||||
* More sophisticated GraphQL servers, such as those which persist queries,
|
||||
* may wish to separate the validation and execution phases to a static time
|
||||
* tooling step, and a server runtime step.
|
||||
*
|
||||
* Available options:
|
||||
*
|
||||
* schema:
|
||||
* The GraphQL type system to use when validating and executing a query.
|
||||
* source:
|
||||
* A GraphQL language formatted string representing the requested operation.
|
||||
* rootValue:
|
||||
* The value provided as the first argument to resolver functions on the top
|
||||
* level type (e.g. the query object type).
|
||||
* context:
|
||||
* The value provided as the third argument to all resolvers.
|
||||
* Use this to pass current session, user data, etc
|
||||
* variableValues:
|
||||
* A mapping of variable name to runtime value to use for all variables
|
||||
* defined in the requestString.
|
||||
* operationName:
|
||||
* The name of the operation to use if requestString contains multiple
|
||||
* possible operations. Can be omitted if requestString contains only
|
||||
* one operation.
|
||||
* fieldResolver:
|
||||
* A resolver function to use when one is not provided by the schema.
|
||||
* If not provided, the default field resolver is used (which looks for a
|
||||
* value on the source value with the field's name).
|
||||
* validationRules:
|
||||
* A set of rules for query validation step. Default value is all available rules.
|
||||
* Empty array would allow to skip query validation (may be convenient for persisted
|
||||
* queries which are validated before persisting and assumed valid during execution)
|
||||
*
|
||||
* @param string|DocumentNode $source
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $context
|
||||
* @param mixed[]|null $variableValues
|
||||
* @param ValidationRule[] $validationRules
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function execute(Schema $schema, $requestString, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null)
|
||||
{
|
||||
$result = self::executeAndReturnResult($schema, $requestString, $rootValue, $contextValue, $variableValues, $operationName);
|
||||
public static function executeQuery(
|
||||
SchemaType $schema,
|
||||
$source,
|
||||
$rootValue = null,
|
||||
$context = null,
|
||||
$variableValues = null,
|
||||
?string $operationName = null,
|
||||
?callable $fieldResolver = null,
|
||||
?array $validationRules = null
|
||||
) : ExecutionResult {
|
||||
$promiseAdapter = new SyncPromiseAdapter();
|
||||
|
||||
if ($result instanceof ExecutionResult) {
|
||||
return $result->toArray();
|
||||
$promise = self::promiseToExecute(
|
||||
$promiseAdapter,
|
||||
$schema,
|
||||
$source,
|
||||
$rootValue,
|
||||
$context,
|
||||
$variableValues,
|
||||
$operationName,
|
||||
$fieldResolver,
|
||||
$validationRules
|
||||
);
|
||||
|
||||
return $promiseAdapter->wait($promise);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as executeQuery(), but requires PromiseAdapter and always returns a Promise.
|
||||
* Useful for Async PHP platforms.
|
||||
*
|
||||
* @param string|DocumentNode $source
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $context
|
||||
* @param mixed[]|null $variableValues
|
||||
* @param ValidationRule[]|null $validationRules
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function promiseToExecute(
|
||||
PromiseAdapter $promiseAdapter,
|
||||
SchemaType $schema,
|
||||
$source,
|
||||
$rootValue = null,
|
||||
$context = null,
|
||||
$variableValues = null,
|
||||
?string $operationName = null,
|
||||
?callable $fieldResolver = null,
|
||||
?array $validationRules = null
|
||||
) : Promise {
|
||||
try {
|
||||
if ($source instanceof DocumentNode) {
|
||||
$documentNode = $source;
|
||||
} else {
|
||||
$documentNode = Parser::parse(new Source($source ?: '', 'GraphQL'));
|
||||
}
|
||||
|
||||
// FIXME
|
||||
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)) {
|
||||
return $promiseAdapter->createFulfilled(
|
||||
new ExecutionResult(null, $validationErrors)
|
||||
);
|
||||
}
|
||||
|
||||
return Executor::promiseToExecute(
|
||||
$promiseAdapter,
|
||||
$schema,
|
||||
$documentNode,
|
||||
$rootValue,
|
||||
$context,
|
||||
$variableValues,
|
||||
$operationName,
|
||||
$fieldResolver
|
||||
);
|
||||
} catch (Error $e) {
|
||||
return $promiseAdapter->createFulfilled(
|
||||
new ExecutionResult(null, [$e])
|
||||
);
|
||||
}
|
||||
if ($result instanceof Promise) {
|
||||
return $result->then(function(ExecutionResult $executionResult) {
|
||||
return $executionResult->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use executeQuery()->toArray() instead
|
||||
*
|
||||
* @param string|DocumentNode $source
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $contextValue
|
||||
* @param mixed[]|null $variableValues
|
||||
*
|
||||
* @return Promise|mixed[]
|
||||
*/
|
||||
public static function execute(
|
||||
SchemaType $schema,
|
||||
$source,
|
||||
$rootValue = null,
|
||||
$contextValue = null,
|
||||
$variableValues = null,
|
||||
?string $operationName = null
|
||||
) {
|
||||
trigger_error(
|
||||
__METHOD__ . ' is deprecated, use GraphQL::executeQuery()->toArray() as a quick replacement',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
|
||||
$promiseAdapter = Executor::getPromiseAdapter();
|
||||
$result = self::promiseToExecute(
|
||||
$promiseAdapter,
|
||||
$schema,
|
||||
$source,
|
||||
$rootValue,
|
||||
$contextValue,
|
||||
$variableValues,
|
||||
$operationName
|
||||
);
|
||||
|
||||
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
||||
$result = $promiseAdapter->wait($result)->toArray();
|
||||
} else {
|
||||
$result = $result->then(static function (ExecutionResult $r) {
|
||||
return $r->toArray();
|
||||
});
|
||||
}
|
||||
throw new InvariantViolation("Unexpected execution result");
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @param string|DocumentNode $requestString
|
||||
* @param mixed $rootValue
|
||||
* @param array|null $variableValues
|
||||
* @param string|null $operationName
|
||||
* @deprecated renamed to executeQuery()
|
||||
*
|
||||
* @param string|DocumentNode $source
|
||||
* @param mixed $rootValue
|
||||
* @param mixed $contextValue
|
||||
* @param mixed[]|null $variableValues
|
||||
*
|
||||
* @return ExecutionResult|Promise
|
||||
*/
|
||||
public static function executeAndReturnResult(Schema $schema, $requestString, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null)
|
||||
{
|
||||
try {
|
||||
if ($requestString instanceof DocumentNode) {
|
||||
$documentNode = $requestString;
|
||||
} else {
|
||||
$source = new Source($requestString ?: '', 'GraphQL request');
|
||||
$documentNode = Parser::parse($source);
|
||||
}
|
||||
public static function executeAndReturnResult(
|
||||
SchemaType $schema,
|
||||
$source,
|
||||
$rootValue = null,
|
||||
$contextValue = null,
|
||||
$variableValues = null,
|
||||
?string $operationName = null
|
||||
) {
|
||||
trigger_error(
|
||||
__METHOD__ . ' is deprecated, use GraphQL::executeQuery() as a quick replacement',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
|
||||
/** @var QueryComplexity $queryComplexity */
|
||||
$queryComplexity = DocumentValidator::getRule('QueryComplexity');
|
||||
$queryComplexity->setRawVariableValues($variableValues);
|
||||
$promiseAdapter = Executor::getPromiseAdapter();
|
||||
$result = self::promiseToExecute(
|
||||
$promiseAdapter,
|
||||
$schema,
|
||||
$source,
|
||||
$rootValue,
|
||||
$contextValue,
|
||||
$variableValues,
|
||||
$operationName
|
||||
);
|
||||
|
||||
$validationErrors = DocumentValidator::validate($schema, $documentNode);
|
||||
|
||||
if (!empty($validationErrors)) {
|
||||
return new ExecutionResult(null, $validationErrors);
|
||||
} else {
|
||||
return Executor::execute($schema, $documentNode, $rootValue, $contextValue, $variableValues, $operationName);
|
||||
}
|
||||
} catch (Error $e) {
|
||||
return new ExecutionResult(null, [$e]);
|
||||
if ($promiseAdapter instanceof SyncPromiseAdapter) {
|
||||
$result = $promiseAdapter->wait($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* Returns directives defined in GraphQL spec
|
||||
*
|
||||
* @return Directive[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function getInternalDirectives()
|
||||
public static function getStandardDirectives() : array
|
||||
{
|
||||
return array_values(Directive::getInternalDirectives());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $fn
|
||||
* Returns types defined in GraphQL spec
|
||||
*
|
||||
* @return Type[]
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function setDefaultFieldResolver(callable $fn)
|
||||
public static function getStandardTypes() : array
|
||||
{
|
||||
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
|
||||
*/
|
||||
public static function getStandardValidationRules() : array
|
||||
{
|
||||
return array_values(DocumentValidator::defaultRules());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default resolver implementation
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
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() : 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;
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue