The validation feature was introduced in version 0.13
This bundle provides a tight integration with the Symfony Validator Component to validate user input data. It currently supports only GraphQL schemas defined with YAML.
In order to validate input data, the only thing you need to do is to apply constraints
in your yaml type definitions (args by object types and fields by input-object types). The bundle will then
automatically validate the data and throw an exception, which will be caught and returned in the response back to the
client.
Follow the example below to get a quick overview of the most basic validation capabilities of this bundle.
# config\graphql\types\Mutation.yaml
Mutation:
type: object
config:
fields:
register:
type: User
resolve: "@=mutation('register', [args])"
args:
username:
type: String!
validation: # applying constraints to `username`
- Length:
min: 6
max: 32
password:
type: String!
validation: # applying constraints to `password`
- Length:
min: 8
max: 32
- IdenticalTo:
propertyPath: passwordRepeat
passwordRepeat:
type: String!
emails:
type: "[String]"
validation: # applying constraints to `emails`
- Unique: ~
- Count:
min: 1
max: 3
- All:
- Email: ~
birthdate:
type: Birthdate
validation: cascade # delegating validation to the embedded type
Birthdate:
type: input-object
config:
fields:
day:
type: Int!
validation:
- Range: { min: 1, max: 31 }
month:
type: Int!
validation:
- Range: { min: 1, max: 12 }
year:
type: Int!
validation:
- Range: { min: 1900, max: 2019 }
The configuration above checks, that:
The birthdate field is of type input-object and is marked as cascade, so its validation will happen according to the constraints declared in the Birthdate type:
The Symfony Validator Component is designed to validate
objects. For this reason, when this bundle starts a validation, all input data is first converted into objects of class
ValidationNode and then validated. This process is performed
automatically by the bundle just before calling corresponding resolvers (each resolver gets its own InputValidator
instance). If validation fails, the corresponding resolver will not be called (except when you perform
validation inside your resolvers).
Note that the created objects are only used for validation purposes. Your resolvers will receive raw unaltered arguments as usual.
Validation objects are created differently depending on the GraphQL type. Take a look at the following scheme:

As you can see, there are 2 GraphQL types: Mutation and DateInput (object and input-object respectively). In
the case of Mutation, this bundle creates an object per each field (createUser and createPost), but in the
case of the DateInput, it creates an object for the entire type.
Keep in mind that objects are not created recursively by default. As you can see, the argument createdAt has its
validation set to cascade. It is a special value, which delegates the validation to the embedded type by doing the
following:
DateInput) into an object.Valid constraint (for a
recursive validation).If you don't mark embedded types as cascade, they will stay arrays, which can still be validated, as shown in the
following examples.
All object properties are created dynamically and then the validation constraints are applied to them. The resulting object composition will then be recursively validated, starting from the root object down to its children.
Note: Although it would have been possible to validate raw arguments, objects provide a better flexibility and more features.
Here is a more complex example to better demonstrate how the InputValidator creates objects from your GraphQL schema
and embeds them into each other:
Mutation:
type: object
config:
fields:
registerUser:
type: User
resolve: "@=mutation('registerUser', [args])"
args:
username:
type: String!
validation:
- App\Constraint\Latin: ~
- Length: { min: 5, max: 16 }
password:
type: String!
validation:
- App\Constraint\Latin: ~
- IdenticalTo:
propertyPath: passwordRepeat
passwordRepeat:
type: String!
emails:
type: "[String]"
validation:
- Unique: ~
- Count:
min: 1
max: 3
- All:
- Email: ~
birthday:
type: Birthday
validation: cascade
job:
type: Job
validation: cascade
address:
type: Address
validation:
- Collection:
fields:
street:
- App\Constraint\Latin: ~
- Length: { min: 2, max: 64 }
city:
- App\Constraint\Latin: ~
- Length: { min: 2, max: 64 }
zip:
- Positive: ~
Job:
type: input-object
config:
fields:
position:
type: String!
validation:
- Choice: [developer, manager, designer]
workPeriod:
type: Period
validation: cascade
address:
type: Address
validation: cascade
Address:
type: input-object
config:
fields:
street:
type: String!
validation:
- App\Constraint\Latin: ~
- Length: { min: 2, max: 64 }
city:
type: String!
validation:
- App\Constraint\Latin: ~
- Length: { min: 2, max: 64 }
zip:
type: Int!
validation:
- Positive: ~
Period:
type: input-object
config:
fields:
startDate:
type: String!
validation:
- Date: ~
endDate:
type: String!
validation:
- Date: ~
- GreaterThan:
propertyPath: 'startDate'
Birthday:
type: input-object
config:
fields:
day:
type: Int!
validation:
- Range: { min: 1, max: 31 }
month:
type: Int!
validation:
- Range: { min: 1, max: 12 }
year:
type: Int!
validation:
- Range: { min: 1900, max: today }
The configuration above would produce an object composition as shown in the UML diagram below:
Note: The argument
addressin the objectMutationwasn't converted into an object, as it doesn't have the keycascade, but it will still be validated against theCollectionconstraint as an array.
If you are familiar with the Symfony Validator Component, you might know that constraints can have different targets (class members or entire classes). Since all input data is represented by objects during the validation, you can also declare member constraints as well as class constraints.
There are 3 different methods to apply validation constraints:
constraints key.link key.cascade key.All 3 methods can be mixed, but if you use only 1 method you can omit the corresponding key and type config directly
under validation.
The most straightforward way to apply validation constraints is to list them under the constraints key. In the chapter
Overview this method has already been demonstrated. Follow the examples below to see how to use
only this method, as well as in combinations with linking:
Property constraints are applied to arguments:
Mutation:
type: object
config:
fields:
updateUser:
type: User
resolve: "@=mutation('updateUser', [args])"
args:
username:
type: String
validation: # using an explicit list of constraints (short form)
- NotBlank: ~
- Length:
min: 6
max: 32
minMessage: "Username must have {{ limit }} characters or more"
maxMessage: "Username must have {{ limit }} characters or less"
email:
type: String
validation: App\Entity\User::$email # using a link (short form)
info:
type: String
validation: # mixing both
link: App\Entity\User::$info
constraints:
- NotBlank: ~
- App\Constraint\MyConstraint: ~ # custom constraint
Class-level constraints are applied to fields:
Mutation:
type: object
config:
fields:
updateUser:
validation:
- Callback: [App\Validation\UserValidator, updateUser]
type: User
resolve: "@=mutation('updateUser', [args])"
args:
username: String
email: String
info: String
It's also possible to declare validation constraints for the entire type. This is useful if you don't want to repeat the configuration for each field or if you want to move the entire validation logic into a function:
Mutation:
type: object
config:
validation:
- Callback: [App\Validation\UserValidator, validate]
fields:
createUser:
type: User
resolve: "@=mutation('createUser', [args])"
args:
username: String
email: String
info: String
updateUser:
type: User
resolve: "@=mutation('updateUser', [args])"
args:
username: String
email: String
info: String
which is equal to:
Mutation:
type: object
config:
fields:
createUser:
validation:
- Callback: [App\Validation\UserValidator, validate]
type: User
resolve: "@=mutation('createUser', [args])"
args:
username: String
email: String
info: String
updateUser:
validation:
- Callback: [App\Validation\UserValidator, validate]
type: User
resolve: "@=mutation('updateUser', [args])"
args:
username: String
email: String
info: String
input-object types are designed to be used as arguments in other types. Basically, they are composite arguments, so
the property constraints are declared for each field unlike object types, where the property constraints are
declared for each argument:
User:
type: input-object
config:
fields:
username:
type: String!
validation: # using an explicit list of constraints
- NotBlank: ~
- Length: { min: 6, max: 32 }
password:
type: String!
validation: App\Entity\User::$password # using a link
email:
type: String!
validation: # mixing both
link: App\Entity\User::$email
constraints:
- Email: ~
Class-level constraints are declared 2 levels higher, under the config key:
User:
type: input-object
config:
validation:
- Callback: [App\Validation\UserValidator, validate]
fields:
username:
type: String!
password:
type: String!
email:
type: String!
If you already have classes (e.g. Doctrine entities) with validation constraints applied to them, you can reuse these constraints in your configuration files by linking corresponding properties, getters or entire classes. What the link key does is simply copy all constraints of the given target without any change and apply them to an argument/field.
A link can have 4 different forms, each of which targets different parts of a class:
<ClassName>::$<propertyName> - the $ symbol indicates a single class property.<ClassName>::<propertyName>() - the parentheses indicate all getters of the given property name.<ClassName>::<propertyName> - the absence of the $ and parentheses indicates a single property and all its getters.<ClassName> - the absence of a class member indicates an entire class.for example:
App\Entity\User::$username - copies constraints of the property $username of the class User.App\Entity\User::username() - copies constraints of the getters getUsername(), isUsername() and hasUsername().App\Entity\User::username - copies constraints of the property $username and its getters getUsername(), isUsername() and hasUsername().App\Entity\User - copies constraints applied to the entire class User.Note: If you target only getters, then prefixes must be omitted. For example, if you want to target getters of the class
Userwith the namesisChild()andhasChildren(), then the link would beApp\Entity\User::child().Only getters with the prefix
get,has, andiswill be searched.
Note: Linked constraints which work in a context (e.g. Expression or Callback) will NOT copy the context of the linked class, but instead will work in its own context. That means that the
thisvariable won't point to the linked class instance, but will point to an object of the classValidationNoderepresenting your input data. See the How does it work? section for more details about internal work of the validation process.
Suppose you have the following class:
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* [@Assert](https://github.com/Assert)\Callback({"App\Validation\PostValidator", "validate"})
*/
class Post
{
/**
* [@Assert](https://github.com/Assert)\NotBlank()
*/
private $title;
/**
* [@Assert](https://github.com/Assert)\Length(max=512)
*/
private $text;
/**
* [@Assert](https://github.com/Assert)\Length(min=5, max=10)
*/
public function getTitle(): string
{
return $this->title;
}
/**
* [@Assert](https://github.com/Assert)\EqualTo("Lorem Ipsum")
*/
public function hasTitle(): bool
{
return strlen($this->title) !== 0;
}
/**
* [@Assert](https://github.com/Assert)\Json()
*/
public function getText(): string
{
return $this->text;
}
}
Then you could link class members this way:
Mutation:
type: object
config:
fields:
editPost:
type: Post
resolve: "@=mutation('edit_post', [args])"
validation:
link: App\Entity\Post # targeting the class
args:
title:
type: String!
validation:
link: App\Entity\Post::title # property and getters
text:
type: String!
validation:
link: App\Entity\Post::$text # only property
or use the short form (omitting the link key), which is equal to the config above:
# ...
validation: App\Entity\Post # targeting the class
args:
title:
type: String!
validation: App\Entity\Post::title # property and getters
text:
type: String!
validation: App\Entity\Post::$text # only property
# ...
The argument title will get 3 assertions: NotBlank(), Length(min=5, max=10) and EqualTo("Lorem Ipsum"), whereas
the argument text will only get Length(max=512). The method validate of the class PostValidator will also be
called once, given an object representing the input data.
When linking constraints, keep in mind that the validation context won't be inherited (copied). For example, suppose you have the following Doctrine entity:
namespace App\Entity;
/**
* [@Assert](https://github.com/Assert)\Callback("validate")
*/
class User
{
public static function validate()
{
// ...
}
}
and this config:
Mutation:
type: object
config:
fields:
createUser:
validation: App\Entity\User # linking
resolve: "@=res('createUser', [args])"
# ...
Now, when you try to validate...
How can I help you explore Laravel packages today?