10000 [DX][Validation] Handling of Validation Groups · Issue #11880 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[DX][Validation] Handling of Validation Groups #11880

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
peterrehm opened this issue Sep 8, 2014 · 34 comments
Closed

[DX][Validation] Handling of Validation Groups #11880

peterrehm opened this issue Sep 8, 2014 · 34 comments

Comments

@peterrehm
Copy link
Contributor

When I first had the need to use validation groups I accidentally stumbled across the GroupSequenceProvider until I found what I actually wanted to do.

What I like about the GroupSequenceProvider is that you actually bind the validation logic to the entity configuration.

http://symfony.com/doc/current/book/validation.html#group-sequence

However with the validation groups it is not tied to the entity. I think a common use case is to validate based on the state of an entity. If you only validate your entities based on Forms you are unlikely to have any issues. However the entity validation logic is tied to the form configuration.

I would suggest an Approach with an Interface lets call it ValidationGroupsProdviderInterface with a method ValidationGroupsProdviderInterface::getValidationGroups(). The Form component could rely on this as well, the logic of the groups is tied to the entity and the risk of missing proper validation is reduced.

One concern which comes to my mind is the fact that the Validate method of the Validator accepts groups as parameters. And we also might travers through relations with the Valid() constraint. So the question would be when to use which groups.

What do you think about this?

@wouterj
Copy link
Member
wouterj commented Sep 9, 2014

I'm not sure if I can follow you. Validation groups are not tied to forms, you can specify the groups to validate based on the second argument.

When using forms, the validation is integrated in the Form component using a ValidationExtension. In this case, it's completely logic that you configure the validation groups using the Form configuration (the forms are "extended" with validation knowledge).

Or am I missing something? (btw, thanks for the feedback and ideas you are sharing with the community!)

@peterrehm
Copy link
Contributor Author

Well the logic to decide which validation_group based on the entity data is usually tied to the form configuration:

    'validation_groups' => function (FormInterface $form) {
        $entity = $form->getData();
        return $entity->getValidationGroups();
    }

So if the form executes the validation it takes care of the proper validation group if you manually validate you have to remember to call which can easily be forgotten.

    $validator->validate($entity, $entity->getValidationGroups);
8000

@wouterj
Copy link
Member
wouterj commented Sep 9, 2014

Validation groups should not be tied to your entity (as you showed in the first example). If you always want to execute validation with some predefined validation groups, it makes no sense to use groups at all (you have enough with using just the Default validation group).

Validation groups are a good idea in case you want to validate the entity differently in some cases. Thus it makes sense to provide the needed validation groups in the validate method instead of in the entity.

@peterrehm
Copy link
Contributor Author

Well the GroupSequences are also tied to the entity. And a common use case for validation groups is validation based on the entity state. If an entity is in a different state you need to apply other validation rules. And this is domain logic which should be tied to the entity.

I think you need to have a way to override this behavior, but it makes sense to keep it consistent.

@stof
Copy link
Member
stof commented Sep 10, 2014

@peterrehm The way I solved it in my project is to have a special constraint which will run the validator for additional validation groups based on the value of a field in the entity. See https://gist.github.com/stof/d21838b00d08858ae6bb for the code.
In this case, the form only runs the Default validation group, which contains constraints which should always be applied (including the ConditionalValidationGroup ones). Note that this snippet is a simplified one. It is possible to run several ConditionalValidationGroup constraint based on several fields (my Prize`` entity actually uses 2 such constraints)

@peterrehm
Copy link
Contributor Author

@stof Well this is what I mean. I see at least two options for a generic solution then.

  1. ValidationGroupsProdviderInterface

We could add an interface which requires you to add add a specific method as ValidationGroupsProdviderInterface::getValidationGroups() which has to provide all (including the default) validation group.

  1. ConditionalValidationGroup Constraint

Basically similar to your implementation with the following considerations:

  • You add only one constraint with an array of the fields or methods as value which
    provide validation groups. All the provided validation groups are merged together
    /**
     * @Assert\ConditionalValidationGroup(groupProviders={"type", "getValidationGroups"})
     */

(1) would allow you to change the entire groups even to avoid the default group if it makes sense, (2) should be straight forward and actually easy to implement.

What do you think about generalize such constraints into Symfony core? I think validation groups are a common use case where we should educate best practices and avoid a pitfall where the developer might forget to add the validation groups when customizing the validation groups.

@stof
Copy link
Member
stof commented Sep 20, 2014

@peterrehm I don't think it needs to support an array of group providers. It complicates the implementation uncessarily. Just use several constraints

@stof stof added the Validator label Sep 20, 2014
@peterrehm
Copy link
Contributor Author

I don't think the implementation would be harder to achieve. The advantage would be that you have to validate the object only once for the custom groups:

https://gist.github.com/stof/d21838b00d08858ae6bb#file-conditionalvalidationgroupvalidator-php-L33

@stof
Copy link
Member
stof commented Sep 20, 2014

@peterrehm if you pass several groups to validate(), it will simply loop over the groups, so this does not chnage anything.

@peterrehm
Copy link
Contributor Author

Oh okay. So indeed this should have no effect then. However I would find the array more intuitive.

@peterrehm
Copy link
Contributor Author

@stof What do you think about this in General? Shall I prepare an implementation?

@webmozart
Copy link
Contributor

I'd rather use a different approach, such as a DefaultGroup annotation which lets you specify either an explicit default group or which can be put onto a property/getter which returns the default group:

/**
 * @DefaultGroup("Strict")
 */
class Foo { ... }

/**
 * That's actually what we do with @GroupSequence right now:
 * @DefaultGroup(@GroupSequence({"Strict", "Bar"}))
 */
class Bar { ... }

class Baz
{
    /**
     * @DefaultGroup
     */
    private $type;
}

I haven't thought about this in detail yet though. This needs very careful consideration so that we don't break the concept of validation groups.

@webmozart
Copy link
Contributor

This would by the way allow us to get rid of the GroupSequenceProviderInterface:

class Foo
{
    /**
     * @DefaultGroup
     */
    public function getGroupSequence() { ... }
}

@webmozart
Copy link
Contributor

ref #5480

@peterrehm
Copy link
Contributor Author

@webmozart I hope that I'll be able to get back to you next week.

@peterrehm
Copy link
Contributor Author

I finally had the time to have a look at your comments and #5480.

I am not that sure about the name DefaultGroup since in fact we always have a default group without specifying it, why would we want to change that behavior? A generic way to support groupSequence or simple validation groups would be great. But as I understand those concepts either or has to be applied.

How about:

/**
 * Validation groups which will all be applied at the same time with static groups:
 * @ValidationGroups(groups = {"Strict", "Bar"})
 */
class Bar { ... }

/**
 * Validation groups which will all be applied at the same time with dynamic groups:
 * @ValidationGroups(groups = myGroupProperty)
 */
class Bar { ... }

/**
 * GroupSequence with static groups:
 * @ValidationGroups(groups = {"Strict", "Bar"}, groupSequence = true)
 */
class Bar { ... }

/**
 * GroupSequence with dynamic groups based on PropertyAccessor:
 * @ValidationGroups(groups = myGroupProperty, groupSequence = true)
 */
class Bar { ... }

I don't know if we would have to divide the options between static and dynamic
groups or if we detect this based on the following scheme:

  1. If Array assume strict groups
  2. Else apply property accessor and check if result is array error if property not found or no array

This however would habe no possibility to get the validation groups from different methods and merge them which is I think a rare use case and could be handled with a separate method by the developer easily. This makes only sense for validation groups however not for the group sequence since the order is important.

I think this generic solution or a simple constraint should be in place for 2.6 or 2.7 dependent on when #12237 would be merged. Also I would like to raise the question again if it would make sense to deprecate the validation_groups form config option in favor of this generic solution.

@webmozart
Copy link
Contributor

The reason why I think @DefaultGroup makes more sense than @ValidationGroups is that the used groups are usually provided when invoking the validator:

$validator->validate($object, null, 'MyGroup');
$validator->validate($object, null, new GroupSequence(array('Strict', 'Default')));

How would we decide when to apply the annotated groups? Currently, when a @GroupSequence annotation is defined on a class, that sequence is used whenever a class is validated in group "Default". The group "Default" becomes an alias of the group sequence:

/**
 * @GroupSequence({"Strict", "Foo"})
 */
class Foo { }

$validator->validate($foo);
$validator->validate($foo, null, 'Default');

Using @DefaultGroup would be a logical next step to extend this functionality. "Default" then becomes an alias of the group(s) passed to the constraint or returned by the constrained property/method.

/**
 * @DefaultGroup({"Foo", "Bar"})
 */
class Foo { }

$validator->validate($foo);
$validator->validate($foo, null, 'Default');

@peterrehm
Copy link
Contributor Author

I understand your considerations. So DefaultGroup would be overwritten in case I use the validation_groups option in the form or explicitly provide groups when calling the validate method.

In any case we would need to add the possibility to add dynamic groups.

/**
 * @DefaultGroup(@GroupProvider("validationGroups"))
 */
class Foo 
{
    public function validationGroups()
    {
        return array('Foo', 'Strict');
    }
}

Still the name DefaultGroup is not really logical to me, somehow not intuitive.
Maybe DefaultValidationGroup(s) might be better.

@peterrehm
< 8000 details class="details-overlay details-reset position-relative d-inline-block"> Copy link
Contributor Author

I also still have the validation_groups option in mind. Lets assume we validate a root form
which has a child entity with the valid constraint.

/**
 * @DefaultGroup({"Task", "StrictTask"})
 */
class Task
{
    /**
     * @Assert\Type(type="Acme\TaskBundle\Entity\Category")
     * @Assert\Valid()
     */
    protected $category;
}

With which validation groups will the Category be validated? With its
DefaultGroup even if I have a validation_group specified in TaskType?

@webmozart
Copy link
Contributor

In any case we would need to add the possibility to add dynamic groups.

class Foo 
{
    /**
     * @DefaultGroup
     */
    public function validationGroups()
    {
        return array('Foo', 'Strict');
    }
}

With which validation groups will the Category be validated?

In group "Default" again. That's the same right now with the @GroupSequence annotation.

@webmozart
Copy link
Contributor

even if I have a validation_group specified in TaskType?

The validator doesn't magically know about the "validation_groups" options in the Form component. What's happening in the back is simply:

$validator->validate($rootData, null, $rootValidationGroups);

The rest happens by cascade and using validation metadata.

@peterrehm
Copy link
Contributor Author

Okay I would prefer to have the annotation on the class and not on any method
for the purpose of visibility.

The validator doesn't magically know about the "validation_groups" options in the Form component. What's happening in the back is simply...

This means applied validation groups will be only respected on the main level or will only
the explicit specified ones be applied?

Would

$validator->validate($task, null, array('Task'));

Validate the Category also only with the group Task.

I can imagine a common use case is to validate the default group of the root entity and additional groups but you might want the child ones to be validated as well with the default validation groups.
This is actually a bit tricky since there could also be the request that you want to validate all with
only a specific group(s).

@webmozart
Copy link
Contributor

Validate the Category also only with the group Task.

Yes. We stuck with JSR-303's specified behavior here. I think there was some work done on "mapping" groups (e.g. "Task" -> "Category") when following a Valid constrained to a referenced object, but I haven't checked lately on their progress.

Anyway, "Task" is a bad example for a validation group, since a validation group should be domain specific and have semantic meaning (e.g. "Checkout"). If you name your validation groups properly, you can (and should) use the same validation group in multiple objects that are validated together in a business case.

// Validate everything in the graph in group "Checkout"
$validator->validate($order, null, 'Checkout');

@peterrehm
Copy link
Contributor Author

I was using Task as name since this reflects the Default validation group of Task.
This is why I was using that in my example. And to have it easy I have just copied
the class skeleton from the docs :)

$validator->validate($order, null, array('Task', 'TaskStrict'));

Since this would be the default group it cascades all child entities with the default group
for the child entities as well or is it keeping the group Task for the entity Category?

We could also quickly chat about that.

@webmozart
Copy link
Contributor

The cascaded group is always the one that you pass to validate(). If you pass "Default", then that is cascaded (independent of whether "Default" is overridden for an individual class). If you pass ["Task", "TaskStrict"], then that is cascaded.

@peterrehm
Copy link
< 6D40 /details>
Contributor Author

I will try to setup an test branch for that to play around with it. But it I have the entity Task with the Default Validation Group "Task" (By default the Entity Name) then I would assume

$validator->validate($task, null, array('Task'));

is equal to

$validator->validate($task);

Everything else I find confusing.

@webmozart
Copy link
Contributor

@peterrehm That depends. If you don't override group "Default", then "Default" == "Task". If you override "Default", then naturally that's not true anymore. Otherwise we'd have an endless recursion if we include group "Task" in the redefinition of "Default":

/** @DefaultGroup({"TaskComplete", "Task"}) */
class Task { ... }

@peterrehm
Copy link
Contributor Author

But with this

/** @DefaultGroup({"TaskComplete", "Task"}) */
class Task { ... }

I would expect that

$validator->validate($task, null, array('Task'));

validates the groups TaskComplete and Task and not only task.

Maybe rather than tying the default group to the class name we could think
about having a group Default which resolves to the DefaultGroup Setting which
I think should only be a class constraint.

@webmozart
Copy link
Contributor

The functionality we're talking about is not new, we already have it:

/** @GroupSequence({"Task", "Strict"}) */
class Task extends extends Activity { ... }
  • when validating in "Default", the group sequence is validated
  • when validating in "Task", the constraints defined in Task and Activity (without group, i.e. the "Default" group) are validated
  • when validating in "Activity", the constraints defined in the base class only (again, without group) are validated
  • when validating in "Strict", the constraints added to that group explicitly are validated

This is deterministic and was designed like that on purpose by the JSR-303 working group, because it let's you "ignore" the overridden default group if you need to, or validate constraints in base classes only (think polymorphism). We're not going to change this. I invite you to read the JSR-303 spec if you want to learn more about this.

@peterrehm
Copy link
Contributor Author

But my concern is you might have a complex Entity structure. Only then you are really affected.
Imagine 3 Levels of Entities.

Now you want to have specific validation on the root entity you have to be very cautious to list all
Class names to cover all default validation groups. In the discussion I mentioned once the tricky
part is to differentiate between the fact that you want to validate a entity with a specific group or
maybe more common you want to validate the entity in the default state but add a stricter validation
group depending on the model. Those are two different use cases.

@webmozart
Copy link
Contributor

We should take this somewhere else, as there seem to be some fundamental misunderstandings. GitHub is not a chat.

@peterrehm
Copy link
Contributor Author

I did some tests today and must admit there is a mistake I primarily got due to the
documentation. So if I have the class Task I will get all of the constraints without any
group validated if I validate with no group, Default or Task.

So having a group with the name Default makes it possible to cascade the default
validation to child entities. So we just need to teach then also that if you use the
Assert\Valid() constraint with validation groups you must be sure to validate against
the group Default on the root level.

And if we define a way so that actually the DefaultGroup can be resolved to multiple
or dynamic groups as well as GroupSequences I think we can cover all use cases and
we can ensure valid domain models.

@webmozart
Copy link
Contributor

I'm happy that was cleared up then :) Could you improve the documentation where you found it confusing? I guess others are having the same problem.

weaverryan referenced this issue in symfony/symfony-docs Jan 3, 2015
This PR was submitted for the master branch but it was merged into the 2.3 branch instead (closes #4454).

Discussion
----------

More concrete explanation of validation groups

| Q             | A
| ------------- | ---
| Doc fix?      | yes
| New docs?     | no
| Applies to    | all
| Fixed tickets | -

Improved the explanation of default validation groups in reference with my discussion with @webmozart in symfony/symfony#�11880.

Commits
-------

81728b6 More concrete explanation of validation groups
@peterrehm
Copy link
Contributor Author

Documentation has been improved, therefore I am closing this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants
0