Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Graphql Bundle Laravel Package

overblog/graphql-bundle

View on GitHub
Deep Wiki
Context7

The validation feature was introduced in version 0.13

Validation

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.

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
                        
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 its 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

How does it work?

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:

enter_description

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 then be recursively validated, starting from the root object down to its children.

Note: Although it would have been possible to validate raw arguments, objects provide a better flexibility and more features.

Here is a more complex example to better demonstrate how the InputValidator creates objects from your GraphQL schema and embeds them into each other:

Mutation:
    type: object
    config:
        fields:
            registerUser:
                type: User
                resolve: "@=mutation('registerUser', [args])"
                args:
                    username:
                        type: String!
                        validation:
                            - App\Constraint\Latin: ~
                            - Length: { min: 5, max: 16 }
                    password:
                        type: String!
                        validation:
                            - App\Constraint\Latin: ~
                            - IdenticalTo:
                                propertyPath: passwordRepeat
                    passwordRepeat:
                        type: String!
                    emails:
                        type: "[String]"
                        validation:
                            - Unique: ~
                            - Count:
                                min: 1
                                max: 3
                            - All:
                                - Email: ~
                    birthday:
                        type: Birthday
                        validation: cascade
                    job:
                        type: Job
                        validation: cascade
                    address:
                        type: Address
                        validation:
                            - Collection:
                                fields:
                                    street:
                                        - App\Constraint\Latin: ~
                                        - Length: { min: 2, max: 64 }
                                    city:
                                        - App\Constraint\Latin: ~
                                        - Length: { min: 2, max: 64 }
                                    zip:
                                        - Positive: ~
                       
Job:
    type: input-object
    config:
        fields:
            position:
                type: String!
                validation:
                    - Choice: [developer, manager, designer]
            workPeriod:
                type: Period
                validation: cascade
            address:
                type: Address
                validation: cascade

Address:
    type: input-object
    config:
        fields:
            street:
                type: String!
                validation:
                    - App\Constraint\Latin: ~
                    - Length: { min: 2, max: 64 }
            city:
                type: String!
                validation:
                    - App\Constraint\Latin: ~
                    - Length: { min: 2, max: 64 }
            zip:
                type: Int!
                validation:
                    - Positive: ~                
                    
Period:
    type: input-object
    config:
        fields:
            startDate:
                type: String!
                validation:
                    - Date: ~
            endDate:
                type: String!
                validation:
                    - Date: ~
                    - GreaterThan:
                          propertyPath: 'startDate'	

Birthday:
    type: input-object
    config:
        fields:
            day:
                type: Int!
                validation:
                    - Range: { min: 1, max: 31 }
            month:
                type: Int!
                validation:
                    - Range: { min: 1, max: 12 }
            year:
                type: Int!	
                validation:
                    - Range: { min: 1900, max: today }

The configuration above would produce an object composition as shown in the UML diagram below:

enter image description here

Note: 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.

Applying validation constraints

If you are familiar with the Symfony Validator Component, you might know that constraints can have different targets (class members or entire classes). Since all input data is represented by objects during the validation, you can also declare member constraints as well as class constraints.

There are 3 different methods to apply validation constraints:

  • 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.

Listing constraints directly

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:

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-level constraints are applied to fields:

Mutation:
    type: object
    config:
        fields:
            updateUser:
                validation:
                    - Callback: [App\Validation\UserValidator, updateUser]
                type: User
                resolve: "@=mutation('updateUser', [args])"
                args:
                    username: String						
                    email: String
                    info: String

It's also possible to declare validation constraints for the entire type. This is useful if you don't want to repeat the configuration for each field or if you want to move the entire validation logic into a function:

Mutation:
    type: object
    config:
        validation:
            - Callback: [App\Validation\UserValidator, validate]
        fields:
            createUser:
                type: User
                resolve: "@=mutation('createUser', [args])"
                args:
                    username: String						
                    email: String
                    info: String
            updateUser:
                type: User
                resolve: "@=mutation('updateUser', [args])"
                args:
                    username: String						
                    email: String
                    info: String

which is equal to:

Mutation:
    type: object
    config:
        fields:
            createUser:
                validation:
                    - Callback: [App\Validation\UserValidator, validate]
                type: User
                resolve: "@=mutation('createUser', [args])"
                args:
                    username: String						
                    email: String
                    info: String
            updateUser:
                validation:
                    - Callback: [App\Validation\UserValidator, validate]
                type: User
                resolve: "@=mutation('updateUser', [args])"
                args:
                    username: String						
                    email: String
                    info: String

input-object:

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!

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 its 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 its 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 its own context. 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 input data. 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](https://github.com/Assert)\Callback({"App\Validation\PostValidator", "validate"})
 */
class Post 
{
    /**
     * [@Assert](https://github.com/Assert)\NotBlank()
     */
    private $title;
    
    /**
     * [@Assert](https://github.com/Assert)\Length(max=512)
     */
    private $text;
    
    /**
     * [@Assert](https://github.com/Assert)\Length(min=5, max=10)
     */
    public function getTitle(): string
    {
        return $this->title;
    }
    
    /**
     * [@Assert](https://github.com/Assert)\EqualTo("Lorem Ipsum")
     */
    public function hasTitle(): bool
    {
        return strlen($this->title) !== 0;
    }
    
    /**
     * [@Assert](https://github.com/Assert)\Json()
     */
    public function getText(): string
    {
        return $this->text;
    }
}

Then you could link class members this way:

Mutation:
    type: object
    config:
        fields:
            editPost:
                type: Post
                resolve: "@=mutation('edit_post', [args])"
                validation:
                    link: App\Entity\Post # targeting the class
                args:
                    title:
                        type: String!
                        validation:
                            link: App\Entity\Post::title # property and getters
                    text:
                        type: String!
                        validation:
                            link: App\Entity\Post::$text # only property

or use the short form (omitting the link key), which is equal to the config above:

 # ...
                    validation: App\Entity\Post # targeting the class
                    args:
                        title:
                            type: String!
                            validation: App\Entity\Post::title # property and getters
                        text:
                            type: String!
                            validation: App\Entity\Post::$text # only property
 # ...

The argument title will get 3 assertions: NotBlank(), Length(min=5, max=10) and EqualTo("Lorem Ipsum"), whereas the argument text will only get Length(max=512). The method validate of the class PostValidator will also be called once, given an object representing the input data.

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](https://github.com/Assert)\Callback("validate")
 */
class User 
{
    public static function validate() 
    {
        // ...
    }
}

and this config:

Mutation:
    type: object
    config:
        fields:
            createUser:
                validation: App\Entity\User # linking
                resolve: "@=res('createUser', [args])"
                # ...

Now, when you try to validate...

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
daikazu/eloquent-salesforce-objects
unseen-codes/chat
romalytar/yammi-jobs-monitoring-laravel
kisame76/filament-db-table-state
nqxcode/laravel-lucene-search
dpfx/laravel-livewire-wizards
workos/workos-php-laravel
sofa/laravel-global-scope
nawasara/auth-primitives
adhocrat-io/arkhe-main
make-dev/orca-harpoon
itsemon245/lamet
baks-dev/dashboard
amoifr/pickle-panther-bundle
make-dev/orca
dmstr/symfony-system-resources-bundle
dmstr/symfony-job-queue-bundle
dmstr/openapi-json-schema-bundle
dmstr/keycloak-security-bundle
dmstr/doctrine-audit-log-bundle