Skip to content

Commit 9b67405

Browse files
authored
Merge pull request #166 from xp-framework/feature/property-hooks
Implement property hooks via virtual properties
2 parents b74ec9a + e605a97 commit 9b67405

14 files changed

+659
-42
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"require" : {
99
"xp-framework/core": "^12.0 | ^11.6 | ^10.16",
1010
"xp-framework/reflection": "^3.0 | ^2.13",
11-
"xp-framework/ast": "^11.0 | ^10.1",
11+
"xp-framework/ast": "^11.1",
1212
"php" : ">=7.4.0"
1313
},
1414
"require-dev" : {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php namespace lang\ast\emit;
2+
3+
use lang\ast\Node;
4+
use lang\ast\nodes\Literal;
5+
6+
/**
7+
* Allows chaining of scope operators and rewrites `[expr]::class` to `get_class($object)`
8+
* except if expression references a type - e.g. `self` or `ClassName`.
9+
*
10+
* @see https://wiki.php.net/rfc/class_name_literal_on_object
11+
* @see https://wiki.php.net/rfc/variable_syntax_tweaks#constant_dereferencability
12+
* @test lang.ast.unittest.emit.ChainScopeOperatorsTest
13+
*/
14+
trait ChainScopeOperators {
15+
use RewriteDynamicClassConstants { emitScope as rewriteDynamicClassConstants; }
16+
17+
protected function emitScope($result, $scope) {
18+
if (!($scope->type instanceof Node)) return $this->rewriteDynamicClassConstants($result, $scope);
19+
20+
if ($scope->member instanceof Literal && 'class' === $scope->member->expression) {
21+
$result->out->write('\\get_class(');
22+
$this->emitOne($result, $scope->type);
23+
$result->out->write(')');
24+
} else {
25+
$t= $result->temp();
26+
$result->out->write('(null==='.$t.'=');
27+
$this->emitOne($result, $scope->type);
28+
$result->out->write(")?null:{$t}::");
29+
$this->emitOne($result, $scope->member);
30+
}
31+
}
32+
}

src/main/php/lang/ast/emit/PHP.class.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
Expression,
1212
InstanceExpression,
1313
Literal,
14+
NewExpression,
1415
Property,
1516
ScopeExpression,
1617
UnpackExpression,
@@ -462,7 +463,7 @@ protected function emitClass($result, $class) {
462463
}
463464
$result->out->write('];');
464465

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

10851086
protected function emitScope($result, $scope) {
10861087

1087-
// $x::<expr> vs. e.g. invoke()::<expr> vs. T::<expr>
1088-
if ($scope->type instanceof Variable) {
1088+
// new T()::<expr> vs. e.g. $x::<expr> vs. T::<expr>
1089+
if ($scope->type instanceof NewExpression) {
1090+
$result->out->write('(');
10891091
$this->emitOne($result, $scope->type);
1090-
$result->out->write('::');
1092+
$result->out->write(')::');
10911093
} else if ($scope->type instanceof Node) {
1092-
$t= $result->temp();
1093-
$result->out->write('(null==='.$t.'=');
10941094
$this->emitOne($result, $scope->type);
1095-
$result->out->write(")?null:{$t}::");
1095+
$result->out->write('::');
10961096
} else {
10971097
$result->out->write("{$scope->type}::");
10981098
}
@@ -1111,7 +1111,7 @@ protected function emitScope($result, $scope) {
11111111
}
11121112

11131113
protected function emitInstance($result, $instance) {
1114-
if ('new' === $instance->expression->kind) {
1114+
if ($instance->expression instanceof NewExpression) {
11151115
$result->out->write('(');
11161116
$this->emitOne($result, $instance->expression);
11171117
$result->out->write(')->');

src/main/php/lang/ast/emit/PHP74.class.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ class PHP74 extends PHP {
1313
ArrayUnpackUsingMerge,
1414
AttributesAsComments,
1515
CallablesAsClosures,
16+
ChainScopeOperators,
1617
MatchAsTernaries,
1718
NonCapturingCatchVariables,
1819
NullsafeAsTernaries,
1920
OmitArgumentNames,
2021
OmitConstantTypes,
2122
ReadonlyClasses,
22-
ReadonlyProperties,
2323
RewriteBlockLambdaExpressions,
24-
RewriteClassOnObjects,
2524
RewriteEnums,
2625
RewriteExplicitOctals,
26+
RewriteProperties,
2727
RewriteStaticVariableInitializations,
2828
RewriteThrowableExpressions
2929
;

src/main/php/lang/ast/emit/PHP80.class.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ class PHP80 extends PHP {
2323
CallablesAsClosures,
2424
OmitConstantTypes,
2525
ReadonlyClasses,
26-
ReadonlyProperties,
2726
RewriteBlockLambdaExpressions,
2827
RewriteDynamicClassConstants,
2928
RewriteEnums,
3029
RewriteExplicitOctals,
30+
RewriteProperties,
3131
RewriteStaticVariableInitializations
3232
;
3333

src/main/php/lang/ast/emit/PHP81.class.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ class PHP81 extends PHP {
2424
RewriteDynamicClassConstants,
2525
RewriteStaticVariableInitializations,
2626
ReadonlyClasses,
27-
OmitConstantTypes
27+
OmitConstantTypes,
28+
PropertyHooks
2829
;
2930

3031
/** Sets up type => literal mappings */

src/main/php/lang/ast/emit/PHP82.class.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ class PHP82 extends PHP {
2424
RewriteDynamicClassConstants,
2525
RewriteStaticVariableInitializations,
2626
ReadonlyClasses,
27-
OmitConstantTypes
27+
OmitConstantTypes,
28+
PropertyHooks
2829
;
2930

3031
/** Sets up type => literal mappings */

src/main/php/lang/ast/emit/PHP83.class.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* @see https://wiki.php.net/rfc#php_83
1919
*/
2020
class PHP83 extends PHP {
21-
use RewriteBlockLambdaExpressions, ReadonlyClasses;
21+
use RewriteBlockLambdaExpressions, ReadonlyClasses, PropertyHooks;
2222

2323
/** Sets up type => literal mappings */
2424
public function __construct() {
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
<?php namespace lang\ast\emit;
2+
3+
use ReflectionProperty;
4+
use lang\ast\Code;
5+
use lang\ast\nodes\{
6+
Assignment,
7+
Block,
8+
InstanceExpression,
9+
InvokeExpression,
10+
Literal,
11+
Method,
12+
OffsetExpression,
13+
Parameter,
14+
ReturnStatement,
15+
ScopeExpression,
16+
Signature,
17+
Variable
18+
};
19+
20+
/**
21+
* Property hooks
22+
*
23+
* @see https://wiki.php.net/rfc/property-hooks
24+
* @test lang.ast.unittest.emit.PropertyHooksTest
25+
*/
26+
trait PropertyHooks {
27+
28+
protected function rewriteHook($node, $name, $virtual, $literal) {
29+
30+
// Magic constant referencing property name
31+
if ($node instanceof Literal && '__PROPERTY__' === $node->expression) return $literal;
32+
33+
// Rewrite $this->propertyName to virtual property
34+
if (
35+
$node instanceof InstanceExpression &&
36+
$node->expression instanceof Variable && 'this' === $node->expression->pointer &&
37+
$node->member instanceof Literal && $name === $node->member->expression
38+
) return $virtual;
39+
40+
// <T>::$field::hook() => <T>::__<hook>_<field>()
41+
if (
42+
$node instanceof ScopeExpression &&
43+
$node->member instanceof InvokeExpression &&
44+
$node->member->expression instanceof Literal &&
45+
$node->type instanceof ScopeExpression &&
46+
$node->type->member instanceof Variable &&
47+
is_string($node->type->type) &&
48+
is_string($node->type->member->pointer)
49+
) {
50+
return new ScopeExpression($node->type->type, new InvokeExpression(
51+
new Literal('__'.$node->member->expression->expression.'_'.$node->type->member->pointer),
52+
$node->member->arguments
53+
));
54+
}
55+
56+
foreach ($node->children() as &$child) {
57+
$child= $this->rewriteHook($child, $name, $virtual, $literal);
58+
}
59+
return $node;
60+
}
61+
62+
protected function withScopeCheck($modifiers, $nodes) {
63+
if ($modifiers & MODIFIER_PRIVATE) {
64+
$check= (
65+
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
66+
'if (__CLASS__ !== $scope && \\lang\\VirtualProperty::class !== $scope)'.
67+
'throw new \\Error("Cannot access private property ".__CLASS__."::".$name);'
68+
);
69+
} else if ($modifiers & MODIFIER_PROTECTED) {
70+
$check= (
71+
'$scope= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]["class"] ?? null;'.
72+
'if (__CLASS__ !== $scope && !is_subclass_of($scope, __CLASS__) && \\lang\\VirtualProperty::class !== $scope)'.
73+
'throw new \\Error("Cannot access protected property ".__CLASS__."::".$name);'
74+
);
75+
} else if (1 === sizeof($nodes)) {
76+
return $nodes[0];
77+
} else {
78+
return new Block($nodes);
79+
}
80+
81+
return new Block([new Code($check), ...$nodes]);
82+
}
83+
84+
protected function emitEmulatedHooks($result, $property) {
85+
static $lookup= [
86+
'public' => MODIFIER_PUBLIC,
87+
'protected' => MODIFIER_PROTECTED,
88+
'private' => MODIFIER_PRIVATE,
89+
'static' => MODIFIER_STATIC,
90+
'final' => MODIFIER_FINAL,
91+
'abstract' => MODIFIER_ABSTRACT,
92+
'readonly' => 0x0080, // XP 10.13: MODIFIER_READONLY
93+
];
94+
95+
// Emit XP meta information for the reflection API
96+
$scope= $result->codegen->scope[0];
97+
$modifiers= 0;
98+
foreach ($property->modifiers as $name) {
99+
$modifiers|= $lookup[$name];
100+
}
101+
102+
$scope->meta[self::PROPERTY][$property->name]= [
103+
DETAIL_RETURNS => $property->type ? $property->type->name() : 'var',
104+
DETAIL_ANNOTATIONS => $property->annotations,
105+
DETAIL_COMMENT => $property->comment,
106+
DETAIL_TARGET_ANNO => [],
107+
DETAIL_ARGUMENTS => ['interface' === $scope->type->kind ? $modifiers | MODIFIER_ABSTRACT : $modifiers]
108+
];
109+
110+
$literal= new Literal("'{$property->name}'");
111+
$virtual= new InstanceExpression(new Variable('this'), new OffsetExpression(new Literal('__virtual'), $literal));
112+
113+
// Emit get and set hooks in-place. Ignore any unknown hooks
114+
$get= $set= null;
115+
foreach ($property->hooks as $type => $hook) {
116+
$method= '__'.$type.'_'.$property->name;
117+
$modifierList= $modifiers & MODIFIER_ABSTRACT ? ['abstract'] : $hook->modifiers;
118+
if ('get' === $type) {
119+
$this->emitOne($result, new Method(
120+
$modifierList,
121+
$method,
122+
new Signature([], null, $hook->byref),
123+
null === $hook->expression ? null : [$this->rewriteHook(
124+
$hook->expression instanceof Block ? $hook->expression : new ReturnStatement($hook->expression),
125+
$property->name,
126+
$virtual,
127+
$literal
128+
)],
129+
null // $hook->annotations
130+
));
131+
$get= $this->withScopeCheck($modifiers, [
132+
new Assignment(new Variable('r'), $hook->byref ? '=&' : '=', new InvokeExpression(
133+
new InstanceExpression(new Variable('this'), new Literal($method)),
134+
[]
135+
)),
136+
new ReturnStatement(new Variable('r'))
137+
]);
138+
} else if ('set' === $type) {
139+
$this->emitOne($result, new Method(
140+
$modifierList,
141+
$method,
142+
new Signature($hook->parameter ? [$hook->parameter] : [new Parameter('value', null)], null),
143+
null === $hook->expression ? null : [$this->rewriteHook(
144+
$hook->expression instanceof Block ? $hook->expression : new Assignment($virtual, '=', $hook->expression),
145+
$property->name,
146+
$virtual,
147+
$literal
148+
)],
149+
null // $hook->annotations
150+
));
151+
$set= $this->withScopeCheck($modifiers, [new InvokeExpression(
152+
new InstanceExpression(new Variable('this'), new Literal($method)),
153+
[new Variable('value')]
154+
)]);
155+
}
156+
}
157+
158+
// Declare virtual properties with __set and __get as well as initializations
159+
// except inside interfaces, which cannot contain properties.
160+
if ('interface' === $scope->type->kind) return;
161+
162+
$scope->virtual[$property->name]= [
163+
$get ?? new ReturnStatement($virtual),
164+
$set ?? new Assignment($virtual, '=', new Variable('value'))
165+
];
166+
if (isset($property->expression)) {
167+
$scope->init[sprintf('$this->__virtual["%s"]', $property->name)]= $property->expression;
168+
}
169+
}
170+
171+
protected function emitNativeHooks($result, $property) {
172+
$result->codegen->scope[0]->meta[self::PROPERTY][$property->name]= [
173+
DETAIL_RETURNS => $property->type ? $property->type->name() : 'var',
174+
DETAIL_ANNOTATIONS => $property->annotations,
175+
DETAIL_COMMENT => $property->comment,
176+
DETAIL_TARGET_ANNO => [],
177+
DETAIL_ARGUMENTS => []
178+
];
179+
180+
$property->comment && $this->emitOne($result, $property->comment);
181+
$property->annotations && $this->emitOne($result, $property->annotations);
182+
$result->at($property->declared)->out->write(implode(' ', $property->modifiers).' '.$this->propertyType($property->type).' $'.$property->name);
183+
if (isset($property->expression)) {
184+
if ($this->isConstant($result, $property->expression)) {
185+
$result->out->write('=');
186+
$this->emitOne($result, $property->expression);
187+
} else if (in_array('static', $property->modifiers)) {
188+
$result->codegen->scope[0]->statics['self::$'.$property->name]= $property->expression;
189+
} else {
190+
$result->codegen->scope[0]->init['$this->'.$property->name]= $property->expression;
191+
}
192+
}
193+
194+
// TODO move this to lang.ast.emit.PHP once https://github.com/php/php-src/pull/13455 is merged
195+
$result->out->write('{');
196+
foreach ($property->hooks as $type => $hook) {
197+
$hook->byref && $result->out->write('&');
198+
$result->out->write($type);
199+
if ($hook->parameter) {
200+
$result->out->write('(');
201+
$this->emitOne($result, $hook->parameter);
202+
$result->out->write(')');
203+
}
204+
205+
if (null === $hook->expression) {
206+
$result->out->write(';');
207+
} else if ($hook->expression instanceof Block) {
208+
$this->emitOne($result, $hook->expression);
209+
} else {
210+
$result->out->write('=>');
211+
$this->emitOne($result, $hook->expression);
212+
$result->out->write(';');
213+
}
214+
}
215+
$result->out->write('}');
216+
}
217+
218+
protected function emitProperty($result, $property) {
219+
static $hooks= null;
220+
221+
if (empty($property->hooks)) {
222+
parent::emitProperty($result, $property);
223+
} else if ($hooks ?? $hooks= method_exists(ReflectionProperty::class, 'getHooks')) {
224+
$this->emitNativeHooks($result, $property);
225+
} else {
226+
$this->emitEmulatedHooks($result, $property);
227+
}
228+
}
229+
}

src/main/php/lang/ast/emit/ReadonlyProperties.class.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ protected function emitProperty($result, $property) {
5555

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

0 commit comments

Comments
 (0)