'Test'], 'dummy'); // Force the id for testing purposes $r = new \ReflectionProperty(PersonIdentifierDefinition::class, 'id'); $r->setAccessible(true); $r->setValue($definition, $personIdentifierDefinition); } else { $definition = $personIdentifierDefinition; } $engine = new class () implements PersonIdentifierEngineInterface { public static function getName(): string { return 'dummy'; } public function canonicalizeValue(array $value, PersonIdentifierDefinition $definition): ?string { // trivial canonicalization for tests return isset($value['content']) ? (string) $value['content'] : null; } public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder, PersonIdentifierDefinition $personIdentifierDefinition): void {} public function renderAsString(?PersonIdentifier $identifier, PersonIdentifierDefinition $definition): string { return ''; } public function isEmpty(PersonIdentifier $identifier): bool { $value = $identifier->getValue(); $content = isset($value['content']) ? trim((string) $value['content']) : ''; return '' === $content; } }; return new PersonIdentifierWorker($engine, $definition); } }; } public function testSupportsDenormalizationReturnsTrueForValidData(): void { $denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager()); $data = [ 'type' => 'person', // important: new Person (creation) must not contain an id ]; self::assertTrue($denormalizer->supportsDenormalization($data, Person::class)); } public function testSupportsDenormalizationReturnsFalseForInvalidData(): void { $denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager()); // not an array self::assertFalse($denormalizer->supportsDenormalization('not-an-array', Person::class)); // missing type self::assertFalse($denormalizer->supportsDenormalization([], Person::class)); // wrong type value self::assertFalse($denormalizer->supportsDenormalization(['type' => 'not-person'], Person::class)); // id present means it's not a create payload for this denormalizer self::assertFalse($denormalizer->supportsDenormalization(['type' => 'person', 'id' => 123], Person::class)); // wrong target class self::assertFalse($denormalizer->supportsDenormalization(['type' => 'person'], \stdClass::class)); } public function testDenormalizeMapsPayloadToPersonProperties(): void { $json = <<<'JSON' { "type": "person", "firstName": "Jérome", "lastName": "diallo", "altNames": [ { "key": "jeune_fille", "value": "FJ" } ], "birthdate": null, "deathdate": null, "phonenumber": "", "mobilenumber": "", "email": "", "gender": { "id": 5, "type": "chill_main_gender" }, "center": { "id": 1, "type": "center" }, "civility": null, "identifiers": [ { "type": "person_identifier", "value": { "content": "789456" }, "definition_id": 5 } ] } JSON; $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); $inner = new class () implements DenormalizerInterface { public ?Gender $gender = null; public ?Center $center = null; public function denormalize($data, $type, $format = null, array $context = []) { if (PhoneNumber::class === $type) { return '' === $data ? null : new PhoneNumber(); } if (\DateTime::class === $type || \DateTimeImmutable::class === $type) { return null === $data ? null : new \DateTimeImmutable((string) $data); } if (Gender::class === $type) { return $this->gender ??= new Gender(); } if (Center::class === $type) { return $this->center ??= new Center(); } if (Civility::class === $type) { return null; // input is null in our payload } return null; } public function supportsDenormalization($data, $type, $format = null) { return true; } }; $denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager()); $denormalizer->setDenormalizer($inner); $person = $denormalizer->denormalize($data, Person::class); self::assertInstanceOf(Person::class, $person); self::assertSame('Jérome', $person->getFirstName()); self::assertSame('diallo', $person->getLastName()); // phone numbers: empty strings map to null via the inner denormalizer stub self::assertNull($person->getPhonenumber()); self::assertNull($person->getMobilenumber()); // email passes through as is self::assertSame('', $person->getEmail()); // nested objects are provided by our inner denormalizer and must be set back on the Person self::assertSame($inner->gender, $person->getGender()); self::assertSame($inner->center, $person->getCenter()); // dates are null in the provided payload self::assertNull($person->getBirthdate()); self::assertNull($person->getDeathdate()); // civility is null as provided self::assertNull($person->getCivility()); // altNames: make sure the alt name with key jeune_fille has label FJ $found = false; foreach ($person->getAltNames() as $altName) { if ('jeune_fille' === $altName->getKey()) { $found = true; self::assertSame('FJ', $altName->getLabel()); } } self::assertTrue($found, 'Expected altNames to contain key "jeune_fille"'); $found = false; foreach ($person->getIdentifiers() as $identifier) { if (5 === $identifier->getDefinition()->getId()) { $found = true; self::assertSame(['content' => '789456'], $identifier->getValue()); } } self::assertTrue($found, 'Expected identifiers with definition id 5'); } public function testDenormalizeRemovesEmptyIdentifier(): void { $data = [ 'type' => 'person', 'firstName' => 'Alice', 'lastName' => 'Smith', 'identifiers' => [ [ 'type' => 'person_identifier', 'value' => ['content' => ''], 'definition_id' => 7, ], ], ]; $denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager()); $person = $denormalizer->denormalize($data, Person::class); // The identifier with empty content must be considered empty and removed self::assertSame(0, $person->getIdentifiers()->count(), 'Expected no identifiers to remain on the person'); } public function testDenormalizeRemovesPreviouslyExistingIdentifierWhenIncomingValueIsEmpty(): void { // Prepare an existing Person with a pre-existing identifier (definition id = 9) $definition = new PersonIdentifierDefinition(['en' => 'Test'], 'dummy'); $ref = new \ReflectionProperty(PersonIdentifierDefinition::class, 'id'); $ref->setValue($definition, 9); $existingIdentifier = new PersonIdentifier($definition); $existingIdentifier->setValue(['content' => 'ABC']); $person = new Person(); $person->addIdentifier($existingIdentifier); // Also set the identifier's own id = 9 so that the denormalizer logic matches it // (the current denormalizer matches by PersonIdentifier->getId() === definition_id) $refId = new \ReflectionProperty(PersonIdentifier::class, 'id'); $refId->setValue($existingIdentifier, 9); // Incoming payload sets the same definition id with an empty value $data = [ 'type' => 'person', 'identifiers' => [ [ 'type' => 'person_identifier', 'value' => ['content' => ''], 'definition_id' => 9, ], ], ]; $denormalizer = new PersonJsonDenormalizer($this->createIdentifierManager()); // Use AbstractNormalizer::OBJECT_TO_POPULATE to update the existing person $result = $denormalizer->denormalize($data, Person::class, null, [ AbstractNormalizer::OBJECT_TO_POPULATE => $person, ]); self::assertSame($person, $result, 'Denormalizer should update and return the provided Person instance'); self::assertSame(0, $person->getIdentifiers()->count(), 'The previously existing identifier should be removed when incoming value is empty'); } }