| Q | A
| ---------------- | -----
| Bug report? | no
| Feature request? | yes
| BC Break report? | no
| RFC? | no
| Version/Branch | 0.13
After using of the validation feature in couple of my projects I noticed some repeating, which can and should be automated with an option to switch it off.
Currently to add validation a user have to inject the validator variable into the resolve expression and have to add validation rules to every arg/field he wishes to validate, e.g.:
Mutation:
type: object
config:
fields:
createUser:
type: String!
resolve: "@=res('my_resolver', [validator, args])"
args:
username:
type: String!
validation:
- Length: { min: 1, max: 10 }
and the resolver:
public function myResolver(InputValidator $validator, ArgumentInterface $args)
{
$validator->validate(); # this line is repeated in every resolver
// ...
}
When a user adds validation rules to a GraphQL type this already shows his intention to validate the input data, so there is no need to additionally inject the validator variable only to call its validate() method. The only exceptions are when users want to validate with specific validation groups or if they want to customize the output of errors:
public function myResolver(InputValidator $validator, ArgumentInterface $args)
{
// first argument is a validation group.
// second argument is a flag to prevent throwing an
// error. Instead it will return an error object.
$errors = $validator->validate('registration', false);
// ...
}
1) Make validation automatic by default. That means the only thing a user have to define is the validation key on a GraphQL type.
2) Introduce new expression variable errors which will contain all validation errors. It's required if a user don't want to automatically throw validation errors and want to customize the output. The variable is not only for validation errors, but can be used for other future features, for example it could contain transformation errors occured in the Hydrator.
3) Add option to blacklist or whitelist some types from the automatic validation. This would be usefull if there are 50 types and only 48 of them need to be validated autimatically (or vice versa)
First of all we need to introduce a new option into the main config:
overblog_graphql:
# global settings vor validation
validation:
auto: true # true by default. Turns on auto validation for all types
If a user wants auto validation only for certain types:
overblog_graphql:
validation:
auto: [User, Post, Book]
If a user wants to validate all types, but exclude few of them:
overblog_graphql:
validation:
auto:
exclude: [User, Post, Book]
Mutation:
type: object
config:
fields:
username:
type: String!
resolve: "@=res('my_resolver', [args])" # no validator injected
args:
username:
type: String!
validation: # but constraints are defined
- Length: { min: 1, max: 10 }
```php
public function myResolver(ArgumentInterface $args)
{
# if we are here, then validation is passed
}
If we want to customize the output of errors, we just inject the `errors` variable:
```yaml
Mutation:
type: object
config:
fields:
username:
type: String!
resolve: "@=res('my_resolver', [args, errors])"
args:
username:
type: String!
validation:
- Length: { min: 1, max: 10 }
By injecting it we automatically prevent the Validator to throw an exception.
public function myResolver(ArgumentInterface $args, Errors $errors)
{
$violations = $error->getValidationErrors();
# Later we could add
$hydrationErrors = $error->getHydrationErrors();
}
The only opened question is: how would users perform validation with groups? Should they blacklist types they want to validate with groups, inject the validator manually and call:
$validator->validate('my_group');
Any other ideas?
If we agree on this, then I am going to implement it.
I like this idea, I have some comments:
Add option to blacklist or whitelist some types from the automatic validation. This would be usefull if there are 50 types and only 48 of them need to be validated autimatically (or vice versa)
I will subject to use a more flexible syntax:
overblog_graphql:
validation:
auto: [User.*, !UserRoles, !Blog, !.*Settings]
in above example all types matching regex User.* pattern will be auto validate (UserEmails, UserBlog) except UserRoles, Blog and all types with Settings suffix.
we can add 2 shortcuts:
overblog_graphql:
validation:
auto: true # equivalent [.*]
overblog_graphql:
validation:
auto: false # equivalent [!.*]
also we can add an option validation.auto as a boolean directly on types so the above option can be cached and used directly in compiled types.
```yaml
MyType:
type: object
validation:
auto: false
> The only opened question is: how would users perform validation with groups?
we can add a option `validation.groups` on types:
```yaml
MyType:
type: object
validation:
groups:
# we auto choosing group depending on the type parent name
'my_group1': ["MyParentType1", "MyParentType2"]
'my_group2': ["*"] # wildcard
We could add a more complex way but this could be a good start.
@murtukov
I currently don't use this feature (have a different hydration/validation concept implemented), but IIRC you had the option to reuse validation mapping. If I recall it correct: Would validation groups not only make sense when reusing validation mapping? Because when you define it inline, like in your example, you basically decide what to validate the moment you add the validation mapping in the type definition. But if you reuse stuff and your mutation field does partial update and only need a subset of fields and validation, then it make sense.
What groups you want to validate will then depends on the field definition and is specific to this (mutation) field. Therefore the groups should be addable where you link/reuse validation mapping in the types definition.
@mcg-web
The last example I guess MyType was assumed an input-object? If not, can you explain why validation should be on an object, if yes, the following question:
For an input type, what would be the parent type name? Either nothing or if it is part of another input-object type, then the type name of this parent.
But would it make sense to control groups from this child? Its an upside-down relation established. Also why would an input include another and not validate it? If an input part is optional, should the parent type's validation not simply allow a null value? If its not nullable from GraphQL perspective, why would you not validate the expected value?
I think for inline mapping in the typing, groups do not make sense or I see not a use case because I do things maybe differently and have no need for it, then you should do what you need for your use case ^^. For linked mapping, it would make sense, but the used group should be specified on the field that accepts the args to be validated.
@mcg-web Thanx, I like the wildcards in the auto validation configs and validation groups depending on parents.
The only thing I would like to note is if you want to turn the auto validation off for a certain type, you don't have to define it explicitly in the type itself like you suggested:
MyType:
type: object
validation: # this could be inserted automatically in the generation time
auto: false
fields:
username:
type: String!
resolve: "@=res('my_resolver', [validator])"
because there is already the validator variable injected into the resolver.
There are 2 main reasons why a user would turn the auto validaton off for certain types:
1) He want's to do validation manually
In this case he just injects the validator variable. This is enough to turn the auto validation off.
2) He don't want to validate the type at all.
In this case he just doesn't declare any constraints on the type and don't inject the variable.
And I want also to ask you isn't it possible to cache the types from the wildcard from the global config?
overblog_graphql:
validation:
auto: [User.*, !UserRoles, !Blog, !.*Settings]
It could be done in the generation time, the wildcards will be read and the validation.auto: false will be automatically inserted in the generated type.
@akomm
Input Types can be embedded multiple times in different fields at the same time, therefore they can have different validation contexts. Take a look at this example:
Mutation:
type: object
config:
fields:
registerUser:
type: User
resolve: "@=('register_user', [validator])."
args:
input:
type: UserInput!
validation: cascade
registerAdmin:
type: User
resolve: "@=('register_admin', [validator])."
args:
input:
type: UserInput!
validation: cascade
UserInput:
type: input-object
config:
fields:
username:
type: String!
validation:
- Length: {min: 3, max: 15} # this constraint is in the Default group
password:
type: String
validation:
- Length: {min: 4, max: 32, groups: 'User'}
- Length: {min: 10, max: 32, groups: 'Admin'}
public function registerUser($validator)
{
$validator->validate(['Default', 'User']);
}
public function registerAdmin($validator)
{
$validator->validate(['Default', 'Admin']);
}
So when validating normal users the password is required to have minimum 4 chars and when validating admins the password is required to be 10 chars minimum
@mcg-web Actually @akomm is right about this piece:
MyType:
type: object
validation:
groups:
# we auto choosing group depending on the type parent name
'my_group1': ["MyParentType1", "MyParentType2"]
'my_group2': ["*"] # wildcard
When you call $validator->validate() you pass a group to the method and it will validate all types in this group.
So actually the groups should be defined in the parent type and NOT in the children. Let's take the example from above and change it to use auto validation:
Mutation:
type: object
config:
fields:
registerUser:
type: User
resolve: "@=('register_user')"
validationGroups: ['User'] # only for auto validation
args:
input:
type: UserInput!
validation: cascade
registerAdmin:
type: User
resolve: "@=('register_admin')"
validationGroups: ['Admin'] # only for auto validation
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'}
This is similar to validation_groups option in Symfony Forms
@murtukov That is exactly what I meant (both the part addressed to you and mcg-web).
You decided what you want to use from the input in the use-context and this is the field you accept the args in. :) It does not matter in this case, if its vial link or like in your example when you use the same input type in two places without link.
@mcg-web @akomm An update.
I think we don't even need this:
overblog_graphql:
validation:
auto: false
validator variable and does it manually like in the docs.We already found a solution for the problem with groups: it could be done with validationGroups in the types.
We already found a solution for the problem with the case when a user doesn't want errors to be automatically thrown: he just injects the error variable.
Do we agree on that?
Sounds good to me
Look good to me :+1:
Most helpful comment
@mcg-web @akomm An update.
I think we don't even need this:
validatorvariable and does it manually like in the docs.We already found a solution for the problem with groups: it could be done with
validationGroupsin the types.We already found a solution for the problem with the case when a user doesn't want errors to be automatically thrown: he just injects the
errorvariable.Do we agree on that?