|
| 1 | +# graphql-php-validation-toolkit |
| 2 | +[](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