Skip to content

Commit bf56e3a

Browse files
Add support for readonly properties
1 parent 91c642d commit bf56e3a

21 files changed

+460
-82
lines changed

src/ProxyManager/ProxyGenerator/LazyLoadingGhost/MethodGenerator/CallInitializer.php

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,22 @@
66

77
use Laminas\Code\Generator\ParameterGenerator;
88
use Laminas\Code\Generator\PropertyGenerator;
9+
use LogicException;
910
use ProxyManager\Generator\MethodGenerator;
1011
use ProxyManager\Generator\Util\IdentifierSuffixer;
1112
use ProxyManager\Generator\ValueGenerator;
1213
use ProxyManager\ProxyGenerator\Util\Properties;
14+
use ReflectionIntersectionType;
15+
use ReflectionNamedType;
1316
use ReflectionProperty;
17+
use ReflectionType;
18+
use ReflectionUnionType;
1419

20+
use function assert;
1521
use function array_map;
22+
use function array_unique;
23+
use function explode;
24+
use function get_class;
1625
use function implode;
1726
use function sprintf;
1827
use function str_replace;
@@ -63,42 +72,55 @@ public function __construct(
6372
%s
6473
6574
$result = $this->%s->__invoke($this, $methodName, $parameters, $this->%s, $properties);
66-
$this->%s = false;
75+
%s$this->%s = false;
6776
6877
return $result;
6978
PHP;
7079

71-
$referenceableProperties = $properties->withoutNonReferenceableProperties();
80+
$referenceableProperties = $properties->withoutNonReferenceableProperties();
81+
$nonReferenceableProperties = $properties->onlyNonReferenceableProperties();
7282

7383
$this->setBody(sprintf(
7484
$bodyTemplate,
7585
$initialization,
7686
$initializer,
7787
$initialization,
7888
$this->propertiesInitializationCode($referenceableProperties),
79-
$this->propertiesReferenceArrayCode($referenceableProperties),
89+
$this->propertiesReferenceArrayCode($referenceableProperties, $nonReferenceableProperties),
8090
$initializer,
8191
$initializer,
92+
$this->propertiesNonReferenceableCode($nonReferenceableProperties),
8293
$initialization
8394
));
8495
}
8596

8697
private function propertiesInitializationCode(Properties $properties): string
8798
{
99+
$scopedPropertyGroups = [];
100+
$nonScopedProperties = [];
101+
102+
foreach ($properties->getInstanceProperties() as $property) {
103+
if ($property->isPrivate() || (\PHP_VERSION_ID >= 80100 && $property->isReadOnly())) {
104+
$scopedPropertyGroups[$property->getDeclaringClass()->getName()][$property->getName()] = $property;
105+
} else {
106+
$nonScopedProperties[] = $property;
107+
}
108+
}
109+
88110
$assignments = [];
89111

90-
foreach ($properties->getAccessibleProperties() as $property) {
112+
foreach ($nonScopedProperties as $property) {
91113
$assignments[] = '$this->'
92114
. $property->getName()
93115
. ' = ' . $this->getExportedPropertyDefaultValue($property)
94116
. ';';
95117
}
96118

97-
foreach ($properties->getGroupedPrivateProperties() as $className => $privateProperties) {
119+
foreach ($scopedPropertyGroups as $className => $scopedProperties) {
98120
$cacheKey = 'cache' . str_replace('\\', '_', $className);
99121
$assignments[] = 'static $' . $cacheKey . ";\n\n"
100122
. '$' . $cacheKey . ' ?? $' . $cacheKey . " = \\Closure::bind(static function (\$instance) {\n"
101-
. $this->getPropertyDefaultsAssignments($privateProperties) . "\n"
123+
. $this->getPropertyDefaultsAssignments($scopedProperties) . "\n"
102124
. '}, null, ' . var_export($className, true) . ");\n\n"
103125
. '$' . $cacheKey . "(\$this);\n\n";
104126
}
@@ -123,17 +145,29 @@ function (ReflectionProperty $property): string {
123145
);
124146
}
125147

126-
private function propertiesReferenceArrayCode(Properties $properties): string
148+
private function propertiesReferenceArrayCode(Properties $properties, Properties $nonReferenceableProperties): string
127149
{
128-
$assignments = [];
150+
$assignments = [];
151+
$nonReferenceablePropertiesDefinition = '';
129152

130153
foreach ($properties->getAccessibleProperties() as $propertyInternalName => $property) {
131154
$assignments[] = ' '
132155
. var_export($propertyInternalName, true) . ' => & $this->' . $property->getName()
133156
. ',';
134157
}
135158

136-
$code = "\$properties = [\n" . implode("\n", $assignments) . "\n];\n\n";
159+
foreach ($nonReferenceableProperties->getInstanceProperties() as $propertyInternalName => $property) {
160+
$propertyAlias = $property->getName() . ($property->isPrivate() ? '_on_' . str_replace('\\', '_', $property->getDeclaringClass()->getName()) : '');
161+
$propertyType = $property->getType();
162+
assert($propertyType !== null);
163+
164+
$nonReferenceablePropertiesDefinition .= sprintf(" public %s $%s;\n", self::getReferenceableType($propertyType), $propertyAlias);
165+
166+
$assignments[] = sprintf(' %s => & $nonReferenceableProperties->%s,', var_export($propertyInternalName, true), $propertyAlias);
167+
}
168+
169+
$code = $nonReferenceableProperties->empty() ? '' : sprintf("\$nonReferenceableProperties = new class() {\n%s};\n", $nonReferenceablePropertiesDefinition);
170+
$code .= "\$properties = [\n" . implode("\n", $assignments) . "\n];\n\n";
137171

138172
// must use assignments, as direct reference during array definition causes a fatal error (not sure why)
139173
foreach ($properties->getGroupedPrivateProperties() as $className => $classPrivateProperties) {
@@ -173,4 +207,63 @@ private function getExportedPropertyDefaultValue(ReflectionProperty $property):
173207

174208
return (new ValueGenerator($defaults[$name] ?? null))->generate();
175209
}
210+
211+
private function propertiesNonReferenceableCode(Properties $properties): string
212+
{
213+
if ($properties->empty()) {
214+
return '';
215+
}
216+
217+
$code = [];
218+
$scopedPropertyGroups = [];
219+
220+
foreach ($properties->getInstanceProperties() as $propertyInternalName => $property) {
221+
if (! $property->isPrivate() && (\PHP_VERSION_ID < 80100 || ! $property->isReadOnly())) {
222+
$propertyAlias = $property->getName() . ($property->isPrivate() ? '_on_' . str_replace('\\', '_', $property->getDeclaringClass()->getName()) : '');
223+
$code[] = sprintf('isset($nonReferenceableProperties->%s) && $this->%s = $nonReferenceableProperties->%1$s;', $propertyAlias, $property->getName());
224+
} else {
225+
$scopedPropertyGroups[$property->getDeclaringClass()->getName()][$propertyInternalName] = $property;
226+
}
227+
}
228+
229+
foreach ($scopedPropertyGroups as $className => $scopedProperties) {
230+
$cacheKey = 'cacheAssign' . str_replace('\\', '_', $className);
231+
232+
$code[] = 'static $' . $cacheKey . ";\n";
233+
$code[] = '$' . $cacheKey . ' ?? $' . $cacheKey . ' = \Closure::bind(function ($instance, $nonReferenceableProperties) {';
234+
235+
foreach ($scopedProperties as $property) {
236+
$propertyAlias = $property->getName() . ($property->isPrivate() ? '_on_' . str_replace('\\', '_', $property->getDeclaringClass()->getName()) : '');
237+
$code[] = sprintf(' isset($nonReferenceableProperties->%s) && $this->%s = $nonReferenceableProperties->%1$s;', $propertyAlias, $property->getName());
238+
}
239+
240+
$code[] = '}, $this, ' . var_export($className, true) . ");\n";
241+
$code[] = '$' . $cacheKey . '($this, $nonReferenceableProperties);';
242+
}
243+
244+
return implode("\n", $code) . "\n";
245+
}
246+
247+
private static function getReferenceableType(ReflectionType $type): string
248+
{
249+
if ($type instanceof ReflectionNamedType) {
250+
return '?' . ($type->isBuiltin() ? '' : '\\') . $type->getName();
251+
}
252+
253+
if ($type instanceof ReflectionIntersectionType) {
254+
return self::getReferenceableType($type->getTypes()[0]);
255+
}
256+
257+
if (! $type instanceof ReflectionUnionType) {
258+
throw new LogicException('Unexpected ' . get_class($type));
259+
}
260+
261+
$union = 'null';
262+
263+
foreach ($type->getTypes() as $subType) {
264+
$union .= '|' . ($subType->isBuiltin() ? '' : '\\') . $subType->getName();
265+
}
266+
267+
return $union;
268+
}
176269
}

src/ProxyManager/ProxyGenerator/LazyLoadingGhost/MethodGenerator/MagicGet.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use ProxyManager\ProxyGenerator\Util\PublicScopeSimulator;
1717
use ReflectionClass;
1818

19+
use function implode;
1920
use function sprintf;
2021

2122
/**
@@ -63,7 +64,7 @@ class MagicGet extends MagicMethodGenerator
6364
$accessor = isset($accessorCache[$cacheKey])
6465
? $accessorCache[$cacheKey]
6566
: $accessorCache[$cacheKey] = \Closure::bind(static function & ($instance) use ($name) {
66-
return $instance->$name;
67+
%s
6768
}, null, $class);
6869
6970
return $accessor($this);
@@ -75,7 +76,7 @@ class MagicGet extends MagicMethodGenerator
7576
$accessor = isset($accessorCache[$cacheKey])
7677
? $accessorCache[$cacheKey]
7778
: $accessorCache[$cacheKey] = \Closure::bind(static function & ($instance) use ($name) {
78-
return $instance->$name;
79+
%s
7980
}, null, $tmpClass);
8081
8182
return $accessor($this);
@@ -110,6 +111,15 @@ public function __construct(
110111
);
111112
}
112113

114+
$readOnlyPropertyNames = $privateProperties->getReadOnlyPropertyNames();
115+
116+
if ($readOnlyPropertyNames) {
117+
$privateReturnCode = sprintf('\in_array($name, [\'%s\'], true) ? $value = $instance->$name : $value = & $instance->$name;', implode("', '", $readOnlyPropertyNames));
118+
$privateReturnCode .= "\n\n return \$value;";
119+
} else {
120+
$privateReturnCode = 'return $instance->$name;';
121+
}
122+
113123
$this->setBody(sprintf(
114124
$this->callParentTemplate,
115125
$initializerProperty->getName(),
@@ -121,8 +131,10 @@ public function __construct(
121131
$protectedProperties->getName(),
122132
$privateProperties->getName(),
123133
$privateProperties->getName(),
134+
$privateReturnCode,
124135
$initializationTracker->getName(),
125136
$privateProperties->getName(),
137+
$privateReturnCode,
126138
$parentAccess
127139
));
128140
}

src/ProxyManager/ProxyGenerator/LazyLoadingGhost/PropertyGenerator/PrivatePropertiesMap.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ class PrivatePropertiesMap extends PropertyGenerator
1616
{
1717
public const KEY_DEFAULT_VALUE = 'defaultValue';
1818

19+
/** @var list<string> */
20+
private $readOnlyPropertyNames = [];
21+
1922
/**
2023
* Constructor
2124
*
@@ -35,14 +38,28 @@ public function __construct(Properties $properties)
3538
$this->setDefaultValue($this->getMap($properties));
3639
}
3740

41+
/**
42+
* @return list<string>
43+
*/
44+
public function getReadOnlyPropertyNames(): array
45+
{
46+
return $this->readOnlyPropertyNames;
47+
}
48+
3849
/**
3950
* @return array<string, array<class-string, bool>>
4051
*/
4152
private function getMap(Properties $properties): array
4253
{
4354
$map = [];
4455

45-
foreach ($properties->getPrivateProperties() as $property) {
56+
foreach ($properties->getInstanceProperties() as $property) {
57+
if (\PHP_VERSION_ID >= 80100 && $property->isReadOnly()) {
58+
$this->readOnlyPropertyNames[] = $property->getName();
59+
} elseif (! $property->isPrivate()) {
60+
continue;
61+
}
62+
4663
$map[$property->getName()][$property->getDeclaringClass()->getName()] = true;
4764
}
4865

src/ProxyManager/ProxyGenerator/LazyLoadingGhost/PropertyGenerator/ProtectedPropertiesMap.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ private function getMap(Properties $properties): array
4141
$map = [];
4242

4343
foreach ($properties->getProtectedProperties() as $property) {
44+
if (\PHP_VERSION_ID >= 80100 && $property->isReadOnly()) {
45+
continue;
46+
}
47+
4448
$map[$property->getName()] = $property->getDeclaringClass()->getName();
4549
}
4650

src/ProxyManager/ProxyGenerator/LazyLoadingGhostGenerator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public function generate(ReflectionClass $originalClass, ClassGenerator $classGe
6363
$filteredProperties = Properties::fromReflectionClass($originalClass)
6464
->filter($proxyOptions['skippedProperties'] ?? []);
6565

66-
$publicProperties = new PublicPropertiesMap($filteredProperties);
66+
$publicProperties = new PublicPropertiesMap($filteredProperties, true);
6767
$privateProperties = new PrivatePropertiesMap($filteredProperties);
6868
$protectedProperties = new ProtectedPropertiesMap($filteredProperties);
6969
$skipDestructor = ($proxyOptions['skipDestructor'] ?? false) && $originalClass->hasMethod('__destruct');

src/ProxyManager/ProxyGenerator/PropertyGenerator/PublicPropertiesMap.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,15 @@ class PublicPropertiesMap extends PropertyGenerator
2020
/**
2121
* @throws InvalidArgumentException
2222
*/
23-
public function __construct(Properties $properties)
23+
public function __construct(Properties $properties, bool $skipReadOnlyProperties = false)
2424
{
2525
parent::__construct(IdentifierSuffixer::getIdentifier('publicProperties'));
2626

2727
foreach ($properties->getPublicProperties() as $publicProperty) {
28+
if ($skipReadOnlyProperties && \PHP_VERSION_ID >= 80100 && $publicProperty->isReadOnly()) {
29+
continue;
30+
}
31+
2832
$this->publicProperties[$publicProperty->getName()] = true;
2933
}
3034

src/ProxyManager/ProxyGenerator/Util/Properties.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,17 @@ public function onlyNonReferenceableProperties(): self
7979
return false;
8080
}
8181

82+
if (\PHP_VERSION_ID >= 80100 && $property->isReadOnly()) {
83+
return true;
84+
}
85+
86+
$type = $property->getType();
87+
assert($type instanceof ReflectionType);
88+
89+
if ($type->allowsNull()) {
90+
return false;
91+
}
92+
8293
return ! array_key_exists(
8394
$property->getName(),
8495
// https://bugs.php.net/bug.php?id=77673
@@ -103,6 +114,10 @@ public function withoutNonReferenceableProperties(): self
103114
return true;
104115
}
105116

117+
if (\PHP_VERSION_ID >= 80100 && $property->isReadOnly()) {
118+
return false;
119+
}
120+
106121
$type = $property->getType();
107122
assert($type instanceof ReflectionType);
108123

0 commit comments

Comments
 (0)