PHPackages                             shmax/graphql-php-validation-toolkit - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. shmax/graphql-php-validation-toolkit

ActiveLibrary[API Development](/categories/api)

shmax/graphql-php-validation-toolkit
====================================

Do validation on fields and args for graphql queries and mutations, and dynamically generate user error types

v2.2.1(1y ago)16.8k↓51.4%1[2 PRs](https://github.com/shmax/graphql-php-validation-toolkit/pulls)MITPHPPHP ^8.1

Since Feb 3Pushed 4mo ago1 watchersCompare

[ Source](https://github.com/shmax/graphql-php-validation-toolkit)[ Packagist](https://packagist.org/packages/shmax/graphql-php-validation-toolkit)[ Docs](https://github.com/shmax/graphql-php-validation-toolkit)[ RSS](/packages/shmax-graphql-php-validation-toolkit/feed)WikiDiscussions master Synced yesterday

READMEChangelog (10)Dependencies (7)Versions (42)Used By (0)

graphql-php-validation-toolkit
==============================

[](#graphql-php-validation-toolkit)

[![License](https://camo.githubusercontent.com/0fc9bdffe74cf96df6b7d110dcd79d45f783c87e47791b9022410c3b235d04f6/68747470733a2f2f706f7365722e707567782e6f72672f73686d61782f6772617068716c2d7068702d76616c69646174696f6e2d746f6f6c6b69742f6c6963656e7365)](https://packagist.org/packages/shmax/graphql-php-validation-toolkit)[![PHPStan lvl-6](https://github.com/shmax/graphql-php-validation-toolkit/actions/workflows/static-analysis.yml/badge.svg)](https://github.com/shmax/graphql-php-validation-toolkit/actions/workflows/static-analysis.yml)[![Coverage Status](https://camo.githubusercontent.com/6907ff95290d0ad4a7ff63a0d2793f844db855ca680a633eb116b27ba1887426/68747470733a2f2f636f6465636f762e696f2f67682f73686d61782f6772617068716c2d7068702d76616c69646174696f6e2d746f6f6c6b69742f6272616e63682f6d61737465722f67726170682f62616467652e737667)](https://codecov.io/gh/shmax/graphql-php-validation-toolkit/branch/master)[![Latest Stable Version](https://camo.githubusercontent.com/faf6bbc6ed05b62a982fe4c5a86a67e3cbfb661a70ae31fcd1ff3f74a93436be/68747470733a2f2f706f7365722e707567782e6f72672f73686d61782f6772617068716c2d7068702d76616c69646174696f6e2d746f6f6c6b69742f76657273696f6e)](https://packagist.org/packages/shmax/graphql-php-validation-toolkit)

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](https://github.com/facebook/graphql/issues/117#issuecomment-170180628):

> ...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](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.

Installation
------------

[](#installation)

Via composer:

```
composer require shmax/graphql-php-validation-toolkit

```

Documentation
-------------

[](#documentation)

- [Basic Usage](#basic-usage)
- [The Validate Callback](#the-validate-callback)
- [Custom Error Codes](#custom-error-codes)
- [Managing Created Types](#managing-created-types)
- [Examples](#examples)

### Basic Usage

[](#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:

FieldTypeDescription`code``int` | `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.`msg``string`A plain, natural language description of the error.`suberrors``_Suberrors`A `suberrors` field will be added to a generated field of type `InputObjectType` if any of the following are true: 1. It is the root node
2. The field has a `validate` method
3. The `InputObjectType` is wrapped in a `ListOfType`

The top-level `ResultType` will have a few additional fields:

FieldTypeDescription`valid``bool`Resolves to `true` if all `args` and nested `fields` pass validation, `false` if not.`result``mixed`This is the original `type` you provided when declaring your field. Eg, If you specified `type` to be a `Book`, then the type of `result` will be `Book`.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

[](#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

[](#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

[](#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](examples/02-custom-error-codes)) 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

[](#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
--------

[](#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](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij?hl=en).

1. [basic-scalar-validation](./examples/01-basic-scalar-validation)
2. [custom-error-types](./examples/02-custom-error-codes)
3. [input-object-validation](./examples/03-input-object-validation)
4. [list-of-validation](./examples/04-list-of-validation)

Contribute
----------

[](#contribute)

Contributions are welcome. Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.

###  Health Score

48

—

FairBetter than 93% of packages

Maintenance58

Moderate activity, may be stable

Popularity26

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity82

Battle-tested with a long release history

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~58 days

Recently: every ~0 days

Total

37

Last Release

620d ago

Major Versions

v0.4.2 → 1.0.02023-03-10

1.0.0 → v2.02023-05-03

v2.2.1 → 3.0.0-rc2024-10-18

PHP version history (3 changes)v0.1PHP ^7.1

v2.0PHP ^8

v2.1PHP ^8.1

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/89408526?v=4)[mloeb](/maintainers/mloeb)[@Mloeb](https://github.com/Mloeb)

---

Top Contributors

[![shmax](https://avatars.githubusercontent.com/u/773172?v=4)](https://github.com/shmax "shmax (74 commits)")

---

Tags

graphqlgraphql-phpphpvalidationphpmutationvalidationgraphqluser errors

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/shmax-graphql-php-validation-toolkit/health.svg)

```
[![Health](https://phpackages.com/badges/shmax-graphql-php-validation-toolkit/health.svg)](https://phpackages.com/packages/shmax-graphql-php-validation-toolkit)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
