diff --git a/src/Internal/ObjectExporter/AnyObjectExporter.php b/src/Internal/ObjectExporter/AnyObjectExporter.php index 090393e..7cb3b62 100644 --- a/src/Internal/ObjectExporter/AnyObjectExporter.php +++ b/src/Internal/ObjectExporter/AnyObjectExporter.php @@ -44,10 +44,10 @@ public function export(object $object, \ReflectionObject $reflectionObject, arra $returnNewObject = ($reflectionObject->getConstructor() === null); while ($current) { - $publicProperties = []; - $nonPublicProperties = []; - $unsetPublicProperties = []; - $unsetNonPublicProperties = []; + $publicNonReadonlyProperties = []; + $nonPublicOrPublicReadonlyProperties = []; + $unsetPublicNonReadonlyProperties = []; + $unsetNonPublicOrPublicReadonlyProperties = []; foreach ($current->getProperties() as $property) { if ($property->isStatic()) { @@ -70,26 +70,26 @@ public function export(object $object, \ReflectionObject $reflectionObject, arra if (array_key_exists($key, $objectAsArray)) { $value = $objectAsArray[$key]; - if ($property->isPublic()) { - $publicProperties[$name] = $value; + if ($property->isPublic() && !(method_exists($property, 'isReadOnly') && $property->isReadOnly())) { + $publicNonReadonlyProperties[$name] = $value; } else { - $nonPublicProperties[$name] = $value; + $nonPublicOrPublicReadonlyProperties[$name] = $value; } } else { - if ($property->isPublic()) { - $unsetPublicProperties[] = $name; + if ($property->isPublic() && !(method_exists($property, 'isReadOnly') && $property->isReadOnly())) { + $unsetPublicNonReadonlyProperties[] = $name; } else { - $unsetNonPublicProperties[] = $name; + $unsetNonPublicOrPublicReadonlyProperties[] = $name; } } $returnNewObject = false; } - if ($publicProperties || $unsetPublicProperties) { + if ($publicNonReadonlyProperties || $unsetPublicNonReadonlyProperties) { $lines[] = ''; - foreach ($publicProperties as $name => $value) { + foreach ($publicNonReadonlyProperties as $name => $value) { /** @psalm-suppress RedundantCast See: https://github.com/vimeo/psalm/issues/4891 */ $name = (string) $name; @@ -104,19 +104,19 @@ public function export(object $object, \ReflectionObject $reflectionObject, arra $lines = array_merge($lines, $exportedValue); } - foreach ($unsetPublicProperties as $name) { + foreach ($unsetPublicNonReadonlyProperties as $name) { $lines[] = 'unset($object->' . $this->escapePropName($name) . ');'; } } - if ($nonPublicProperties || $unsetNonPublicProperties) { + if ($nonPublicOrPublicReadonlyProperties || $unsetNonPublicOrPublicReadonlyProperties) { $closureLines = []; if ($this->exporter->addTypeHints) { $closureLines[] = '/** @var \\' . $current->getName() . ' $this */'; } - foreach ($nonPublicProperties as $name => $value) { + foreach ($nonPublicOrPublicReadonlyProperties as $name => $value) { $newPath = $path; $newPath[] = $name; @@ -128,7 +128,7 @@ public function export(object $object, \ReflectionObject $reflectionObject, arra $closureLines = array_merge($closureLines, $exportedValue); } - foreach ($unsetNonPublicProperties as $name) { + foreach ($unsetNonPublicOrPublicReadonlyProperties as $name) { $closureLines[] = 'unset($this->' . $this->escapePropName($name) . ');'; } diff --git a/src/Internal/ObjectExporter/EnumExporter.php b/src/Internal/ObjectExporter/EnumExporter.php index 6c47358..dfec28f 100644 --- a/src/Internal/ObjectExporter/EnumExporter.php +++ b/src/Internal/ObjectExporter/EnumExporter.php @@ -18,16 +18,11 @@ class EnumExporter extends ObjectExporter * {@inheritDoc} * * See: https://github.com/vimeo/psalm/pull/8117 - * @psalm-suppress MixedInferredReturnType - * @psalm-suppress MixedReturnStatement + * @psalm-suppress RedundantCondition */ public function supports(\ReflectionObject $reflectionObject) : bool { - if (! method_exists($reflectionObject, 'isEnum')) { - return false; - } - - return $reflectionObject->isEnum(); + return method_exists($reflectionObject, 'isEnum') && $reflectionObject->isEnum(); } /** diff --git a/tests/Classes/PublicReadonlyPropertiesWithoutConstructor.php b/tests/Classes/PublicReadonlyPropertiesWithoutConstructor.php new file mode 100644 index 0000000..46ae1d5 --- /dev/null +++ b/tests/Classes/PublicReadonlyPropertiesWithoutConstructor.php @@ -0,0 +1,12 @@ +assertExportEquals($expected, $object); } + /** + * @requires PHP 8.1 + */ + public function testExportClassWithReadonlyPublicPropertiesAndConstructor(): void + { + $object = new ReadonlyPropertiesWithConstructor('public readonly', 'private readonly', 'public'); + + $expected = <<<'PHP' +(static function() { + $class = new \ReflectionClass(\Brick\VarExporter\Tests\Classes\ReadonlyPropertiesWithConstructor::class); + $object = $class->newInstanceWithoutConstructor(); + + $object->baz = 'public'; + + (function() { + $this->foo = 'public readonly'; + $this->bar = 'private readonly'; + })->bindTo($object, \Brick\VarExporter\Tests\Classes\ReadonlyPropertiesWithConstructor::class)(); + + return $object; +})() +PHP; + + $this->assertExportEquals($expected, $object); + } + + /** + * @requires PHP 8.1 + */ + public function testExportClassWithStateAndReadonlyPublicProperties(): void + { + $object = new PublicReadonlyPropertiesWithoutConstructor(); + + (function () { + $this->foo = 'foo'; + })->bindTo($object, PublicReadonlyPropertiesWithoutConstructor::class)(); + + $expected = <<<'PHP' +(static function() { + $object = new \Brick\VarExporter\Tests\Classes\PublicReadonlyPropertiesWithoutConstructor; + + unset($object->baz); + + (function() { + $this->foo = 'foo'; + unset($this->bar); + })->bindTo($object, \Brick\VarExporter\Tests\Classes\PublicReadonlyPropertiesWithoutConstructor::class)(); + + return $object; +})() +PHP; + + $this->assertExportEquals($expected, $object); + } + public function testExportClassWithSerializeMagicMethods(): void { $object = new SerializeMagicMethods;