From b497bea0a0a5e72e6f00363e5522289ad47e8f66 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 10 Jan 2026 17:12:44 +0100 Subject: [PATCH 1/2] feat(mapper): read CastWith/SerializeWith from interface definitions --- packages/mapper/src/CasterFactory.php | 2 +- packages/mapper/src/SerializerFactory.php | 2 +- .../Integration/Mapper/CasterFactoryTest.php | 10 ++++++++++ .../Fixtures/ConcreteInterfaceValue.php | 18 +++++++++++++++++ .../Mapper/Fixtures/InterfaceValueCaster.php | 20 +++++++++++++++++++ .../Fixtures/InterfaceValueSerializer.php | 20 +++++++++++++++++++ .../Mapper/Fixtures/InterfaceWithCastWith.php | 15 ++++++++++++++ .../Fixtures/InterfaceWithSerializeWith.php | 15 ++++++++++++++ .../ObjectWithInterfaceTypedProperties.php | 14 +++++++++++++ tests/Integration/Mapper/MapperTest.php | 17 ++++++++++++++++ .../Mapper/SerializerFactoryTest.php | 10 ++++++++++ 11 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 tests/Integration/Mapper/Fixtures/ConcreteInterfaceValue.php create mode 100644 tests/Integration/Mapper/Fixtures/InterfaceValueCaster.php create mode 100644 tests/Integration/Mapper/Fixtures/InterfaceValueSerializer.php create mode 100644 tests/Integration/Mapper/Fixtures/InterfaceWithCastWith.php create mode 100644 tests/Integration/Mapper/Fixtures/InterfaceWithSerializeWith.php create mode 100644 tests/Integration/Mapper/Fixtures/ObjectWithInterfaceTypedProperties.php diff --git a/packages/mapper/src/CasterFactory.php b/packages/mapper/src/CasterFactory.php index 3e5c7dd3e..f9f488a27 100644 --- a/packages/mapper/src/CasterFactory.php +++ b/packages/mapper/src/CasterFactory.php @@ -61,7 +61,7 @@ public function forProperty(PropertyReflector $property): ?Caster $type = $property->getType(); $castWith = $property->getAttribute(CastWith::class); - if ($castWith === null && $type->isClass()) { + if ($castWith === null && ($type->isClass() || $type->isInterface())) { $castWith = $type->asClass()->getAttribute(CastWith::class, recursive: true); } diff --git a/packages/mapper/src/SerializerFactory.php b/packages/mapper/src/SerializerFactory.php index d82f19ffa..336d3f65a 100644 --- a/packages/mapper/src/SerializerFactory.php +++ b/packages/mapper/src/SerializerFactory.php @@ -64,7 +64,7 @@ public function forProperty(PropertyReflector $property): ?Serializer $type = $property->getType(); $serializeWith = $property->getAttribute(SerializeWith::class); - if ($serializeWith === null && $type->isClass()) { + if ($serializeWith === null && ($type->isClass() || $type->isInterface())) { $serializeWith = $type->asClass()->getAttribute(SerializeWith::class, recursive: true); } diff --git a/tests/Integration/Mapper/CasterFactoryTest.php b/tests/Integration/Mapper/CasterFactoryTest.php index 83cf61a51..a2e965160 100644 --- a/tests/Integration/Mapper/CasterFactoryTest.php +++ b/tests/Integration/Mapper/CasterFactoryTest.php @@ -10,6 +10,8 @@ use Tempest\Mapper\Casters\IntegerCaster; use Tempest\Mapper\Casters\NativeDateTimeCaster; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; +use Tests\Tempest\Integration\Mapper\Fixtures\InterfaceValueCaster; +use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithInterfaceTypedProperties; use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithSerializerProperties; use function Tempest\Reflection\reflect; @@ -31,4 +33,12 @@ public function test_for_property(): void $this->assertInstanceOf(EnumCaster::class, $factory->forProperty($class->getProperty('unitEnum'))); $this->assertInstanceOf(EnumCaster::class, $factory->forProperty($class->getProperty('backedEnum'))); } + + public function test_caster_from_interface_attribute(): void + { + $factory = $this->container->get(CasterFactory::class); + $class = reflect(ObjectWithInterfaceTypedProperties::class); + + $this->assertInstanceOf(InterfaceValueCaster::class, $factory->forProperty($class->getProperty('castable'))); + } } diff --git a/tests/Integration/Mapper/Fixtures/ConcreteInterfaceValue.php b/tests/Integration/Mapper/Fixtures/ConcreteInterfaceValue.php new file mode 100644 index 000000000..1bbefa2f9 --- /dev/null +++ b/tests/Integration/Mapper/Fixtures/ConcreteInterfaceValue.php @@ -0,0 +1,18 @@ +value; + } +} diff --git a/tests/Integration/Mapper/Fixtures/InterfaceValueCaster.php b/tests/Integration/Mapper/Fixtures/InterfaceValueCaster.php new file mode 100644 index 000000000..4b863ac90 --- /dev/null +++ b/tests/Integration/Mapper/Fixtures/InterfaceValueCaster.php @@ -0,0 +1,20 @@ +getValue(); + } +} diff --git a/tests/Integration/Mapper/Fixtures/InterfaceWithCastWith.php b/tests/Integration/Mapper/Fixtures/InterfaceWithCastWith.php new file mode 100644 index 000000000..3c60e8c0f --- /dev/null +++ b/tests/Integration/Mapper/Fixtures/InterfaceWithCastWith.php @@ -0,0 +1,15 @@ +from([ + 'castable' => 'test-value', + 'serializable' => 'another-value', + ]); + + $this->assertSame('casted:test-value', $object->castable->getValue()); + $this->assertSame('casted:another-value', $object->serializable->getValue()); + + $array = map($object)->toArray(); + + $this->assertSame('serialized:casted:test-value', $array['castable']); + $this->assertSame('serialized:casted:another-value', $array['serializable']); + } } diff --git a/tests/Integration/Mapper/SerializerFactoryTest.php b/tests/Integration/Mapper/SerializerFactoryTest.php index fbd2cac29..7e381d5d0 100644 --- a/tests/Integration/Mapper/SerializerFactoryTest.php +++ b/tests/Integration/Mapper/SerializerFactoryTest.php @@ -16,8 +16,10 @@ use Tempest\Mapper\Serializers\StringSerializer; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; use Tests\Tempest\Integration\Mapper\Fixtures\DoubleStringSerializer; +use Tests\Tempest\Integration\Mapper\Fixtures\InterfaceValueSerializer; use Tests\Tempest\Integration\Mapper\Fixtures\JsonSerializableObject; use Tests\Tempest\Integration\Mapper\Fixtures\NestedObjectB; +use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithInterfaceTypedProperties; use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithSerializerProperties; use Tests\Tempest\Integration\Mapper\Fixtures\SerializableObject; @@ -66,4 +68,12 @@ public function test_for_property(): void $this->assertInstanceOf(NativeDateTimeSerializer::class, $factory->forProperty($class->getProperty('nativeDateTimeInterfaceProp'))); $this->assertInstanceOf(DateTimeSerializer::class, $factory->forProperty($class->getProperty('dateTimeProp'))); } + + public function test_serializer_from_interface_attribute(): void + { + $factory = $this->container->get(SerializerFactory::class); + $class = reflect(ObjectWithInterfaceTypedProperties::class); + + $this->assertInstanceOf(InterfaceValueSerializer::class, $factory->forProperty($class->getProperty('serializable'))); + } } From ed1c4afedafa232cf09fb15fcf78d9ddf783e820 Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 10 Jan 2026 17:13:33 +0100 Subject: [PATCH 2/2] chore: formatting --- tests/Integration/Mapper/Fixtures/ConcreteInterfaceValue.php | 3 +-- .../Mapper/Fixtures/ObjectWithInterfaceTypedProperties.php | 3 +-- tests/Integration/Mapper/MapperTest.php | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/Integration/Mapper/Fixtures/ConcreteInterfaceValue.php b/tests/Integration/Mapper/Fixtures/ConcreteInterfaceValue.php index 1bbefa2f9..bf211d1a7 100644 --- a/tests/Integration/Mapper/Fixtures/ConcreteInterfaceValue.php +++ b/tests/Integration/Mapper/Fixtures/ConcreteInterfaceValue.php @@ -8,8 +8,7 @@ final class ConcreteInterfaceValue implements InterfaceWithCastWith, InterfaceWi { public function __construct( private string $value, - ) { - } + ) {} public function getValue(): string { diff --git a/tests/Integration/Mapper/Fixtures/ObjectWithInterfaceTypedProperties.php b/tests/Integration/Mapper/Fixtures/ObjectWithInterfaceTypedProperties.php index e0e38af31..5f2bb430f 100644 --- a/tests/Integration/Mapper/Fixtures/ObjectWithInterfaceTypedProperties.php +++ b/tests/Integration/Mapper/Fixtures/ObjectWithInterfaceTypedProperties.php @@ -9,6 +9,5 @@ final class ObjectWithInterfaceTypedProperties public function __construct( public InterfaceWithCastWith $castable, public InterfaceWithSerializeWith $serializable, - ) { - } + ) {} } diff --git a/tests/Integration/Mapper/MapperTest.php b/tests/Integration/Mapper/MapperTest.php index d712d1cbc..dc8bb3c4c 100644 --- a/tests/Integration/Mapper/MapperTest.php +++ b/tests/Integration/Mapper/MapperTest.php @@ -18,12 +18,12 @@ use Tests\Tempest\Integration\Mapper\Fixtures\ObjectA; use Tests\Tempest\Integration\Mapper\Fixtures\ObjectFactoryA; use Tests\Tempest\Integration\Mapper\Fixtures\ObjectThatShouldUseCasters; +use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithInterfaceTypedProperties; use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithMapFromAttribute; use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithMapToAttribute; use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithMapToCollisions; use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithMapToCollisionsJsonSerializable; use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithMultipleMapFrom; -use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithInterfaceTypedProperties; use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithStrictOnClass; use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithStrictProperty; use Tests\Tempest\Integration\Mapper\Fixtures\Person;