-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add union object normalizer #40
base: 1.3.x
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Patchlevel\Hydrator\Normalizer; | ||
|
||
use Attribute; | ||
use Patchlevel\Hydrator\Hydrator; | ||
|
||
use function array_flip; | ||
use function array_key_exists; | ||
use function array_keys; | ||
use function implode; | ||
use function is_array; | ||
use function is_object; | ||
use function is_string; | ||
use function sprintf; | ||
|
||
#[Attribute(Attribute::TARGET_PROPERTY)] | ||
final class UnionObjectNormalizer implements Normalizer, HydratorAwareNormalizer | ||
{ | ||
private Hydrator|null $hydrator = null; | ||
|
||
/** @var array<string, class-string> */ | ||
private array $typeToClassMap; | ||
|
||
/** @param array<class-string, string> $classToTypeMap */ | ||
public function __construct( | ||
private readonly array|null $classToTypeMap = null, | ||
private readonly string $typeFieldName = '_type', | ||
) { | ||
$this->typeToClassMap = array_flip($classToTypeMap); | ||
Check failure on line 32 in src/Normalizer/UnionObjectNormalizer.php
|
||
} | ||
|
||
public function setHydrator(Hydrator $hydrator): void | ||
{ | ||
$this->hydrator = $hydrator; | ||
} | ||
|
||
public function normalize(mixed $value): mixed | ||
{ | ||
if (!$this->hydrator) { | ||
throw new MissingHydrator(); | ||
} | ||
|
||
if ($value === null) { | ||
return null; | ||
} | ||
|
||
if (!is_object($value)) { | ||
throw InvalidArgument::withWrongType( | ||
sprintf('%s|null', implode('|', array_keys($this->classToTypeMap))), | ||
Check failure on line 52 in src/Normalizer/UnionObjectNormalizer.php
|
||
$value, | ||
); | ||
} | ||
|
||
if (!array_key_exists($value::class, $this->classToTypeMap)) { | ||
Check failure on line 57 in src/Normalizer/UnionObjectNormalizer.php
|
||
throw InvalidArgument::withWrongType( | ||
sprintf('%s|null', implode('|', array_keys($this->classToTypeMap))), | ||
Check failure on line 59 in src/Normalizer/UnionObjectNormalizer.php
|
||
$value, | ||
); | ||
} | ||
|
||
$data = $this->hydrator->extract($value); | ||
$data[$this->typeFieldName] = $this->classToTypeMap[$value::class]; | ||
Check failure on line 65 in src/Normalizer/UnionObjectNormalizer.php
|
||
|
||
return $data; | ||
} | ||
|
||
public function denormalize(mixed $value): mixed | ||
{ | ||
if (!$this->hydrator) { | ||
throw new MissingHydrator(); | ||
} | ||
|
||
if ($value === null) { | ||
return null; | ||
} | ||
|
||
if (!is_array($value)) { | ||
throw InvalidArgument::withWrongType('array<string, mixed>|null', $value); | ||
} | ||
|
||
if (!array_key_exists($this->typeFieldName, $value)) { | ||
throw new InvalidArgument(sprintf('missing type field "%s"', $this->typeFieldName)); | ||
} | ||
|
||
$type = $value[$this->typeFieldName]; | ||
|
||
if (!is_string($type)) { | ||
throw InvalidArgument::withWrongType('string', $type); | ||
} | ||
|
||
if (!array_key_exists($type, $this->typeToClassMap)) { | ||
throw new InvalidArgument(sprintf('unknown type "%s"', $type)); | ||
} | ||
|
||
$className = $this->typeToClassMap[$type]; | ||
unset($value[$this->typeFieldName]); | ||
|
||
return $this->hydrator->hydrate($className, $value); | ||
} | ||
|
||
/** | ||
* @return array{ | ||
Check failure on line 105 in src/Normalizer/UnionObjectNormalizer.php
|
||
* typeToClassMap: array<string, class-string>, | ||
* classToTypeMap: array<class-string, string>, | ||
* typeFieldName: string, | ||
* hydrator: null | ||
* } | ||
*/ | ||
public function __serialize(): array | ||
{ | ||
return [ | ||
Check failure on line 114 in src/Normalizer/UnionObjectNormalizer.php
|
||
'typeToClassMap' => $this->typeToClassMap, | ||
'classToTypeMap' => $this->classToTypeMap, | ||
'typeFieldName' => $this->typeFieldName, | ||
'hydrator' => null, | ||
]; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Patchlevel\Hydrator\Tests\Unit\Normalizer; | ||
|
||
use Attribute; | ||
use Patchlevel\Hydrator\Hydrator; | ||
use Patchlevel\Hydrator\Normalizer\InvalidArgument; | ||
use Patchlevel\Hydrator\Normalizer\MissingHydrator; | ||
use Patchlevel\Hydrator\Normalizer\UnionObjectNormalizer; | ||
use Patchlevel\Hydrator\Tests\Unit\Fixture\Email; | ||
use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated; | ||
use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileId; | ||
use PHPUnit\Framework\TestCase; | ||
use Prophecy\PhpUnit\ProphecyTrait; | ||
|
||
use function serialize; | ||
use function unserialize; | ||
|
||
#[Attribute(Attribute::TARGET_PROPERTY)] | ||
final class UnionObjectNormalizerTest extends TestCase | ||
{ | ||
use ProphecyTrait; | ||
|
||
public function testNormalizeMissingHydrator(): void | ||
{ | ||
$this->expectException(MissingHydrator::class); | ||
|
||
$normalizer = new UnionObjectNormalizer([ProfileCreated::class => 'created']); | ||
$this->assertEquals(null, $normalizer->normalize(null)); | ||
} | ||
|
||
public function testDenormalizeMissingHydrator(): void | ||
{ | ||
$this->expectException(MissingHydrator::class); | ||
|
||
$normalizer = new UnionObjectNormalizer([ProfileCreated::class => 'created']); | ||
$this->assertEquals(null, $normalizer->denormalize(null)); | ||
} | ||
|
||
public function testNormalizeWithNull(): void | ||
{ | ||
$hydrator = $this->prophesize(Hydrator::class); | ||
|
||
$normalizer = new UnionObjectNormalizer([ProfileCreated::class => 'created']); | ||
$normalizer->setHydrator($hydrator->reveal()); | ||
|
||
$this->assertEquals(null, $normalizer->normalize(null)); | ||
} | ||
|
||
public function testDenormalizeWithNull(): void | ||
{ | ||
$hydrator = $this->prophesize(Hydrator::class); | ||
|
||
$normalizer = new UnionObjectNormalizer([ProfileCreated::class => 'created']); | ||
$normalizer->setHydrator($hydrator->reveal()); | ||
|
||
$this->assertEquals(null, $normalizer->denormalize(null)); | ||
} | ||
|
||
public function testNormalizeWithInvalidArgument(): void | ||
{ | ||
$this->expectException(InvalidArgument::class); | ||
$this->expectExceptionMessage('type "Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated|null" was expected but "string" was passed.'); | ||
|
||
$hydrator = $this->prophesize(Hydrator::class); | ||
|
||
$normalizer = new UnionObjectNormalizer([ProfileCreated::class => 'created']); | ||
$normalizer->setHydrator($hydrator->reveal()); | ||
$normalizer->normalize('foo'); | ||
} | ||
|
||
public function testDenormalizeWithInvalidArgument(): void | ||
{ | ||
$this->expectException(InvalidArgument::class); | ||
$this->expectExceptionMessage('array<string, mixed>|null" was expected but "string" was passed.'); | ||
|
||
$hydrator = $this->prophesize(Hydrator::class); | ||
|
||
$normalizer = new UnionObjectNormalizer([ProfileCreated::class => 'created']); | ||
$normalizer->setHydrator($hydrator->reveal()); | ||
$normalizer->denormalize('foo'); | ||
} | ||
|
||
public function testNormalizeWithValue(): void | ||
{ | ||
$hydrator = $this->prophesize(Hydrator::class); | ||
|
||
$event = new ProfileCreated( | ||
ProfileId::fromString('1'), | ||
Email::fromString('info@patchlevel.de'), | ||
); | ||
|
||
$hydrator->extract($event) | ||
->willReturn(['profileId' => '1', 'email' => 'info@patchlevel.de']) | ||
->shouldBeCalledOnce(); | ||
|
||
$normalizer = new UnionObjectNormalizer([ProfileCreated::class => 'created']); | ||
$normalizer->setHydrator($hydrator->reveal()); | ||
|
||
self::assertEquals( | ||
$normalizer->normalize($event), | ||
['profileId' => '1', 'email' => 'info@patchlevel.de', '_type' => 'created'], | ||
); | ||
} | ||
|
||
public function testDenormalizeWithValue(): void | ||
{ | ||
$hydrator = $this->prophesize(Hydrator::class); | ||
|
||
$expected = new ProfileCreated( | ||
ProfileId::fromString('1'), | ||
Email::fromString('info@patchlevel.de'), | ||
); | ||
|
||
$hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', 'email' => 'info@patchlevel.de']) | ||
->willReturn($expected) | ||
->shouldBeCalledOnce(); | ||
|
||
$normalizer = new UnionObjectNormalizer([ProfileCreated::class => 'created']); | ||
$normalizer->setHydrator($hydrator->reveal()); | ||
|
||
$this->assertEquals( | ||
$expected, | ||
$normalizer->denormalize(['profileId' => '1', 'email' => 'info@patchlevel.de', '_type' => 'created']), | ||
); | ||
} | ||
|
||
public function testSerialize(): void | ||
{ | ||
$hydrator = $this->prophesize(Hydrator::class); | ||
|
||
$normalizer = new UnionObjectNormalizer([ProfileCreated::class => 'created']); | ||
$normalizer->setHydrator($hydrator->reveal()); | ||
|
||
$serialized = serialize($normalizer); | ||
$normalizer2 = unserialize($serialized); | ||
|
||
self::assertInstanceOf(UnionObjectNormalizer::class, $normalizer2); | ||
self::assertEquals(new UnionObjectNormalizer([ProfileCreated::class => 'created']), $normalizer2); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's not the type but instead the type key right?