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 ccd80c0
Show file tree
Hide file tree
Showing 21 changed files with 466 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,21 @@

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 array_map;
use function array_unique;
use function explode;
use function get_class;
use function implode;
use function sprintf;
use function str_replace;
Expand Down Expand Up @@ -63,42 +71,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,17 +144,26 @@ 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[] = ' '
. var_export($propertyInternalName, true) . ' => & $this->' . $property->getName()
. ',';
}

$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()) : '');
$nonReferenceablePropertiesDefinition .= sprintf(" public %s $%s;\n", self::getReferenceableType($property->getType()), $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) {
Expand Down Expand Up @@ -173,4 +203,73 @@ 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';
$dedup = false;

foreach ($type->getTypes() as $subType) {
if ($subType instanceof ReflectionNamedType) {
$union .= '|' . ($subType->isBuiltin() ? '' : '\\') . $subType->getName();
} else {
$union .= '|' . self::getReferenceableType($subType);
$dedup = true;
}
}

if ($dedup) {
$union = implode('|', array_unique(explode('|', str_replace('|?', '', $union))));
}

return $union;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use ProxyManager\ProxyGenerator\Util\PublicScopeSimulator;
use ReflectionClass;

use function implode;
use function sprintf;

/**
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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(),
Expand All @@ -121,8 +131,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
Loading

0 comments on commit ccd80c0

Please sign in to comment.