Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Validator] Improve support for collection validation #9888

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
webmozart opened this issue Dec 29, 2013 · 31 comments
Closed

[Validator] Improve support for collection validation #9888

webmozart opened this issue Dec 29, 2013 · 31 comments

Comments

@webmozart
Copy link
Contributor

I propose to add the following constraints for validating Traversable instances and arrays:

  • Each: Validates that each entry satisfies some constraint (replaces All, which should be deprecated).
  • Some: Validates that at least one entry satisfies some constraint. The lower limit (default 1) and upper limit (default null, i.e. unbounded) can be configured. Alternatively, a precise number of matches can be given.
  • None: Validates that 0 entries satisfy some constraint (alias for Some(exactly = 0, ...))
  • Unique: Validates that the collection does not contain duplicates.

Example:

/**
 * @Each(@GreaterThan(6))
 */
private $numbers.

/**
 * @Some(@GreaterThan(6))
 */
private $numbers.

/**
 * @Some(@GreaterThan(6), min = 2, max = 5)
 */
private $numbers.

/**
 * @Some(@GreaterThan(6), exactly = 3)
 */
private $numbers.

/**
 * @None(@GreaterThan(6))
 */
private $numbers.

/**
 * @Unique
 */
private $numbers.

Each constraint should expect either a single constraint or an array of constraints to be given. That means you can also do:

/**
 * @Each({
 *     @NotNull,
 *     @GreaterThan(6)
 * })
 */
private $numbers.

"min" or "max" and "exactly" must not be given at the same time:

// ConstraintDefinitionException
new Some(array(
    'constraints' => array(...),
    'min' => 2,
    'exactly' => 2,
));

Groups on inner constraints should be supported (ref #4453).

  • If a collection constraint has explicit groups given, it should call addImplicitGroupName() on the nested constraints with each of these groups (except "Default").

    new Each(array(
        'groups' => array('Default', 'Strict'),
        'constraints' => new GreaterThan(6), // implicitly group "Default" and "Strict"
    ));
  • If a collection constraint has no explicit group given, its groups should be the merged result of all nested groups.

    new Each(array(
        'constraints' => array(
            new GreaterThan(array(
                'value' => 6,
                'groups' => array('Default', 'Strict'),
            )),
            new NotNull(array('groups' => 'Filled')),
        ),
        // implicitly group "Default", "Strict" and "Filled"
    ));
  • If both the collection constraint and a nested constraint have explicit groups given, the groups of the nested constraint need to be a subset of the groups of the outer constraint.

    // ok
    new Each(array(
        'groups' => array('Default', 'Strict'),
        'constraints' => new GreaterThan(array(
            'value' => 6,
            'groups' => array('Default', 'Strict'),
        )),
    ));
    
    // ok
    new Each(array(
        'groups' => array('Default', 'Strict'),
        'constraints' => new GreaterThan(array(
            'value' => 6,
            'groups' => 'Strict',
        )),
    ));
    
    // ConstraintDefinitionException
    new Each(array(
        'groups' => array('Default', 'Strict'),
        'constraints' => new GreaterThan(array(
            'value' => 6,
            'groups' => array('Strict', 'Sales'),
        )),
    ));
  • Calls to addImplicitGroupName() on the collection constraint should be propagated to nested constraints.

@mickaelandrieu
Copy link
Contributor

Definitively a good idea +1

@wouterj
Copy link
Member

wouterj commented Dec 29, 2013

Looks promising: +1

(btw, never knew lines end with a . in PHP, I always thought they ended with ; 😉 )

@mmoreram
Copy link
Contributor

Hi there!

I will start working on it. :)

@mmoreram
Copy link
Contributor

We will deprecate All annotation as will be replaced by Each, but I see that constructor of All have some checks inside ( If all are Constraint instances and no one is Valid ). I think we could just make an abstraction of that, something like AbstractGroupConstraint ( Naming is not my strong... sorry about that ), because all will share constructor... Am I wrong?

@wouterj
Copy link
Member

wouterj commented Dec 29, 2013

@mmoreram yes, it's better to great an abstract class, but that's independent of the All annotation (while you have to use the constructor from it).

The All constraint + validator just needs to be renamed to Each, but as we need to provide BC till 3.0, we have to keep the All and AllValidator classes (which only extend Each and EachValidator) and deprecate them.

@mmoreram
Copy link
Contributor

Yes, sure, I was thinking about the code inside All. In fact, I will also extend All annotation provided that the tests pass.

Talking about tests... they are not passing... :(

@webmozart
Copy link
Contributor Author

You can add the common functionality to an abstract base constraint called Composite. There you can define the property $constraints and the functionality for validating them, as is done in All. All other constraints can then extend Composite and add their custom functionality.

@mmoreram
Copy link
Contributor

Great!

@mrcmorales
Copy link
Contributor

Hi,

I have done firs part:

Each: Validates that each entry satisfies some constraint (replaces All, which should be deprecated).

Can I do pull request with only it, or is necessary do all issue ?

Sorry is my first PR :)

This is the code: mrcmorales/Validator@bc1aa65

Thanks

@wouterj
Copy link
Member

wouterj commented Jan 1, 2014

@mrcmorales it's better that you open a PR and add '[WIP]' (meaning Work In Progress) to the title. This means that it'll not be merged until you finished everything you want (and imo, this complete issue should be done in 1 go), but we can already easy review and discuss your code.

@mmoreram
Copy link
Contributor

mmoreram commented Jan 2, 2014

Marc and me will work on this new Features.

Please, anyone else working on this feature, ping us.

@piotrpasich
Copy link

@mmoreram @mrcmorales If you need some help please add some comment what has to be done and we will help, of course. That's really nice feature.

@mmoreram
Copy link
Contributor

mmoreram commented Jan 2, 2014

Thanks @piotrpasich

@mrcmorales
Copy link
Contributor

Hi,

We are near to finish this feature.

Finally we have a question about Unique validation.

To Unique Validation is sufficient validate with __toString method or is necessary compare every element?

Thanks

@stof
Copy link
Member

stof commented Jan 7, 2014

you cannot assume that elements in the collection have a __toString method, and you can even less assume that the output of the string conversion identifies the object. Objects themselves should be compared

@mmoreram
Copy link
Contributor

mmoreram commented Jan 7, 2014

Ok, but we must assume and document as well the cost of UNIQUE().

On 7 January 2014 11:55, Christophe Coevoet [email protected]:

you cannot assume that elements in the collection have a __toStringmethod, and you can even less assume that the output of the string
conversion identifies the object. Objects themselves should be compared


Reply to this email directly or view it on GitHubhttps://github.com//issues/9888#issuecomment-31728564
.

Marc Morera I Arquitecto web
[email protected]

BeFactory
c/ València, 333 - 08009 Barcelona
Tel. 902 73 40 50 - Fax: 902 73 40 51

@twitter
@linkedin
@github

fabpot added a commit that referenced this issue Mar 31, 2014
…mozart)

This PR was merged into the 2.5-dev branch.

Discussion
----------

[WIP][Validator] New NodeTraverser implementation

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | TODO
| License       | MIT
| Doc PR        | TODO

This PR is ready for review.

#### Todo

- [x] Test extensively to avoid regressions
- [x] Test more extensively
- [x] Finish inline documentation
- [x] Provide a layer to choose the desired API through ValidatorBuilder
- [x] Provide a layer to choose the desired API through FrameworkBundle
- [x] Update UPGRADE file
- [x] Update CHANGELOG
- [ ] Update user documentation

#### Goal

The goal of this PR is to be able to fix the following tickets:

- [x] #6138 Simplify adding of constraint violations
- [x] #7146 Support group sequences in Validator API
- [x] #7432 Poorly implemented Visitor Pattern
- [x] #8617 Control traversal on class level
- [x] #9888 Improve support for collection validation (PR: #9988)

The following tickets are probably fixed, but require testing first:

- [ ] #8376 Using validation_group causes error message to display multiple times
- [ ] #9939 GroupSequences still execute next group if first fail

Of course, full backwards compatibility **must** be guaranteed.

Other tickets I want to fix in follow-up PRs:

* #3622 Constraints\Valid does not respect "groups" option
* #4453 walk constraints by groups
* #7700 Propagate implicit group names in constraints
* #9051 Always ask value event if field isn't in the validating group
* #10163 poor collection validation test coverage
* #10221 TypeValidator does not enforce desired type when value is NULL
* #10495 Class Valid Constraint can't be used on a Form Type

#### In a nutshell

The implementation removes the Visitor pattern, which was implemented badly. I tried fixing it via a NodeTraverser/NodeVisitor implementation, but performance degraded too much so I decided to remove the pattern altogether.

A couple of new features and bug fixes are possible thanks to the new implementation. See below for details.

#### PHP Versions

PHP 5.3.8 and older does not allow to implement two different interfaces which both contain a method with the same name. This is used in the compatibility layer that supports both the old and the new API.

For this reason, the compatibility layer is disabled on PHP < 5.3.9. Older PHP versions need to decide on the old API or the new API (without compatibility layer).

#### Choosing the API Version

The API version can be specified by one of `Validation::API_VERSION_2_4`, `Validation::API_VERSION_2_5` and `Validation::API_VERSION_2_5_BC` to `setApiVersion()` when building the validator:

```php
// Old implementation
$validator = Validation::createValidatorBuilder()
    ->setApiVersion(Validation::API_VERSION_2_4)
    ->getValidator();

// New implementation with BC API
// Does not work on PHP < 5.3.9
$validator = Validation::createValidatorBuilder()
    ->setApiVersion(Validation::API_VERSION_2_5)
    ->getValidator();

// New implementation without BC API
$validator = Validation::createValidatorBuilder()
    ->setApiVersion(Validation::API_VERSION_2_5_BC)
    ->getValidator();
```

#### Features

##### Constraint validation as first-class citizen

The new API merges `validateValue()` and `validate()`. The idea is that the validation of values against constraints should be as simple as possible. Object validation is a special case where an object is tested against the `Valid` constraint. A backwards compatibility layer is provided to use both `validate()` and `validateValue()` with the old signature.

```php
// Validate against explicit constraints
$violations = $validator->validate($firstName, array(
    new NotNull(),
    new Length(array('min' => 3)),
));

// Validate against metadata
$violations = $validator->validate($object);

// Same, more expressive notation
$violations = $validator->validate($object, new Valid());

// Validate each entry against its metadata
$violations = $validator->validate($array);
```

##### Aggregate violations

It is now possible to call the methods of the validator multiple times and aggregate the violations to a common violation list. To do so, call `startContext()`, execute the calls and call `getViolations()` in the end to retrieve the violations:

```php
$violations = $validator->startContext()
    ->validate($title, new NotNull())
    ->validate($text, new NotNull())
    ->validate($author->getName(), new NotNull())
    ->getViolations()
```

Most of the time, you will want to specify a property path for each validation. Use the method `atPath()` for that:

```php
$violations = $validator->startContext()
    ->atPath('title')->validate($title, new NotNull())
    ->atPath('text')->validate($text, new NotNull())
    ->atPath('author.name')->validate($author->getName(), new NotNull())
    ->getViolations()
```

##### Control the context of nested validations

In Symfony <= 2.4, you can validate objects or constraints from within a constraint validator like this:

```php
$this->context->validate($object);
$this->context->validateValue($value, new Length(array('min' => 3)));
```

The validations will run and all violations will be added to the current context.

This way, it is impossible though to validate something, inspect the result and then decide what kind of violations to add to the context. This is needed, for example, for the `Some` constraint (proposed in #9888), which should succeed if any of the validated values did *not* generate violations.

For this reason, the new context API features a method `getValidator()`. This method returns the validator instance, you can use it to validate anything in a new context (as the validator always does):

```php
$validator = $this->context->getValidator();
$violations = $validator->validate($object);

if (count($violations)  > 0) {
    $this->context->addViolation('The validation did not pass');
}
```

You can also explicitly start a new context:

```php
$validator = $this->context->getValidator();
$violations = $validator->startContext()
    ->atPath('title')->validate($title, new NotNull())
    ->atPath('text')->validate($text, new NotNull())
    ->getViolations()
```

If you want to execute the validation in the current context, use the `inContext()` method of the validator instead:

```php
// violations are added to $this->context
$validator->inContext($this->context)
    ->atPath('title')->validate($title, new NotNull())
    ->atPath('text')->validate($text, new NotNull())
;
```

With this feature, #9888 (especially the PR for it: #9988) can be finished.

##### Custom group sequences (#7146)

It is now possible to pass `GroupSequence` instances whenever you can pass a group to the validator. For example:

```php
$violations = $validator->validate($object, new Valid(), new GroupSequence('Basic', 'Strict'));
```

Or in the context of the Form component:

```php
$form = $this->createForm(new BlogType(), new Blog(), array(
    'validation_groups' => new GroupSequence('Basic', 'Strict'),
));
```

##### Constraint violation builders (#6138)

The API for adding constraint violations was simplified:

```php
$this->context->addViolation('message', array('param' => 'value'));

// or

$this->context->buildViolation('message')
    ->atPath('property')
    ->setParameter('param', 'value')
    ->setTranslationDomain('validation_strict')
    ->addViolation();
```

##### Control traversal at class level (#8617)

Currently, it is possible whether to traverse a `Traversable` object or not in the `Valid` constraint:

```php
/**
 * @Assert\Valid(traverse=true)
 */
private $tags = new TagList();
```

(actually, `true` is the default)

In this way, the validator will iterate the `TagList` instance and validate each of the contained objects. You can also set "traverse" to `false` to disable iteration.

What if you want to specify, that `TagList` instances should always (or never) be traversed? That's currently not possible.

With this PR, you can do the following:

```php
/**
 * @Assert\Traverse(false)
 */
class TagList implements \IteratorAggregate
{
    // ...
}
```

#### Follow-up features

Features of the follow-up PRs will be described directly there.

#### Backwards compatibility

I implemented a new `AbstractValidatorTest` which tests both the old and the new implementation for compatibility. I still want to extend this test to make sure we don't introduce any regressions.

Almost none of the existing classes were modified (or only slightly). If users depend on the current (now "legacy") implementation, they will have the choice to continue using it until 3.0 (of course, without the new features).

#### Your task

Congrats, you made it till here :) If you have time, please skim over the code and give me feedback on the overall implementation and the class/method names. Again, no feedback on details yet, there are quite a few areas in the code that are still work in progress.

Thanks,
Bernhard

[1] That means that only the nodes from the root of the graph until the currently validated node are held in memory.

Commits
-------

ca6a722 [Validator] Converted `@deprecate` doc comment into regular doc comment
68d8018 [Validator] Documented changes in the UPGRADE files
b1badea [Validator] Fixed failing CsrfFormLoginTest
0bfde4a [Validator] Fixed misnamed method calls in FrameworkExtension
3dc2b4d [Validator] Made "symfony/property-access" an optional dependency
c5629bb [Validator] Added getObject() to ExecutionContextInterface
9b204c9 [FrameworkBundle] Implemented configuration to select the desired Validator API
0946dbe [Validator] Adapted CHANGELOG
1b111d0 [Validator] Fixed typos pointed out by @cordoval
7bc952d [Validator] Improved inline documentation of RecursiveContextualValidator
166d71a [Validator] Removed unused property
90c27bb [Validator] Removed traverser implementation
3183aed [Validator] Improved performance of cache key generation
029a716 [Validator] Moved logic of replaceDefaultGroup() to validateNode()
2f23d97 [Validator] Reduced number of method calls on the execution context
73c9cc5 [Validator] Optimized performance by calling spl_object_hash() only once per object
94ef21e [Validator] Optimized use statements
1622eb3 [Validator] Fixed reference to removed class in ValidatorBuilder
be508e0 [Validator] Merged DefaultGroupReplacingVisitor and ContextUpdateVisitor into NodeValidationVisitor
50bb84d [Validator] Optimized RecursiveContextualValidator
eed29d8 [Validator] Improved performance of *ContextualValidator::validate()
5c479d8 [Validator] Simplified validateNodeForGroup
eeed509 [Validator] Improved phpdoc of RecursiveValidator
274d4e6 [Validator] Changed ValidatorBuilder to always use LegacyExecutionContext
38e26fb [Validator] Decoupled RecursiveContextualValidator from Node
23534ca [Validator] Added a recursive clone of the new implementation for speed comparison
f61d31e [Validator] Fixed grammar
886e05e [Validator] Removed unused use statement
93fdff7 [Validator] The supported API versions can now be passed to the ValidatorBuilder
987313d [Validator] Improved inline documentation of the violation builder
79387a7 [Validator] Improved inline documentation of the metadata classes
01ceeda [Validator] Improved test coverage of the Traverse constraint
9ca61df [Validator] Improved inline documentation of CascadingStrategy and TraversalStrategy
524a953 [Validator] Improved inline documentation of the validators
9986f03 [Validator] Added inline documentation for the PropertyPath utility class
be7f055 [Validator] Visitors may now abort the traversal by returning false from beforeTraversal()
299c2dc [Validator] Improved test coverage and prevented duplicate validation of constraints
186c115 [Validator] Improved test coverage of NonRecursiveNodeTraverser
822fe47 [Validator] Completed inline documentation of the Node classes and the NodeTraverser
dbce5a2 [Validator] Updated outdated doc blocks
8558377 [Validator] Added deprecation notes
e8fa15b [Validator] Fixed the new validator API under PHP < 5.3.9
2936d10 [Validator] Removed unused use statement
6fc6ecd [Validator] Fixed tests under PHP<5.3.9
778ec24 [Validator] Removed helper class Traversal
76d8c9a [Validator] Fixed typos
4161371 [Validator] Removed unused use statements
aeb6822 [Validator] Improved visitor names
08172bf [Validator] Merged validate(), validateObject() and validateObjects() to simplify usage
51197f6 [Validator] Made traversal of Traversables consistent
117b1b9 [Validator] Wrapped collections into CollectionNode instances
94583a9 [Validator] Changed NodeTraverser to traverse nodes iteratively, not recursively
cf1281f [Validator] Added "Visitor" suffix to all node visitors
230f2a7 [Validator] Fixed exception message
e057b19 [Validator] Decoupled ContextRefresher from ExecutionContext
e440690 [Validator] Renamed validateCollection() to validateObjects()
df41974 [Validator] Changed context manager to context factory
26eafa4 [Validator] Removed unused use statements
bc29591 [Validator] Clearly separated classes supporting the API <2.5/2.5+
a3555fb [Validator] Fixed: Objects are not traversed unless they are instances of Traversable
2c65a28 [Validator] Completed test coverage and documentation of the Node classes
9c9e715 [Validator] Completed documentation of GroupManagerInterface
1e81f3b [Validator] Finished test coverage and documentation of ExecutionContextManager
feb3d6f [Validator] Tested the validation in a separate context
718601c [Validator] Changed validateValue() to validate() in the new API
ee1adad [Validator] Implemented handling of arrays and Traversables in LegacyExecutionContext::validate()
09f744b [Validator] Implemented BC traversal of traversables through validate()
297ba4f [Validator] Added a note why scalars are passed to cascadeObject() in NodeTraverser
9b07b0c [Validator] Implemented BC validation of arrays through validate()
405a03b [Validator] Updated deprecation notes in GroupSequence
499b2bb [Validator] Completed test coverage of ExecutionContext
adc1437 [Validator] Fixed failing tests
4ea3ff6 [Validator] Finished inline documentation of ExecutionContext[Interface]
f6b7288 [Validator] Removed unused use statement
8318286 [Validator] Completed GroupSequence implementation
5fbf848 [Validator] Added note about Callback constraint to CHANGELOG
c1b1e03 [Validator] Added TODO reminder
8ae68c9 [Validator] Made tests green (yay!)
680f1ee [Validator] Renamed $params to $parameters
321d5bb [Validator] Throw exception if ObjectInitializer is constructed without visitors
1156bde [Validator] Extracted code for group sequence resolving into GroupSequenceResolver
b1a9477 [Validator] Added ObjectInitializer visitor
7e3a41d [Validator] Moved visitors to NodeVisitor namespace
a40189c [Validator] Decoupled the new classes a bit
a6ed4ca [Validator] Prototype of the traverser implementation
25cdc68 [Validator] Refactored ValidatorTest and ValidationVisitorTest into an abstract validator test class
webmozart added a commit to webmozart/symfony that referenced this issue Aug 5, 2014
…onstraints (webmozart)

This PR was merged into the 2.6-dev branch.

Discussion
----------

[Validator] Fixed group handling in composite constraints

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | symfony#4453
| License       | MIT
| Doc PR        | -

With this PR, it finally becomes possible to set the groups of nested constraints in All and Collection:

```php
/**
 * @Assert\Collection({
 *     "foo" = @Assert\NotNull(groups = "Strict"),
 *     "bar" = @Assert\NotBlank(groups = "Regular"),
 * })
 */
private $attr;
```

The group logic is implemented as described in [this comment](symfony#4453 (comment)).

This PR supports the implementation of symfony#9888 which should now become fairly simple.

Commits
-------

f6c070d [Validator] Fixed group handling in composite constraints
@Destroy666x
Copy link

Destroy666x commented Feb 20, 2018

Great ideas, any reason this got silent? I could try working on it if I find time

@ostrolucky
Copy link
Contributor

@Destroy666x Please see referenced closed PR. I think this is welcome to contribute.

@01e9
Copy link

01e9 commented Apr 30, 2020

UniqueEntity was confusing because I expected it to work as Unique from this proposal.

The fix was:

  1. Create a constraint similar to Unique proposed here (mine has two options)

  2. Put it on collection field

  3. On entity edit, before save, delete all collection relations which will be recreated.

    Because the constraint checks only in memory collection items and can't resolve conflicts with values from database, which appear when you edit collection items in form. For example, in form you can remove one item and change the unique field from another item to be the same as of the deleted item - now the controller should be very smart to detect which item exactly was removed and which was edited to do the same changes in database (it becomes hard when you have nested collections in advanced forms), so the simplest solution is to recreate collection database values on edit.

@carsonbot
Copy link

Thank you for this suggestion.
There has not been a lot of activity here for a while. Would you still like to see this feature?

@wkania
Copy link
Contributor

wkania commented Feb 20, 2021

Yes, I have a proposal to improve Unique constraint.
Currently, it does not work with the Collection constraint.
Unique constraint requires a new optional parameter.
It could work as in the examples below:

  1. Basic example
use Symfony\Component\Validator\Constraints as Assert;
...

    /**
     * @Assert\Count(min=1),
     * @Assert\Unique(fields={"language"}),
     * @Assert\Collection(
     *         fields = {
     *             "language" = {
     *                  @Assert\NotBlank,
     *                  @Assert\Length(min = 2, max = 2),
     *                  @Assert\Language
     *             },
     *             "name" = {
     *                  @Assert\NotBlank,
     *                  @Assert\Length(max = 255)
     *             }
     *         }
     * )
     */
    public array $translations = [];
  1. An example where Optional is recognizable
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Constraints\Optional;
...

    /**
     * @Assert\Unique(fields={"id"}),
     * @Assert\Collection(
     *         fields = {
     *             "id" = @Assert\Optional({
     *                  @Assert\Uuid
     *             }),
     *             "name" = {
     *                  @Assert\NotBlank,
     *                  @Assert\Length(max = 255)
     *             }
     *         }
     * )
     */
    public array $items = [];
  1. An example with composite uniqueness
use Symfony\Component\Validator\Constraints as Assert;
...

    /**
     * @Assert\Unique(fields={"latitude", "longitude"}),
     * @Assert\Collection(
     *         fields = {
     *             "latitude" = {
     *                  @Assert\NotBlank
     *             },
     *             "longitude" = {
     *                  @Assert\NotBlank
     *             }
     *         }
     * )
     */
    public array $coordinates = [];  
  1. I can't imagine an example with two different Unique constraints on the same collection.

I plan to work on it and add it as a feature.

@carsonbot
Copy link

Thank you for this suggestion.
There has not been a lot of activity here for a while. Would you still like to see this feature?

@zerkms
Copy link
Contributor

zerkms commented Aug 22, 2021

Still actual, not stalled.

@carsonbot carsonbot removed the Stalled label Aug 22, 2021
@wkania
Copy link
Contributor

wkania commented Aug 22, 2021

Thank you for this suggestion.
There has not been a lot of activity here for a while. Would you still like to see this feature?

So linked pull request is not an activity.

@mindaugasvcs

This comment has been minimized.

@stof
Copy link
Member

stof commented Nov 18, 2021

Well, we solved this partially: we have All (already the case when this was opened), Any and AtLeastOneOf. This is still open, because Unique is not there yet.

@norkunas
Copy link
Contributor

This is still open, because Unique is not there yet.

https://symfony.com/doc/current/reference/constraints/Unique.html it's there :)

@stof
Copy link
Member

stof commented Nov 18, 2021

Indeed. The basic feature of Unique is indeed implemented. the part still not done yet is only the fields option of it.

@mindaugasvcs
Copy link
Contributor

Unique constraint, a basic feature, implemented only in 2019? Still normalizer option is undocumented and not really clear how to use it to be useful. Symfony Form Collection implements only patching approach, what if on form submit I want to replace the collection instead? No luck with Symfony, have to manually check submitted items against values in DB to implement the replace approach.

@fabpot fabpot closed this as completed Apr 14, 2022
fabpot added a commit that referenced this issue Apr 14, 2022
…cked for uniqueness (wkania)

This PR was squashed before being merged into the 6.1 branch.

Discussion
----------

[Validator] Define which collection keys should be checked for uniqueness

| Q             | A
| ------------- | ---
| Branch?       | 5.4
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #9888
| License       | MIT
| Doc PR        | symfony/symfony-docs#16713

Currently, the validator checks each element of the collection as a whole. We already have a custom normalizer (which is great), but it would be nice to be able to check for uniqueness certain [collection](https://symfony.com/doc/current/reference/constraints/Collection.html) keys.

For example, some fields in the collection element can be identifiers. They should be unique within the collection, even when the rest of the element data are different.

Current state:
- validates that all the elements of the given collection are unique

New state:
- preserve the current state,
- all old tests pass (no changes in them),
- no breaking changes,
- define which collection fields should be checked for uniqueness (optional),
- fields are optional in each element of the collection. Use [collection constraints](https://symfony.com/doc/current/reference/constraints/Collection.html) if they are required

Examples:

1. Basic example. Each translation of the same resource must be in a different language.
```php
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @Assert\Count(min=1),
 * @Assert\Unique(fields={"language"}),
 * @Assert\Collection(
 *         fields = {
 *             "language" = {
 *                  @Assert\NotBlank,
 *                  @Assert\Length(min = 2, max = 2),
 *                  @Assert\Language
 *             },
 *             "title" = {
 *                  @Assert\NotBlank,
 *                  @Assert\Length(max = 255)
 *             },
 *             "description" = {
 *                  @Assert\NotBlank,
 *                  @Assert\Length(max = 255)
 *             }
 *         }
 * )
 */
public array $translations = [];
```
2. An example where Optional is recognizable. Items with the id are changed and without are new.
```php
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Constraints\Optional;

/**
 * @Assert\Unique(fields={"id"}),
 * @Assert\Collection(
 *         fields = {
 *             "id" = @Assert\Optional({
 *                  @Assert\Uuid
 *             }),
 *             "name" = {
 *                  @Assert\NotBlank,
 *                  @Assert\Length(max = 255)
 *             }
 *         }
 * )
 */
public array $items = [];
```
3. An example with composite uniqueness
```php
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @Assert\Unique(fields={"latitude", "longitude"}),
 * @Assert\Collection(
 *         fields = {
 *             "latitude" = {
 *                  @Assert\NotBlank
 *             },
 *             "longitude" = {
 *                  @Assert\NotBlank
 *             },
 *             "poi" = {
 *                  @Assert\Length(max = 255)
 *             }
 *         }
 * )
 */
public array $coordinates = [];
```

Commits
-------

0e8f4ce [Validator] Define which collection keys should be checked for uniqueness
@norkunas
Copy link
Contributor

I'd say this is not completed as #44688 is in queue :)

@xabbuh xabbuh reopened this Apr 19, 2022
@xabbuh
Copy link
Member

xabbuh commented Jul 21, 2022

I am going to close here as #44688 was rejected for being too special.

@xabbuh xabbuh closed this as completed Jul 21, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.