diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c39c82..00ad293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 1.4.1 under development -- no changes in this release. +- Bug #95: Fix populating readonly properties from parent classes (@vjik) ## 1.4.0 August 23, 2024 diff --git a/src/Hydrator.php b/src/Hydrator.php index ce73c78..ee214c1 100644 --- a/src/Hydrator.php +++ b/src/Hydrator.php @@ -79,6 +79,7 @@ public function hydrate(object $object, array|DataInterface $data = []): void $this->hydrateInternal( $object, + $reflectionClass, ReflectionFilter::filterProperties($object, $reflectionClass), $data ); @@ -108,6 +109,7 @@ public function create(string $class, array|DataInterface $data = []): object $this->hydrateInternal( $object, + $reflectionClass, ReflectionFilter::filterProperties($object, $reflectionClass, $excludeProperties), $data ); @@ -120,6 +122,7 @@ public function create(string $class, array|DataInterface $data = []): object */ private function hydrateInternal( object $object, + ReflectionClass $reflectionClass, array $reflectionProperties, DataInterface $data, ): void { @@ -141,9 +144,25 @@ private function hydrateInternal( new TypeCastContext($this, $property), ); if ($result->isResolved()) { - $property->setValue($object, $result->getValue()); + $this + ->preparePropertyToSetValue($reflectionClass, $property, $propertyName) + ->setValue($object, $result->getValue()); } } } } + + private function preparePropertyToSetValue( + ReflectionClass $class, + ReflectionProperty $property, + string $propertyName, + ): ReflectionProperty { + if ($property->isReadOnly()) { + $declaringClass = $property->getDeclaringClass(); + if ($declaringClass !== $class) { + return $declaringClass->getProperty($propertyName); + } + } + return $property; + } } diff --git a/tests/HydratorTest.php b/tests/HydratorTest.php index 2722ea7..a27e5e3 100644 --- a/tests/HydratorTest.php +++ b/tests/HydratorTest.php @@ -30,6 +30,8 @@ use Yiisoft\Hydrator\Tests\Support\Classes\ConstructorTypeClass; use Yiisoft\Hydrator\Tests\Support\Classes\CounterClass; use Yiisoft\Hydrator\Tests\Support\Classes\FromPredefinedArrayClass; +use Yiisoft\Hydrator\Tests\Support\Classes\Inheritance\ReadOnly\ImageSlideDto; +use Yiisoft\Hydrator\Tests\Support\Classes\Inheritance\Figure\Circle; use Yiisoft\Hydrator\Tests\Support\Classes\InvalidDataResolverClass; use Yiisoft\Hydrator\Tests\Support\Classes\NestedModel\UserModel; use Yiisoft\Hydrator\Tests\Support\Classes\NonInitializedReadonlyProperties; @@ -769,4 +771,42 @@ public function testHydrateNonInitializedReadonlyProperties(): void $this->assertSame('1', $object->a); $this->assertSame('3', $object->b); } + + public function testBaseInheritance(): void + { + $hydrator = new Hydrator(); + + $object = $hydrator->create( + Circle::class, + [ + 'name' => 'Wheel', + 'color' => 'Red', + 'id' => 'x7', + 'radius' => 17, + ] + ); + + $this->assertSame(17, $object->radius); + $this->assertSame('Wheel', $object->name); + $this->assertSame('Red', $object->getColor()); + $this->assertNull($object->getId()); + } + + public function testReadOnlyInheritance(): void + { + $hydrator = new Hydrator(); + + $object = $hydrator->create( + ImageSlideDto::class, + [ + 'src' => '/images/slide.jpg', + 'width' => 200, + 'height' => 300, + ] + ); + + $this->assertSame('/images/slide.jpg', $object->src); + $this->assertSame(200, $object->width); + $this->assertSame(300, $object->height); + } } diff --git a/tests/Support/Classes/Inheritance/Figure/Circle.php b/tests/Support/Classes/Inheritance/Figure/Circle.php new file mode 100644 index 0000000..a244b33 --- /dev/null +++ b/tests/Support/Classes/Inheritance/Figure/Circle.php @@ -0,0 +1,10 @@ +color; + } + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/tests/Support/Classes/Inheritance/ReadOnly/ImageSlideDto.php b/tests/Support/Classes/Inheritance/ReadOnly/ImageSlideDto.php new file mode 100644 index 0000000..ede189a --- /dev/null +++ b/tests/Support/Classes/Inheritance/ReadOnly/ImageSlideDto.php @@ -0,0 +1,10 @@ +