Introduce PersonJsonReadDenormalizer and PersonJsonDenormalizer to separate responsibilities for handling person denormalization. Add corresponding test classes for improved coverage. Refactor PersonJsonNormalizer to remove denormalization logic.

This commit is contained in:
2025-09-16 11:14:22 +02:00
parent 27548ad654
commit d9b730627f
5 changed files with 272 additions and 139 deletions

View File

@@ -0,0 +1,113 @@
<?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\Serializer\Normalizer;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\Gender;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonAltName;
use libphonenumber\PhoneNumber;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;
/**
* Denormalize a Person entity from a JSON-like array structure, creating or updating an existing instance.
*
* To find an existing instance by his id, see the @see{PersonJsonReadDenormalizer}.
*/
class PersonJsonDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
use ObjectToPopulateTrait;
public function denormalize($data, string $type, ?string $format = null, array $context = []): Person
{
$person = $this->extractObjectToPopulate($type, $context);
if (null === $person) {
$person = new Person();
}
// Setters applied directly per known field for readability
if (\array_key_exists('firstName', $data)) {
$person->setFirstName($data['firstName']);
}
if (\array_key_exists('lastName', $data)) {
$person->setLastName($data['lastName']);
}
if (\array_key_exists('phonenumber', $data)) {
$person->setPhonenumber($this->denormalizer->denormalize($data['phonenumber'], PhoneNumber::class, $format, $context));
}
if (\array_key_exists('mobilenumber', $data)) {
$person->setMobilenumber($this->denormalizer->denormalize($data['mobilenumber'], PhoneNumber::class, $format, $context));
}
if (\array_key_exists('gender', $data)) {
$gender = $this->denormalizer->denormalize($data['gender'], Gender::class, $format, []);
$person->setGender($gender);
}
if (\array_key_exists('birthdate', $data)) {
$object = $this->denormalizer->denormalize($data['birthdate'], \DateTime::class, $format, $context);
$person->setBirthdate($object);
}
if (\array_key_exists('deathdate', $data)) {
$object = $this->denormalizer->denormalize($data['deathdate'], \DateTimeImmutable::class, $format, $context);
$person->setDeathdate($object);
}
if (\array_key_exists('center', $data)) {
$object = $this->denormalizer->denormalize($data['center'], Center::class, $format, $context);
$person->setCenter($object);
}
if (\array_key_exists('altNames', $data)) {
foreach ($data['altNames'] as $altName) {
$oldAltName = $person
->getAltNames()
->filter(static fn (PersonAltName $n): bool => $n->getKey() === $altName['key'])->first();
if (false === $oldAltName) {
$newAltName = new PersonAltName();
$newAltName->setKey($altName['key']);
$newAltName->setLabel($altName['label']);
$person->addAltName($newAltName);
} else {
$oldAltName->setLabel($altName['label']);
}
}
}
if (\array_key_exists('email', $data)) {
$person->setEmail($data['email']);
}
if (\array_key_exists('civility', $data)) {
$civility = $this->denormalizer->denormalize($data['civility'], Civility::class, $format, []);
$person->setCivility($civility);
}
return $person;
}
public function supportsDenormalization($data, $type, $format = null): bool
{
return Person::class === $type && 'person' === ($data['type'] ?? null) && !isset($data['id']);
}
}

View File

@@ -11,169 +11,31 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\Gender;
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonAltName;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Repository\ResidentialAddressRepository;
use Doctrine\Common\Collections\Collection;
use libphonenumber\PhoneNumber;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;
/**
* Serialize a Person entity.
*/
class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwareInterface, PersonJsonNormalizerInterface
class PersonJsonNormalizer implements NormalizerAwareInterface
{
use DenormalizerAwareTrait;
use NormalizerAwareTrait;
use ObjectToPopulateTrait;
public function __construct(
private readonly ChillEntityRenderExtension $render,
/* TODO: replace by PersonRenderInterface, as sthis is the only one required */
private readonly PersonRepository $repository,
private readonly CenterResolverManagerInterface $centerResolverManager,
private readonly ResidentialAddressRepository $residentialAddressRepository,
private readonly PhoneNumberHelperInterface $phoneNumberHelper,
) {}
public function denormalize($data, $type, $format = null, array $context = [])
{
$person = $this->extractObjectToPopulate($type, $context);
if (\array_key_exists('id', $data) && null === $person) {
$person = $this->repository->find($data['id']);
if (null === $person) {
throw new UnexpectedValueException("The person with id \"{$data['id']}\" does ".'not exists');
}
// currently, not allowed to update a person through api
// if instantiated with id
return $person;
}
if (null === $person) {
$person = new Person();
}
$fields = [
'firstName',
'lastName',
'phonenumber',
'mobilenumber',
'gender',
'birthdate',
'deathdate',
'center',
'altNames',
'email',
'civility',
];
$fields = array_filter(
$fields,
static fn (string $field): bool => \array_key_exists($field, $data)
);
foreach ($fields as $item) {
switch ($item) {
case 'firstName':
$person->setFirstName($data[$item]);
break;
case 'lastName':
$person->setLastName($data[$item]);
break;
case 'phonenumber':
$person->setPhonenumber($this->denormalizer->denormalize($data[$item], PhoneNumber::class, $format, $context));
break;
case 'mobilenumber':
$person->setMobilenumber($this->denormalizer->denormalize($data[$item], PhoneNumber::class, $format, $context));
break;
case 'gender':
$gender = $this->denormalizer->denormalize($data[$item], Gender::class, $format, []);
$person->setGender($gender);
break;
case 'birthdate':
$object = $this->denormalizer->denormalize($data[$item], \DateTime::class, $format, $context);
$person->setBirthdate($object);
break;
case 'deathdate':
$object = $this->denormalizer->denormalize($data[$item], \DateTimeImmutable::class, $format, $context);
$person->setDeathdate($object);
break;
case 'center':
$object = $this->denormalizer->denormalize($data[$item], Center::class, $format, $context);
$person->setCenter($object);
break;
case 'altNames':
foreach ($data[$item] as $altName) {
$oldAltName = $person
->getAltNames()
->filter(static fn (PersonAltName $n): bool => $n->getKey() === $altName['key'])->first();
if (false === $oldAltName) {
$newAltName = new PersonAltName();
$newAltName->setKey($altName['key']);
$newAltName->setLabel($altName['label']);
$person->addAltName($newAltName);
} else {
$oldAltName->setLabel($altName['label']);
}
}
break;
case 'email':
$person->setEmail($data[$item]);
break;
case 'civility':
$civility = $this->denormalizer->denormalize($data[$item], Civility::class, $format, []);
$person->setCivility($civility);
break;
}
}
return $person;
}
/**
* @param Person $person
* @param string|null $format

View File

@@ -0,0 +1,51 @@
<?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\Serializer\Normalizer;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\PersonRepository;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
/**
* Find a Person entity by his id during the denormalization process.
*/
readonly class PersonJsonReadDenormalizer implements DenormalizerInterface
{
public function __construct(private PersonRepository $repository) {}
public function denormalize($data, string $type, ?string $format = null, array $context = []): Person
{
if (!is_array($data)) {
throw new InvalidArgumentException();
}
if (\array_key_exists('id', $data)) {
$person = $this->repository->find($data['id']);
if (null === $person) {
throw new UnexpectedValueException("The person with id \"{$data['id']}\" does ".'not exists');
}
return $person;
}
throw new LogicException();
}
public function supportsDenormalization($data, string $type, ?string $format = null)
{
return is_array($data) && Person::class === $type && 'person' === ($data['type'] ?? null) && isset($data['id']);
}
}

View File

@@ -0,0 +1,21 @@
<?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\Serializer\Normalizer;
use PHPUnit\Framework\TestCase;
/**
* @internal
*
* @coversNothing
*/
class PersonJsonDenormalizerTest extends TestCase {}

View File

@@ -0,0 +1,86 @@
<?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\Serializer\Normalizer;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Serializer\Normalizer\PersonJsonReadDenormalizer;
use PHPUnit\Framework\TestCase;
/**
* @internal
*
* @covers \Chill\PersonBundle\Serializer\Normalizer\PersonJsonReadDenormalizer
*/
final class PersonJsonReadDenormalizerTest extends TestCase
{
public function testSupportsDenormalizationReturnsTrueForValidData(): void
{
$repository = $this->getMockBuilder(PersonRepository::class)
->disableOriginalConstructor()
->getMock();
$denormalizer = new PersonJsonReadDenormalizer($repository);
$data = [
'type' => 'person',
'id' => 123,
];
self::assertTrue($denormalizer->supportsDenormalization($data, Person::class));
}
public function testSupportsDenormalizationReturnsFalseForInvalidData(): void
{
$repository = $this->getMockBuilder(PersonRepository::class)
->disableOriginalConstructor()
->getMock();
$denormalizer = new PersonJsonReadDenormalizer($repository);
// not an array
self::assertFalse($denormalizer->supportsDenormalization('not-an-array', Person::class));
// missing type
self::assertFalse($denormalizer->supportsDenormalization(['id' => 1], Person::class));
// wrong type value
self::assertFalse($denormalizer->supportsDenormalization(['type' => 'not-person', 'id' => 1], Person::class));
// missing id
self::assertFalse($denormalizer->supportsDenormalization(['type' => 'person'], Person::class));
// wrong target class
self::assertFalse($denormalizer->supportsDenormalization(['type' => 'person', 'id' => 1], \stdClass::class));
}
public function testDenormalizeReturnsPersonFromRepository(): void
{
$person = new Person();
$repository = $this->getMockBuilder(PersonRepository::class)
->disableOriginalConstructor()
->onlyMethods(['find'])
->getMock();
$repository->expects(self::once())
->method('find')
->with(123)
->willReturn($person);
$denormalizer = new PersonJsonReadDenormalizer($repository);
$result = $denormalizer->denormalize(['id' => 123], Person::class);
self::assertSame($person, $result);
}
}