Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for readonly properties #22

Merged
merged 1 commit into from
May 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,42 +72,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 +145,29 @@ 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()) : '');
$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) {
Expand Down Expand Up @@ -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;
}
}
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