From 22431c5d318bc9125c2c64cb8d48a5e36428c835 Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Mon, 3 Oct 2022 15:27:34 +0200 Subject: [PATCH 1/3] Implement DateTime type conversion in PropertyAccess --- .../PropertyAccess/PropertyAccessor.php | 23 ++++++++ .../Tests/Fixtures/DateTimeConversion.php | 56 +++++++++++++++++++ .../Tests/PropertyAccessorTest.php | 27 +++++++++ 3 files changed, 106 insertions(+) create mode 100644 src/Symfony/Component/PropertyAccess/Tests/Fixtures/DateTimeConversion.php diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index cd70a65e18e77..517380531d1f1 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,19 @@ 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 && $type !== get_class($value) && $type !== \DateTimeInterface::class) { + $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..2b6e636403de4 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/DateTimeConversion.php @@ -0,0 +1,56 @@ + + * + * 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; + + private \DateTime $dateTime; + private \DateTimeImmutable $dateTimeImmutable; + + /** + * @return \DateTime + */ + public function getDateTime(): \DateTime + { + return $this->dateTime; + } + + /** + * @param \DateTime $dateTime + */ + public function setDateTime(\DateTime $dateTime): void + { + $this->dateTime = $dateTime; + } + + /** + * @return \DateTimeImmutable + */ + public function getDateTimeImmutable(): \DateTimeImmutable + { + return $this->dateTimeImmutable; + } + + /** + * @param \DateTimeImmutable $dateTimeImmutable + */ + public function setDateTimeImmutable(\DateTimeImmutable $dateTimeImmutable): void + { + $this->dateTimeImmutable = $dateTimeImmutable; + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index d98cd73fba5cd..6cbda5eae94f5 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,30 @@ 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'); + } } From c48d34a8cdc40a706b6907f5ead611e0c031c193 Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Mon, 3 Oct 2022 16:23:36 +0200 Subject: [PATCH 2/3] Fix code style --- .../Component/PropertyAccess/PropertyAccessor.php | 2 +- .../Tests/Fixtures/DateTimeConversion.php | 12 ------------ .../Tests/Fixtures/ExtendedUninitializedProperty.php | 1 - 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 517380531d1f1..00f36ebdbbd03 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -553,7 +553,7 @@ private static function coerceValue(mixed $value, ?\ReflectionType $targetType): // Only attempt coercion for specifically typed targets if ($targetType instanceof \ReflectionNamedType) { $type = $targetType->getName(); - if ($value instanceof \DateTimeInterface && $type !== get_class($value) && $type !== \DateTimeInterface::class) { + if ($value instanceof \DateTimeInterface && $type !== $value::class && \DateTimeInterface::class !== $type) { $value = $type::createFromInterface($value); } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/DateTimeConversion.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/DateTimeConversion.php index 2b6e636403de4..eadac7f9a83b5 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/DateTimeConversion.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/DateTimeConversion.php @@ -22,33 +22,21 @@ class DateTimeConversion private \DateTime $dateTime; private \DateTimeImmutable $dateTimeImmutable; - /** - * @return \DateTime - */ public function getDateTime(): \DateTime { return $this->dateTime; } - /** - * @param \DateTime $dateTime - */ public function setDateTime(\DateTime $dateTime): void { $this->dateTime = $dateTime; } - /** - * @return \DateTimeImmutable - */ public function getDateTimeImmutable(): \DateTimeImmutable { return $this->dateTimeImmutable; } - /** - * @param \DateTimeImmutable $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 { - } From bef7f66f2356adf74adc749bbf4fb28f10d762d3 Mon Sep 17 00:00:00 2001 From: Niels Keurentjes Date: Mon, 3 Oct 2022 16:35:07 +0200 Subject: [PATCH 3/3] Improve type safety --- src/Symfony/Component/PropertyAccess/PropertyAccessor.php | 3 ++- .../PropertyAccess/Tests/Fixtures/DateTimeConversion.php | 1 + .../Component/PropertyAccess/Tests/PropertyAccessorTest.php | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 00f36ebdbbd03..ce2355a13a579 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -553,7 +553,8 @@ private static function coerceValue(mixed $value, ?\ReflectionType $targetType): // Only attempt coercion for specifically typed targets if ($targetType instanceof \ReflectionNamedType) { $type = $targetType->getName(); - if ($value instanceof \DateTimeInterface && $type !== $value::class && \DateTimeInterface::class !== $type) { + if ($value instanceof \DateTimeInterface && is_a($type, \DateTimeInterface::class, true) + && $type !== $value::class && \DateTimeInterface::class !== $type) { $value = $type::createFromInterface($value); } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/DateTimeConversion.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/DateTimeConversion.php index eadac7f9a83b5..2cc554843ecc8 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/DateTimeConversion.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/DateTimeConversion.php @@ -18,6 +18,7 @@ class DateTimeConversion { public \DateTime $publicDateTime; public \DateTimeImmutable $publicDateTimeImmutable; + public \DateTimeInterface $publicDateTimeInterface; private \DateTime $dateTime; private \DateTimeImmutable $dateTimeImmutable; diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 6cbda5eae94f5..43d118ecc363a 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -945,5 +945,9 @@ public function testSetValueConvertDateTimeTypes(): void $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); } }