-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,6 +30,7 @@ class UniqueEntity extends Constraint | |
public $em = null; | ||
public $entityClass = null; | ||
public $repositoryMethod = 'findBy'; | ||
public $isEntityToUpdateMethod = null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. Maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DTO can't be equal to an entity There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 /**
* @UniqueEntity(
* fields={"email"},
* entityClass="App\Entity\Author",
* isEqualToMethod="isEqualTo"
* )
*/
class AuthorDTO
{
private $authorId;
private $email;
// ...
public function isEqualTo(Author $author)
{
return $author->getId() === $this->authorId;
}
} Results:
The validator doesn't know nothing about the current event, so the method must be called always. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also think something like |
||
public $fields = array(); | ||
public $errorPath = null; | ||
public $ignoreNull = true; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,13 +34,13 @@ public function __construct(ManagerRegistry $registry) | |
} | ||
|
||
/** | ||
* @param object $entity | ||
* @param object $object | ||
* @param Constraint $constraint | ||
* | ||
* @throws UnexpectedTypeException | ||
* @throws ConstraintDefinitionException | ||
*/ | ||
public function validate($entity, Constraint $constraint) | ||
public function validate($object, Constraint $constraint) | ||
{ | ||
if (!$constraint instanceof UniqueEntity) { | ||
throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\UniqueEntity'); | ||
|
@@ -60,36 +60,42 @@ public function validate($entity, Constraint $constraint) | |
throw new ConstraintDefinitionException('At least one field has to be specified.'); | ||
} | ||
|
||
if (null === $entity) { | ||
if (null === $object) { | ||
return; | ||
} | ||
|
||
$objectClass = get_class($object); | ||
$entityClass = $constraint->entityClass ?: $objectClass; | ||
|
||
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)); | ||
$em = $this->registry->getManagerForClass($entityClass); | ||
|
||
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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should use |
||
} | ||
} | ||
|
||
$class = $em->getClassMetadata(get_class($entity)); | ||
/* @var $class \Doctrine\Common\Persistence\Mapping\ClassMetadata */ | ||
$class = $em->getClassMetadata($entityClass); | ||
|
||
$criteria = array(); | ||
$hasNullValue = false; | ||
|
||
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 $objectField => $entityField) { | ||
if (!$class->hasField($entityField) && !$class->hasAssociation($entityField)) { | ||
throw new ConstraintDefinitionException(sprintf('The field "%s" is not mapped by Doctrine, so it cannot be validated for uniqueness.', $entityField)); | ||
} | ||
|
||
$fieldValue = $class->reflFields[$fieldName]->getValue($entity); | ||
$field = new \ReflectionProperty($objectClass, is_int($objectField) ? $entityField : $objectField); | ||
if (!$field->isPublic()) { | ||
$field->setAccessible(true); | ||
} | ||
$fieldValue = $field->getValue($object); | ||
|
||
if (null === $fieldValue) { | ||
$hasNullValue = true; | ||
|
@@ -99,14 +105,14 @@ public function validate($entity, Constraint $constraint) | |
continue; | ||
} | ||
|
||
$criteria[$fieldName] = $fieldValue; | ||
$criteria[$entityField] = $fieldValue; | ||
|
||
if (null !== $criteria[$fieldName] && $class->hasAssociation($fieldName)) { | ||
if (null !== $criteria[$entityField] && $class->hasAssociation($entityField)) { | ||
/* Ensure the Proxy is initialized before using reflection to | ||
* read its identifiers. This is necessary because the wrapped | ||
* getter methods in the Proxy are being bypassed. | ||
*/ | ||
$em->initializeObject($criteria[$fieldName]); | ||
$em->initializeObject($criteria[$entityField]); | ||
} | ||
} | ||
|
||
|
@@ -121,22 +127,7 @@ public function validate($entity, Constraint $constraint) | |
return; | ||
} | ||
|
||
if (null !== $constraint->entityClass) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess there was a reason for this? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
/* Retrieve repository from given entity name. | ||
* We ensure the retrieved repository can handle the entity | ||
* by checking the entity is the same, or subclass of the supported entity. | ||
*/ | ||
$repository = $em->getRepository($constraint->entityClass); | ||
$supportedClass = $repository->getClassName(); | ||
|
||
if (!$entity instanceof $supportedClass) { | ||
throw new ConstraintDefinitionException(sprintf('The "%s" entity repository does not support the "%s" entity. The entity should be an instance of or extend "%s".', $constraint->entityClass, $class->getName(), $supportedClass)); | ||
} | ||
} else { | ||
$repository = $em->getRepository(get_class($entity)); | ||
} | ||
|
||
$result = $repository->{$constraint->repositoryMethod}($criteria); | ||
$result = $em->getRepository($entityClass)->{$constraint->repositoryMethod}($criteria); | ||
|
||
if ($result instanceof \IteratorAggregate) { | ||
$result = $result->getIterator(); | ||
|
@@ -152,14 +143,29 @@ public function validate($entity, Constraint $constraint) | |
reset($result); | ||
} | ||
|
||
/* If no entity matched the query criteria or a single entity matched, | ||
* which is the same as the entity being validated, the criteria is | ||
* unique. | ||
*/ | ||
if (0 === count($result) || (1 === count($result) && $entity === ($result instanceof \Iterator ? $result->current() : current($result)))) { | ||
if (0 === count($result)) { | ||
return; | ||
} | ||
|
||
if (1 === count($result)) { | ||
$entity = $result instanceof \Iterator ? $result->current() : current($result); | ||
|
||
if ($object === $entity) { | ||
return; | ||
} | ||
|
||
$method = $constraint->isEntityToUpdateMethod; | ||
if (null !== $method) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if (null !== $method = $constraint->isEntityToUpdateMethod) { |
||
if (!method_exists($object, $method)) { | ||
throw new ConstraintDefinitionException(sprintf('Method "%s" does not exist in class %s', $method, $objectClass)); | ||
} | ||
|
||
if (call_user_func([$object, $method], $entity)) { | ||
return; | ||
} | ||
} | ||
} | ||
|
||
$errorPath = null !== $constraint->errorPath ? $constraint->errorPath : $fields[0]; | ||
$invalidValue = isset($criteria[$errorPath]) ? $criteria[$errorPath] : $criteria[$fields[0]]; | ||
|
||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.