Issue 1:
Symfony version(s) affected: 4.1.0

Description

I'm using the form component to map request data to a DTO (not an entity), and I'm doing some validation in the form component. One of the validations I'm doing is on an email field. I'm using the UniqueEntity constraint to do that. But in order to keep my domain logic separated from the Validation component, I'm registering the constraints in the form itself. Apparently the UniqueEntity constraint does not work in that scenario.

How to reproduce

First, create a dummy User entity, just with email.

Then, create this DTO class outside the entity folder:

<?php

namespace App\Domain\Register;

/**
* Class RegisterStoreCommand
* @package App\Domain\Register
* @author Matías Navarro Carter <mnavarro@option.cl>
*/
class RegisterNewStoreCommand
{
/**
* @var string
*/
private $name;
/**
* @var string
*/
private $email;
/**
* @var string
*/
private $plainPassword;
/**
* @var string
*/
private $storeName;
/**
* @var string
*/
private $storeWebsite;

public function __construct(string $name, string $email, string $plainPassword, string $storeName, string $storeWebsite)
{
$this->name = $name;
$this->email = $email;
$this->plainPassword = $plainPassword;
$this->storeName = $storeName;
$this->storeWebsite = $storeWebsite;
}

/**
* @return string
*/
public function getName(): string
{
return $this->name;
}

/**
* @return string
*/
public function getEmail(): string
{
return $this->email;
}

/**
* @return string
*/
public function getPlainPassword(): string
{
return $this->plainPassword;
}

/**
* @return string
*/
public function getStoreName(): string
{
return $this->storeName;
}

/**
* @return string
*/
public function getStoreWebsite(): string
{
return $this->storeWebsite;
}
}

After that, the FormType:

<?php

namespace App\Form;

use App\Domain\Register\RegisterNewStoreCommand;
use App\Entity\User;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Url;

/**
* Class RegisterStoreType
* @package App\Form
* @author Matías Navarro Carter <mnavarro@option.cl>
*/
class RegisterStoreType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class, [
'constraints' => [
new NotBlank()
]
])
->add('email', EmailType::class, [
'constraints' => [
new NotBlank()
]
])
->add('plainPassword', RepeatedType::class, [
'type' => PasswordType::class,
'constraints' => [
new NotBlank()
]
])
->add('storeName', TextType::class, [
'constraints' => [
new NotBlank()
]
])
->add('storeWebsite', TextType::class, [
'constraints' => [
new NotBlank(),
new Url()
]
])
;
}

public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'constraints' => [
new UniqueEntity(['fields' => ['email'], 'entityClass' => User::class])
],
'data_class' => RegisterNewStoreCommand::class,
'empty_data' => function(FormInterface $form) {
return new RegisterNewStoreCommand(
$form->get('name')->getData(),
$form->get('email')->getData(),
$form->get('plainPassword')->getData(),
$form->get('storeName')->getData(),
$form->get('storeWebsite')->getData()
);
}
]);
}
}

Possible Solution

I think that the solution may be to give more consideration to the 'entityClass' setting in the constraint definition. Could be as simple as doing a getManagerForClass($constraint->entityClass) in the validator. I don't know the side effects of that, but if a user is setting an entity class, I think that is what should be used to get the manager.

Additional context

I'm doing all this DTO thing because I'm working on a DDD project, and I have a command bus that will handle the resulting object from the form.

Issue 2:
Q
A

Bug report?
yes/no

Feature request?
no

BC Break report?
yes/no

RFC?
yes

Symfony version
3.2.0

I wonder how this should work for very common example using DTOs for forms. I have applied validation to my DTO

AppBundle\Command\Register:
constraints:
- Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity:
fields: email
entityClass: AppBundle\Entity\User
em: default

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.

# Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator

if ($constraint->em) {
$em = $this->registry->getManager($constraint->em);

if (!$em) {
throw new ConstraintDefinitionException(sprintf('Object manager "%s" does not exist.', $constraint->em));
}
} else {
$em = $this->registry->getManagerForClass(get_class($entity));

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

$class = $em->getClassMetadata(get_class($entity));
/* @var $class \Doctrine\Common\Persistence\Mapping\ClassMetadata */

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 ?