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

Implement property hooks via virtual properties #166

Merged
merged 39 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f6d6a62
Implement property hooks via virtual properties
thekid May 13, 2023
8abde26
Fix PHP 7.3
thekid May 13, 2023
87f629c
Add support for `__PROPERTY__`
thekid May 14, 2023
e3f2ae1
Do not declare virtual properties inside interfaces
thekid May 14, 2023
ffe6c4b
Change implementation to emit `__[hook]_[name]()` methods
thekid May 14, 2023
4138f72
QA: Reorder code
thekid May 14, 2023
c9d348c
Test abstract hooks
thekid May 14, 2023
c92001d
QA: Make tests consistent
thekid May 14, 2023
e747819
Make $this->propertyName work
thekid May 14, 2023
3756cf4
Support abstract properties
thekid May 15, 2023
1d73741
Merge branch 'master' into feature/property-hooks
thekid May 18, 2023
9830d04
Fix "Undefined property: lang\ast\nodes\Hook::$annotations"
thekid May 18, 2023
ed5003f
Add scope checks to private and protected properties
thekid May 18, 2023
6031c35
Simplify withScopeCheck()
thekid May 18, 2023
f21a615
Add support for for `<T>::$field::hook()`
thekid May 18, 2023
bbdf5a1
Merge branch 'master' into feature/property-hooks
thekid May 18, 2023
828ec93
Merge branch 'master' into feature/property-hooks
thekid May 21, 2023
d7650a2
MFH 8.12.0-RELEASE
thekid May 21, 2023
c32235c
Implement get-hooks to use by-reference semantics
thekid May 21, 2023
d96a487
Fix "Only variable references should be returned by reference"
thekid May 21, 2023
c1f1e61
Fix PHP 7.0 compatibility
thekid May 21, 2023
707500f
MFH
thekid Jun 6, 2023
5aa7780
Migrate to new reflection library
thekid Jun 6, 2023
9b022fe
MFH
thekid Jun 8, 2023
0cfa858
MFH
thekid Feb 24, 2024
8584d20
Remove support for `$field`
thekid Feb 24, 2024
5929c83
Implement short set in accordance with RFC
thekid Mar 16, 2024
2fbca6f
Use simplemost `set { ... }` form
thekid Mar 16, 2024
0b823d5
Use simplemost `set { ... }` form for block with exceptions
thekid Mar 16, 2024
19f5645
Merge from master
thekid Apr 17, 2024
d02f93f
Merge branch 'feature/property-hooks' of github.com:xp-framework/comp…
thekid Apr 17, 2024
39f4929
Remove abstract hooks, they are no longer included in the RFC
thekid May 18, 2024
df4a889
Fix "Type of parameter $times of hook T::$test::set must be compatibl…
thekid May 18, 2024
9a4186a
Emit property hooks natively if its PR has been merged
thekid May 18, 2024
53f0256
Change emitScope() to no longer include type in braces except for `new`
thekid May 18, 2024
7cd4039
Refactor chaining scope operators for PHP 7.x
thekid May 18, 2024
00e15f1
Add tests for reflective access to virtual properties
thekid May 18, 2024
e0b89f3
Simplify code
thekid Jun 15, 2024
e605a97
Use release version of AST library
thekid Jun 15, 2024
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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"require" : {
"xp-framework/core": "^12.0 | ^11.6 | ^10.16",
"xp-framework/reflection": "^3.0 | ^2.13",
"xp-framework/ast": "^11.0 | ^10.1",
"xp-framework/ast": "^11.1",
"php" : ">=7.4.0"
},
"require-dev" : {
Expand Down
32 changes: 32 additions & 0 deletions src/main/php/lang/ast/emit/ChainScopeOperators.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php namespace lang\ast\emit;

use lang\ast\Node;
use lang\ast\nodes\Literal;

/**
* Allows chaining of scope operators and rewrites `[expr]::class` to `get_class($object)`
* except if expression references a type - e.g. `self` or `ClassName`.
*
* @see https://wiki.php.net/rfc/class_name_literal_on_object
* @see https://wiki.php.net/rfc/variable_syntax_tweaks#constant_dereferencability
* @test lang.ast.unittest.emit.ChainScopeOperatorsTest
*/
trait ChainScopeOperators {
use RewriteDynamicClassConstants { emitScope as rewriteDynamicClassConstants; }

protected function emitScope($result, $scope) {
if (!($scope->type instanceof Node)) return $this->rewriteDynamicClassConstants($result, $scope);

if ($scope->member instanceof Literal && 'class' === $scope->member->expression) {
$result->out->write('\\get_class(');
$this->emitOne($result, $scope->type);
$result->out->write(')');
} else {
$t= $result->temp();
$result->out->write('(null==='.$t.'=');
$this->emitOne($result, $scope->type);
$result->out->write(")?null:{$t}::");
$this->emitOne($result, $scope->member);
}
}
}
16 changes: 8 additions & 8 deletions src/main/php/lang/ast/emit/PHP.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
Expression,
InstanceExpression,
Literal,
NewExpression,
Property,
ScopeExpression,
UnpackExpression,
Expand Down Expand Up @@ -462,7 +463,7 @@ protected function emitClass($result, $class) {
}
$result->out->write('];');

$result->out->write('public function __get($name) { switch ($name) {');
$result->out->write('public function &__get($name) { switch ($name) {');
foreach ($context->virtual as $name => $access) {
$result->out->write($name ? 'case "'.$name.'":' : 'default:');
$this->emitOne($result, $access[0]);
Expand Down Expand Up @@ -1084,15 +1085,14 @@ protected function emitInvoke($result, $invoke) {

protected function emitScope($result, $scope) {

// $x::<expr> vs. e.g. invoke()::<expr> vs. T::<expr>
if ($scope->type instanceof Variable) {
// new T()::<expr> vs. e.g. $x::<expr> vs. T::<expr>
if ($scope->type instanceof NewExpression) {
$result->out->write('(');
$this->emitOne($result, $scope->type);
$result->out->write('::');
$result->out->write(')::');
} else if ($scope->type instanceof Node) {
$t= $result->temp();
$result->out->write('(null==='.$t.'=');
$this->emitOne($result, $scope->type);
$result->out->write(")?null:{$t}::");
$result->out->write('::');
} else {
$result->out->write("{$scope->type}::");
}
Expand All @@ -1111,7 +1111,7 @@ protected function emitScope($result, $scope) {
}

protected function emitInstance($result, $instance) {
if ('new' === $instance->expression->kind) {
if ($instance->expression instanceof NewExpression) {
$result->out->write('(');
$this->emitOne($result, $instance->expression);
$result->out->write(')->');
Expand Down
4 changes: 2 additions & 2 deletions src/main/php/lang/ast/emit/PHP74.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ class PHP74 extends PHP {
ArrayUnpackUsingMerge,
AttributesAsComments,
CallablesAsClosures,
ChainScopeOperators,
MatchAsTernaries,
NonCapturingCatchVariables,
NullsafeAsTernaries,
OmitArgumentNames,
OmitConstantTypes,
ReadonlyClasses,
ReadonlyProperties,
RewriteBlockLambdaExpressions,
RewriteClassOnObjects,
RewriteEnums,
RewriteExplicitOctals,
RewriteProperties,
RewriteStaticVariableInitializations,
RewriteThrowableExpressions
;
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP80.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ class PHP80 extends PHP {
CallablesAsClosures,
OmitConstantTypes,
ReadonlyClasses,
ReadonlyProperties,
RewriteBlockLambdaExpressions,
RewriteDynamicClassConstants,
RewriteEnums,
RewriteExplicitOctals,
RewriteProperties,
RewriteStaticVariableInitializations
;

Expand Down
3 changes: 2 additions & 1 deletion src/main/php/lang/ast/emit/PHP81.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ class PHP81 extends PHP {
RewriteDynamicClassConstants,
RewriteStaticVariableInitializations,
ReadonlyClasses,
OmitConstantTypes
OmitConstantTypes,
PropertyHooks
;

/** Sets up type => literal mappings */
Expand Down
3 changes: 2 additions & 1 deletion src/main/php/lang/ast/emit/PHP82.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ class PHP82 extends PHP {
RewriteDynamicClassConstants,
RewriteStaticVariableInitializations,
ReadonlyClasses,
OmitConstantTypes
OmitConstantTypes,
PropertyHooks
;

/** Sets up type => literal mappings */
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/PHP83.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* @see https://wiki.php.net/rfc#php_83
*/
class PHP83 extends PHP {
use RewriteBlockLambdaExpressions, ReadonlyClasses;
use RewriteBlockLambdaExpressions, ReadonlyClasses, PropertyHooks;

/** Sets up type => literal mappings */
public function __construct() {
Expand Down
229 changes: 229 additions & 0 deletions src/main/php/lang/ast/emit/PropertyHooks.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
<?php namespace lang\ast\emit;

use ReflectionProperty;
use lang\ast\Code;
use lang\ast\nodes\{
Assignment,
Block,
InstanceExpression,
InvokeExpression,
Literal,
Method,
OffsetExpression,
Parameter,
ReturnStatement,
ScopeExpression,
Signature,
Variable
};

/**
* Property hooks
*
* @see https://wiki.php.net/rfc/property-hooks
* @test lang.ast.unittest.emit.PropertyHooksTest
*/
trait PropertyHooks {

protected function rewriteHook($node, $name, $virtual, $literal) {

// Magic constant referencing property name
if ($node instanceof Literal && '__PROPERTY__' === $node->expression) return $literal;

// Rewrite $this->propertyName to virtual property
if (
$node instanceof InstanceExpression &&
$node->expression instanceof Variable && 'this' === $node->expression->pointer &&
$node->member instanceof Literal && $name === $node->member->expression
) return $virtual;

// <T>::$field::hook() => <T>::__<hook>_<field>()
if (
$node instanceof ScopeExpression &&
$node->member instanceof InvokeExpression &&
$node->member->expression instanceof Literal &&
$node->type instanceof ScopeExpression &&
$node->type->member instanceof Variable &&
is_string($node->type->type) &&
is_string($node->type->member->pointer)
) {
return new ScopeExpression($node->type->type, new InvokeExpression(
new Literal('__'.$node->member->expression->expression.'_'.$node->type->member->pointer),
$node->member->arguments
));
}

foreach ($node->children() as &$child) {
$child= $this->rewriteHook($child, $name, $virtual, $literal);
}
return $node;
}

protected function withScopeCheck($modifiers, $nodes) {
if ($modifiers & MODIFIER_PRIVATE) {
$check= (
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'.
'throw new \\Error("Cannot access private property ".__CLASS__."::".$name);'
);
} else if ($modifiers & MODIFIER_PROTECTED) {
$check= (
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'.
'throw new \\Error("Cannot access protected property ".__CLASS__."::".$name);'
);
} else if (1 === sizeof($nodes)) {
return $nodes[0];
} else {
return new Block($nodes);
}

return new Block([new Code($check), ...$nodes]);
}

protected function emitEmulatedHooks($result, $property) {
static $lookup= [
'public' => MODIFIER_PUBLIC,
'protected' => MODIFIER_PROTECTED,
'private' => MODIFIER_PRIVATE,
'static' => MODIFIER_STATIC,
'final' => MODIFIER_FINAL,
'abstract' => MODIFIER_ABSTRACT,
'readonly' => 0x0080, // XP 10.13: MODIFIER_READONLY
];

// Emit XP meta information for the reflection API
$scope= $result->codegen->scope[0];
$modifiers= 0;
foreach ($property->modifiers as $name) {
$modifiers|= $lookup[$name];
}

$scope->meta[self::PROPERTY][$property->name]= [
DETAIL_RETURNS => $property->type ? $property->type->name() : 'var',
DETAIL_ANNOTATIONS => $property->annotations,
DETAIL_COMMENT => $property->comment,
DETAIL_TARGET_ANNO => [],
DETAIL_ARGUMENTS => ['interface' === $scope->type->kind ? $modifiers | MODIFIER_ABSTRACT : $modifiers]
];

$literal= new Literal("'{$property->name}'");
$virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(new Literal('__virtual'), $literal));

// Emit get and set hooks in-place. Ignore any unknown hooks
$get= $set= null;
foreach ($property->hooks as $type => $hook) {
$method= '__'.$type.'_'.$property->name;
$modifierList= $modifiers & MODIFIER_ABSTRACT ? ['abstract'] : $hook->modifiers;
if ('get' === $type) {
$this->emitOne($result, new Method(
$modifierList,
$method,
new Signature([], null, $hook->byref),
null === $hook->expression ? null : [$this->rewriteHook(
$hook->expression instanceof Block ? $hook->expression : new ReturnStatement($hook->expression),
$property->name,
$virtual,
$literal
)],
null // $hook->annotations
));
$get= $this->withScopeCheck($modifiers, [
new Assignment(new Variable('r'), $hook->byref ? '=&' : '=', new InvokeExpression(
new InstanceExpression(new Variable('this'), new Literal($method)),
[]
)),
new ReturnStatement(new Variable('r'))
]);
} else if ('set' === $type) {
$this->emitOne($result, new Method(
$modifierList,
$method,
new Signature($hook->parameter ? [$hook->parameter] : [new Parameter('value', null)], null),
null === $hook->expression ? null : [$this->rewriteHook(
$hook->expression instanceof Block ? $hook->expression : new Assignment($virtual, '=', $hook->expression),
$property->name,
$virtual,
$literal
)],
null // $hook->annotations
));
$set= $this->withScopeCheck($modifiers, [new InvokeExpression(
new InstanceExpression(new Variable('this'), new Literal($method)),
[new Variable('value')]
)]);
}
}

// Declare virtual properties with __set and __get as well as initializations
// except inside interfaces, which cannot contain properties.
if ('interface' === $scope->type->kind) return;

$scope->virtual[$property->name]= [
$get ?? new ReturnStatement($virtual),
$set ?? new Assignment($virtual, '=', new Variable('value'))
];
if (isset($property->expression)) {
$scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression;
}
}

protected function emitNativeHooks($result, $property) {
$result->codegen->scope[0]->meta[self::PROPERTY][$property->name]= [
DETAIL_RETURNS => $property->type ? $property->type->name() : 'var',
DETAIL_ANNOTATIONS => $property->annotations,
DETAIL_COMMENT => $property->comment,
DETAIL_TARGET_ANNO => [],
DETAIL_ARGUMENTS => []
];

$property->comment && $this->emitOne($result, $property->comment);
$property->annotations && $this->emitOne($result, $property->annotations);
$result->at($property->declared)->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name);
if (isset($property->expression)) {
if ($this->isConstant($result, $property->expression)) {
$result->out->write('=');
$this->emitOne($result, $property->expression);
} else if (in_array('static', $property->modifiers)) {
$result->codegen->scope[0]->statics['self::$'.$property->name]= $property->expression;
} else {
$result->codegen->scope[0]->init['$this->'.$property->name]= $property->expression;
}
}

// TODO move this to lang.ast.emit.PHP once https://github.com/php/php-src/pull/13455 is merged
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$result->out->write('{');
foreach ($property->hooks as $type => $hook) {
$hook->byref && $result->out->write('&');
$result->out->write($type);
if ($hook->parameter) {
$result->out->write('(');
$this->emitOne($result, $hook->parameter);
$result->out->write(')');
}

if (null === $hook->expression) {
$result->out->write(';');
} else if ($hook->expression instanceof Block) {
$this->emitOne($result, $hook->expression);
} else {
$result->out->write('=>');
$this->emitOne($result, $hook->expression);
$result->out->write(';');
}
}
$result->out->write('}');
}

protected function emitProperty($result, $property) {
static $hooks= null;

if (empty($property->hooks)) {
parent::emitProperty($result, $property);
} else if ($hooks ?? $hooks= method_exists(ReflectionProperty::class, 'getHooks')) {
$this->emitNativeHooks($result, $property);
} else {
$this->emitEmulatedHooks($result, $property);
}
}
}
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/emit/ReadonlyProperties.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ protected function emitProperty($result, $property) {

// Create virtual property implementing the readonly semantics
$scope->virtual[$property->name]= [
new Code(sprintf($check.'return $this->__virtual["%1$s"][0] ?? null;', $property->name)),
new Code(sprintf($check.'return $this->__virtual["%1$s"][0];', $property->name)),
new Code(sprintf(
($check ?: '$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;').
'if (isset($this->__virtual["%1$s"])) throw new \\Error("Cannot modify readonly property ".__CLASS__."::{$name}");'.
Expand Down
Loading
Loading