Skip to content

Commit 7211624

Browse files
committed
Initial commit
0 parents  commit 7211624

32 files changed

+3176
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.phpcs-cache
2+
composer.lock
3+
composer.phar
4+
phpcs.xml
5+
phpstan.neon
6+
vendor/

.scrutinizer.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
build:
2+
nodes:
3+
analysis:
4+
environment:
5+
php:
6+
version: 7.1
7+
cache:
8+
disabled: false
9+
directories:
10+
- ~/.composer/cache
11+
project_setup:
12+
override: true
13+
tests:
14+
override:
15+
- php-scrutinizer-run
16+
17+
dependencies:
18+
override:
19+
- composer install --ignore-platform-reqs --no-interaction
20+
21+
tools:
22+
external_code_coverage:
23+
timeout: 900
24+
25+
build_failure_conditions:
26+
- 'elements.rating(<= C).new.exists' # No new classes/methods with a rating of C or worse allowed
27+
- 'issues.label("coding-style").new.exists' # No new coding style issues allowed
28+
- 'issues.severity(>= MAJOR).new.exists' # New issues of major or higher severity
29+
- 'project.metric_change("scrutinizer.test_coverage", < 0)' # Code Coverage decreased from previous inspection

.travis.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
dist: trusty
2+
language: php
3+
4+
php:
5+
- 7.1
6+
- 7.2
7+
- 7.3
8+
- nightly
9+
10+
env:
11+
matrix:
12+
- EXECUTOR= DEPENDENCIES=--prefer-lowest
13+
- EXECUTOR=coroutine DEPENDENCIES=--prefer-lowest
14+
- EXECUTOR=
15+
- EXECUTOR=coroutine
16+
17+
cache:
18+
directories:
19+
- $HOME/.composer/cache
20+
21+
before_install:
22+
- mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{,.disabled} || echo "xdebug not available"
23+
- travis_retry composer self-update
24+
25+
install: travis_retry composer update --prefer-dist
26+
27+
script: ./vendor/bin/phpunit --group default
28+
29+
jobs:
30+
allow_failures:
31+
- php: nightly
32+
33+
include:
34+
- stage: Test
35+
install:
36+
- travis_retry composer update --prefer-dist {$DEPENDENCIES}
37+
38+
- stage: Test
39+
env: COVERAGE
40+
before_script:
41+
- mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{.disabled,}
42+
- if [[ ! $(php -m | grep -si xdebug) ]]; then echo "xdebug required for coverage"; exit 1; fi
43+
script:
44+
- ./vendor/bin/phpunit --coverage-php /tmp/coverage/clover_executor.cov
45+
- EXECUTOR=coroutine ./vendor/bin/phpunit --coverage-php /tmp/coverage/clover_executor-coroutine.cov
46+
after_script:
47+
- ./vendor/bin/phpcov merge /tmp/coverage --clover /tmp/clover.xml
48+
- wget https://github.com/scrutinizer-ci/ocular/releases/download/1.5.2/ocular.phar
49+
- php ocular.phar code-coverage:upload --format=php-clover /tmp/clover.xml
50+
51+
- stage: Code Quality
52+
php: 7.1
53+
env: CODING_STANDARD
54+
install: travis_retry composer install --prefer-dist
55+
script:
56+
- ./vendor/bin/phpcs
57+
58+
- stage: Code Quality
59+
php: 7.1
60+
env: STATIC_ANALYSIS
61+
install: travis_retry composer install --prefer-dist
62+
script: composer static-analysis

CONTRIBUTING.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Contributing to GraphQL PHP Validation Toolkit
2+
PRs and suggestions are welcome.
3+
4+
## Workflow
5+
6+
### Install
7+
1. Fork this repo
8+
2. clone it into your repos directory and navigate to that directory:
9+
```
10+
git clone https://github.com/shmax/graphql-php-validation-toolkit.git
11+
cd graphql-php-validation-toolkit
12+
```
13+
3. Install dependencies:
14+
```
15+
composer install
16+
```
17+
18+
### Iterate
19+
1. Make your changes
20+
2. Add one or more tests to the `/tests` folder to affirm your changes
21+
3. Execute `composer run test`.
22+
4. Execute `composer run lint` to make sure there are no style issues.
23+
5. If there are a lot of style isues, you can run `composer run fix-style` to automatically fix them.
24+
5. Repeat 1-4 until the code does what you want, all tests pass, and there are no style issues. To save time, you can run `composer check-all`
25+
26+
### Submit
27+
1. Commit your changes and push your branch
28+
2. Open a PR

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 Max Loeb
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# graphql-php-validation-toolkit
2+
[![Build Status](https://travis-ci.org/shmax/graphql-php-validation-toolkit.svg?branch=master)](https://travis-ci.org/shmax/graphql-php-validation-toolkit)
3+
4+
GraphQL is great when it comes to validating types and checking syntax, but isn't much help when it comes to providing additional validation on user input. The authors of GraphQL have generally opined that the correct response to bad user input is not to throw an exception, but rather to return any validation feedback along with the result. That's where this small library comes in.
5+
6+
`graphql-php-validation-toolkit` extends the built-in definitions provided by the wonderful [graphql-php](https://github.com/webonyx/graphql-php) library with a new `ValidatedFieldDefinition` class. Simply instantiate one of these in place of the usual field config, add `validate` callback properties to your `args` definitions, and the `type` of your field will be replaced by a new, dynamically-generated `ResultType` with queryable error fields for each of your args. It's a recursive process, so your `args` can have `InputObjectType` types with subfields and `validate` callbacks of their own. Your originally-defined `type` gets moved to the `result` field of the generated type.
7+
8+
## Documentation
9+
10+
- [Basic Usage](#basic-usage)
11+
- [The Validate Callback](#the-validate-callback)
12+
- [Custom Error Codes](#custom-error-codes)
13+
- [Examples](#examples)
14+
15+
### Basic Usage
16+
In a nutshell, replace your usual vanilla field definition with an instance of `ValidatedFieldDefinition`, and add `validate` callbacks to one or more of the `args` configs. Let's say you want to make a mutation called `updateBook`:
17+
18+
```php
19+
//...
20+
'updateBook' => new ValidatedFieldDefinition([
21+
'name' => 'updateBook',
22+
'type' => Types::book(),
23+
'args' => [
24+
'bookId' => [
25+
'type' => Type::id(),
26+
'validate' => function ($bookId) {
27+
global $books;
28+
if (!Book::find($bookId) {
29+
return 0;
30+
}
31+
32+
return [1, 'Unknown book!'];
33+
},
34+
],
35+
],
36+
'resolve' => static function ($value, $args) : bool {
37+
return Book::find($args['bookId']);
38+
},
39+
],
40+
```
41+
42+
In the sample above, the `book` type property of your field definition will be replaced by a new dynamically-generated type called `UpdateBookResultType` type.
43+
44+
The type generation process is recursive, traveling down through any nested `InputObjectType` or `ListOf` types and checking their `fields` for more `validate` callbacks. Every field definition--including the very top one--that has a `validate` callback will be represented by a custom, generated type with the following queryable fields:
45+
46+
| Field | Type | Description |
47+
|-------------|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
48+
| `code` | `int|<field-name>ErrorCode` | This will resolve to `0` for a valid field, otherwise `1`. If `errorCodes` were provided, then this will be a custom generated Enum type. |
49+
| `msg` | `string` | A plain, natural language description of the error. |
50+
| `suberrors` | `<field-name>_Suberrors` | If your field has a complex type (eg. `InputObjectType` or `ListOfType`), then a `suberrors` field will be added with its own custom, generated type. |
51+
52+
The top-level `<field-name>ResultType` will have an additional field:
53+
54+
| Field | Type | Description |
55+
|-------------|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
56+
| `valid` | `bool` | Resolves to `true` if all `args` and nested `fields` pass validation, `false` if not. |
57+
58+
You can then simply query for these fields along with `result`:
59+
```graphql
60+
mutation {
61+
updateAuthor(
62+
authorId: 1
63+
) {
64+
valid
65+
result {
66+
id
67+
name
68+
}
69+
code
70+
msg
71+
suberrors {
72+
authorId {
73+
code
74+
msg
75+
}
76+
}
77+
}
78+
}
79+
```
80+
81+
### The Validate Callback
82+
83+
Any field definition can have a `validate` callback. The first argument passed to the `validate` callback will be the value to validate.
84+
If the value is valid, return `0`, otherwise `1`.
85+
86+
```php
87+
//...
88+
'updateAuthor' => new ValidatedFieldDefinition([
89+
'type' => Types::author(),
90+
'args' => [
91+
'authorId' => [
92+
'validate' => function(string $authorId) {
93+
if(Author::find($authorId)) {
94+
return 0;
95+
}
96+
return 1;
97+
}
98+
]
99+
]
100+
])
101+
```
102+
103+
If you want to return an error message, return an array with the message in the second bucket:
104+
```php
105+
//...
106+
'updateAuthor' => new ValidatedFieldDefinition([
107+
'type' => Types::author(),
108+
'args' => [
109+
'authorId' => [
110+
'validate' => function(string $authorId) {
111+
if(Author::find($authorId)) {
112+
return 0;
113+
}
114+
return [1, "We can't find that author"];
115+
}
116+
]
117+
]
118+
])
119+
```
120+
Note that `ListOf` types are a special case, and support an additional `validateItem` callback for checking each item in their array. Its generated error type will have an array of suberror types, each with their own `index` field that you can query so you can know exactly which array items failed validation:
121+
122+
```php
123+
//...
124+
'setPhoneNumbers' => new ValidatedFieldDefinition([
125+
'type' => Types::bool(),
126+
'args' => [
127+
'phoneNumbers' => [
128+
'type' => Type::listOf(Type::string()),
129+
'validate' => function(array $phoneNumbers) {
130+
if(!count($phoneNumbers)) {
131+
return [1, "At least one phone number is required"];
132+
}
133+
return 0;
134+
},
135+
'validateItem' => function(string $phoneNumber) {
136+
$res = preg_match('/^[0-9\-]+$/', $phoneNumber) === 1;
137+
if (!$res) {
138+
return [1, 'That does not seem to be a valid phone number'];
139+
}
140+
return 0;
141+
}
142+
]
143+
]
144+
])
145+
```
146+
147+
### Custom Error Codes
148+
149+
If you would like to use custom error codes, add an `errorCodes` property at the same level as your `validate` callback:
150+
151+
```php
152+
//...
153+
'updateAuthor' => [
154+
'type' => Types::author(),
155+
'errorCodes' => [
156+
'authorNotFound`
157+
],
158+
'validate' => function(string $authorId) {
159+
if(Author::find($authorId)) {
160+
return 1;
161+
}
162+
return ['authorNotFound', "We can't find that author"];
163+
}
164+
]
165+
```
166+
167+
`ListOf` types are again a special case, and support an additional `suberrorCodes` property for validating their items:
168+
169+
```php
170+
//...
171+
'setPhoneNumbers' => new ValidatedFieldDefinition([
172+
'type' => Types::bool(),
173+
'args' => [
174+
'phoneNumbers' => [
175+
'errorCodes' => [
176+
'atLeastOneRequired`
177+
],
178+
'suberrorCodes' => [
179+
'invalidPhoneNumber'
180+
],
181+
'type' => Type::listOf(Type::string()),
182+
'validate' => function(array $phoneNumbers) {
183+
if(!count($phoneNumbers)) {
184+
return ['atLeastOneRequired', "At least one phone number is required"];
185+
}
186+
return 0;
187+
},
188+
'validateItem' => function(string $phoneNumber) {
189+
$res = preg_match('/^[0-9\-]+$/', $phoneNumber) === 1;
190+
if (!$res) {
191+
return ['invalidPhoneNumber', 'That does not seem to be a valid phone number'];
192+
}
193+
return 0;
194+
}
195+
]
196+
]
197+
])
198+
```
199+
200+
## Examples
201+
The best way to understand how all this works is to experiment with it. There are a series of increasingly complex one-page samples in the `/examples` folder. Each is accompanied by its own `README.md`, with instructions for running the code. Run each sample, and be sure to inspect the dynamically-generated types in [ChromeiQL](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij?hl=en).
202+
203+
01. [basic-scalar-validation](./examples/01-basic-scalar-validation)
204+
02. [custom-error-types](./examples/02-custom-error-codes)
205+
03. [input-object-validation](./examples/03-input-object-validation)
206+
03. [list-of-validation](./examples/04-list-of-validation)
207+
208+
## Contribute
209+
Contributions are welcome. Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
210+

0 commit comments

Comments
 (0)