diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/Identifier/StringIdentifier.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/Identifier/StringIdentifier.php index 9979d0f83..4a032a226 100644 --- a/src/Bundle/ChillPersonBundle/PersonIdentifier/Identifier/StringIdentifier.php +++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/Identifier/StringIdentifier.php @@ -26,7 +26,7 @@ final readonly class StringIdentifier implements PersonIdentifierEngineInterface public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string { - return $value['content'] ?? ''; + return trim($value['content'] ?? ''); } public function buildForm(FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void @@ -36,7 +36,7 @@ final readonly class StringIdentifier implements PersonIdentifierEngineInterface public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string { - return $identifier?->getValue()['content'] ?? ''; + return trim($identifier?->getValue()['content'] ?? ''); } public function isEmpty(PersonIdentifier $identifier): bool diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/RequiredIdentifierConstraint.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/RequiredIdentifierConstraint.php new file mode 100644 index 000000000..872dd8113 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/RequiredIdentifierConstraint.php @@ -0,0 +1,27 @@ +identifierManager->getWorkers() as $worker) { + if (IdentifierPresenceEnum::REQUIRED !== $worker->getDefinition()->getPresence()) { + continue; + } + + $identifier = $value->findFirst(fn (int $key, PersonIdentifier $identifier) => $identifier->getDefinition() === $worker->getDefinition()); + + if (null === $identifier || $worker->isEmpty($identifier)) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ value }}', $worker->renderAsString($identifier)) + ->setParameter('definition_id', (string) $worker->getDefinition()->getId()) + ->atPath('identifiers') + ->setCode('c08b7b32-947f-11f0-8608-9b8560e9bf05') + ->addViolation(); + } + } + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Validator/RequiredIdentifierConstraintValidatorTest.php b/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Validator/RequiredIdentifierConstraintValidatorTest.php new file mode 100644 index 000000000..31df89315 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Validator/RequiredIdentifierConstraintValidatorTest.php @@ -0,0 +1,133 @@ +requiredDefinition = new PersonIdentifierDefinition( + label: ['fr' => 'Identifiant requis'], + engine: 'test.engine', + ); + $this->requiredDefinition->setPresence(IdentifierPresenceEnum::REQUIRED); + $reflection = new \ReflectionClass($this->requiredDefinition); + $id = $reflection->getProperty('id'); + $id->setValue($this->requiredDefinition, 1); + + // Mock only the required methods of the engine used by the validator through the worker + $engineProphecy = $this->prophesize(PersonIdentifierEngineInterface::class); + $engineProphecy->isEmpty(Argument::type(PersonIdentifier::class)) + ->will(function (array $args): bool { + /** @var PersonIdentifier $identifier */ + $identifier = $args[0]; + + return '' === trim($identifier->getValue()['content'] ?? ''); + }); + $engineProphecy->renderAsString(Argument::any(), Argument::any()) + ->will(function (array $args): string { + /** @var PersonIdentifier|null $identifier */ + $identifier = $args[0] ?? null; + + return $identifier?->getValue()['content'] ?? ''; + }); + + $worker = new PersonIdentifierWorker($engineProphecy->reveal(), $this->requiredDefinition); + + // Mock only the required method used by the validator + $managerProphecy = $this->prophesize(PersonIdentifierManagerInterface::class); + $managerProphecy->getWorkers()->willReturn([$worker]); + + return new RequiredIdentifierConstraintValidator($managerProphecy->reveal()); + } + + public function testThrowsOnNonCollectionValue(): void + { + $this->expectException(UnexpectedValueException::class); + $this->validator->validate(new \stdClass(), new RequiredIdentifierConstraint()); + } + + public function testThrowsOnInvalidConstraintType(): void + { + $this->expectException(UnexpectedTypeException::class); + // Provide a valid Collection value so the type check reaches the constraint check + $this->validator->validate(new ArrayCollection(), new NotBlank()); + } + + public function testNoViolationWhenRequiredIdentifierPresentAndNotEmpty(): void + { + $identifier = new PersonIdentifier($this->requiredDefinition); + $identifier->setValue(['content' => 'ABC']); + + $collection = new ArrayCollection([$identifier]); + + $this->validator->validate($collection, new RequiredIdentifierConstraint()); + + $this->assertNoViolation(); + } + + public function testViolationWhenRequiredIdentifierMissing(): void + { + $collection = new ArrayCollection(); + + $this->validator->validate($collection, new RequiredIdentifierConstraint()); + + $this->buildViolation('This identifier must be set') + ->atPath('property.path.identifiers') + ->setParameter('{{ value }}', '') + ->setParameter('definition_id', '1') + ->setCode('c08b7b32-947f-11f0-8608-9b8560e9bf05') + ->assertRaised(); + } + + public function testViolationWhenRequiredIdentifierIsEmpty(): void + { + $identifier = new PersonIdentifier($this->requiredDefinition); + $identifier->setValue(['content' => ' ']); + + $collection = new ArrayCollection([$identifier]); + + $this->validator->validate($collection, new RequiredIdentifierConstraint()); + + $this->buildViolation('This identifier must be set') + ->atPath('property.path.identifiers') + ->setParameter('{{ value }}', ' ') + ->setParameter('definition_id', '1') + ->setCode('c08b7b32-947f-11f0-8608-9b8560e9bf05') + ->assertRaised(); + } +}