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
+ }
0 commit comments