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
[DoctrineBridge] UniqueEntity validator problem with entityClass #22592
Comments
The try {
$this->em->flush();
} catch (UniqueConstraintViolationException $exception) {
$form->get('username')->addError(new FormError(
$this->translator->trans('authentication.username.taken', [], 'validators')
));
} |
Even when the purpose of the The target of this constraint is validate that a particular field (or fields) in a Doctrine entity is (are) unique, so what if:
We would have DTO's with multiple /**
* @UniqueEntity(fields={"email"}, entityClass="AppBundle\Entity\User")
* @UniqueEntity(fields={"phone"}, entityClass="AppBundle\Entity\Phone", em="custom")
*/
class RegisterDTO
{
/** @var string */
private $email;
/** @var string */
private $phone;
//...
} Is it something we want/can to support? #14917 related ping @ogizanagi, @HeahDude |
@yceruto I considered something similar in the past; I wasn't sure about reusing the same constraint but now I'd say it makes sense. And the feature would be particularly useful for validating commands (command bus) for instance. But we will also need a mapping between the DTOs fields and the targeted entity fields as their naming might slightly differ. Foolish example: /**
* @UniqueEntity(fields={"userEmail": "email"}, entityClass="AppBundle\Entity\User")
* @UniqueEntity(fields={"userPhone": "phoneNumber"}, entityClass="AppBundle\Entity\Phone", em="custom")
*/
class RegisterDTO
{
/** @var string */
private $userEmail;
/** @var string */
private $userPhone;
//...
} |
That's probably not enough. You also need to consider a way to determine whether or not a possible result is the object currently being modified (the current implementation checks for the object being returned to be the same as the one being validated). |
Please also note that validating unique fields might still trigger the case I've listed before, so you'll need to add that either way. If so, you might just as well omit the validation imo, saves you a lot of work. |
While this is not properly supported in core, one may consider using custom constraints to do so (example https://github.com/EnMarche/en-marche.fr/blob/master/src/Validator/UniqueMembershipValidator.php). |
Here's more generic solution we are using https://gist.github.com/ostrolucky/7b4a7133e51f6b5ffcb598a287acb5f7 Also, can we give this issue more appropriate title please? |
Another possible solution: https://gist.github.com/Xymanek/369f468d02069090770a1e4626d9f1e9 I can make a PR if it looks good |
@iltar I'm unsure how your proposed solution would work. Or maybe I'm confused. But at the point of calling How would you then add errors to a field of an already submitted form? |
While it may not display the errors in the WDT, the error will still be shown when you render the form.
This is true. However, 1ms after the form was valid (it was unique at the moment of validation) and after that moment, someone else just flushed their entity with the exact same value, your flush will now fail with a |
Interesting. You are correct. I was only calling In this case, I rather like the solution. Checking the entities uniqueness is pointless in most regards seeing as you'd have to catch the exception regardless. Many thanks, @iltar. |
We should also consider that in some cases matches should be case-sensitive, while some others should be case-insensitive (i.e. uniqueness of e-mail addresses in a db). |
the comparison (and case-sensitivity) is usually handled by the db engine |
@ogizanagi Perhaps this could help? https://gist.github.com/webbertakken/569409670bfc7c079e276f79260105ed A working mapping like you suggested.
|
I'm facing the same issue using DTO for forms' validation and it would be really nice to have this feature. I just gave a try at @webbertakken and it's working for my use case. Is someone already working on this feature taking care of @xabbuh note to make it implemented and released? |
This breaks my DDD |
My workaround for now is a manual check inside a
And yes, this could be affected by race conditions, where the database will throw a non unique error when this check passes and in the meantime another request creates another entity with that username. But @webbertakken s solution looks good though. P.S. Btw. would it be cool, if the count()-method of a DoctrineRepository would accept a Criteria object (as the |
I use this as a solution to this problem, I'm adding the error to the $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
try {
$this->userRepository->persist($user);
return $this->redirectToRoute('admin.user.index');
} catch (UniqueConstraintViolationException $e) {
$form->get('code')->addError(new FormError('This code is already taken'));
}
}
return $this->render('user/form.html.twig', [
'form' => $form->createView()
]); |
@Albert221 |
I'm coming back on this issue since I'm having it right away. While working on my side-project, I wanted to create a DTO that would contain the For now, as a first solution, I suggest that we provide a quick "fix" (I think it's an issue and not a feature to make sure we can properly use the constraint with DTO-like classes. Next, on a second plan, I think we can provide another format for the I can make a PR for the first one, since I started the work on it before discovering this issue. Friendly ping @nicolas-grekas for a potential decision on the core-team side |
I am also facing this issue with the same scenario: command bus and message using DTO with |
Thank you for this suggestion. |
Facing the same issue and definitely interested 👍 |
Facing the very same issue, this would be a must have feature |
Wondering if this could be worked out (or worked-around) on Doctrine side. Wouldn't it make sense to configure/extend from Doctrine Bridge to make it aware that a DTO corresponds to a specific Entity? This way the UniqueEntity constraints would work transparently and wouldn't need to care if the variable is an actual Entity or a representation of that (DTO). |
I just encountered the same issue and it should definitely be fixed as using a DTO is just cleaner than using the entity directly, especially with multiple APIs and possible necessary transformations. Would be great if it would make it into 5.4 And the PR above #38662 seems to address and fix this issue already, it even takes possible property / field mappings into account, would be nice if it is merged or, if necessary, adjusted and merged so it can make it into 5.4 and 6.0 |
Resurrecting that issue; i just came across this need today myself. Having the ability to call UniqueEntity constraint from outisde an entity would be the perfect answer. I'm totally for it too : class RegistrationInput {
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addConstraint(new UniqueEntity([
'entityClass' => User::class,
'fields' => ['email']
]));
}
}
// sorry - i'm not keen on annotations ^^ And by doing an assoc array we could eventually indicate the input value to field mapping : $metadata->addConstraint(new UniqueEntity([
'entityClass' => User::class,
'fields' => ['email' => 'username']
])); Reading the error message, it sounds like the usage of UniqueEntity is being unnecessary restricted to within an entity.
|
Thank you for this suggestion. |
Personally I still think this is quite an important feature. |
I made my own like this. Not perfect, but it works fine so far. class UniqueDTO extends Constraint
{
public ?string $atPath = null;
public string $entityClass;
public string|array $fields;
public string $message = 'error.duplicate';
public function getTargets(): string
{
return parent::CLASS_CONSTRAINT;
}
} class UniqueDTOValidator extends ConstraintValidator
{
private EntityManagerInterface $em;
private PropertyAccessorInterface $accessor;
public function __construct(EntityManagerInterface $em, PropertyAccessorInterface $accessor)
{
$this->em = $em;
$this->accessor = $accessor;
}
public function validate($value, Constraint $constraint): void
{
if (!$constraint instanceof UniqueDTO) {
throw new UnexpectedTypeException($constraint, UniqueDTO::class);
}
if(!$constraint->entityClass) {
throw new InvalidArgumentException('Entity class is required.');
}
$repository = $this->em->getRepository($constraint->entityClass);
$fields = (array) $constraint->fields;
$criteria = [];
foreach ($fields as $from => $to) {
$criteria[$to] = $this->accessor->getValue($value, $from);
}
if ($repository->count($criteria)) {
$cvb = $this->context->buildViolation($constraint->message);
if ($constraint->atPath) {
$cvb->atPath($constraint->atPath);
}
$cvb->addViolation();
}
}
} $metadata->addConstraint(new UniqueDTO([
'atPath' => 'email',
'entityClass' => User::class,
'fields' => ['email' => 'email'],
'message' => 'email.duplicate',
])); Cheers |
Another good solution for validate DTO's on unique field here |
Hello @nickicool, This won't work if you intend to make an update of your entity. |
It's pretty odd the As for my solution, I decorated the standard <?php
namespace App\Validator\Constraint;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator as SymfonyUniqueEntityValidator;
// ... other imports, trimmed for brevity
#[AsDecorator('doctrine.orm.validator.unique')]
class UniqueEntityValidator extends ConstraintValidator
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
#[MapDecorated] private readonly SymfonyUniqueEntityValidator $inner
) {}
public function validate($value, Constraint $constraint): void
{
if (!$value instanceof CrudDtoInterface) {
$this->inner->validate($value, $constraint);
return;
}
// Custom validation logic here. Part of my CrudDtoInterface is a method `belogsTo()` that returns FQCN of the Doctrine entity the DTO works with. Fetching the EM & checking for uniqueness is a simple use of `->findOneBy(...)`
}
} I looked at the stock validator, and the way it mingles |
I wonder how this should work for very common example using DTOs for forms. I have applied validation to my DTO
With this configuration I get error: "The class 'AppBundle\Command\Register' was not found in the chain configured namespaces AppBundle\Entity"
The flow currently works like this I set
em
for my entity:AppBundle\Entity\User
but in validator we have getting current class metadata for main entity (in my example AppBundle\Command\Register this is not an entity) $em->getClassMetadata(get_class($entity)); so this is why it fails.IMHO if we specify entityClass we should expect that everything will concern this entityClass. Another example of failure is when two entities: entity1 and entity2 belong to different entity managers. If this is an expected behaviour (IMHO it should work like this) we should at least add this information to documentation ?
The text was updated successfully, but these errors were encountered: