Add validate method to PersonIdentifierEngineInterface and related classes

- Introduced `validate` method in `PersonIdentifierEngineInterface`.
- Added `ValidIdentifierConstraint` to `PersonIdentifier` entity.
- Updated `PersonIdentifierWorker` to implement the new `validate` method.
This commit is contained in:
2025-10-06 15:15:06 +02:00
parent e566f60a4a
commit 60937152c3
12 changed files with 179 additions and 4 deletions

View File

@@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Entity\Identifier;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\PersonIdentifier\Validator\UniqueIdentifierConstraint;
use Chill\PersonBundle\PersonIdentifier\Validator\ValidIdentifierConstraint;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
@@ -20,6 +21,7 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\UniqueConstraint(name: 'chill_person_identifier_unique', columns: ['definition_id', 'canonical'])]
#[ORM\UniqueConstraint(name: 'chill_person_identifier_unique_person_definition', columns: ['definition_id', 'person_id'])]
#[UniqueIdentifierConstraint]
#[ValidIdentifierConstraint]
class PersonIdentifier
{
#[ORM\Id]

View File

@@ -14,6 +14,7 @@ namespace Chill\PersonBundle\PersonIdentifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
interface PersonIdentifierEngineInterface
{
@@ -32,4 +33,6 @@ interface PersonIdentifierEngineInterface
* by the definition, the validation will fails.
*/
public function isEmpty(PersonIdentifier $identifier): bool;
public function validate(ExecutionContextInterface $context, PersonIdentifier $identifier, PersonIdentifierDefinition $definition): void;
}

View File

@@ -24,6 +24,8 @@ interface PersonIdentifierManagerInterface
/**
* @param int|PersonIdentifierDefinition $personIdentifierDefinition an instance of PersonIdentifierDefinition, or his id
*
* @throw PersonIdentifierNotFoundException
*/
public function buildWorkerByPersonIdentifierDefinition(int|PersonIdentifierDefinition $personIdentifierDefinition): PersonIdentifierWorker;
}

View File

@@ -14,8 +14,9 @@ namespace Chill\PersonBundle\PersonIdentifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
final readonly class PersonIdentifierWorker
readonly class PersonIdentifierWorker
{
public function __construct(
private PersonIdentifierEngineInterface $identifierEngine,
@@ -54,4 +55,9 @@ final readonly class PersonIdentifierWorker
{
return $this->identifierEngine->isEmpty($identifier);
}
public function validate(ExecutionContextInterface $context, PersonIdentifier $identifier, PersonIdentifierDefinition $definition): void
{
$this->identifierEngine->validate($context, $identifier, $definition);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\PersonIdentifier\Validator;
use Symfony\Component\Validator\Constraint;
#[\Attribute]
class ValidIdentifierConstraint extends Constraint
{
public function getTargets(): string
{
return self::CLASS_CONSTRAINT;
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\PersonIdentifier\Validator;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
final class ValidIdentifierConstraintValidator extends ConstraintValidator
{
public function __construct(private readonly PersonIdentifierManagerInterface $personIdentifierManager) {}
public function validate($value, Constraint $constraint): void
{
if (!$constraint instanceof ValidIdentifierConstraint) {
throw new UnexpectedTypeException($constraint, ValidIdentifierConstraint::class);
}
if (!$value instanceof PersonIdentifier) {
throw new UnexpectedValueException($value, PersonIdentifier::class);
}
$worker = $this->personIdentifierManager->buildWorkerByPersonIdentifierDefinition($value->getDefinition());
$worker->validate($this->context, $value, $value->getDefinition());
}
}

View File

@@ -14,6 +14,7 @@ namespace Chill\PersonBundle\Tests\Controller;
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
use Chill\MainBundle\Serializer\Normalizer\CollectionNormalizer;
use Chill\PersonBundle\Controller\PersonIdentifierListApiController;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
use Chill\PersonBundle\PersonIdentifier\Normalizer\PersonIdentifierWorkerNormalizer;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface;
@@ -25,6 +26,7 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* @internal
@@ -72,10 +74,12 @@ class PersonIdentifierListApiControllerTest extends TestCase
public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void {}
public function renderAsString(?\Chill\PersonBundle\Entity\Identifier\PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
{
return '';
}
public function validate(ExecutionContextInterface $context, PersonIdentifier $identifier, PersonIdentifierDefinition $definition): void {}
};
$definition1 = new PersonIdentifierDefinition(['en' => 'Label 1'], 'dummy');

View File

@@ -11,12 +11,14 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Tests\PersonIdentifier\Normalizer;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
use Chill\PersonBundle\PersonIdentifier\Normalizer\PersonIdentifierWorkerNormalizer;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* @internal
@@ -40,7 +42,7 @@ class PersonIdentifierWorkerNormalizerTest extends TestCase
public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void {}
public function renderAsString(?\Chill\PersonBundle\Entity\Identifier\PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
{
return '';
}
@@ -70,10 +72,12 @@ class PersonIdentifierWorkerNormalizerTest extends TestCase
public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void {}
public function renderAsString(?\Chill\PersonBundle\Entity\Identifier\PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string
{
return '';
}
public function validate(ExecutionContextInterface $context, PersonIdentifier $identifier, PersonIdentifierDefinition $definition): void {}
};
$definition = new PersonIdentifierDefinition(label: ['en' => 'SSN'], engine: 'string');

View File

@@ -24,6 +24,7 @@ use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* @internal
@@ -71,6 +72,8 @@ class PersonIdRenderingTest extends TestCase
// same behavior as StringIdentifier::renderAsString
return $identifier?->getValue()['content'] ?? '';
}
public function validate(ExecutionContextInterface $context, PersonIdentifier $identifier, PersonIdentifierDefinition $definition): void {}
};
return new PersonIdentifierWorker($engine, $definition);

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\PersonBundle\Tests\PersonIdentifier\Validator;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifier;
use Chill\PersonBundle\Entity\Identifier\PersonIdentifierDefinition;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierManagerInterface;
use Chill\PersonBundle\PersonIdentifier\PersonIdentifierWorker;
use Chill\PersonBundle\PersonIdentifier\Validator\ValidIdentifierConstraint;
use Chill\PersonBundle\PersonIdentifier\Validator\ValidIdentifierConstraintValidator;
use PHPUnit\Framework\Attributes\CoversClass;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
/**
* @internal
*/
#[CoversClass(ValidIdentifierConstraintValidator::class)]
final class ValidIdentifierConstraintValidatorTest extends ConstraintValidatorTestCase
{
use ProphecyTrait;
private object $manager;
private PersonIdentifierManagerInterface|\Prophecy\Prophecy\ObjectProphecy $managerProphecy;
protected function createValidator(): ValidIdentifierConstraintValidator
{
// $this->manager is set in setUp() before parent::setUp() calls this method
return new ValidIdentifierConstraintValidator($this->manager);
}
protected function setUp(): void
{
// Prepare manager prophecy and reveal it before parent::setUp()
$managerProphecy = $this->prophesize(PersonIdentifierManagerInterface::class);
$this->manager = $managerProphecy->reveal();
// Store the prophecy itself for later configuration in tests
$this->managerProphecy = $managerProphecy; // dynamic property for test methods
parent::setUp();
}
public function testItCallsEngineValidateThroughWorker(): void
{
// Arrange a definition and corresponding identifier
$definition = new PersonIdentifierDefinition(label: ['en' => 'Any'], engine: 'any.engine');
$identifier = new PersonIdentifier($definition);
// Create an engine prophecy; we only care that validate() is called once with expected args
$engineProphecy = $this->prophesize(\Chill\PersonBundle\PersonIdentifier\PersonIdentifierEngineInterface::class);
$engineProphecy
->validate($this->context, $identifier, $definition)
->shouldBeCalled();
// Build a real worker that wraps our mocked engine and the concrete definition
$worker = new PersonIdentifierWorker($engineProphecy->reveal(), $definition);
// Configure the manager to return our worker for this definition
$this->managerProphecy
->buildWorkerByPersonIdentifierDefinition($definition)
->willReturn($worker);
// Act: run the validator
$this->validator->validate($identifier, new ValidIdentifierConstraint());
// Assert: no explicit assertion needed; Prophecy will fail if method wasn't called
$this->assertNoViolation();
}
}

View File

@@ -25,6 +25,7 @@ use libphonenumber\PhoneNumber;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* @internal
@@ -79,6 +80,8 @@ final class PersonJsonDenormalizerTest extends TestCase
return '' === $content;
}
public function validate(ExecutionContextInterface $context, PersonIdentifier $identifier, PersonIdentifierDefinition $definition): void {}
};
return new PersonIdentifierWorker($engine, $definition);

View File

@@ -0,0 +1,7 @@
person_identifier:
fixed_length: >-
{limit, plural,
=1 {L'identifier doit contenir exactement 1 caractère}
other {L'identifiant doit contenir exactement # caractères}
}
only_number: "L'identifiant ne doit contenir que des chiffres"