8000 Issue with validator service on unique entities in collections · Issue #17884 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

Issue with validator service on unique entities in collections #17884

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
liamsorsby opened this issue Feb 22, 2016 · 14 comments
Closed

Issue with validator service on unique entities in collections #17884

liamsorsby opened this issue Feb 22, 2016 · 14 comments

Comments

@liamsorsby
Copy link

We seem to be having an issue with the validator service when using collections and the validator to check for unique entities.

The validator will correctly find any issues before persisting the entity to the doctrine manager.

The validator seems to check the data in the current database but doesn't check the data that is going to be saved.

For instance if title is a unique entity and in my collection I am adding two new collections to the database both with the title abc it will not throw an error with the validator but will try to add them both causing an internal server error.

We are adding clientside checks but wasn't sure if this was a bug in the validator service.

@linaori
Copy link
Contributor
linaori commented Feb 22, 2016

You could put a constraint on your collection which specifies that the titles have to be unique. This is not a bug in the unique entity constraint afaik as it only checks the database.

@liamsorsby
Copy link
Author

@iltar The constraint is on the collection. However as the data is duplicated. I imagine that the database is only checked a single time for performance reasons so on the second insert the database will fail.
When the entity is persisted it wouldn't invalidate as you said it only checks the database. So on the second insert the insert will fail but it would have already passed on the constraint.
Would it not be feasable to do a check on the unique entity data through the constraint prior to inserting as well as checking the database?

@xabbuh
Copy link
Member
xabbuh commented Feb 22, 2016

The constraint doesn't know that there is another object containing the same data somewhere in your object graph. And as all validation happens before any of your data is written to the database it cannot catch this kind of error, but you have to implement that yourself as a separate constraint on the collection.

@liamsorsby
Copy link
Author

@xabbuh should this not be implemented into the unique constraint to check data to be persisted aswel.

To me it wouldn't seem to make sense that the unique constraint would fail to check all data with itself and the database before being persisted as there would be nothing benificial that could be gained by not adding the check to the constraint.

Would it be feasible to create a PR on the unique constraint to add this feature in? Or would this not be generic enough to resolve any specific problem?

@xabbuh
Copy link
Member
xabbuh commented Feb 27, 2016

@liamsorsby I don't see how you would implement it in the unique constraint. But if you have an idea, feel free to submit a pull request.

@liamsorsby
Copy link
Author

@xabbuh I may be wrong but isn't the unique constraint to be checked against the data in the form of an array. If so then couldn't I just do an array search on that data? To be honest I've not actually had a look at the internals of how the unique constraint works to be honest. But I will certainly give it a go.

@xabbuh
Copy link
Member
xabbuh commented Feb 28, 2016

@liamsorsby The unique entity constraint checks that there is no existing entity in your database that already contains the same value(s) for a particular property (or a set of properties).

@linaori
Copy link
Contributor
linaori commented Feb 28, 2016

What you probably want is something like this:

class FooData
{
   /**
    * @Assert\Unique(fields={"username"})
    * @var MyUser[]
    */
    private $myUsers = [];
}

As opposing to the unique entity constraint -which checks just that one field in the database- you want a constraint that checks all the fields in all items of the collection for that constraint.

@liamsorsby
Copy link
Author

@iltar would this be instead of the unique entity check Or alongside the unique entity check? Is the unique entity check designed so it doesn't check for unique data in the array collection before persisting?

The issue I'm having is a collection with duplicate data being persisted and failing due to duplicates in the data that hasn't been persisted yet.

@linaori
Copy link
Contributor
linaori commented Feb 28, 2016

@liamsorsby in theory you can make the unique constraint do this (but requires a PR)

@poolerMF
Copy link
poolerMF commented Jul 21, 2016

my solution for checking same item in collection:

CollectionSameItem.php

/**
 * @Annotation
 * @Target({"CLASS", "ANNOTATION"})
 */
class CollectionSameItem extends Constraint
{
    const ERROR_PATH_FORM = 'form';
    const ERROR_PATH_FIELD = 'field';

    public $message = 'Item already exists in collection "%collectionName%" .'; // %variable%
    public $variable;               // property access for specific variable
    public $toString;               // property access of item
    public $collection;             // property access for collection
    public $errorPath = self::ERROR_PATH_FIELD;    // "form" or "field" or "form,field"

    public function __construct($options = null)
    {
        parent::__construct($options);

        if (null === $this->collection) {
            throw new MissingOptionsException(sprintf('Option collection" must be given for constraint %s', __CLASS__));
        }

        if (!$this->toString) {
            $this->toString = $this->variable;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getTargets()
    {
        return self::CLASS_CONSTRAINT;
    }
}

CollectionSameItemValidator.php

class CollectionSameItemValidator extends ConstraintValidator
{

    public function validate($value, Constraint $constraint)
    {
        if (null === $value) {
            return;
        }

        $accessor = new PropertyAccessor();
        $value = $accessor->getValue($value, $constraint->collection);

        if (!is_array($value) && !$value instanceof \Countable) {
            throw new UnexpectedTypeException($value, 'array or \Countable');
        }

        if (count($value)) {
            $repeatedItems = [];
            $existedItems = [];

            $itemIndex = 0;
            foreach ($value as $item) {
                if ($constraint->variable) {
                    $variable = $accessor->getValue($item, $constraint->variable);
                } else {
                    $variable = $item;
                }
                if (in_array($variable, $existedItems)) {
                    if (!in_array($variable, $repeatedItems)) {
                        $repeatedItems[] = [
                            'itemIndex' => $itemIndex,
                            'item' => $item
                        ];
                    }
                } else {
                    $existedItems[] = $variable;
                }
                $itemIndex++;
            }

            foreach ($repeatedItems as $item) {
                $value = null;
                if($constraint->toString) {
                    $value = $accessor->getValue($item['item'], $constraint->toString);
                }

                foreach (array_unique(array_map('trim', explode(',', $constraint->errorPath))) as $path) {
                    $this->addViolation(trim($path), $constraint, $item, $value);
                }
            }
        }
    }

    private function addViolation($path, $constraint, $row, $value = null)
    {
        $atPath = null;

        if ($path == CollectionSameItem::ERROR_PATH_FIELD && $constraint->errorForm) {
            $atPath = sprintf('%s[%d].%s', $constraint->collection, $row['itemIndex'], $constraint->variable);
        }

        $violation = $this->context->buildViolation($constraint->message)
            ->setParameter('%collectionName%', $constraint->collection)
            ->atPath($atPath);

        if($value) {
            $violation->setParameter('%variable%', $value);
        }

        $violation->addViolation();
    }
}

Example of usage:
Entity Project has more users (Project->getUsers())
Entity User has variables firstName, secondName .. method getFullName()

example1:

DemoBundle\Entity\Project:
    constraints:
        - DemoBundle\Validator\CollectionSameItem:
            variable: firstName
            collection: users
            toString: getFullName
            errorPath: "form,field"
            message: User "%variable%" already exist in project

message -> User "Firstname Secondname" already exist in project

example2:

DemoBundle\Entity\Project:
    constraints:
        - DemoBundle\Validator\CollectionSameItem:
            variable: firstName
            collection: users
            message: User "%variable%" already exist in project

message -> User "Firstname" already exist in project

example3:

DemoBundle\Entity\Project:
    constraints:
        - DemoBundle\Validator\CollectionSameItem:
            collection: users
            errorPath: "field"
            message: This user already exist this project

message -> This user already exist this project

@Seb33300
Copy link
Contributor

Looking for something like this too.

@ostrolucky
Copy link
Contributor

Duplicity of #9888. Linked issue contains proposal for Unique constraint.

@fabpot
Copy link
Member
fabpot commented Dec 18, 2017

Closing as a duplicate then.

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

8 participants
0