From d2ffaf574db94b0d8ad8dabf3a45ad1d6f736b0e Mon Sep 17 00:00:00 2001 From: Chris Riley Date: Tue, 14 Dec 2021 16:48:33 +0000 Subject: [PATCH] \#126: Fixes issues reading/writing typed properties which are uninitialised and not passed in the data array --- .../Visitor/HydratorMethodsVisitor.php | 20 ++++++++++++++-- .../Functional/HydratorFunctionalTest.php | 24 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/GeneratedHydrator/CodeGenerator/Visitor/HydratorMethodsVisitor.php b/src/GeneratedHydrator/CodeGenerator/Visitor/HydratorMethodsVisitor.php index ce4f7391..3a6c7f98 100644 --- a/src/GeneratedHydrator/CodeGenerator/Visitor/HydratorMethodsVisitor.php +++ b/src/GeneratedHydrator/CodeGenerator/Visitor/HydratorMethodsVisitor.php @@ -110,9 +110,14 @@ private function generatePropertyHydrateCall(ObjectProperty $property, string $i return ['$object->' . $propertyName . ' = ' . $inputArrayName . '[' . $escapedName . '] ?? null;']; } + $nullCheck = ' || $object->' . $propertyName . ' !== null && \\array_key_exists(' . $escapedName . ', ' . $inputArrayName . ')'; + if ($property->hasType) { + $nullCheck = ' || isset($object->' . $propertyName . ') && $object->' . $propertyName . ' !== null && \\array_key_exists(' . $escapedName . ', ' . $inputArrayName . ')'; + } + return [ 'if (isset(' . $inputArrayName . '[' . $escapedName . '])', - ' || $object->' . $propertyName . ' !== null && \\array_key_exists(' . $escapedName . ', ' . $inputArrayName . ')', + $nullCheck, ') {', ' $object->' . $propertyName . ' = ' . $inputArrayName . '[' . $escapedName . '];', '}', @@ -141,7 +146,18 @@ private function replaceConstructor(ClassMethod $method): void $bodyParts[] = '$this->extractCallbacks[] = \\Closure::bind(static function ($object, &$values) {'; foreach ($properties as $property) { $propertyName = $property->name; - $bodyParts[] = " \$values['" . $propertyName . "'] = \$object->" . $propertyName . ';'; + $requiresGuard = $property->hasType && !($property->hasDefault || $property->allowsNull); + $indent = $requiresGuard ? ' ' : ' '; + + if ($requiresGuard) { + $bodyParts[] = ' if (isset($object->' . $propertyName . ')) {'; + } + + $bodyParts[] = $indent . "\$values['" . $propertyName . "'] = \$object->" . $propertyName . ';'; + + if ($requiresGuard) { + $bodyParts[] = ' }'; + } } $bodyParts[] = '}, null, ' . var_export($className, true) . ');' . "\n"; diff --git a/tests/GeneratedHydratorTest/Functional/HydratorFunctionalTest.php b/tests/GeneratedHydratorTest/Functional/HydratorFunctionalTest.php index 0c5c4ca9..28deb8fb 100644 --- a/tests/GeneratedHydratorTest/Functional/HydratorFunctionalTest.php +++ b/tests/GeneratedHydratorTest/Functional/HydratorFunctionalTest.php @@ -100,6 +100,30 @@ public function testHydratorWillNotRaisedUnitiliazedTypedPropertyAccessError(): ], $hydrator->extract($instance)); } + /** + * Ensures that the hydrator will not attempt to read unitialized PHP >= 7.4 + * typed property, which would cause "Uncaught Error: Typed property Foo::$a + * must not be accessed before initialization" PHP engine errors. + * + * @requires PHP >= 7.4 + */ + public function testHydratorWillNotRaisedUnitiliazedTypedPropertyAccessErrorIfPropertyIsntHydrated(): void + { + $instance = new ClassWithTypedProperties(); + $hydrator = $this->generateHydrator($instance); + + $hydrator->hydrate(['untyped0' => 3], $instance); + + self::assertSame([ + 'property0' => 1, // 'property0' has a default value, it should keep it. + 'property1' => 2, // 'property1' has a default value, it should keep it. + 'property3' => null, // 'property3' is not required, it should remain null. + 'property4' => null, // 'property4' default value is null, it should remain null. + 'untyped0' => 3, // 'untyped0' is null by default + 'untyped1' => null, // 'untyped1' is null by default + ], $hydrator->extract($instance)); + } + /** @requires PHP >= 7.4 */ public function testHydratorWillSetAllTypedProperties(): void {