Skip to content

Commit d228d34

Browse files
committed
bug symfony#21288 [Doctrine Bridge] fix UniqueEntityValidator for composite object primary keys (dmaicher, HeahDude)
This PR was merged into the 3.1 branch. Discussion ---------- [Doctrine Bridge] fix UniqueEntityValidator for composite object primary keys | Q | A | ------------- | --- | Branch? | master / 3.1 | Bug fix? | yes | New feature? |no | BC breaks? | no | Deprecations? |no | Tests pass? | yes (fail on php 7.1 unrelated?) | Fixed tickets | symfony#21274 | License | MIT | Doc PR | - This PR fixes an issue with the UniqueEntityValidator in case the entity being validated uses a composite primary key via relations to other entities whose classes do not have a `__toString` method. Commits ------- b3ced86 [DoctrineBridge] Fixed invalid unique value as composite key 5aadce3 [Doctrine Bridge] fix UniqueEntityValidator for composite object primary keys
2 parents 4d247b0 + b3ced86 commit d228d34

File tree

3 files changed

+128
-11
lines changed

3 files changed

+128
-11
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace Symfony\Bridge\Doctrine\Tests\Fixtures;
4+
5+
use Doctrine\ORM\Mapping as ORM;
6+
7+
/**
8+
* an entity that has two objects (class without toString methods) as primary key.
9+
*
10+
* @ORM\Entity
11+
*/
12+
class CompositeObjectNoToStringIdEntity
13+
{
14+
/**
15+
* @var SingleIntIdNoToStringEntity
16+
*
17+
* @ORM\Id
18+
* @ORM\ManyToOne(targetEntity="SingleIntIdNoToStringEntity", cascade={"persist"})
19+
* @ORM\JoinColumn(name="object_one_id")
20+
*/
21+
protected $objectOne;
22+
23+
/**
24+
* @var SingleIntIdNoToStringEntity
25+
*
26+
* @ORM\Id
27+
* @ORM\ManyToOne(targetEntity="SingleIntIdNoToStringEntity", cascade={"persist"})
28+
* @ORM\JoinColumn(name="object_two_id")
29+
*/
30+
protected $objectTwo;
31+
32+
public function __construct(SingleIntIdNoToStringEntity $objectOne, SingleIntIdNoToStringEntity $objectTwo)
33+
{
34+
$this->objectOne = $objectOne;
35+
$this->objectTwo = $objectTwo;
36+
}
37+
38+
/**
39+
* @return SingleIntIdNoToStringEntity
40+
*/
41+
public function getObjectOne()
42+
{
43+
return $this->objectOne;
44+
}
45+
46+
/**
47+
* @return SingleIntIdNoToStringEntity
48+
*/
49+
public function getObjectTwo()
50+
{
51+
return $this->objectTwo;
52+
}
53+
}

src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Doctrine\Common\Persistence\ObjectRepository;
1818
use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper;
1919
use Symfony\Bridge\Doctrine\Test\TestRepositoryFactory;
20+
use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeObjectNoToStringIdEntity;
2021
use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity;
2122
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoubleNameEntity;
2223
use Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity;
@@ -140,6 +141,7 @@ private function createSchema(ObjectManager $em)
140141
$em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity'),
141142
$em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity'),
142143
$em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity2'),
144+
$em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeObjectNoToStringIdEntity'),
143145
));
144146
}
145147

@@ -173,7 +175,7 @@ public function testValidateUniqueness()
173175
$this->buildViolation('myMessage')
174176
->atPath('property.path.name')
175177
->setParameter('{{ value }}', '"Foo"')
176-
->setInvalidValue('Foo')
178+
->setInvalidValue($entity2)
177179
->setCode(UniqueEntity::NOT_UNIQUE_ERROR)
178180
->assertRaised();
179181
}
@@ -198,7 +200,7 @@ public function testValidateCustomErrorPath()
198200
$this->buildViolation('myMessage')
199201
->atPath('property.path.bar')
200202
->setParameter('{{ value }}', '"Foo"')
201-
->setInvalidValue('Foo')
203+
->setInvalidValue($entity2)
202204
->setCode(UniqueEntity::NOT_UNIQUE_ERROR)
203205
->assertRaised();
204206
}
@@ -417,7 +419,7 @@ public function testAssociatedEntity()
417419

418420
$this->buildViolation('myMessage')
419421
->atPath('property.path.single')
420-
->setParameter('{{ value }}', $entity1)
422+
->setParameter('{{ value }}', 'object("Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity") identified by (id => 1)')
421423
->setInvalidValue($entity1)
422424
->setCode(UniqueEntity::NOT_UNIQUE_ERROR)
423425
->assertRaised();
@@ -450,12 +452,12 @@ public function testValidateUniquenessNotToStringEntityWithAssociatedEntity()
450452

451453
$this->validator->validate($associated2, $constraint);
452454

453-
$expectedValue = 'Object of class "Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity2" identified by "2"';
455+
$expectedValue = 'object("Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdNoToStringEntity") identified by (id => 1)';
454456

455457
$this->buildViolation('myMessage')
456458
->atPath('property.path.single')
457-
->setParameter('{{ value }}', '"'.$expectedValue.'"')
458-
->setInvalidValue($expectedValue)
459+
->setParameter('{{ value }}', $expectedValue)
460+
->setInvalidValue($entity1)
459461
->setCode(UniqueEntity::NOT_UNIQUE_ERROR)
460462
->assertRaised();
461463
}
@@ -561,4 +563,38 @@ public function testEntityManagerNullObject()
561563

562564
$this->validator->validate($entity, $constraint);
563565
}
566+
567+
public function testValidateUniquenessWithCompositeObjectNoToStringIdEntity()
568+
{
569+
$constraint = new UniqueEntity(array(
570+
'message' => 'myMessage',
571+
'fields' => array('objectOne', 'objectTwo'),
572+
'em' => self::EM_NAME,
573+
));
574+
575+
$objectOne = new SingleIntIdNoToStringEntity(1, 'foo');
576+
$objectTwo = new SingleIntIdNoToStringEntity(2, 'bar');
577+
578+
$this->em->persist($objectOne);
579+
$this->em->persist($objectTwo);
580+
$this->em->flush();
581+
582+
$entity = new CompositeObjectNoToStringIdEntity($objectOne, $objectTwo);
583+
584+
$this->em->persist($entity);
585+
$this->em->flush();
586+
587+
$newEntity = new CompositeObjectNoToStringIdEntity($objectOne, $objectTwo);
588+
589+
$this->validator->validate($newEntity, $constraint);
590+
591+
$expectedValue = 'object("Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdNoToStringEntity") identified by (id => 1)';
592+
593+
$this->buildViolation('myMessage')
594+
->atPath('property.path.objectOne')
595+
->setParameter('{{ value }}', $expectedValue)
596+
->setInvalidValue($objectOne)
597+
->setCode(UniqueEntity::NOT_UNIQUE_ERROR)
598+
->assertRaised();
599+
}
564600
}

src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
namespace Symfony\Bridge\Doctrine\Validator\Constraints;
1313

1414
use Doctrine\Common\Persistence\ManagerRegistry;
15+
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
16+
use Doctrine\Common\Persistence\ObjectManager;
1517
use Symfony\Component\Validator\Constraint;
1618
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
1719
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
@@ -127,15 +129,41 @@ public function validate($entity, Constraint $constraint)
127129
$errorPath = null !== $constraint->errorPath ? $constraint->errorPath : $fields[0];
128130
$invalidValue = isset($criteria[$errorPath]) ? $criteria[$errorPath] : $criteria[$fields[0]];
129131

130-
if (is_object($invalidValue) && !method_exists($invalidValue, '__toString')) {
131-
$invalidValue = sprintf('Object of class "%s" identified by "%s"', get_class($entity), implode(', ', $class->getIdentifierValues($entity)));
132-
}
133-
134132
$this->context->buildViolation($constraint->message)
135133
->atPath($errorPath)
136-
->setParameter('{{ value }}', $this->formatValue($invalidValue, static::OBJECT_TO_STRING | static::PRETTY_DATE))
134+
->setParameter('{{ value }}', $this->formatWithIdentifiers($em, $class, $invalidValue))
137135
->setInvalidValue($invalidValue)
138136
->setCode(UniqueEntity::NOT_UNIQUE_ERROR)
139137
->addViolation();
140138
}
139+
140+
private function formatWithIdentifiers(ObjectManager $em, ClassMetadata $class, $value)
141+
{
142+
if (!is_object($value) || $value instanceof \DateTimeInterface) {
143+
return $this->formatValue($value, self::PRETTY_DATE);
144+
}
145+
146+
// non unique value is a composite PK
147+
if ($class->getName() !== $idClass = get_class($value)) {
148+
$identifiers = $em->getClassMetadata($idClass)->getIdentifierValues($value);
149+
} else {
150+
$identifiers = $class->getIdentifierValues($value);
151+
}
152+
153+
if (!$identifiers) {
154+
return sprintf('object("%s")', $idClass);
155+
}
156+
157+
array_walk($identifiers, function (&$id, $field) {
158+
if (!is_object($id) || $id instanceof \DateTimeInterface) {
159+
$idAsString = $this->formatValue($id, self::PRETTY_DATE);
160+
} else {
161+
$idAsString = sprintf('object("%s")', get_class($id));
162+
}
163+
164+
$id = sprintf('%s => %s', $field, $idAsString);
165+
});
166+
167+
return sprintf('object("%s") identified by (%s)', $idClass, implode(', ', $identifiers));
168+
}
141169
}

0 commit comments

Comments
 (0)