Replace value with canonical in PersonIdentifier unique constraint, repository logic, and tests

- Update unique constraint on `PersonIdentifier` to use `canonical` instead of `value`.
- Refactor repository method `findByDefinitionAndValue` to `findByDefinitionAndCanonical`, updating logic accordingly.
- Adjust validation logic in `UniqueIdentifierConstraintValidator` to align with the new canonical-based approach.
- Modify related integration and unit tests to support the changes.
- Inject `PersonIdentifierManagerInterface` into the repository to handle canonical value generation.
This commit is contained in:
2025-09-24 12:39:37 +02:00
parent 0fd76d3fa8
commit 6ea9af588b
7 changed files with 28 additions and 17 deletions

View File

@@ -17,7 +17,7 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity] #[ORM\Entity]
#[ORM\Table(name: 'chill_person_identifier')] #[ORM\Table(name: 'chill_person_identifier')]
#[ORM\UniqueConstraint(name: 'chill_person_identifier_unique', columns: ['definition_id', 'value'])] #[ORM\UniqueConstraint(name: 'chill_person_identifier_unique', columns: ['definition_id', 'canonical'])]
#[UniqueIdentifierConstraint] #[UniqueIdentifierConstraint]
class PersonIdentifier class PersonIdentifier
{ {
@@ -38,7 +38,7 @@ class PersonIdentifier
public function __construct( public function __construct(
#[ORM\ManyToOne(targetEntity: PersonIdentifierDefinition::class)] #[ORM\ManyToOne(targetEntity: PersonIdentifierDefinition::class)]
#[ORM\JoinColumn(name: 'definition_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] #[ORM\JoinColumn(name: 'definition_id', referencedColumnName: 'id', nullable: false, onDelete: 'RESTRICT')]
private PersonIdentifierDefinition $definition, private PersonIdentifierDefinition $definition,
) {} ) {}

View File

@@ -19,9 +19,11 @@ use Symfony\Component\Form\FormBuilderInterface;
final readonly class StringIdentifier implements PersonIdentifierEngineInterface final readonly class StringIdentifier implements PersonIdentifierEngineInterface
{ {
public const NAME = 'chill-person-bundle.string-identifier';
public static function getName(): string public static function getName(): string
{ {
return 'chill-person-bundle.string-identifier'; return self::NAME;
} }
public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string

View File

@@ -36,7 +36,7 @@ class UniqueIdentifierConstraintValidator extends ConstraintValidator
throw new UnexpectedValueException($value, PersonIdentifier::class); throw new UnexpectedValueException($value, PersonIdentifier::class);
} }
$identifiers = $this->personIdentifierRepository->findByDefinitionAndValue($value->getDefinition(), $value->getValue()); $identifiers = $this->personIdentifierRepository->findByDefinitionAndCanonical($value->getDefinition(), $value->getValue());
if (count($identifiers) > 0) { if (count($identifiers) > 0) {
$persons = array_map(fn (PersonIdentifier $idf): string => $this->personRender->renderString($idf->getPerson(), []), $identifiers); $persons = array_map(fn (PersonIdentifier $idf): string => $this->personRender->renderString($idf->getPerson(), []), $identifiers);

View File

@@ -13,24 +13,29 @@ namespace Chill\PersonBundle\Repository\Identifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier; use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition; use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
class PersonIdentifierRepository extends ServiceEntityRepository class PersonIdentifierRepository extends ServiceEntityRepository
{ {
public function __construct(ManagerRegistry $registry) public function __construct(ManagerRegistry $registry, private readonly PersonIdentifierManagerInterface $personIdentifierManager)
{ {
parent::__construct($registry, PersonIdentifier::class); parent::__construct($registry, PersonIdentifier::class);
} }
public function findByDefinitionAndValue(PersonIdentifierDefinition $definition, array $value): array public function findByDefinitionAndCanonical(PersonIdentifierDefinition $definition, array|string $valueOrCanonical): array
{ {
return $this->createQueryBuilder('p') return $this->createQueryBuilder('p')
->where('p.definition = :definition') ->where('p.definition = :definition')
->andWhere('p.value = :value') ->andWhere('p.canonical = :canonical')
->setParameter('definition', $definition) ->setParameter('definition', $definition)
->setParameter('value', $value, Types::JSON) ->setParameter(
'canonical',
is_string($valueOrCanonical) ?
$valueOrCanonical :
$this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($definition)->canonicalizeValue($valueOrCanonical),
)
->getQuery() ->getQuery()
->getResult(); ->getResult();
} }

View File

@@ -82,7 +82,7 @@ final class UniqueIdentifierConstraintValidatorTest extends ConstraintValidatorT
$identifier->setValue(['value' => 'UNIQ']); $identifier->setValue(['value' => 'UNIQ']);
// Configure repository mock to return empty array // Configure repository mock to return empty array
$this->repository->findByDefinitionAndValue($definition, ['value' => 'UNIQ'])->willReturn([]); $this->repository->findByDefinitionAndCanonical($definition, ['value' => 'UNIQ'])->willReturn([]);
$this->validator->validate($identifier, new UniqueIdentifierConstraint()); $this->validator->validate($identifier, new UniqueIdentifierConstraint());
$this->assertNoViolation(); $this->assertNoViolation();
@@ -108,7 +108,7 @@ final class UniqueIdentifierConstraintValidatorTest extends ConstraintValidatorT
$dup2->setValue(['value' => '123']); $dup2->setValue(['value' => '123']);
// Repository returns duplicates // Repository returns duplicates
$this->repository->findByDefinitionAndValue($definition, ['value' => '123'])->willReturn([$dup1, $dup2]); $this->repository->findByDefinitionAndCanonical($definition, ['value' => '123'])->willReturn([$dup1, $dup2]);
// Person renderer returns names // Person renderer returns names
$this->personRender->renderString($personA, Argument::type('array'))->willReturn('Alice Anderson'); $this->personRender->renderString($personA, Argument::type('array'))->willReturn('Alice Anderson');

View File

@@ -14,6 +14,8 @@ namespace Chill\PersonBundle\Tests\Repository\Identifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier; use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition; use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\PersonIdentifier\Identifier\StringIdentifier;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
use Chill\PersonBundle\Repository\Identifier\PersonIdentifierRepository; use Chill\PersonBundle\Repository\Identifier\PersonIdentifierRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
@@ -25,10 +27,12 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
*/ */
class PersonIdentifierRepositoryTest extends KernelTestCase class PersonIdentifierRepositoryTest extends KernelTestCase
{ {
public function testFindByDefinitionAndValue(): void public function testFindByDefinitionAndCanonical(): void
{ {
self::bootKernel(); self::bootKernel();
$container = self::getContainer(); $container = self::getContainer();
/** @var PersonIdentifierManagerInterface $personIdentifierManager */
$personIdentifierManager = $container->get(PersonIdentifierManagerInterface::class);
/** @var EntityManagerInterface $em */ /** @var EntityManagerInterface $em */
$em = $container->get(EntityManagerInterface::class); $em = $container->get(EntityManagerInterface::class);
@@ -39,23 +43,23 @@ class PersonIdentifierRepositoryTest extends KernelTestCase
self::assertNotNull($person, 'An existing Person is required for this integration test.'); self::assertNotNull($person, 'An existing Person is required for this integration test.');
// Create a definition // Create a definition
$definition = new PersonIdentifierDefinition(['en' => 'Test Identifier'], 'string'); $definition = new PersonIdentifierDefinition(['en' => 'Test Identifier'], StringIdentifier::NAME);
$em->persist($definition); $em->persist($definition);
$em->flush(); $em->flush();
// Create an identifier attached to the person // Create an identifier attached to the person
$value = ['value' => 'ABC-'.bin2hex(random_bytes(4))]; $value = ['content' => 'ABC-'.bin2hex(random_bytes(4))];
$identifier = new PersonIdentifier($definition); $identifier = new PersonIdentifier($definition);
$identifier->setPerson($person); $identifier->setPerson($person);
$identifier->setValue($value); $identifier->setValue($value);
$identifier->setCanonical('canonical-'.$value['value']); $identifier->setCanonical($personIdentifierManager->buildWorkerByPersonIdentifierDefinition($definition)->canonicalizeValue($identifier->getValue()));
$em->persist($identifier); $em->persist($identifier);
$em->flush(); $em->flush();
// Use the repository to find by definition and value // Use the repository to find by definition and value
/** @var PersonIdentifierRepository $repo */ /** @var PersonIdentifierRepository $repo */
$repo = $container->get(PersonIdentifierRepository::class); $repo = $container->get(PersonIdentifierRepository::class);
$results = $repo->findByDefinitionAndValue($definition, $value); $results = $repo->findByDefinitionAndCanonical($definition, $value);
self::assertNotEmpty($results, 'Repository should return at least one result.'); self::assertNotEmpty($results, 'Repository should return at least one result.');
self::assertContainsOnlyInstancesOf(PersonIdentifier::class, $results); self::assertContainsOnlyInstancesOf(PersonIdentifier::class, $results);

View File

@@ -23,7 +23,7 @@ final class Version20250922151020 extends AbstractMigration
public function up(Schema $schema): void public function up(Schema $schema): void
{ {
$this->addSql('CREATE UNIQUE INDEX chill_person_identifier_unique ON chill_person_identifier (definition_id, value)'); $this->addSql('CREATE UNIQUE INDEX chill_person_identifier_unique ON chill_person_identifier (definition_id, canonical)');
} }
public function down(Schema $schema): void public function down(Schema $schema): void