Skip to content

Commit

Permalink
Add support for readonly properties
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-grekas committed May 4, 2022
1 parent 91c642d commit 2d13a8e
Show file tree
Hide file tree
Showing 21 changed files with 388 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,42 +63,55 @@ 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,
$initialization,
$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";
}
Expand All @@ -123,7 +136,7 @@ function (ReflectionProperty $property): string {
);
}

private function propertiesReferenceArrayCode(Properties $properties): string
private function propertiesReferenceArrayCode(Properties $properties, Properties $nonReferenceableProperties): string
{
$assignments = [];

Expand All @@ -133,7 +146,12 @@ private function propertiesReferenceArrayCode(Properties $properties): string
. ',';
}

$code = "\$properties = [\n" . implode("\n", $assignments) . "\n];\n\n";
foreach ($nonReferenceableProperties->getInstanceProperties() as $propertyInternalName => $property) {
$assignments[] = sprintf(' %s => & $nonReferenceableProperties[%1$s],', var_export($propertyInternalName, true));
}

$code = $nonReferenceableProperties->empty() ? '' : "\$nonReferenceableProperties = [];\n";
$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) {
Expand Down Expand Up @@ -173,4 +191,39 @@ 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())) {
$code[] = sprintf('isset($nonReferenceableProperties[%s]) && $this->%s = $nonReferenceableProperties[%1$s];', var_export($propertyInternalName, true), $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, array $properties) {';

foreach ($scopedProperties as $propertyInternalName => $property) {
$code[] = sprintf(' isset($properties[%s]) && $this->%s = $properties[%1$s];', var_export($propertyInternalName, true), $property->getName());
}

$code[] = '}, $this, ' . var_export($className, true) . ");\n";
$code[] = '$' . $cacheKey . '($this, $nonReferenceableProperties);';
}

return implode("\n", $code) . "\n";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,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);
Expand All @@ -75,7 +75,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);
Expand Down Expand Up @@ -110,6 +110,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(),
Expand All @@ -121,8 +130,10 @@ public function __construct(
$protectedProperties->getName(),
$privateProperties->getName(),
$privateProperties->getName(),
$privateReturnCode,
$initializationTracker->getName(),
$privateProperties->getName(),
$privateReturnCode,
$parentAccess
));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ class PrivatePropertiesMap extends PropertyGenerator
{
public const KEY_DEFAULT_VALUE = 'defaultValue';

/** @var list<string> */
private $readOnlyPropertyNames = [];

/**
* Constructor
*
Expand All @@ -35,14 +38,28 @@ public function __construct(Properties $properties)
$this->setDefaultValue($this->getMap($properties));
}

/**
* @return list<string>
*/
public function getReadOnlyPropertyNames(): array
{
return $this->readOnlyPropertyNames;
}

/**
* @return array<string, array<class-string, bool>>
*/
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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
15 changes: 15 additions & 0 deletions src/ProxyManager/ProxyGenerator/Util/Properties.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);

Expand Down
41 changes: 25 additions & 16 deletions src/ProxyManager/ProxyGenerator/Util/UnsetPropertiesGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 $property) {
if ($property->isPrivate() || \PHP_VERSION_ID >= 80100 && $property->isReadOnly()) {
$scopedPropertyGroups[$property->getDeclaringClass()->getName()][$property->getName()] = $property;
} else {
$nonScopedProperties[] = $property;
}
}

return self::generateUnsetNonScopedPropertiesCode($nonScopedProperties, $instanceName)
. self::generateUnsetScopedPropertiesCode($scopedPropertyGroups, $instanceName);
}

private static function generateUnsetAccessiblePropertiesCode(Properties $properties, string $instanceName): string
/** @param array<string, ReflectionProperty> $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<class-string, array<string, ReflectionProperty>> $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
);
}
Expand All @@ -67,7 +76,7 @@ private static function generateUnsetPrivatePropertiesCode(Properties $propertie
}

/** @param array<string, ReflectionProperty> $properties */
private static function generateUnsetClassPrivatePropertiesBlock(
private static function generateUnsetClassScopedPropertiesBlock(
ReflectionClass $declaringClass,
array $properties,
string $instanceName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 2d13a8e

Please sign in to comment.