diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index cd70a65e18e77..ce2355a13a579 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -519,10 +519,20 @@ private function writeProperty(array $zval, string $property, mixed $value) if (PropertyWriteInfo::TYPE_NONE !== $mutator->getType()) { $type = $mutator->getType(); + $reflClass = new \ReflectionClass($object); if (PropertyWriteInfo::TYPE_METHOD === $type) { + if ($reflClass->hasMethod($mutator->getName())) { + $reflMethod = $reflClass->getMethod($mutator->getName()); + if ($reflMethod->getNumberOfParameters() >= 1 && $type = $reflMethod->getParameters()[0]->getType()) { + $value = self::coerceValue($value, $type); + } + } $object->{$mutator->getName()}($value); } elseif (PropertyWriteInfo::TYPE_PROPERTY === $type) { + if ($reflClass->hasProperty($mutator->getName())) { + $value = self::coerceValue($value, $reflClass->getProperty($mutator->getName())->getType()); + } $object->{$mutator->getName()} = $value; } elseif (PropertyWriteInfo::TYPE_ADDER_AND_REMOVER === $type) { $this->writeCollection($zval, $property, $value, $mutator->getAdderInfo(), $mutator->getRemoverInfo()); @@ -538,6 +548,20 @@ private function writeProperty(array $zval, string $property, mixed $value) } } + private static function coerceValue(mixed $value, ?\ReflectionType $targetType): mixed + { + // Only attempt coercion for specifically typed targets + if ($targetType instanceof \ReflectionNamedType) { + $type = $targetType->getName(); + if ($value instanceof \DateTimeInterface && is_a($type, \DateTimeInterface::class, true) + && $type !== $value::class && \DateTimeInterface::class !== $type) { + $value = $type::createFromInterface($value); + } + } + + return $value; + } + /** * Adjusts a collection-valued property by calling add*() and remove*() methods. */ diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/DateTimeConversion.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/DateTimeConversion.php new file mode 100644 index 0000000000000..2cc554843ecc8 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/DateTimeConversion.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests\Fixtures; + +/** + * @author Niels Keurentjes + */ +class DateTimeConversion +{ + public \DateTime $publicDateTime; + public \DateTimeImmutable $publicDateTimeImmutable; + public \DateTimeInterface $publicDateTimeInterface; + + private \DateTime $dateTime; + private \DateTimeImmutable $dateTimeImmutable; + + public function getDateTime(): \DateTime + { + return $this->dateTime; + } + + public function setDateTime(\DateTime $dateTime): void + { + $this->dateTime = $dateTime; + } + + public function getDateTimeImmutable(): \DateTimeImmutable + { + return $this->dateTimeImmutable; + } + + public function setDateTimeImmutable(\DateTimeImmutable $dateTimeImmutable): void + { + $this->dateTimeImmutable = $dateTimeImmutable; + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/ExtendedUninitializedProperty.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/ExtendedUninitializedProperty.php index e0c5950d258ec..4c26ab3414036 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/ExtendedUninitializedProperty.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/ExtendedUninitializedProperty.php @@ -13,5 +13,4 @@ class ExtendedUninitializedProperty extends UninitializedProperty { - } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index d98cd73fba5cd..43d118ecc363a 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -20,6 +20,7 @@ use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyAccess\Tests\Fixtures\DateTimeConversion; use Symfony\Component\PropertyAccess\Tests\Fixtures\ExtendedUninitializedProperty; use Symfony\Component\PropertyAccess\Tests\Fixtures\ReturnTyped; use Symfony\Component\PropertyAccess\Tests\Fixtures\TestAdderRemoverInvalidArgumentLength; @@ -919,4 +920,34 @@ public function testSetValueWrongTypeShouldThrowWrappedException() $this->expectExceptionMessage('Expected argument of type "float", "string" given at property path "publicProperty"'); $this->propertyAccessor->setValue($object, 'publicProperty', 'string'); } + + public function testSetValueConvertDateTimeTypes(): void + { + $fixture = new DateTimeConversion(); + $mutable = new \DateTime($mutableDate = '2021-01-01'); + $immutable = new \DateTimeImmutable($immutableDate = '2022-02-02'); + + // Test conversion with public properties + $this->propertyAccessor->setValue($fixture, 'publicDateTimeImmutable', $mutable); + $this->assertSame($this->propertyAccessor->getValue($fixture, 'publicDateTimeImmutable')->format('Y-m-d'), $mutableDate); + + $this->propertyAccessor->setValue($fixture, 'publicDateTime', $immutable); + $this->assertSame($this->propertyAccessor->getValue($fixture, 'publicDateTime')->format('Y-m-d'), $immutableDate); + + // Test conversion via setters/getters + $this->propertyAccessor->setValue($fixture, 'dateTimeImmutable', $mutable); + $this->assertSame($this->propertyAccessor->getValue($fixture, 'dateTimeImmutable')->format('Y-m-d'), $mutableDate); + + $this->propertyAccessor->setValue($fixture, 'dateTime', $immutable); + $this->assertSame($this->propertyAccessor->getValue($fixture, 'dateTime')->format('Y-m-d'), $immutableDate); + + // Ensure conversion also happens on nested properties + $array = [$fixture]; + $this->propertyAccessor->setValue($array, '[0].publicDateTime', new \DateTimeImmutable('2023-03-03')); + $this->assertSame($this->propertyAccessor->getValue($fixture, 'publicDateTime')->format('Y-m-d'), '2023-03-03'); + + // Ensure setting to an interface does not cause conversion or issues + $this->propertyAccessor->setValue($fixture, 'publicDateTimeInterface', $immutable); + $this->assertSame($fixture->publicDateTimeInterface, $immutable); + } }