Validation
October 29, 2019 · View on GitHub
This bundle provides a tight integration with the Symfony Validator Component to validate user input data.
Contents:
- 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
Overview
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
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: 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
- email
- 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 birthday field is of type input-object and is marked as cascade so it's validation will happen according to the constraints declared in the Birthday type:
- day is between 1 and 31
- month is between 1 and 12
- year is between 1900 and 2019
The validation system ensures, that all arguments in your resolver are always valid. If validation fails, your resolver will never be called.
How does it work?
The Symfony Validator Component is designed to validate objects. For this reason this bundle creates temporary objects
for each of your GraphQL types during the validation process and populates them with the input data. Resulting objects
will repeat the nesting structure of your GraphQL schema. The object properties are created dynamically in runtime with
the same names as the corresponding args or fields, depending on GraphQL type (object and input-object
respectively). All newly created objects will be instances of the class ValidationNode
(see ValidationNode API). The resulting object composition will be then recursively validated,
starting from the root object down to it's children.
Please note, that the original arguments won't be altered in any way.
Let's take the example from the chapter Overview. When a user requests the register field, two following
objects will be created (for both GraphQL types):
If the
birthdayargument weren't marked ascascadeit would remain an array and we would have only 1 object.
Here is a more complex example to better demonstrate how the InputValidator creates objects from your GraphQL schema and embeds them in 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: ~
registerAdmin:
type: User
resolve: "@=mutation('registerAdmin', [args])"
args:
username:
type: String!
validation:
- Length: { min: 8 }
password:
type: String!
validation:
- Length: { min: 10 }
- IdenticalTo:
propertyPath: passwordRepeat
passwordRepeat:
type: String!
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 object compositions as shown in the UML diagrams below:
for the registerUser resolver:
Note that 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.
for the registerAdmin resolver:
Applying of validation constraints
If you are familiar with Symfony Validator Сomponent, then you might know that constraints can have different targets (class members or entire classes). Since each of your GraphQL types is represented by an object 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
constraintskey. - Link to an existing class with the
linkkey. - Delegate validation to a child type (input-object) with the
cascadekey.
All 3 methods can be mixed. If you use only 1 method you can omit the corresponding key (short form). Only definitions of type object and input-object can have validation rules.
Listing constraints explicitly
The most straightforward way to apply validation constraints to input data 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:
object:
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 constraints are applied to fields:
Mutation:
type: object
config:
fields:
updateUser:
type: User
resolve: "@=mutation('updateUser', [args])"
validation:
- Callback: [App\Validation\UserValidator, updateUser]
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
input-object:
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 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!
Linking to class constraints
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$usernameof 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$usernameand 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
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 it's own. That means that the
thisvariable won't point to the linked class instance, but will point to an object of the classValidationNoderepresenting your GraphQL type. See the How does it work? section for more details about internal work of the validation process.
Example:
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 current GraphQL type.
Context of linked constraints
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? each GraphQL type is represented by an object of class ValidationNode during the validation process.
Validation groups of linked constraints
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).
Cascade
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.
Example:
Mutation:
type: object
config:
fields:
updateUser:
type: Post
resolve: "@=mutation('update_user', [args])"
args:
id:
type: ID!
address:
type: Address
validation: cascade
workPeriod:
type: Period
validation: cascade
Address:
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: ~
Period:
type: input-object
config:
fields:
startDate:
type: String!
validation:
- Date: ~
endDate:
type: String!
validation:
- Date: ~
- GreaterThan:
propertyPath: 'startDate'
Groups
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: "@=('register_user')"
validationGroups: ['User']
args:
input:
type: UserInput!
validation: cascade
registerAdmin:
type: User
resolve: "@=('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 onstraints 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 againt '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.
Group Sequences
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']
# ...
Validating inside resolvers
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) {
// ...
}
Injecting errors
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'];
}
}
Error Messages
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.
Customizing the response
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.
Translations
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.
Using built-in expression functions
This bundle comes with pre-registered expression functions and variables. By default the Expression constraint has no access to them, because it uses the default instance of the ExpressionLanguage class. In order to tell the Expression constraint to use the instance of this bundle, add the following config to the services.yaml to rewrite the default service declaration:
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)"
Note Expressions in the
Expressionconstraint shouldn't be prefixed with@=.
and it's also possible to use 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
As you might, know the
Expressionconstraint has one built-in variable calledvalue. In order to avoid name conflicts, the resolver variablevalueis renamed toparentValue, when using in theExpressionconstraint.In short: the
valuerepresents currently validated input data, andparentValuerepresents the data returned by the parent resolver.
ValidationNode API
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.
Methods
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.
Examples
Usage in the Expression constraints:
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: "@=resolver('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"
# ...
Usage with Callback constraints:
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: "@=resolver('createUser', [args])"
args:
username: String!
password: String!
passwordRepeat: String!
email: String!
createAdmin:
type: User
resolve: "@=resolver('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
}
}
// ...
Limitations
Annotations and GraphQL Schema language
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.
Unsupported constraints
These are the validation constraints, which are not currently supported:
- File
- Image
- UniqueEntity
- Traverse - although you can use this constraint in your type definitions, it would make no sense, as nested objects will be automatically validated with the
Validconstraint. See How does it work? section to get familiar with the internals.