Compare commits

...

966 commits

Author SHA1 Message Date
Vladimir Razuvaev
b72ba3c93a
Merge pull request #518 from simPod/fix-internal-directives
Fix internal directives
2019-07-13 21:12:53 +07:00
Simon Podlipsky
1ac5af1d8b Fix internal directives 2019-07-12 21:01:10 +03:00
Vladimir Razuvaev
a01b089058
Merge pull request #517 from simPod/upgrade-phpstan
Upgrade PHPStan to 0.11.12
2019-07-12 16:35:34 +07:00
Vladimir Razuvaev
4401f4dd18
Merge pull request #504 from simPod/fielddef-types
Add FieldDefinition return type
2019-07-12 16:32:45 +07:00
Vladimir Razuvaev
005b1a38c5
Merge pull request #502 from simPod/drop-deprecated-schema
Drop deprecated GraphQL\Schema
2019-07-12 16:31:50 +07:00
Vladimir Razuvaev
974258b352
Merge pull request #503 from simPod/fix-reference-executor
Fix ReferenceExecutor
2019-07-12 16:31:29 +07:00
Simon Podlipsky
a502c33254 Upgrade PHPStan to 0.11.12 2019-07-10 18:05:47 +03:00
Vladimir Razuvaev
22a0da9b98
Merge pull request #505 from spawnia/root-value-consistent-naming
Consistently name the first resolver argument and the root value
2019-07-03 17:33:20 +07:00
Benedikt Franke
8c4e7b178d Fix up some overly eager renamings in the docs 2019-07-01 12:17:04 +02:00
Benedikt Franke
3b33167c87 Rename $rootValue where applicable 2019-07-01 12:12:01 +02:00
Benedikt Franke
d037ab7ec3 Merge branch 'master' into root-value-consistent-naming
# Conflicts:
#	docs/data-fetching.md
#	src/Executor/Executor.php
#	tests/Executor/DeferredFieldsTest.php
2019-07-01 08:23:00 +02:00
Vladimir Razuvaev
54064b37b3
Merge pull request #511 from spawnia/scalar-coercion
Resolve todo in Boolean coercion, add explanation, update test names to match reference implementation
2019-07-01 13:03:03 +07:00
Vladimir Razuvaev
f7443b6f0c
Merge pull request #510 from spawnia/split-into-multiline
Split some long lines into multiples
2019-07-01 12:56:45 +07:00
Vladimir Razuvaev
9704baf422
Merge pull request #508 from spawnia/consistency-and-clarity
Nonfunctional changes to improve consistency and clarity
2019-07-01 12:52:21 +07:00
Vladimir Razuvaev
ed1d835bd5
Merge pull request #507 from spawnia/resolver-arguments-order
Fix misleading comments and docs about resolver arguments: Context is 3rd, ResolveInfo is 4th
2019-07-01 12:50:01 +07:00
Vladimir Razuvaev
3b27abafca
Merge pull request #506 from spawnia/multiline-ternary
Spread ternary expressions across multiple lines
2019-07-01 12:49:03 +07:00
spawnia
c069d20ca7 And a few more in tests 2019-06-30 23:09:32 +02:00
spawnia
19a37609f4 Add a few more 2019-06-30 23:08:24 +02:00
spawnia
99453076b5 Remove &$ in phpdoc 2019-06-30 20:57:08 +02:00
spawnia
24f236403a More consistent naming 2019-06-30 20:54:56 +02:00
spawnia
8da3043702 Rename some test variable names 2019-06-23 21:26:38 +02:00
spawnia
a222cc9137 Reword a comment 2019-06-23 21:08:49 +02:00
spawnia
e704f8cc5c Split some long lines into multiples 2019-06-23 21:05:58 +02:00
spawnia
a34bb68d65 Resolve todo in Boolean coercion, add explanation, update test names to match reference implementation 2019-06-23 21:03:17 +02:00
spawnia
218e02a88c Revert composer.json changes 2019-06-23 18:46:27 +02:00
spawnia
9ca7bb6ea1 Expand one letter variable names 2019-06-23 18:42:25 +02:00
spawnia
65e4488ce8 Reformat some comments 2019-06-23 18:41:47 +02:00
spawnia
bc66034f40 Rename parameters and private fields to match what they contain 2019-06-23 18:40:56 +02:00
spawnia
65a3a8d13e Add missing renaming in ReferenceExecutor 2019-06-23 18:36:07 +02:00
spawnia
8381f67bd8 Add one more 2019-06-23 18:34:19 +02:00
spawnia
91b72f145d Context is the 3rd, ResolveInfo the 4th resolver argument 2019-06-23 18:25:02 +02:00
spawnia
6e91e2181c Spread ternary expressions across multiple lines 2019-06-23 18:04:30 +02:00
spawnia
03c33c9dc2 Consistently name the $rootValue argument 2019-06-23 17:30:57 +02:00
Vladimir Razuvaev
93ccd7351d Array in variables in place of object shouldn't cause fatal error (fixes #467) 2019-06-19 19:29:03 +07:00
Vladimir Razuvaev
ed1746e800 Error handling and schema validation improvements (#404) 2019-06-19 18:59:51 +07:00
Vladimir Razuvaev
84a52c6c76 Provide a path with a correct list index to resolveType callback of the union and interface types (fixes #396) 2019-06-19 17:11:24 +07:00
Vladimir Razuvaev
261f8f5ebd AST: Replaced array with NodeList where missing 2019-06-19 15:52:16 +07:00
Vladimir Razuvaev
d1d4455eaa Fixed warning added by one of the previous commits 2019-06-19 15:49:22 +07:00
Vladimir Razuvaev
d5fbf1b29f Merge branch 'master' of https://github.com/webonyx/graphql-php 2019-06-19 15:09:05 +07:00
Vladimir Razuvaev
c336d01bd2 Added .idea to .gitignore 2019-06-19 15:08:51 +07:00
Vladimir Razuvaev
752010b341 Documentation fix (#499) 2019-06-19 15:06:45 +07:00
Simon Podlipsky
368a9ee2f7 Add FieldDefinition return type 2019-06-17 14:54:05 +02:00
Simon Podlipsky
6086792824 Fix ReferenceExecutor 2019-06-17 14:18:12 +02:00
Simon Podlipsky
d259a303d3 Drop deprecated GraphQL\Schema 2019-06-17 14:15:46 +02:00
Vladimir Razuvaev
0a71f9fba9
Merge pull request #496 from simPod/fixes
Another code cleanup
2019-06-17 16:48:04 +07:00
Vladimir Razuvaev
61453a4f0b
Merge pull request #495 from spyl94/query-plan-should-work-on-interfaces
QueryPlan can now be used on interfaces not only objects.
2019-06-17 16:46:45 +07:00
Simon Podlipsky
6a5325a448 Another code cleanup 2019-06-12 15:43:17 +02:00
Aurélien David
e87460880c
QueryPlan can now be used on interfaces not only objects.
It's often the case to use interfaces in queries:

interface Pet { name: String! }

Query {
 pets: [Pet]
}
2019-06-12 11:59:42 +02:00
Vladimir Razuvaev
173a4297d9
Merge pull request #494 from simPod/cleanup-warning
Cleanup Warning
2019-06-12 16:40:02 +07:00
Vladimir Razuvaev
6f6a39468c
Merge pull request #493 from simPod/upgrade-phpstan
Upgrade PHPStan
2019-06-12 16:03:40 +07:00
Simon Podlipsky
a22a083220 Cleanup Warning 2019-06-12 11:03:14 +02:00
Vladimir Razuvaev
acc0442152 Merged branch 0.13.x into master 2019-06-12 15:45:44 +07:00
Simon Podlipsky
21592f8f28 Upgrade PHPStan 2019-06-12 10:22:18 +02:00
Vladimir Razuvaev
e17f578842
Merge pull request #492 from spawnia/circular-references
Add schema validation: Input Objects must not contain non-nullable circular references
2019-06-12 14:55:05 +07:00
Vladimir Razuvaev
6d9275e6bc
Merge pull request #474 from theofidry/patch-1
Ignore examples directory
2019-06-12 14:50:02 +07:00
Vladimir Razuvaev
747cb49ae3
Merge pull request #364 from simPod/instanceof
Replace node kind checks by InstanceOf checks
2019-06-12 14:48:50 +07:00
Vladimir Razuvaev
cdcf5b4473 v0.13.5 2019-06-12 14:16:37 +07:00
spawnia
692d10c127 Add Unreleases section to the Changelog 2019-06-10 22:30:25 +02:00
spawnia
6c82b85e79 Revert "Add changelog entry"
This reverts commit e5528d14
2019-06-10 22:24:54 +02:00
spawnia
e5528d14ab Add changelog entry 2019-06-10 22:23:06 +02:00
spawnia
c9faa3489b Add schema validation: Input Objects must not contain non-nullable circular references
Spec change: https://github.com/graphql/graphql-spec/pull/445
Reference implementation: https://github.com/graphql/graphql-js/pull/1359
2019-06-10 22:15:23 +02:00
Vladimir Razuvaev
bd02ccd47e
Merge pull request #486 from mcg-web/fix-coroutine-when-using-promise
Fix coroutine executor when using with promise
2019-06-06 16:50:40 +07:00
Jeremiah VALERIE
08d9493b2c
Fix coroutine executor when using with promise 2019-06-02 20:49:10 +02:00
Jeremiah VALERIE
0b4b1485e0
Add a test to cover promises 2019-06-02 20:49:10 +02:00
Vladimir Razuvaev
387f416984
Merge pull request #483 from mfn/patch-1
Add Laravel GraphQL implementation
2019-05-29 18:06:17 +07:00
Markus Podar
cf90a8d338
Add Laravel GraphQL implementation
It's a spiritual success to the archived https://github.com/Folkloreatelier/laravel-graphql project which was previously removed in https://github.com/webonyx/graphql-php/pull/455
2019-05-28 22:15:29 +02:00
Vladimir Razuvaev
2fd21dd231
Merge pull request #479 from stefsic/stefsic-readme-typo
Update README.md
2019-05-23 16:50:35 +07:00
stefania
2173bb9696
Update README.md
Fix "LICENCE" typo
2019-05-22 15:27:40 -04:00
Vladimir Razuvaev
e55c7d72cb v0.13.4 2019-05-13 14:12:05 +07:00
Vladimir Razuvaev
ef9a24b01f
Merge pull request #477 from mcg-web/max-query-int-forced
Force int when setting max query depth
2019-05-13 14:10:33 +07:00
Jeremiah VALERIE
40ac5ed269
Force int when setting max query depth 2019-05-13 07:50:47 +02:00
Vladimir Razuvaev
27539d5af0 v0.13.3 2019-05-11 14:48:12 +07:00
Vladimir Razuvaev
58d86abaf7
Merge pull request #476 from mcg-web/fix-bc
Fix BC
2019-05-11 11:44:23 +07:00
Jeremiah VALERIE
c803c455b4
Fix bc 2019-05-10 14:22:06 +02:00
Vladimir Razuvaev
9fc4d11425 v0.13.2 2019-05-10 13:03:54 +07:00
Vladimir Razuvaev
d0ab4dc8d8 Merge branch 'master' into 0.13.x
# Conflicts:
#	composer.json
#	src/Server/OperationParams.php
#	src/Type/Definition/EnumType.php
#	src/Type/Definition/InputObjectType.php
#	src/Type/Definition/InterfaceType.php
#	src/Type/Definition/ListOfType.php
#	src/Type/Definition/ResolveInfo.php
#	src/Type/Definition/UnionType.php
#	src/Validator/Rules/ValuesOfCorrectType.php
#	tests/Utils/SchemaExtenderTest.php
2019-05-10 12:19:45 +07:00
Vladimir Razuvaev
231919fbb2 Merge branch 'master' of https://github.com/webonyx/graphql-php 2019-05-10 12:03:22 +07:00
Vladimir Razuvaev
22e3b0e981
Merge pull request #475 from mll-lab/nodelist-offsets
Use foreach for iterating over NodeList
2019-05-10 11:57:44 +07:00
Benedikt Franke
019ed04a51
Update src/Language/AST/NodeList.php
Co-Authored-By: Šimon Podlipský <simon@podlipsky.net>
2019-05-09 19:33:37 +02:00
spawnia
e5b955ecc8 Use array_keys for iterating over NodeList 2019-05-09 17:04:17 +02:00
Théo FIDRY
e94db8a045
Update .gitattributes 2019-05-08 17:28:01 +02:00
Vladimir Razuvaev
49b7aced87
Merge pull request #472 from terabaud/patch-1
Documentation fix: add missing semicolon in generated cache file
2019-05-06 12:21:57 +07:00
Vladimir Razuvaev
0bd13d1828
Merge pull request #469 from simPod/fix-batch-query
Fix invalid query in docs
2019-05-06 12:17:05 +07:00
Lea Rosema
179944495e
fix: add missing semicolon in generated cache file
The `require $cacheFilename` statement leads to a PHP error due to a missing semicolon in the generated cache file.
2019-05-03 15:15:22 +02:00
Simon Podlipsky
0308cf0c0c Fix invalid query in docs 2019-05-03 00:56:11 +02:00
Vladimir Razuvaev
11d32d4568
Merge pull request #465 from mll-lab/enum-default-values
Enum default values
2019-03-29 15:41:12 +07:00
spawnia
db915d8812 Fix codestyle 2019-03-29 09:27:33 +01:00
Vladimir Razuvaev
5821caa249
Merge pull request #466 from simPod/upgrade-doctrine-cs
Upgrade Doctrine CS
2019-03-29 15:14:04 +07:00
Vladimir Razuvaev
8435c3111e
Merge pull request #464 from mll-lab/contributing
Clarify some bits of CONTRIBUTING.md and unify formatting
2019-03-28 19:09:04 +07:00
Vladimir Razuvaev
edec095055
Update README.md 2019-03-28 19:04:50 +07:00
Simon Podlipsky
06529e1924 Upgrade Doctrine CS 2019-03-28 12:18:35 +01:00
spawnia
ddebd9a414 Add test for default enum input coercion 2019-03-28 11:26:30 +01:00
spawnia
1acddf4e22 Mention default value definition for enum args in docs 2019-03-28 11:25:57 +01:00
spawnia
569945cd37 Clarify some bits of CONTRIBUTING.md and unify formatting 2019-03-28 11:24:06 +01:00
Vladimir Razuvaev
b088720d40
Merge pull request #458 from simPod/upgrade-phpstan
Upgrade PHPStan
2019-03-23 17:57:09 +07:00
Vladimir Razuvaev
b3bb316224
Merge pull request #457 from enumag/patch-4
Fix annotations
2019-03-19 12:19:51 +07:00
Simon Podlipsky
23ece09407 Upgrade PHPStan 2019-03-18 13:02:58 +01:00
Jáchym Toušek
1864facda8
Fix annotations 2019-03-18 12:31:33 +01:00
Vladimir Razuvaev
d15a9405cd
Merge pull request #455 from spawnia/patch-1
Remove deprecated tools and improve Lighthouse description
2019-03-17 23:22:41 +07:00
Benedikt Franke
d4742a76e5
Remove deprecated tools and improve Lighthouse description
- The folklore package was archived by the owner.
- The lighthouse relay tools were integrated with the core
2019-03-14 11:16:15 +01:00
Vladimir Razuvaev
f107cc2076 Merge branch 'master' of https://github.com/webonyx/graphql-php 2019-03-09 22:35:12 +07:00
Vladimir Razuvaev
0226b08429 v0.13.1 2019-03-09 22:34:29 +07:00
Simon Podlipsky
255ecbd709 Test against PHP 7.4
(cherry picked from commit dfefdf24cb)
2019-03-09 22:27:54 +07:00
Petr Skoda
8c66fa8d1e Standardise whitespace
(cherry picked from commit d20a6a9d56)
2019-03-09 22:27:40 +07:00
Petr Skoda
7405ddc852 Fix incorrect array type of rootValue in PHPDocs
(cherry picked from commit 77448ba623)
2019-03-09 22:27:30 +07:00
Simon Podlipsky
0cbc1c9c07 Support PHP 8
(cherry picked from commit bc637414e5)
2019-03-09 22:27:17 +07:00
Petr Skoda
c628fa39a1 Fix incorrect array type of contextValue in PHPDocs
(cherry picked from commit 7d59811c4f)
2019-03-09 22:27:03 +07:00
Stefano Torresi
a0f214a9f9 Update docs intro verbiage
Just throwing my 2 cents: I don't think it's fair to say that "it's intended to be a replacement", given that multiple API paradigms can coexist in the same system and each of them have their trade-offs.

(cherry picked from commit b1ab1820b6)
2019-03-09 22:26:44 +07:00
Jan Bukva
153f6f862e Fix return annotation of resolveType() in InterfaceType
(cherry picked from commit edb5268583)
2019-03-09 22:26:29 +07:00
Simon Podlipsky
610979555d Merge clovers
(cherry picked from commit fd7465521a)
2019-03-09 22:26:10 +07:00
Simon Podlipsky
ababa18157 Fix Deferred
(cherry picked from commit 828a9fb002)
2019-03-09 22:25:47 +07:00
Jan Bukva
f52dfcfaef Use key-value foreach
(cherry picked from commit 00490d289c)
2019-03-09 22:24:46 +07:00
Jan Bukva
8b8ea0d4a3 Fix CoroutineExecutor::resultToArray for associative arrays
(cherry picked from commit 2295b96a49)
2019-03-09 22:24:39 +07:00
Jan Bukva
42b20e7651 Failing test for CoroutineExecutor removes associative array when using custom scalar producing JSON
(cherry picked from commit 31d89acfae)
2019-03-09 22:24:17 +07:00
chriszarate
08992de960 Allow extensions to be provided in GET request.
(cherry picked from commit 244ec66ecc)
2019-03-09 22:20:14 +07:00
Torsten Blindert
1d8f526d91 TASK: Code style
(cherry picked from commit 9609d2ac84)
2019-03-09 22:18:54 +07:00
Torsten Blindert
fda73f3212 TASK: Added test
(cherry picked from commit d5fddfd504)
2019-03-09 22:18:45 +07:00
Torsten Blindert
22cee49747 BUGFIX: expect ->getType() to throw
(cherry picked from commit 62b0036437)
2019-03-09 22:18:35 +07:00
Erik Gaal
59c128c54a Add NullableType interface
(cherry picked from commit 9a0dbff26b)
2019-03-09 22:17:22 +07:00
chriszarate
a116127436 Add extensions to OperationParams instance.
(cherry picked from commit f644c1a837)
2019-03-09 22:16:45 +07:00
chriszarate
9ada606919 Fix linting issue and typos.
(cherry picked from commit c33e41f2bf)
2019-03-09 22:16:30 +07:00
chriszarate
1e778d259e Apollo server/client compatibility. Look for queryid in extensions.
(cherry picked from commit 63b4e3f0a4)
2019-03-09 22:15:55 +07:00
Simon Podlipsky
b005803bf6 Add PHP 7.3 to Travis
(cherry picked from commit f95d1e81ea)
2019-03-09 22:15:26 +07:00
Yury
21dc3fe664 Error handling improvements
(cherry picked from commit 33e3c9c338)
2019-03-09 22:15:11 +07:00
Simon Podlipsky
16d42dead3 Document BC and fix types in ResolveInfo
(cherry picked from commit 376e927505)
2019-03-09 22:14:35 +07:00
Simon Podlipsky
bf471838ae Added all possible scalar types to Node constructor
(cherry picked from commit ead1b864bc)
2019-03-09 22:14:24 +07:00
Vladimir Razuvaev
29eba82093
Merge pull request #436 from keulinho/add-queryplan
Added query plan
2019-03-09 21:37:44 +07:00
Vladimir Razuvaev
e6e9d9ea22 Merge branch 'master' of https://github.com/webonyx/graphql-php 2019-03-04 19:03:55 +07:00
Vladimir Razuvaev
f24e00f4b4
Merge pull request #449 from hughdevore/patch-1
Add WP-GraphQL to complementary tools.
2019-03-01 23:54:04 +07:00
Hughie Devore
4a39dadd0d
Add WP-GraphQL to complementary tools.
We utilized this repo for our work in WP GraphQL and wanted to let WP users know that it's available for use.
2019-02-28 13:23:23 -07:00
Vladimir Razuvaev
616fc10837
Merge pull request #431 from simPod/fix-bc-finder
Invert instance of in BCFinder
2019-02-19 10:53:03 +07:00
Vladimir Razuvaev
2f73bbe96c
Merge pull request #442 from moufmouf/patch-1
Adding GraphQLite to the list of PHP Tools
2019-02-19 10:48:52 +07:00
Vladimir Razuvaev
b667d8b3a3
Merge pull request #445 from Chrisdowson/master
change Document url
2019-02-19 10:48:01 +07:00
段小强
441d70c7e5
change Document url
change Document url
2019-02-19 11:21:25 +08:00
David Négrier
972532cf6c
Adding GraphQLite to the list of PHP Tools 2019-02-12 10:46:28 +01:00
Vladimir Razuvaev
e467f80149
Merge pull request #440 from simPod/php7.4
Test against PHP 7.4
2019-02-11 15:31:09 +07:00
Simon Podlipsky
d97fac6ab0 Invert instance of in BCFinder 2019-02-11 09:16:01 +01:00
Simon Podlipsky
dfefdf24cb Test against PHP 7.4 2019-02-11 08:37:05 +01:00
Vladimir Razuvaev
20e98aefa4
Merge pull request #438 from skodak/executioncontext
Fix incorrect array type of contextValue in PHPDocs
2019-02-11 10:56:05 +07:00
Vladimir Razuvaev
ff16350aa1
Merge pull request #439 from simPod/php8
Support PHP 8
2019-02-11 10:55:23 +07:00
Petr Skoda
d20a6a9d56 Standardise whitespace 2019-02-11 08:53:36 +13:00
Petr Skoda
77448ba623 Fix incorrect array type of rootValue in PHPDocs 2019-02-11 08:53:24 +13:00
Simon Podlipsky
bc637414e5 Support PHP 8 2019-02-08 15:08:11 +01:00
Petr Skoda
7d59811c4f Fix incorrect array type of contextValue in PHPDocs 2019-02-07 09:18:17 +13:00
Vladimir Razuvaev
31bbc416a5
Merge pull request #437 from stefanotorresi/patch-1
Update docs intro verbiage
2019-02-04 19:18:53 +07:00
Stefano Torresi
b1ab1820b6
Update docs intro verbiage
Just throwing my 2 cents: I don't think it's fair to say that "it's intended to be a replacement", given that multiple API paradigms can coexist in the same system and each of them have their trade-offs.
2019-02-04 12:38:31 +01:00
Jonas Elfering
5b3f44c7a3 add queryplan 2019-02-01 22:39:00 +01:00
Vladimir Razuvaev
1dc291b073
Merge pull request #435 from JanBukva/fix-interface-type-return
Fix return annotation of resolveType() in InterfaceType
2019-01-29 17:50:03 +07:00
Jan Bukva
edb5268583
Fix return annotation of resolveType() in InterfaceType 2019-01-28 22:46:20 +01:00
Vladimir Razuvaev
6544197ef8
Merge pull request #426 from simPod/merge-clovers
Merge clovers
2018-12-31 13:07:26 +03:00
Vladimir Razuvaev
5ac3eeab18
Merge pull request #418 from Quartz/improvement/apollo-compatible-persisted-queries
Allow extensions to be provided in GET request.
2018-12-31 13:05:09 +03:00
Vladimir Razuvaev
a80d38747f
Merge pull request #421 from JanBukva/new-executor-forces-array
Fix new executor is removing associative array when using custom scalar
2018-12-31 13:00:58 +03:00
Vladimir Razuvaev
9c1a89710e
Merge pull request #422 from simPod/fix-deferred
Fix Deferred
2018-12-31 12:40:58 +03:00
Simon Podlipsky
fd7465521a Merge clovers 2018-12-31 00:32:42 +01:00
Simon Podlipsky
828a9fb002 Fix Deferred 2018-12-27 22:50:12 +01:00
Jan Bukva
00490d289c
Use key-value foreach 2018-12-27 21:53:16 +01:00
Jan Bukva
2295b96a49
Fix CoroutineExecutor::resultToArray for associative arrays 2018-12-27 16:38:40 +01:00
Jan Bukva
31d89acfae
Failing test for CoroutineExecutor removes associative array when using custom scalar producing JSON 2018-12-27 16:01:27 +01:00
chriszarate
244ec66ecc Allow extensions to be provided in GET request. 2018-12-11 22:58:57 -05:00
Vladimir Razuvaev
f96bd2740d
Merge pull request #416 from erikgaal/master
Add NullableType interface
2018-12-07 18:26:04 +07:00
Vladimir Razuvaev
ead704022c
Merge pull request #417 from bytorsten/extension-fix
BUGFIX: ExtendSchema should be able to introduce new types
2018-12-07 16:58:05 +07:00
Torsten Blindert
9609d2ac84 TASK: Code style 2018-12-05 19:23:07 +01:00
Torsten Blindert
d5fddfd504 TASK: Added test 2018-12-05 19:13:32 +01:00
Torsten Blindert
62b0036437 BUGFIX: expect ->getType() to throw 2018-12-05 18:55:14 +01:00
Erik Gaal
9a0dbff26b Add NullableType interface 2018-12-05 11:43:27 +01:00
Vladimir Razuvaev
e22b400373
Merge pull request #413 from Quartz/improvement/apollo-compatible-persisted-queries
Apollo persisted query compatibility: Look for queryId in extensions.
2018-12-04 19:33:04 +07:00
chriszarate
f644c1a837 Add extensions to OperationParams instance. 2018-12-02 15:10:09 -05:00
chriszarate
c33e41f2bf Fix linting issue and typos. 2018-12-01 19:43:51 -05:00
chriszarate
63b4e3f0a4 Apollo server/client compatibility. Look for queryid in extensions. 2018-12-01 19:06:59 -05:00
Vladimir Razuvaev
039577d9eb
Merge pull request #403 from simPod/fix-conditions
Improve comparison in ReferenceExecutor
2018-12-01 23:04:34 +07:00
Vladimir Razuvaev
6610f4e2da
Merge pull request #406 from simPod/fix-type
Added all possible scalar types to Node constructor
2018-12-01 23:04:11 +07:00
Vladimir Razuvaev
19d9c523c5
Merge pull request #407 from simPod/document-bc
Document BC and fix types in ResolveInfo
2018-12-01 23:03:19 +07:00
Vladimir Razuvaev
99f93939db
Merge pull request #408 from yura3d/error-handling-improvements
Error handling improvements
2018-12-01 23:02:57 +07:00
Vladimir Razuvaev
8338db0480
Merge pull request #409 from simPod/php7.3
Add PHP 7.3 to Travis
2018-12-01 22:59:16 +07:00
Simon Podlipsky
f95d1e81ea Add PHP 7.3 to Travis 2018-11-29 11:10:51 +01:00
Yury
33e3c9c338
Error handling improvements 2018-11-28 17:35:21 +03:00
Simon Podlipsky
376e927505 Document BC and fix types in ResolveInfo 2018-11-27 14:05:55 +01:00
Simon Podlipsky
ead1b864bc Added all possible scalar types to Node constructor 2018-11-27 12:22:01 +01:00
Vladimir Razuvaev
b2cea8b538
Merge pull request #402 from simPod/complementary-tools
Complementary tools
2018-11-27 18:09:57 +07:00
Vladimir Razuvaev
012082d1d9 Minor documentation fix 2018-11-27 17:53:03 +07:00
Vladimir Razuvaev
9bb8e73277 v0.13.0 2018-11-27 17:23:58 +07:00
Vladimir Razuvaev
627cc786a5 Fixed codestyle in benchmarks 2018-11-27 17:22:29 +07:00
Vladimir Razuvaev
c45fa1a4b1 Default Error Formatting: Display error category under extensions key (#389) 2018-11-27 16:37:33 +07:00
Vladimir Razuvaev
54263d50c0 Added a note about experimental executor in the changelog 2018-11-27 15:51:53 +07:00
Vladimir Razuvaev
e1b4d438db Ability to override internal types (closes #401) 2018-11-27 15:39:20 +07:00
Simon Podlipsky
e344c8a441 Improve comparison in ReferenceExecutor 2018-11-23 12:17:33 +01:00
Simon Podlipsky
90b51e1a5d Replace node kind checks by InstanceOf checks 2018-11-23 12:07:48 +01:00
Simon Podlipsky
396d3370dc Mention simPod/GraphQL-Utils
Resolves #70
Closes #393
2018-11-23 11:00:26 +01:00
Simon Podlipsky
d591eccd9f Clean complementary tools 2018-11-23 10:58:58 +01:00
Vladimir Razuvaev
bdbb30c604 Schema validation improvements 2018-11-22 22:23:14 +07:00
Vladimir Razuvaev
89fa0c3e67 More composer scripts + related changes to CONTRIBUTING.md 2018-11-22 19:57:53 +07:00
Vladimir Razuvaev
9cb6b4e6f3 Additional tests for errors with extensions 2018-11-22 19:47:10 +07:00
Vladimir Razuvaev
d8d032f0f6 Merge branch 'markhuot-patch-1' 2018-11-22 18:44:22 +07:00
Vladimir Razuvaev
c0ae3ccdaf Fix possessive its vs contraction it's (resolving conflicts) 2018-11-22 18:43:35 +07:00
Vladimir Razuvaev
7c19777dff Ensure interface has at least 1 concrete type 2018-11-21 20:11:11 +07:00
Vladimir Razuvaev
779774b162 ReferenceExecutor: Remove excessive arguments 2018-11-21 18:54:11 +07:00
Vladimir Razuvaev
b4be42acdf Fix printing of ASTs of argument definitions with descriptions 2018-11-21 18:53:44 +07:00
Vladimir Razuvaev
25cfebbd37 Lexer: unify unexpected character errors 2018-11-21 18:09:57 +07:00
Vladimir Razuvaev
0f3ebaa20b Note about new executor in UPGRADE.md 2018-11-21 17:53:12 +07:00
Vladimir Razuvaev
21e0c830a6
Merge pull request #314 from jakubkulhan/compiler
Experimental executor with improved performance
2018-11-21 14:19:21 +07:00
Mark Huot
4811cd198f
Fixing test for possessive/contraction "its" change 2018-11-19 09:13:57 -05:00
Mark Huot
7249e2611a
Fix possessive its vs contraction it's 2018-11-11 07:59:45 -05:00
Jakub Kulhan
b5d3341995 Pluggable executor implementations; new faster executor using coroutines 2018-11-06 23:32:50 +01:00
Vladimir Razuvaev
98807286f7
Merge pull request #386 from nagledb/fix-definition-spelling
Fixed misspelling "defintion" in several places
2018-11-05 03:19:01 +07:00
David B. Nagle
f39f0f5517 Fixed misspelling "defintion" in several places 2018-11-03 15:35:18 -07:00
Vladimir Razuvaev
23169603fc Removed old experimental code 2018-11-03 23:25:23 +07:00
Vladimir Razuvaev
6f8aed800e
Merge pull request #369 from nagledb/nagledb-doc-default-field-resolver
Updated default field resolver code in documentation.
2018-11-03 16:13:57 +07:00
Vladimir Razuvaev
1a1bf17c6a Added test case for extended schema resolvers 2018-11-03 16:01:41 +07:00
Vladimir Razuvaev
cd38568aaa
Merge pull request #381 from jbtbnl/master
Fix resolveFn key for schema extensions
2018-11-03 15:56:57 +07:00
Vladimir Razuvaev
9ee62b014e
Merge pull request #384 from nagledb/restore-resolver-docs
Restore "Defining resolvers" documentation
2018-11-03 15:48:53 +07:00
David B. Nagle
4a096b14ec Restore "Defining resolvers" documentation
Content was inadvertently removed in d6add77540
2018-11-02 21:10:54 -07:00
jbtbnl
427ac1b5d5
Fix resolve Fn key 2018-10-26 12:09:14 +02:00
Vladimir Razuvaev
2bddfe2225
Merge pull request #375 from simPod/fix-build
Fixing build
2018-10-25 13:29:41 +02:00
Simon Podlipsky
0d954b6ecc Constrain PHPStan versions 2018-10-22 23:37:28 +02:00
Simon Podlipsky
644f97634b Little fixes 2018-10-22 23:37:28 +02:00
Vladimir Razuvaev
6cce6742eb
Merge pull request #367 from simPod/phpstan-strict
Use PHPStan strict rules
2018-10-22 17:28:59 +02:00
David B. Nagle
e32bbb726d Updated default field resolver code in documentation.
Updated code to match definition in GraphQL\Executor\Executor::defaultFieldResolver. Change is notable because previous version did not indicate that the closure receives the $info variable.
2018-10-10 20:05:21 -07:00
Vladimir Razuvaev
5fb970b3f1 Codestyle fix 2018-10-10 11:41:24 +02:00
Vladimir Razuvaev
0ea7ffa601 Node in UPGRADE.md about broken AST caches 2018-10-10 11:33:53 +02:00
Vladimir Razuvaev
4bc9dfc6f8 Reverted unneeded change in the defaultFieldResolver 2018-10-10 11:29:14 +02:00
Vladimir Razuvaev
c7fcd4eb48 Got rid of PHP_EOL usages (we never do or expect carriage returns) 2018-10-10 11:26:48 +02:00
Simon Podlipsky
90d0156291
Use PHPStan strict rules
Two rules excluded: missing type hints and empty() usage
2018-10-09 17:46:55 +02:00
Vladimir Razuvaev
0070cb4039
Merge pull request #366 from enumag/patch-1
Fix annotation
2018-10-09 16:18:36 +02:00
Jáchym Toušek
e4c743cf8c
Fix annotation 2018-10-09 14:46:41 +02:00
Torsten Blindert
e7de069bd5 FEATURE: SchemaExtender (#362)
FEATURE: SchemaExtender (#362)
2018-10-08 15:55:20 +02:00
Gilles Maes
7ff3e9399f Expand is_array check in defaultTypeResolver to allow for ArrayAccess… (#361)
Expand is_array check in defaultTypeResolver to allow for ArrayAccess objects as well
2018-10-08 15:53:07 +02:00
Vladimir Razuvaev
1417a43697
Merge pull request #363 from simPod/bump-cs
Bump cs
2018-10-05 12:04:42 +02:00
Simon Podlipsky
dd4a5076f6
Fix CS in src/Validator 2018-10-05 10:47:58 +02:00
Simon Podlipsky
e664c4455e
Fix CS in src/Utils 2018-10-05 10:47:58 +02:00
Simon Podlipsky
18a5639cb7
Fix CS in src/Type 2018-10-05 10:47:57 +02:00
Simon Podlipsky
7ba98ce773
Fix CS in src/Server 2018-10-05 10:47:57 +02:00
Simon Podlipsky
a95d2ad140
Fix CS in src/Language 2018-10-05 10:47:57 +02:00
Simon Podlipsky
bfebcd7bee
Fix CS in src/Executor 2018-10-05 10:47:57 +02:00
Simon Podlipsky
0063bd6c15
Fix CS in src/Error 2018-10-05 10:47:57 +02:00
Simon Podlipsky
73e75b6314
Fix CS in src 2018-10-05 10:47:57 +02:00
Simon Podlipsky
07c070d795
Fix CS in tests 2018-10-05 10:47:57 +02:00
Simon Podlipsky
af31ca7ad8
Bump Doctrine CS to v5 2018-10-05 10:24:10 +02:00
Vladimir Razuvaev
849c15dbf8
Merge pull request #354 from simPod/self
Use self:: in tests where appropriate
2018-10-02 15:12:05 +02:00
Vladimir Razuvaev
84f13650a2
Merge pull request #358 from simPod/php-bench
PHPBench
2018-10-02 15:11:50 +02:00
Vladimir Razuvaev
aed406eade
Merge pull request #360 from bytorsten/expand-extension
Add missing extensionASTNodes
2018-10-02 15:08:26 +02:00
Torsten Blindert
5f5c7d89be TASK: Code style #4 2018-10-02 09:58:44 +02:00
Torsten Blindert
5a1caf0549 TASK: Code style #2 2018-10-02 09:34:29 +02:00
Torsten Blindert
a3d050ff6b TASK: Code style 2018-10-02 09:09:12 +02:00
Torsten Blindert
0262f59a3f BUGFIX: Directives args have to be FieldArguments, not FieldDefinitions 2018-10-01 20:28:05 +02:00
Torsten Blindert
b65aa4e657 TASK: Change KnownDirectives rule to match reference
https://github.com/graphql/graphql-js/blob/master/src/validation/rules/KnownDirectives.js
2018-10-01 19:48:56 +02:00
Torsten Blindert
b1cd086177 TASK: Allow Schema extensions 2018-10-01 19:46:36 +02:00
Torsten Blindert
0c33cfa88f TASK: Allow UnionType extensions 2018-10-01 19:45:48 +02:00
Torsten Blindert
1dc2b939cb TASK: Allow ScalarType extensions 2018-10-01 19:45:36 +02:00
Torsten Blindert
b0ee36feb4 TASK: Allow InputObjectType extensions 2018-10-01 19:45:20 +02:00
Torsten Blindert
1d15ae7f3e TASK: Allow EnumType extensions 2018-10-01 19:44:55 +02:00
Simon Podlipsky
68bfde953d
PHPBench 2018-09-26 11:18:14 +02:00
Simon Podlipsky
c4e06ba528
Use self:: in tests where appropriate 2018-09-19 20:50:32 +02:00
Vladimir Razuvaev
4d4282b60f
Merge pull request #351 from simPod/fix-typo
Fix typo and cleanup
2018-09-10 14:21:33 +07:00
Simon Podlipsky
450a1932e5
Fix typo and cleanup 2018-09-09 17:32:53 +02:00
Vladimir Razuvaev
6bdead3fe3
Merge pull request #337 from spawnia/patch-1
Add Debug::RETHROW_UNSAFE_EXCEPTIONS flag
2018-09-06 12:09:05 +07:00
Vladimir Razuvaev
85f4c774a6
Merge pull request #349 from simPod/improve-stan-config
Improve PHPStan Configuration
2018-09-06 12:08:06 +07:00
Simon Podlipsky
e2e6d70ea8
Improve PHPStan Configuration 2018-09-05 00:09:22 +02:00
spawnia
0ddb7519bb More coding style -.-
Can this be done automatically?
2018-09-03 22:46:14 +02:00
spawnia
1ec4927f69 Add namespace to class 2018-09-03 22:13:05 +02:00
spawnia
c4fe304efe Fix coding style 2018-09-03 21:43:02 +02:00
Benedikt Franke
e7513e356a
Merge branch 'master' into patch-1 2018-09-03 21:25:36 +02:00
spawnia
326e0b4719 Add Debug::RETHROW_UNSAFE_EXCEPTIONS flag
This allows for a more granular default for which Exceptions are thrown, taking
the settings made through the ClientAware interface into account.

- Add docs
- Add a test case
- Differentiate clearly from other test
2018-09-03 21:18:04 +02:00
Vladimir Razuvaev
86503e2e35
Merge pull request #333 from mcg-web/static_analysis
Static analysis
2018-09-02 23:53:29 +07:00
Jeremiah VALERIE
a400f27dce
Ignore phpcs cache file 2018-09-02 18:26:35 +02:00
Jeremiah VALERIE
48c6f56640
Use php 7.1 build instead of 7.2 2018-09-02 18:26:35 +02:00
Jeremiah VALERIE
eed9cc7f1b
Give some love to PHP CS 2018-09-02 18:26:35 +02:00
Jeremiah VALERIE
2f63387864
Add CODING_STANDARD env var 2018-09-02 17:39:09 +02:00
Jeremiah VALERIE
be12d6f273
Sort gitignore alphabetic 2018-09-02 17:20:38 +02:00
Jeremiah VALERIE
21a7611820
fix script path 2018-09-02 17:20:38 +02:00
Jeremiah VALERIE
ff0733d013
Use .dist extension 2018-09-02 17:20:38 +02:00
Jeremiah VALERIE
7428cb8a31
Fix namespaces 2018-09-02 17:20:38 +02:00
Jeremiah VALERIE
62de403f27
Use right assert method 2018-09-02 17:20:38 +02:00
Jeremiah VALERIE
eb7ff7048d
Removed unused use for anonymous functions 2018-09-02 17:20:38 +02:00
Jeremiah VALERIE
b5b27c95b1
Also check tests folder 2018-09-02 17:20:38 +02:00
Jeremiah VALERIE
6a4c815b6d
Cleanup composer script 2018-09-02 17:19:54 +02:00
Jeremiah VALERIE
da70134c38
Fix travis globale install 2018-09-02 17:19:54 +02:00
Jeremiah VALERIE
6866779d26
Remove bin custom path 2018-09-02 17:19:53 +02:00
Jeremiah VALERIE
18954ea655
Fix typehint 2018-09-02 17:19:16 +02:00
Jeremiah VALERIE
05dbc0fb96
Fix sprintf call 2018-09-02 17:19:16 +02:00
Jeremiah VALERIE
31601d710b
Add default config file 2018-09-02 17:19:16 +02:00
Jeremiah VALERIE
d8f41e854f
Add static analysis tool 2018-09-02 17:19:15 +02:00
Vladimir Razuvaev
6c40fec35a
Merge pull request #341 from adactio/patch-1
Remove the word "simply"
2018-09-02 22:05:51 +07:00
Vladimir Razuvaev
c7d8cf4ea2
Merge pull request #348 from simPod/check-cs
Check CS in the whole project
2018-09-02 22:05:12 +07:00
Simon Podlipsky
7dbd72cebf
Check CS in the whole project
Closes #284
2018-09-02 16:46:58 +02:00
Vladimir Razuvaev
a4edb34deb
Merge pull request #343 from simPod/update-contibuting
Add it() explanation to Contribution guidelines
2018-09-02 21:36:59 +07:00
Vladimir Razuvaev
bfff27ef34
Merge pull request #347 from simPod/fix-cs-test
Fix CS in tests
2018-09-02 21:30:04 +07:00
Vladimir Razuvaev
ec43a2e01a
Merge pull request #346 from simPod/fix-cs-tests-validator
Fix CS in tests/Validator
2018-09-02 21:29:33 +07:00
Vladimir Razuvaev
d70c8e87e5
Merge pull request #345 from simPod/fix-cs-test-utils
Fix CS in tests/Utils
2018-09-02 21:27:39 +07:00
Vladimir Razuvaev
4e43a2cbcd
Merge pull request #344 from simPod/fix-cs-test-type
Fix CS in tests/Type
2018-09-02 21:26:11 +07:00
Vladimir Razuvaev
ce0272b447
Merge pull request #340 from simPod/cs-language-server
Fix CS in tests/Server
2018-09-02 21:23:47 +07:00
Vladimir Razuvaev
a7af4663b8
Merge pull request #339 from simPod/cs-language-test
Fix CS in test/Language
2018-09-02 21:22:39 +07:00
Vladimir Razuvaev
7f99bf478f
Merge pull request #338 from simPod/cs-executor-test
Fix CS in tests/Executor
2018-09-02 21:20:54 +07:00
Vladimir Razuvaev
9327e75a16
Merge pull request #342 from simPod/cs-src
Fix CS in src
2018-09-02 21:17:07 +07:00
Simon Podlipsky
d398e59ced
Fix CS in tests 2018-09-02 13:39:30 +02:00
Simon Podlipsky
737da333fb
Fix CS in tests/Utils 2018-09-02 13:10:47 +02:00
Simon Podlipsky
caa50d6db9
Fix CS in tests/Validator 2018-09-02 13:08:49 +02:00
Simon Podlipsky
b886742968
Fix CS in tests/Type 2018-09-02 12:55:40 +02:00
Simon Podlipsky
046bd02d6c
Add it() explanation to Contribution guidelines 2018-09-02 12:07:37 +02:00
Simon Podlipsky
76e1c33b68
Fix CS in src 2018-09-02 10:39:03 +02:00
Simon Podlipsky
bd8722652a
Fix CS in tests/Server 2018-09-02 10:18:45 +02:00
Jeremy Keith
33cb80335e
Remove the word "simply" 2018-09-01 22:32:02 +01:00
Simon Podlipsky
d1f49bedbd
Fix CS in test/Language 2018-09-01 22:16:02 +02:00
Simon Podlipsky
b02d25e62c
Fix CS in tests/Executor 2018-09-01 20:05:38 +02:00
Benedikt Franke
503ac4619a
Check if an exception is internal before rethrowing 2018-08-31 17:45:41 +02:00
Vladimir Razuvaev
ec54d6152b
Merge pull request #335 from simPod/test-void
Add void return typehint to test methods
2018-08-31 19:57:04 +07:00
Simon Podlipsky
715146cdd1
Add void return typehint to test methods 2018-08-31 14:41:18 +02:00
Vladimir Razuvaev
573a77ce0c
Merge pull request #334 from simPod/replace-it
Replace misleading @it by @see annotation
2018-08-31 18:51:00 +07:00
Simon Podlipsky
a30b3104a0
Replace misleading @it by @see annotation 2018-08-31 10:55:14 +02:00
Vladimir Razuvaev
cf4cefc2bc
Merge pull request #332 from simPod/cs-tests-error
Fix CS in tests/Error
2018-08-28 17:24:55 +07:00
Vladimir Razuvaev
ef93557a5d
Merge pull request #331 from simPod/cs-type
Fix CS in src/Type
2018-08-28 17:23:42 +07:00
Simon Podlipsky
3170951620
Fix CS in /tests/Error 2018-08-28 11:09:46 +02:00
Simon Podlipsky
00d547dc06
Fix CS in src/Type 2018-08-27 18:14:07 +02:00
Vladimir Razuvaev
8817e8e010 Throws descriptive error when non-type used instead of interface 2018-08-22 18:53:25 +07:00
Vladimir Razuvaev
ea6a21a13b Cleanup variables test 2018-08-22 18:40:15 +07:00
Vladimir Razuvaev
672ff0b7d6 Add nested nullable test in VariablesInAllowedPosition 2018-08-22 17:55:42 +07:00
Vladimir Razuvaev
804daa188e Add more BuildSchema tests + cleanup 2018-08-22 16:14:47 +07:00
Vladimir Razuvaev
f123e5c954 Remove redundancy in schema printer tests 2018-08-22 15:41:26 +07:00
Vladimir Razuvaev
227f0b867d Serial execution should support sync execution (+tests for the serial execution) 2018-08-22 15:10:10 +07:00
Vladimir Razuvaev
d87c1aba5c Additional printer test 2018-08-22 12:28:13 +07:00
Vladimir Razuvaev
6e7cf27579 Fixed composer lint command 2018-08-22 12:07:40 +07:00
Vladimir Razuvaev
90623f68d7 Fixed code style after recent changes 2018-08-22 12:07:21 +07:00
Vladimir Razuvaev
00caed811b Fixed SchemaPrinter (broken after the recent merge) 2018-08-22 11:45:54 +07:00
Vladimir Razuvaev
9ae8b9f26e Merge branch 'master' of https://github.com/webonyx/graphql-php
# Conflicts:
#	src/Utils/AST.php
2018-08-22 11:43:30 +07:00
Vladimir Razuvaev
7d326c44d8
Merge pull request #328 from simPod/utils-cs
Fix CS in Utils
2018-08-22 11:33:55 +07:00
Simon Podlipsky
49f34d3243
Fix CS in Utils 2018-08-22 02:51:55 +02:00
Vladimir Razuvaev
d44ec9e809 Merge branch 'master' of https://github.com/webonyx/graphql-php
# Conflicts:
#	src/Utils/AST.php
2018-08-21 22:10:50 +07:00
Vladimir Razuvaev
a3ef1be1ab
Merge pull request #323 from simPod/language-cs
Fix CS in src/Language
2018-08-21 22:00:01 +07:00
Simon Podlipsky
ad8693cb8a
Fix CS in src/Language 2018-08-20 20:26:21 +02:00
Vladimir Razuvaev
dbeaf46631
Merge pull request #324 from bkonetzny/docs-scalar-method-signature
Fixed method signature in custom scalar docs
2018-08-20 17:40:06 +07:00
Vladimir Razuvaev
0caaa1fa3b
Merge pull request #325 from Smolevich/fixes-in-benchmarks
Remove use instance of deprecated class GraphQL\Schema
2018-08-20 17:39:44 +07:00
Vladimir Razuvaev
23fce6385f
Merge pull request #319 from jrots/master
Fix for fatal php error: Call to a member function getLocation() on null, before normal error handling
2018-08-20 16:27:10 +07:00
Stanislav Shupilkin
189d474633 Remove use instance of deprecated class GraphQL\Schema 2018-08-17 19:19:33 +03:00
Bastian Konetzny
86fa8b1301
Fixed method signature in custom scalar docs 2018-08-16 09:43:15 +02:00
Vladimir Razuvaev
cc39b3ecbf
Merge pull request #321 from simPod/validator-cs
Fix CS in Validator folder
2018-08-12 01:08:05 +07:00
Vladimir Razuvaev
64c463e889 Schema: getTypeMap() should include input types defined in directive arguments 2018-08-08 15:47:42 +07:00
Simon Podlipsky
4c327a6c16
Fix CS in Validator folder 2018-08-08 10:44:05 +02:00
Vladimir Razuvaev
0d93d190f8 Updated introspection test 2018-08-08 15:11:33 +07:00
Vladimir Razuvaev
56e91d008e Update printSchema for recent SDL change to implements 2018-08-08 15:07:08 +07:00
Vladimir Razuvaev
fcb9c24bb5 Fix astFromValue to correctly handle integers and strings 2018-08-08 14:57:44 +07:00
Vladimir Razuvaev
49ec89b28f Fixed broken description printing 2018-08-08 01:46:32 +07:00
Vladimir Razuvaev
03d7d1851c NonNull test: fixed naming for consistency 2018-08-08 01:43:23 +07:00
Vladimir Razuvaev
d9aee43129 Printer: created special function to add descriptions 2018-08-08 01:28:34 +07:00
Vladimir Razuvaev
ccb9486d21 SchemaParser: additional test case 2018-08-08 01:18:59 +07:00
Vladimir Razuvaev
a19fc3d208 RFC: SDL - Separate multiple inherited interfaces with & 2018-08-08 01:11:47 +07:00
Vladimir Razuvaev
8e02fdc537 Parser: allowLegacySDLEmptyFields option + minor naming tweaks 2018-08-08 00:41:20 +07:00
Vladimir Razuvaev
ea13c9edbf Updated PHPUnit to the latest version 2018-08-08 00:13:21 +07:00
Vladimir Razuvaev
c3d69c7c2b Fixed tests for PHPUnit 7+ 2018-08-07 23:59:48 +07:00
Vladimir Razuvaev
4227404aee Added test for useful error when returning invalid value from resolveType 2018-08-07 23:33:20 +07:00
Vladimir Razuvaev
f4008f0fb2 Error formatting: display error extensions under extensions key 2018-08-07 23:33:20 +07:00
Vladimir Razuvaev
39df711eac More definition tests (for type validation) 2018-08-07 23:32:26 +07:00
Jayme Rotsaert
942d4995c5 Fix: fatal Call to a member function getLocation(), before normal error is thrown 2018-08-01 18:16:48 +02:00
Vladimir Razuvaev
392b567f23
Merge pull request #317 from simPod/upgrade-phpunit
Upgrade PHPUnit
2018-07-31 01:15:46 +07:00
Vladimir Razuvaev
48b44fbdc5
Merge pull request #318 from robbieaverill/patch-1
FIX Ensure that __toString return value is always casted as a string
2018-07-30 13:29:34 +07:00
Robbie Averill
c962afc566
FIX Ensure that __toString return value is always casted as a string
`json_encode` can return false on failure, which causes PHP errors since `__toString` must return a string.
2018-07-30 16:36:09 +12:00
Vladimir Razuvaev
2f2b54a3d6 Perf: memoize collectSubfields 2018-07-30 00:36:10 +07:00
Simon Podlipsky
24b6b736b2
Upgrade PHPUnit 2018-07-29 19:01:39 +02:00
Vladimir Razuvaev
bedbd32fb7
Merge pull request #311 from Smolevich/update-complementary
Add link to GraphQL IDE in complementary-tools.md
2018-07-20 00:29:06 +07:00
Stanislav Shupilkin
34500bd8b2 Add link to GraphQL IDE in complementary-tools.md 2018-07-19 19:24:55 +03:00
Vladimir Razuvaev
0a6eaa1173
Merge pull request #306 from simPod/cs-server
Fixed code style for the /Server
2018-07-12 21:03:10 +07:00
Vladimir Razuvaev
c02c218c71
Merge pull request #304 from simPod/update-contibuting
Add CS info into Contributing docs
2018-07-12 20:56:34 +07:00
Simon Podlipsky
1b42de0658
@return self 2018-07-10 01:34:35 +03:00
Simon Podlipsky
ec2ff0d4bf
CS /Server 2018-07-10 01:34:35 +03:00
Simon Podlipsky
54456b1160
Add CS info into Contributing docs 2018-07-10 00:51:08 +03:00
Théo FIDRY
c1a62fdb05 Allow stringeable objects to be serialized by StringType
Closes #302

(cherry picked from commit c258109)
2018-07-08 04:44:10 +07:00
Vladimir Razuvaev
e515964a73
Merge pull request #295 from simPod/refactoring
Executor: fixed code style / minor refactoring
2018-07-07 21:58:54 +07:00
Vladimir Razuvaev
15672ab66c
Merge pull request #296 from danez/patch-2
fix: Correct namespace and link for DirectiveLocation in docs
2018-07-07 21:46:38 +07:00
Vladimir Razuvaev
a59b2803be
Merge pull request #298 from vasily-kartashov/master
Update complementary-tools.md
2018-07-07 21:46:04 +07:00
Vladimir Razuvaev
dbafdf849e
Merge pull request #300 from MySchoolManagement/support-multipart-formdata
Adds support for the multipart/form-data content type
2018-07-07 21:45:21 +07:00
Iain Mckay
750ce383ec Adds support for the multipart/form-data content type 2018-07-05 08:52:29 +02:00
Vasily Kartashov
ec77f439fb
Update complementary-tools.md
Adding reference to graphql-batch-processing library
2018-06-29 13:56:14 +04:00
Daniel Tschinder
1b6fb4c29c
fix: Correct namespace and link for DirectiveLocation in docs 2018-06-28 11:58:51 +02:00
Simon Podlipsky
cd1cc911e7
Refactoring 2018-06-26 14:37:19 +02:00
Vladimir Razuvaev
ddc3a01f09
Merge pull request #294 from danez/fix-block-printing
Use multi-line block for trailing quote
2018-06-25 23:45:29 +07:00
Daniel Tschinder
6e64983f82 Use multi-line block for trailing quote
ref: fdc10bb918 (diff-ebaed8492e8d884ee4f2255e39909568)
2018-06-25 14:26:53 +02:00
Vladimir Razuvaev
a032367e26 Disabled code style check of the whole project in Travis (for now) 2018-06-23 13:50:25 +07:00
Vladimir Razuvaev
5a90e9bd64
Merge pull request #288 from simPod/phpcs
RFC: PHP CS
2018-06-23 12:32:26 +07:00
Vladimir Razuvaev
89369fd8ec
Merge pull request #291 from danez/patch-1
Add one more breaking change in 0.12
2018-06-23 11:19:13 +07:00
Vladimir Razuvaev
6195029215
Merge pull request #292 from danez/patch-2
Fix wrong length being used in validator.
2018-06-23 11:18:07 +07:00
Daniel Tschinder
8ba146071d
Fix wrong length being used in validator. 2018-06-22 16:56:19 +02:00
Daniel Tschinder
3a4f520da7
Improve example 2018-06-22 15:46:42 +02:00
Daniel Tschinder
300b58093b
Add one more breaking change in 0.12 2018-06-22 15:45:21 +02:00
Simon Podlipsky
f44ff2cfe7
Add composer script for linting 2018-06-21 10:18:34 +02:00
Simon Podlipsky
4bc3b7885c
Fix CS in Error folder 2018-06-19 19:50:12 +02:00
Simon Podlipsky
fe5c3bdee5
Introduce PHPCS 2018-06-19 19:50:12 +02:00
Vladimir Razuvaev
e31947a452
Removed scrutinizer badge 2018-06-19 19:21:15 +07:00
Vladimir Razuvaev
f0c2f12222
Merge pull request #286 from simPod/cleanup-travis
Cleanup CI
2018-06-19 17:55:29 +07:00
Simon Podlipsky
4e9ad1fd75
Cleanup CI 2018-06-16 00:20:54 +02:00
Vladimir Razuvaev
82b1dbd836
Merge pull request #282 from han8gui/patch-1
Docs: fixed invalid port number in getting started guide
2018-05-27 20:04:42 +07:00
Vladimir Razuvaev
9452655fcd
Merge pull request #281 from icerockdev/master
Removing data elements from response if the error throwing
2018-05-27 20:02:14 +07:00
Vladimir Razuvaev
01e084a9ee Added changelog for 0.12.0 2018-05-27 19:42:05 +07:00
Vladimir Razuvaev
9201df1b39 v0.12.0 2018-05-27 19:23:13 +07:00
Vladimir Razuvaev
1b22f95a86 Removed previously deprecated classes/methods 2018-05-27 19:13:32 +07:00
Vladimir Razuvaev
2580750d4c
Merge pull request #277 from danez/throwitall
Make Types throw instead of returning Utils::undefined()
2018-05-27 18:08:39 +07:00
Ben Zhu
72e8607e47
fix port bug 2018-05-09 11:18:43 +08:00
Ilya Shaydullin
06490cae8b Fix is null condition 2018-05-07 17:12:21 +07:00
Ilya Shaydullin
c7f114d90b Removing data elements from response if the error throwing 2018-05-07 17:01:18 +07:00
Daniel Tschinder
62748279d4 Cleanup imports 2018-04-24 15:38:44 +02:00
Daniel Tschinder
f140149127 Make Types throw instead of returning Utils::undefined() 2018-04-24 15:14:31 +02:00
Vladimir Razuvaev
ec0985619f
Merge pull request #273 from kikoseijo/master
Add Nuwave Lighthouse to Docs.
2018-04-19 11:52:54 +08:00
Vladimir Razuvaev
f265320c3c Merge branch 'master' of https://github.com/webonyx/graphql-php 2018-04-17 17:49:52 +08:00
Vladimir Razuvaev
63299157d8 Note in UPGRADE.md about minimum PHP version requirement in the next major release 2018-04-17 17:49:10 +08:00
Kiko Seijo
98b4355b94 Fix misspell. 2018-04-16 20:52:38 +02:00
Kiko Seijo
d6b16ba0ec Add Nuwave Lighthouse to Docs. 2018-04-16 20:49:49 +02:00
Vladimir Razuvaev
afbc327ea7
Merge pull request #271 from PowerKiKi/github-case
Use correct case for GitHub
2018-04-15 22:39:19 +08:00
Adrien Crivelli
6c55f20f43
Use correct case for GitHub 2018-04-15 17:38:16 +09:00
Vladimir Razuvaev
4418f4f975
Merge pull request #270 from adri/allow-validation-to-be-skipped
Validation > Performance > Return early if rules are empty to avoid visiting elements
2018-04-13 13:18:04 +08:00
Adrian Philipp
b3791378fa Return early if rules are empty to avoid visiting elements
This is tested already in testPassesValidationWithEmptyRules
2018-04-12 17:36:24 +02:00
Vladimir Razuvaev
568cae584b
Merge pull request #260 from simPod/drop-unsupported-php
Drop support for EOL PHP versions
2018-04-08 20:37:29 +08:00
Vladimir Razuvaev
ccbc91a97f
Merge pull request #267 from ruudk/patch-1
Fix missing sprintf in Schema.php
2018-04-06 17:14:58 +08:00
Ruud Kamphuis
66108bec84
Fix missing sprintf 2018-04-05 17:14:28 +02:00
Vladimir Razuvaev
87729589e0
Merge pull request #265 from sarukuku/master
Add content type header to curl commands on server example
2018-04-05 14:47:40 +08:00
sarukuku
9b94ac2f06 Better argument order for curl. Related to #264. 2018-04-04 16:34:05 +03:00
sarukuku
f2678b4a10 Add content type header to curl. This fixes #264. 2018-04-04 16:26:57 +03:00
Simon Podlipsky
bcf396868a
Drop support for EOL PHP versions 2018-03-30 14:48:49 +02:00
Vladimir Razuvaev
5c57d3b379 Note in UPGRADE.md about changes in input format of the Standard Server 2018-03-29 20:31:31 +08:00
Vladimir Razuvaev
b141ed2d72 Merge branch 'master' of https://github.com/webonyx/graphql-php 2018-03-29 20:30:32 +08:00
Vladimir Razuvaev
7762430bc3
Merge pull request #256 from camuthig/fix-operation-param-operation-name
Parse operation name from operationName instead of operation
2018-03-29 20:25:39 +08:00
Chris Muthig
68eb325d18
Add a unit test covering the operationName parsing 2018-03-28 13:55:59 -07:00
Vladimir Razuvaev
a1b1436f7d Remove notice about removing typeConfigDecorator from UPGRADE.md since we reverted this change and it still exists 2018-03-26 12:28:11 +08:00
Vladimir Razuvaev
2913f07050
Merge pull request #258 from ScullWM/patch-1
Fix Symfony PSR7 link
2018-03-26 12:17:24 +08:00
Vladimir Razuvaev
45baa5f185
Merge pull request #248 from danez/012
Port 0.12.3 changes from graphql-js
2018-03-26 11:43:59 +08:00
Daniel Tschinder
3e067cc60f Readd type decorator and fix lazy type loading 2018-03-06 12:53:28 +01:00
Thomas P
2a4c0a111a
Fix Symfony PSR7 link
Previous URL no longer exist and return a 404.
I've update the url to https://symfony.com/doc/current/components/psr7.html
2018-03-05 15:06:38 +01:00
Chris Muthig
8aa6dc17a5
Parse operation name from operationName instead of operation 2018-03-03 15:41:55 -08:00
Daniel Tschinder
f9a366e69a Add Fallback for DirectiveLocations 2018-02-16 16:54:06 +01:00
Daniel Tschinder
61fe317faf Update docs 2018-02-16 16:50:38 +01:00
Daniel Tschinder
5e7cf2aacb Skip test on PHP < 7 2018-02-16 16:47:11 +01:00
Daniel Tschinder
dc6e814de3 Fix orList to be the same as in JS and follow the chicago style for commas 2018-02-16 16:39:59 +01:00
Daniel Tschinder
d92a2dab21 Add suggestions for invalid values
For misspelled enums or field names, these suggestions can be helpful.

This also changes the suggestions algorithm to better detect case-sensitivity mistakes, which are common

ref: graphql/graphql-js#1153
2018-02-16 16:19:25 +01:00
Daniel Tschinder
48c5e64a08 Adding an interface to a type is now a dangerous change.
ref: graphql/graphql-js#992
2018-02-16 15:30:27 +01:00
Daniel Tschinder
d71b45d60e Find breaking directive changes
ref: graphql/graphql-js#1152
2018-02-16 00:15:19 +01:00
Daniel Tschinder
ddfeee314c Fix path argument. Enchance visit test to validate all arguments
ref: graphl/graphql-js#1149
2018-02-15 22:44:17 +01:00
Daniel Tschinder
58e0c7a178 Validate literals in a single rule with finer precision
This generalizes the "arguments of correct type" and "default values of correct type" to a single rule "values of correct type" which has been re-written to rely on a traversal rather than the utility function `isValidLiteralValue`. To reduce breaking scope, this does not remove that utility even though it's no longer used directly within the library. Since the default values rule included another validation rule that rule was renamed to a more apt "variable default value allowed".

This also includes the original errors from custom scalars in the validation error output, solving the remainder of graphql/graphql-js#821.

ref: graphql/graphql-js#1144
2018-02-15 21:29:14 +01:00
Daniel Tschinder
17520876d8 Update some validators to latest upstream version
This includes:
graphql/graphql-js#1147
graphql/graphql-js#355

This also fixes two bugs in the Schema
 - types that were not found where still added to the typeMap
 - InputObject args should not be searched for types.
2018-02-15 17:19:53 +01:00
Daniel Tschinder
949b853678 Add experimental support for parsing variable definitions in fragments
ref: graphql/graphql-js#1141
2018-02-15 13:37:45 +01:00
Daniel Tschinder
fde7df534d Robust type info
There are possibilities for errors during validation if a schema is not valid when provided to TypeInfo. Most checks for validity existed, but some did not. This asks flow to make those checks required and adds the remaining ones. Important now that we allow construction of invalid schema.

ref: graphql/graphql-js#1143
2018-02-15 12:22:29 +01:00
Daniel Tschinder
97e8a9e200 Move schema validation into separate step (type constructors)
This is the second step of moving work from type constructors to the schema validation function.

ref: graphql/graphql-js#1132
2018-02-15 12:14:08 +01:00
Daniel Tschinder
6d08c342c9 Address recent SDL spec changes
This should be the last set of spec changes for a standardized SDL

ref: graphql/graphql-js#1139
2018-02-13 18:18:50 +01:00
Daniel Tschinder
50cbfb4a44 Fix Bug in PossibleFragmentSpreads validator
ref: graphql/graphql-js@7e147a8dd6
2018-02-13 18:08:05 +01:00
Daniel Tschinder
9387548aa1 Better Predicates
Introduces new assertion functions for each kind of type mirroring the existing ones for the higher order types.

ref: graphql/graphql-js#1137
2018-02-13 18:04:03 +01:00
Daniel Tschinder
60df83f47e Preserve original coercion errors, improve error quality.
This is a fairly major refactoring of coerceValue which returns an Either so it can return a complete collection of errors. This allows originalError to be preserved for scalar coercion errors and ensures *all* errors are represented in the response.

This had a minor change to the logic in execute / subscribe to allow for buildExecutionContext to abrupt complete with multiple errors.

ref: graphql/graphql-js#1133
2018-02-13 16:51:44 +01:00
Daniel Tschinder
6d45a22ba4 Always extract extensions from the original error if possible
ref: graphql/graphql-js#2d08496720088dbe65ebea312c8526bd48fb8ee8
2018-02-13 10:42:56 +01:00
Daniel Tschinder
cf276340a4 Fix printError/locations for multiple nodes.
If a GraphQLError represents multiple nodes across files (could happen for validation across multiple parsed files) then the reported locations and printError output can be incorrect for the second node. This ensures locations are derived from nodes whenever possible to get correct location and amends comment documentation.
ref: graphql/graphql-js#1131
2018-02-13 10:42:51 +01:00
Daniel Tschinder
06c6c4bd97 Validate schema root types and directives
This moves validation out of GraphQLSchema's constructor (but not yet from other type constructors), which is responsible for root type validation and interface implementation checking.

Reduces time to construct GraphQLSchema significantly, shifting the time to validation.

This also allows for much looser rules within the schema builders, which implicitly validate while trying to adhere to flow types. Instead we use any casts to loosen the rules to defer that to validation where errors can be richer.

This also loosens the rule that a schema can only be constructed if it has a query type, moving that to validation as well. That makes flow typing slightly less nice, but allows for incremental schema building which is valuable

ref: graphql/graphql-js#1124
2018-02-13 10:42:35 +01:00
Daniel Tschinder
15374a31dd New: printError()
Lifted from / inspired by a similar change in graphql/graphql-js#722, this creates a new function `printError()` (and uses it as the implementation for `GraphQLError#toString()`) which prints location information in the context of an error.

This is moved from the syntax error where it used to be hard-coded, so it may now be used to format validation errors, value coercion errors, or any other error which may be associated with a location.

ref: graphql/graphql-js

BREAKING CHANGE: The SyntaxError message does not contain the codeframe anymore and only the message, (string) $error will print the codeframe.
2018-02-12 12:23:39 +01:00
Daniel Tschinder
f661f38215 Fix unhandled error when parsing custom scalar literals.
This factors out the enum value validation from scalar value validation and ensures the same try/catch is used in isValidLiteralValue as isValidPHPValue and protecting from errors in valueFromAST.
ref: graphql/graphql-js#1126
2018-02-11 22:31:04 +01:00
Daniel Tschinder
481cdc9a82 Include test that printSchema includes non-spec directives.
ref: graphql/graphql-js@007407deb0

Also fixes expected <-> actual in this testfile
2018-02-11 21:32:40 +01:00
Daniel Tschinder
0c984a83bb Allow constructing GraphQLError with single node.
A common case is encountering an error which blames to a single AST node. Ensure the GraphQLError constructor can handle this case.

ref: graphql/graphql-js#1123
2018-02-11 21:18:54 +01:00
Daniel Tschinder
b5106a06c9 SDL Spec changes
This adds the recent changes to the SDL proposal.

ref: graphql/graphql-js#1117
2018-02-11 21:08:53 +01:00
Daniel Tschinder
74854d55a0 Read-only AST types
ref: graphql/graphql-js#1121
2018-02-11 18:28:34 +01:00
Daniel Tschinder
ff63e07b05 Improve introspection types + new getIntrospectionQuery()
This adds a new function `getIntrospectionQuery()` which allows for some minor configuration over the resulting query text: to exclude descriptions if your use case does not require them.

ref: graphql/graphql-js#1113
2018-02-11 18:19:52 +01:00
Daniel Tschinder
6e358eb26c Fix infinite loop on invalid queries in OverlappingFields
`OverlappingFieldsCanBeMerged` would infinite loop when passed something like

```graphql
fragment A on User {
  name
  ...A
}
```

It's not `OverlappingFieldsCanBeMerged`'s responsibility to detect that validation error, but we still would ideally avoid infinite looping.

This detects that case, and pretends that the infinite spread wasn't there for the purposes of this validation step.

Also, by memoizing and checking for self-references this removes duplicate reports.

ref: graphql/graphql-js#1111
2018-02-11 17:58:48 +01:00
Daniel Tschinder
7b05673d8d Validation: improving overlapping fields quality
This improves the overlapping fields validation performance and improves error reporting quality by separating the concepts of checking fields "within" a single collection of fields from checking fields "between" two different collections of fields. This ensures for deeply overlapping fields that nested fields are not checked against each other repeatedly. Extending this concept further, fragment spreads are no longer expanded inline before looking for conflicts, instead the fields within a fragment are compared to the fields with the selection set which contained the referencing fragment spread.

e.g.

```graphql
{
  same: a
  same: b
  ...X
}

fragment X on T {
  same: c
  same: d
}
```

In the above example, the initial query body is checked "within" so `a` is compared to `b`. Also, the fragment `X` is checked "within" so `c` is compared to `d`. Because of the fragment spread, the query body and fragment `X` are checked "between" so that `a` and `b` are each compared to `c` and `d`. In this trivial example, no fewer checks are performed, but in the case where fragments are referenced multiple times, this reduces the overall number of checks (regardless of memoization).

**BREAKING**: This can change the order of fields reported when a conflict arises when fragment spreads are involved. If you are checking the precise output of errors (e.g. for unit tests), you may find existing errors change from `"a" and "c" are different fields` to `"c" and "a" are different fields`.

From a perf point of view, this is fairly minor as the memoization "PairSet" was already keeping these repeated checks from consuming time, however this will reduce the number of memoized hits because of the algorithm improvement.

From an error reporting point of view, this reports nearest-common-ancestor issues when found in a fragment that comes later in the validation process. I've added a test which fails with the existing impl and now passes, as well as changed a comment.

This also fixes an error where validation issues could be missed because of an over-eager memoization. I've also modified the `PairSet` to be aware of both forms of memoization, also represented by a previously failing test.

ref: graphql/graphql-js#386
2018-02-11 17:45:35 +01:00
Daniel Tschinder
58453c31f7 Improve validation error message when field names conflict
ref: graphql/graphql-js#363
2018-02-11 14:11:21 +01:00
Daniel Tschinder
d70a9a5e53 Update to match SDL changes
This changes the parsing grammar and validation rules to more correctly implement the current state of the GraphQL SDL proposal (facebook/graphql#90)

ref: graphql/graphl-js#1102
2018-02-11 13:27:26 +01:00
Daniel Tschinder
0c32982171 Fix KnownDirectives validator to support all directives 2018-02-11 13:15:51 +01:00
Daniel Tschinder
d6add77540 Add Docs 2018-02-10 18:46:37 +01:00
Daniel Tschinder
1da3801614 Add predicates to for built-in types
ref: graphql/graphql-js#924
2018-02-10 18:45:52 +01:00
Daniel Tschinder
c4f11a577e Allow to extend GraphQL errors with additional properties
ref: graphql/graphql-js#928
2018-02-10 18:45:38 +01:00
Daniel Tschinder
2cbccb87db Remove duplicated code from buildASTSchema and extendSchema
ref: graphql/graphql-js#1000

BREAKING CHANGE: SchemaBuilder::build() and buildAST() and constructor
removed the typedecorator, as not needed anymore as library can now resolve
union and interfaces from generated schemas.
2018-02-10 18:45:32 +01:00
Daniel Tschinder
48c33302a8 (Potentially Breaking) Allow serializing scalars as null.
This changes the check for null/undefined to a check for undefined to determine if scalar serialization was successful or not, allowing `null` to be returned from serialize() without indicating error.

This is potentially breaking for any existing custom scalar which returned `null` from `serialize()` to indicate failure. To account for this change, it should either throw an error or return `undefined`.

ref: graphql/graphql-js#1104
2018-02-10 18:45:27 +01:00
Daniel Tschinder
27ce24b5fe Fix parsing of default values in build-schema
* Generalizes building a value from an AST, since "scalar" could be misleading, and supporting variable values within custom scalar literals can be valuable.
* Replaces isNullish with isInvalid since `null` is a meaningful value as a result of literal parsing.
* Provide reasonable default version of 'parseLiteral'

ref: 714ee980aa
ref: https://github.com/graphql/graphql-js/pull/903

# Conflicts:
#	src/Utils/BuildSchema.php
#	tests/Utils/BuildSchemaTest.php
2018-02-10 18:45:23 +01:00
Daniel Tschinder
2123946dbd Add warnings for nullable changes
ref: db4cfdc31d
ref: graphql/graphql-js#1096

# Conflicts:
#	tests/Utils/FindBreakingChangesTest.php
2018-02-10 18:45:18 +01:00
Daniel Tschinder
17a8c26fc9 Simplify operationTypes validation
ref: graphql/graphql-js#999

# Conflicts:
#	src/Utils/BuildSchema.php
2018-02-10 18:45:14 +01:00
Daniel Tschinder
1fdb3da7fb Remove notes about subscription being experimental
ref: graphql/graphql-js#bf4a25a33a62280e82680518adc279e34ec816e0
2018-02-10 18:45:10 +01:00
Daniel Tschinder
98e397ce44 Add additional number lexing test
ref: graphql/graphql-js#72421378550cf51b13c6db59b8fc912591fd1a4b
2018-02-10 18:45:06 +01:00
Daniel Tschinder
4e26de3588 Support for union types when using buildSchema
* Adds support for resolving union/interface types when using a generated schema
* Move resolveType __typename checking into defaultResolveType
* Clean up existing tests and improve error messages

ref: graphql/graphql-js#947

# Conflicts:
#	src/Utils/BuildSchema.php
#	tests/Utils/BuildSchemaTest.php
2018-02-10 18:45:01 +01:00
Daniel Tschinder
7705e50e44 Fix print of block string with leading space and quotation
ref: graphql/graphql-js#1190
2018-02-10 18:44:56 +01:00
Daniel Tschinder
022c490011 RFC: Descriptions as strings
As discussed in facebook/graphql#90

This proposes replacing leading comment blocks as descriptions in the schema definition language with leading strings (typically block strings).

While I think there is some reduced ergonomics of using a string literal instead of a comment to write descriptions (unless perhaps you are accustomed to Python or Clojure), there are some compelling advantages:

* Descriptions are first-class in the AST of the schema definition language.
* Comments can remain "ignored" characters.
* No ambiguity between commented out regions and descriptions.

Specific to this reference implementation, since this is a breaking change and comment descriptions in the experimental SDL have fairly wide usage, I've left the comment description implementation intact and allow it to be enabled via an option. This should help with allowing upgrading with minimal impact on existing codebases and aid in automated transforms.

BREAKING CHANGE: This does not parse descriptions from comments by default anymore and the value of description in Nodes changed from string to StringValueNode
2018-02-10 18:44:51 +01:00
Daniel Tschinder
e65638f6f4 Improvements to printing block strings
ref: graphql/graphql-js#f9e67c403a4667372684ee8c3e82e1f0ba27031b
2018-02-10 18:44:45 +01:00
Daniel Tschinder
8747ff8954 RFC: Block String
This RFC adds a new form of `StringValue`, the multi-line string, similar to that found in Python and Scala.

A multi-line string starts and ends with a triple-quote:

```
"""This is a triple-quoted string
and it can contain multiple lines"""
```

Multi-line strings are useful for typing literal bodies of text where new lines should be interpretted literally. In fact, the only escape sequence used is `\"""` and `\` is otherwise allowed unescaped. This is beneficial when writing documentation within strings which may reference the back-slash often:

```
"""
In a multi-line string \n and C:\\ are unescaped.
"""
```

The primary value of multi-line strings are to write long-form input directly in query text, in tools like GraphiQL, and as a prerequisite to another pending RFC to allow docstring style documentation in the Schema Definition Language.

Ref: graphql/graphql-js#926
2018-02-10 18:43:26 +01:00
Daniel Tschinder
46816a7cda Uniform parsing of queries with short-hand syntax with regular queries 2018-02-09 14:33:04 +01:00
Daniel Tschinder
eb9ac66af8 Fix how TypeInfo handles inline fragments without type
ref: graphql/graphql-js#1041
2018-02-09 14:32:44 +01:00
Vladimir Razuvaev
94525c0025
Merge pull request #234 from mcg-web/some-fixes
Some fast fixes
2018-02-08 11:39:15 +07:00
Jeremiah VALERIE
944ccebc08
Fix Utils::printSafe with bool true 2018-02-02 08:18:46 +01:00
Vladimir Razuvaev
ccecc3ce1b
Merge pull request #232 from Poky85/fix-content-type-header
Fix Standard Server PSR request parsing
2018-01-22 13:21:18 +07:00
Jiri Pokorny
dca2091351 Fix content-type header match when parsing PSR request. 2018-01-21 21:02:43 +01:00
Vladimir Razuvaev
7310b25730 Switched license to MIT for full compatibility with graphql-js 2018-01-19 14:12:52 +07:00
Vladimir Razuvaev
8cd154776e Added graphql-upload to the list of complementary tools 2018-01-19 13:58:07 +07:00
Vladimir Razuvaev
4f223ba11d Fixed PSR request parsing, broken after recent changes 2018-01-13 18:08:07 +07:00
Vladimir Razuvaev
918bbff2bd
Merge branch 'master' into no-parsing 2018-01-13 17:20:00 +07:00
Vladimir Razuvaev
9944a689bf Exclude nulls from serialized AST 2018-01-13 16:25:06 +07:00
Vladimir Razuvaev
8b17953fe5 Fixed bug preventing use of parser noLocation option for serialization / deserialization 2018-01-13 15:45:09 +07:00
Vladimir Razuvaev
61f6ccfe76
Merge pull request #222 from giansalex/master
Exclude files in release
2018-01-02 22:43:31 +07:00
Giancarlos Salas
90978ea78d
update ignore files 2018-01-02 10:02:18 -05:00
Vladimir Razuvaev
7c3737609f Doc-block fix 2018-01-01 22:12:51 +07:00
Vladimir Razuvaev
b534bfbbf1 Docs: Mention PSR15-compliant middleware in complementary tools section 2018-01-01 22:07:15 +07:00
Vladimir Razuvaev
88db2f8a21
Merge pull request #224 from PowerKiKi/concise-tests
Leverage PHPUnit `setExpectedException()` instead of custom code
2018-01-01 21:33:08 +07:00
Adrien Crivelli
5cbaf973e1
Leverage PHPUnit setExpectedException() instead of custom code
Closes #219
2018-01-01 18:06:08 +09:00
Giancarlos Salas
5c6f69c254 exclude files in release 2017-12-29 21:01:42 -05:00
Adrien Crivelli
178b179db3
Drop support non pre-parsed PSR-7 request body
This revert #202 (commit 9d37f4c) because trying to parse PSR-7 request
was a mistake. The whole point of PSR-7 is to allow for interoperability
and be able to use specialized libs for body parsing (amongst many other
things). Trying to parse ourselves would be opening a can of worm if/when
other content types have to be supported. It is more correct and future
safe to require that the body is parsed before being passed to GraphQL.
2017-12-21 15:01:57 +09:00
Vladimir Razuvaev
a3f18b51f7
Merge pull request #211 from enumag/patch-2
indentation fix
2017-12-13 14:24:29 +07:00
Jáchym Toušek
f6c3fe3758
indentation 2017-12-12 13:36:20 +01:00
Vladimir Razuvaev
b97cad0f4a v0.11.5 2017-12-12 16:03:21 +07:00
Vladimir Razuvaev
bb75586aa5
Merge pull request #210 from enumag/patch-1
Allow objects with __toString in IDType
2017-12-12 16:02:09 +07:00
Jáchym Toušek
9e2c1dae87
Add test 2017-12-12 09:53:15 +01:00
Jáchym Toušek
25e341e9d9
Allow objects with __toString in IDType 2017-12-12 08:56:03 +01:00
Vladimir Razuvaev
3536280fac v0.11.4 2017-11-28 20:27:06 +07:00
Vladimir Razuvaev
9c563d5c00
Merge pull request #199 from roippi/findbreakingchanges
port findBreakingChanges
2017-11-28 20:14:36 +07:00
Vladimir Razuvaev
ea05c92723 v0.11.3 2017-11-28 12:38:56 +07:00
Vladimir Razuvaev
0af2fe79f2 StandardServer: a bit more validation for parsed json PSR-7 request (related to #202) 2017-11-28 12:28:54 +07:00
Vladimir Razuvaev
9d37f4c0d9
Merge pull request #202 from PowerKiKi/support-non-parsed-psr7
Support non pre-parsed PSR-7 request body
2017-11-28 12:01:15 +07:00
Adrien Crivelli
11c9429fab
Support non pre-parsed PSR-7 request body
Because PSR-7 specification only specify that `getParsedBody()` **may**
return the parsed body for `application/json`, we cannot assume that it
is always the case. So if the value returned parsed body is an empty array,
it means we should try to parse it ourselves (`null` would mean no body at
all according to spec).

With this modification we try to used given parsed body, but fallback on
trying to parse the body if necessary. This leave the door open to custom
implementation of parsing if needed, while making it easier to use out of
the box.
2017-11-26 19:57:32 +09:00
Ben Roberts
b18dfd670f testFindsAllDangerousChanges 2017-11-21 12:30:18 -05:00
Ben Roberts
533b8b8b5f testDetectsAdditionsToUnionType 2017-11-21 12:18:28 -05:00
Ben Roberts
e0a63ec792 testDetectsEnumValueAdditions 2017-11-21 11:53:14 -05:00
Ben Roberts
c4ae03454a testFindDangerousArgChanges 2017-11-21 11:50:11 -05:00
Ben Roberts
3c0ed787ba testDetectsAllBreakingChanges 2017-11-21 11:33:45 -05:00
Ben Roberts
dbccf9b196 testDetectsRemovalOfInterfaces 2017-11-20 14:39:06 -05:00
Ben Roberts
0bb689d340 testArgsThatMoveAwayFromNonNull 2017-11-20 14:12:26 -05:00
Ben Roberts
90f35f26a2 testDoesNotFlagArgsWithSameTypeSignature 2017-11-20 13:57:52 -05:00
Ben Roberts
0fd5abc833 testDetectsAdditionOfFieldArg 2017-11-20 13:48:21 -05:00
Ben Roberts
42d8ac07f9 testDetectsFieldArgumentTypeChange 2017-11-20 12:52:49 -05:00
Ben Roberts
4ea6cbe839 bugfix var ref 2017-11-17 17:35:33 -05:00
Ben Roberts
fc9c5e85aa testDetectsRemovalOfFieldArgument 2017-11-17 17:18:26 -05:00
Ben Roberts
cac011246e testDetectsValuesRemovedFromEnum 2017-11-17 16:24:04 -05:00
Ben Roberts
98ce1ccc69 testDetectsIfTypeWasRemovedFromUnion 2017-11-17 16:08:44 -05:00
Ben Roberts
dde2747918 testDetectsNonNullFieldAddedToInputType 2017-11-17 15:56:53 -05:00
Ben Roberts
cf4cccf4d6 testShouldDetectInputFieldChanges 2017-11-17 15:43:16 -05:00
Ben Roberts
68dbcc9ca3 testShouldDetectFieldChangesAndDeletions test 2017-11-17 14:29:47 -05:00
Ben Roberts
b2b5d6f080 findTypesThatChangedKind test 2017-11-17 13:04:01 -05:00
Ben Roberts
d9ce567cc8 findRemovedTypes test 2017-11-17 11:21:05 -05:00
Ben Roberts
4207adc098 change fns to static 2017-11-17 10:54:18 -05:00
Ben Roberts
6bdb7b7f80 improve docstrings 2017-11-16 17:44:08 -05:00
Ben Roberts
af60f1ee4d finish mechanical conversions 2017-11-16 17:42:38 -05:00
Ben Roberts
a1325eeb3f top level API functions, docstrings 2017-11-16 15:53:20 -05:00
Ben Roberts
55f6d6cf47 interfaces and enums 2017-11-16 15:25:25 -05:00
Ben Roberts
6e95b81aee dangerous changes consts 2017-11-16 14:17:21 -05:00
Ben Roberts
e649ef307a couple more functions 2017-11-16 14:15:39 -05:00
Ben Roberts
3811181f49 some functions converted over 2017-11-16 13:53:01 -05:00
Ben Roberts
7aebf2dbf7 initial porting 2017-11-15 16:12:56 -05:00
Vladimir Razuvaev
eaadae4a5b Merge pull request #187 from gabidj/patch-1
Fixed minor bug in example
2017-10-19 12:02:46 +07:00
Gabi DJ
9b449745ab Update graphql.php 2017-10-18 16:34:14 +03:00
Gabi DJ
7f54b1f7e3 removed redundant code and comments
removed redundant code and comments from PR
2017-10-18 16:32:43 +03:00
Gabi DJ
cf3ca86246 $data Unsupported operand types error fix
the operator `+=` only works like `array_merge ($left, $right) ;` if both variables are arrays
2017-10-17 16:47:26 +03:00
Vladimir Razuvaev
57f5ee3783 Fixed typo in docs, see #185 2017-10-16 23:06:21 +07:00
Vladimir Razuvaev
b17b1c3336 Merge remote-tracking branch 'origin/master' 2017-10-14 00:45:55 +07:00
Vladimir Razuvaev
1487741f37 Preserve description for custom scalars (#181) 2017-10-14 00:45:23 +07:00
Vladimir Razuvaev
0c3a657800 Merge pull request #177 from GabrielDeveloper/develop
Improve test coverage
2017-10-05 19:58:23 +07:00
Gabriel Goncalves
440d38d3bc Creating test to Language\Token 2017-10-04 12:06:25 -03:00
Vladimir Razuvaev
fb0ca607e2 Fixed a mistake in the type language documentation (#176) 2017-09-29 20:07:36 +07:00
Vladimir Razuvaev
cb40e111a3 v0.11.1 2017-09-22 23:16:34 +07:00
Vladimir Razuvaev
f7248dec76 Ability to override internal types (using types option of Schema class) #174 2017-09-22 23:08:51 +07:00
Vladimir Razuvaev
d46ad09108 v0.11.0 2017-09-20 19:50:23 +07:00
Vladimir Razuvaev
c5efd1d65b Additional checks for possible fragment spreads 2017-09-20 19:06:04 +07:00
Vladimir Razuvaev
1e34982bda Additional tests for variable coercion + use printSafeJson vs printSafe for input variables 2017-09-20 18:40:45 +07:00
Vladimir Razuvaev
a1e06b2e61 Account for query offset in files for errors 2017-09-20 18:36:19 +07:00
Vladimir Razuvaev
6050af4e67 Add support for directives applied on IDL & Schema 2017-09-20 17:43:06 +07:00
Vladimir Razuvaev
39f378ece7 Fixed type in tests: Testcase -> TestCase 2017-09-20 17:03:22 +07:00
Vladimir Razuvaev
5f5c8118c0 Fixed parseValue of StringType and IDType: (it should return null on invalid value, not throw) 2017-09-20 16:38:02 +07:00
Vladimir Razuvaev
2023b427ae Fixed failed integer test (only fails on some OSs and PHP versions) 2017-09-20 16:20:51 +07:00
Vladimir Razuvaev
c97438cd7d Merge branch 'master' of https://github.com/webonyx/graphql-php 2017-09-20 16:16:38 +07:00
Vladimir Razuvaev
537dbabe8f Merge pull request #171 from dereklavigne18/query_variable_coercion
Update query variable coercion to meet the rules outlined in the specification.
2017-09-20 15:57:29 +07:00
Derek Lavigne
d22385cc93 Update query variable coercion to meet the rules outlined in the
specification.

The framework currently coerces query variables similar to the way it
treats output values, which means it attempts to coerce the value into
the field's corresponding data type regardless of the received value.
According to items 3f and 3g in section 6.1.2
(http://facebook.github.io/graphql/#sec-Validating-Requests) of
Facebook's GraphQL specification query variables should be coerced
according to their type's input coercion rules laid out in section
3.1.1 (http://facebook.github.io/graphql/#sec-Scalars). If the value
can not be coerced into the correct type according the the input
coercion rules for the type a query error should be thrown. This
ensures that client provided query variables were of the correct format
and will be a valid format and type by the time they are passed into an
implementing resolver.

This patch fixes the above issue by updating the way query variables
are sanitized during the process of parsing the query. It directly
follows the rules for scalar input coercion laid out by the
specification and throws query errors when a value that cannot be
coerced to the correct type is given. Tests for isValidPHPValue will
also be updated to ensure that it is doing the correct type checks on
Values::isValidPHPValue for the given type and value provided. A new
test case will also be added to test Values::getVariableValues and make
sure it is also enforcing the scalar input coercion rules and throwing
errors for invalid values.
2017-09-18 12:14:09 -04:00
Vladimir Razuvaev
79ebc54538 Minor docs improvements 2017-09-05 15:29:46 +07:00
Vladimir Razuvaev
7cc863df37 v0.10.2 2017-08-30 23:33:58 +07:00
Vladimir Razuvaev
6ff427d241 Server: do not raise an error when variables are passed as empty string (#156) 2017-08-30 23:26:45 +07:00
Vladimir Razuvaev
46477c75c4 Added entry about graphql.org in the README + minor README tweaks 2017-08-22 17:24:17 +07:00
Vladimir Razuvaev
524a01a3a4 Fixed typos and mistakes in README 2017-08-22 17:11:48 +07:00
Vladimir Razuvaev
2ccc631ff3 Added CONTRIBUTING.md and updated README.md 2017-08-22 17:05:54 +07:00
Vladimir Razuvaev
1eb2ccac76 v0.10.1 2017-08-21 01:27:58 +07:00
Vladimir Razuvaev
d95fb461ee Server: fixed constructor screwed during rebasing + restored tests for the server 2017-08-21 01:18:23 +07:00
Vladimir Razuvaev
a1652468f0 Fixed minor error in docblock comment 2017-08-21 00:19:07 +07:00
Vladimir Razuvaev
df1b575469 Changed CHANGELOG documentation links to documentation site 2017-08-20 23:25:48 +07:00
Vladimir Razuvaev
6fdcfd9bb0 Merge branch 'master' of https://github.com/webonyx/graphql-php into v0.10 2017-08-20 23:16:51 +07:00
Vladimir Razuvaev
cbc744ea08 Added entries about deprecation of old server and type resolution strategies to UPGRADE.md document 2017-08-20 23:16:23 +07:00
Vladimir Razuvaev
1f68909eb0 Added CHANGELOG document 2017-08-20 23:15:22 +07:00
Vladimir Razuvaev
a3b6974249 Added entry in docs about schema config class 2017-08-20 22:59:40 +07:00
Vladimir Razuvaev
90602b31ba Added link to docs for generic error about unique type instance (#149) 2017-08-20 22:28:17 +07:00
Vladimir Razuvaev
7f346d5658 Documentation improvements 2017-08-20 22:10:37 +07:00
Vladimir Razuvaev
de791536ce Docblocks improvements 2017-08-20 22:10:13 +07:00
Vladimir Razuvaev
d5e3d08d85 Minor Executor tweaks 2017-08-20 20:09:55 +07:00
Vladimir Razuvaev
085516bdda Moved GraphQL\Language\AST\Node::fromArray to GraphQL\Utils\AST::fromArray 2017-08-20 19:50:44 +07:00
Vladimir Razuvaev
bd444752f8 Documentation and docblock improvements 2017-08-20 16:00:44 +07:00
Vladimir Razuvaev
199caf3080 New example for server usage 2017-08-20 02:33:16 +07:00
Vladimir Razuvaev
71343f2f62 Server: Extracted method for emitting response 2017-08-20 02:32:50 +07:00
Vladimir Razuvaev
8098b2b886 Fixing examples 2017-08-20 02:31:11 +07:00
Vladimir Razuvaev
009cdecb94 Reference docs generated from docblocks 2017-08-19 23:02:34 +07:00
Vladimir Razuvaev
1b4f983f3f Improved docblock comments (suitable for reference docs generation) 2017-08-19 23:01:46 +07:00
Vladimir Razuvaev
3ef2d2827b Added psr/http-message as a dev dependency to ensure that unit tests run without additional deps installation steps 2017-08-19 22:58:44 +07:00
Vladimir Razuvaev
0af1fb2793 Reference docs generator 2017-08-19 22:57:26 +07:00
Vladimir Razuvaev
ed66291308 Merge pull request #152 from mcg-web/add-hooks-while-completing-sync-promise
Add hooks to helps promise completion with custom backend
2017-08-18 22:30:03 +07:00
Jeremiah VALERIE
1c143360ca Add hooks to helps promise completion with custom backend 2017-08-18 16:18:48 +02:00
Vladimir Razuvaev
99356f7faf Docs on security 2017-08-18 20:56:47 +07:00
Vladimir Razuvaev
4f374bca83 Docs about custom validation rules 2017-08-18 20:56:34 +07:00
Vladimir Razuvaev
203fddfe4e Abstract base class for validation rules 2017-08-18 20:56:04 +07:00
Vladimir Razuvaev
9499e5ae8e Suppressing Config deprecation warning in tests 2017-08-18 18:07:23 +07:00
Vladimir Razuvaev
637156fe65 Further documentation improvements 2017-08-18 02:56:22 +07:00
Vladimir Razuvaev
2537a62ec2 Documented server usage 2017-08-18 02:56:07 +07:00
Vladimir Razuvaev
2bfce65484 Fixed Parser docblock comments 2017-08-18 02:55:22 +07:00
Vladimir Razuvaev
83cc9132a0 Server: minor improvements 2017-08-18 02:54:35 +07:00
Vladimir Razuvaev
d578b8a22f Split sync and async facade methods 2017-08-18 01:49:10 +07:00
Vladimir Razuvaev
c04d037fb1 Documentation improvements 2017-08-17 20:35:58 +07:00
Vladimir Razuvaev
c65d8d8624 Documented lazy loading of types in schema and ability to define schema using GraphQL type language 2017-08-17 20:35:35 +07:00
Vladimir Razuvaev
e52fe8c384 BuildSchema::build() now accepts DocumentNode as well 2017-08-17 20:33:36 +07:00
Vladimir Razuvaev
03629c1e3c Refactored error formatting (debugging part) 2017-08-17 18:49:17 +07:00
Vladimir Razuvaev
1d38643538 Ability to re-throw resolver exceptions 2017-08-17 03:01:23 +07:00
Vladimir Razuvaev
a2be92937e Documentation improvements (wip) 2017-08-17 02:17:01 +07:00
Vladimir Razuvaev
b294329a40 Added reference section to docs 2017-08-17 02:16:45 +07:00
Vladimir Razuvaev
51e877bfba Improving docblock comments 2017-08-17 02:15:49 +07:00
Vladimir Razuvaev
b4d767bad6 Moved typeFromAST() to AST util 2017-08-17 02:14:55 +07:00
Vladimir Razuvaev
a50c9a4c1f Renamed error category constants 2017-08-17 02:13:44 +07:00
Vladimir Razuvaev
4634f214ea Revamping GraphQL facade methods 2017-08-17 02:12:37 +07:00
Vladimir Razuvaev
b56083b7de Improved flexibility of error handling 2017-08-17 02:11:21 +07:00
Vladimir Razuvaev
1ee226465b Server: added missing error message 2017-08-16 19:22:45 +07:00
Vladimir Razuvaev
f369d4e2d4 Reverted unnecessary breaking change 2017-08-15 23:39:07 +07:00
Vladimir Razuvaev
88c959edad Server: fixed broken Travis build 2017-08-15 21:56:21 +07:00
Vladimir Razuvaev
3971001f6d Server: additional tests + related fixes 2017-08-15 20:59:48 +07:00
Vladimir Razuvaev
828c6b0fc3 Server: disable query batching by default; allow array as server config 2017-08-15 18:05:09 +07:00
Vladimir Razuvaev
9931cde6d4 Tests for lazy type loading during query execution + related changed 2017-08-15 01:49:56 +07:00
Vladimir Razuvaev
e4813c3a05 Additional checks and tests for situations when user-land code returns duplicate type instances 2017-08-14 23:52:17 +07:00
Vladimir Razuvaev
8817d54e83 Validate that type loader always returns the same type instance as referenced in other parts of the schema 2017-08-14 22:32:07 +07:00
Vladimir Razuvaev
b9d3a11785 Extracted lazy schema test; minor related refactoring 2017-08-14 20:41:08 +07:00
Vladimir Razuvaev
f47db61907 Fully load all schema types in constructor when type loader is not set 2017-08-14 19:44:16 +07:00
Vladimir Razuvaev
884a8967f3 Type loader tests 2017-08-14 19:42:01 +07:00
Vladimir Razuvaev
20f8cab943 Removed schema descriptor (as lazy loading of types can work without it now) 2017-08-14 01:42:02 +07:00
Vladimir Razuvaev
f9eb14869f Removed callbacks in field types (previously deprecated in #35) 2017-08-14 00:50:24 +07:00
Vladimir Razuvaev
6845b28a35 Deprecated GraphQL\Type\Definition\Config (#148) 2017-08-14 00:09:02 +07:00
Vladimir Razuvaev
ed3591c1a9 Fixed broken build 2017-08-13 23:24:23 +07:00
Vladimir Razuvaev
34eae0b891 Schema validation + tests (#148) 2017-08-13 23:04:03 +07:00
Vladimir Razuvaev
9d150c7702 Merge pull request #147 from swinyx/patch-1
Fixed typo in unions.md
2017-08-13 00:23:25 +07:00
Yannick Yeboue
658816180a Update unions.md
replace semi colon with a comma as it would cause a syntax error
2017-08-12 12:17:13 -04:00
Vladimir Razuvaev
d3580e959e Moved Schema to GraphQL\Type namespace (but preserved BC) 2017-08-12 21:40:03 +07:00
Vladimir Razuvaev
2c8c7baa87 Writing UPGRADE.md 2017-08-12 16:31:35 +07:00
Vladimir Razuvaev
f911fac7b1 Default error reporting now includes "category" key for every error 2017-08-08 02:02:07 +07:00
Vladimir Razuvaev
fa17d6c461 Merge pull request #145 from zeeshanu/patch-1
Fixed typo in README
2017-07-28 18:00:09 +07:00
Vladimir Razuvaev
09070485c1 Added ability to decorate type configs in BuildSchema + made type creation lazy 2017-07-28 17:55:25 +07:00
Vladimir Razuvaev
3a8301f6c6 Allow types schema option to be callable 2017-07-28 17:53:57 +07:00
Zeeshan Ahmed
7afd6d3f9a Fix typo 2017-07-28 11:24:15 +05:00
Vladimir Razuvaev
6a20483b87 Server: Ability to use thunk for root value and context 2017-07-27 20:11:45 +07:00
Vladimir Razuvaev
e7838d2253 Do not output trace for trivial errors even in debug mode 2017-07-25 20:08:34 +07:00
Vladimir Razuvaev
3e6f2c9e83 Fixed minor bugs 2017-07-25 19:26:41 +07:00
Vladimir Razuvaev
1af902865b AST: new NodeList class for collections of nodes (vs array) to enable effective conversion of libgraphqlparser output to our AST tree 2017-07-21 22:29:59 +07:00
Vladimir Razuvaev
e04d3300a7 Server: send result for regular responses + prepare response for PSR7 request 2017-07-21 22:11:20 +07:00
Vladimir Razuvaev
b2ec265d4f Server: ability to execute PSR7 request 2017-07-19 23:35:22 +07:00
Vladimir Razuvaev
87c812b221 Better located error handling 2017-07-19 20:08:42 +07:00
Vladimir Razuvaev
81986145fe Server: moving things around to simplify 2017-07-19 19:55:22 +07:00
Vladimir Razuvaev
08a68d4857 Throwing GraphQL\Error\Error vs GraphQL\Error\UserError from type definitions 2017-07-19 19:39:10 +07:00
Vladimir Razuvaev
e6e531b88b Server: throw only when there is a configuration or logic error (invariant violation) 2017-07-19 19:30:39 +07:00
Vladimir Razuvaev
90088c7bde Note in UPGRADE.md about new base class of GraphQL\Error\UserError 2017-07-19 15:52:00 +07:00
Vladimir Razuvaev
38922dbbed Default error formatter now returns "Internal server error" unless error is client-aware and safe to report directly to end-users 2017-07-18 20:57:30 +07:00
Vladimir Razuvaev
fbcd20814a Scalar type serialize method now throws InvariantViolation and parseValue throws UserError 2017-07-18 20:52:39 +07:00
Vladimir Razuvaev
8e3d1eb29b Merge branch 'master' of https://github.com/webonyx/graphql-php into v0.10 2017-07-18 00:42:52 +07:00
Vladimir Razuvaev
8fe26a1a21 String and ID types should not try to convert non-scalar values to string (#121) 2017-07-18 00:25:45 +07:00
Vladimir Razuvaev
49208d758d Server: use proper batching helper 2017-07-17 23:48:30 +07:00
Vladimir Razuvaev
919cf80240 Server: batched queries with shared deferreds (promises) #105 2017-07-17 20:31:26 +07:00
Vladimir Razuvaev
24ffd605f4 New method "Executor::promiseToExecute()" which always returns promise (even for SyncPromiseAdapter) 2017-07-17 19:47:29 +07:00
Vladimir Razuvaev
0e2ac57515 Split HTTP server execution to canonical replaceable steps: parsing, validation, execution with separate tests for each step 2017-07-17 16:57:30 +07:00
Vladimir Razuvaev
f8c3195e54 Granular methods for HTTP request parsing + tests 2017-07-16 19:04:58 +07:00
Vladimir Razuvaev
d2cbb0c354 Restored tests for deprecated GraphQL\Server 2017-07-14 20:06:24 +07:00
Vladimir Razuvaev
794d3672ef Initial pass on standard server implementation (also deprecated current GraphQL\Server which is undocumented anyway) 2017-07-14 19:44:18 +07:00
Vladimir Razuvaev
a3b40db0fb Enhanced Utils::printSafe() to output more information about arrays 2017-07-14 15:16:43 +07:00
Vladimir Razuvaev
3f04d29628 Ability to pass custom set of validation rules when executing a query (including empty set to skip validation) #134 2017-07-13 20:33:09 +07:00
Vladimir Razuvaev
37a42ededd Query validation should pass if empty array of rules is provided 2017-07-13 02:39:24 +07:00
Vladimir Razuvaev
65d9472b0b Moved PromiseAdapter to ExecutionContext; allow passing it to Executor::execute() directly vs setting statically 2017-07-12 19:44:04 +07:00
Vladimir Razuvaev
bf0a7a8e2b Change minimum PHP version requirement from 5.4.0 to 5.5.0 (to enable try-finally and generators) 2017-07-12 17:46:43 +07:00
Vladimir Razuvaev
aaa5b7af41 Global config; descriptor moved to appropriate namespace; minor cleanup 2017-07-12 13:16:34 +07:00
Vladimir Razuvaev
5e6acb60a6 Merge pull request #142 from symm/shorthand-docs
Add example for building schema from IDL
2017-07-11 21:49:49 +07:00
Gareth Jones
81376e7c34
Document shorthand usage 2017-07-10 21:14:34 +01:00
Vladimir Razuvaev
296544089c Moved GraphQL\Utils to GraphQL\Utils\Utils 2017-07-10 19:53:46 +07:00
Vladimir Razuvaev
ed28deda81 Replaced trigger_error with Warning for resolveType warning 2017-07-10 19:38:12 +07:00
Vladimir Razuvaev
9551569ffe Merge branch 'lazy-types' into v0.10
# Conflicts:
#	src/Executor/Executor.php
2017-07-10 17:11:41 +07:00
Vladimir Razuvaev
463d995d95 Reverted #116 (now Executor::defaultFieldResolver checks for instanceof Closure vs is_callable again) 2017-07-08 15:22:17 +07:00
Vladimir Razuvaev
3beeb06340 Merge branches 'master' and 'v0.10' of https://github.com/webonyx/graphql-php into v0.10
# Conflicts:
#	src/Utils/MixedStore.php
2017-07-06 19:50:23 +07:00
Vladimir Razuvaev
bc6a7a3d1d Fix: allow MixedStore to accept true, false, null and floats as keys + related tests 2017-07-06 19:29:33 +07:00
Vladimir Razuvaev
24bcc65314 SchemaPrinter: reverted sorting of fields in printed version (as it breaks s = parse(print(s)) rule) 2017-07-05 19:45:02 +07:00
Vladimir Razuvaev
ea94ee7515 Utility function getDirectiveValues + related refactoring 2017-07-05 19:33:25 +07:00
Vladimir Razuvaev
a79a51d445 Schema Parsing: allow leading pipe for union type definitions 2017-07-05 19:01:13 +07:00
Vladimir Razuvaev
30632050a5 Validation: added test cases to check support for good negative floats and ints 2017-07-05 18:35:45 +07:00
Vladimir Razuvaev
0b7d55c30d SchemaPrinter: sort fields before printing to get more stable diff 2017-07-05 18:33:16 +07:00
Vladimir Razuvaev
9b9a74c1d1 Spec compliance: errors in buildExecutionContext() are caught and included in result rather than thrown 2017-07-05 17:33:59 +07:00
Vladimir Razuvaev
678cf5d0bf Renamed argument of GraphQL::execute() 2017-07-05 16:26:02 +07:00
Vladimir Razuvaev
78d9ba0d5e Allow providing default field resolver for execution call 2017-07-05 16:22:01 +07:00
Vladimir Razuvaev
9f4980ce49 Fix for broken build on PHP < 5.6 2017-07-04 22:18:55 +07:00
Vladimir Razuvaev
b47c87f793 Allow null values for enums 2017-07-04 20:19:52 +07:00
Vladimir Razuvaev
f569c6de2d Spec compliance: coercion of Int values 2017-07-04 18:27:20 +07:00
Vladimir Razuvaev
90e1ea4d22 Added tools for warnings with ability to suppress them 2017-07-04 17:13:05 +07:00
Vladimir Razuvaev
88b85c9761 Make 'errors' top property in response array 2017-07-04 16:29:30 +07:00
Vladimir Razuvaev
1c41fb27ed Added test for enums with null values 2017-07-04 16:27:40 +07:00
Vladimir Razuvaev
189877c173 Allow passing custom TypeInfo to validate 2017-07-04 16:19:16 +07:00
Vladimir Razuvaev
a53b798f29 Minor spelling fix 2017-07-04 16:12:56 +07:00
Vladimir Razuvaev
fc9ad7e37a Unify wording in error messages 2017-07-04 14:02:35 +07:00
Vladimir Razuvaev
76e182e616 Forbid duplicate type definitions 2017-07-04 14:01:00 +07:00
Vladimir Razuvaev
c3db8de9e7 Note about dunderscore becoming hard error in next version 2017-07-04 13:58:56 +07:00
Vladimir Razuvaev
3e1fc1a922 Refactored isThenable checks in Executor 2017-07-04 13:47:50 +07:00
Vladimir Razuvaev
32376dd6ee Fix: type safety for TypeInfo 2017-07-04 13:25:01 +07:00
Vladimir Razuvaev
4c96193027 Fix: isLeafType should not return true for ListOfType and NonNull wrappers 2017-07-04 13:17:09 +07:00
Vladimir Razuvaev
c5484ae6f9 Convert error to warning for non-compliant usage of __ in names 2017-07-04 00:28:17 +07:00
Vladimir Razuvaev
34bd378c7e Refactored executor logic related to isTypeOf 2017-07-04 00:09:32 +07:00
Vladimir Razuvaev
29c1132554 getValue() for EnumType (and getEnumValue() for TypeInfo) 2017-07-03 23:23:12 +07:00
Vladimir Razuvaev
14ef8ef835 Fixed typo in buildSchema 2017-07-03 23:09:50 +07:00
Vladimir Razuvaev
d64c352262 Ability for interface types to resolve type asynchronously 2017-07-03 23:08:20 +07:00
Vladimir Razuvaev
445f579f09 Include expected type in isValidPHPValue error message 2017-07-03 18:24:58 +07:00
Vladimir Razuvaev
e30f2a99cf Pass ResolveInfo in default field resolver when value is callable 2017-07-03 18:14:45 +07:00
Vladimir Razuvaev
faf81ef18a Catch exceptions in isValidPHPValue and return corresponding error message 2017-07-03 18:12:12 +07:00
Vladimir Razuvaev
7937b15855 Fixed several typos 2017-07-03 18:04:32 +07:00
Vladimir Razuvaev
b471938f16 Consistent validation of type names + reject names starting with __ 2017-07-03 18:04:08 +07:00
Vladimir Razuvaev
f668300cd8 Fixed minor bug in blog example (#114) 2017-06-27 20:04:12 +07:00
Vladimir Razuvaev
b147b528e2 Merge branch 'v0.10' of https://github.com/webonyx/graphql-php into v0.10 2017-06-27 16:41:31 +07:00
Vladimir Razuvaev
21e3445754 Merge pull request #116 from leocavalcante/resolve-callables
Resolve callables by default
2017-06-27 00:37:30 +07:00
Vladimir Razuvaev
1657f0e9bd Merge branch 'master' into v0.10 2017-06-26 17:47:36 +07:00
Vladimir Razuvaev
d18cb84ec4 Merge pull request #127 from danez/throwable
Support PHP7 error exceptions everywhere
2017-06-25 21:56:00 +07:00
Daniel Tschinder
65ef159ddc
Support PHP7 error exceptions everywhere
Also replace \Error with \Throwable
2017-06-25 07:33:28 -07:00
Vladimir Razuvaev
61368c59f2 Merge branch 'master' of https://github.com/webonyx/graphql-php 2017-06-24 22:45:40 +07:00
Vladimir Razuvaev
a0657b7847 Refactored Lexer algorithm for better performance. Now O(N) vs O(N^2) previously (#137) 2017-06-24 22:42:55 +07:00
Vladimir Razuvaev
141afc1cf7 Merge pull request #131 from n1ru4l/feature-disable-introspection-validation-rule
Add DisableIntrospection validation rule
2017-06-19 13:35:23 +07:00
Vladimir Razuvaev
393a741d5e Merge pull request #132 from simPod/master
Fixed incorrect type hints in method docs
2017-06-19 13:33:49 +07:00
Simon Podlipsky
6a4785dc4a
Fixed incorrect type hints in method docs 2017-06-18 12:29:29 +02:00
Laurin Quast
beaf91d080 Adds example for enabling the DisableIntrospection rule. 2017-06-17 15:01:37 +02:00
Laurin Quast
719a438628 Adds DisableIntrospection validation rule to the DocumentValidator (default: disabled) 2017-06-17 14:51:38 +02:00
Laurin Quast
1c62f554ae Implements DisableIntrospection validation rule 2017-06-17 14:50:24 +02:00
Laurin Quast
6d6d1ac01b Adds tests for DisableIntrospection Validation rule 2017-06-17 14:49:07 +02:00
Vladimir Razuvaev
ffc4542cd0 Improvements in config validation 2017-06-12 22:48:48 +07:00
Vladimir Razuvaev
e7cde5ecf3 Fixed broken 5.4 build 2017-06-03 17:17:42 +07:00
Vladimir Razuvaev
bc4b990946 Do not run query complexity validation if there were other validation errors (as it will throw and mess up previous validation results), see #125 2017-06-03 17:07:01 +07:00
Vladimir Razuvaev
bc6c0e2eea Reverted float literal parsing, as it was not the cause of #125 2017-06-03 16:48:29 +07:00
Vladimir Razuvaev
8c9a2a5f12 Travis tweaks: use composer version of phpunit vs phar version as HHVM will mess with paths within phars resulting in broken builds 2017-06-03 15:34:24 +07:00
Vladimir Razuvaev
56eaaa2400 Attempting to fix broken HHVM build in Travis 2017-06-03 15:01:30 +07:00
Vladimir Razuvaev
4eb68bf63f Do not parse invalid input strings as floats (see #125) 2017-06-03 14:04:47 +07:00
Vladimir Razuvaev
6fb62b25b3 Fixed "out of memory" error for edge cases of resolveType() calls 2017-05-29 22:54:35 +07:00
Vladimir Razuvaev
154fdfee11 Merge pull request #124 from mcg-web/use-dedicated-exception-for-type-parsing-error
Throw UserError vs InvariantViolationError for errors caused by client
2017-05-29 22:32:10 +07:00
Jeremiah VALERIE
6d5b4e5a37 Use dedicated exception for scalar type parsing error 2017-05-29 10:32:48 +02:00
Vladimir Razuvaev
53edfa0f84 Merge pull request #117 from shinderohitt/patch-1
Fix commands for hello world example
2017-05-16 17:20:42 +07:00
Rohit Shinde
06183635f5 Fix test commands 2017-05-11 13:35:25 +05:30
leocavalcante
8a5f337469 Extract anonymous class 2017-05-07 19:42:59 -03:00
leocavalcante
e07c86bd5e Default resolve callables 2017-05-07 19:21:20 -03:00
Vladimir Razuvaev
848f9c3edf Preserve backwards compatibility of Ast\Node::toArray(): do shallow conversion by default 2017-04-25 18:02:45 +07:00
Vladimir Razuvaev
835e4e6b2d Removed unnecessary loc entry in AST\Node::toArray() 2017-04-25 18:00:06 +07:00
Vladimir Razuvaev
86f01ac1a6 Merge pull request #113 from pascaldevink/add_directives_to_complexity_calculation
Use directives when calculating query complexity
2017-04-25 14:49:13 +07:00
Pascal de Vink
11a1b13b72
Use directives to calculate query complexity 2017-04-24 14:21:58 +02:00
Vladimir Razuvaev
5948d5198e Merge pull request #106 from AndreasHeiberg/fix-to-string-node
Fix __toString() for AST Nodes
2017-04-24 18:04:02 +07:00
Andreas Heiberg
ed8bf4e2b2 fix __toString() for AST Node
previously it would only shallowly convert to array causing json_encode to fail
2017-04-07 11:34:33 +01:00
Vladimir Razuvaev
cb40df220e Merge pull request #101 from PowerKiKi/patch-1
Make Hello World compatible with GraphiQL
2017-03-22 13:41:59 +07:00
Adrien Crivelli
5ca69c6ec8
Make Hello World compatible with GraphiQL
This allow newcomers to follow documentation to get started and then
keep exploring with GraphiQL

Closes #87
2017-03-22 10:53:00 +09:00
Vladimir Razuvaev
ac5c518cbe Merge pull request #100 from PowerKiKi/patch-1
Simplify install instructions
2017-03-21 13:18:58 +07:00
Adrien Crivelli
070ab7d430 Simplify install instructions
Composer can create `composer.json` if missing, and letting 
composer pick the latest version by itself is more future proof than
hardcoding a version that need to be changed on each release.
2017-03-20 23:15:17 +09:00
Vladimir Razuvaev
7ef9f91672 Merge pull request #98 from decebal/master
Make ResolveInfo::getFieldSelection() to merge field sub-selections defined in different fragments
2017-03-16 23:51:38 +07:00
Decebal Dobrica
ca92ae4688 pass failing test 2017-03-16 15:41:29 +00:00
Decebal Dobrica
0bedebe392 fragment type caps 2017-03-16 15:09:09 +00:00
Decebal Dobrica
c51dda097a fix tests typo 2017-03-16 15:00:01 +00:00
Decebal Dobrica
b9550d3ecb replies aliased 2017-03-16 14:44:21 +00:00
Decebal Dobrica
7bed5ef4a8 add replies fragments 2017-03-16 14:43:47 +00:00
Decebal Dobrica
51e67d49c7 php merge for associative array non-integer intexed 2017-03-16 10:21:26 +00:00
Vladimir Razuvaev
f77bd17eba ResolveInfo: Take in account inline fragments in getFieldSelection() 2017-03-10 19:21:27 +07:00
Vladimir Razuvaev
ce9bf33f20 Ability to load types in schema on demand (#69) 2017-03-05 03:26:22 +07:00
Vladimir Razuvaev
d5866e194a Merge branch 'master' of https://github.com/webonyx/graphql-php into lazy-types 2017-03-04 17:36:28 +07:00
Vladimir Razuvaev
827bfa7907 Server: added missing return $this; statements 2017-02-27 18:19:09 +07:00
Vladimir Razuvaev
752668df8d Removed phpbench from composer to satisfy PHP5.4 requirement 2017-02-25 02:55:52 +07:00
Vladimir Razuvaev
66acb73a47 Added first meaningful benchmarks to have some grounds for future performance optimizations 2017-02-25 02:32:11 +07:00
Vladimir Razuvaev
34ca931533 Sanity check to ensure that GraphQL query is string 2017-02-24 17:14:30 +07:00
Vladimir Razuvaev
3f909e3e11 Separate utility for extractTypes 2017-02-24 16:29:28 +07:00
Vladimir Razuvaev
416733b4db Separate utility for extractTypes 2017-02-24 16:22:33 +07:00
Vladimir Razuvaev
dd4acfd3b6 Merge pull request #92 from webmozart/sync-promise-adapter-indices
Fixed SyncPromiseAdapter::all() to not change the order of arrays
2017-02-24 01:06:35 +07:00
Bernhard Schussek
e35b57601d Fixed SyncPromiseAdapter::all() to not change the order of arrays 2017-02-23 11:45:13 +01:00
Vladimir Razuvaev
ed41a4ce43 Merge pull request #91 from petecoop/build-schema
Build Schema & Schema Printer
2017-02-21 15:25:14 +07:00
petecoop
664af3d44a use constants 2017-02-20 10:31:11 +00:00
petecoop
fc629a292d BuildSchema and SchemaPrinter 2017-02-19 19:26:56 +00:00
Vladimir Razuvaev
0bd7c9d405 Merge pull request #89 from aldumas/assign_fix
Fixed Utils::assign() bug relating to detecting missing required keys
2017-02-14 02:51:33 +07:00
Adam Dumas
97674cbbb9 Fixed Utils::assign() bug relating to detecting missing required keys 2017-02-13 12:50:34 -05:00
Vladimir Razuvaev
c2f0749d8e Merge pull request #88 from jane-olszewska/schema-description-in-comments
Schema Definition Language: element descriptions can be set through comments
2017-02-07 13:01:36 +07:00
jane-olszewska
008fd20498 Added test for adding a schema element description in comments 2017-02-06 19:11:12 +00:00
jane-olszewska
060dc51595 Set schema element description to strings retrieved from a continuous chunk of comment tokens immediately preceding the element 2017-02-06 18:33:21 +00:00
jane-olszewska
dd31ab2b80 Added a description field to schema type definition classes 2017-02-06 18:33:18 +00:00
jane-olszewska
bbb5cf1e4d Fixed comment token extraction: will no longer read one character from the next line 2017-02-06 18:13:43 +00:00
vladar
c18cd16960 GraphQL\Server: set PromiseAdapter before executing query 2017-01-26 19:24:53 +07:00
vladar
d10e933a00 Merge branch 'master' of https://github.com/webonyx/graphql-php 2017-01-26 19:18:40 +07:00
vladar
c545474715 Fixed DocBlock comment 2017-01-26 19:00:10 +07:00
vladar
2ddae2bf88 Added EXPERIMENTAL note for code related to type resolution strategies 2017-01-26 18:59:14 +07:00
vladar
b4e6630c1d PHPUnit: Replaced deprecated method getMock() with getMockBuilder() 2017-01-26 18:48:23 +07:00
Vladimir Razuvaev
81d28f767a Merge pull request #86 from danez/patch-1
Fix typo in DocBlock
2017-01-26 17:57:06 +07:00
Daniel Tschinder
1ff9c429d6 Fix typo in DocBlock 2017-01-26 11:18:53 +01:00
vladar
fa9611738d v0.9.0 2017-01-19 19:28:41 +07:00
vladar
e2875953e1 Tests for new GraphQL\Server facade 2017-01-19 19:23:00 +07:00
vladar
8e75cc3d60 Merge branch 'master' of https://github.com/webonyx/graphql-php 2017-01-19 11:55:10 +07:00
Vladimir Razuvaev
ddea764ff5 Merge pull request #84 from OwlyCode/php7-errors-handling
Added compatibility with php7 error handling
2017-01-14 12:31:02 +07:00
OwlyCode
d022b19b4e Added compatibility with php7 error handling. 2017-01-12 17:18:25 +01:00
Vladimir Razuvaev
5be0944453 Merge pull request #83 from bertrandgauthier/master
Small correction in examples
2017-01-06 22:57:41 +07:00
bertrandgauthier
1adfe21185 Small correction in examples 2017-01-06 12:50:21 +01:00
bertrandgauthier
8df665610b Small correction in examples 2017-01-06 12:46:37 +01:00
vladar
ceaf798b07 Merge branch 'master' of https://github.com/webonyx/graphql-php 2016-12-23 22:42:05 +07:00
Vladimir Razuvaev
595ae52e85 Merge pull request #80 from lordthorzonus/nested-async-queries-mess-the-indexes-of-arrays
Nested async queries messes up the order of keys in arrays producing unwanted responses
2016-12-23 15:29:43 +07:00
Juuso Leinonen
8626e0b45d Make tests run on php 5.4 2016-12-22 15:35:38 +02:00
Juuso Leinonen
e3a864f071 Make travis run also the ReactPromiseAdapter tests 2016-12-22 15:24:47 +02:00
Juuso Leinonen
90c4b5d9fa Changed ReactPromiseAdapter::all to preserve the array key order and added tests 2016-12-22 15:08:28 +02:00
vladar
296cc7530d Added Server class as a new facade for library, including HTTP endpoint compatible with express-graphql format 2016-12-19 20:40:46 +07:00
vladar
ff3a40d329 Execution: modified deferred behavior to be more predictable (+added more tests for them) 2016-12-17 05:14:51 +07:00
vladar
9826f9a1f3 Docs: fixed version name 9.0 -> 0.9.0 2016-12-15 19:27:43 +07:00
vladar
9e8e77a071 Execution: Calling resolve function directly (vs calling it via call_user_func) 2016-12-15 15:34:27 +07:00
vladar
0483560ee5 Fixed wrong "current version" number in docs 2016-12-14 20:34:48 +07:00
vladar
c7688c9249 Added "since" entry to docs for N+1 problem and async support 2016-12-14 20:22:38 +07:00
vladar
8e39b3d88d Added entry on data fetching to docs 2016-12-14 20:19:12 +07:00
vladar
078a1efd50 Added GraphQL\GraphQL::setDefaultFieldResolver() 2016-12-14 20:18:10 +07:00
vladar
2043cc7e75 Introduced type resolution strategies: eager and lazy (for #69) 2016-12-14 17:12:09 +07:00
vladar
97f9c1c201 Added deprecated.php to PHPUnit exclude list 2016-12-08 06:06:17 +07:00
vladar
ae57a72461 Deleted unused SchemaValidator 2016-12-08 06:05:40 +07:00
vladar
90e29ac704 Fixing tests for HHVM 2016-12-08 04:50:33 +07:00
vladar
51d816d280 Tests for Config 2016-12-08 04:16:13 +07:00
vladar
c5bba0e7d4 Cleaning up old unused messages 2016-12-06 03:45:52 +07:00
vladar
a165c3aaab Shortened method names of PromiseAdapter 2016-12-04 15:53:23 +07:00
vladar
26db4c76c6 Fixed tests for PHP7+ 2016-12-03 04:31:01 +07:00
vladar
fd335a1d13 Execution: tests are green 2016-12-03 04:23:21 +07:00
vladar
2ffc14cee6 Removed ReactPHP dependency for dev environment (using own simple promises implementation) 2016-12-03 04:13:23 +07:00
vladar
7ab75cd05a Execution: tests are green 2016-12-03 04:11:14 +07:00
vladar
ab4ae779af Execution: fixed rejection issue in sync promise 2016-12-03 02:48:51 +07:00
vladar
e97ca7f971 Execution: added SyncPromiseAdapter and made it default for Executor (+removed GenericPromiseAdapter) 2016-12-03 02:14:14 +07:00
vladar
48d78412ec Execution: refactored promise adapters 2016-12-03 02:06:28 +07:00
vladar
3a375bb78e Simple implementation of Promises A+ for our sync case (using queue) 2016-12-02 18:43:54 +07:00
vladar
821e96508b Execution: added promiseForAssocArray method for cases when result contains promise 2016-12-01 18:29:50 +07:00
vladar
418ee48b20 Execution: moved then method to promise adapter 2016-12-01 17:52:42 +07:00
vladar
77244d3aab Refactored executor: now works as instance with exeContext and promise adapter as properties 2016-12-01 17:09:22 +07:00
Vladimir Razuvaev
f3fca81e9d Merge pull request #67 from mcg-web/promise
Initial implementation of promises (starting addressing N+1 problem)
2016-12-01 16:22:15 +07:00
Jeremiah VALERIE
76c31df1af Add Executor Promise tests 2016-11-26 23:51:42 +01:00
Jeremiah VALERIE
35d7d83088 Add Lists Promise tests 2016-11-26 22:28:46 +01:00
Jeremiah VALERIE
7f1e9d051b Add Mutation Promise tests 2016-11-26 20:50:01 +01:00
Jeremiah VALERIE
2ad79adf0c Add NonNull Promise tests 2016-11-26 20:50:01 +01:00
Jeremiah VALERIE
dd9062d77e Add a generic promise support.
Make lib supports promises, using a promise adapter interface.
2016-11-26 20:50:01 +01:00
vladar
4945317406 Fixing broken build for 0.8 2016-11-25 18:45:38 +07:00
vladar
c08cc2b00b Fixing broken build for 0.8 2016-11-25 18:18:53 +07:00
vladar
961e44c1fc Version 0.8.0 2016-11-25 18:09:18 +07:00
vladar
2cc2255b1a Work in progress on better docs 2016-11-25 18:07:51 +07:00
vladar
7ee3431298 Added badges to docs + added section on complementary tools 2016-11-25 17:44:34 +07:00
vladar
0969073b8a Reverted DefinitionContainer (YAGNI) 2016-11-25 16:54:57 +07:00
vladar
7c0aa4ceec Better docs and comments on breaks and deprecations for 8.0 2016-11-25 16:37:00 +07:00
vladar
b665d267c3 Changed scope of Schema props (and some others) to private 2016-11-25 16:35:46 +07:00
vladar
16bfc12ab1 Moved directive location constants to separate class 2016-11-25 16:30:35 +07:00
vladar
018ac819cf Removed deprecated error classes and added class_alias for them instead in deprecated.php 2016-11-25 16:03:04 +07:00
vladar
3d4cd15678 Added file with deprecations (for moved/renamed classes) 2016-11-25 16:02:19 +07:00
vladar
7fe5fc3980 Fixed invalid version reference in README which may be confusing 2016-11-19 17:52:41 +07:00
vladar
bf748e1421 Added notes to UPGRADE document about AST classes renaming 2016-11-19 17:46:54 +07:00
vladar
63d44fe002 Disabled deprecation notice for ResolveInfo->fieldASTs (until next version) 2016-11-19 17:37:54 +07:00
vladar
660200ed50 GraphQL\Language\AST\NodeType -> GraphQL\Language\AST\NodeKind 2016-11-19 17:31:47 +07:00
vladar
7340e30753 Types are now serializable to JSON (output their name) 2016-11-19 17:25:34 +07:00
vladar
04550f805f Differentiate between input values with no default and with default = null 2016-11-19 17:21:22 +07:00
vladar
5a17ae8246 Clearer lexer errors 2016-11-19 17:08:20 +07:00
vladar
0ab55ec0d9 Replaced "AST" with "Node" in variable names for better readability 2016-11-19 06:47:55 +07:00
vladar
5aad8b596b Consistent docblock comments for arrays 2016-11-19 06:19:41 +07:00
vladar
8d696edee5 Renamed AST nodes to *Node to disambiguate types 2016-11-19 06:12:18 +07:00
vladar
5ce9a7009a Renamed default resolver 2016-11-19 04:22:08 +07:00
vladar
439959b292 Enforce input coercion rules 2016-11-19 04:15:40 +07:00
vladar
f672f0c90c Added validation rule for unique directives per location 2016-11-19 00:21:56 +07:00
vladar
8a676cde99 Support for NullValue 2016-11-18 23:59:28 +07:00
vladar
9bf8e82d7c Minor fix to StarWarsSchema type language in comments 2016-11-18 20:32:09 +07:00
vladar
00060a142a Merge branch 'master' of https://github.com/webonyx/graphql-php 2016-11-16 18:42:47 +07:00
vladar
5d889ccacd Reverted several breaking changes from #75; tests are green 2016-11-16 18:37:35 +07:00
Andreas Heiberg
b2c8d8eeb1 [style changes] Lexer 2016-11-16 18:14:46 +07:00
Andreas Heiberg
d8ca5f4183 move to NodeType enum 2016-11-16 18:07:56 +07:00
Andreas Heiberg
816fa067b3 type hint Name node in visitor 2016-11-16 18:06:25 +07:00
Andreas Heiberg
46788f2ddb move NodeTypes enum to seperate class 2016-11-16 18:02:58 +07:00
Andreas Heiberg
2e25f51829 [style change] put VisitorOperation up top 2016-11-16 17:30:25 +07:00
Andreas Heiberg
80a662564e [style changes] arrays and callback 2016-11-16 17:29:50 +07:00
Andreas Heiberg
3c98963f72 remove static functions from Printer 2016-11-16 17:22:57 +07:00
Vladimir Razuvaev
1ee29e373c Merge pull request #73 from mcg-web/optimized-travis-ci-tests
Optimized travis CI tests
2016-11-12 21:48:43 +07:00
Vladimir Razuvaev
936af262ba Merge pull request #72 from mcg-web/fix-missing-requirement
Add ext-mbstring missing requirement
2016-11-12 21:47:39 +07:00
Jeremiah VALERIE
4e6b052465 Optimized travis CI tests
* Speed up tests by testing coverage only on php 5.6
* Add php nightly because composer accept it
* Add composer cache dir to travis cache
2016-11-12 10:23:06 +01:00
Jeremiah VALERIE
4867c4ceca Add ext-mbstring missing requirement
This fix "PHP Fatal error:  Uncaught Error: Call to undefined function GraphQL\Language\mb_strlen()"
2016-11-12 09:59:05 +01:00
vladar
399a340973 Minor change in error handling docs 2016-11-11 01:00:55 +07:00
vladar
bacb0649c2 Minor objectType refactoring 2016-11-11 01:00:32 +07:00
vladar
986eff9bff Added note in Readme about new documentation site 2016-11-08 20:47:46 +07:00
vladar
37cb4d00ab Work in progress on better docs (minor changes) 2016-11-08 20:27:45 +07:00
vladar
ccad34517c Work in progress on better docs (added section on query execution and error handling) 2016-11-08 20:02:10 +07:00
vladar
24b2285ffe Work in progress on better docs (added sections on directives and schema to docs) 2016-11-08 17:17:48 +07:00
vladar
add2621a33 Replaced directive locations array with constants 2016-11-08 16:55:33 +07:00
vladar
1927102183 Minor executor tweaks 2016-11-07 19:52:44 +07:00
vladar
e6addd4644 Minor fixes in docs 2016-11-07 18:23:59 +07:00
vladar
6023c0ff60 Refactoring abstract type resolution 2016-11-07 17:28:47 +07:00
vladar
afbd5dbc90 Removed last traces of field memoization 2016-11-07 16:26:02 +07:00
vladar
ca01900e9d Work in progress on better docs (added docs for field arguments and input types) 2016-11-07 01:52:15 +07:00
vladar
3514b5ac83 Work in progress on better docs (added docs for interfaces and unions) 2016-11-06 23:45:22 +07:00
vladar
360bf39c9b Simplified blog example 2016-11-06 21:33:13 +07:00
vladar
e69667c633 Work in progress on better docs 2016-11-05 23:55:51 +07:00
vladar
eb3f54b98e Better custom scalars in blog example + more comments 2016-11-02 05:10:04 +07:00
vladar
1672bd33e3 Minor comments improvement 2016-11-02 05:08:17 +07:00
vladar
61245213c4 Minor instanceof tweak 2016-11-02 00:41:44 +07:00
vladar
22e41a3729 Validate type/field/argument names 2016-11-02 00:14:44 +07:00
vladar
3b0e52f254 Updated AST Utils to match the behavior of reference implementation 2016-11-02 00:11:33 +07:00
vladar
443845d1f9 Added missing short-hand notation for input type fields 2016-11-02 00:07:10 +07:00
vladar
8f80389ecf Moved parse/serialize methods from abstract ScalarType class to LeafType interface (as EnumType implements them as well) 2016-11-01 23:50:18 +07:00
vladar
e56eb6e10d Replaced \UnexpectedValueException with GraphQL\Error\InvariantViolationException; Improved some instanceof checks 2016-11-01 23:40:37 +07:00
vladar
4a75bc6d2f Added coveralls and packagist badges to README 2016-11-01 17:54:21 +07:00
vladar
c846809d2d Added coveralls to travis config 2016-11-01 17:39:52 +07:00
vladar
29670b378b Updated blog example: demonstrated and commented inheritance vs composition options; added describing comments 2016-10-24 17:07:52 +07:00
vladar
19436c326d Infer type name from className for enum and scalar types as well 2016-10-24 16:47:39 +07:00
vladar
f9740c5f2c Work in progress on better docs 2016-10-23 22:16:32 +07:00
vladar
e4fa881cc3 Infer type name from class name for those using inheritance 2016-10-23 21:32:54 +07:00
vladar
221ec6f792 Renamed index.php to graphql.php for consistency in blog example 2016-10-23 18:35:31 +07:00
vladar
1787c1377d Added hello world example 2016-10-23 18:34:51 +07:00
vladar
96e4f01ee6 Added "Learn by example" section to Readme 2016-10-23 05:31:12 +07:00
vladar
5125741899 Several other updates to blog example 2016-10-23 05:30:31 +07:00
vladar
85d2c2cef3 Updated blog example 2016-10-23 05:13:55 +07:00
vladar
2ef58a615f Several minor tweaks 2016-10-23 05:06:36 +07:00
vladar
9941a0143a Added DefinitionContainer interface to enable compositional use of user-land types (vs extending ObjectType, InterfaceType, etc). Very similar to IteratorAggregate vs Iterator 2016-10-23 05:05:50 +07:00
vladar
c11f25794a Deprecated callbacks in "type" option of field/argument definitions (see #35) 2016-10-23 00:49:25 +07:00
vladar
9964c88f32 Updated StarWars tests 2016-10-23 00:46:15 +07:00
vladar
f443c6b1ba Fixed broken test 2016-10-22 17:29:18 +07:00
vladar
276a58f6d1 Reverted one minor breaking change 2016-10-22 17:19:40 +07:00
vladar
a612b780c9 Return stdClass vs empty array for empty ObjectType values (see #59) 2016-10-22 17:16:47 +07:00
vladar
d41687913a First official example that should help newcomers to start (incomplete yet, but still useful) 2016-10-21 18:43:11 +07:00
vladar
6c076e21d4 Another minor improvement to error formatting 2016-10-21 18:36:49 +07:00
vladar
7f1d74f980 Improved error reporting for invalid field definitions 2016-10-21 18:17:20 +07:00
vladar
927997a705 Various tools for better error reporting / formatting 2016-10-21 17:46:35 +07:00
vladar
2675b65095 Moved all error-related classes to separate namespace; fixed related broken tests 2016-10-21 16:40:56 +07:00
vladar
2b305ad6e2 Updated config validation rules 2016-10-21 15:52:58 +07:00
vladar
6d7a4a4f34 Fixed tests broken by recent changes to serialized error format 2016-10-21 15:28:24 +07:00
vladar
c25a29213c Fixed reported path for error; deprecated old FormattedError tool 2016-10-21 04:47:07 +07:00
vladar
2369454687 Minor comment tweaks + ability to define enum values as simple k=>v pairs 2016-10-21 03:10:06 +07:00
vladar
3ba187ec04 Added missing one-line shorthand for field arguments 2016-10-21 00:06:34 +07:00
vladar
fc37515ec2 Replaced instanceof check with is_array for shorthand notation of field definitions (as instanceof might be pretty slow) 2016-10-20 19:15:20 +07:00
vladar
5148c3bf03 Allow one-liner shorthand for field definitions (#47) 2016-10-20 19:09:48 +07:00
vladar
09cc313072 Allow to define enum values by simply passing array of strings, e.g. 'values' => ['ONE', 'TWO', 'THREE'] 2016-10-20 19:01:30 +07:00
vladar
0a79be8409 Updated introspection query and related tests for recent changes 2016-10-19 01:35:18 +07:00
vladar
236021acf8 Added deprecated directive; changed custom directives handling in schema; various minor tweaks 2016-10-19 01:34:46 +07:00
vladar
7625d6abf1 Scalar serialization now throws on spec-incompatible values vs returning null 2016-10-19 01:23:38 +07:00
vladar
0a182ac53c Improved enums (now they can handle complex values) 2016-10-19 01:21:47 +07:00
vladar
7f22d4b874 Spec compliance improvement: data key should not exist in response when it is null / missing (#56) 2016-10-18 22:25:39 +07:00
vladar
3e2d9459aa resolveType for interface/unions is now allowed to return type name vs instance 2016-10-18 22:23:20 +07:00
vladar
89eb6dede9 Breaking change when returning Closure from resolver: this closure is expected to have different signature now (shouldn't affect most of the users) 2016-10-18 22:20:51 +07:00
vladar
a94640f9d2 Enabled GraphQL\Error to include path to failed value during execution step (not location in source query, but exact path to value, including index in array, etc) + tests for errors 2016-10-18 22:15:21 +07:00
vladar
c0f7ec099d Several executor tests 2016-10-18 20:30:15 +07:00
vladar
357166791a Consistent coding style + doc block comments for Types and Utils 2016-10-17 19:14:29 +07:00
vladar
a2e6502b68 Merge branch 'master' of https://github.com/webonyx/graphql-php 2016-10-17 03:08:42 +07:00
vladar
cd14146032 Updated parser to consume latest lexer; New public parser API methods: parseType and parseValue; added directives to schema parser/printer 2016-10-17 02:57:24 +07:00
vladar
3eeb4d450b Reworked lexer: tokens are now represented as double linked list 2016-10-17 02:53:50 +07:00
Vladimir Razuvaev
9df93d4d46 Update README.md 2016-10-15 22:43:46 +07:00
Vladimir Razuvaev
609859081a Update README.md 2016-10-15 22:36:39 +07:00
Vladimir Razuvaev
a31cd9db3e Update README.md 2016-10-15 22:35:52 +07:00
Vladimir Razuvaev
f91cbf3409 Merge pull request #51 from geshido/master
Fix fatal error with deprecated directive fields in introspection query
2016-09-16 16:48:07 +07:00
vladar
351e5ee056 Added updates to README missing after recent version bump 2016-09-16 16:24:06 +07:00
vladar
08b96c59bf Added link to cleaner hello world example in README (#52) 2016-09-16 16:03:43 +07:00
vladar
cf38297d75 Updated docs for migration from v0.6.0 to v0.7.0 2016-09-15 18:44:24 +07:00
vladar
4b651d80a5 Fixed php notice with deprecated schema constructor 2016-09-15 17:32:54 +07:00
Yury
6b97439877 Fix fatal error when GraphiQL tries to run introspection query 2016-09-15 15:47:56 +07:00
Jérémiah VALERIE
b93519170f Merge pull request #50 from geshido/master
Fix fatal error
2016-09-15 09:53:22 +02:00
Yury
dab3e52242 Fix fatal error 2016-09-15 14:34:50 +07:00
vladar
f9e97ea296 Fixed several broken tests 2016-09-14 19:36:10 +07:00
vladar
560ad20954 Added .gitattributes for repository 2016-09-14 19:35:03 +07:00
vladar
2c50c51432 Fixed issue in lazy interface test where query type definition incorrectly initialized type that was supposed to be tested for laziness 2016-09-14 18:47:34 +07:00
Ivo Meißner
473bdb62a1 Updated documentation to spec April2016, upgrade instructions 2016-09-14 18:47:34 +07:00
vladar
1f71ffc3fc Added test to catch regressions in lazy interface declarations (see #38) 2016-09-14 18:47:34 +07:00
vladar
f7c8c670fe Fixed syntax errors specific to PHP5.5- to still support PHP5.4+ 2016-09-14 18:47:34 +07:00
vladar
26db43e7c4 Enabled subscriptions in executor 2016-09-14 18:47:34 +07:00
vladar
31f40f5e26 Fixed scalar Int to respect min/max values according to graphql spec; added descriptions to scalars 2016-09-14 18:47:34 +07:00
vladar
c3d7a49a08 Updated executor and it's tests for april2016 specs 2016-09-14 18:47:34 +07:00
vladar
00f12b3197 Fixed definition tests; added enum tests 2016-09-14 18:41:02 +07:00
vladar
183a9d72cf Introspection for april 2016 spec + fixed tests 2016-09-14 18:41:02 +07:00
vladar
e7c7924dc0 New AST utils + test 2016-09-14 18:41:02 +07:00
vladar
a6a4f7862b Several validator fixes 2016-09-14 18:41:02 +07:00
vladar
e82f887918 Fixed visitor; more visitor tests 2016-09-14 18:41:02 +07:00
vladar
71924f1154 Added phpbench for benchmarking 2016-09-14 18:41:02 +07:00
vladar
3dec7a9995 Test complete validation 2016-09-14 18:41:02 +07:00
vladar
800d8ba25f Continue updating validator rules for april2016 spec 2016-09-14 18:41:02 +07:00
vladar
8ab7a9a438 Implemented covariant return types for interface fields (#23) 2016-09-14 18:41:02 +07:00
vladar
f1ddc98390 Updating validator rules for april2016 spec 2016-09-14 18:41:02 +07:00
vladar
17081cec1c More AST visitor tests 2016-09-14 18:41:02 +07:00
vladar
687b023616 Schema language parsing / printing 2016-09-14 18:41:02 +07:00
vladar
4f4776726d Updated parser to 2016 spec version; schema language parsing 2016-09-14 18:41:02 +07:00
vladar
86adfde0a0 Updated AST to 2016 spec version 2016-09-14 18:41:02 +07:00
vladar
c053169671 Updated Lexer and LexerTest to april 2016 spec 2016-09-14 18:41:02 +07:00
Vladimir Razuvaev
ae5cffb6d5 Merge pull request #45 from konforce/decode-escaped-strings
Change Lexer::readString to decode escaped characters to their litera…
2016-09-14 17:43:32 +07:00
Matthew Leverton
7c6f216578 Change Lexer::readString to decode escaped characters to their literal values 2016-07-14 13:32:57 -05:00
Jérémiah VALERIE
0c1ad084d4 Merge pull request #44 from mcg-web/removed_memoization_on_executor_resolve_field
Removed memoization on executor resolveField (see #43)
2016-06-11 08:43:32 +02:00
Jeremiah VALERIE
3ae6c73367 Removed memoization on executor resolveField (see #43) 2016-06-10 11:57:43 +02:00
Vladimir Razuvaev
0e1929f0b1 Merge pull request #39 from mcg-web/fix_resolve_info_field_selection_when_using_multiple_fragments
Fix `ResolveInfo::getFieldSelection()` when using multiple fragments
2016-05-25 12:14:29 +06:00
Jeremiah VALERIE
be46b14441 Fix ResolveInfo::getFieldSelection() when using multiple fragments on the same field. 2016-05-18 16:20:53 +02:00
Vladimir Razuvaev
7916c54926 Merge pull request #37 from mcg-web/execution-result-extensions
Add ExecutionResult $extensions
2016-04-25 19:55:11 +06:00
Jeremiah VALERIE
de4eba7077 Fix interface implementaion when interface is extends. 2016-04-25 14:44:08 +02:00
Jérémiah VALERIE
327cc52601 Add ExecutionResult $extensions
https://facebook.github.io/graphql/#sec-Response-Format
2016-04-25 14:44:08 +02:00
450 changed files with 76428 additions and 16123 deletions

13
.gitattributes vendored Normal file
View file

@ -0,0 +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
View file

@ -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
View 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

View file

@ -1,15 +1,66 @@
dist: trusty
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
- nightly
- hhvm
- 7.1
- 7.2
- 7.3
- 7.4snapshot
- nightly
install:
- composer install --prefer-dist
env:
matrix:
- EXECUTOR= DEPENDENCIES=--prefer-lowest
- EXECUTOR=coroutine DEPENDENCIES=--prefer-lowest
- EXECUTOR=
- EXECUTOR=coroutine
cache:
directories:
- $HOME/.composer/cache
before_install:
- mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{,.disabled} || echo "xdebug not available"
- travis_retry composer self-update
install: travis_retry composer update --prefer-dist
script: ./vendor/bin/phpunit --group default,ReactPromise
jobs:
allow_failures:
- php: 7.4snapshot
- php: nightly
include:
- stage: Test
install:
- travis_retry composer update --prefer-dist {$DEPENDENCIES}
- stage: Test
env: COVERAGE
before_script:
- mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{.disabled,}
- if [[ ! $(php -m | grep -si xdebug) ]]; then echo "xdebug required for coverage"; exit 1; fi
script:
- ./vendor/bin/phpunit --coverage-php /tmp/coverage/clover_executor.cov
- EXECUTOR=coroutine ./vendor/bin/phpunit --coverage-php /tmp/coverage/clover_executor-coroutine.cov
after_script:
- ./vendor/bin/phpcov merge /tmp/coverage --clover /tmp/clover.xml
- wget https://github.com/scrutinizer-ci/ocular/releases/download/1.5.2/ocular.phar
- php ocular.phar code-coverage:upload --format=php-clover /tmp/clover.xml
- stage: Code Quality
php: 7.1
env: CODING_STANDARD
install: travis_retry composer install --prefer-dist
script:
- ./vendor/bin/phpcs
- stage: Code Quality
php: 7.1
env: STATIC_ANALYSIS
install: travis_retry composer install --prefer-dist
script: composer static-analysis
script:
- ./bin/phpunit tests/

200
CHANGELOG.md Normal file
View 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
View 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
View file

@ -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.

526
README.md
View file

@ -1,510 +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.
[![Build Status](https://travis-ci.org/webonyx/graphql-php.svg?branch=master)](https://travis-ci.org/webonyx/graphql-php)
[![Code Coverage](https://scrutinizer-ci.com/g/webonyx/graphql-php/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/webonyx/graphql-php)
[![Latest Stable Version](https://poser.pugx.org/webonyx/graphql-php/version)](https://packagist.org/packages/webonyx/graphql-php)
[![License](https://poser.pugx.org/webonyx/graphql-php/license)](https://packagist.org/packages/webonyx/graphql-php)
## Table of Contents
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).
- [Overview](#overview)
- [Installation](#installing-graphql-php)
- [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.
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
Actual data fetching has to be implemented on the user land.
## Installing graphql-php
## Installation
Via composer:
```
$> curl -sS https://getcomposer.org/installer | php
$> php composer.phar require webonyx/graphql-php='dev-master'
composer require webonyx/graphql-php
```
## Requirements
PHP >=5.4
## 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.
## 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.
If you don't know what GraphQL is, visit this [official website](http://graphql.org)
by the Facebook engineering team.
### Type System
To start using GraphQL you are expected to implement a Type system.
## Examples
There are several ready examples in the [examples](examples/) folder of the distribution with specific
README file per example.
GraphQL PHP provides several *kinds* of types to build hierarchical type system:
`scalar`, `enum`, `object`, `interface`, `union`, `listOf`, `nonNull`.
## Contributors
#### Internal types
Only several `scalar` types are implemented out of the box:
`ID`, `String`, `Int`, `Float`, `Boolean`
This project exists thanks to [all the people](https://github.com/webonyx/graphql-php/graphs/contributors) who contribute. [[Contribute](CONTRIBUTING.md)].
As well as two internal modifier types: `ListOf` and `NonNull`.
## Backers
All internal types are exposed as static methods of `GraphQL\Type\Definition\Type` class:
<a href="https://opencollective.com/webonyx-graphql-php#backers" target="_blank"><img src="https://opencollective.com/webonyx-graphql-php/backers.svg?width=890"></a>
```php
use GraphQL\Type\Definition\Type;
## Sponsors
// Internal Scalar types:
Type::string(); // String type
Type::int(); // Int type
Type::float(); // Float type
Type::boolean(); // Boolean type
Type::id(); // ID type
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)]
// Internal wrapping types:
Type::nonNull(Type::string()) // String! type
Type::listOf(Type::string()) // String[] type
```
<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>
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.
## License
#### 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) => 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, 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.
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, ResolveInfo $info) => $fieldValue` | Function that receives `$value` of parent type and returns value for this field.
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($queryType, $mutationType);
```
**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,
$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(/*...*/);
```
### 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).

532
UPGRADE.md Normal file
View file

@ -0,0 +1,532 @@
## 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.
If you only use the library and don't try to extend it - everything should work without breaks.
### Breaking: Custom directives handling
When passing custom directives to schema, default directives (like `@skip` and `@include`)
are not added to schema automatically anymore. If you need them - add them explicitly with
your other directives.
Before the change:
```php
$schema = new Schema([
// ...
'directives' => [$myDirective]
]);
```
After the change:
```php
$schema = new Schema([
// ...
'directives' => array_merge(GraphQL::getInternalDirectives(), [$myDirective])
]);
```
### Breaking: Schema protected property and methods visibility
Most of the `protected` properties and methods of `GraphQL\Schema` were changed to `private`.
Please use public interface instead.
### Breaking: Node kind constants
Node kind constants were extracted from `GraphQL\Language\AST\Node` to
separate class `GraphQL\Language\AST\NodeKind`
### Non-breaking: AST node classes renamed
AST node classes were renamed to disambiguate with types. e.g.:
```
GraphQL\Language\AST\Field -> GraphQL\Language\AST\FieldNode
GraphQL\Language\AST\OjbectValue -> GraphQL\Language\AST\OjbectValueNode
```
etc.
Old names are still available via `class_alias` defined in `src/deprecated.php`.
This file is included automatically when using composer autoloading.
### Deprecations
There are several deprecations which still work, but trigger `E_USER_DEPRECATED` when used.
For example `GraphQL\Executor\Executor::setDefaultResolveFn()` is renamed to `setDefaultResolver()`
but still works with old name.
## Upgrade v0.6.x > v0.7.x
There are a few new breaking changes in v0.7.0 that were added to the graphql-js reference implementation
with the spec of April2016
### 1. Context for resolver
You can now pass a custom context to the `GraphQL::execute` function that is available in all resolvers as 3rd argument.
This can for example be used to pass the current user etc.
Make sure to update all calls to `GraphQL::execute`, `GraphQL::executeAndReturnResult`, `Executor::execute` and all
`'resolve'` callbacks in your app.
Before v0.7.0 `GraphQL::execute` signature looked this way:
```php
GraphQL::execute(
$schema,
$query,
$rootValue,
$variables,
$operationName
);
```
Starting from v0.7.0 the signature looks this way (note the new `$context` argument):
```php
GraphQL::execute(
$schema,
$query,
$rootValue,
$context,
$variables,
$operationName
);
```
Before v.0.7.0 resolve callbacks had following signature:
```php
/**
* @param mixed $object The parent resolved object
* @param array $args Input arguments
* @param ResolveInfo $info ResolveInfo object
* @return mixed
*/
function resolveMyField($object, array $args, ResolveInfo $info) {
//...
}
```
Starting from v0.7.0 the signature has changed to (note the new `$context` argument):
```php
/**
* @param mixed $object The parent resolved object
* @param array $args Input arguments
* @param mixed $context The context object hat was passed to GraphQL::execute
* @param ResolveInfo $info ResolveInfo object
* @return mixed
*/
function resolveMyField($object, array $args, $context, ResolveInfo $info){
//...
}
```
### 2. Schema constructor signature
The signature of the Schema constructor now accepts an associative config array instead of positional arguments:
Before v0.7.0:
```php
$schema = new Schema($queryType, $mutationType);
```
Starting from v0.7.0:
```php
$schema = new Schema([
'query' => $queryType,
'mutation' => $mutationType,
'types' => $arrayOfTypesWithInterfaces // See 3.
]);
```
### 3. Types can be directly passed to schema
There are edge cases when GraphQL cannot infer some types from your schema.
One example is when you define a field of interface type and object types implementing
this interface are not referenced anywhere else.
In such case object types might not be available when an interface is queried and query
validation will fail. In that case, you need to pass the types that implement the
interfaces directly to the schema, so that GraphQL knows of their existence during query validation.
For example:
```php
$schema = new Schema([
'query' => $queryType,
'mutation' => $mutationType,
'types' => $arrayOfTypesWithInterfaces
]);
```
Note that you don't need to pass all types here - only those types that GraphQL "doesn't see"
automatically. Before v7.0.0 the workaround for this was to create a dumb (non-used) field per
each "invisible" object type.
Also see [webonyx/graphql-php#38](https://github.com/webonyx/graphql-php/issues/38)

View file

@ -0,0 +1,75 @@
<?php
namespace GraphQL\Benchmarks;
use GraphQL\Benchmarks\Utils\QueryGenerator;
use GraphQL\Benchmarks\Utils\SchemaGenerator;
use GraphQL\GraphQL;
use GraphQL\Type\Schema;
use GraphQL\Type\SchemaConfig;
/**
* @BeforeMethods({"setUp"})
* @OutputTimeUnit("milliseconds", precision=3)
* @Warmup(1)
* @Revs(5)
* @Iterations(1)
*/
class HugeSchemaBench
{
/** @var SchemaGenerator */
private $schemaBuilder;
private $schema;
/** @var string */
private $smallQuery;
public function setUp()
{
$this->schemaBuilder = new SchemaGenerator([
'totalTypes' => 600,
'fieldsPerType' => 8,
'listFieldsPerType' => 2,
'nestingLevel' => 10,
]);
$this->schema = $this->schemaBuilder->buildSchema();
$queryBuilder = new QueryGenerator($this->schema, 0.05);
$this->smallQuery = $queryBuilder->buildQuery();
}
public function benchSchema()
{
$this->schemaBuilder
->buildSchema()
->getTypeMap();
}
public function benchSchemaLazy()
{
$this->createLazySchema();
}
public function benchSmallQuery()
{
$result = GraphQL::executeQuery($this->schema, $this->smallQuery);
}
public function benchSmallQueryLazy()
{
$schema = $this->createLazySchema();
$result = GraphQL::executeQuery($schema, $this->smallQuery);
}
private function createLazySchema()
{
return new Schema(
SchemaConfig::create()
->setQuery($this->schemaBuilder->buildQueryType())
->setTypeLoader(function ($name) {
return $this->schemaBuilder->loadType($name);
})
);
}
}

35
benchmarks/LexerBench.php Normal file
View file

@ -0,0 +1,35 @@
<?php
namespace GraphQL\Benchmarks;
use GraphQL\Language\Lexer;
use GraphQL\Language\Source;
use GraphQL\Language\Token;
use GraphQL\Type\Introspection;
/**
* @BeforeMethods({"setUp"})
* @OutputTimeUnit("milliseconds", precision=3)
*/
class LexerBench
{
private $introQuery;
public function setUp()
{
$this->introQuery = new Source(Introspection::getIntrospectionQuery());
}
/**
* @Warmup(2)
* @Revs(100)
* @Iterations(5)
*/
public function benchIntrospectionQuery()
{
$lexer = new Lexer($this->introQuery);
do {
$token = $lexer->advance();
} while ($token->kind !== Token::EOF);
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace GraphQL\Benchmarks;
use GraphQL\GraphQL;
use GraphQL\Tests\StarWarsSchema;
use GraphQL\Type\Introspection;
/**
* @BeforeMethods({"setIntroQuery"})
* @OutputTimeUnit("milliseconds", precision=3)
* @Warmup(2)
* @Revs(10)
* @Iterations(2)
*/
class StarWarsBench
{
private $introQuery;
public function setIntroQuery()
{
$this->introQuery = Introspection::getIntrospectionQuery();
}
public function benchSchema()
{
StarWarsSchema::build();
}
public function benchHeroQuery()
{
$q = '
query HeroNameQuery {
hero {
name
}
}
';
GraphQL::executeQuery(
StarWarsSchema::build(),
$q
);
}
public function benchNestedQuery()
{
$q = '
query NestedQuery {
hero {
name
friends {
name
appearsIn
friends {
name
}
}
}
}
';
GraphQL::executeQuery(
StarWarsSchema::build(),
$q
);
}
public function benchQueryWithFragment()
{
$q = '
query UseFragment {
luke: human(id: "1000") {
...HumanFragment
}
leia: human(id: "1003") {
...HumanFragment
}
}
fragment HumanFragment on Human {
name
homePlanet
}
';
GraphQL::executeQuery(
StarWarsSchema::build(),
$q
);
}
public function benchStarWarsIntrospectionQuery()
{
GraphQL::executeQuery(
StarWarsSchema::build(),
$this->introQuery
);
}
}

View file

@ -0,0 +1,105 @@
<?php
namespace GraphQL\Benchmarks\Utils;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\Printer;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\WrappingType;
use GraphQL\Type\Schema;
use GraphQL\Utils\Utils;
use function count;
use function max;
use function round;
class QueryGenerator
{
private $schema;
private $maxLeafFields;
private $currentLeafFields;
public function __construct(Schema $schema, $percentOfLeafFields)
{
$this->schema = $schema;
Utils::invariant(0 < $percentOfLeafFields && $percentOfLeafFields <= 1);
$totalFields = 0;
foreach ($schema->getTypeMap() as $type) {
if (! ($type instanceof ObjectType)) {
continue;
}
$totalFields += count($type->getFields());
}
$this->maxLeafFields = max(1, round($totalFields * $percentOfLeafFields));
$this->currentLeafFields = 0;
}
public function buildQuery()
{
$qtype = $this->schema->getQueryType();
$ast = new DocumentNode([
'definitions' => [new OperationDefinitionNode([
'name' => new NameNode(['value' => 'TestQuery']),
'operation' => 'query',
'selectionSet' => $this->buildSelectionSet($qtype->getFields()),
]),
],
]);
return Printer::doPrint($ast);
}
/**
* @param FieldDefinition[] $fields
*
* @return SelectionSetNode
*/
public function buildSelectionSet($fields)
{
$selections[] = new FieldNode([
'name' => new NameNode(['value' => '__typename']),
]);
$this->currentLeafFields++;
foreach ($fields as $field) {
if ($this->currentLeafFields >= $this->maxLeafFields) {
break;
}
$type = $field->getType();
if ($type instanceof WrappingType) {
$type = $type->getWrappedType(true);
}
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
$selectionSet = $this->buildSelectionSet($type->getFields());
} else {
$selectionSet = null;
$this->currentLeafFields++;
}
$selections[] = new FieldNode([
'name' => new NameNode(['value' => $field->name]),
'selectionSet' => $selectionSet,
]);
}
$selectionSet = new SelectionSetNode([
'selections' => $selections,
]);
return $selectionSet;
}
}

View file

@ -0,0 +1,159 @@
<?php
namespace GraphQL\Benchmarks\Utils;
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
{
private $config = [
'totalTypes' => 100,
'nestingLevel' => 10,
'fieldsPerType' => 10,
'listFieldsPerType' => 2
];
private $typeIndex = 0;
private $objectTypes = [];
/**
* BenchmarkSchemaBuilder constructor.
* @param array $config
*/
public function __construct(array $config)
{
$this->config = array_merge($this->config, $config);
}
public function buildSchema()
{
return new Schema([
'query' => $this->buildQueryType()
]);
}
public function buildQueryType()
{
$this->typeIndex = 0;
$this->objectTypes = [];
return $this->createType(0);
}
public function loadType($name)
{
$tokens = explode('_', $name);
$nestingLevel = (int) $tokens[1];
return $this->createType($nestingLevel, $name);
}
protected function createType($nestingLevel, $typeName = null)
{
if ($this->typeIndex > $this->config['totalTypes']) {
throw new \Exception(
"Cannot create new type: there are already {$this->typeIndex} ".
"which exceeds allowed number of {$this->config['totalTypes']} types total"
);
}
$this->typeIndex++;
if (!$typeName) {
$typeName = 'Level_' . $nestingLevel . '_Type' . $this->typeIndex;
}
$type = new ObjectType([
'name' => $typeName,
'fields' => function() use ($typeName, $nestingLevel) {
return $this->createTypeFields($typeName, $nestingLevel + 1);
}
]);
$this->objectTypes[$typeName] = $type;
return $type;
}
protected function getFieldTypeAndName($nestingLevel, $fieldIndex)
{
if ($nestingLevel >= $this->config['nestingLevel']) {
$fieldType = Type::string();
$fieldName = 'leafField' . $fieldIndex;
} else if ($this->typeIndex >= $this->config['totalTypes']) {
$fieldType = $this->objectTypes[array_rand($this->objectTypes)];
$fieldName = 'randomTypeField' . $fieldIndex;
} else {
$fieldType = $this->createType($nestingLevel);
$fieldName = 'field' . $fieldIndex;
}
return [$fieldType, $fieldName];
}
protected function createTypeFields($typeName, $nestingLevel)
{
$fields = [];
for ($index = 0; $index < $this->config['fieldsPerType']; $index++) {
list($type, $name) = $this->getFieldTypeAndName($nestingLevel, $index);
$fields[] = [
'name' => $name,
'type' => $type,
'resolve' => [$this, 'resolveField']
];
}
for ($index = 0; $index < $this->config['listFieldsPerType']; $index++) {
list($type, $name) = $this->getFieldTypeAndName($nestingLevel, $index);
$name = 'listOf' . ucfirst($name);
$fields[] = [
'name' => $name,
'type' => Type::listOf($type),
'args' => $this->createFieldArgs($name, $typeName),
'resolve' => function() {
return [
'string1',
'string2',
'string3',
'string4',
'string5',
];
}
];
}
return $fields;
}
protected function createFieldArgs($fieldName, $typeName)
{
return [
'argString' => [
'type' => Type::string()
],
'argEnum' => [
'type' => new EnumType([
'name' => $typeName . $fieldName . 'Enum',
'values' => [
"ONE", "TWO", "THREE"
]
])
],
'argInputObject' => [
'type' => new InputObjectType([
'name' => $typeName . $fieldName . 'Input',
'fields' => [
'field1' => Type::string(),
'field2' => Type::int()
]
])
]
];
}
public function resolveField($objectValue, $args, $context, $resolveInfo)
{
return $resolveInfo->fieldName . '-value';
}
}

View file

@ -2,20 +2,31 @@
"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": {
"psr-4": {
@ -24,7 +35,22 @@
},
"autoload-dev": {
"psr-4": {
"GraphQL\\Tests\\": "tests/"
"GraphQL\\Tests\\": "tests/",
"GraphQL\\Benchmarks\\": "benchmarks/",
"GraphQL\\Examples\\Blog\\": "examples/01-blog/Blog/"
}
},
"suggest": {
"react/promise": "To leverage async resolving on React PHP platform",
"psr/http-message": "To use standard GraphQL server"
},
"scripts": {
"api-docs": "php tools/gendocs.php",
"bench": "phpbench run .",
"test": "phpunit",
"lint" : "phpcs",
"fix-style" : "phpcbf",
"static-analysis": "phpstan analyse --ansi --memory-limit 256M",
"check-all": "composer lint && composer static-analysis && composer test"
}
}

19
docs/best-practices.md Normal file
View file

@ -0,0 +1,19 @@
# Config Validation
Defining types using arrays may be error-prone, but **graphql-php** provides config validation
tool to report when config has unexpected structure.
This validation tool is **disabled by default** because it is time-consuming operation which only
makes sense during development.
To enable validation - call: `GraphQL\Type\Definition\Config::enableValidation();` in your bootstrap
but make sure to restrict it to debug/development mode only.
# Type Registry
**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\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
implementation below that introduces TypeRegistry class:

View file

@ -0,0 +1,26 @@
# Integrations
* [Standard Server](executing-queries.md/#using-server) Out of the box integration with any PSR-7 compatible framework (like [Slim](http://slimframework.com) or [Zend Expressive](http://zendframework.github.io/zend-expressive/)).
* [Relay Library for graphql-php](https://github.com/ivome/graphql-relay-php) Helps construct Relay related schema definitions.
* [Lighthouse](https://github.com/nuwave/lighthouse) Laravel based, uses Schema Definition Language
* [Laravel GraphQL](https://github.com/rebing/graphql-laravel) - Laravel wrapper for Facebook's GraphQL
* [OverblogGraphQLBundle](https://github.com/overblog/GraphQLBundle) Bundle for Symfony
* [WP-GraphQL](https://github.com/wp-graphql/wp-graphql) - GraphQL API for WordPress
# 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

139
docs/concepts.md Normal file
View file

@ -0,0 +1,139 @@
# Overview
GraphQL is data-centric. On the very top level it is built around three major concepts:
**Schema**, **Query** and **Mutation**.
You are expected to express your application as **Schema** (aka Type System) and expose it
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
Queries are expressed in simple language that resembles JSON:
```graphql
{
hero {
name
friends {
name
}
}
}
```
It was designed to mirror the structure of expected response:
```json
{
"hero": {
"name": "R2-D2",
"friends": [
{"name": "Luke Skywalker"},
{"name": "Han Solo"},
{"name": "Leia Organa"}
]
}
}
```
**graphql-php** runtime parses Queries, makes sure that they are valid for given Type System
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:
```graphql
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
```
Variables `$ep` and `$review` are sent alongside with mutation. Full HTTP request might look like this:
```json
// POST /graphql-endpoint
// Content-Type: application/javascript
//
{
"query": "mutation CreateReviewForEpisode...",
"variables": {
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
}
```
As you see variables may include complex objects and they will be correctly validated by
**graphql-php** runtime.
Another nice feature of GraphQL mutations is that they also hold the query for data to be
returned after mutation. In our example mutation will return:
```
{
"createReview": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
```
# Type System
Conceptually GraphQL type is a collection of fields. Each field in turn
has it's own type which allows to build complex hierarchies.
Quick example on pseudo-language:
```
type BlogPost {
title: String!
author: User
body: String
}
type User {
id: Id!
firstName: String
lastName: String
}
```
Type system is a heart of GraphQL integration. That's where **graphql-php** comes into play.
It provides following tools and primitives to describe your App as hierarchy of types:
* Primitives for defining **objects** and **interfaces**
* Primitives for defining **enumerations** and **unions**
* Primitives for defining custom **scalar types**
* Built-in scalar types: `ID`, `String`, `Int`, `Float`, `Boolean`
* Built-in type modifiers: `ListOf` and `NonNull`
Same example expressed in **graphql-php**:
```php
<?php
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ObjectType;
$userType = new ObjectType([
'name' => 'User',
'fields' => [
'id' => Type::nonNull(Type::id()),
'firstName' => Type::string(),
'lastName' => Type::string()
]
]);
$blogPostType = new ObjectType([
'name' => 'BlogPost',
'fields' => [
'title' => Type::nonNull(Type::string()),
'author' => $userType
]
]);
```
# 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.md)

274
docs/data-fetching.md Normal file
View file

@ -0,0 +1,274 @@
# Overview
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 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 the response (for scalars and enums)
or passed down to nested fields (for objects).
Let's walk through an example. Consider following GraphQL query:
```graphql
{
lastStory {
title
author {
name
}
}
}
```
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' => [
'lastStory' => [
'type' => $blogStoryType,
'resolve' => function() {
return [
'id' => 1,
'title' => 'Example blog post',
'authorId' => 1
];
}
]
]
]);
```
As we see field **lastStory** has **resolve** function that is responsible for fetching data.
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 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' => [
'author' => [
'type' => $userType,
'resolve' => function($blogStory) {
$users = [
1 => [
'id' => 1,
'name' => 'Smith'
],
2 => [
'id' => 2,
'name' => 'Anderson'
]
];
return $users[$blogStory['authorId']];
}
],
'title' => [
'type' => Type::string()
]
]
]);
```
Here **$blogStory** is the array returned by **lastStory** field above.
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?
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
<?php
function defaultFieldResolver($objectValue, $args, $context, \GraphQL\Type\Definition\ResolveInfo $info)
{
$fieldName = $info->fieldName;
$property = null;
if (is_array($objectValue) || $objectValue instanceof \ArrayAccess) {
if (isset($objectValue[$fieldName])) {
$property = $objectValue[$fieldName];
}
} elseif (is_object($objectValue)) {
if (isset($objectValue->{$fieldName})) {
$property = $objectValue->{$fieldName};
}
}
return $property instanceof Closure
? $property($objectValue, $args, $context, $info)
: $property;
}
```
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 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.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' => [
'name' => Type::string(),
'email' => Type::string()
],
'resolveField' => function(User $user, $args, $context, ResolveInfo $info) {
switch ($info->fieldName) {
case 'name':
return $user->getName();
case 'email':
return $user->getEmail();
default:
return null;
}
}
]);
```
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 a so-called
[N+1 problem](https://secure.phabricator.com/book/phabcontrib/article/n_plus_one/). <br>
Consider following GraphQL query:
```
{
topStories(limit: 10) {
title
author {
name
email
}
}
}
```
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 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:
```php
<?php
'resolve' => function($blogStory) {
MyUserBuffer::add($blogStory['authorId']);
return new GraphQL\Deferred(function () use ($blogStory) {
MyUserBuffer::loadBuffered();
return MyUserBuffer::get($blogStory['authorId']);
});
}
```
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 returns final field value.
Originally this approach was advocated by Facebook in their [Dataloader](https://github.com/facebook/dataloader)
project. This solution enables very interesting optimizations at no cost. Consider the following query:
```graphql
{
topStories(limit: 10) {
author {
email
}
}
category {
stories(limit: 10) {
author {
email
}
}
}
}
```
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.10.0 (version 0.9.0 had slightly different API which still works, but is deprecated)
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 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 **$promiseAdapter** is an instance of:
* For [ReactPHP](https://github.com/reactphp/react) (requires **react/promise** as composer dependency): <br>
`GraphQL\Executor\Promise\Adapter\ReactPromiseAdapter`
* Other platforms: write your own class implementing interface: <br>
[`GraphQL\Executor\Promise\PromiseAdapter`](reference.md#graphqlexecutorpromisepromiseadapter).
Then your **resolve** functions should return promises of your platform instead of `GraphQL\Deferred`s.

193
docs/error-handling.md Normal file
View file

@ -0,0 +1,193 @@
# Errors in GraphQL
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).
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:
```php
<?php
[
'message' => 'Error message',
'category' => 'graphql',
'locations' => [
['line' => 1, 'column' => 2]
],
'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'
]
];
```
To change default **"Internal server error"** message to something else, use:
```
GraphQL\Error\FormattedError::setInternalErrorMessage("Unexpected error");
```
# Debugging tools
During development or debugging use `$result->toArray(true)` to add **debugMessage** key to
each formatted error entry. If you also want to add exception trace - pass flags instead:
```
use GraphQL\Error\Debug;
$debug = Debug::INCLUDE_DEBUG_MESSAGE | Debug::INCLUDE_TRACE;
$result = GraphQL::executeQuery(/*args*/)->toArray($debug);
```
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 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;
// Following will throw if there was an exception in resolver during execution:
$result = GraphQL::executeQuery(/*args*/)->toArray($debug);
```
If you only want to re-throw Exceptions that are not marked as safe through the `ClientAware` interface, use
the flag `Debug::RETHROW_UNSAFE_EXCEPTIONS`.
# Custom Error Handling and Formatting
It is possible to define custom **formatter** and **handler** for result errors.
**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 `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::executeQuery($schema, $query);
$status = 200;
} catch(\Exception $e) {
$body = [
'errors' => [FormattedError::createFromException($e)]
];
$status = 500;
}
header('Content-Type: application/json', true, $status);
echo json_encode($body);
```

208
docs/executing-queries.md Normal file
View file

@ -0,0 +1,208 @@
# 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.md).
**graphql-php** provides a convenient facade for this process in class
[`GraphQL\GraphQL`](reference.md#graphqlgraphql):
```php
<?php
use GraphQL\GraphQL;
$result = GraphQL::executeQuery(
$schema,
$queryString,
$rootValue = null,
$context = null,
$variableValues = null,
$operationName = null,
$fieldResolver = null,
$validationRules = null
);
```
It returns an instance of [`GraphQL\Executor\ExecutionResult`](reference.md#graphqlexecutorexecutionresult)
which can be easily converted to array:
```php
$serializableResult = $result->toArray();
```
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\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)
# 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.
Usage example (with plain PHP):
```php
<?php
use GraphQL\Server\StandardServer;
$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
]);
```

125
docs/getting-started.md Normal file
View file

@ -0,0 +1,125 @@
# Prerequisites
This documentation assumes your familiarity with GraphQL concepts. If it is not the case -
first learn about GraphQL on [the official website](http://graphql.org/learn/).
# Installation
Using [composer](https://getcomposer.org/doc/00-intro.md), run:
```sh
composer require webonyx/graphql-php
```
# Upgrading
We try to keep library releases backwards compatible. But when breaking changes are inevitable
they are explained in [upgrade instructions](https://github.com/webonyx/graphql-php/blob/master/UPGRADE.md).
# 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.
It provides syntax-highlighting, auto-completion and auto-generated documentation for
GraphQL API.
The easiest way to use it is to install one of the existing Google Chrome extensions:
- [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 [the GraphiQL](https://github.com/graphql/graphiql)
page and install it locally.
# Hello World
Let's create a type system that will be capable to process following simple query:
```
query {
echo(message: "Hello World")
}
```
To do so we need an object type with field `echo`:
```php
<?php
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'echo' => [
'type' => Type::string(),
'args' => [
'message' => Type::nonNull(Type::string()),
],
'resolve' => function ($rootValue, $args) {
return $rootValue['prefix'] . $args['message'];
}
],
],
]);
```
(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 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 file for it **graphql.php**:
```php
<?php
use GraphQL\GraphQL;
use GraphQL\Type\Schema;
$schema = new Schema([
'query' => $queryType
]);
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
$query = $input['query'];
$variableValues = isset($input['variables']) ? $input['variables'] : null;
try {
$rootValue = ['prefix' => 'You said: '];
$result = GraphQL::executeQuery($schema, $query, $rootValue, null, $variableValues);
$output = $result->toArray();
} catch (\Exception $e) {
$output = [
'errors' => [
[
'message' => $e->getMessage()
]
]
];
}
header('Content-Type: application/json');
echo json_encode($output);
```
Our example is finished. Try it by running:
```sh
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
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/index.md).
# Blog example
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).
It is quite close to real-world GraphQL hierarchies. Follow instructions and try it yourself in ~10 minutes.

35
docs/how-it-works.md Normal file
View 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.

55
docs/index.md Normal file
View file

@ -0,0 +1,55 @@
[![GitHub stars](https://img.shields.io/github/stars/webonyx/graphql-php.svg?style=social&label=Star)](https://github.com/webonyx/graphql-php)
[![Build Status](https://travis-ci.org/webonyx/graphql-php.svg?branch=master)](https://travis-ci.org/webonyx/graphql-php)
[![Coverage Status](https://coveralls.io/repos/github/webonyx/graphql-php/badge.svg)](https://coveralls.io/github/webonyx/graphql-php)
[![Latest Stable Version](https://poser.pugx.org/webonyx/graphql-php/version)](https://packagist.org/packages/webonyx/graphql-php)
[![License](https://poser.pugx.org/webonyx/graphql-php/license)](https://packagist.org/packages/webonyx/graphql-php)
# About GraphQL
GraphQL is a modern way to build HTTP APIs consumed by the web and mobile clients.
It is intended to be an alternative to REST and SOAP APIs (even for **existing applications**).
GraphQL itself is a [specification](https://github.com/facebook/graphql) designed by Facebook
engineers. Various implementations of this specification were written
[in different languages and environments](http://graphql.org/code/).
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.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.
Library features 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)
Also, several [complementary tools](complementary-tools.md) are available which provide integrations with
existing PHP frameworks, add support for Relay, etc.
## Current Status
The first version of this library (v0.1) was released on August 10th 2015.
The current version supports all features described by GraphQL specification
as well as some experimental features like
[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).

2336
docs/reference.md Normal file

File diff suppressed because it is too large Load diff

94
docs/security.md Normal file
View 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).

View file

@ -0,0 +1,61 @@
# Built-in directives
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**
For example:
```graphql
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends) {
name
}
}
}
```
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 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 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',
'locations' => [
DirectiveLocation::FIELD,
],
'args' => [
new FieldArgument([
'name' => 'details',
'type' => Type::string(),
'description' => 'String with additional details of field usage scenario',
'defaultValue' => ''
])
]
]);
```
See possible directive locations in
[`GraphQL\Language\DirectiveLocation`](../reference.md#graphqllanguagedirectivelocation).

View file

@ -0,0 +1,182 @@
# Enum Type Definition
Enumeration types are a special kind of scalar that is restricted to a particular set
of allowed values.
In **graphql-php** enum type is an instance of `GraphQL\Type\Definition\EnumType`
which accepts configuration array in constructor:
```php
<?php
use GraphQL\Type\Definition\EnumType;
$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.'
],
]
]);
```
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 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](#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
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](#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)
# Shorthand definitions
If internal representation of enumerated item is the same as item name, then you can use
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',
'values' => ['NEWHOPE', 'EMPIRE', 'JEDI']
]);
```
which is equivalent of:
```php
<?php
use GraphQL\Type\Definition\EnumType;
$episodeEnum = new EnumType([
'name' => 'Episode',
'description' => 'One of the films in the Star Wars Trilogy',
'values' => [
'NEWHOPE' => ['value' => 'NEWHOPE'],
'EMPIRE' => ['value' => 'EMPIRE'],
'JEDI' => ['value' => 'JEDI']
]
]);
```
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',
'values' => [
['name' => 'NEWHOPE', 'value' => 'NEWHOPE'],
['name' => 'EMPIRE', 'value' => 'EMPIRE'],
['name' => 'JEDI', 'value' => 'JEDI']
]
]);
```
# Field Resolution
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;
$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.'
],
]
]);
$heroType = new ObjectType([
'name' => 'Hero',
'fields' => [
'appearsIn' => [
'type' => $episodeEnum,
'resolve' => function() {
return 5; // Actual entry in response will be 'appearsIn' => 'EMPIRE'
}
]
]
]);
```
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' => [
'appearsIn' => [
'type' => Type::boolean(),
'args' => [
'episode' => Type::nonNull($enumType)
],
'resolve' => function($hero, $args) {
return $args['episode'] === 5 ? true : false;
}
]
]
]);
```
Then following query:
```graphql
fragment on Hero {
appearsInNewHope: appearsIn(NEWHOPE)
appearsInEmpire: appearsIn(EMPIRE)
}
```
will return:
```php
[
'appearsInNewHope' => false,
'appearsInEmpire' => true
]
```

127
docs/type-system/index.md Normal file
View file

@ -0,0 +1,127 @@
# Type System
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: [`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](object-types.md).
# Type Definition Styles
Several styles of type definitions are supported depending on your preferences.
Inline definitions:
```php
<?php
namespace MyApp;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
$myType = new ObjectType([
'name' => 'MyType',
'fields' => [
'id' => Type::id()
]
]);
```
Class per type:
```php
<?php
namespace MyApp;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
class MyType extends ObjectType
{
public function __construct()
{
$config = [
// Note: 'name' is not needed in this form:
// it will be inferred from class name by omitting namespace and dropping "Type" suffix
'fields' => [
'id' => Type::id()
]
];
parent::__construct($config);
}
}
```
Using [GraphQL Type language](http://graphql.org/learn/schema/#type-language):
```graphql
schema {
query: Query
mutation: Mutation
}
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 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 the schema.
The typical way to do this is to create a registry of your types:
```php
<?php
namespace MyApp;
class TypeRegistry
{
private $myAType;
private $myBType;
public function myAType()
{
return $this->myAType ?: ($this->myAType = new MyAType($this));
}
public function myBType()
{
return $this->myBType ?: ($this->myBType = new MyBType($this));
}
}
```
And use this registry in type definition:
```php
<?php
namespace MyApp;
use GraphQL\Type\Definition\ObjectType;
class MyAType extends ObjectType
{
public function __construct(TypeRegistry $types)
{
parent::__construct([
'fields' => [
'b' => $types->myBType()
]
]);
}
}
```
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 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.

View file

@ -0,0 +1,172 @@
# 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
All types in GraphQL are of two categories: **input** and **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)
* **Input** types (or argument types) are: [Scalar](scalar-types.md), [Enum](enum-types.md), InputObject
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.md) or
[Enum](enum-types.md) types. But you can also pass complex objects.
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`
(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' => [
'author' => [
'type' => Type::id(),
'description' => 'Only show stories with this author id'
],
'popular' => [
'type' => Type::boolean(),
'description' => 'Only show popular stories (liked by several people)'
],
'tags' => [
'type' => Type::listOf(Type::string()),
'description' => 'Only show stories which contain all of those tags'
]
]
]);
```
Every field may be of other InputObjectType (thus complex hierarchies of inputs are possible)
# Configuration 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 `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:
Option | Type | Notes
------ | ---- | -----
name | `string` | **Required.** Name of the input field. When not set - inferred from **fields** array key
type | `Type` | **Required.** Instance of one of [Input Types](input-types.md) (**Scalar**, **Enum**, **InputObjectType** + any combination of those with **nonNull** and **listOf** modifiers)
description | `string` | Plain-text description of this input field for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation)
defaultValue | `scalar` | Default value of this input field. Use the internal value if specifying a default for an **enum** type
# 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' => [
'stories' => [
'type' => Type::listOf($storyType),
'args' => [
'filters' => [
'type' => $filters,
'defaultValue' => [
'popular' => true
]
]
],
'resolve' => function($rootValue, $args) {
return DataSource::filterStories($args['filters']);
}
]
]
]);
```
(note that you can define **defaultValue** for fields with complex inputs as associative array).
Then GraphQL query could include filters as literal value:
```graphql
{
stories(filters: {author: "1", popular: false})
}
```
Or as query variable:
```graphql
query($filters: StoryFiltersInput!) {
stories(filters: $filters)
}
```
```php
$variables = [
'filters' => [
"author" => "1",
"popular" => false
]
];
```
**graphql-php** will validate the input against your InputObjectType definition and pass it to your
resolver as `$args['filters']`

View file

@ -0,0 +1,136 @@
# Interface Type Definition
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 its subclasses) which accepts configuration array in a constructor:
```php
<?php
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\Type;
$character = 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.'
]
],
'resolveType' => function ($value) {
if ($value->type === 'human') {
return MyTypes::human();
} else {
return MyTypes::droid();
}
}
]);
```
This example uses **inline** style for Interface definition, but you can also use
[inheritance or type language](index.md#type-definition-styles).
# Configuration 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](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` | **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' => [
'id' => [
'type' => Type::nonNull(Type::string()),
'description' => 'The id of the character.',
],
'name' => [
'type' => Type::string(),
'description' => 'The name of the character.'
]
],
'interfaces' => [
$character
]
]);
```
Note that Object Type must include all fields of interface with exact same types
(including **nonNull** specification) and arguments.
The only exception is when object's field type is more specific than the type of this field defined in interface
(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 the field type to more specific.
Example:
```
interface A {
field1: A
}
type B implements A {
field1: B
}
```
# Sharing Interface fields
Since every Object Type implementing an Interface must have the same set of fields - it often makes
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' => [
$character
],
'fields' => [
'height' => Type::float(),
$character->getField('id'),
$character->getField('name')
]
]);
```
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 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 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
concrete Object Type.
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.

View file

@ -0,0 +1,62 @@
# Lists
**graphql-php** provides built-in support for lists. In order to create list type - wrap
existing type with `GraphQL\Type\Definition\Type::listOf()` modifier:
```php
<?php
namespace MyApp;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ObjectType;
$userType = new ObjectType([
'name' => 'User',
'fields' => [
'emails' => [
'type' => Type::listOf(Type::string()),
'resolve' => function() {
return ['jon@example.com', 'jonny@example.com'];
}
]
]
]);
```
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 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 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;
$humanType = new ObjectType([
'name' => 'User',
'fields' => [
'id' => [
'type' => Type::nonNull(Type::id()),
'resolve' => function() {
return uniqid();
}
],
'emails' => [
'type' => Type::nonNull(Type::listOf(Type::string())),
'resolve' => function() {
return ['jon@example.com', 'jonny@example.com'];
}
]
]
]);
```
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 the section on [Data Fetching](../data-fetching.md) for details.

View file

@ -0,0 +1,210 @@
# Object Type Definition
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 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:
```php
<?php
namespace MyApp;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Examples\Blog\Data\DataSource;
use GraphQL\Examples\Blog\Data\Story;
$userType = new ObjectType([
'name' => 'User',
'description' => 'Our blog visitor',
'fields' => [
'firstName' => [
'type' => Type::string(),
'description' => 'User first name'
],
'email' => Type::string()
]
]);
$blogStory = new ObjectType([
'name' => 'Story',
'fields' => [
'body' => Type::string(),
'author' => [
'type' => $userType,
'description' => 'Story author',
'resolve' => function(Story $blogStory) {
return DataSource::findUser($blogStory->authorId);
}
],
'likes' => [
'type' => Type::listOf($userType),
'description' => 'List of users who liked the story',
'args' => [
'limit' => [
'type' => Type::int(),
'description' => 'Limit the number of recent likes returned',
'defaultValue' => 10
]
],
'resolve' => function(Story $blogStory, $args) {
return DataSource::findLikes($blogStory->id, $args['limit']);
}
]
]
]);
```
This example uses **inline** style for Object Type definitions, but you can also use
[inheritance or type language](index.md#type-definition-styles).
# Configuration options
Object type constructor expects configuration array. Below is a full list of available options:
Option | Type | Notes
------------ | -------- | -----
name | `string` | **Required.** Unique name of this object type within Schema
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 `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](#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)
# Field arguments
Every field on a GraphQL object type can have zero or more arguments, defined in **args** option of field definition.
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.md) (**scalar**, **enum**, **InputObjectType** + any combination of those with **nonNull** and **listOf** modifiers)
description | `string` | Plain-text description of this argument for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation)
defaultValue | `scalar` | Default value for this argument. Use the internal value if specifying a default for an **enum** type
# Shorthand field definitions
Fields can be also defined in **shorthand** notation (with only **name** and **type** options):
```php
'fields' => [
'id' => Type::id(),
'fieldName' => $fieldType
]
```
which is equivalent of:
```php
'fields' => [
'id' => ['type' => Type::id()],
'fieldName' => ['type' => $fieldName]
]
```
which is in turn equivalent of the full form:
```php
'fields' => [
['name' => 'id', 'type' => Type::id()],
['name' => 'fieldName', 'type' => $fieldName]
]
```
Same shorthand notation applies to field arguments as well.
# Recurring and circular types
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 `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([
'name' => 'User',
'fields' => function() use (&$userType) {
return [
'email' => [
'type' => Type::string()
],
'friends' => [
'type' => Type::listOf($userType)
]
];
}
]);
```
Same example for [inheritance style of type definitions](index.md#type-definition-styles) using [TypeRegistry](index.md#type-registry):
```php
<?php
namespace MyApp;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ObjectType;
class UserType extends ObjectType
{
public function __construct()
{
$config = [
'fields' => function() {
return [
'email' => MyTypes::string(),
'friends' => MyTypes::listOf(MyTypes::user())
];
}
];
parent::__construct($config);
}
}
class MyTypes
{
private static $user;
public static function user()
{
return self::$user ?: (self::$user = new UserType());
}
public static function string()
{
return Type::string();
}
public static function listOf($type)
{
return Type::listOf($type);
}
}
```
# Field Resolution
Field resolution is the primary mechanism in **graphql-php** for returning actual data for your fields.
It is implemented using **resolveField** callable in type definition or **resolve**
callable in field definition (which has precedence).
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
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.

View file

@ -0,0 +1,130 @@
# 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`](../reference.md#graphqltypedefinitiontype) class:
```php
<?php
use GraphQL\Type\Definition\Type;
// Built-in Scalar types:
Type::string(); // String type
Type::int(); // Int type
Type::float(); // Float type
Type::boolean(); // Boolean type
Type::id(); // ID type
```
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.
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 a database
or hardcoded in the source code) to **serialized** representation included in the response.
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
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 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\Utils;
class EmailType extends ScalarType
{
// Note: name can be omitted. In this case it will be inferred from class name
// (suffix "Type" will be dropped)
public $name = 'Email';
/**
* Serializes an internal value to include in a response.
*
* @param string $value
* @return string
*/
public function serialize($value)
{
// 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 (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
// throw new InvariantViolation("Could not serialize following value as email: " . Utils::printSafe($value));
// }
// return $this->parseValue($value);
}
/**
* Parses an externally provided value (query variable) to use as an input
*
* @param mixed $value
* @return mixed
*/
public function parseValue($value)
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new Error("Cannot represent following value as email: " . Utils::printSafeJson($value));
}
return $value;
}
/**
* Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input.
*
* E.g.
* {
* user(email: "user@example.com")
* }
*
* @param \GraphQL\Language\AST\Node $valueNode
* @param array|null $variables
* @return string
* @throws Error
*/
public function parseLiteral($valueNode, array $variables = null)
{
// Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL
// error location in query:
if (!$valueNode instanceof StringValueNode) {
throw new Error('Query error: Can only parse strings got: ' . $valueNode->kind, [$valueNode]);
}
if (!filter_var($valueNode->value, FILTER_VALIDATE_EMAIL)) {
throw new Error("Not a valid email", [$valueNode]);
}
return $valueNode->value;
}
}
```
Or with inline style:
```php
<?php
use GraphQL\Type\Definition\CustomScalarType;
$emailType = new CustomScalarType([
'name' => 'Email',
'serialize' => function($value) {/* See function body above */},
'parseValue' => function($value) {/* See function body above */},
'parseLiteral' => function($valueNode, array $variables = null) {/* See function body above */},
]);
```

194
docs/type-system/schema.md Normal file
View file

@ -0,0 +1,194 @@
# Schema Definition
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\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).
# Query and Mutation 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 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;
$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'hello' => [
'type' => Type::string(),
'resolve' => function() {
return 'Hello World!';
}
],
'hero' => [
'type' => $characterInterface,
'args' => [
'episode' => [
'type' => $episodeEnum
]
],
'resolve' => function ($rootValue, $args) {
return StarWarsData::getHero(isset($args['episode']) ? $args['episode'] : null);
},
]
]
]);
$mutationType = new ObjectType([
'name' => 'Mutation',
'fields' => [
'createReview' => [
'type' => $createReviewOutput,
'args' => [
'episode' => $episodeEnum,
'review' => $reviewInputObject
],
'resolve' => function($rootValue, $args) {
// TODOC
}
]
]
]);
```
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 [Mutations and Input Types](input-types.md) for details).
# Configuration 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[]` | 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();
}
```

View 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);
```

View file

@ -0,0 +1,39 @@
# Union Type Definition
A Union is an abstract type that simply enumerates other 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 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();
} else {
return MyTypes::user();
}
}
]);
```
This example uses **inline** style for Union definition, but you can also use
[inheritance or type language](index.md#type-definition-styles).
# Configuration 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` | **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**.

View file

@ -0,0 +1,18 @@
# Hello world
Clean and simple single-file example of main GraphQL concepts originally proposed and
implemented by [Leo Cavalcante](https://github.com/leocavalcante)
### 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) }" }'
```

View file

@ -0,0 +1,69 @@
<?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\GraphQL;
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,
]);
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
$query = $input['query'];
$variableValues = isset($input['variables']) ? $input['variables'] : null;
$rootValue = ['prefix' => 'You said: '];
$result = GraphQL::executeQuery($schema, $query, $rootValue, null, $variableValues);
$output = $result->toArray();
} catch (\Exception $e) {
$output = [
'error' => [
'message' => $e->getMessage()
]
];
}
header('Content-Type: application/json; charset=UTF-8');
echo json_encode($output);

View file

@ -0,0 +1,28 @@
<?php
namespace GraphQL\Examples\Blog;
use GraphQL\Examples\Blog\Data\User;
/**
* Class AppContext
* Instance available in all GraphQL resolvers as 3rd argument
*
* @package GraphQL\Examples\Blog
*/
class AppContext
{
/**
* @var string
*/
public $rootUrl;
/**
* @var User
*/
public $viewer;
/**
* @var \mixed
*/
public $request;
}

View file

@ -0,0 +1,25 @@
<?php
namespace GraphQL\Examples\Blog\Data;
use GraphQL\Utils\Utils;
class Comment
{
public $id;
public $authorId;
public $storyId;
public $parentId;
public $body;
public $isAnonymous;
public function __construct(array $data)
{
Utils::assign($this, $data);
}
}

View file

@ -0,0 +1,206 @@
<?php
namespace GraphQL\Examples\Blog\Data;
/**
* Class DataSource
*
* This is just a simple in-memory data holder for the sake of example.
* Data layer for real app may use Doctrine or query the database directly (e.g. in CQRS style)
*
* @package GraphQL\Examples\Blog
*/
class DataSource
{
private static $users = [];
private static $stories = [];
private static $storyLikes = [];
private static $comments = [];
private static $storyComments = [];
private static $commentReplies = [];
private static $storyMentions = [];
public static function init()
{
self::$users = [
'1' => new User([
'id' => '1',
'email' => 'john@example.com',
'firstName' => 'John',
'lastName' => 'Doe'
]),
'2' => new User([
'id' => '2',
'email' => 'jane@example.com',
'firstName' => 'Jane',
'lastName' => 'Doe'
]),
'3' => new User([
'id' => '3',
'email' => 'john@example.com',
'firstName' => 'John',
'lastName' => 'Doe'
]),
];
self::$stories = [
'1' => new Story(['id' => '1', 'authorId' => '1', 'body' => '<h1>GraphQL is awesome!</h1>']),
'2' => new Story(['id' => '2', 'authorId' => '1', 'body' => '<a>Test this</a>']),
'3' => new Story(['id' => '3', 'authorId' => '3', 'body' => "This\n<br>story\n<br>spans\n<br>newlines"]),
];
self::$storyLikes = [
'1' => ['1', '2', '3'],
'2' => [],
'3' => ['1']
];
self::$comments = [
// thread #1:
'100' => new Comment(['id' => '100', 'authorId' => '3', 'storyId' => '1', 'body' => 'Likes']),
'110' => new Comment(['id' =>'110', 'authorId' =>'2', 'storyId' => '1', 'body' => 'Reply <b>#1</b>', 'parentId' => '100']),
'111' => new Comment(['id' => '111', 'authorId' => '1', 'storyId' => '1', 'body' => 'Reply #1-1', 'parentId' => '110']),
'112' => new Comment(['id' => '112', 'authorId' => '3', 'storyId' => '1', 'body' => 'Reply #1-2', 'parentId' => '110']),
'113' => new Comment(['id' => '113', 'authorId' => '2', 'storyId' => '1', 'body' => 'Reply #1-3', 'parentId' => '110']),
'114' => new Comment(['id' => '114', 'authorId' => '1', 'storyId' => '1', 'body' => 'Reply #1-4', 'parentId' => '110']),
'115' => new Comment(['id' => '115', 'authorId' => '3', 'storyId' => '1', 'body' => 'Reply #1-5', 'parentId' => '110']),
'116' => new Comment(['id' => '116', 'authorId' => '1', 'storyId' => '1', 'body' => 'Reply #1-6', 'parentId' => '110']),
'117' => new Comment(['id' => '117', 'authorId' => '2', 'storyId' => '1', 'body' => 'Reply #1-7', 'parentId' => '110']),
'120' => new Comment(['id' => '120', 'authorId' => '3', 'storyId' => '1', 'body' => 'Reply #2', 'parentId' => '100']),
'130' => new Comment(['id' => '130', 'authorId' => '3', 'storyId' => '1', 'body' => 'Reply #3', 'parentId' => '100']),
'200' => new Comment(['id' => '200', 'authorId' => '2', 'storyId' => '1', 'body' => 'Me2']),
'300' => new Comment(['id' => '300', 'authorId' => '3', 'storyId' => '1', 'body' => 'U2']),
# thread #2:
'400' => new Comment(['id' => '400', 'authorId' => '2', 'storyId' => '2', 'body' => 'Me too']),
'500' => new Comment(['id' => '500', 'authorId' => '2', 'storyId' => '2', 'body' => 'Nice!']),
];
self::$storyComments = [
'1' => ['100', '200', '300'],
'2' => ['400', '500']
];
self::$commentReplies = [
'100' => ['110', '120', '130'],
'110' => ['111', '112', '113', '114', '115', '116', '117'],
];
self::$storyMentions = [
'1' => [
self::$users['2']
],
'2' => [
self::$stories['1'],
self::$users['3']
]
];
}
public static function findUser($id)
{
return isset(self::$users[$id]) ? self::$users[$id] : null;
}
public static function findStory($id)
{
return isset(self::$stories[$id]) ? self::$stories[$id] : null;
}
public static function findComment($id)
{
return isset(self::$comments[$id]) ? self::$comments[$id] : null;
}
public static function findLastStoryFor($authorId)
{
$storiesFound = array_filter(self::$stories, function(Story $story) use ($authorId) {
return $story->authorId == $authorId;
});
return !empty($storiesFound) ? $storiesFound[count($storiesFound) - 1] : null;
}
public static function findLikes($storyId, $limit)
{
$likes = isset(self::$storyLikes[$storyId]) ? self::$storyLikes[$storyId] : [];
$result = array_map(
function($userId) {
return self::$users[$userId];
},
$likes
);
return array_slice($result, 0, $limit);
}
public static function isLikedBy($storyId, $userId)
{
$subscribers = isset(self::$storyLikes[$storyId]) ? self::$storyLikes[$storyId] : [];
return in_array($userId, $subscribers);
}
public static function getUserPhoto($userId, $size)
{
return new Image([
'id' => $userId,
'type' => Image::TYPE_USERPIC,
'size' => $size,
'width' => rand(100, 200),
'height' => rand(100, 200)
]);
}
public static function findLatestStory()
{
return array_pop(self::$stories);
}
public static function findStories($limit, $afterId = null)
{
$start = $afterId ? (int) array_search($afterId, array_keys(self::$stories)) + 1 : 0;
return array_slice(array_values(self::$stories), $start, $limit);
}
public static function findComments($storyId, $limit = 5, $afterId = null)
{
$storyComments = isset(self::$storyComments[$storyId]) ? self::$storyComments[$storyId] : [];
$start = isset($after) ? (int) array_search($afterId, $storyComments) + 1 : 0;
$storyComments = array_slice($storyComments, $start, $limit);
return array_map(
function($commentId) {
return self::$comments[$commentId];
},
$storyComments
);
}
public static function findReplies($commentId, $limit = 5, $afterId = null)
{
$commentReplies = isset(self::$commentReplies[$commentId]) ? self::$commentReplies[$commentId] : [];
$start = isset($after) ? (int) array_search($afterId, $commentReplies) + 1: 0;
$commentReplies = array_slice($commentReplies, $start, $limit);
return array_map(
function($replyId) {
return self::$comments[$replyId];
},
$commentReplies
);
}
public static function countComments($storyId)
{
return isset(self::$storyComments[$storyId]) ? count(self::$storyComments[$storyId]) : 0;
}
public static function countReplies($commentId)
{
return isset(self::$commentReplies[$commentId]) ? count(self::$commentReplies[$commentId]) : 0;
}
public static function findStoryMentions($storyId)
{
return isset(self::$storyMentions[$storyId]) ? self::$storyMentions[$storyId] :[];
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace GraphQL\Examples\Blog\Data;
use GraphQL\Utils\Utils;
class Image
{
const TYPE_USERPIC = 'userpic';
const SIZE_ICON = 'icon';
const SIZE_SMALL = 'small';
const SIZE_MEDIUM = 'medium';
const SIZE_ORIGINAL = 'original';
public $id;
public $type;
public $size;
public $width;
public $height;
public function __construct(array $data)
{
Utils::assign($this, $data);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace GraphQL\Examples\Blog\Data;
use GraphQL\Utils\Utils;
class Story
{
public $id;
public $authorId;
public $title;
public $body;
public $isAnonymous = false;
public function __construct(array $data)
{
Utils::assign($this, $data);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace GraphQL\Examples\Blog\Data;
use GraphQL\Utils\Utils;
class User
{
public $id;
public $email;
public $firstName;
public $lastName;
public $hasPhoto;
public function __construct(array $data)
{
Utils::assign($this, $data);
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\AppContext;
use GraphQL\Examples\Blog\Data\Comment;
use GraphQL\Examples\Blog\Data\DataSource;
use GraphQL\Examples\Blog\Types;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
class CommentType extends ObjectType
{
public function __construct()
{
$config = [
'name' => 'Comment',
'fields' => function() {
return [
'id' => Types::id(),
'author' => Types::user(),
'parent' => Types::comment(),
'isAnonymous' => Types::boolean(),
'replies' => [
'type' => Types::listOf(Types::comment()),
'args' => [
'after' => Types::int(),
'limit' => [
'type' => Types::int(),
'defaultValue' => 5
]
]
],
'totalReplyCount' => Types::int(),
Types::htmlField('body')
];
},
'resolveField' => function($comment, $args, $context, ResolveInfo $info) {
$method = 'resolve' . ucfirst($info->fieldName);
if (method_exists($this, $method)) {
return $this->{$method}($comment, $args, $context, $info);
} else {
return $comment->{$info->fieldName};
}
}
];
parent::__construct($config);
}
public function resolveAuthor(Comment $comment)
{
if ($comment->isAnonymous) {
return null;
}
return DataSource::findUser($comment->authorId);
}
public function resolveParent(Comment $comment)
{
if ($comment->parentId) {
return DataSource::findComment($comment->parentId);
}
return null;
}
public function resolveReplies(Comment $comment, $args)
{
$args += ['after' => null];
return DataSource::findReplies($comment->id, $args['limit'], $args['after']);
}
public function resolveTotalReplyCount(Comment $comment)
{
return DataSource::countReplies($comment->id);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace GraphQL\Examples\Blog\Type\Enum;
use GraphQL\Type\Definition\EnumType;
class ContentFormatEnum extends EnumType
{
const FORMAT_TEXT = 'TEXT';
const FORMAT_HTML = 'HTML';
public function __construct()
{
$config = [
'name' => 'ContentFormatEnum',
'values' => [self::FORMAT_TEXT, self::FORMAT_HTML]
];
parent::__construct($config);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace GraphQL\Examples\Blog\Type\Enum;
use GraphQL\Examples\Blog\Data\Image;
use GraphQL\Type\Definition\EnumType;
class ImageSizeEnumType extends EnumType
{
public function __construct()
{
$config = [
// Note: 'name' option is not needed in this form - it will be inferred from className
'values' => [
'ICON' => Image::SIZE_ICON,
'SMALL' => Image::SIZE_SMALL,
'MEDIUM' => Image::SIZE_MEDIUM,
'ORIGINAL' => Image::SIZE_ORIGINAL
]
];
parent::__construct($config);
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace GraphQL\Examples\Blog\Type\Field;
use GraphQL\Examples\Blog\Type\Enum\ContentFormatEnum;
use GraphQL\Examples\Blog\Types;
class HtmlField
{
public static function build($name, $objectKey = null)
{
$objectKey = $objectKey ?: $name;
// Demonstrates how to organize re-usable fields
// Usual example: when the same field with same args shows up in different types
// (for example when it is a part of some interface)
return [
'name' => $name,
'type' => Types::string(),
'args' => [
'format' => [
'type' => Types::contentFormatEnum(),
'defaultValue' => ContentFormatEnum::FORMAT_HTML
],
'maxLength' => Types::int()
],
'resolve' => function($object, $args) use ($objectKey) {
$html = $object->{$objectKey};
$text = strip_tags($html);
if (!empty($args['maxLength'])) {
$safeText = mb_substr($text, 0, $args['maxLength']);
} else {
$safeText = $text;
}
switch ($args['format']) {
case ContentFormatEnum::FORMAT_HTML:
if ($safeText !== $text) {
// Text was truncated, so just show what's safe:
return nl2br($safeText);
} else {
return $html;
}
case ContentFormatEnum::FORMAT_TEXT:
default:
return $safeText;
}
}
];
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\AppContext;
use GraphQL\Examples\Blog\Data\Image;
use GraphQL\Examples\Blog\Types;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\ObjectType;
class ImageType extends ObjectType
{
public function __construct()
{
$config = [
'name' => 'ImageType',
'fields' => [
'id' => Types::id(),
'type' => new EnumType([
'name' => 'ImageTypeEnum',
'values' => [
'USERPIC' => Image::TYPE_USERPIC
]
]),
'size' => Types::imageSizeEnum(),
'width' => Types::int(),
'height' => Types::int(),
'url' => [
'type' => Types::url(),
'resolve' => [$this, 'resolveUrl']
],
// Just for the sake of example
'fieldWithError' => [
'type' => Types::string(),
'resolve' => function() {
throw new \Exception("Field with exception");
}
],
'nonNullFieldWithError' => [
'type' => Types::nonNull(Types::string()),
'resolve' => function() {
throw new \Exception("Non-null field with exception");
}
]
]
];
parent::__construct($config);
}
public function resolveUrl(Image $value, $args, AppContext $context)
{
switch ($value->type) {
case Image::TYPE_USERPIC:
$path = "/images/user/{$value->id}-{$value->size}.jpg";
break;
default:
throw new \UnexpectedValueException("Unexpected image type: " . $value->type);
}
return $context->rootUrl . $path;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\Data\Story;
use GraphQL\Examples\Blog\Data\User;
use GraphQL\Examples\Blog\Data\Image;
use GraphQL\Examples\Blog\Types;
use GraphQL\Type\Definition\InterfaceType;
class NodeType extends InterfaceType
{
public function __construct()
{
$config = [
'name' => 'Node',
'fields' => [
'id' => Types::id()
],
'resolveType' => [$this, 'resolveNodeType']
];
parent::__construct($config);
}
public function resolveNodeType($object)
{
if ($object instanceof User) {
return Types::user();
} else if ($object instanceof Image) {
return Types::image();
} else if ($object instanceof Story) {
return Types::story();
}
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\AppContext;
use GraphQL\Examples\Blog\Data\DataSource;
use GraphQL\Examples\Blog\Types;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
class QueryType extends ObjectType
{
public function __construct()
{
$config = [
'name' => 'Query',
'fields' => [
'user' => [
'type' => Types::user(),
'description' => 'Returns user by id (in range of 1-5)',
'args' => [
'id' => Types::nonNull(Types::id())
]
],
'viewer' => [
'type' => Types::user(),
'description' => 'Represents currently logged-in user (for the sake of example - simply returns user with id == 1)'
],
'stories' => [
'type' => Types::listOf(Types::story()),
'description' => 'Returns subset of stories posted for this blog',
'args' => [
'after' => [
'type' => Types::id(),
'description' => 'Fetch stories listed after the story with this ID'
],
'limit' => [
'type' => Types::int(),
'description' => 'Number of stories to be returned',
'defaultValue' => 10
]
]
],
'lastStoryPosted' => [
'type' => Types::story(),
'description' => 'Returns last story posted for this blog'
],
'deprecatedField' => [
'type' => Types::string(),
'deprecationReason' => 'This field is deprecated!'
],
'fieldWithException' => [
'type' => Types::string(),
'resolve' => function() {
throw new \Exception("Exception message thrown in field resolver");
}
],
'hello' => Type::string()
],
'resolveField' => function($rootValue, $args, $context, ResolveInfo $info) {
return $this->{$info->fieldName}($rootValue, $args, $context, $info);
}
];
parent::__construct($config);
}
public function user($rootValue, $args)
{
return DataSource::findUser($args['id']);
}
public function viewer($rootValue, $args, AppContext $context)
{
return $context->viewer;
}
public function stories($rootValue, $args)
{
$args += ['after' => null];
return DataSource::findStories($args['limit'], $args['after']);
}
public function lastStoryPosted()
{
return DataSource::findLatestStory();
}
public function hello()
{
return 'Your graphql-php endpoint is ready! Use GraphiQL to browse API';
}
public function deprecatedField()
{
return 'You can request deprecated field, but it is not displayed in auto-generated documentation by default.';
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace GraphQL\Examples\Blog\Type\Scalar;
use GraphQL\Error\Error;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Utils\Utils;
class EmailType
{
public static function create()
{
return new CustomScalarType([
'name' => 'Email',
'serialize' => [__CLASS__, 'serialize'],
'parseValue' => [__CLASS__, 'parseValue'],
'parseLiteral' => [__CLASS__, 'parseLiteral'],
]);
}
/**
* Serializes an internal value to include in a response.
*
* @param string $value
* @return string
*/
public static function serialize($value)
{
// 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:
// return $this->parseValue($value);
}
/**
* Parses an externally provided value (query variable) to use as an input
*
* @param mixed $value
* @return mixed
*/
public static function parseValue($value)
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new \UnexpectedValueException("Cannot represent value as email: " . Utils::printSafe($value));
}
return $value;
}
/**
* Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input
*
* @param \GraphQL\Language\AST\Node $valueNode
* @return string
* @throws Error
*/
public static function parseLiteral($valueNode)
{
// Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL
// error location in query:
if (!$valueNode instanceof StringValueNode) {
throw new Error('Query error: Can only parse strings got: ' . $valueNode->kind, [$valueNode]);
}
if (!filter_var($valueNode->value, FILTER_VALIDATE_EMAIL)) {
throw new Error("Not a valid email", [$valueNode]);
}
return $valueNode->value;
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace GraphQL\Examples\Blog\Type\Scalar;
use GraphQL\Error\Error;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Utils\Utils;
class UrlType extends ScalarType
{
/**
* Serializes an internal value to include in a response.
*
* @param mixed $value
* @return mixed
*/
public function serialize($value)
{
// Assuming internal representation of url 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:
// return $this->parseValue($value);
}
/**
* Parses an externally provided value (query variable) to use as an input
*
* @param mixed $value
* @return mixed
* @throws Error
*/
public function parseValue($value)
{
if (!is_string($value) || !filter_var($value, FILTER_VALIDATE_URL)) { // quite naive, but after all this is example
throw new Error("Cannot represent value as URL: " . Utils::printSafe($value));
}
return $value;
}
/**
* Parses an externally provided literal value to use as an input (e.g. in Query AST)
*
* @param Node $valueNode
* @param array|null $variables
* @return null|string
* @throws Error
*/
public function parseLiteral($valueNode, array $variables = null)
{
// Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL
// error location in query:
if (!($valueNode instanceof StringValueNode)) {
throw new Error('Query error: Can only parse strings got: ' . $valueNode->kind, [$valueNode]);
}
if (!is_string($valueNode->value) || !filter_var($valueNode->value, FILTER_VALIDATE_URL)) {
throw new Error('Query error: Not a valid URL', [$valueNode]);
}
return $valueNode->value;
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\Data\Story;
use GraphQL\Examples\Blog\Data\User;
use GraphQL\Examples\Blog\Types;
use GraphQL\Type\Definition\UnionType;
class SearchResultType extends UnionType
{
public function __construct()
{
$config = [
'name' => 'SearchResultType',
'types' => function() {
return [
Types::story(),
Types::user()
];
},
'resolveType' => function($value) {
if ($value instanceof Story) {
return Types::story();
} else if ($value instanceof User) {
return Types::user();
}
}
];
parent::__construct($config);
}
}

View file

@ -0,0 +1,127 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\AppContext;
use GraphQL\Examples\Blog\Data\DataSource;
use GraphQL\Examples\Blog\Data\Story;
use GraphQL\Examples\Blog\Types;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
/**
* Class StoryType
* @package GraphQL\Examples\Social\Type
*/
class StoryType extends ObjectType
{
const EDIT = 'EDIT';
const DELETE = 'DELETE';
const LIKE = 'LIKE';
const UNLIKE = 'UNLIKE';
const REPLY = 'REPLY';
public function __construct()
{
$config = [
'name' => 'Story',
'fields' => function() {
return [
'id' => Types::id(),
'author' => Types::user(),
'mentions' => Types::listOf(Types::mention()),
'totalCommentCount' => Types::int(),
'comments' => [
'type' => Types::listOf(Types::comment()),
'args' => [
'after' => [
'type' => Types::id(),
'description' => 'Load all comments listed after given comment ID'
],
'limit' => [
'type' => Types::int(),
'defaultValue' => 5
]
]
],
'likes' => [
'type' => Types::listOf(Types::user()),
'args' => [
'limit' => [
'type' => Types::int(),
'description' => 'Limit the number of recent likes returned',
'defaultValue' => 5
]
]
],
'likedBy' => [
'type' => Types::listOf(Types::user()),
],
'affordances' => Types::listOf(new EnumType([
'name' => 'StoryAffordancesEnum',
'values' => [
self::EDIT,
self::DELETE,
self::LIKE,
self::UNLIKE,
self::REPLY
]
])),
'hasViewerLiked' => Types::boolean(),
Types::htmlField('body'),
];
},
'interfaces' => [
Types::node()
],
'resolveField' => function($story, $args, $context, ResolveInfo $info) {
$method = 'resolve' . ucfirst($info->fieldName);
if (method_exists($this, $method)) {
return $this->{$method}($story, $args, $context, $info);
} else {
return $story->{$info->fieldName};
}
}
];
parent::__construct($config);
}
public function resolveAuthor(Story $story)
{
return DataSource::findUser($story->authorId);
}
public function resolveAffordances(Story $story, $args, AppContext $context)
{
$isViewer = $context->viewer === DataSource::findUser($story->authorId);
$isLiked = DataSource::isLikedBy($story->id, $context->viewer->id);
if ($isViewer) {
$affordances[] = self::EDIT;
$affordances[] = self::DELETE;
}
if ($isLiked) {
$affordances[] = self::UNLIKE;
} else {
$affordances[] = self::LIKE;
}
return $affordances;
}
public function resolveHasViewerLiked(Story $story, $args, AppContext $context)
{
return DataSource::isLikedBy($story->id, $context->viewer->id);
}
public function resolveTotalCommentCount(Story $story)
{
return DataSource::countComments($story->id);
}
public function resolveComments(Story $story, $args)
{
$args += ['after' => null];
return DataSource::findComments($story->id, $args['limit'], $args['after']);
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\AppContext;
use GraphQL\Examples\Blog\Data\DataSource;
use GraphQL\Examples\Blog\Data\User;
use GraphQL\Examples\Blog\Types;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
class UserType extends ObjectType
{
public function __construct()
{
$config = [
'name' => 'User',
'description' => 'Our blog authors',
'fields' => function() {
return [
'id' => Types::id(),
'email' => Types::email(),
'photo' => [
'type' => Types::image(),
'description' => 'User photo URL',
'args' => [
'size' => Types::nonNull(Types::imageSizeEnum()),
]
],
'firstName' => [
'type' => Types::string(),
],
'lastName' => [
'type' => Types::string(),
],
'lastStoryPosted' => Types::story(),
'fieldWithError' => [
'type' => Types::string(),
'resolve' => function() {
throw new \Exception("This is error field");
}
]
];
},
'interfaces' => [
Types::node()
],
'resolveField' => function($user, $args, $context, ResolveInfo $info) {
$method = 'resolve' . ucfirst($info->fieldName);
if (method_exists($this, $method)) {
return $this->{$method}($user, $args, $context, $info);
} else {
return $user->{$info->fieldName};
}
}
];
parent::__construct($config);
}
public function resolvePhoto(User $user, $args)
{
return DataSource::getUserPhoto($user->id, $args['size']);
}
public function resolveLastStoryPosted(User $user)
{
return DataSource::findLastStoryFor($user->id);
}
}

View file

@ -0,0 +1,209 @@
<?php
namespace GraphQL\Examples\Blog;
use GraphQL\Examples\Blog\Type\CommentType;
use GraphQL\Examples\Blog\Type\Enum\ContentFormatEnum;
use GraphQL\Examples\Blog\Type\Enum\ImageSizeEnumType;
use GraphQL\Examples\Blog\Type\Field\HtmlField;
use GraphQL\Examples\Blog\Type\SearchResultType;
use GraphQL\Examples\Blog\Type\NodeType;
use GraphQL\Examples\Blog\Type\QueryType;
use GraphQL\Examples\Blog\Type\Scalar\EmailType;
use GraphQL\Examples\Blog\Type\StoryType;
use GraphQL\Examples\Blog\Type\Scalar\UrlType;
use GraphQL\Examples\Blog\Type\UserType;
use GraphQL\Examples\Blog\Type\ImageType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type;
/**
* Class Types
*
* Acts as a registry and factory for your types.
*
* As simplistic as possible for the sake of clarity of this example.
* Your own may be more dynamic (or even code-generated).
*
* @package GraphQL\Examples\Blog
*/
class Types
{
// Object types:
private static $user;
private static $story;
private static $comment;
private static $image;
private static $query;
/**
* @return UserType
*/
public static function user()
{
return self::$user ?: (self::$user = new UserType());
}
/**
* @return StoryType
*/
public static function story()
{
return self::$story ?: (self::$story = new StoryType());
}
/**
* @return CommentType
*/
public static function comment()
{
return self::$comment ?: (self::$comment = new CommentType());
}
/**
* @return ImageType
*/
public static function image()
{
return self::$image ?: (self::$image = new ImageType());
}
/**
* @return QueryType
*/
public static function query()
{
return self::$query ?: (self::$query = new QueryType());
}
// Interface types
private static $node;
/**
* @return NodeType
*/
public static function node()
{
return self::$node ?: (self::$node = new NodeType());
}
// Unions types:
private static $mention;
/**
* @return SearchResultType
*/
public static function mention()
{
return self::$mention ?: (self::$mention = new SearchResultType());
}
// Enum types
private static $imageSizeEnum;
private static $contentFormatEnum;
/**
* @return ImageSizeEnumType
*/
public static function imageSizeEnum()
{
return self::$imageSizeEnum ?: (self::$imageSizeEnum = new ImageSizeEnumType());
}
/**
* @return ContentFormatEnum
*/
public static function contentFormatEnum()
{
return self::$contentFormatEnum ?: (self::$contentFormatEnum = new ContentFormatEnum());
}
// Custom Scalar types:
private static $urlType;
private static $emailType;
public static function email()
{
return self::$emailType ?: (self::$emailType = EmailType::create());
}
/**
* @return UrlType
*/
public static function url()
{
return self::$urlType ?: (self::$urlType = new UrlType());
}
/**
* @param $name
* @param null $objectKey
* @return array
*/
public static function htmlField($name, $objectKey = null)
{
return HtmlField::build($name, $objectKey);
}
// Let's add internal types as well for consistent experience
public static function boolean()
{
return Type::boolean();
}
/**
* @return \GraphQL\Type\Definition\FloatType
*/
public static function float()
{
return Type::float();
}
/**
* @return \GraphQL\Type\Definition\IDType
*/
public static function id()
{
return Type::id();
}
/**
* @return \GraphQL\Type\Definition\IntType
*/
public static function int()
{
return Type::int();
}
/**
* @return \GraphQL\Type\Definition\StringType
*/
public static function string()
{
return Type::string();
}
/**
* @param Type $type
* @return ListOfType
*/
public static function listOf($type)
{
return new ListOfType($type);
}
/**
* @param Type $type
* @return NonNull
*/
public static function nonNull($type)
{
return new NonNull($type);
}
}

120
examples/01-blog/README.md Normal file
View file

@ -0,0 +1,120 @@
## Blog Example
Simple yet full-featured example of GraphQL API. Models blogging platform with Stories, Users
and hierarchical comments.
### Run locally
```
php -S localhost:8080 ./graphql.php
```
### Test if GraphQL is running
If you open `http://localhost:8080` in browser you should see `json` response with
following message:
```
{
data: {
hello: "Your GraphQL endpoint is ready! Install GraphiQL to browse API"
}
}
```
Note that some browsers may try to download JSON file instead of showing you the response.
In this case try to install browser plugin that adds JSON support (like JSONView or similar)
### Debugging Mode
By default GraphQL endpoint exposed at `http://localhost:8080` runs in production mode without
additional debugging tools enabled.
In order to enable debugging mode with additional validation, error handling and reporting -
use `http://localhost:8080?debug=1` as endpoint
### Browsing API
The most convenient way to browse GraphQL API is by using [GraphiQL](https://github.com/graphql/graphiql)
But setting it up from scratch may be inconvenient. An easy alternative is to use one of
the existing Google Chrome extensions:
- [ChromeiQL](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij)
- [GraphiQL Feen](https://chrome.google.com/webstore/detail/graphiql-feen/mcbfdonlkfpbfdpimkjilhdneikhfklp)
Set `http://localhost:8080?debug=1` as your GraphQL endpoint/server in one of these extensions
and try clicking "Docs" button (usually in the top-right corner) to browse auto-generated
documentation.
### Running GraphQL queries
Copy following query to GraphiQL and execute (by clicking play button on top bar)
```
{
viewer {
id
email
}
user(id: "2") {
id
email
}
stories(after: "1") {
id
body
comments {
...CommentView
}
}
lastStoryPosted {
id
hasViewerLiked
author {
id
photo(size: ICON) {
id
url
type
size
width
height
# Uncomment following line to see validation error:
# nonExistingField
# Uncomment to see error reporting for fields with exceptions thrown in resolvers
# fieldWithError
# nonNullFieldWithError
}
lastStoryPosted {
id
}
}
body(format: HTML, maxLength: 10)
}
}
fragment CommentView on Comment {
id
body
totalReplyCount
replies {
id
body
}
}
```
### Run your own query
Use GraphiQL autocomplete (via CTRL+space) to easily create your own query.
Note: GraphQL query requires at least one field per object type (to prevent accidental overfetching).
For example following query is invalid in GraphQL:
```
{
viewer
}
```
Try copying this query and see what happens
### Run mutation query
TODOC
### Dig into source code
Now when you tried GraphQL API as a consumer, see how it is implemented by browsing
source code.

View file

@ -0,0 +1,71 @@
<?php
// Test this using following command
// php -S localhost:8080 ./graphql.php
require_once __DIR__ . '/../../vendor/autoload.php';
use \GraphQL\Examples\Blog\Types;
use \GraphQL\Examples\Blog\AppContext;
use \GraphQL\Examples\Blog\Data\DataSource;
use \GraphQL\Type\Schema;
use \GraphQL\GraphQL;
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'])) {
set_error_handler(function($severity, $message, $file, $line) use (&$phpErrors) {
throw new ErrorException($message, 0, $severity, $file, $line);
});
$debug = Debug::INCLUDE_DEBUG_MESSAGE | Debug::INCLUDE_TRACE;
}
try {
// Initialize our fake data source
DataSource::init();
// Prepare context that will be available in all field resolvers (as 3rd argument):
$appContext = new AppContext();
$appContext->viewer = DataSource::findUser('1'); // simulated "currently logged-in user"
$appContext->rootUrl = 'http://localhost:8080';
$appContext->request = $_REQUEST;
// 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) ?: [];
} else {
$data = $_REQUEST;
}
$data += ['query' => null, 'variables' => null];
if (null === $data['query']) {
$data['query'] = '{hello}';
}
// GraphQL schema to be passed to query executor:
$schema = new Schema([
'query' => Types::query()
]);
$result = GraphQL::executeQuery(
$schema,
$data['query'],
null,
$appContext,
(array) $data['variables']
);
$output = $result->toArray($debug);
$httpStatus = 200;
} catch (\Exception $error) {
$httpStatus = 500;
$output['errors'] = [
FormattedError::createFromException($error, $debug)
];
}
header('Content-Type: application/json', true, $httpStatus);
echo json_encode($output);

View 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) }" }'
```

View 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);

View 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: ',
];

View file

@ -0,0 +1,13 @@
schema {
query: Query
mutation: Calc
}
type Calc {
sum(x: Int, y: Int): Int
}
type Query {
echo(message: String): String
}

View 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
```

View 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);
}

26
mkdocs.yml Normal file
View file

@ -0,0 +1,26 @@
site_name: graphql-php
pages:
- About: index.md
- Getting Started: getting-started.md
- 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
- Enumeration Types: type-system/enum-types.md
- Lists and Non-Null: type-system/lists-and-nonnulls.md
- Interfaces: type-system/interfaces.md
- Unions: type-system/unions.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
# - Mutations: mutations.md
- Security: security.md
# - Performance tips: performance.md
- How it works: how-it-works.md
- Class Reference: reference.md
theme: readthedocs

6
phpbench.json Normal file
View file

@ -0,0 +1,6 @@
{
"bootstrap": "vendor/autoload.php",
"path": "benchmarks",
"retry_threshold": 5,
"time_unit": "milliseconds"
}

102
phpcs.xml.dist Normal file
View 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
View 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

View file

@ -1,34 +1,28 @@
<?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>
</testsuite>
</testsuites>
<groups>
<exclude>
<group>ReactPromise</group>
</exclude>
</groups>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./tests</directory>
<directory>./vendor</directory>
</exclude>
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
<php>
<ini name="error_reporting" value="E_ALL"/>
</php>
</phpunit>

65
src/Deferred.php Normal file
View file

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

View file

@ -1,132 +0,0 @@
<?php
namespace GraphQL;
use GraphQL\Language\Source;
// /graphql-js/src/error/GraphQLError.js
class Error extends \Exception
{
/**
* @var string
*/
public $message;
/**
* @var array
*/
public $nodes;
/**
* @var array
*/
private $positions;
/**
* @var array<SourceLocation>
*/
private $locations;
/**
* @var Source|null
*/
private $source;
/**
* 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
* @return Error
*/
public static function createLocatedError($error, $nodes = null)
{
if ($error instanceof \Exception) {
$message = $error->getMessage();
$previous = $error;
} else {
$message = (string) $error;
$previous = null;
}
return new Error($message, $nodes, $previous);
}
/**
* @param Error $error
* @return array
*/
public static function formatError(Error $error)
{
return FormattedError::create($error->getMessage(), $error->getLocations());
}
/**
* @param string|\Exception $message
* @param array|null $nodes
* @param Source $source
* @param null $positions
*/
public function __construct($message, $nodes = null, \Exception $previous = null, Source $source = null, $positions = null)
{
parent::__construct($message, 0, $previous);
if ($nodes instanceof \Traversable) {
$nodes = iterator_to_array($nodes);
}
$this->nodes = $nodes;
$this->source = $source;
$this->positions = $positions;
}
/**
* @return Source|null
*/
public function getSource()
{
if (null === $this->source) {
if (!empty($this->nodes[0]) && !empty($this->nodes[0]->loc)) {
$this->source = $this->nodes[0]->loc->source;
}
}
return $this->source;
}
/**
* @return array
*/
public function getPositions()
{
if (null === $this->positions) {
if (!empty($this->nodes)) {
$positions = array_map(function($node) { return isset($node->loc) ? $node->loc->start : null; }, $this->nodes);
$this->positions = array_filter($positions);
}
}
return $this->positions;
}
/**
* @return array<SourceLocation>
*/
public function getLocations()
{
if (null === $this->locations) {
$positions = $this->getPositions();
$source = $this->getSource();
if ($positions && $source) {
$this->locations = array_map(function ($pos) use ($source) {
return $source->getLocation($pos);
}, $positions);
} else {
$this->locations = [];
}
}
return $this->locations;
}
}

36
src/Error/ClientAware.php Normal file
View 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
View 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;
}

380
src/Error/Error.php Normal file
View file

@ -0,0 +1,380 @@
<?php
declare(strict_types=1);
namespace GraphQL\Error;
use Exception;
use GraphQL\Language\AST\Node;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
use GraphQL\Utils\Utils;
use JsonSerializable;
use Throwable;
use Traversable;
use function array_filter;
use function array_map;
use function array_values;
use function is_array;
use function iterator_to_array;
/**
* Describes an Error found during the parse, validate, or
* execute phases of performing a GraphQL operation. In addition to a message
* and stack trace, it also includes information about the locations in a
* GraphQL document and/or execution result that correspond to the Error.
*
* When the error was caused by an exception thrown in resolver, original exception
* is available via `getPrevious()`.
*
* Also read related docs on [error handling](error-handling.md)
*
* Class extends standard PHP `\Exception`, so all standard methods of base `\Exception` class
* are available in addition to those listed below.
*/
class Error extends Exception implements JsonSerializable, ClientAware
{
const CATEGORY_GRAPHQL = 'graphql';
const CATEGORY_INTERNAL = 'internal';
/**
* A message describing the Error for debugging purposes.
*
* @var string
*/
public $message;
/** @var SourceLocation[] */
private $locations;
/**
* An array describing the JSON-path into the execution response which
* corresponds to this error. Only included for errors during execution.
*
* @var mixed[]|null
*/
public $path;
/**
* An array of GraphQL AST Nodes corresponding to this error.
*
* @var Node[]|null
*/
public $nodes;
/**
* The source GraphQL document for the first location of this error.
*
* Note that if this Error represents more than one node, the source may not
* represent nodes after the first node.
*
* @var Source|null
*/
private $source;
/** @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 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) {
if ($error->path && $error->nodes) {
return $error;
}
$nodes = $nodes ?: $error->nodes;
$path = $path ?: $error->path;
}
$source = $positions = $originalError = null;
$extensions = [];
if ($error instanceof self) {
$message = $error->getMessage();
$originalError = $error;
$nodes = $error->nodes ?: $nodes;
$source = $error->source;
$positions = $error->positions;
$extensions = $error->extensions;
} elseif ($error instanceof Exception || $error instanceof Throwable) {
$message = $error->getMessage();
$originalError = $error;
} else {
$message = (string) $error;
}
return new static(
$message ?: 'An unknown error occurred.',
$nodes,
$source,
$positions,
$path,
$originalError,
$extensions
);
}
/**
* @return mixed[]
*/
public static function formatError(Error $error)
{
return $error->toSerializableArray();
}
/**
* @inheritdoc
*/
public function isClientSafe()
{
return $this->isClientSafe;
}
/**
* @inheritdoc
*/
public function getCategory()
{
return $this->category;
}
/**
* @return Source|null
*/
public function getSource()
{
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 int[]
*/
public function getPositions()
{
if ($this->positions === null && ! empty($this->nodes)) {
$positions = array_map(
static function ($node) {
return isset($node->loc) ? $node->loc->start : null;
},
$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 ($this->locations === null) {
$positions = $this->getPositions();
$source = $this->getSource();
$nodes = $this->nodes;
if ($positions && $source) {
$this->locations = array_map(
static function ($pos) use ($source) {
return $source->getLocation($pos);
},
$positions
);
} elseif ($nodes) {
$locations = array_filter(
array_map(
static function ($node) {
if ($node->loc && $node->loc->source) {
return $node->loc->source->getLocation($node->loc->start);
}
},
$nodes
)
);
$this->locations = array_values($locations);
} else {
$this->locations = [];
}
}
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
*
* @deprecated Use FormattedError::createFromException() instead
*
* @return mixed[]
*/
public function toSerializableArray()
{
$arr = [
'message' => $this->getMessage(),
];
$locations = Utils::map(
$this->getLocations(),
static function (SourceLocation $loc) {
return $loc->toSerializableArray();
}
);
if (! empty($locations)) {
$arr['locations'] = $locations;
}
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.
*/
public function jsonSerialize()
{
return $this->toSerializableArray();
}
/**
* @return string
*/
public function __toString()
{
return FormattedError::printError($this);
}
}

View file

@ -0,0 +1,440 @@
<?php
declare(strict_types=1);
namespace GraphQL\Error;
use Countable;
use ErrorException;
use Exception;
use GraphQL\Language\AST\Node;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\WrappingType;
use GraphQL\Utils\Utils;
use Throwable;
use function addcslashes;
use function array_filter;
use function array_intersect_key;
use function array_map;
use function array_merge;
use function array_shift;
use function count;
use function get_class;
use function gettype;
use function implode;
use function is_array;
use function is_bool;
use function is_object;
use function is_scalar;
use function is_string;
use function mb_strlen;
use function preg_split;
use function sprintf;
use function str_repeat;
use function strlen;
/**
* This class is used for [default error formatting](error-handling.md).
* It converts PHP exceptions to [spec-compliant errors](https://facebook.github.io/graphql/#sec-Errors)
* and provides tools for error debugging.
*/
class FormattedError
{
/** @var string */
private static $internalErrorMessage = 'Internal server error';
/**
* Set default error message for internal errors formatted using createFormattedError().
* This value can be overridden by passing 3rd argument to `createFormattedError()`.
*
* @param string $msg
*
* @api
*/
public static function setInternalErrorMessage($msg)
{
self::$internalErrorMessage = $msg;
}
/**
* Prints a GraphQLError to a string, representing useful location information
* about the error's position in the source.
*
* @return string
*/
public static function printError(Error $error)
{
$printedLocations = [];
if ($error->nodes) {
/** @var Node $node */
foreach ($error->nodes as $node) {
if (! $node->loc) {
continue;
}
if ($node->loc->source === null) {
continue;
}
$printedLocations[] = self::highlightSourceAtLocation(
$node->loc->source,
$node->loc->source->getLocation($node->loc->start)
);
}
} elseif ($error->getSource() && $error->getLocations()) {
$source = $error->getSource();
foreach ($error->getLocations() as $location) {
$printedLocations[] = self::highlightSourceAtLocation($source, $location);
}
}
return ! $printedLocations
? $error->getMessage()
: implode("\n\n", array_merge([$error->getMessage()], $printedLocations)) . "\n";
}
/**
* Render a helpful description of the location of the error in the GraphQL
* Source document.
*
* @return string
*/
private static function highlightSourceAtLocation(Source $source, SourceLocation $location)
{
$line = $location->line;
$lineOffset = $source->locationOffset->line - 1;
$columnOffset = self::getColumnOffset($source, $location);
$contextLine = $line + $lineOffset;
$contextColumn = $location->column + $columnOffset;
$prevLineNum = (string) ($contextLine - 1);
$lineNum = (string) $contextLine;
$nextLineNum = (string) ($contextLine + 1);
$padLen = strlen($nextLineNum);
$lines = preg_split('/\r\n|[\n\r]/', $source->body);
$lines[0] = self::whitespace($source->locationOffset->column - 1) . $lines[0];
$outputLines = [
sprintf('%s (%s:%s)', $source->name, $contextLine, $contextColumn),
$line >= 2 ? (self::lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2]) : null,
self::lpad($padLen, $lineNum) . ': ' . $lines[$line - 1],
self::whitespace(2 + $padLen + $contextColumn - 1) . '^',
$line < count($lines) ? self::lpad($padLen, $nextLineNum) . ': ' . $lines[$line] : null,
];
return implode("\n", array_filter($outputLines));
}
/**
* @return int
*/
private static function getColumnOffset(Source $source, SourceLocation $location)
{
return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
}
/**
* @param int $len
*
* @return string
*/
private static function whitespace($len)
{
return str_repeat(' ', $len);
}
/**
* @param int $len
*
* @return string
*/
private static function lpad($len, $str)
{
return self::whitespace($len - mb_strlen($str)) . $str;
}
/**
* Standard GraphQL error formatter. Converts any exception to array
* conforming to GraphQL spec.
*
* 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;
}
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;
}
/**
* 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)
{
if ($var instanceof Type) {
// FIXME: Replace with schema printer call
if ($var instanceof WrappingType) {
$var = $var->getWrappedType(true);
}
return 'GraphQLType: ' . $var->name;
}
if (is_object($var)) {
return 'instance of ' . get_class($var) . ($var instanceof Countable ? '(' . count($var) . ')' : '');
}
if (is_array($var)) {
return 'array(' . count($var) . ')';
}
if ($var === '') {
return '(empty string)';
}
if (is_string($var)) {
return "'" . addcslashes($var, "'") . "'";
}
if (is_bool($var)) {
return $var ? 'true' : 'false';
}
if (is_scalar($var)) {
return $var;
}
if ($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),
];
}
}

View file

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

25
src/Error/SyntaxError.php Normal file
View file

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

29
src/Error/UserError.php Normal file
View file

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace GraphQL\Error;
use RuntimeException;
/**
* Error caused by actions of GraphQL clients. Can be safely displayed to a client...
*/
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
View 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);
}
}
}

View 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)));
}
}

View file

@ -1,773 +0,0 @@
<?php
namespace GraphQL\Executor;
use GraphQL\Error;
use GraphQL\Language\AST\Document;
use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\FragmentDefinition;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Language\AST\SelectionSet;
use GraphQL\Schema;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Introspection;
use GraphQL\Utils;
/**
* @deprecated Use regular executor instead
*
* Terminology
*
* "Definitions" are the generic name for top-level statements in the document.
* Examples of this include:
* 1) Operations (such as a query)
* 2) Fragments
*
* "Operations" are a generic name for requests in the document.
* Examples of this include:
* 1) query,
* 2) mutation
*
* "Selections" are the statements that can appear legally and at
* single level of the query. These include:
* 1) field references e.g "a"
* 2) fragment "spreads" e.g. "...c"
* 3) inline fragment "spreads" e.g. "...on Type { a }"
*/
class MappingExecutor
{
private static $UNDEFINED;
private static $defaultResolveFn = [__CLASS__, 'defaultResolveFn'];
/**
* Custom default resolve function
*
* @param $fn
* @throws \Exception
*/
public function setDefaultResolveFn($fn)
{
Utils::invariant(is_callable($fn), 'Expecting callable, but got ' . Utils::getVariableType($fn));
self::$defaultResolveFn = $fn;
}
/**
* @param Schema $schema
* @param Document $ast
* @param $rootValue
* @param array|\ArrayAccess $variableValues
* @param null $operationName
* @return ExecutionResult
*/
public static function execute(Schema $schema, Document $ast, $rootValue = null, $variableValues = null, $operationName = null)
{
if (!self::$UNDEFINED) {
self::$UNDEFINED = new \stdClass();
}
if (null !== $variableValues) {
Utils::invariant(
is_array($variableValues) || $variableValues instanceof \ArrayAccess,
"Variable values are expected to be array or instance of ArrayAccess, got " . Utils::getVariableType($variableValues)
);
}
if (null !== $operationName) {
Utils::invariant(
is_string($operationName),
"Operation name is supposed to be string, got " . Utils::getVariableType($operationName)
);
}
$exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $variableValues, $operationName);
try {
$data = self::executeOperation($exeContext, $exeContext->operation, $rootValue);
} catch (Error $e) {
$exeContext->addError($e);
$data = null;
}
return new ExecutionResult($data, $exeContext->errors);
}
/**
* Constructs a ExecutionContext object from the arguments passed to
* execute, which we will pass throughout the other execution methods.
*/
private static function buildExecutionContext(Schema $schema, Document $documentAst, $rootValue, $rawVariableValues, $operationName = null)
{
$errors = [];
$operations = [];
$fragments = [];
foreach ($documentAst->definitions as $statement) {
switch ($statement->kind) {
case Node::OPERATION_DEFINITION:
$operations[$statement->name ? $statement->name->value : ''] = $statement;
break;
case Node::FRAGMENT_DEFINITION:
$fragments[$statement->name->value] = $statement;
break;
}
}
if (!$operationName && count($operations) !== 1) {
throw new Error(
'Must provide operation name if query contains multiple operations.'
);
}
$opName = $operationName ?: key($operations);
if (empty($operations[$opName])) {
throw new Error('Unknown operation named ' . $opName);
}
$operation = $operations[$opName];
$variableValues = Values::getVariableValues($schema, $operation->variableDefinitions ?: [], $rawVariableValues ?: []);
$exeContext = new ExecutionContext($schema, $fragments, $rootValue, $operation, $variableValues, $errors);
return $exeContext;
}
/**
* Implements the "Evaluating operations" section of the spec.
*/
private static function executeOperation(ExecutionContext $exeContext, OperationDefinition $operation, $rootValue)
{
$type = self::getOperationRootType($exeContext->schema, $operation);
$fields = self::collectFields($exeContext, $type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
if ($operation->operation === 'mutation') {
$result = self::executeFieldsSerially($exeContext, $type, [$rootValue], $fields);
} else {
$result = self::executeFields($exeContext, $type, [$rootValue], $fields);
}
return null === $result || $result === [] ? [] : $result[0];
}
/**
* Extracts the root type of the operation from the schema.
*
* @param Schema $schema
* @param OperationDefinition $operation
* @return ObjectType
* @throws Error
*/
private static function getOperationRootType(Schema $schema, OperationDefinition $operation)
{
switch ($operation->operation) {
case 'query':
return $schema->getQueryType();
case 'mutation':
$mutationType = $schema->getMutationType();
if (!$mutationType) {
throw new Error(
'Schema is not configured for mutations',
[$operation]
);
}
return $mutationType;
default:
throw new Error(
'Can only execute queries and mutations',
[$operation]
);
}
}
/**
* Implements the "Evaluating selection sets" section of the spec
* for "write" mode.
*
* @param ExecutionContext $exeContext
* @param ObjectType $parentType
* @param $sourceList
* @param $fields
* @return array
* @throws Error
* @throws \Exception
*/
private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $sourceList, $fields)
{
$results = [];
foreach ($fields as $responseName => $fieldASTs) {
self::resolveField($exeContext, $parentType, $sourceList, $fieldASTs, $responseName, $results);
}
return $results;
}
/**
* Implements the "Evaluating selection sets" section of the spec
* for "read" mode.
* @param ExecutionContext $exeContext
* @param ObjectType $parentType
* @param $sourceList
* @param $fields
* @return array
*/
private static function executeFields(ExecutionContext $exeContext, ObjectType $parentType, $sourceList, $fields)
{
// Native PHP doesn't support promises.
// Custom executor should be built for platforms like ReactPHP
return self::executeFieldsSerially($exeContext, $parentType, $sourceList, $fields);
}
/**
* Given a selectionSet, adds all of the fields in that selection to
* the passed in map of fields, and returns it at the end.
*
* @return \ArrayObject
*/
private static function collectFields(
ExecutionContext $exeContext,
ObjectType $type,
SelectionSet $selectionSet,
$fields,
$visitedFragmentNames
)
{
for ($i = 0; $i < count($selectionSet->selections); $i++) {
$selection = $selectionSet->selections[$i];
switch ($selection->kind) {
case Node::FIELD:
if (!self::shouldIncludeNode($exeContext, $selection->directives)) {
continue;
}
$name = self::getFieldEntryKey($selection);
if (!isset($fields[$name])) {
$fields[$name] = new \ArrayObject();
}
$fields[$name][] = $selection;
break;
case Node::INLINE_FRAGMENT:
if (!self::shouldIncludeNode($exeContext, $selection->directives) ||
!self::doesFragmentConditionMatch($exeContext, $selection, $type)
) {
continue;
}
self::collectFields(
$exeContext,
$type,
$selection->selectionSet,
$fields,
$visitedFragmentNames
);
break;
case Node::FRAGMENT_SPREAD:
$fragName = $selection->name->value;
if (!empty($visitedFragmentNames[$fragName]) || !self::shouldIncludeNode($exeContext, $selection->directives)) {
continue;
}
$visitedFragmentNames[$fragName] = true;
/** @var FragmentDefinition|null $fragment */
$fragment = isset($exeContext->fragments[$fragName]) ? $exeContext->fragments[$fragName] : null;
if (!$fragment ||
!self::shouldIncludeNode($exeContext, $fragment->directives) ||
!self::doesFragmentConditionMatch($exeContext, $fragment, $type)
) {
continue;
}
self::collectFields(
$exeContext,
$type,
$fragment->selectionSet,
$fields,
$visitedFragmentNames
);
break;
}
}
return $fields;
}
/**
* Determines if a field should be included based on the @include and @skip
* directives, where @skip has higher precedence than @include.
*/
private static function shouldIncludeNode(ExecutionContext $exeContext, $directives)
{
$skipDirective = Directive::skipDirective();
$includeDirective = Directive::includeDirective();
/** @var \GraphQL\Language\AST\Directive $skipAST */
$skipAST = $directives
? Utils::find($directives, function(\GraphQL\Language\AST\Directive $directive) use ($skipDirective) {
return $directive->name->value === $skipDirective->name;
})
: null;
if ($skipAST) {
$argValues = Values::getArgumentValues($skipDirective->args, $skipAST->arguments, $exeContext->variableValues);
return empty($argValues['if']);
}
/** @var \GraphQL\Language\AST\Directive $includeAST */
$includeAST = $directives
? Utils::find($directives, function(\GraphQL\Language\AST\Directive $directive) use ($includeDirective) {
return $directive->name->value === $includeDirective->name;
})
: null;
if ($includeAST) {
$argValues = Values::getArgumentValues($includeDirective->args, $includeAST->arguments, $exeContext->variableValues);
return !empty($argValues['if']);
}
return true;
}
/**
* Determines if a fragment is applicable to the given type.
*/
private static function doesFragmentConditionMatch(ExecutionContext $exeContext,/* FragmentDefinition | InlineFragment*/ $fragment, ObjectType $type)
{
$conditionalType = Utils\TypeInfo::typeFromAST($exeContext->schema, $fragment->typeCondition);
if ($conditionalType === $type) {
return true;
}
if ($conditionalType instanceof InterfaceType ||
$conditionalType instanceof UnionType
) {
return $conditionalType->isPossibleType($type);
}
return false;
}
/**
* Implements the logic to compute the key of a given fields entry
*/
private static function getFieldEntryKey(Field $node)
{
return $node->alias ? $node->alias->value : $node->name->value;
}
/**
* Given list of parent type values returns corresponding list of field values
*
* In particular, this
* figures out the value that the field returns by calling its `resolve` or `map` function,
* then calls `completeValue` on each value to serialize scalars, or execute the sub-selection-set
* for objects.
*
* @param ExecutionContext $exeContext
* @param ObjectType $parentType
* @param $sourceValueList
* @param $fieldASTs
* @return array
* @throws Error
*/
private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $sourceValueList, $fieldASTs, $responseName, &$resolveResult)
{
$fieldAST = $fieldASTs[0];
$fieldName = $fieldAST->name->value;
$fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName);
if (!$fieldDef) {
return ;
}
$returnType = $fieldDef->getType();
// Build hash of arguments from the field.arguments AST, using the
// variables scope to fulfill any variable references.
// TODO: find a way to memoize, in case this field is within a List type.
$args = Values::getArgumentValues(
$fieldDef->args,
$fieldAST->arguments,
$exeContext->variableValues
);
// The resolve function's optional third argument is a collection of
// information about the current execution state.
$info = new ResolveInfo([
'fieldName' => $fieldName,
'fieldASTs' => $fieldASTs,
'returnType' => $returnType,
'parentType' => $parentType,
'schema' => $exeContext->schema,
'fragments' => $exeContext->fragments,
'rootValue' => $exeContext->rootValue,
'operation' => $exeContext->operation,
'variableValues' => $exeContext->variableValues,
]);
$mapFn = $fieldDef->mapFn;
// If an error occurs while calling the field `map` or `resolve` function, ensure that
// it is wrapped as a GraphQLError with locations. Log this error and return
// null if allowed, otherwise throw the error so the parent field can handle
// it.
if ($mapFn) {
try {
$mapped = call_user_func($mapFn, $sourceValueList, $args, $info);
$validType = is_array($mapped) || ($mapped instanceof \Traversable && $mapped instanceof \Countable);
$mappedCount = count($mapped);
$sourceCount = count($sourceValueList);
Utils::invariant(
$validType && count($mapped) === count($sourceValueList),
"Function `map` of $parentType.$fieldName is expected to return array or " .
"countable traversable with exact same number of items as list being mapped. ".
"Got '%s' with count '$mappedCount' against '$sourceCount' expected.",
Utils::getVariableType($mapped)
);
} catch (\Exception $error) {
$reportedError = Error::createLocatedError($error, $fieldASTs);
if ($returnType instanceof NonNull) {
throw $reportedError;
}
$exeContext->addError($reportedError);
return null;
}
foreach ($mapped as $index => $value) {
$resolveResult[$index][$responseName] = self::completeValueCatchingError(
$exeContext,
$returnType,
$fieldASTs,
$info,
$value
);
}
} else {
if (isset($fieldDef->resolveFn)) {
$resolveFn = $fieldDef->resolveFn;
} else if (isset($parentType->resolveFieldFn)) {
$resolveFn = $parentType->resolveFieldFn;
} else {
$resolveFn = self::$defaultResolveFn;
}
foreach ($sourceValueList as $index => $value) {
try {
$resolved = call_user_func($resolveFn, $value, $args, $info);
} catch (\Exception $error) {
$reportedError = Error::createLocatedError($error, $fieldASTs);
if ($returnType instanceof NonNull) {
throw $reportedError;
}
$exeContext->addError($reportedError);
$resolved = null;
}
$resolveResult[$index][$responseName] = self::completeValueCatchingError(
$exeContext,
$returnType,
$fieldASTs,
$info,
$resolved
);
}
}
}
public static function completeValueCatchingError(
ExecutionContext $exeContext,
Type $returnType,
$fieldASTs,
ResolveInfo $info,
$result
)
{
// If the field type is non-nullable, then it is resolved without any
// protection from errors.
if ($returnType instanceof NonNull) {
return self::completeValue($exeContext, $returnType, $fieldASTs, $info, $result);
}
// Otherwise, error protection is applied, logging the error and resolving
// a null value for this field if one is encountered.
try {
return self::completeValue($exeContext, $returnType, $fieldASTs, $info, $result);
} catch (Error $err) {
$exeContext->addError($err);
return null;
}
}
/**
* Implements the instructions for completeValue as defined in the
* "Field entries" section of the spec.
*
* If the field type is Non-Null, then this recursively completes the value
* for the inner type. It throws a field error if that completion returns null,
* as per the "Nullability" section of the spec.
*
* If the field type is a List, then this recursively completes the value
* for the inner type on each item in the list.
*
* If the field type is a Scalar or Enum, ensures the completed value is a legal
* value of the type by calling the `serialize` method of GraphQL type
* definition.
*
* Otherwise, the field type expects a sub-selection set, and will complete the
* value by evaluating all sub-selections.
*/
private static function completeValue(ExecutionContext $exeContext, Type $returnType,/* Array<Field> */ $fieldASTs, ResolveInfo $info, &$result)
{
// If field type is NonNull, complete for inner type, and throw field error
// if result is null.
if ($returnType instanceof NonNull) {
$completed = self::completeValue(
$exeContext,
$returnType->getWrappedType(),
$fieldASTs,
$info,
$result
);
if ($completed === null) {
throw new Error(
'Cannot return null for non-nullable type.',
$fieldASTs instanceof \ArrayObject ? $fieldASTs->getArrayCopy() : $fieldASTs
);
}
return $completed;
}
// If result is null-like, return null.
if (null === $result) {
return null;
}
// If field type is Scalar or Enum, serialize to a valid value, returning
// null if serialization is not possible.
if ($returnType instanceof ScalarType ||
$returnType instanceof EnumType) {
return $returnType->serialize($result);
}
// If field type is List, and return type is Composite - complete by executing these fields with list value as parameter
if ($returnType instanceof ListOfType) {
$itemType = $returnType->getWrappedType();
Utils::invariant(
is_array($result) || $result instanceof \Traversable,
'User Error: expected iterable, but did not find one.'
);
// For Object[]:
// Allow all object fields to process list value in it's `map` callback:
if ($itemType instanceof ObjectType) {
// Filter out nulls (as `map` doesn't expect it):
$list = [];
foreach ($result as $index => $item) {
if (null !== $item) {
$list[] = $item;
}
}
$subFieldASTs = self::collectSubFields($exeContext, $itemType, $fieldASTs);
$mapped = self::executeFields($exeContext, $itemType, $list, $subFieldASTs);
$i = 0;
$completed = [];
foreach ($result as $index => $item) {
if (null === $item) {
// Complete nulls separately
$completed[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
} else {
// Assuming same order of mapped values
$completed[] = $mapped[$i++];
}
}
return $completed;
} else if ($itemType instanceof AbstractType) {
// Values sharded by ObjectType
$listPerObjectType = [];
// Helper structures to restore ordering after resolve calls
$resultTypeMap = [];
$typeNameMap = [];
$cursors = [];
$copied = [];
foreach ($result as $index => $item) {
$copied[$index] = $item;
if (null !== $item) {
$objectType = $itemType->getObjectType($item, $info);
if ($objectType && !$itemType->isPossibleType($objectType)) {
$exeContext->addError(new Error(
"Runtime Object type \"$objectType\" is not a possible type for \"$itemType\"."
));
$copied[$index] = null;
} else {
$listPerObjectType[$objectType->name][] = $item;
$resultTypeMap[$index] = $objectType->name;
$typeNameMap[$objectType->name] = $objectType;
}
}
}
$mapped = [];
foreach ($listPerObjectType as $typeName => $list) {
$objectType = $typeNameMap[$typeName];
$subFieldASTs = self::collectSubFields($exeContext, $objectType, $fieldASTs);
$mapped[$typeName] = self::executeFields($exeContext, $objectType, $list, $subFieldASTs);
$cursors[$typeName] = 0;
}
// Restore order:
$completed = [];
foreach ($copied as $index => $item) {
if (null === $item) {
// Complete nulls separately
$completed[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
} else {
$typeName = $resultTypeMap[$index];
$completed[] = $mapped[$typeName][$cursors[$typeName]++];
}
}
return $completed;
} else {
// For simple lists:
$tmp = [];
foreach ($result as $item) {
$tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
}
return $tmp;
}
}
if ($returnType instanceof ObjectType) {
$objectType = $returnType;
} else if ($returnType instanceof AbstractType) {
$objectType = $returnType->getObjectType($result, $info);
if ($objectType && !$returnType->isPossibleType($objectType)) {
throw new Error(
"Runtime Object type \"$objectType\" is not a possible type for \"$returnType\"."
);
}
} else {
$objectType = null;
}
if (!$objectType) {
return null;
}
// If there is an isTypeOf predicate function, call it with the
// current result. If isTypeOf returns false, then raise an error rather
// than continuing execution.
if (false === $objectType->isTypeOf($result, $info)) {
throw new Error(
"Expected value of type $objectType but got: $result.",
$fieldASTs
);
}
// Collect sub-fields to execute to complete this value.
$subFieldASTs = self::collectSubFields($exeContext, $objectType, $fieldASTs);
$executed = self::executeFields($exeContext, $objectType, [$result], $subFieldASTs);
return isset($executed[0]) ? $executed[0] : null;
}
/**
* @param ExecutionContext $exeContext
* @param ObjectType $objectType
* @param $fieldASTs
* @return \ArrayObject
*/
private static function collectSubFields(ExecutionContext $exeContext, ObjectType $objectType, $fieldASTs)
{
$subFieldASTs = new \ArrayObject();
$visitedFragmentNames = new \ArrayObject();
for ($i = 0; $i < count($fieldASTs); $i++) {
$selectionSet = $fieldASTs[$i]->selectionSet;
if ($selectionSet) {
$subFieldASTs = self::collectFields(
$exeContext,
$objectType,
$selectionSet,
$subFieldASTs,
$visitedFragmentNames
);
}
}
return $subFieldASTs;
}
/**
* If a resolve function is not given, then a default resolve behavior is used
* which takes the property of the source object of the same name as the field
* and returns it as the result, or if it's a function, returns the result
* of calling that function.
*/
public static function defaultResolveFn($source, $args, 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};
}
}
return $property instanceof \Closure ? $property($source) : $property;
}
/**
* This method looks up the field on the given type defintion.
* It has special casing for the two introspection fields, __schema
* and __typename. __typename is special because it can always be
* queried as a field, even in situations where no other fields
* are allowed, like on a Union. __schema could get automatically
* added to the query type, but that would require mutating type
* definitions, which would cause issues.
*
* @return FieldDefinition
*/
private static function getFieldDef(Schema $schema, ObjectType $parentType, $fieldName)
{
$schemaMetaFieldDef = Introspection::schemaMetaFieldDef();
$typeMetaFieldDef = Introspection::typeMetaFieldDef();
$typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef();
if ($fieldName === $schemaMetaFieldDef->name && $schema->getQueryType() === $parentType) {
return $schemaMetaFieldDef;
} else if ($fieldName === $typeMetaFieldDef->name && $schema->getQueryType() === $parentType) {
return $typeMetaFieldDef;
} else if ($fieldName === $typeNameMetaFieldDef->name) {
return $typeNameMetaFieldDef;
}
$tmp = $parentType->getFields();
return isset($tmp[$fieldName]) ? $tmp[$fieldName] : null;
}
}

View file

@ -1,66 +1,78 @@
<?php
declare(strict_types=1);
namespace GraphQL\Executor;
use GraphQL\Error;
use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Schema;
use GraphQL\Error\Error;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Language\AST\FragmentDefinitionNode;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Type\Schema;
/**
* Data that must be available at all points during query execution.
*
* Namely, schema of the type system that is currently executing,
* and the fragments defined in the query document
* and the fragments defined in the query document.
*
* @internal
*/
class ExecutionContext
{
/**
* @var Schema
*/
/** @var Schema */
public $schema;
/**
* @var array<string, FragmentDefinition>
*/
/** @var FragmentDefinitionNode[] */
public $fragments;
/**
* @var
*/
/** @var mixed */
public $rootValue;
/**
* @var OperationDefinition
*/
/** @var mixed */
public $contextValue;
/** @var OperationDefinitionNode */
public $operation;
/**
* @var array
*/
/** @var mixed[] */
public $variableValues;
/**
* @var array
*/
/** @var callable */
public $fieldResolver;
/** @var Error[] */
public $errors;
/**
* @var array
*/
public $memoized = [];
/** @var PromiseAdapter */
public $promiseAdapter;
public function __construct($schema, $fragments, $root, $operation, $variables, $errors)
{
$this->schema = $schema;
$this->fragments = $fragments;
$this->rootValue = $root;
$this->operation = $operation;
$this->variableValues = $variables;
$this->errors = $errors ?: [];
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;
}
}

View file

@ -1,39 +1,160 @@
<?php
declare(strict_types=1);
namespace GraphQL\Executor;
use GraphQL\Error;
use GraphQL\Error\Error;
use GraphQL\Error\FormattedError;
use JsonSerializable;
use function array_map;
class ExecutionResult
/**
* 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;
/**
* @param array $data
* @param array $errors
* User-defined serializable array of extensions included in serialized result.
* Conforms to
*
* @api
* @var mixed[]
*/
public function __construct(array $data = null, array $errors = [])
public $extensions;
/** @var callable */
private $errorFormatter;
/** @var callable */
private $errorsHandler;
/**
* @param mixed[] $data
* @param Error[] $errors
* @param mixed[] $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;
}
/**
* @return array
* 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 toArray()
public function setErrorFormatter(callable $errorFormatter)
{
$result = ['data' => $this->data];
$this->errorFormatter = $errorFormatter;
if (!empty($this->errors)) {
$result['errors'] = array_map(['GraphQL\Error', 'formatError'], $this->errors);
return $this;
}
/**
* Define custom logic for error handling (filtering, logging, etc).
*
* Expected handler signature is: function (array $errors, callable $formatter): array
*
* Default handler is:
* function (array $errors, callable $formatter) {
* return array_map($formatter, $errors);
* }
*
* @return self
*
* @api
*/
public function setErrorsHandler(callable $handler)
{
$this->errorsHandler = $handler;
return $this;
}
/**
* @return mixed[]
*/
public function jsonSerialize()
{
return $this->toArray();
}
/**
* Converts GraphQL query result to spec-compliant serializable array using provided
* errors handler and formatter.
*
* 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;

View file

@ -1,678 +1,189 @@
<?php
declare(strict_types=1);
namespace GraphQL\Executor;
use GraphQL\Error;
use GraphQL\Language\AST\Document;
use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\FragmentDefinition;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Language\AST\SelectionSet;
use GraphQL\Schema;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use ArrayAccess;
use Closure;
use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Introspection;
use GraphQL\Utils;
use GraphQL\Type\Schema;
use function is_array;
use function is_object;
/**
* Terminology
*
* "Definitions" are the generic name for top-level statements in the document.
* Examples of this include:
* 1) Operations (such as a query)
* 2) Fragments
*
* "Operations" are a generic name for requests in the document.
* Examples of this include:
* 1) query,
* 2) mutation
*
* "Selections" are the statements that can appear legally and at
* single level of the query. These include:
* 1) field references e.g "a"
* 2) fragment "spreads" e.g. "...c"
* 3) inline fragment "spreads" e.g. "...on Type { a }"
* Implements the "Evaluating requests" section of the GraphQL specification.
*/
class Executor
{
private static $UNDEFINED;
/** @var callable|string[] */
private static $defaultFieldResolver = [self::class, 'defaultFieldResolver'];
private static $defaultResolveFn = [__CLASS__, 'defaultResolveFn'];
/** @var PromiseAdapter */
private static $defaultPromiseAdapter;
/** @var callable */
private static $implementationFactory = [ReferenceExecutor::class, 'create'];
public static function getDefaultFieldResolver() : callable
{
return self::$defaultFieldResolver;
}
/**
* Custom default resolve function
* Custom default resolve function.
*/
public static function setDefaultFieldResolver(callable $fieldResolver)
{
self::$defaultFieldResolver = $fieldResolver;
}
public static function getPromiseAdapter() : PromiseAdapter
{
return self::$defaultPromiseAdapter ?: (self::$defaultPromiseAdapter = new SyncPromiseAdapter());
}
public static function setPromiseAdapter(?PromiseAdapter $defaultPromiseAdapter = null)
{
self::$defaultPromiseAdapter = $defaultPromiseAdapter;
}
public static function getImplementationFactory() : callable
{
return self::$implementationFactory;
}
/**
* Custom executor implementation factory.
*
* @param $fn
* @throws \Exception
* Will be called with as
*/
public static function setDefaultResolveFn($fn)
public static function setImplementationFactory(callable $implementationFactory)
{
Utils::invariant(is_callable($fn), 'Expecting callable, but got ' . Utils::getVariableType($fn));
self::$defaultResolveFn = $fn;
self::$implementationFactory = $implementationFactory;
}
/**
* @param Schema $schema
* @param Document $ast
* @param $rootValue
* @param array|\ArrayAccess $variableValues
* @param null $operationName
* @return ExecutionResult
*/
public static function execute(Schema $schema, Document $ast, $rootValue = null, $variableValues = null, $operationName = null)
{
if (!self::$UNDEFINED) {
self::$UNDEFINED = new \stdClass();
}
if (null !== $variableValues) {
Utils::invariant(
is_array($variableValues) || $variableValues instanceof \ArrayAccess,
"Variable values are expected to be array or instance of ArrayAccess, got " . Utils::getVariableType($variableValues)
);
}
if (null !== $operationName) {
Utils::invariant(
is_string($operationName),
"Operation name is supposed to be string, got " . Utils::getVariableType($operationName)
);
}
$exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $variableValues, $operationName);
try {
$data = self::executeOperation($exeContext, $exeContext->operation, $rootValue);
} catch (Error $e) {
$exeContext->addError($e);
$data = null;
}
return new ExecutionResult($data, $exeContext->errors);
}
/**
* Constructs a ExecutionContext object from the arguments passed to
* execute, which we will pass throughout the other execution methods.
*/
private static function buildExecutionContext(Schema $schema, Document $documentAst, $rootValue, $rawVariableValues, $operationName = null)
{
$errors = [];
$operations = [];
$fragments = [];
foreach ($documentAst->definitions as $statement) {
switch ($statement->kind) {
case Node::OPERATION_DEFINITION:
$operations[$statement->name ? $statement->name->value : ''] = $statement;
break;
case Node::FRAGMENT_DEFINITION:
$fragments[$statement->name->value] = $statement;
break;
}
}
if (!$operationName && count($operations) !== 1) {
throw new Error(
'Must provide operation name if query contains multiple operations.'
);
}
$opName = $operationName ?: key($operations);
if (empty($operations[$opName])) {
throw new Error('Unknown operation named ' . $opName);
}
$operation = $operations[$opName];
$variableValues = Values::getVariableValues($schema, $operation->variableDefinitions ?: [], $rawVariableValues ?: []);
$exeContext = new ExecutionContext($schema, $fragments, $rootValue, $operation, $variableValues, $errors);
return $exeContext;
}
/**
* Implements the "Evaluating operations" section of the spec.
*/
private static function executeOperation(ExecutionContext $exeContext, OperationDefinition $operation, $rootValue)
{
$type = self::getOperationRootType($exeContext->schema, $operation);
$fields = self::collectFields($exeContext, $type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
if ($operation->operation === 'mutation') {
return self::executeFieldsSerially($exeContext, $type, $rootValue, $fields);
}
return self::executeFields($exeContext, $type, $rootValue, $fields);
}
/**
* Extracts the root type of the operation from the schema.
* Executes DocumentNode against given $schema.
*
* @param Schema $schema
* @param OperationDefinition $operation
* @return ObjectType
* @throws Error
*/
private static function getOperationRootType(Schema $schema, OperationDefinition $operation)
{
switch ($operation->operation) {
case 'query':
return $schema->getQueryType();
case 'mutation':
$mutationType = $schema->getMutationType();
if (!$mutationType) {
throw new Error(
'Schema is not configured for mutations',
[$operation]
);
}
return $mutationType;
default:
throw new Error(
'Can only execute queries and mutations',
[$operation]
);
}
}
/**
* Implements the "Evaluating selection sets" section of the spec
* for "write" mode.
*/
private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $sourceValue, $fields)
{
$results = [];
foreach ($fields as $responseName => $fieldASTs) {
$result = self::resolveField($exeContext, $parentType, $sourceValue, $fieldASTs);
if ($result !== self::$UNDEFINED) {
// Undefined means that field is not defined in schema
$results[$responseName] = $result;
}
}
return $results;
}
/**
* Implements the "Evaluating selection sets" section of the spec
* for "read" mode.
*/
private static function executeFields(ExecutionContext $exeContext, ObjectType $parentType, $source, $fields)
{
// Native PHP doesn't support promises.
// Custom executor should be built for platforms like ReactPHP
return self::executeFieldsSerially($exeContext, $parentType, $source, $fields);
}
/**
* Given a selectionSet, adds all of the fields in that selection to
* the passed in map of fields, and returns it at the end.
* Always returns ExecutionResult and never throws. All errors which occur during operation
* execution are collected in `$result->errors`.
*
* @return \ArrayObject
* @param mixed|null $rootValue
* @param mixed|null $contextValue
* @param mixed[]|ArrayAccess|null $variableValues
* @param string|null $operationName
*
* @return ExecutionResult|Promise
*
* @api
*/
private static function collectFields(
ExecutionContext $exeContext,
ObjectType $type,
SelectionSet $selectionSet,
$fields,
$visitedFragmentNames
)
{
for ($i = 0; $i < count($selectionSet->selections); $i++) {
$selection = $selectionSet->selections[$i];
switch ($selection->kind) {
case Node::FIELD:
if (!self::shouldIncludeNode($exeContext, $selection->directives)) {
continue;
}
$name = self::getFieldEntryKey($selection);
if (!isset($fields[$name])) {
$fields[$name] = new \ArrayObject();
}
$fields[$name][] = $selection;
break;
case Node::INLINE_FRAGMENT:
if (!self::shouldIncludeNode($exeContext, $selection->directives) ||
!self::doesFragmentConditionMatch($exeContext, $selection, $type)
) {
continue;
}
self::collectFields(
$exeContext,
$type,
$selection->selectionSet,
$fields,
$visitedFragmentNames
);
break;
case Node::FRAGMENT_SPREAD:
$fragName = $selection->name->value;
if (!empty($visitedFragmentNames[$fragName]) || !self::shouldIncludeNode($exeContext, $selection->directives)) {
continue;
}
$visitedFragmentNames[$fragName] = true;
public static function execute(
Schema $schema,
DocumentNode $documentNode,
$rootValue = null,
$contextValue = null,
$variableValues = null,
$operationName = null,
?callable $fieldResolver = null
) {
// TODO: deprecate (just always use SyncAdapter here) and have `promiseToExecute()` for other cases
/** @var FragmentDefinition|null $fragment */
$fragment = isset($exeContext->fragments[$fragName]) ? $exeContext->fragments[$fragName] : null;
if (!$fragment ||
!self::shouldIncludeNode($exeContext, $fragment->directives) ||
!self::doesFragmentConditionMatch($exeContext, $fragment, $type)
) {
continue;
}
self::collectFields(
$exeContext,
$type,
$fragment->selectionSet,
$fields,
$visitedFragmentNames
);
break;
}
}
return $fields;
}
$promiseAdapter = static::getPromiseAdapter();
/**
* Determines if a field should be included based on the @include and @skip
* directives, where @skip has higher precedence than @include.
*/
private static function shouldIncludeNode(ExecutionContext $exeContext, $directives)
{
$skipDirective = Directive::skipDirective();
$includeDirective = Directive::includeDirective();
$result = static::promiseToExecute(
$promiseAdapter,
$schema,
$documentNode,
$rootValue,
$contextValue,
$variableValues,
$operationName,
$fieldResolver
);
/** @var \GraphQL\Language\AST\Directive $skipAST */
$skipAST = $directives
? Utils::find($directives, function(\GraphQL\Language\AST\Directive $directive) use ($skipDirective) {
return $directive->name->value === $skipDirective->name;
})
: null;
if ($skipAST) {
$argValues = Values::getArgumentValues($skipDirective->args, $skipAST->arguments, $exeContext->variableValues);
return empty($argValues['if']);
}
/** @var \GraphQL\Language\AST\Directive $includeAST */
$includeAST = $directives
? Utils::find($directives, function(\GraphQL\Language\AST\Directive $directive) use ($includeDirective) {
return $directive->name->value === $includeDirective->name;
})
: null;
if ($includeAST) {
$argValues = Values::getArgumentValues($includeDirective->args, $includeAST->arguments, $exeContext->variableValues);
return !empty($argValues['if']);
}
return true;
}
/**
* Determines if a fragment is applicable to the given type.
*/
private static function doesFragmentConditionMatch(ExecutionContext $exeContext,/* FragmentDefinition | InlineFragment*/ $fragment, ObjectType $type)
{
$typeConditionAST = $fragment->typeCondition;
if (!$typeConditionAST) {
return true;
}
$conditionalType = Utils\TypeInfo::typeFromAST($exeContext->schema, $typeConditionAST);
if ($conditionalType === $type) {
return true;
}
if ($conditionalType instanceof InterfaceType ||
$conditionalType instanceof UnionType
) {
return $conditionalType->isPossibleType($type);
}
return false;
}
/**
* Implements the logic to compute the key of a given fields entry
*/
private static function getFieldEntryKey(Field $node)
{
return $node->alias ? $node->alias->value : $node->name->value;
}
/**
* Resolves the field on the given source object. In particular, this
* figures out the value that the field returns by calling its resolve function,
* then calls completeValue to complete promises, serialize scalars, or execute
* the sub-selection-set for objects.
*/
private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $source, $fieldASTs)
{
$fieldAST = $fieldASTs[0];
$uid = self::getFieldUid($fieldAST, $parentType);
// Get memoized variables if they exist
if (isset($exeContext->memoized['resolveField'][$uid])) {
$memoized = $exeContext->memoized['resolveField'][$uid];
$fieldDef = $memoized['fieldDef'];
$returnType = $fieldDef->getType();
$args = $memoized['args'];
$info = $memoized['info'];
}
else {
$fieldName = $fieldAST->name->value;
$fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName);
if (!$fieldDef) {
return self::$UNDEFINED;
}
$returnType = $fieldDef->getType();
// Build hash of arguments from the field.arguments AST, using the
// variables scope to fulfill any variable references.
$args = Values::getArgumentValues(
$fieldDef->args,
$fieldAST->arguments,
$exeContext->variableValues
);
// The resolve function's optional third argument is a collection of
// information about the current execution state.
$info = new ResolveInfo([
'fieldName' => $fieldName,
'fieldASTs' => $fieldASTs,
'returnType' => $returnType,
'parentType' => $parentType,
'schema' => $exeContext->schema,
'fragments' => $exeContext->fragments,
'rootValue' => $exeContext->rootValue,
'operation' => $exeContext->operation,
'variableValues' => $exeContext->variableValues,
]);
// Memoizing results for same query field
// (useful for lists when several values are resolved against the same field)
$exeContext->memoized['resolveField'][$uid] = $memoized = [
'fieldDef' => $fieldDef,
'args' => $args,
'info' => $info,
'results' => new \SplObjectStorage
];
}
// When source value is object it is possible to memoize certain subset of results
$isObject = is_object($source);
if ($isObject && isset($memoized['results'][$source])) {
$result = $memoized['results'][$source];
} else {
if (isset($fieldDef->resolveFn)) {
$resolveFn = $fieldDef->resolveFn;
} else if (isset($parentType->resolveFieldFn)) {
$resolveFn = $parentType->resolveFieldFn;
} else {
$resolveFn = self::$defaultResolveFn;
}
// Get the resolve function, regardless of if its result is normal
// or abrupt (error).
$result = self::resolveOrError($resolveFn, $source, $args, $info);
$result = self::completeValueCatchingError(
$exeContext,
$returnType,
$fieldASTs,
$info,
$result
);
if ($isObject) {
$memoized['results'][$source] = $result;
}
if ($promiseAdapter instanceof SyncPromiseAdapter) {
$result = $promiseAdapter->wait($result);
}
return $result;
}
// Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField`
// function. Returns the result of resolveFn or the abrupt-return Error object.
private static function resolveOrError($resolveFn, $source, $args, $info)
{
try {
return call_user_func($resolveFn, $source, $args, $info);
} catch (\Exception $error) {
return $error;
}
}
public static function completeValueCatchingError(
ExecutionContext $exeContext,
Type $returnType,
$fieldASTs,
ResolveInfo $info,
$result
)
{
// If the field type is non-nullable, then it is resolved without any
// protection from errors.
if ($returnType instanceof NonNull) {
return self::completeValue($exeContext, $returnType, $fieldASTs, $info, $result);
}
// Otherwise, error protection is applied, logging the error and resolving
// a null value for this field if one is encountered.
try {
return self::completeValue($exeContext, $returnType, $fieldASTs, $info, $result);
} catch (Error $err) {
// If `completeValue` returned abruptly (threw an error), log the error
// and return null.
$exeContext->addError($err);
return null;
}
}
/**
* Implements the instructions for completeValue as defined in the
* "Field entries" section of the spec.
* Same as execute(), but requires promise adapter and returns a promise which is always
* fulfilled with an instance of ExecutionResult and never rejected.
*
* If the field type is Non-Null, then this recursively completes the value
* for the inner type. It throws a field error if that completion returns null,
* as per the "Nullability" section of the spec.
* Useful for async PHP platforms.
*
* If the field type is a List, then this recursively completes the value
* for the inner type on each item in the list.
* @param mixed|null $rootValue
* @param mixed|null $contextValue
* @param mixed[]|null $variableValues
* @param string|null $operationName
*
* If the field type is a Scalar or Enum, ensures the completed value is a legal
* value of the type by calling the `serialize` method of GraphQL type
* definition.
* @return Promise
*
* Otherwise, the field type expects a sub-selection set, and will complete the
* value by evaluating all sub-selections.
* @api
*/
private static function completeValue(ExecutionContext $exeContext, Type $returnType,/* Array<Field> */ $fieldASTs, ResolveInfo $info, &$result)
{
if ($result instanceof \Exception) {
throw Error::createLocatedError($result, $fieldASTs);
}
public static function promiseToExecute(
PromiseAdapter $promiseAdapter,
Schema $schema,
DocumentNode $documentNode,
$rootValue = null,
$contextValue = null,
$variableValues = null,
$operationName = null,
?callable $fieldResolver = null
) {
$factory = self::$implementationFactory;
// If field type is NonNull, complete for inner type, and throw field error
// if result is null.
if ($returnType instanceof NonNull) {
$completed = self::completeValue(
$exeContext,
$returnType->getWrappedType(),
$fieldASTs,
$info,
$result
);
if ($completed === null) {
throw new Error(
'Cannot return null for non-nullable type.',
$fieldASTs instanceof \ArrayObject ? $fieldASTs->getArrayCopy() : $fieldASTs
);
}
return $completed;
}
/** @var ExecutorImplementation $executor */
$executor = $factory(
$promiseAdapter,
$schema,
$documentNode,
$rootValue,
$contextValue,
$variableValues,
$operationName,
$fieldResolver ?: self::$defaultFieldResolver
);
// If result is null-like, return null.
if (null === $result) {
return null;
}
// If field type is List, complete each item in the list with the inner type
if ($returnType instanceof ListOfType) {
$itemType = $returnType->getWrappedType();
Utils::invariant(
is_array($result) || $result instanceof \Traversable,
'User Error: expected iterable, but did not find one.'
);
$tmp = [];
foreach ($result as $item) {
$tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
}
return $tmp;
}
// If field type is Scalar or Enum, serialize to a valid value, returning
// null if serialization is not possible.
if ($returnType instanceof ScalarType ||
$returnType instanceof EnumType) {
Utils::invariant(method_exists($returnType, 'serialize'), 'Missing serialize method on type');
return $returnType->serialize($result);
}
// Field type must be Object, Interface or Union and expect sub-selections.
if ($returnType instanceof ObjectType) {
$runtimeType = $returnType;
} else if ($returnType instanceof AbstractType) {
$runtimeType = $returnType->getObjectType($result, $info);
if ($runtimeType && !$returnType->isPossibleType($runtimeType)) {
throw new Error(
"Runtime Object type \"$runtimeType\" is not a possible type for \"$returnType\"."
);
}
} else {
$runtimeType = null;
}
if (!$runtimeType) {
return null;
}
// If there is an isTypeOf predicate function, call it with the
// current result. If isTypeOf returns false, then raise an error rather
// than continuing execution.
if (false === $runtimeType->isTypeOf($result, $info)) {
throw new Error(
"Expected value of type $runtimeType but got: " . Utils::getVariableType($result),
$fieldASTs
);
}
// Collect sub-fields to execute to complete this value.
$subFieldASTs = new \ArrayObject();
$visitedFragmentNames = new \ArrayObject();
for ($i = 0; $i < count($fieldASTs); $i++) {
// Get memoized value if it exists
$uid = self::getFieldUid($fieldASTs[$i], $runtimeType);
if (isset($exeContext->memoized['collectSubFields'][$uid])) {
$subFieldASTs = $exeContext->memoized['collectSubFields'][$uid];
}
else {
$selectionSet = $fieldASTs[$i]->selectionSet;
if ($selectionSet) {
$subFieldASTs = self::collectFields(
$exeContext,
$runtimeType,
$selectionSet,
$subFieldASTs,
$visitedFragmentNames
);
$exeContext->memoized['collectSubFields'][$uid] = $subFieldASTs;
}
}
}
return self::executeFields($exeContext, $runtimeType, $result, $subFieldASTs);
return $executor->doExecute();
}
/**
* If a resolve function is not given, then a default resolve behavior is used
* which takes the property of the source object of the same name as the field
* which takes the property of the root value of the same name as the field
* and returns it as the result, or if it's a function, returns the result
* of calling that function.
* of calling that function while passing along args and context.
*
* @param mixed $objectValue
* @param mixed[] $args
* @param mixed|null $context
*
* @return mixed|null
*/
public static function defaultResolveFn($source, $args, ResolveInfo $info)
public static function defaultFieldResolver($objectValue, $args, $context, ResolveInfo $info)
{
$fieldName = $info->fieldName;
$property = null;
$property = null;
if (is_array($source) || $source instanceof \ArrayAccess) {
if (isset($source[$fieldName])) {
$property = $source[$fieldName];
if (is_array($objectValue) || $objectValue instanceof ArrayAccess) {
if (isset($objectValue[$fieldName])) {
$property = $objectValue[$fieldName];
}
} else if (is_object($source)) {
if (isset($source->{$fieldName})) {
$property = $source->{$fieldName};
} elseif (is_object($objectValue)) {
if (isset($objectValue->{$fieldName})) {
$property = $objectValue->{$fieldName};
}
}
return $property instanceof \Closure ? $property($source) : $property;
}
/**
* This method looks up the field on the given type defintion.
* It has special casing for the two introspection fields, __schema
* and __typename. __typename is special because it can always be
* queried as a field, even in situations where no other fields
* are allowed, like on a Union. __schema could get automatically
* added to the query type, but that would require mutating type
* definitions, which would cause issues.
*
* @return FieldDefinition
*/
private static function getFieldDef(Schema $schema, ObjectType $parentType, $fieldName)
{
$schemaMetaFieldDef = Introspection::schemaMetaFieldDef();
$typeMetaFieldDef = Introspection::typeMetaFieldDef();
$typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef();
if ($fieldName === $schemaMetaFieldDef->name && $schema->getQueryType() === $parentType) {
return $schemaMetaFieldDef;
} else if ($fieldName === $typeMetaFieldDef->name && $schema->getQueryType() === $parentType) {
return $typeMetaFieldDef;
} else if ($fieldName === $typeNameMetaFieldDef->name) {
return $typeNameMetaFieldDef;
}
$tmp = $parentType->getFields();
return isset($tmp[$fieldName]) ? $tmp[$fieldName] : null;
}
/**
* Get an unique identifier for a FieldAST.
*
* @param object $fieldAST
* @return string
*/
private static function getFieldUid($fieldAST, ObjectType $fieldType)
{
return $fieldAST->loc->start . '-' . $fieldAST->loc->end . '-' . $fieldType->name;
return $property instanceof Closure
? $property($objectValue, $args, $context, $info)
: $property;
}
}

View 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;
}

View file

@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace GraphQL\Executor\Promise\Adapter;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Utils\Utils;
use React\Promise\Promise as ReactPromise;
use React\Promise\PromiseInterface as ReactPromiseInterface;
use function React\Promise\all;
use function React\Promise\reject;
use function React\Promise\resolve;
class ReactPromiseAdapter implements PromiseAdapter
{
/**
* @inheritdoc
*/
public function isThenable($value)
{
return $value instanceof ReactPromiseInterface;
}
/**
* @inheritdoc
*/
public function convertThenable($thenable)
{
return new Promise($thenable, $this);
}
/**
* @inheritdoc
*/
public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null)
{
/** @var ReactPromiseInterface $adoptedPromise */
$adoptedPromise = $promise->adoptedPromise;
return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this);
}
/**
* @inheritdoc
*/
public function create(callable $resolver)
{
$promise = new ReactPromise($resolver);
return new Promise($promise, $this);
}
/**
* @inheritdoc
*/
public function createFulfilled($value = null)
{
$promise = resolve($value);
return new Promise($promise, $this);
}
/**
* @inheritdoc
*/
public function createRejected($reason)
{
$promise = reject($reason);
return new Promise($promise, $this);
}
/**
* @inheritdoc
*/
public function all(array $promisesOrValues)
{
// TODO: rework with generators when PHP minimum required version is changed to 5.5+
$promisesOrValues = Utils::map(
$promisesOrValues,
static function ($item) {
return $item instanceof Promise ? $item->adoptedPromise : $item;
}
);
$promise = all($promisesOrValues)->then(static function ($values) use ($promisesOrValues) {
$orderedResults = [];
foreach ($promisesOrValues as $key => $value) {
$orderedResults[$key] = $values[$key];
}
return $orderedResults;
});
return new Promise($promise, $this);
}
}

View file

@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace GraphQL\Executor\Promise\Adapter;
use Exception;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Utils\Utils;
use SplQueue;
use Throwable;
use function is_object;
use function method_exists;
/**
* Simplistic (yet full-featured) implementation of Promises A+ spec for regular PHP `sync` mode
* (using queue to defer promises execution)
*/
class SyncPromise
{
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
/** @var SplQueue */
public static $queue;
/** @var string */
public $state = self::PENDING;
/** @var ExecutionResult|Throwable */
public $result;
/**
* 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 !== null && ! $q->isEmpty()) {
$task = $q->dequeue();
$task();
}
}
public function resolve($value) : self
{
switch ($this->state) {
case self::PENDING:
if ($value === $this) {
throw new Exception('Cannot resolve promise with self');
}
if (is_object($value) && method_exists($value, 'then')) {
$value->then(
function ($resolvedValue) {
$this->resolve($resolvedValue);
},
function ($reason) {
$this->reject($reason);
}
);
return $this;
}
$this->state = self::FULFILLED;
$this->result = $value;
$this->enqueueWaitingPromises();
break;
case self::FULFILLED:
if ($this->result !== $value) {
throw new Exception('Cannot change value of fulfilled promise');
}
break;
case self::REJECTED:
throw new Exception('Cannot resolve rejected promise');
}
return $this;
}
public function reject($reason) : self
{
if (! $reason instanceof Exception && ! $reason instanceof Throwable) {
throw new Exception('SyncPromise::reject() has to be called with an instance of \Throwable');
}
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 === null) {
return $this;
}
$tmp = new self();
$this->waiting[] = [$tmp, $onFulfilled, $onRejected];
if ($this->state !== self::PENDING) {
$this->enqueueWaitingPromises();
}
return $tmp;
}
}

View file

@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
namespace GraphQL\Executor\Promise\Adapter;
use Exception;
use GraphQL\Deferred;
use GraphQL\Error\InvariantViolation;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Utils\Utils;
use Throwable;
use function count;
/**
* Allows changing order of field resolution even in sync environments
* (by leveraging queue of deferreds and promises)
*/
class SyncPromiseAdapter implements PromiseAdapter
{
/**
* @inheritdoc
*/
public function isThenable($value)
{
return $value instanceof Deferred;
}
/**
* @inheritdoc
*/
public function convertThenable($thenable)
{
if (! $thenable instanceof Deferred) {
throw new InvariantViolation('Expected instance of GraphQL\Deferred, got ' . Utils::printSafe($thenable));
}
return new Promise($thenable->promise, $this);
}
/**
* @inheritdoc
*/
public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null)
{
/** @var SyncPromise $adoptedPromise */
$adoptedPromise = $promise->adoptedPromise;
return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this);
}
/**
* @inheritdoc
*/
public function create(callable $resolver)
{
$promise = new SyncPromise();
try {
$resolver(
[
$promise,
'resolve',
],
[
$promise,
'reject',
]
);
} catch (Exception $e) {
$promise->reject($e);
} catch (Throwable $e) {
$promise->reject($e);
}
return new Promise($promise, $this);
}
/**
* @inheritdoc
*/
public function createFulfilled($value = null)
{
$promise = new SyncPromise();
return new Promise($promise->resolve($value), $this);
}
/**
* @inheritdoc
*/
public function createRejected($reason)
{
$promise = new SyncPromise();
return new Promise($promise->reject($reason), $this);
}
/**
* @inheritdoc
*/
public function all(array $promisesOrValues)
{
$all = new SyncPromise();
$total = count($promisesOrValues);
$count = 0;
$result = [];
foreach ($promisesOrValues as $index => $promiseOrValue) {
if ($promiseOrValue instanceof Promise) {
$result[$index] = null;
$promiseOrValue->then(
static function ($value) use ($index, &$count, $total, &$result, $all) {
$result[$index] = $value;
$count++;
if ($count < $total) {
return;
}
$all->resolve($result);
},
[$all, 'reject']
);
} else {
$result[$index] = $promiseOrValue;
$count++;
}
}
if ($count === $total) {
$all->resolve($result);
}
return new Promise($all, $this);
}
/**
* Synchronously wait when promise completes
*
* @return ExecutionResult
*/
public function wait(Promise $promise)
{
$this->beforeWait($promise);
$dfdQueue = Deferred::getQueue();
$promiseQueue = SyncPromise::getQueue();
while ($promise->adoptedPromise->state === SyncPromise::PENDING &&
! ($dfdQueue->isEmpty() && $promiseQueue->isEmpty())
) {
Deferred::runQueue();
SyncPromise::runQueue();
$this->onWait($promise);
}
/** @var SyncPromise $syncPromise */
$syncPromise = $promise->adoptedPromise;
if ($syncPromise->state === SyncPromise::FULFILLED) {
return $syncPromise->result;
}
if ($syncPromise->state === SyncPromise::REJECTED) {
throw $syncPromise->result;
}
throw new InvariantViolation('Could not resolve promise');
}
/**
* Execute just before starting to run promise completion
*/
protected function beforeWait(Promise $promise)
{
}
/**
* Execute while running promise completion
*/
protected function onWait(Promise $promise)
{
}
}

View file

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

View file

@ -0,0 +1,92 @@
<?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 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 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 instance of GraphQL\Executor\Promise\Promise.
*
* @return Promise
*
* @api
*/
public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null);
/**
* Creates a Promise
*
* Expected resolver signature:
* function(callable $resolve, callable $reject)
*
* @return Promise
*
* @api
*/
public function create(callable $resolver);
/**
* Creates a fulfilled Promise for a value if the value is not a promise.
*
* @param mixed $value
*
* @return Promise
*
* @api
*/
public function createFulfilled($value = null);
/**
* Creates a rejected promise for a reason if the reason is not a promise. If
* the provided reason is a promise, then it is returned as-is.
*
* @param Throwable $reason
*
* @return Promise
*
* @api
*/
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 Promise[]|mixed[] $promisesOrValues Promises or values.
*
* @return Promise
*
* @api
*/
public function all(array $promisesOrValues);
}

File diff suppressed because it is too large Load diff

View file

@ -1,30 +1,38 @@
<?php
declare(strict_types=1);
namespace GraphQL\Executor;
use GraphQL\Error;
use GraphQL\Language\AST\Argument;
use GraphQL\Language\AST\ListType;
use GraphQL\Language\AST\ListValue;
use GraphQL\Error\Error;
use GraphQL\Language\AST\ArgumentNode;
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\EnumValueDefinitionNode;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\ObjectValue;
use GraphQL\Language\AST\Value;
use GraphQL\Language\AST\Variable;
use GraphQL\Language\AST\VariableDefinition;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\ValueNode;
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\EnumType;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils;
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
{
@ -33,254 +41,239 @@ 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 VariableDefinition[] $definitionASTs
* @param array $inputs
* @return array
* @throws Error
* @param VariableDefinitionNode[] $varDefNodes
* @param mixed[] $inputs
*
* @return mixed[]
*/
public static function getVariableValues(Schema $schema, $definitionASTs, array $inputs)
public static function getVariableValues(Schema $schema, $varDefNodes, array $inputs)
{
$values = [];
foreach ($definitionASTs as $defAST) {
$varName = $defAST->variable->name->value;
$values[$varName] = self::getvariableValue($schema, $defAST, isset($inputs[$varName]) ? $inputs[$varName] : null);
$errors = [];
$coercedValues = [];
foreach ($varDefNodes as $varDefNode) {
$varName = $varDefNode->variable->name->value;
/** @var InputType|Type $varType */
$varType = TypeInfo::typeFromAST($schema, $varDefNode->type);
if (Type::isInputType($varType)) {
if (array_key_exists($varName, $inputs)) {
$value = $inputs[$varName];
$coerced = Value::coerceValue($value, $varType, $varDefNode);
/** @var Error[] $coercionErrors */
$coercionErrors = $coerced['errors'];
if (empty($coercionErrors)) {
$coercedValues[$varName] = $coerced['value'];
} else {
$messagePrelude = sprintf(
'Variable "$%s" got invalid value %s; ',
$varName,
Utils::printSafeJson($value)
);
foreach ($coercionErrors as $error) {
$errors[] = new Error(
$messagePrelude . $error->getMessage(),
$error->getNodes(),
$error->getSource(),
$error->getPositions(),
$error->getPath(),
$error,
$error->getExtensions()
);
}
}
} else {
if ($varType instanceof NonNull) {
$errors[] = new Error(
sprintf(
'Variable "$%s" of required type "%s" was not provided.',
$varName,
$varType
),
[$varDefNode]
);
} elseif ($varDefNode->defaultValue !== null) {
$coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType);
}
}
} else {
$errors[] = new Error(
sprintf(
'Variable "$%s" expected value of type "%s" which cannot be used as an input type.',
$varName,
Printer::doPrint($varDefNode->type)
),
[$varDefNode->type]
);
}
}
return $values;
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;
}
/**
* Prepares an object map of argument values given a list of argument
* definitions and list of argument AST nodes.
*
* @param FieldArgument[] $argDefs
* @param Argument[] $argASTs
* @param $variableValues
* @return array
* @param FieldDefinition|Directive $def
* @param FieldNode|DirectiveNode $node
* @param mixed[] $variableValues
*
* @return mixed[]
*
* @throws Error
*/
public static function getArgumentValues($argDefs, $argASTs, $variableValues)
public static function getArgumentValues($def, $node, $variableValues = null)
{
if (!$argDefs) {
if (empty($def->args)) {
return [];
}
$argASTMap = $argASTs ? Utils::keyMap($argASTs, function ($arg) {
return $arg->name->value;
}) : [];
$result = [];
foreach ($argDefs as $argDef) {
$name = $argDef->name;
$valueAST = isset($argASTMap[$name]) ? $argASTMap[$name]->value : null;
$value = self::valueFromAST($valueAST, $argDef->getType(), $variableValues);
if (null === $value) {
$value = $argDef->defaultValue;
}
if (null !== $value) {
$result[$name] = $value;
}
}
return $result;
}
public static function valueFromAST($valueAST, InputType $type, $variables = null)
{
if ($type instanceof NonNull) {
return self::valueFromAST($valueAST, $type->getWrappedType(), $variables);
$argumentNodes = $node->arguments;
if (empty($argumentNodes)) {
return [];
}
if (!$valueAST) {
return null;
$argumentValueMap = [];
foreach ($argumentNodes as $argumentNode) {
$argumentValueMap[$argumentNode->name->value] = $argumentNode->value;
}
if ($valueAST instanceof Variable) {
$variableName = $valueAST->name->value;
if (!$variables || !isset($variables[$variableName])) {
return null;
}
return $variables[$variableName];
}
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if ($valueAST instanceof ListValue) {
return array_map(function($itemAST) use ($itemType, $variables) {
return Values::valueFromAST($itemAST, $itemType, $variables);
}, $valueAST->values);
} else {
return [self::valueFromAST($valueAST, $itemType, $variables)];
}
}
if ($type instanceof InputObjectType) {
$fields = $type->getFields();
if (!$valueAST instanceof ObjectValue) {
return null;
}
$fieldASTs = Utils::keyMap($valueAST->fields, function($field) {return $field->name->value;});
$values = [];
foreach ($fields as $field) {
$fieldAST = isset($fieldASTs[$field->name]) ? $fieldASTs[$field->name] : null;
$fieldValue = self::valueFromAST($fieldAST ? $fieldAST->value : null, $field->getType(), $variables);
if (null === $fieldValue) {
$fieldValue = $field->defaultValue;
}
if (null !== $fieldValue) {
$values[$field->name] = $fieldValue;
}
}
return $values;
}
Utils::invariant($type instanceof ScalarType || $type instanceof EnumType, 'Must be input type');
return $type->parseLiteral($valueAST);
return static::getArgumentValuesForMap($def, $argumentValueMap, $variableValues, $node);
}
/**
* Given a variable definition, and any value of input, return a value which
* adheres to the variable definition, or throw an error.
*/
private static function getVariableValue(Schema $schema, VariableDefinition $definitionAST, $input)
{
$type = Utils\TypeInfo::typeFromAST($schema, $definitionAST->type);
$variable = $definitionAST->variable;
if (!$type || !Type::isInputType($type)) {
$printed = Printer::doPrint($definitionAST->type);
throw new Error(
"Variable \"\${$variable->name->value}\" expected value of type " .
"\"$printed\" which cannot be used as an input type.",
[ $definitionAST ]
);
}
if (self::isValidValue($input, $type)) {
if (null === $input) {
$defaultValue = $definitionAST->defaultValue;
if ($defaultValue) {
return self::valueFromAST($defaultValue, $type);
}
}
return self::coerceValue($type, $input);
}
throw new Error(
"Variable \${$definitionAST->variable->name->value} expected value of type " .
Printer::doPrint($definitionAST->type) . " but got: " . json_encode($input) . '.',
[$definitionAST]
);
}
/**
* Given a PHP value and a GraphQL type, determine if the value will be
* accepted for that type. This is primarily useful for validating the
* runtime values of query variables.
* @param FieldDefinition|Directive $fieldDefinition
* @param ArgumentNode[] $argumentValueMap
* @param mixed[] $variableValues
* @param Node|null $referenceNode
*
* @param $value
* @param Type $type
* @return bool
* @return mixed[]
*
* @throws Error
*/
private static function isValidValue($value, Type $type)
public static function getArgumentValuesForMap($fieldDefinition, $argumentValueMap, $variableValues = null, $referenceNode = null)
{
if ($type instanceof NonNull) {
if (null === $value) {
return false;
}
return self::isValidValue($value, $type->getWrappedType());
}
$argumentDefinitions = $fieldDefinition->args;
$coercedValues = [];
if ($value === null) {
return true;
}
foreach ($argumentDefinitions as $argumentDefinition) {
$name = $argumentDefinition->name;
$argType = $argumentDefinition->getType();
$argumentValueNode = $argumentValueMap[$name] ?? null;
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
if (is_array($value)) {
foreach ($value as $item) {
if (!self::isValidValue($item, $itemType)) {
return false;
}
if ($argumentValueNode === null) {
if ($argumentDefinition->defaultValueExists()) {
$coercedValues[$name] = $argumentDefinition->defaultValue;
} elseif ($argType instanceof NonNull) {
throw new Error(
'Argument "' . $name . '" of required type ' .
'"' . Utils::printSafe($argType) . '" was not provided.',
$referenceNode
);
}
} elseif ($argumentValueNode instanceof VariableNode) {
$variableName = $argumentValueNode->name->value;
if ($variableValues !== null && array_key_exists($variableName, $variableValues)) {
// Note: this does not check that this variable value is correct.
// This assumes that this query has been validated and the variable
// usage here is of the correct type.
$coercedValues[$name] = $variableValues[$variableName];
} elseif ($argumentDefinition->defaultValueExists()) {
$coercedValues[$name] = $argumentDefinition->defaultValue;
} elseif ($argType instanceof NonNull) {
throw new Error(
'Argument "' . $name . '" of required type "' . Utils::printSafe($argType) . '" was ' .
'provided the variable "$' . $variableName . '" which was not provided ' .
'a runtime value.',
[$argumentValueNode]
);
}
return true;
} else {
return self::isValidValue($value, $itemType);
}
}
if ($type instanceof InputObjectType) {
if (!is_array($value)) {
return false;
}
$fields = $type->getFields();
$fieldMap = [];
// Ensure every defined field is valid.
foreach ($fields as $fieldName => $field) {
/** @var FieldDefinition $field */
if (!self::isValidValue(isset($value[$fieldName]) ? $value[$fieldName] : null, $field->getType())) {
return false;
$valueNode = $argumentValueNode;
$coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues);
if (Utils::isInvalid($coercedValue)) {
// Note: ValuesOfCorrectType validation should catch this before
// execution. This is a runtime check to ensure execution does not
// continue with an invalid argument value.
throw new Error(
'Argument "' . $name . '" has invalid value ' . Printer::doPrint($valueNode) . '.',
[$argumentValueNode]
);
}
$fieldMap[$field->name] = $field;
$coercedValues[$name] = $coercedValue;
}
// Ensure every provided field is defined.
$diff = array_diff_key($value, $fieldMap);
if (!empty($diff)) {
return false;
}
return true;
}
Utils::invariant($type instanceof ScalarType || $type instanceof EnumType, 'Must be input type');
return null !== $type->parseValue($value);
return $coercedValues;
}
/**
* Given a type and any value, return a runtime value coerced to match the type.
* @deprecated as of 8.0 (Moved to \GraphQL\Utils\AST::valueFromAST)
*
* @param ValueNode $valueNode
* @param mixed[]|null $variables
*
* @return mixed[]|stdClass|null
*/
private static function coerceValue(Type $type, $value)
public static function valueFromAST($valueNode, InputType $type, ?array $variables = null)
{
if ($type instanceof NonNull) {
// Note: we're not checking that the result of coerceValue is non-null.
// We only call this function after calling isValidValue.
return self::coerceValue($type->getWrappedType(), $value);
}
return AST::valueFromAST($valueNode, $type, $variables);
}
if (null === $value) {
return null;
}
/**
* @deprecated as of 0.12 (Use coerceValue() directly for richer information)
*
* @param mixed[] $value
*
* @return string[]
*/
public static function isValidPHPValue($value, InputType $type)
{
$errors = Value::coerceValue($value, $type)['errors'];
if ($type instanceof ListOfType) {
$itemType = $type->getWrappedType();
// TODO: support iterable input
if (is_array($value)) {
return array_map(function ($item) use ($itemType) {
return Values::coerceValue($itemType, $item);
}, $value);
} else {
return [self::coerceValue($itemType, $value)];
}
}
if ($type instanceof InputObjectType) {
$fields = $type->getFields();
$obj = [];
foreach ($fields as $fieldName => $field) {
$fieldValue = self::coerceValue($field->getType(), isset($value[$fieldName]) ? $value[$fieldName] : null);
if (null === $fieldValue) {
$fieldValue = $field->defaultValue;
}
if (null !== $fieldValue) {
$obj[$fieldName] = $fieldValue;
}
}
return $obj;
}
Utils::invariant($type instanceof ScalarType || $type instanceof EnumType, 'Must be input type');
return $type->parseValue($value);
return $errors
? array_map(
static function (Throwable $error) {
return $error->getMessage();
},
$errors
)
: [];
}
}

View 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);
}
}
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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);
}

View 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;
}
}

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