From bf56e3aedca6576996985ff950a2ffa0a1e501b2 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 3 May 2022 19:14:53 +0200 Subject: [PATCH] Add support for readonly properties --- .../MethodGenerator/CallInitializer.php | 111 ++++++++++++-- .../MethodGenerator/MagicGet.php | 16 +- .../PrivatePropertiesMap.php | 19 ++- .../ProtectedPropertiesMap.php | 4 + .../LazyLoadingGhostGenerator.php | 2 +- .../PropertyGenerator/PublicPropertiesMap.php | 6 +- .../ProxyGenerator/Util/Properties.php | 15 ++ .../Util/UnsetPropertiesGenerator.php | 41 +++-- .../UnsupportedProxiedClassExceptionTest.php | 44 +++--- ...nterceptorScopeLocalizerFunctionalTest.php | 4 +- .../LazyLoadingGhostFunctionalTest.php | 39 +++++ .../MultipleProxyGenerationTest.php | 5 +- .../AbstractProxyGeneratorTest.php | 2 + .../AccessInterceptorScopeLocalizerTest.php | 5 +- .../MethodGenerator/CallInitializerTest.php | 142 ++++++++++++++++++ .../StaticProxyConstructorTest.php | 24 +++ .../NullObjectGeneratorTest.php | 4 +- .../RemoteObjectGeneratorTest.php | 2 + .../ProxyGenerator/Util/PropertiesTest.php | 26 +--- .../Util/UnsetPropertiesGeneratorTest.php | 14 ++ .../ClassWithReadOnlyProperties.php | 17 +++ 21 files changed, 460 insertions(+), 82 deletions(-) create mode 100644 tests/ProxyManagerTestAsset/ClassWithReadOnlyProperties.php diff --git a/src/ProxyManager/ProxyGenerator/LazyLoadingGhost/MethodGenerator/CallInitializer.php b/src/ProxyManager/ProxyGenerator/LazyLoadingGhost/MethodGenerator/CallInitializer.php index 52f02d54..42a00dad 100644 --- a/src/ProxyManager/ProxyGenerator/LazyLoadingGhost/MethodGenerator/CallInitializer.php +++ b/src/ProxyManager/ProxyGenerator/LazyLoadingGhost/MethodGenerator/CallInitializer.php @@ -6,13 +6,22 @@ use Laminas\Code\Generator\ParameterGenerator; use Laminas\Code\Generator\PropertyGenerator; +use LogicException; use ProxyManager\Generator\MethodGenerator; use ProxyManager\Generator\Util\IdentifierSuffixer; use ProxyManager\Generator\ValueGenerator; use ProxyManager\ProxyGenerator\Util\Properties; +use ReflectionIntersectionType; +use ReflectionNamedType; use ReflectionProperty; +use ReflectionType; +use ReflectionUnionType; +use function assert; use function array_map; +use function array_unique; +use function explode; +use function get_class; use function implode; use function sprintf; use function str_replace; @@ -63,12 +72,13 @@ public function __construct( %s $result = $this->%s->__invoke($this, $methodName, $parameters, $this->%s, $properties); -$this->%s = false; +%s$this->%s = false; return $result; PHP; - $referenceableProperties = $properties->withoutNonReferenceableProperties(); + $referenceableProperties = $properties->withoutNonReferenceableProperties(); + $nonReferenceableProperties = $properties->onlyNonReferenceableProperties(); $this->setBody(sprintf( $bodyTemplate, @@ -76,29 +86,41 @@ public function __construct( $initializer, $initialization, $this->propertiesInitializationCode($referenceableProperties), - $this->propertiesReferenceArrayCode($referenceableProperties), + $this->propertiesReferenceArrayCode($referenceableProperties, $nonReferenceableProperties), $initializer, $initializer, + $this->propertiesNonReferenceableCode($nonReferenceableProperties), $initialization )); } private function propertiesInitializationCode(Properties $properties): string { + $scopedPropertyGroups = []; + $nonScopedProperties = []; + + foreach ($properties->getInstanceProperties() as $property) { + if ($property->isPrivate() || (\PHP_VERSION_ID >= 80100 && $property->isReadOnly())) { + $scopedPropertyGroups[$property->getDeclaringClass()->getName()][$property->getName()] = $property; + } else { + $nonScopedProperties[] = $property; + } + } + $assignments = []; - foreach ($properties->getAccessibleProperties() as $property) { + foreach ($nonScopedProperties as $property) { $assignments[] = '$this->' . $property->getName() . ' = ' . $this->getExportedPropertyDefaultValue($property) . ';'; } - foreach ($properties->getGroupedPrivateProperties() as $className => $privateProperties) { + foreach ($scopedPropertyGroups as $className => $scopedProperties) { $cacheKey = 'cache' . str_replace('\\', '_', $className); $assignments[] = 'static $' . $cacheKey . ";\n\n" . '$' . $cacheKey . ' ?? $' . $cacheKey . " = \\Closure::bind(static function (\$instance) {\n" - . $this->getPropertyDefaultsAssignments($privateProperties) . "\n" + . $this->getPropertyDefaultsAssignments($scopedProperties) . "\n" . '}, null, ' . var_export($className, true) . ");\n\n" . '$' . $cacheKey . "(\$this);\n\n"; } @@ -123,9 +145,10 @@ function (ReflectionProperty $property): string { ); } - private function propertiesReferenceArrayCode(Properties $properties): string + private function propertiesReferenceArrayCode(Properties $properties, Properties $nonReferenceableProperties): string { - $assignments = []; + $assignments = []; + $nonReferenceablePropertiesDefinition = ''; foreach ($properties->getAccessibleProperties() as $propertyInternalName => $property) { $assignments[] = ' ' @@ -133,7 +156,18 @@ private function propertiesReferenceArrayCode(Properties $properties): string . ','; } - $code = "\$properties = [\n" . implode("\n", $assignments) . "\n];\n\n"; + foreach ($nonReferenceableProperties->getInstanceProperties() as $propertyInternalName => $property) { + $propertyAlias = $property->getName() . ($property->isPrivate() ? '_on_' . str_replace('\\', '_', $property->getDeclaringClass()->getName()) : ''); + $propertyType = $property->getType(); + assert($propertyType !== null); + + $nonReferenceablePropertiesDefinition .= sprintf(" public %s $%s;\n", self::getReferenceableType($propertyType), $propertyAlias); + + $assignments[] = sprintf(' %s => & $nonReferenceableProperties->%s,', var_export($propertyInternalName, true), $propertyAlias); + } + + $code = $nonReferenceableProperties->empty() ? '' : sprintf("\$nonReferenceableProperties = new class() {\n%s};\n", $nonReferenceablePropertiesDefinition); + $code .= "\$properties = [\n" . implode("\n", $assignments) . "\n];\n\n"; // must use assignments, as direct reference during array definition causes a fatal error (not sure why) foreach ($properties->getGroupedPrivateProperties() as $className => $classPrivateProperties) { @@ -173,4 +207,63 @@ private function getExportedPropertyDefaultValue(ReflectionProperty $property): return (new ValueGenerator($defaults[$name] ?? null))->generate(); } + + private function propertiesNonReferenceableCode(Properties $properties): string + { + if ($properties->empty()) { + return ''; + } + + $code = []; + $scopedPropertyGroups = []; + + foreach ($properties->getInstanceProperties() as $propertyInternalName => $property) { + if (! $property->isPrivate() && (\PHP_VERSION_ID < 80100 || ! $property->isReadOnly())) { + $propertyAlias = $property->getName() . ($property->isPrivate() ? '_on_' . str_replace('\\', '_', $property->getDeclaringClass()->getName()) : ''); + $code[] = sprintf('isset($nonReferenceableProperties->%s) && $this->%s = $nonReferenceableProperties->%1$s;', $propertyAlias, $property->getName()); + } else { + $scopedPropertyGroups[$property->getDeclaringClass()->getName()][$propertyInternalName] = $property; + } + } + + foreach ($scopedPropertyGroups as $className => $scopedProperties) { + $cacheKey = 'cacheAssign' . str_replace('\\', '_', $className); + + $code[] = 'static $' . $cacheKey . ";\n"; + $code[] = '$' . $cacheKey . ' ?? $' . $cacheKey . ' = \Closure::bind(function ($instance, $nonReferenceableProperties) {'; + + foreach ($scopedProperties as $property) { + $propertyAlias = $property->getName() . ($property->isPrivate() ? '_on_' . str_replace('\\', '_', $property->getDeclaringClass()->getName()) : ''); + $code[] = sprintf(' isset($nonReferenceableProperties->%s) && $this->%s = $nonReferenceableProperties->%1$s;', $propertyAlias, $property->getName()); + } + + $code[] = '}, $this, ' . var_export($className, true) . ");\n"; + $code[] = '$' . $cacheKey . '($this, $nonReferenceableProperties);'; + } + + return implode("\n", $code) . "\n"; + } + + private static function getReferenceableType(ReflectionType $type): string + { + if ($type instanceof ReflectionNamedType) { + return '?' . ($type->isBuiltin() ? '' : '\\') . $type->getName(); + } + + if ($type instanceof ReflectionIntersectionType) { + return self::getReferenceableType($type->getTypes()[0]); + } + + if (! $type instanceof ReflectionUnionType) { + throw new LogicException('Unexpected ' . get_class($type)); + } + + $union = 'null'; + + foreach ($type->getTypes() as $subType) { + $union .= '|' . ($subType->isBuiltin() ? '' : '\\') . $subType->getName(); + } + + return $union; + } } diff --git a/src/ProxyManager/ProxyGenerator/LazyLoadingGhost/MethodGenerator/MagicGet.php b/src/ProxyManager/ProxyGenerator/LazyLoadingGhost/MethodGenerator/MagicGet.php index e73688d8..9d9014af 100644 --- a/src/ProxyManager/ProxyGenerator/LazyLoadingGhost/MethodGenerator/MagicGet.php +++ b/src/ProxyManager/ProxyGenerator/LazyLoadingGhost/MethodGenerator/MagicGet.php @@ -16,6 +16,7 @@ use ProxyManager\ProxyGenerator\Util\PublicScopeSimulator; use ReflectionClass; +use function implode; use function sprintf; /** @@ -63,7 +64,7 @@ class MagicGet extends MagicMethodGenerator $accessor = isset($accessorCache[$cacheKey]) ? $accessorCache[$cacheKey] : $accessorCache[$cacheKey] = \Closure::bind(static function & ($instance) use ($name) { - return $instance->$name; + %s }, null, $class); return $accessor($this); @@ -75,7 +76,7 @@ class MagicGet extends MagicMethodGenerator $accessor = isset($accessorCache[$cacheKey]) ? $accessorCache[$cacheKey] : $accessorCache[$cacheKey] = \Closure::bind(static function & ($instance) use ($name) { - return $instance->$name; + %s }, null, $tmpClass); return $accessor($this); @@ -110,6 +111,15 @@ public function __construct( ); } + $readOnlyPropertyNames = $privateProperties->getReadOnlyPropertyNames(); + + if ($readOnlyPropertyNames) { + $privateReturnCode = sprintf('\in_array($name, [\'%s\'], true) ? $value = $instance->$name : $value = & $instance->$name;', implode("', '", $readOnlyPropertyNames)); + $privateReturnCode .= "\n\n return \$value;"; + } else { + $privateReturnCode = 'return $instance->$name;'; + } + $this->setBody(sprintf( $this->callParentTemplate, $initializerProperty->getName(), @@ -121,8 +131,10 @@ public function __construct( $protectedProperties->getName(), $privateProperties->getName(), $privateProperties->getName(), + $privateReturnCode, $initializationTracker->getName(), $privateProperties->getName(), + $privateReturnCode, $parentAccess )); } diff --git a/src/ProxyManager/ProxyGenerator/LazyLoadingGhost/PropertyGenerator/PrivatePropertiesMap.php b/src/ProxyManager/ProxyGenerator/LazyLoadingGhost/PropertyGenerator/PrivatePropertiesMap.php index f1505f62..53f4fb6c 100644 --- a/src/ProxyManager/ProxyGenerator/LazyLoadingGhost/PropertyGenerator/PrivatePropertiesMap.php +++ b/src/ProxyManager/ProxyGenerator/LazyLoadingGhost/PropertyGenerator/PrivatePropertiesMap.php @@ -16,6 +16,9 @@ class PrivatePropertiesMap extends PropertyGenerator { public const KEY_DEFAULT_VALUE = 'defaultValue'; + /** @var list */ + private $readOnlyPropertyNames = []; + /** * Constructor * @@ -35,6 +38,14 @@ public function __construct(Properties $properties) $this->setDefaultValue($this->getMap($properties)); } + /** + * @return list + */ + public function getReadOnlyPropertyNames(): array + { + return $this->readOnlyPropertyNames; + } + /** * @return array> */ @@ -42,7 +53,13 @@ private function getMap(Properties $properties): array { $map = []; - foreach ($properties->getPrivateProperties() as $property) { + foreach ($properties->getInstanceProperties() as $property) { + if (\PHP_VERSION_ID >= 80100 && $property->isReadOnly()) { + $this->readOnlyPropertyNames[] = $property->getName(); + } elseif (! $property->isPrivate()) { + continue; + } + $map[$property->getName()][$property->getDeclaringClass()->getName()] = true; } diff --git a/src/ProxyManager/ProxyGenerator/LazyLoadingGhost/PropertyGenerator/ProtectedPropertiesMap.php b/src/ProxyManager/ProxyGenerator/LazyLoadingGhost/PropertyGenerator/ProtectedPropertiesMap.php index 01600b05..b001db04 100644 --- a/src/ProxyManager/ProxyGenerator/LazyLoadingGhost/PropertyGenerator/ProtectedPropertiesMap.php +++ b/src/ProxyManager/ProxyGenerator/LazyLoadingGhost/PropertyGenerator/ProtectedPropertiesMap.php @@ -41,6 +41,10 @@ private function getMap(Properties $properties): array $map = []; foreach ($properties->getProtectedProperties() as $property) { + if (\PHP_VERSION_ID >= 80100 && $property->isReadOnly()) { + continue; + } + $map[$property->getName()] = $property->getDeclaringClass()->getName(); } diff --git a/src/ProxyManager/ProxyGenerator/LazyLoadingGhostGenerator.php b/src/ProxyManager/ProxyGenerator/LazyLoadingGhostGenerator.php index a8873ed1..42fabe69 100644 --- a/src/ProxyManager/ProxyGenerator/LazyLoadingGhostGenerator.php +++ b/src/ProxyManager/ProxyGenerator/LazyLoadingGhostGenerator.php @@ -63,7 +63,7 @@ public function generate(ReflectionClass $originalClass, ClassGenerator $classGe $filteredProperties = Properties::fromReflectionClass($originalClass) ->filter($proxyOptions['skippedProperties'] ?? []); - $publicProperties = new PublicPropertiesMap($filteredProperties); + $publicProperties = new PublicPropertiesMap($filteredProperties, true); $privateProperties = new PrivatePropertiesMap($filteredProperties); $protectedProperties = new ProtectedPropertiesMap($filteredProperties); $skipDestructor = ($proxyOptions['skipDestructor'] ?? false) && $originalClass->hasMethod('__destruct'); diff --git a/src/ProxyManager/ProxyGenerator/PropertyGenerator/PublicPropertiesMap.php b/src/ProxyManager/ProxyGenerator/PropertyGenerator/PublicPropertiesMap.php index 05c653b7..c8d1cdfa 100644 --- a/src/ProxyManager/ProxyGenerator/PropertyGenerator/PublicPropertiesMap.php +++ b/src/ProxyManager/ProxyGenerator/PropertyGenerator/PublicPropertiesMap.php @@ -20,11 +20,15 @@ class PublicPropertiesMap extends PropertyGenerator /** * @throws InvalidArgumentException */ - public function __construct(Properties $properties) + public function __construct(Properties $properties, bool $skipReadOnlyProperties = false) { parent::__construct(IdentifierSuffixer::getIdentifier('publicProperties')); foreach ($properties->getPublicProperties() as $publicProperty) { + if ($skipReadOnlyProperties && \PHP_VERSION_ID >= 80100 && $publicProperty->isReadOnly()) { + continue; + } + $this->publicProperties[$publicProperty->getName()] = true; } diff --git a/src/ProxyManager/ProxyGenerator/Util/Properties.php b/src/ProxyManager/ProxyGenerator/Util/Properties.php index 3b9243f8..8fe1c914 100644 --- a/src/ProxyManager/ProxyGenerator/Util/Properties.php +++ b/src/ProxyManager/ProxyGenerator/Util/Properties.php @@ -79,6 +79,17 @@ public function onlyNonReferenceableProperties(): self return false; } + if (\PHP_VERSION_ID >= 80100 && $property->isReadOnly()) { + return true; + } + + $type = $property->getType(); + assert($type instanceof ReflectionType); + + if ($type->allowsNull()) { + return false; + } + return ! array_key_exists( $property->getName(), // https://bugs.php.net/bug.php?id=77673 @@ -103,6 +114,10 @@ public function withoutNonReferenceableProperties(): self return true; } + if (\PHP_VERSION_ID >= 80100 && $property->isReadOnly()) { + return false; + } + $type = $property->getType(); assert($type instanceof ReflectionType); diff --git a/src/ProxyManager/ProxyGenerator/Util/UnsetPropertiesGenerator.php b/src/ProxyManager/ProxyGenerator/Util/UnsetPropertiesGenerator.php index 39462ac0..8670f238 100644 --- a/src/ProxyManager/ProxyGenerator/Util/UnsetPropertiesGenerator.php +++ b/src/ProxyManager/ProxyGenerator/Util/UnsetPropertiesGenerator.php @@ -27,38 +27,47 @@ final class UnsetPropertiesGenerator public static function generateSnippet(Properties $properties, string $instanceName): string { - return self::generateUnsetAccessiblePropertiesCode($properties, $instanceName) - . self::generateUnsetPrivatePropertiesCode($properties, $instanceName); + $scopedPropertyGroups = []; + $nonScopedProperties = []; + + foreach ($properties->getInstanceProperties() as $propertyInternalName => $property) { + if ($property->isPrivate() || (\PHP_VERSION_ID >= 80100 && $property->isReadOnly())) { + $scopedPropertyGroups[$property->getDeclaringClass()->getName()][$property->getName()] = $property; + } else { + $nonScopedProperties[$propertyInternalName] = $property; + } + } + + return self::generateUnsetNonScopedPropertiesCode($nonScopedProperties, $instanceName) + . self::generateUnsetScopedPropertiesCode($scopedPropertyGroups, $instanceName); } - private static function generateUnsetAccessiblePropertiesCode(Properties $properties, string $instanceName): string + /** @param array $nonScopedProperties */ + private static function generateUnsetNonScopedPropertiesCode(array $nonScopedProperties, string $instanceName): string { - $accessibleProperties = $properties->getAccessibleProperties(); - - if (! $accessibleProperties) { + if (! $nonScopedProperties) { return ''; } - return self::generateUnsetStatement($accessibleProperties, $instanceName) . "\n\n"; + return self::generateUnsetStatement($nonScopedProperties, $instanceName) . "\n\n"; } - private static function generateUnsetPrivatePropertiesCode(Properties $properties, string $instanceName): string + /** @param array> $scopedPropertyGroups */ + private static function generateUnsetScopedPropertiesCode(array $scopedPropertyGroups, string $instanceName): string { - $groups = $properties->getGroupedPrivateProperties(); - - if (! $groups) { + if (! $scopedPropertyGroups) { return ''; } $unsetClosureCalls = []; - foreach ($groups as $privateProperties) { - $firstProperty = reset($privateProperties); + foreach ($scopedPropertyGroups as $scopedProperties) { + $firstProperty = reset($scopedProperties); assert($firstProperty instanceof ReflectionProperty); - $unsetClosureCalls[] = self::generateUnsetClassPrivatePropertiesBlock( + $unsetClosureCalls[] = self::generateUnsetClassScopedPropertiesBlock( $firstProperty->getDeclaringClass(), - $privateProperties, + $scopedProperties, $instanceName ); } @@ -67,7 +76,7 @@ private static function generateUnsetPrivatePropertiesCode(Properties $propertie } /** @param array $properties */ - private static function generateUnsetClassPrivatePropertiesBlock( + private static function generateUnsetClassScopedPropertiesBlock( ReflectionClass $declaringClass, array $properties, string $instanceName diff --git a/tests/ProxyManagerTest/Exception/UnsupportedProxiedClassExceptionTest.php b/tests/ProxyManagerTest/Exception/UnsupportedProxiedClassExceptionTest.php index 5ed2ced6..18c243b9 100644 --- a/tests/ProxyManagerTest/Exception/UnsupportedProxiedClassExceptionTest.php +++ b/tests/ProxyManagerTest/Exception/UnsupportedProxiedClassExceptionTest.php @@ -41,28 +41,28 @@ public function testNonReferenceableLocalizedReflectionProperties(): void self::assertSame( 'Cannot create references for following properties of class ' . ClassWithMixedTypedProperties::class - . ': publicBoolPropertyWithoutDefaultValue, publicNullableBoolPropertyWithoutDefaultValue, ' - . 'publicIntPropertyWithoutDefaultValue, publicNullableIntPropertyWithoutDefaultValue, ' - . 'publicFloatPropertyWithoutDefaultValue, publicNullableFloatPropertyWithoutDefaultValue, ' - . 'publicStringPropertyWithoutDefaultValue, publicNullableStringPropertyWithoutDefaultValue, ' - . 'publicArrayPropertyWithoutDefaultValue, publicNullableArrayPropertyWithoutDefaultValue, ' - . 'publicIterablePropertyWithoutDefaultValue, publicNullableIterablePropertyWithoutDefaultValue, ' - . 'publicObjectProperty, publicNullableObjectProperty, publicClassProperty, publicNullableClassProperty, ' - . 'protectedBoolPropertyWithoutDefaultValue, protectedNullableBoolPropertyWithoutDefaultValue, ' - . 'protectedIntPropertyWithoutDefaultValue, protectedNullableIntPropertyWithoutDefaultValue, ' - . 'protectedFloatPropertyWithoutDefaultValue, protectedNullableFloatPropertyWithoutDefaultValue, ' - . 'protectedStringPropertyWithoutDefaultValue, protectedNullableStringPropertyWithoutDefaultValue, ' - . 'protectedArrayPropertyWithoutDefaultValue, protectedNullableArrayPropertyWithoutDefaultValue, ' - . 'protectedIterablePropertyWithoutDefaultValue, protectedNullableIterablePropertyWithoutDefaultValue, ' - . 'protectedObjectProperty, protectedNullableObjectProperty, protectedClassProperty, ' - . 'protectedNullableClassProperty, privateBoolPropertyWithoutDefaultValue, ' - . 'privateNullableBoolPropertyWithoutDefaultValue, privateIntPropertyWithoutDefaultValue, ' - . 'privateNullableIntPropertyWithoutDefaultValue, privateFloatPropertyWithoutDefaultValue, ' - . 'privateNullableFloatPropertyWithoutDefaultValue, privateStringPropertyWithoutDefaultValue, ' - . 'privateNullableStringPropertyWithoutDefaultValue, privateArrayPropertyWithoutDefaultValue, ' - . 'privateNullableArrayPropertyWithoutDefaultValue, privateIterablePropertyWithoutDefaultValue, ' - . 'privateNullableIterablePropertyWithoutDefaultValue, privateObjectProperty, ' - . 'privateNullableObjectProperty, privateClassProperty, privateNullableClassProperty', + . ': publicBoolPropertyWithoutDefaultValue, ' + . 'publicIntPropertyWithoutDefaultValue, ' + . 'publicFloatPropertyWithoutDefaultValue, ' + . 'publicStringPropertyWithoutDefaultValue, ' + . 'publicArrayPropertyWithoutDefaultValue, ' + . 'publicIterablePropertyWithoutDefaultValue, ' + . 'publicObjectProperty, publicClassProperty, ' + . 'protectedBoolPropertyWithoutDefaultValue, ' + . 'protectedIntPropertyWithoutDefaultValue, ' + . 'protectedFloatPropertyWithoutDefaultValue, ' + . 'protectedStringPropertyWithoutDefaultValue, ' + . 'protectedArrayPropertyWithoutDefaultValue, ' + . 'protectedIterablePropertyWithoutDefaultValue, ' + . 'protectedObjectProperty, protectedClassProperty, ' + . 'privateBoolPropertyWithoutDefaultValue, ' + . 'privateIntPropertyWithoutDefaultValue, ' + . 'privateFloatPropertyWithoutDefaultValue, ' + . 'privateStringPropertyWithoutDefaultValue, ' + . 'privateArrayPropertyWithoutDefaultValue, ' + . 'privateIterablePropertyWithoutDefaultValue, ' + . 'privateObjectProperty, ' + . 'privateClassProperty', UnsupportedProxiedClassException::nonReferenceableLocalizedReflectionProperties( $reflectionClass, Properties::fromReflectionClass($reflectionClass) diff --git a/tests/ProxyManagerTest/Functional/AccessInterceptorScopeLocalizerFunctionalTest.php b/tests/ProxyManagerTest/Functional/AccessInterceptorScopeLocalizerFunctionalTest.php index dae2bfc6..f02eaf7c 100644 --- a/tests/ProxyManagerTest/Functional/AccessInterceptorScopeLocalizerFunctionalTest.php +++ b/tests/ProxyManagerTest/Functional/AccessInterceptorScopeLocalizerFunctionalTest.php @@ -21,7 +21,7 @@ use ProxyManagerTestAsset\ClassWithParentHint; use ProxyManagerTestAsset\ClassWithPublicArrayPropertyAccessibleViaMethod; use ProxyManagerTestAsset\ClassWithPublicProperties; -use ProxyManagerTestAsset\ClassWithPublicStringNullableTypedProperty; +use ProxyManagerTestAsset\ClassWithPublicStringTypedProperty; use ProxyManagerTestAsset\ClassWithSelfHint; use ProxyManagerTestAsset\EmptyClass; use ProxyManagerTestAsset\ReferenceIncrementDecrementClass; @@ -568,7 +568,7 @@ public function testWillInterceptAndReturnEarlyOnVoidMethod(): void */ public function testWillRefuseToGenerateReferencesToTypedPropertiesWithoutDefaultValues(): void { - $instance = new ClassWithPublicStringNullableTypedProperty(); + $instance = new ClassWithPublicStringTypedProperty(); $factory = new AccessInterceptorScopeLocalizerFactory(); $this->expectException(UnsupportedProxiedClassException::class); diff --git a/tests/ProxyManagerTest/Functional/LazyLoadingGhostFunctionalTest.php b/tests/ProxyManagerTest/Functional/LazyLoadingGhostFunctionalTest.php index db3892af..357c6ca0 100644 --- a/tests/ProxyManagerTest/Functional/LazyLoadingGhostFunctionalTest.php +++ b/tests/ProxyManagerTest/Functional/LazyLoadingGhostFunctionalTest.php @@ -30,6 +30,7 @@ use ProxyManagerTestAsset\ClassWithPublicProperties; use ProxyManagerTestAsset\ClassWithPublicStringNullableTypedProperty; use ProxyManagerTestAsset\ClassWithPublicStringTypedProperty; +use ProxyManagerTestAsset\ClassWithReadOnlyProperties; use ProxyManagerTestAsset\ClassWithSelfHint; use ProxyManagerTestAsset\EmptyClass; use ProxyManagerTestAsset\OtherObjectAccessClass; @@ -772,6 +773,44 @@ static function ( self::assertSame('public0', $proxy->publicStringProperty); } + /** + * @requires PHP 8.1 + */ + public function testInitializationOfReadOnlyProperties(): void + { + $proxy = (new LazyLoadingGhostFactory())->createProxy( + ClassWithReadOnlyProperties::class, + static function ( + GhostObjectInterface $proxy, + string $method, + array $params, + ?Closure & $initializer, + array $properties + ): bool { + $initializer = null; + $properties["\0" . ClassWithReadOnlyProperties::class . "\0property2"] = 'private0'; + $properties["\0*\0property1"] = 'protected0'; + $properties['property0'] = 'public0'; + + return true; + } + ); + + $reflectionClass = new ReflectionClass(ClassWithReadOnlyProperties::class); + + $properties = Properties::fromReflectionClass($reflectionClass)->getInstanceProperties(); + + $privateProperty = $properties["\0" . ClassWithReadOnlyProperties::class . "\0property2"]; + $protectedProperty = $properties["\0*\0property1"]; + + $privateProperty->setAccessible(true); + $protectedProperty->setAccessible(true); + + self::assertSame('private0', $privateProperty->getValue($proxy)); + self::assertSame('protected0', $properties["\0*\0property1"]->getValue($proxy)); + self::assertSame('public0', $proxy->property0); + } + /** * @group 115 * @group 175 diff --git a/tests/ProxyManagerTest/Functional/MultipleProxyGenerationTest.php b/tests/ProxyManagerTest/Functional/MultipleProxyGenerationTest.php index bccc7978..e162dd77 100644 --- a/tests/ProxyManagerTest/Functional/MultipleProxyGenerationTest.php +++ b/tests/ProxyManagerTest/Functional/MultipleProxyGenerationTest.php @@ -26,6 +26,7 @@ use ProxyManagerTestAsset\ClassWithPrivateProperties; use ProxyManagerTestAsset\ClassWithProtectedProperties; use ProxyManagerTestAsset\ClassWithPublicProperties; +use ProxyManagerTestAsset\ClassWithReadOnlyProperties; use ProxyManagerTestAsset\ClassWithSelfHint; use ProxyManagerTestAsset\EmptyClass; use ProxyManagerTestAsset\HydratedObject; @@ -36,6 +37,7 @@ use ProxyManagerTestAsset\VoidMethodTypeHintedClass; use function get_class; +use function in_array; use const PHP_VERSION_ID; @@ -77,7 +79,7 @@ public function testCanGenerateMultipleDifferentProxiesForSameClass($object): vo $accessInterceptorFactory->createProxy($object), ]; - if ($className !== ClassWithMixedTypedProperties::class) { + if (! in_array($className, [ClassWithMixedTypedProperties::class, ClassWithReadOnlyProperties::class], true)) { $generated[] = $accessInterceptorScopeLocalizerFactory->createProxy($object); } @@ -141,6 +143,7 @@ public function getTestedClasses(): array if (PHP_VERSION_ID >= 80100) { $objects['php81defaults'] = [new ClassWithPhp81Defaults()]; + $objects['readonly'] = [new ClassWithReadOnlyProperties()]; } return $objects; diff --git a/tests/ProxyManagerTest/ProxyGenerator/AbstractProxyGeneratorTest.php b/tests/ProxyManagerTest/ProxyGenerator/AbstractProxyGeneratorTest.php index 22a70199..627b3e4a 100644 --- a/tests/ProxyManagerTest/ProxyGenerator/AbstractProxyGeneratorTest.php +++ b/tests/ProxyManagerTest/ProxyGenerator/AbstractProxyGeneratorTest.php @@ -19,6 +19,7 @@ use ProxyManagerTestAsset\ClassWithMixedTypedProperties; use ProxyManagerTestAsset\ClassWithPhp80TypedMethods; use ProxyManagerTestAsset\ClassWithPhp81Defaults; +use ProxyManagerTestAsset\ClassWithReadOnlyProperties; use ProxyManagerTestAsset\IterableMethodTypeHintedInterface; use ProxyManagerTestAsset\ObjectMethodTypeHintedInterface; use ProxyManagerTestAsset\ReturnTypeHintedClass; @@ -115,6 +116,7 @@ public function getTestedImplementations(): array if (PHP_VERSION_ID >= 80100) { $implementations['php81defaults'] = [ClassWithPhp81Defaults::class]; + $implementations['readonly'] = [ClassWithReadOnlyProperties::class]; } return $implementations; diff --git a/tests/ProxyManagerTest/ProxyGenerator/AccessInterceptorScopeLocalizerTest.php b/tests/ProxyManagerTest/ProxyGenerator/AccessInterceptorScopeLocalizerTest.php index d830bb89..c9176b34 100644 --- a/tests/ProxyManagerTest/ProxyGenerator/AccessInterceptorScopeLocalizerTest.php +++ b/tests/ProxyManagerTest/ProxyGenerator/AccessInterceptorScopeLocalizerTest.php @@ -12,8 +12,11 @@ use ProxyManager\ProxyGenerator\ProxyGeneratorInterface; use ProxyManagerTestAsset\BaseInterface; use ProxyManagerTestAsset\ClassWithMixedTypedProperties; +use ProxyManagerTestAsset\ClassWithReadOnlyProperties; use ReflectionClass; +use function in_array; + /** * Tests for {@see \ProxyManager\ProxyGenerator\AccessInterceptorScopeLocalizerGenerator} * @@ -40,7 +43,7 @@ public function testGeneratesValidCode(string $className): void $this->expectException(InvalidProxiedClassException::class); } - if ($reflectionClass->getName() === ClassWithMixedTypedProperties::class) { + if (in_array($reflectionClass->getName(), [ClassWithMixedTypedProperties::class, ClassWithReadOnlyProperties::class], true)) { $this->expectException(UnsupportedProxiedClassException::class); } diff --git a/tests/ProxyManagerTest/ProxyGenerator/LazyLoadingGhost/MethodGenerator/CallInitializerTest.php b/tests/ProxyManagerTest/ProxyGenerator/LazyLoadingGhost/MethodGenerator/CallInitializerTest.php index 1e55e4cd..a834e265 100644 --- a/tests/ProxyManagerTest/ProxyGenerator/LazyLoadingGhost/MethodGenerator/CallInitializerTest.php +++ b/tests/ProxyManagerTest/ProxyGenerator/LazyLoadingGhost/MethodGenerator/CallInitializerTest.php @@ -10,6 +10,7 @@ use ProxyManager\ProxyGenerator\Util\Properties; use ProxyManagerTestAsset\ClassWithMixedProperties; use ProxyManagerTestAsset\ClassWithMixedTypedProperties; +use ProxyManagerTestAsset\ClassWithReadOnlyProperties; use ReflectionClass; /** @@ -191,6 +192,32 @@ public function testBodyStructureWithTypedProperties(): void +$nonReferenceableProperties = new class() { + public ?bool $publicBoolPropertyWithoutDefaultValue; + public ?int $publicIntPropertyWithoutDefaultValue; + public ?float $publicFloatPropertyWithoutDefaultValue; + public ?string $publicStringPropertyWithoutDefaultValue; + public ?array $publicArrayPropertyWithoutDefaultValue; + public ?iterable $publicIterablePropertyWithoutDefaultValue; + public ?object $publicObjectProperty; + public ?\ProxyManagerTestAsset\EmptyClass $publicClassProperty; + public ?bool $protectedBoolPropertyWithoutDefaultValue; + public ?int $protectedIntPropertyWithoutDefaultValue; + public ?float $protectedFloatPropertyWithoutDefaultValue; + public ?string $protectedStringPropertyWithoutDefaultValue; + public ?array $protectedArrayPropertyWithoutDefaultValue; + public ?iterable $protectedIterablePropertyWithoutDefaultValue; + public ?object $protectedObjectProperty; + public ?\ProxyManagerTestAsset\EmptyClass $protectedClassProperty; + public ?bool $privateBoolPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties; + public ?int $privateIntPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties; + public ?float $privateFloatPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties; + public ?string $privateStringPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties; + public ?array $privateArrayPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties; + public ?iterable $privateIterablePropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties; + public ?object $privateObjectProperty_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties; + public ?\ProxyManagerTestAsset\EmptyClass $privateClassProperty_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties; +}; $properties = [ 'publicUnTypedProperty' => & $this->publicUnTypedProperty, 'publicUnTypedPropertyWithoutDefaultValue' => & $this->publicUnTypedPropertyWithoutDefaultValue, @@ -236,6 +263,30 @@ public function testBodyStructureWithTypedProperties(): void '' . "\0" . '*' . "\0" . 'protectedNullableIterablePropertyWithoutDefaultValue' => & $this->protectedNullableIterablePropertyWithoutDefaultValue, '' . "\0" . '*' . "\0" . 'protectedNullableObjectProperty' => & $this->protectedNullableObjectProperty, '' . "\0" . '*' . "\0" . 'protectedNullableClassProperty' => & $this->protectedNullableClassProperty, + 'publicBoolPropertyWithoutDefaultValue' => & $nonReferenceableProperties->publicBoolPropertyWithoutDefaultValue, + 'publicIntPropertyWithoutDefaultValue' => & $nonReferenceableProperties->publicIntPropertyWithoutDefaultValue, + 'publicFloatPropertyWithoutDefaultValue' => & $nonReferenceableProperties->publicFloatPropertyWithoutDefaultValue, + 'publicStringPropertyWithoutDefaultValue' => & $nonReferenceableProperties->publicStringPropertyWithoutDefaultValue, + 'publicArrayPropertyWithoutDefaultValue' => & $nonReferenceableProperties->publicArrayPropertyWithoutDefaultValue, + 'publicIterablePropertyWithoutDefaultValue' => & $nonReferenceableProperties->publicIterablePropertyWithoutDefaultValue, + 'publicObjectProperty' => & $nonReferenceableProperties->publicObjectProperty, + 'publicClassProperty' => & $nonReferenceableProperties->publicClassProperty, + '' . "\0" . '*' . "\0" . 'protectedBoolPropertyWithoutDefaultValue' => & $nonReferenceableProperties->protectedBoolPropertyWithoutDefaultValue, + '' . "\0" . '*' . "\0" . 'protectedIntPropertyWithoutDefaultValue' => & $nonReferenceableProperties->protectedIntPropertyWithoutDefaultValue, + '' . "\0" . '*' . "\0" . 'protectedFloatPropertyWithoutDefaultValue' => & $nonReferenceableProperties->protectedFloatPropertyWithoutDefaultValue, + '' . "\0" . '*' . "\0" . 'protectedStringPropertyWithoutDefaultValue' => & $nonReferenceableProperties->protectedStringPropertyWithoutDefaultValue, + '' . "\0" . '*' . "\0" . 'protectedArrayPropertyWithoutDefaultValue' => & $nonReferenceableProperties->protectedArrayPropertyWithoutDefaultValue, + '' . "\0" . '*' . "\0" . 'protectedIterablePropertyWithoutDefaultValue' => & $nonReferenceableProperties->protectedIterablePropertyWithoutDefaultValue, + '' . "\0" . '*' . "\0" . 'protectedObjectProperty' => & $nonReferenceableProperties->protectedObjectProperty, + '' . "\0" . '*' . "\0" . 'protectedClassProperty' => & $nonReferenceableProperties->protectedClassProperty, + '' . "\0" . 'ProxyManagerTestAsset\\ClassWithMixedTypedProperties' . "\0" . 'privateBoolPropertyWithoutDefaultValue' => & $nonReferenceableProperties->privateBoolPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties, + '' . "\0" . 'ProxyManagerTestAsset\\ClassWithMixedTypedProperties' . "\0" . 'privateIntPropertyWithoutDefaultValue' => & $nonReferenceableProperties->privateIntPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties, + '' . "\0" . 'ProxyManagerTestAsset\\ClassWithMixedTypedProperties' . "\0" . 'privateFloatPropertyWithoutDefaultValue' => & $nonReferenceableProperties->privateFloatPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties, + '' . "\0" . 'ProxyManagerTestAsset\\ClassWithMixedTypedProperties' . "\0" . 'privateStringPropertyWithoutDefaultValue' => & $nonReferenceableProperties->privateStringPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties, + '' . "\0" . 'ProxyManagerTestAsset\\ClassWithMixedTypedProperties' . "\0" . 'privateArrayPropertyWithoutDefaultValue' => & $nonReferenceableProperties->privateArrayPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties, + '' . "\0" . 'ProxyManagerTestAsset\\ClassWithMixedTypedProperties' . "\0" . 'privateIterablePropertyWithoutDefaultValue' => & $nonReferenceableProperties->privateIterablePropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties, + '' . "\0" . 'ProxyManagerTestAsset\\ClassWithMixedTypedProperties' . "\0" . 'privateObjectProperty' => & $nonReferenceableProperties->privateObjectProperty_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties, + '' . "\0" . 'ProxyManagerTestAsset\\ClassWithMixedTypedProperties' . "\0" . 'privateClassProperty' => & $nonReferenceableProperties->privateClassProperty_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties, ]; static $cacheFetchProxyManagerTestAsset_ClassWithMixedTypedProperties; @@ -268,6 +319,97 @@ public function testBodyStructureWithTypedProperties(): void $cacheFetchProxyManagerTestAsset_ClassWithMixedTypedProperties($this, $properties); $result = $this->init->__invoke($this, $methodName, $parameters, $this->init, $properties); +isset($nonReferenceableProperties->publicBoolPropertyWithoutDefaultValue) && $this->publicBoolPropertyWithoutDefaultValue = $nonReferenceableProperties->publicBoolPropertyWithoutDefaultValue; +isset($nonReferenceableProperties->publicIntPropertyWithoutDefaultValue) && $this->publicIntPropertyWithoutDefaultValue = $nonReferenceableProperties->publicIntPropertyWithoutDefaultValue; +isset($nonReferenceableProperties->publicFloatPropertyWithoutDefaultValue) && $this->publicFloatPropertyWithoutDefaultValue = $nonReferenceableProperties->publicFloatPropertyWithoutDefaultValue; +isset($nonReferenceableProperties->publicStringPropertyWithoutDefaultValue) && $this->publicStringPropertyWithoutDefaultValue = $nonReferenceableProperties->publicStringPropertyWithoutDefaultValue; +isset($nonReferenceableProperties->publicArrayPropertyWithoutDefaultValue) && $this->publicArrayPropertyWithoutDefaultValue = $nonReferenceableProperties->publicArrayPropertyWithoutDefaultValue; +isset($nonReferenceableProperties->publicIterablePropertyWithoutDefaultValue) && $this->publicIterablePropertyWithoutDefaultValue = $nonReferenceableProperties->publicIterablePropertyWithoutDefaultValue; +isset($nonReferenceableProperties->publicObjectProperty) && $this->publicObjectProperty = $nonReferenceableProperties->publicObjectProperty; +isset($nonReferenceableProperties->publicClassProperty) && $this->publicClassProperty = $nonReferenceableProperties->publicClassProperty; +isset($nonReferenceableProperties->protectedBoolPropertyWithoutDefaultValue) && $this->protectedBoolPropertyWithoutDefaultValue = $nonReferenceableProperties->protectedBoolPropertyWithoutDefaultValue; +isset($nonReferenceableProperties->protectedIntPropertyWithoutDefaultValue) && $this->protectedIntPropertyWithoutDefaultValue = $nonReferenceableProperties->protectedIntPropertyWithoutDefaultValue; +isset($nonReferenceableProperties->protectedFloatPropertyWithoutDefaultValue) && $this->protectedFloatPropertyWithoutDefaultValue = $nonReferenceableProperties->protectedFloatPropertyWithoutDefaultValue; +isset($nonReferenceableProperties->protectedStringPropertyWithoutDefaultValue) && $this->protectedStringPropertyWithoutDefaultValue = $nonReferenceableProperties->protectedStringPropertyWithoutDefaultValue; +isset($nonReferenceableProperties->protectedArrayPropertyWithoutDefaultValue) && $this->protectedArrayPropertyWithoutDefaultValue = $nonReferenceableProperties->protectedArrayPropertyWithoutDefaultValue; +isset($nonReferenceableProperties->protectedIterablePropertyWithoutDefaultValue) && $this->protectedIterablePropertyWithoutDefaultValue = $nonReferenceableProperties->protectedIterablePropertyWithoutDefaultValue; +isset($nonReferenceableProperties->protectedObjectProperty) && $this->protectedObjectProperty = $nonReferenceableProperties->protectedObjectProperty; +isset($nonReferenceableProperties->protectedClassProperty) && $this->protectedClassProperty = $nonReferenceableProperties->protectedClassProperty; +static $cacheAssignProxyManagerTestAsset_ClassWithMixedTypedProperties; + +$cacheAssignProxyManagerTestAsset_ClassWithMixedTypedProperties ?? $cacheAssignProxyManagerTestAsset_ClassWithMixedTypedProperties = \Closure::bind(function ($instance, $nonReferenceableProperties) { + isset($nonReferenceableProperties->privateBoolPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties) && $this->privateBoolPropertyWithoutDefaultValue = $nonReferenceableProperties->privateBoolPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties; + isset($nonReferenceableProperties->privateIntPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties) && $this->privateIntPropertyWithoutDefaultValue = $nonReferenceableProperties->privateIntPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties; + isset($nonReferenceableProperties->privateFloatPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties) && $this->privateFloatPropertyWithoutDefaultValue = $nonReferenceableProperties->privateFloatPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties; + isset($nonReferenceableProperties->privateStringPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties) && $this->privateStringPropertyWithoutDefaultValue = $nonReferenceableProperties->privateStringPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties; + isset($nonReferenceableProperties->privateArrayPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties) && $this->privateArrayPropertyWithoutDefaultValue = $nonReferenceableProperties->privateArrayPropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties; + isset($nonReferenceableProperties->privateIterablePropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties) && $this->privateIterablePropertyWithoutDefaultValue = $nonReferenceableProperties->privateIterablePropertyWithoutDefaultValue_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties; + isset($nonReferenceableProperties->privateObjectProperty_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties) && $this->privateObjectProperty = $nonReferenceableProperties->privateObjectProperty_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties; + isset($nonReferenceableProperties->privateClassProperty_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties) && $this->privateClassProperty = $nonReferenceableProperties->privateClassProperty_on_ProxyManagerTestAsset_ClassWithMixedTypedProperties; +}, $this, 'ProxyManagerTestAsset\\ClassWithMixedTypedProperties'); + +$cacheAssignProxyManagerTestAsset_ClassWithMixedTypedProperties($this, $nonReferenceableProperties); +$this->track = false; + +return $result; +PHP; + + self::assertSame( + $expectedCode, + $callInitializer->getBody() + ); + } + + /** + * @requires PHP 8.1 + */ + public function testBodyStructureWithReadOnlyProperties(): void + { + $initializer = $this->createMock(PropertyGenerator::class); + $initializationTracker = $this->createMock(PropertyGenerator::class); + + $initializer->method('getName')->willReturn('init'); + $initializationTracker->method('getName')->willReturn('track'); + + $callInitializer = new CallInitializer( + $initializer, + $initializationTracker, + Properties::fromReflectionClass(new ReflectionClass(ClassWithReadOnlyProperties::class)) + ); + + $expectedCode = <<<'PHP' +if ($this->track || ! $this->init) { + return; +} + +$this->track = true; + + + + +$nonReferenceableProperties = new class() { + public null|\stdClass|string $property0; + public ?string $property1; + public ?string $property2_on_ProxyManagerTestAsset_ClassWithReadOnlyProperties; +}; +$properties = [ + 'property0' => & $nonReferenceableProperties->property0, + '' . "\0" . '*' . "\0" . 'property1' => & $nonReferenceableProperties->property1, + '' . "\0" . 'ProxyManagerTestAsset\\ClassWithReadOnlyProperties' . "\0" . 'property2' => & $nonReferenceableProperties->property2_on_ProxyManagerTestAsset_ClassWithReadOnlyProperties, +]; + + + +$result = $this->init->__invoke($this, $methodName, $parameters, $this->init, $properties); +static $cacheAssignProxyManagerTestAsset_ClassWithReadOnlyProperties; + +$cacheAssignProxyManagerTestAsset_ClassWithReadOnlyProperties ?? $cacheAssignProxyManagerTestAsset_ClassWithReadOnlyProperties = \Closure::bind(function ($instance, $nonReferenceableProperties) { + isset($nonReferenceableProperties->property0) && $this->property0 = $nonReferenceableProperties->property0; + isset($nonReferenceableProperties->property1) && $this->property1 = $nonReferenceableProperties->property1; + isset($nonReferenceableProperties->property2_on_ProxyManagerTestAsset_ClassWithReadOnlyProperties) && $this->property2 = $nonReferenceableProperties->property2_on_ProxyManagerTestAsset_ClassWithReadOnlyProperties; +}, $this, 'ProxyManagerTestAsset\\ClassWithReadOnlyProperties'); + +$cacheAssignProxyManagerTestAsset_ClassWithReadOnlyProperties($this, $nonReferenceableProperties); $this->track = false; return $result; diff --git a/tests/ProxyManagerTest/ProxyGenerator/NullObject/MethodGenerator/StaticProxyConstructorTest.php b/tests/ProxyManagerTest/ProxyGenerator/NullObject/MethodGenerator/StaticProxyConstructorTest.php index 35170aab..c0e4ebd4 100644 --- a/tests/ProxyManagerTest/ProxyGenerator/NullObject/MethodGenerator/StaticProxyConstructorTest.php +++ b/tests/ProxyManagerTest/ProxyGenerator/NullObject/MethodGenerator/StaticProxyConstructorTest.php @@ -9,6 +9,7 @@ use ProxyManagerTestAsset\ClassWithMixedProperties; use ProxyManagerTestAsset\ClassWithMixedTypedProperties; use ProxyManagerTestAsset\ClassWithPrivateProperties; +use ProxyManagerTestAsset\ClassWithReadOnlyProperties; use ReflectionClass; /** @@ -101,6 +102,29 @@ public function testBodyStructureWithTypedProperties(): void $instance->publicNullableObjectProperty = null; $instance->publicNullableClassProperty = null; +return $instance;', + $constructor->getBody() + ); + } + + /** + * @requires PHP 8.1 + */ + public function testBodyStructureWithReadOnlyProperties(): void + { + $constructor = new StaticProxyConstructor(new ReflectionClass(ClassWithReadOnlyProperties::class)); + + self::assertSame('staticProxyConstructor', $constructor->getName()); + self::assertSame(ClassWithReadOnlyProperties::class, (string) $constructor->getReturnType()); + self::assertTrue($constructor->isStatic()); + self::assertSame('public', $constructor->getVisibility()); + self::assertCount(0, $constructor->getParameters()); + self::assertSame( + 'static $reflection; + +$reflection = $reflection ?? new \ReflectionClass(__CLASS__); +$instance = $reflection->newInstanceWithoutConstructor(); + return $instance;', $constructor->getBody() ); diff --git a/tests/ProxyManagerTest/ProxyGenerator/NullObjectGeneratorTest.php b/tests/ProxyManagerTest/ProxyGenerator/NullObjectGeneratorTest.php index 039e8f42..a81db809 100644 --- a/tests/ProxyManagerTest/ProxyGenerator/NullObjectGeneratorTest.php +++ b/tests/ProxyManagerTest/ProxyGenerator/NullObjectGeneratorTest.php @@ -20,6 +20,7 @@ use ProxyManagerTestAsset\ClassWithMixedTypedProperties; use ProxyManagerTestAsset\ClassWithPhp80TypedMethods; use ProxyManagerTestAsset\ClassWithPhp81Defaults; +use ProxyManagerTestAsset\ClassWithReadOnlyProperties; use ReflectionClass; use ReflectionMethod; @@ -133,7 +134,8 @@ public function getTestedImplementations(): array } if (PHP_VERSION_ID >= 80100) { - $implementations[] = [ClassWithPhp81Defaults::class]; + $implementations['php81defaults'] = [ClassWithPhp81Defaults::class]; + $implementations['readonly'] = [ClassWithReadOnlyProperties::class]; } return $implementations; diff --git a/tests/ProxyManagerTest/ProxyGenerator/RemoteObjectGeneratorTest.php b/tests/ProxyManagerTest/ProxyGenerator/RemoteObjectGeneratorTest.php index 3eedc968..8fa66d3b 100644 --- a/tests/ProxyManagerTest/ProxyGenerator/RemoteObjectGeneratorTest.php +++ b/tests/ProxyManagerTest/ProxyGenerator/RemoteObjectGeneratorTest.php @@ -19,6 +19,7 @@ use ProxyManagerTestAsset\ClassWithMixedTypedProperties; use ProxyManagerTestAsset\ClassWithPhp80TypedMethods; use ProxyManagerTestAsset\ClassWithPhp81Defaults; +use ProxyManagerTestAsset\ClassWithReadOnlyProperties; use ReflectionClass; use function array_diff; @@ -104,6 +105,7 @@ public function getTestedImplementations(): array if (PHP_VERSION_ID >= 80100) { $implementations['php81defaults'] = [ClassWithPhp81Defaults::class]; + $implementations['readonly'] = [ClassWithReadOnlyProperties::class]; } return $implementations; diff --git a/tests/ProxyManagerTest/ProxyGenerator/Util/PropertiesTest.php b/tests/ProxyManagerTest/ProxyGenerator/Util/PropertiesTest.php index 3792f3df..b52dc73a 100644 --- a/tests/ProxyManagerTest/ProxyGenerator/Util/PropertiesTest.php +++ b/tests/ProxyManagerTest/ProxyGenerator/Util/PropertiesTest.php @@ -248,57 +248,33 @@ public function testOnlyNonReferenceableProperties(): void ->onlyNonReferenceableProperties() ->getInstanceProperties(); - self::assertCount(48, $nonReferenceableProperties); + self::assertCount(24, $nonReferenceableProperties); self::assertSame( [ 'publicBoolPropertyWithoutDefaultValue', - 'publicNullableBoolPropertyWithoutDefaultValue', 'publicIntPropertyWithoutDefaultValue', - 'publicNullableIntPropertyWithoutDefaultValue', 'publicFloatPropertyWithoutDefaultValue', - 'publicNullableFloatPropertyWithoutDefaultValue', 'publicStringPropertyWithoutDefaultValue', - 'publicNullableStringPropertyWithoutDefaultValue', 'publicArrayPropertyWithoutDefaultValue', - 'publicNullableArrayPropertyWithoutDefaultValue', 'publicIterablePropertyWithoutDefaultValue', - 'publicNullableIterablePropertyWithoutDefaultValue', 'publicObjectProperty', - 'publicNullableObjectProperty', 'publicClassProperty', - 'publicNullableClassProperty', 'protectedBoolPropertyWithoutDefaultValue', - 'protectedNullableBoolPropertyWithoutDefaultValue', 'protectedIntPropertyWithoutDefaultValue', - 'protectedNullableIntPropertyWithoutDefaultValue', 'protectedFloatPropertyWithoutDefaultValue', - 'protectedNullableFloatPropertyWithoutDefaultValue', 'protectedStringPropertyWithoutDefaultValue', - 'protectedNullableStringPropertyWithoutDefaultValue', 'protectedArrayPropertyWithoutDefaultValue', - 'protectedNullableArrayPropertyWithoutDefaultValue', 'protectedIterablePropertyWithoutDefaultValue', - 'protectedNullableIterablePropertyWithoutDefaultValue', 'protectedObjectProperty', - 'protectedNullableObjectProperty', 'protectedClassProperty', - 'protectedNullableClassProperty', 'privateBoolPropertyWithoutDefaultValue', - 'privateNullableBoolPropertyWithoutDefaultValue', 'privateIntPropertyWithoutDefaultValue', - 'privateNullableIntPropertyWithoutDefaultValue', 'privateFloatPropertyWithoutDefaultValue', - 'privateNullableFloatPropertyWithoutDefaultValue', 'privateStringPropertyWithoutDefaultValue', - 'privateNullableStringPropertyWithoutDefaultValue', 'privateArrayPropertyWithoutDefaultValue', - 'privateNullableArrayPropertyWithoutDefaultValue', 'privateIterablePropertyWithoutDefaultValue', - 'privateNullableIterablePropertyWithoutDefaultValue', 'privateObjectProperty', - 'privateNullableObjectProperty', 'privateClassProperty', - 'privateNullableClassProperty', ], array_values(array_map(static function (ReflectionProperty $property): string { return $property->getName(); diff --git a/tests/ProxyManagerTest/ProxyGenerator/Util/UnsetPropertiesGeneratorTest.php b/tests/ProxyManagerTest/ProxyGenerator/Util/UnsetPropertiesGeneratorTest.php index 89246e34..47ff4db8 100644 --- a/tests/ProxyManagerTest/ProxyGenerator/Util/UnsetPropertiesGeneratorTest.php +++ b/tests/ProxyManagerTest/ProxyGenerator/Util/UnsetPropertiesGeneratorTest.php @@ -11,6 +11,7 @@ use ProxyManagerTestAsset\ClassWithCollidingPrivateInheritedProperties; use ProxyManagerTestAsset\ClassWithMixedProperties; use ProxyManagerTestAsset\ClassWithMixedTypedProperties; +use ProxyManagerTestAsset\ClassWithReadOnlyProperties; use ProxyManagerTestAsset\EmptyClass; use ReflectionClass; @@ -29,6 +30,10 @@ final class UnsetPropertiesGeneratorTest extends TestCase */ public function testGeneratedCode(string $className, string $expectedCode, string $instanceName): void { + if (false !== strpos($className, 'ReadOnlyProp') && \PHP_VERSION_ID < 80100) { + self::markTestSkipped('PHP 8.1 required.'); + } + if (false !== strpos($className, 'TypedProp') && \PHP_VERSION_ID < 70400) { self::markTestSkipped('PHP 7.4 required.'); } @@ -106,6 +111,15 @@ public function classNamesProvider(): array , 'bar', ], + ClassWithReadOnlyProperties::class => [ + ClassWithReadOnlyProperties::class, + '\Closure::bind(function (\ProxyManagerTestAsset\ClassWithReadOnlyProperties $instance) { + unset($instance->property0, $instance->property1, $instance->property2); +}, $bar, \'ProxyManagerTestAsset\\\\ClassWithReadOnlyProperties\')->__invoke($bar); + +', + 'bar', + ], ]; } } diff --git a/tests/ProxyManagerTestAsset/ClassWithReadOnlyProperties.php b/tests/ProxyManagerTestAsset/ClassWithReadOnlyProperties.php new file mode 100644 index 00000000..16691a83 --- /dev/null +++ b/tests/ProxyManagerTestAsset/ClassWithReadOnlyProperties.php @@ -0,0 +1,17 @@ +