The validation feature was introduced in the 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.
- Overview
- How does it work?
- Applying of validation constraints
- Groups
- Group Sequences
- Validating inside resolvers
- Injecting errors
- Error messages
- Translations
- Using built-in expression functions
- ValidationNode API
- Limitations
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:
- username
- has length between 6 and 32
- password
- has length between 8 and 32
- is equal to the passwordRepeat value
- emails
- every item in the collection is unique
- the number of items in the collection is between 1 and 3
- every item in the collection is a valid email address
The birthdate
field is of type input-object
and is marked as cascade
so it's validation will happen according to the constraints declared in the Birthdate
type:
- day is between 1 and 31
- month is between 1 and 12
- year is between 1900 and 2019
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:
- convert the subtype (
DateInput
) into an object. - embed the resulting object into its parent, making it a sub-object.
- apply to it the
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 be then recursively validated, starting from the root object down to it's 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
address
in the objectMutation
wasn't converted into an object, as it doesn't have the keycascade
, but it will still be validated against theCollection
constraint as an array.
If you are familiar with Symfony Validator Сomponent, then 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:
- List them directly in the type definitions with the
constraints
key. - Link to an existing class with the
link
key. - Delegate validation to a child type (input-object) with the
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 to 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:
- property:
<ClassName>::$<propertyName>
- the$
symbol indicates a single class property. - getters:
<ClassName>::<propertyName>()
- the parentheses indicate all getters of the given property name. - property and getters:
<ClassName>::<propertyName>
- the absence of the$
and parentheses indicates a single property and all it's getters. - class:
<ClassName>
- the absence of a class member indicates an entire class.
for example:
- property:
App\Entity\User::$username
- copies constraints of the property$username
of the classUser
. - getters:
App\Entity\User::username()
- copies constraints of the gettersgetUsername()
,isUsername()
andhasUsername()
. - property and getters:
App\Entity\User::username
- copies constraints of the property$username
and it's gettersgetUsername()
,isUsername()
andhasUsername()
. - class:
App\Entity\User
- copies constraints applied to the entire classUser
.
Note: If you target only getters, then prefixes must be omitted. For example, if you want to target getters of the class
User
with the namesisChild()
andhasChildren()
, then the link would beApp\Entity\User::child()
.Only getters with the prefix
get
,has
, andis
will 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 it's own. That means that the
this
variable won't point to the linked class instance, but will point to an object of the classValidationNode
representing 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\Callback({"App\Validation\PostValidator", "validate"})
*/
class Post
{
/**
* @Assert\NotBlank()
*/
private $title;
/**
* @Assert\Length(max=512)
*/
private $text;
/**
* @Assert\Length(min=5, max=10)
*/
public function getTitle(): string
{
return $this->title;
}
/**
* @Assert\EqualTo("Lorem Ipsum")
*/
public function hasTitle(): bool
{
return strlen($this->title) !== 0;
}
/**
* @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\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 the arguments in your resolver, it will throw an exception, because it will try to call a
method with the name validate
on the object of class ValidationNode
, which doesn't have such. As explained in the
section How does it work? all input data is represented objects of class ValidationNode
during
the validation process.
Linked constraints will be used as it is. This means that it's not possible to change any of their params including
groups. For example, if you link a property on class User
, then all copied constraints will be in the groups
Default
and User
(unless other groups declared explicitly in the linked class itself).
The validation of arguments of the type input-object
, which are marked as cascade
, will be delegated to the embedded
type. The nesting can be any depth.
Mutation:
type: object
config:
fields:
updateUser:
type: Post
resolve: "@=mutation('update_user', [args])"
args:
id:
type: ID!
address:
type: AddressInput
validation: cascade # delegate to AddressInput
workPeriod:
type: PeriodInput
validation: cascade # delegate to PeriodInput
AddressInput:
type: input-object
config:
fields:
street:
type: String!
validation:
- Length: { min: 5, max: 15 }
city:
type: String!
validation:
- Choice: ['Berlin', 'New York', 'Moscow']
house:
type: Int!
validation:
- Positive: ~
PeriodInput:
type: input-object
config:
fields:
startDate:
type: String!
validation:
- Date: ~
endDate:
type: String!
validation:
- Date: ~
- GreaterThan:
propertyPath: 'startDate'
It is possible to organize constraints into validation groups.
By default, if you don't declare groups explicitly, every constraint of your type will be in 2 groups: Default and
the name of the type. For example, if the type's name is Mutation and the declaration of constraint is NotBlank: ~
(no explicit groups declared), then it automatically falls into 2 default groups: Default and Mutation. These
default groups will be removed, if you declare groups explicitly. Follow the
link for more details about validation groups in the Symfony
Validator Component.
Validation groups could be useful if you use a same input-object
type in different contexts and want it to be
validated differently (with different groups). Take a look at the following example:
Mutation:
type: object
config:
fields:
registerUser:
type: User
resolve: "@=mut('register_user')"
validationGroups: ['User']
args:
input:
type: UserInput!
validation: cascade
registerAdmin:
type: User
resolve: "@=mut('register_admin')"
validationGroups: ['Admin']
args:
input:
type: UserInput!
validation: cascade
UserInput:
type: input-object
config:
fields:
username:
type: String!
validation:
- Length: {min: 3, max: 15}
password:
type: String
validation:
- Length: {min: 4, max: 32, groups: 'User'}
- Length: {min: 10, max: 32, groups: 'Admin'}
As you can see the password
field of the UserInput
type has a same constraint applied to it twice, but with
different groups. The validationGroups
option ensures that validation will only use the constraints that are listed
in it.
In case you inject the validator into the resolver (as described here), the validationGroups
option will be ignored. Instead you should pass groups directly to the injected validator. This approach could be
necessary in some few cases.
Let's take the example from the chapter Overview and edit the configuration to inject the validator
and
to use validation groups:
# config\graphql\types\Mutation.yaml
Mutation:
type: object
config:
fields:
register:
type: User
resolve: "@=mut('register', [args, validator])" # injecting validator
args:
username:
type: String!
validation:
- Length:
min: 6
max: 32
groups: ['registration']
password:
type: String!
validation:
- Length:
min: 8
max: 32
- IdenticalTo:
propertyPath: passwordRepeat
groups: ['registration']
passwordRepeat:
type: String!
emails:
type: "[String]"
validation:
- Unique: ~
- Count:
min: 1
max: 3
- All:
- Email: ~
birthday:
type: Birthday
validation: cascade
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 }
Here we injected the validator
variable into the register
resolver. By doing so we are turning the automatic
validation off to perform it inside the resolver (see Validating inside resolvers). The
injected instance of the InputValidator
class could be used in a resolver as follows:
namespace App\GraphQL\Mutation\Mutation
use Overblog\GraphQLBundle\Definition\Argument;
use Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface;
use Overblog\GraphQLBundle\Definition\Resolver\MutationInterface;
use Overblog\GraphQLBundle\Validator\InputValidator;
class UserResolver implements MutationInterface, AliasedInterface
{
public function register(Argument $args, InputValidator $validator)
{
/*
* Validates:
* - username against 'Length'
* - password against 'IdenticalTo'
*/
$validator->validate('registration');
/*
* Validates:
* - password against 'Length'
* - emails against 'Unique', 'Count' and 'All'
* - birthday against 'Valid' (cascade).
* - day against 'Range'
* - month against 'Range'
* - year against 'Range'
*/
$validator->validate('Default');
// ... which is in this case equal to:
$validator->validate();
/**
* Validates only arguments in the 'Birthday' type
* against constraints with no explicit groups.
*/
$validator->validate('Birthdate');
// Validates all arguments in each type against all constraints.
$validator->validate(['registration', 'Default']);
// ... which is in this case equal to:
$validator->validate(['registration', 'Mutation', 'Birthdate']);
}
public static function getAliases(): array
{
return ['register' => 'register'];
}
}
Note: All arguments marked for cascade validation will be automatically validated against the Valid constraint.
You can use GroupSequence
constraint to sequentially apply validation groups. See the official documentation for more details.
Applying group sequences is similar to normal constraints:
Mutation:
type: object
config:
validation:
- GroupSequence: ['group1', 'group2']
fields:
create:
# ...
update:
# ...
or for each field:
Mutation:
type: object
config:
fields:
create:
validation:
- GroupSequence: ['group1', 'group2']
# ...
update:
validation:
- GroupSequence: ['group3', 'group4']
# ...
You can turn the auto-validation off by injecting the validator into your resolver. This can be useful if you want to do something before the actual validation happens or customize other aspects, for example validate data multiple times with different groups or make the validation conditional.
Here is how you can inject the validator:
Mutation:
type: object
config:
fields:
register:
resolve: "@=mutation('register', [args, validator])"
# ...
resolver:
namespace App\GraphQL\Mutation\Mutation
use Overblog\GraphQLBundle\Definition\Argument;
use Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface;
use Overblog\GraphQLBundle\Definition\Resolver\MutationInterface;
use Overblog\GraphQLBundle\Validator\InputValidator;
class UserResolver implements MutationInterface, AliasedInterface
{
public function register(Argument $args, InputValidator $validator): User
{
// This line executes a validation process and throws ArgumentsValidationException
// on fail. The client will then get a well formatted error message.
$validator->validate();
// To validate with groups just pass a string or an array
$validator->validate(['my_group', 'group2']);
// Or use a short syntax, which is equal to $validator->validate().
// This is possible thanks to the __invoke magic method.
$validator();
// The code below won't be reached if one of the validations above fails
$user = $this->userManager->createUser($args);
$this->userManager->save($user);
return $user;
}
public static function getAliases(): array
{
return ['register' => 'register'];
}
}
If you want to prevent the validator to automatically throw an exception just pass false
as the second argument. It
will return an instance of the ConstraintViolationList
class instead:
$errors = $validator->validate('my_group', false);
// Do something with errors
if ($errors->count() > 0) {
// ...
}
It's possible to inject the errors
variable with all validation violations instead of automatic exception throw:
Mutation:
type: object
config:
fields:
register:
resolve: "@=mutation('register', [args, errors])"
# ...
namespace App\GraphQL\Mutation\Mutation
use Overblog\GraphQLBundle\Definition\Argument;
use Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface;
use Overblog\GraphQLBundle\Definition\Resolver\MutationInterface;
use Overblog\GraphQLBundle\Error\ResolveErrors;
class UserResolver implements MutationInterface, AliasedInterface
{
public function register(Argument $args, ResolveErrors $errors): User
{
$violations = $errors->getValidationErrors();
// ...
}
public static function getAliases(): array
{
return ['register' => 'register'];
}
}
By default the InputValidator
throws an ArgumentsValidationException
, which will be caught and serialized into
a readable response. The GraphQL specification defines a
certain shape of all errors returned in the response. According to it all validation violations are to be found under
the path errors[index].extensions.validation
of the response object.
Example of a response with validation errors:
{
"data": null,
"errors": [{
"message": "validation",
"extensions": {
"category": "arguments_validation_error",
"validation": {
"username": [
{
"message": "This value should be equal to 'Lorem Ipsum'.",
"code": "478618a7-95ba-473d-9101-cabd45e49115"
}
],
"email": [
{
"message": "This value is not a valid email address.",
"code": "bd79c0ab-ddba-46cc-a703-a7a4b08de310"
},
{
"message": "This value is too short. It should have 5 character or more.",
"code": "9ff3fdc4-b214-49db-8718-39c315e33d45"
}
]
}
},
"locations": [
{"line": 3, "column": 17}
],
"path": ["linkedConstraintsValidation"]
}]
}
The codes in the response could be used to perform a client-side translation of the validation violations.
You can customize the output by passing false
as a second argument to the validate
method.
This will prevent an exception to be thrown and a ConstraintViolationList
object will be returned instead:
public function resolver(InputValidator $validator)
{
$errors = $validator->validate(null, false);
// Use $errors to build your own exception
...
}
See more about Error handling.
All validation violations are automatically translated from the validators
domain.
Example:
Mutation:
type: object
config:
fields:
register:
type: User
resolve: "@=mutation('register', [args])"
args:
username:
type: String!
validation:
- Length:
min: 6
max: 32
minMessage: "register.username.length.min"
maxMessage: "register.username.length.max"
password:
type: String!
validation:
- Length:
min: 8
max: 32
minMessage: "register.password.length.min"
maxMessage: "register.password.length.max"
- IdenticalTo:
propertyPath: passwordRepeat
message: "register.password.identical"
passwordRepeat:
type: String!
Create a translation resource for the validators
domain:
# translations\validators.en.yaml
register.username.length.min: "The username should have {{ length }} characters or more"
register.username.length.max: "The username should have {{ length }} characters or less"
register.password.length.min: "The password should have {{ length }} characters or more"
register.password:length.max: "The password should have {{ length }} characters or less"
register.password.identical: "The passwords are not equal."
or use another format, which is more readable:
# translations\validators.en.yaml
register:
username:
length:
min: "The username should have {{ length }} characters or more"
max: "The username should have {{ length }} characters or less"
password:
identical: "The passwords are not equal."
length:
min: "The password should have {{ length }} characters or more"
max: "The password should have {{ length }} characters or less"
To translate into other languages just create additional translation resource with a required suffix, for example validators.de.yaml
for German and validators.ru.yaml
for Russian.
This bundle comes with built-in ExpressionLanguage
instance and pre-registered expression functions and variables.
By default the Expression
constraint in your project has no access to these functions and variables, because it uses the default instance of the
ExpressionLanguage
class. In order to tell the Expression
constraint to use the instance of this bundle, you need
to rewrite its service declaration. Add the following config to the services.yaml
:
validator.expression:
class: Overblog\GraphQLBundle\Validator\Constraints\ExpressionValidator
arguments: ['@Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage']
tags:
- name: validator.constraint_validator
alias: validator.expression
This will make possible to use all functions, registered in this bundle:
# ...
args:
username:
type: String!
validation:
- Expression: "service('my_service').entityExists(value)"
as well as variables from the resolver context (value
, args
, context
and info
):
# ...
args:
username:
type: String!
validation:
- Expression: "service('my_service').isValid(value, args, info, context, parentValue)"
Note:
Expressions in the
Expression
constraint should NOT be prefixed with@=
. As you might know, theExpression
constraint has one built-in variable calledvalue
. In order to avoid name conflicts, the resolver variablevalue
is renamed toparentValue
, when using in theExpression
constraint.In short: the
value
represents currently validated input data, andparentValue
represents the data returned by the parent resolver.
The ValidationNode class is used internally during the validation process. See the How does it work? section for more details.
This class has methods that may be useful when using such constraints as Callback
or Expression
, which work in a context.
getType(): GraphQL\Type\Definition\Type
Returns the Type
object associated with current validation node.
getName(): string
Returns the name of the associated Type object. Shorthand for getType()->name
.
getFieldName(): string|null
Returns the field name if the object is associated with an object
type, otherwise returns null
getParent(): ValidationNode|null
Returns the parent node.
findParent(string $name): ValidationNode|null
Traverses up through parent nodes and returns first object with matching name.
In this example we are checking if the value of the field shownEmail
is contained in the emails
array. We are using the method getParent()
to access a field of the type Mutation
from within the type Profile
:
Mutation:
type: object
config:
fields:
registerUser:
type: User
resolve: "@=query('register_user', args)"
args:
username: String!
password: String!
passwordRepeat: String!
emails:
type: "[String]"
validation:
- Unique: ~
- Count:
min: 1
max: 5
- All:
- Email: ~
profile:
type: Profile
validation: cascade
Profile:
type: input-object
config:
fields:
shownEmail:
type: String!
validation:
- Expression: "value in this.getParent().emails"
# ...
In this example we are applying a same validation constraint to both createUser
and createAdmin
resolvers.
Mutation:
type: object
config:
validation:
- Callback: [App\Validation\Validator, validate]
fields:
createUser:
type: User
resolve: "@=query('createUser', args)"
args:
username: String!
password: String!
passwordRepeat: String!
email: String!
createAdmin:
type: User
resolve: "@=query('createAdmin', args)"
args:
username: String!
password: String!
passwordRepeat: String!
email: String!
To find out which of 2 fields is being validated inside the method, we can use method getFieldName
:
namespace App\Validation;
use Overblog\GraphQLBundle\Validator\ValidationNode;
// ...
public static function validate(ValidationNode $object, ExecutionContextInterface $context, $payload): void
{
switch ($object->getFieldName()) {
case 'createUser':
// Validation logic for users
break;
case 'createAdmin':
// Validation logic for admins
break;
default:
// Validation logic for all other fields
}
}
// ...
The current implementation of InputValidator
works only for schema types declared in yaml files. Types declared with annotations or with GraphQL schema language are not supported. This can be changed in the future versions.
The annotations system of this bundle has its own limited validation implementation, see the Arguments Transformer section for more details.
These are the validation constraints, which are not currently supported or have no effect to the validation:
- File - not supported (under development)
- Image - not supported (under development)
- UniqueEntity
- Traverse - although you can use this constraint,
it would make no sense, as nested objects will be automatically validated with the
Valid
constraint. See How does it work? section to get familiar with the internals.