Skip to content

Commit

Permalink
issue Ocramius#656 - hack around reflection in order to hydrate reado…
Browse files Browse the repository at this point in the history
…nly properties
  • Loading branch information
pounard committed Sep 13, 2023
1 parent 9537dff commit 9618af7
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class HydratorMethodsVisitor extends NodeVisitorAbstract
*/
private array $hiddenPropertyMap = [];

public function __construct(ReflectionClass $reflectedClass)
public function __construct(private ReflectionClass $reflectedClass)
{
foreach ($this->findAllInstanceProperties($reflectedClass) as $property) {
$className = $property->getDeclaringClass()->getName();
Expand Down Expand Up @@ -101,20 +101,30 @@ static function (ReflectionProperty $property): bool {
* @return string[]
* @psalm-return list<string>
*/
private function generatePropertyHydrateCall(ObjectProperty $property, string $inputArrayName): array
private function generatePropertyHydrateCall(ObjectProperty $property, string $inputArrayName, string|null $className = null): array
{
$propertyName = $property->name;
$escapedName = var_export($propertyName, true);
$propertyName = $property->name;
$escapedName = var_export($propertyName, true);
$valueAccessor = $inputArrayName . '[' . $escapedName . ']';
$escapedClassName = var_export($className ?? $this->reflectedClass->getName(), true);

if ($property->isReadOnly) {
return [
'if (\\array_key_exists(' . $escapedName . ', ' . $inputArrayName . ')) {',
' ($ref ?? $ref = new \ReflectionClass(' . $escapedClassName . '))->getProperty(' . $escapedName . ')->setValue($object, ' . $valueAccessor . ');',
'}',
];
}

if ($property->allowsNull && ! $property->hasDefault) {
return ['$object->' . $propertyName . ' = ' . $inputArrayName . '[' . $escapedName . '] ?? null;'];
return ['$object->' . $propertyName . ' = ' . $valueAccessor . ' ?? null;'];
}

return [
'if (isset(' . $inputArrayName . '[' . $escapedName . '])',
'if (isset(' . $valueAccessor . ')',
' || $object->' . $propertyName . ' !== null && \\array_key_exists(' . $escapedName . ', ' . $inputArrayName . ')',
') {',
' $object->' . $propertyName . ' = ' . $inputArrayName . '[' . $escapedName . '];',
' $object->' . $propertyName . ' = ' . $valueAccessor . ';',
'}',
];
}
Expand All @@ -132,7 +142,7 @@ private function replaceConstructor(ClassMethod $method): void
// Hydrate closures
$bodyParts[] = '$this->hydrateCallbacks[] = \\Closure::bind(static function ($object, $values) {';
foreach ($properties as $property) {
$bodyParts = array_merge($bodyParts, $this->generatePropertyHydrateCall($property, '$values'));
$bodyParts = array_merge($bodyParts, $this->generatePropertyHydrateCall($property, '$values', $className));
}

$bodyParts[] = '}, null, ' . var_export($className, true) . ');' . "\n";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ final class ObjectProperty
public string $name;

/** @psalm-param non-empty-string $name */
private function __construct(string $name, public bool $hasType, public bool $allowsNull, public bool $hasDefault)
private function __construct(string $name, public bool $hasType, public bool $allowsNull, public bool $hasDefault, public bool $isReadOnly)
{
$this->name = $name;
}
Expand All @@ -32,14 +32,15 @@ public static function fromReflection(ReflectionProperty $property): self
$defaultValues = $property->getDeclaringClass()->getDefaultProperties();

if ($type === null) {
return new self($propertyName, false, true, array_key_exists($propertyName, $defaultValues));
return new self($propertyName, false, true, array_key_exists($propertyName, $defaultValues), $property->isReadOnly());
}

return new self(
$propertyName,
true,
$type->allowsNull(),
array_key_exists($propertyName, $defaultValues),
$property->isReadOnly(),
);
}
}
16 changes: 16 additions & 0 deletions tests/GeneratedHydratorTest/Functional/HydratorFunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use GeneratedHydratorTestAsset\ClassWithPrivatePropertiesAndParents;
use GeneratedHydratorTestAsset\ClassWithProtectedProperties;
use GeneratedHydratorTestAsset\ClassWithPublicProperties;
use GeneratedHydratorTestAsset\ClassWithReadonlyProperties;
use GeneratedHydratorTestAsset\ClassWithStaticProperties;
use GeneratedHydratorTestAsset\ClassWithTypedProperties;
use GeneratedHydratorTestAsset\EmptyClass;
Expand Down Expand Up @@ -100,6 +101,21 @@ public function testHydratorWillNotRaisedUnitiliazedTypedPropertyAccessError():
], $hydrator->extract($instance));
}

/**
* Ensures that readonly properties are hydrated as well without raising.
*
* @requires PHP >= 8.1
*/
public function testHydratorWillNotRaisedErrorWhenHydratingReadonlyProperties(): void
{
$instance = new ClassWithReadonlyProperties();
$hydrator = $this->generateHydrator($instance);

$hydrator->hydrate(['readonly0' => 7], $instance);

self::assertSame(['readonly0' => 7], $hydrator->extract($instance));
}

/** @requires PHP >= 7.4 */
public function testHydratorWillSetAllTypedProperties(): void
{
Expand Down
13 changes: 13 additions & 0 deletions tests/GeneratedHydratorTestAsset/ClassWithReadonlyProperties.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace GeneratedHydratorTestAsset;

/**
* Test PHP 8.1 readonly property hydration and dehydration.
*/
final class ClassWithReadonlyProperties
{
private readonly int $readonly0;
}

0 comments on commit 9618af7

Please sign in to comment.