shmax / graphql-php-validation-toolkit
Do validation on fields and args for graphql queries and mutations, and dynamically generate user error types
Installs: 3 913
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 2
Forks: 1
Open Issues: 2
Requires
- php: ^8.1
- ext-intl: *
- ext-json: *
- ext-mbstring: *
Requires (Dev)
- mll-lab/php-cs-fixer-config: ^5.0
- phpbench/phpbench: ^1.2.0
- phpstan/phpstan: 1.10.6
- phpstan/phpstan-phpunit: 1.3.10
- phpstan/phpstan-strict-rules: 1.5.0
- phpunit/phpunit: ^9.5
- webonyx/graphql-php: ^v15.3.0
- dev-master
- v3.0.0-rc23
- v3.0.0-rc22
- v3.0.0-rc21
- v3.0.0-rc20
- v3.0.0-rc19
- v3.0.0-rc18
- v3.0.0-rc17
- v3.0.0-rc16
- v3.0.0-rc15
- v3.0.0-rc14
- v3.0.0-rc13
- v3.0.0-rc12
- v3.0.0-rc11
- v3.0.0-rc10
- v3.0.0-rc9
- v3.0.0-rc8
- v3.0.0-rc7
- v3.0.0-rc6
- v3.0.0-rc5
- v3.0.0-rc4
- v3.0.0-rc3
- 3.0.0-rc1
- 3.0.0-rc
- v2.2.1
- v2.2
- v2.1
- v2.0
- 1.0.0
- v0.4.2
- v0.4.1
- v0.4.0
- v0.3.0
- v0.2.0
- v0.1.3
- v0.1.2
- v0.1.1
- v0.1
- dev-3.0-rc
- dev-rewrite
- dev-dependabot/github_actions/dot-github/workflows/actions/download-artifact-4.1.7
This package is auto-updated.
Last update: 2024-11-04 03:26:15 UTC
README
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.
As Lee Byron explains here:
...allow for data for a user-facing report in the payload of your mutation. It's often the case that mutation payloads include a "didSucceed" field and a "userError" field. If your UI requires rich information about potential errors, then you should include this information in your payload as well.
That's where this small library comes in.
graphql-php-validation-toolkit
extends the built-in definitions provided by the wonderful 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.
Installation
Via composer:
composer require shmax/graphql-php-validation-toolkit
Documentation
Basic Usage
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
:
//... 'updateBook' => new ValidatedFieldDefinition([ 'name' => 'updateBook', 'type' => Types::book(), 'args' => [ 'bookId' => [ 'type' => Type::id(), 'validate' => function ($bookId) { global $books; if (!Book::find($bookId) { return 0; } return [1, 'Unknown book!']; }, ], ], 'resolve' => static function ($value, $args) : bool { return Book::find($args['bookId']); }, ],
In the sample above, the book
type property of your field definition will be replaced by a new dynamically-generated type called UpdateBookResultType
.
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:
The top-level <field-name>ResultType
will have a few additional fields:
You can then simply query for these fields along with result
:
mutation { updateAuthor( authorId: 1 ) { valid result { id name } code msg suberrors { authorId { code msg } } } }
The Validate Callback
Any field definition can have a validate
callback. The first argument passed to the validate
callback will be the value to validate.
If the value is valid, return 0
, otherwise 1
.
//... 'updateAuthor' => new ValidatedFieldDefinition([ 'type' => Types::author(), 'args' => [ 'authorId' => [ 'validate' => function(string $authorId) { if(Author::find($authorId)) { return 0; } return 1; } ] ] ])
The required
property
You can mark any field as required
, and if the value is not provided, then an automatic validation will happen for you (thus removing the need for you to weaken your validation callback with null
types). You can set it to true
, or you can provide an error array similar to the one returned by your validate callback. You can also set it to a callable that returns the same bool or error array.
//... 'updateThing' => new ValidatedFieldDefinition([ 'type' => Types::thing(), 'args' => [ 'foo' => [ 'required' => true, // if not provided, then an error of the form [1, 'foo is required'] will be returned. 'validate' => function(string $foo) { if(Foo::find($foo)) { return 0; } return 1; } ], 'bar' => [ 'required' => [1, 'Oh, where is the bar?!'], 'validate' => function(string $bar) { if(Bar::find($bar)) { return 0; } return 1; } ], 'naz' => [ 'required' => static fn() => !Moderator::loggedIn(), 'validate' => function(string $naz) { if(Naz::find($naz)) { return 0; } return 1; } ] ] ])
If you want to return an error message, return an array with the message in the second bucket:
//... 'updateAuthor' => new ValidatedFieldDefinition([ 'type' => Types::author(), 'args' => [ 'authorId' => [ 'validate' => function(string $authorId) { if(Author::find($authorId)) { return 0; } return [1, "We can't find that author"]; } ] ] ])
Generated ListOf
error types also have a path
field that you can query so you can know the exact address in the multidimensional array of each item that failed validation:
//... 'setPhoneNumbers' => new ValidatedFieldDefinition([ 'type' => Types::bool(), 'args' => [ 'phoneNumbers' => [ 'type' => Type::listOf(Type::string()), 'validate' => function(string $phoneNumber) { $res = preg_match('/^[0-9\-]+$/', $phoneNumber) === 1; if (!$res) { return [1, 'That does not seem to be a valid phone number']; } return 0; } ] ] ])
Custom Error Codes
If you would like to use custom error codes, add an errorCodes property at the same level as your validate callback and feed it the path to a PHP native enum:
enum AuthorErrors { case AuthorNotFound; } 'updateAuthor' => [ 'type' => Types::author(), 'errorCodes' => AuthorErrors::class, 'validate' => function(string $authorId) { if(Author::find($authorId)) { return 0; } return [AuthorErrors::AuthorNotFound, "We can't find that author"]; } ]
Keep in mind that the library will generate unique names for the error code types, and they can become quite long depending on how deeply they are nested in the field structure:
echo $errorType->name; // Author_Attributes_FirstName_PriceErrorCode
If this becomes a problem for you, be sure to provide a type setter (see example) that returns the type that was set, and then the generated name will simply be the name of the enum class that was passed in, plus "ErrorCode":
echo $errorType->name; // PriceErrorCode
Managing Created Types
This library will create new types as needed. If you are using some kind of type manager to store and retrieve types, you can integrate it by providing a typeSetter
callback. Make sure it returns the type that was set:
new ValidatedFieldDefinition([ 'typeSetter' => static function ($type) { return Types::set($type); }, ]);
Examples
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.
Contribute
Contributions are welcome. Please refer to CONTRIBUTING.md for guidelines.