Skip to content

[Validator] Dynamic constraints #3114

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

Merged
merged 4 commits into from
Jan 22, 2012
Merged

Conversation

sebhoerl
Copy link
Contributor

Bug fix: no
Feature addition: yes
Backwards compatibility break: no
Symfony2 tests pass: yes

By now the Validator component is based on a per-class configuration of
constraints, but in some cases it might be neccessary to add new constraints
dynamically at runtime.
This pull request adds a "ConstraintProviderInterface" to the Validator component. If an object is validated that implements this interface the method "getConstraints" is used to add dynamic constraints:

class User implements ConstraintProviderInterface
{
    protected $isPremium;
    protected $paymentInformation;

    public function getConstraints(ClassMetadata $metadata)
    {
        if ($this->isPremium) {
            $metadata->addPropertyConstraint('paymentInformation', new NotBlank());
        }
    }
}

*
* @param Mapping\ClassMetadata
*/
public function getConstraints(ClassMetadata $metadata);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GETConstraints? This function does not seem to return anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right. Maybe something like "buildConstraints" would be better, but I'm not sure if it is the right name. What's your suggestion?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the coding standard would state that you do not need public for interfaces.

@alexandresalome
Copy link

Related to #1151

@canni
Copy link
Contributor

canni commented Jan 16, 2012

👍

@webmozart
Copy link
Contributor

I think this is a good addition. I think we still have a naming problem though. When constraints are loaded using a static method, the default name for the loader method is loadValidatorMetadata. Since the method for dynamic constraint loading is basically the same, I think the two names should be related.

Solution (1): Rename the method in your interface to loadDynamicValidatorMetadata. Ugly and long.

class MyClass implements ConstraintProviderInterface
{
    public static loadValidatorMetadata(ClassMetadata $metadata) ...

    public loadDynamicValidatorMetadata(ClassMetadata $metadata) ...
}

Solution (2): Rename the default method name in StaticMethodLoader to registerConstraints and adjust the docs. Breaks BC.

class MyClass implements ConstraintProviderInterface
{
    public static registerConstraints(ClassMetadata $metadata) ...

    public registerDynamicConstraints(ClassMetadata $metadata) ...
}

@fabpot: Are we allowed to break BC here? If not, we should probably stick to (1).

@fabpot
Copy link
Member

fabpot commented Jan 16, 2012

I would prefer to not break BC if possible.

@sebhoerl
Copy link
Contributor Author

So "loadDynamicValidatorMetadata" would be the best solution?

@althaus
Copy link
Contributor

althaus commented Jan 17, 2012

So "loadDynamicValidatorMetadata" would be the best solution?

Sounds fine for me based on @bschussek's comment.

fabpot added a commit that referenced this pull request Jan 22, 2012
Commits
-------

92f820a Renamed registerConstraints to loadDynamicValidatorMetadata
dd12ff8 CS fix, getConstraints renamed
09c1911 [Validator] Improved dynamic constraints
54cb6e4 [Validator] Added dynamic constraints

Discussion
----------

[Validator] Dynamic constraints

Bug fix: no
Feature addition: yes
Backwards compatibility break: no
Symfony2 tests pass: yes

By now the Validator component is based on a per-class configuration of
constraints, but in some cases it might be neccessary to add new constraints
dynamically at runtime.
This pull request adds a "ConstraintProviderInterface" to the Validator component. If an object is validated that implements this interface the method "getConstraints" is used to add dynamic constraints:

    class User implements ConstraintProviderInterface
    {
        protected $isPremium;
        protected $paymentInformation;

        public function getConstraints(ClassMetadata $metadata)
        {
            if ($this->isPremium) {
                $metadata->addPropertyConstraint('paymentInformation', new NotBlank());
            }
        }
    }

---------------------------------------------------------------------------

by alexandresalome at 2012-01-15T11:20:04Z

Related to #1151

---------------------------------------------------------------------------

by canni at 2012-01-16T09:22:28Z

:+1:

---------------------------------------------------------------------------

by bschussek at 2012-01-16T12:32:44Z

I think this is a good addition. I think we still have a naming problem though. When constraints are loaded using a static method, the default name for the loader method is `loadValidatorMetadata`. Since the method for dynamic constraint loading is basically the same, I think the two names should be related.

Solution (1): Rename the method in your interface to `loadDynamicValidatorMetadata`. Ugly and long.

    class MyClass implements ConstraintProviderInterface
    {
        public static loadValidatorMetadata(ClassMetadata $metadata) ...

        public loadDynamicValidatorMetadata(ClassMetadata $metadata) ...
    }

Solution (2): Rename the default method name in `StaticMethodLoader` to `registerConstraints` and adjust the docs. Breaks BC.

    class MyClass implements ConstraintProviderInterface
    {
        public static registerConstraints(ClassMetadata $metadata) ...

        public registerDynamicConstraints(ClassMetadata $metadata) ...
    }

@fabpot: Are we allowed to break BC here? If not, we should probably stick to (1).

---------------------------------------------------------------------------

by fabpot at 2012-01-16T12:36:14Z

I would prefer to not break BC if possible.

---------------------------------------------------------------------------

by blogsh at 2012-01-16T15:25:46Z

So "loadDynamicValidatorMetadata" would be the best solution?

---------------------------------------------------------------------------

by althaus at 2012-01-17T13:39:19Z

>So "loadDynamicValidatorMetadata" would be the best solution?

Sounds fine for me based on @bschussek's comment.
@fabpot fabpot merged commit 92f820a into symfony:master Jan 22, 2012
@webmozart
Copy link
Contributor

I'm sorry to be late to the party, but I fear I have to put my veto on this PR. First of all, the implementation is flawed. Dynamic constraints should not be loaded in the Validator, but in the GraphWalker. Second, this will circumvent any caching (of course) and I would like to see if a solution like in Hibernate 4.2 wouldn't fit better to solve this use case.

@sebhoerl
Copy link
Contributor Author

I could move the code of this PR into the GraphWalker.
However the approach in your link looks interesting. Basically it would remove the need to use the interface and let the user configure the dynamic constraint provider in the configuration files. I could implement an alternative PR using this approach.

@fabpot
Copy link
Member

fabpot commented Jan 22, 2012

I've reverted the merge.

fabpot added a commit that referenced this pull request Jan 22, 2012
This reverts commit 6b9a355, reversing
changes made to 811ead8.
@webmozart
Copy link
Contributor

@blogsh: Feel free to do so! I'm not sure whether it's good to port it to PHP exactly as it is in Java though. Maybe it makes sense to allow classes to implement the dynamic group sequence provider interface themselves.

@sebhoerl
Copy link
Contributor Author

I thought you just wanted to use the "idea" behind that "dynamic group sequence provider". As I see it it's only an utility that allows the user to specify which validation groups should be used and in which order.
I would go one step further and let the user define a DynamicConstraintProvider for each class (as an annotation, yml, xml...) which somehow can add new constraints at runtime.

@webmozart
Copy link
Contributor

No, you can already define group sequences on entities. The idea is to do so dynamically, depending on the entity's state.

Adding constraints during runtime sounds nice, I don't know though if this results in a performance penalty because it totally circumvents any potential caching.

fabpot added a commit that referenced this pull request Feb 9, 2012
Commits
-------

411a0cc [Validator] Added GroupSequenceProvider to changelog
815c769 [Validator] Renamed getValidationGroups to getGroupSequence
d84a2e4 [Validator] Updated test expectations
9f2310b [Validator] Fixed typos, renamed hasGroupSequenceProvider
e0d2828 [Validator] GroupSequenceProvider tests improved, configuration changed
c3b04a3 [Validator] Changed GroupSequenceProvider implementation
6c4455f [Validator] Added GroupSequenceProvider

Discussion
----------

[Validator] Added GroupSequenceProvider

Bug fix: no
Feature addition: yes
Backwards compatibility break: no
Symfony2 tests pass: ![](https://secure.travis-ci.org/blogsh/symfony.png?branch=dynamic_group_sequence)

As discussed in #3114 I implemented the "GroupSequenceProvider" pattern for the validator component. It allows the user to select certain validation groups based on the current state of an object. Here is an example:

    /**
     * @Assert\GroupSequenceProvider("UserGroupSequnceProvider")
     */
    class User
    {
        /**
         * @Assert\NotBlank(groups={"Premium"})
         */
        public function getAddress();

        public function hasPremiumSubscription();
    }

    class UserGroupSequenceProvider implements GroupSequenceProviderInterface
    {
        public function getValidationGroups($user)
        {
            if ($user->hasPremiumSubscription()) {
                return array('User', 'Premium');
            } else {
                return array('User');
            }
        }
    }

With this patch there are two mechanisms to define the group sequence now. Either you can use @GroupSequence to define a static order of validation groups or you can use @GroupSequenceProvider to create dynamic validation group arrays.
The ClassMetadata therefore has methods now which implement quite similar things. The question is whether it would make sense to interpret the static group sequence as a special case and create something like a DefaultGroupSequenceProvider or StaticGroupSequenceProvider which is assigned by default. This would cause a BC break inside the validator component.

---------------------------------------------------------------------------

by bschussek at 2012-01-28T13:39:54Z

I like the implementation, but I think we should differ a little bit from Java here.

1. `GroupSequenceProviderInterface` should be implemented by the domain classes themselves (`User`), not by a separate class.
2. As such, the parameter `$object` from `getValidationGroups($object)` can be removed
3. `ClassMetadata::setGroupSequenceProvider()` should accept a boolean to activate/deactivate this functionality. Also the check for the interface (does the underlying class implement it?) should be done here

Apart from that, special cases need to be treated:

* A definition of a group sequence and a group sequence provider in the same `ClassMetadata` should not be allowed. Either of them must not be set.
* Metadata loaders must take care of settings made by parent classes. If `Animal` is extended by `Dog`, `Animal` defines a group sequence (or group sequence provider) and `Dog` a group sequence provider (or group sequence), only the setting of `Dog` should apply

---------------------------------------------------------------------------

by blogsh at 2012-01-28T21:25:37Z

Changes of the latest commit:

- GroupSequenceProviderInterface has to be implemented by the domain class
- The annotation/configuration options let the user define whether the provider is activated or not (is this neccessary at all?)
- An error is thrown if the user wants to use static group sequences and the provider simultaneously

At the moment neither the static group sequence nor the provider is inherited from parent classes or interfaces. I don't know if it would make sense to enable this feature. There could be problems if a user wants to define a static group sequence in the parent class and a sequence provider in the child class.

---------------------------------------------------------------------------

by bschussek at 2012-01-30T13:07:04Z

> There could be problems if a user wants to define a static group sequence in the parent class and a sequence provider in the child class.

In this case, the setting in the child class should override the setting of the parent class.

But we can leave this open for now. As it seems, [this issue is unresolved in Hibernate as well](https://hibernate.onjira.com/browse/HV-467).

---------------------------------------------------------------------------

by blogsh at 2012-01-30T22:54:41Z

Okay, finally I managed to upload the latest commit. If you got a bunch of notifications or so I'm sorry, but I had to revert some accidental changes in the commit :(

I've rewritten the tests and have removed the "active" setting in the XML configuration.

---------------------------------------------------------------------------

by blogsh at 2012-02-02T15:24:01Z

Okay, typos are fixed now and `hasGroupSequenceProvider` has been renamed to `isGroupSequenceProvider`. I also had to adjust some tests after the rebase with master.

---------------------------------------------------------------------------

by bschussek at 2012-02-03T09:25:19Z

Looks good.

@fabpot 👍

---------------------------------------------------------------------------

by fabpot at 2012-02-03T09:46:52Z

Can you add a note in the CHANGELOG before I merge? Thanks.

---------------------------------------------------------------------------

by blogsh at 2012-02-09T12:31:27Z

@fabpot done
@jgornick
Copy link

For anyone reading this, the ConstraintProviderInterface was reverted and never implemented. In order to dynamically add/remove validation constraints at runtime, you should use the GroupSequenceProvider found in PR #3199 and documented at http://symfony.com/doc/current/book/validation.html#group-sequence-providers.

@fago
Copy link
Contributor

fago commented Oct 6, 2013

@jgornick Is there another issue about that? I'd like to know why this was removed as I'm trying to figure out what's the best way for implementing conditional validation for Drupal, see https://drupal.org/node/2105797#comment-7937693

@jgornick
Copy link

jgornick commented Oct 7, 2013

@fago The only two options that I know of are the callback validator constraint and the group sequence providers. I started off my using the group sequence validators approach then went to move all of my contextual validations in the callback validator.

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

Successfully merging this pull request may close these issues.

10 participants