Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[DoctrineBridge][WIP] Allow validating every class against unique entity constraint#24974

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Closed
MatTheCat wants to merge5 commits intosymfony:masterfromMatTheCat:ticket_22592

Conversation

MatTheCat
Copy link
Contributor

@MatTheCatMatTheCat commentedNov 15, 2017
edited
Loading

QA
Branch?master
Bug fix?no
New feature?yes
BC breaks?no
Deprecations?no
Tests pass?yes
Fixed tickets#14917,#22592
LicenseMIT

For nowUniqueEntity constraint only works if the validated value is an entity, which prevent its usage on DTOs. This PR tries to make this constraint work on every class.

  • entityClass is used to get metadata if not null
  • fields can be used to map class fields to entity fields
  • a newforUpdate option allows to tell a single matching entity does not constitues a violation

WDYT?

ro0NL and Koc reacted with thumbs up emoji
@MatTheCatMatTheCat changed the titleAllow validating every class against unique entity fieldAllow validating every class against unique entity constraintNov 15, 2017
@MatTheCatMatTheCat changed the titleAllow validating every class against unique entity constraint[DoctrineBridge] Allow validating every class against unique entity constraintNov 15, 2017
@MatTheCatMatTheCat changed the title[DoctrineBridge] Allow validating every class against unique entity constraint[DoctrineBridge][WIP] Allow validating every class against unique entity constraintNov 15, 2017
foreach ($fields as $fieldName) {
if (!$class->hasField($fieldName) && !$class->hasAssociation($fieldName)) {
throw new ConstraintDefinitionException(sprintf('The field "%s" is not mapped by Doctrine, so it cannot be validated for uniqueness.', $fieldName));
foreach ($fields as $formField => $entityField) {
Copy link
Contributor

Choose a reason for hiding this comment

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

$sourceField?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Why not

Copy link
Member

Choose a reason for hiding this comment

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

$formField =>$objectField? because it is about the mapping field configuration between a DTO and the Entity.

}

$fieldValue = $class->reflFields[$fieldName]->getValue($entity);
$fieldValue = $class->reflFields[is_int($formField) ? $entityField : $formField]->getValue($entity);
Copy link
Contributor

Choose a reason for hiding this comment

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

we need to consider#22592 (comment) here?

we use PropertyAccess component instead of ReflectionProperty to read the field value from current object (maintain the metadata verification and the DTO fields must be readable too),

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Yes indeed

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

But that would add a dependency, as we just read fields maybe reflection is simpler.

Copy link
Contributor

Choose a reason for hiding this comment

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

why not :P

return;
}

if (1 === count($result)) {
if ($constraint->forUpdate) {
Copy link
Contributor

Choose a reason for hiding this comment

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

what about checking the corresponding source fields against the entity fields instead?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

I don't see how this could work?

Copy link
Contributor

Choose a reason for hiding this comment

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

for each source field get the entity field value, compare entity field values against source field values. But im not sure thats waterproof; nor isforUpdate.

Copy link
ContributorAuthor

@MatTheCatMatTheCatNov 15, 2017
edited
Loading

Choose a reason for hiding this comment

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

If I want to update an entity from a DTO without changing a unique constrained field then this wouldn't work, that's why I let the decision to the developer.

@dmaicher
Copy link
Contributor

dmaicher commentedNov 15, 2017
edited
Loading

but there is a fundamental logic issue with theforUpdate option:

I have a user with emailfoo@bar.com. Now I want to edit my user's email tobar@foo.com but there isone other user with that email already in the DB. So now this will not result in any violation when validating?

I mean we have to make sure the existing entity is actually my user. Only then its fine. Just counting does not really work.

Or am I missing something?

yceruto reacted with thumbs up emoji

@MatTheCat
Copy link
ContributorAuthor

MatTheCat commentedNov 15, 2017
edited
Loading

@dmaicher nope you're right. Any idea welcomed! Maybe anentityId option? But how could it be set in an annotation..?

@MatTheCat
Copy link
ContributorAuthor

Another idea:0b87dea

dmaicher reacted with thumbs up emoji

return;
}

if ($entity instanceof EntityDto && $entity->isForEntity($match)) {
Copy link
Member

@ycerutoycerutoNov 15, 2017
edited
Loading

Choose a reason for hiding this comment

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

Probably as constraint option should be "better", wdyt? something likeisEqualToMethod?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Hmm maybe, this way would be more callback-constraint-like

Copy link
Member

Choose a reason for hiding this comment

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

This approach will solve#22592 (comment)

}

$fieldValue = $class->reflFields[$fieldName]->getValue($entity);
$field = new \ReflectionProperty($entityClass, is_int($formField) ? $entityField : $formField);
Copy link
Member

@ycerutoycerutoNov 15, 2017
edited
Loading

Choose a reason for hiding this comment

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

This seems wrong,$entityClass is not the class of the subject$entity always, should beget_class($entity) here, no?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Yes indeed. Followingyour suggestion I'll rename$entity$object this will be clearer

yceruto reacted with thumbs up emoji

$reflMethod = new \ReflectionMethod($object, $method);

if ($reflMethod->isStatic() ? $reflMethod->invoke(null, $object, $match) : $reflMethod->invoke($object, $match)) {
Copy link
Member

@ycerutoycerutoNov 15, 2017
edited
Loading

Choose a reason for hiding this comment

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

Also we could pass here the current$em as argument too?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

Do you have an example showing how it could be used?

Copy link
Member

Choose a reason for hiding this comment

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

Both objects can't be compared directly, right? So I guess inside this method we need to find somehow ($em) the current object to compare it.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

A DTO would include the entity identifier so it would not need the entity manager.

Copy link
Contributor

Choose a reason for hiding this comment

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

not sure about dto's always including an entity id or so; in my experience dto + entity live decoupled.

Both objects can't be compared directly, right?

Dunno, depends on context i guess. So yes having$em might be needed 👍

Also calling this "ToUpdate" is opinionated. I would expect something likeentityComparator or so.

yceruto reacted with thumbs up emoji
Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

How can an handler update an entity if its identifier is not part of the DTO?

Copy link
Contributor

@ro0NLro0NLNov 15, 2017
edited
Loading

Choose a reason for hiding this comment

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

because i generate a new id / use the current id from url (or so) directly ;-)

handle(new Id(), $dto->email) for example.

Copy link
Contributor

Choose a reason for hiding this comment

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

not sure what's the better practice though, now that i think of it. It's just a decision i made beforehand, i also dont dostatic Dto::fromEntity() and such.

return;
}

$method = $constraint->isEntityToUpdateMethod;
Copy link
Member

Choose a reason for hiding this comment

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

We must add support to\Closure andarray callable too.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

This method is just for saying an entity is the one we're updating or not I don't think we need more complicated?

Copy link
Member

Choose a reason for hiding this comment

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

This constraint can be used in PHP flat and maybe for advanced checking where I need some dependencies as service for instance.

Copy link
Contributor

@ro0NLro0NLNov 15, 2017
edited
Loading

Choose a reason for hiding this comment

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

It should be acallable yes. I would not like to add this method to my dto. Defeats decoupling IMHO.

We can do the standard approach no? Check for callable, then for method on current context which we do elsewhere also believe.

yceruto reacted with thumbs up emoji
@@ -30,6 +30,7 @@ class UniqueEntity extends Constraint
public $em = null;
public $entityClass = null;
public $repositoryMethod = 'findBy';
public $isEntityToUpdateMethod = null;
Copy link
Member

@ycerutoycerutoNov 15, 2017
edited
Loading

Choose a reason for hiding this comment

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

This method is not just about update event, on create it is called too. MaybeisEqualToEntityMethod? Basically it is what the validator asks to throw the violation or not.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

DTO can't be equal to an entity

Copy link
ContributorAuthor

Choose a reason for hiding this comment

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

It would make no sense to call it if an entity is created: if an entity exists in database with fields of same value it cannot be the one which comes from the DTO as it isn't in the database yet.

Copy link
Member

@ycerutoycerutoNov 16, 2017
edited
Loading

Choose a reason for hiding this comment

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

When we are creating a new entity through a DTO, we also need to call to the comparison method to check the equality.

Take the example below, on create$this->authorId isnull, on update$this->authorId is the author ID that is being modified:

/** * @UniqueEntity( *     fields={"email"}, *     entityClass="App\Entity\Author", *     isEqualToMethod="isEqualTo" * ) */class AuthorDTO{private$authorId;private$email;// ...publicfunctionisEqualTo(Author$author)    {return$author->getId() ===$this->authorId;    }}

Results:

  • On create it always returnfalse, so throws the violation.
  • On update it depends of the equality, iftrue pass, else throws the violation.

The validator doesn't know nothing about the current event, so the method must be called always.

Copy link
Contributor

Choose a reason for hiding this comment

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

I also think something likeisEqualToEntityMethod would make more sense.


if (!$em) {
throw new ConstraintDefinitionException(sprintf('Unable to find the object manager associated with an entity of class "%s".',get_class($entity)));
throw new ConstraintDefinitionException(sprintf('Unable to find the object manager associated with an entity of class "%s".',$objectClass));
Copy link
Contributor

Choose a reason for hiding this comment

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

this should use$entityClass instead of$objectClass

yceruto reacted with thumbs up emoji
@@ -121,22 +127,7 @@ public function validate($entity, Constraint $constraint)
return;
}

if (null !== $constraint->entityClass) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess there was a reason for this?

#15002

Copy link
Contributor

Choose a reason for hiding this comment

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

well.. given $entity now can be any $object, it doesnt make sense to validate the inheritance anymore. I dont think it hurts.

cc@ogizanagi

* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
* @expectedExceptionMessage The "Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity" entity repository does not support the "Symfony\Bridge\Doctrine\Tests\Fixtures\Person" entity. The entity should be an instance of or extend "Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity".
*/
public function testInvalidateRepositoryForInheritance()
Copy link
Contributor

Choose a reason for hiding this comment

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

see my other comment: I think there was a reason for this:#15002

Copy link
Contributor

Choose a reason for hiding this comment

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

we should add new tests for sure.

@@ -30,6 +30,7 @@ class UniqueEntity extends Constraint
public $em = null;
public $entityClass = null;
public $repositoryMethod = 'findBy';
public $isEntityToUpdateMethod = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

I also think something likeisEqualToEntityMethod would make more sense.

}

$method = $constraint->isEntityToUpdateMethod;
if (null !== $method) {
Copy link
Contributor

Choose a reason for hiding this comment

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

if (null !==$method =$constraint->isEntityToUpdateMethod) {

yceruto and ro0NL reacted with thumbs up emoji
@stofstof added this to the4.1 milestoneNov 17, 2017
@linaori
Copy link
Contributor

Why would I use this for a DTO? Why would I even use this for an Entity? This check is very sensitive to race conditions and makes it therefore redundant IMO. You still have to catch the duplicate key exception either and give the user a unique field error, why would to first do the check and then check via a catch statement?

@MatTheCat
Copy link
ContributorAuthor

@iltar what would be the easiest way to map a duplicate exception to a form field?

Anyways I find my proposal becoming too heavy to be a better alternative to just create specific constraints per DTO.

@ro0NL
Copy link
Contributor

ro0NL commentedNov 19, 2017
edited
Loading

agree. I would settle with validating beforehand in case of e.g. a user form. In general the unique field exception would be caught anyway - resulting in a system error page or so.

@linaori
Copy link
Contributor

@MatTheCat Whenever you try to flush an entity that would trigger the database to violate the unique constraint, it will throw a unique constraint exception. You can catch this exception, add a form error and handle it gracefully. Even if you use a check for unique values, if 2 users submit the same value, it might return non-unique for both and then trigger the exception either way for 1 of the users as the database is the only point where this can be enforced. So you're either going to have to implement it twice, or accept the fact that you spew 500 errors to the user, just because you want the validation constraint on object. I understand that it's more logical to add it to the object, but it's also useless in this scenario if you have to rely on the database insertion.

yceruto reacted with thumbs up emoji

@MatTheCat
Copy link
ContributorAuthor

If I understand correctly this constraint seems quite useless whatever a form populates..! Optimally the controller would catch a UniqueConstraintViolationException and add an error to the form? Is there a way to get the concerned field?

@MatTheCatMatTheCat deleted the ticket_22592 branchNovember 20, 2017 11:21
fabpot added a commit that referenced this pull requestMay 2, 2024
…ss against unique entity constraint (wkania)This PR was merged into the 7.1 branch.Discussion----------[DoctrineBridge][Validator] Allow validating every class against unique entity constraint| Q             | A| ------------- | ---| Branch?       | 7.x <!-- see below -->| Bug fix?      | no| New feature?  | yes <!-- pleasedate src/**/CHANGELOG.md files -->| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->| Tickets       |Fix#22592| License       | MIT| Doc PR        |symfony/symfony-docs#14458 <!-- required for new features -->Yet another try to allow validating every class against unique entity constraint.Based on the knowledge from issue#22592 and pull request#24974.This constraint doesn’t provide any protection against race conditions, which is enough in most cases. You can always try-catch flush method. Let's not talk about this problem.Current state:- can be applied only to an entity,- support entity inheritance,- can validate unique combinations of multiple fields,- meaningful errors related to entities,- is valid during an update, when the only matched entity is the same as the entity being validatedNew state:- preserve the current state,- all old tests pass (no changes in them),- no breaking changes,- can be applied to any class (like DTO),- can map object fields to entity fields (optional),- when the object update some entity, there is an option to provide the identifier field names to match that entity (optional)Examples:1. DTO adds a new entity.```namespace App\Message;use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;/** * `@UniqueEntity`(fields={"name"}, entityClass="App\Entity\User") */class HireAnEmployee{    public $name;    public function __construct($name)    {        $this->name = $name;    }}```2. DTO adds a new entity, but the name of the field in the entity is different.```namespace App\Message;use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;/** * `@UniqueEntity`(fields={"name": "username"}, entityClass="App\Entity\User") */class HireAnEmployee{    public $name;    public function __construct($name)    {        $this->name = $name;    }}```3. DTO updates an entity.```namespace App\Message;use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;/** * `@UniqueEntity`(fields={"name"}, entityClass="App\Entity\User", identifierFieldNames={"uid": "id"}) */class UpdateEmployeeProfile{    public $uid;    public $name;    public function __construct($uid, $name)    {        $this->uid = $uid;        $this->name = $name;    }}```Commits-------adb9afa [DoctrineBridge][Validator] Allow validating every class against unique entity constraint
symfony-splitter pushed a commit to symfony/doctrine-bridge that referenced this pull requestMay 2, 2024
…ss against unique entity constraint (wkania)This PR was merged into the 7.1 branch.Discussion----------[DoctrineBridge][Validator] Allow validating every class against unique entity constraint| Q             | A| ------------- | ---| Branch?       | 7.x <!-- see below -->| Bug fix?      | no| New feature?  | yes <!-- pleasedate src/**/CHANGELOG.md files -->| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->| Tickets       | Fix #22592| License       | MIT| Doc PR        |symfony/symfony-docs#14458 <!-- required for new features -->Yet another try to allow validating every class against unique entity constraint.Based on the knowledge from issuesymfony/symfony#22592 and pull requestsymfony/symfony#24974.This constraint doesn’t provide any protection against race conditions, which is enough in most cases. You can always try-catch flush method. Let's not talk about this problem.Current state:- can be applied only to an entity,- support entity inheritance,- can validate unique combinations of multiple fields,- meaningful errors related to entities,- is valid during an update, when the only matched entity is the same as the entity being validatedNew state:- preserve the current state,- all old tests pass (no changes in them),- no breaking changes,- can be applied to any class (like DTO),- can map object fields to entity fields (optional),- when the object update some entity, there is an option to provide the identifier field names to match that entity (optional)Examples:1. DTO adds a new entity.```namespace App\Message;use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;/** * `@UniqueEntity`(fields={"name"}, entityClass="App\Entity\User") */class HireAnEmployee{    public $name;    public function __construct($name)    {        $this->name = $name;    }}```2. DTO adds a new entity, but the name of the field in the entity is different.```namespace App\Message;use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;/** * `@UniqueEntity`(fields={"name": "username"}, entityClass="App\Entity\User") */class HireAnEmployee{    public $name;    public function __construct($name)    {        $this->name = $name;    }}```3. DTO updates an entity.```namespace App\Message;use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;/** * `@UniqueEntity`(fields={"name"}, entityClass="App\Entity\User", identifierFieldNames={"uid": "id"}) */class UpdateEmployeeProfile{    public $uid;    public $name;    public function __construct($uid, $name)    {        $this->uid = $uid;        $this->name = $name;    }}```Commits-------adb9afa4a5 [DoctrineBridge][Validator] Allow validating every class against unique entity constraint
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@dmaicherdmaicherdmaicher left review comments

@ro0NLro0NLro0NL left review comments

@ycerutoycerutoyceruto left review comments

Assignees
No one assigned
Projects
None yet
Milestone
4.1
Development

Successfully merging this pull request may close these issues.

7 participants
@MatTheCat@dmaicher@linaori@ro0NL@yceruto@stof@carsonbot

[8]ページ先頭

©2009-2025 Movatter.jp