Skip to content

Latest commit

 

History

History
1203 lines (1072 loc) · 44.5 KB

index.md

File metadata and controls

1203 lines (1072 loc) · 44.5 KB

Validation

This bundle provides a tight integration with the Symfony Validator Component to validate user input data.

Contents:

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):

enter_image_description_here

If the birthday argument weren't marked as cascade it 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:

enter image description here

Note that the argument address in the object Mutation wasn't converted into an object, as it doesn't have the key cascade, but it will still be validated against the Collection constraint as an array.

for the registerAdmin resolver:

enter image description here

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 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. 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 $username of the class User.
  • getters: App\Entity\User::username() - copies constraints of the getters getUsername(), isUsername() and hasUsername().
  • property and getters: App\Entity\User::username - copies constraints of the property $username and it's getters getUsername(), isUsername() and hasUsername().
  • class: 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 User with the names isChild() and hasChildren(), then the link would be App\Entity\User::child().

Only getters with the prefix get, has, and is 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 class ValidationNode representing 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 Expression constraint 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 Expression constraint has one built-in variable called value. In order to avoid name conflicts, the resolver variable value is renamed to parentValue, when using in the Expression constraint.

In short: the value represents currently validated input data, and parentValue represents 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 Valid constraint. See How does it work? section to get familiar with the internals.