Skip to content

Commit fb3e921

Browse files
committed
Implement __set and __unset for JsObject
1 parent 0c9666d commit fb3e921

File tree

2 files changed

+230
-77
lines changed

2 files changed

+230
-77
lines changed

src/node_php_jsobject_class.cc

Lines changed: 108 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,26 @@ zend_class_entry *php_ce_jsobject;
2323
/* Object Handlers */
2424
static zend_object_handlers node_php_jsobject_handlers;
2525

26+
/* Helpful macros for common tasks in PHP magic methods. */
27+
28+
#define PARSE_PARAMS(method, ...) \
29+
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, __VA_ARGS__) == FAILURE) { \
30+
zend_throw_exception(zend_exception_get_default(TSRMLS_C), "bad args to " #method, 0 TSRMLS_CC); \
31+
return; \
32+
} \
33+
node_php_jsobject *obj = (node_php_jsobject *) \
34+
zend_object_store_get_object(this_ptr TSRMLS_CC)
35+
36+
#define THROW_IF_EXCEPTION(...) \
37+
do { if (msg.HasException()) { \
38+
zend_throw_exception_ex( \
39+
zend_exception_get_default(TSRMLS_C), 0 TSRMLS_CC, \
40+
__VA_ARGS__); \
41+
return; \
42+
} } while (0)
2643

2744
/* JsObject handlers */
45+
2846
class JsHasPropertyMsg : public MessageToJs {
2947
Value object_;
3048
Value member_;
@@ -87,26 +105,13 @@ class JsHasPropertyMsg : public MessageToJs {
87105

88106
PHP_METHOD(JsObject, __isset) {
89107
zval *member;
90-
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z/", &member) == FAILURE) {
91-
zend_throw_exception(
92-
zend_exception_get_default(TSRMLS_C),
93-
"bad property name for __isset", 0 TSRMLS_CC);
94-
return;
95-
}
108+
PARSE_PARAMS(__isset, "z/", &member);
96109
convert_to_string(member);
97-
node_php_jsobject *obj = (node_php_jsobject *)
98-
zend_object_store_get_object(this_ptr TSRMLS_CC);
99110
JsHasPropertyMsg msg(obj->channel, obj->id, member, 0);
100111
obj->channel->Send(&msg);
101112
msg.WaitForResponse();
102-
// ok, result is in msg.retval_ or msg.exception_
103-
if (msg.HasException()) {
104-
zend_throw_exception_ex(
105-
zend_exception_get_default(TSRMLS_C), 0 TSRMLS_CC,
106-
"JS exception thrown during __isset of \"%*s\"",
107-
Z_STRLEN_P(member), Z_STRVAL_P(member));
108-
return;
109-
}
113+
THROW_IF_EXCEPTION("JS exception thrown during __isset of \"%*s\"",
114+
Z_STRLEN_P(member), Z_STRVAL_P(member));
110115
RETURN_BOOL(msg.retval_.AsBool());
111116
}
112117

@@ -165,29 +170,88 @@ class JsReadPropertyMsg : public MessageToJs {
165170

166171
PHP_METHOD(JsObject, __get) {
167172
zval *member;
168-
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z/", &member) == FAILURE) {
169-
zend_throw_exception(zend_exception_get_default(TSRMLS_C), "bad property name", 0 TSRMLS_CC);
170-
return;
171-
}
173+
PARSE_PARAMS(__get, "z/", &member);
172174
convert_to_string(member);
173-
node_php_jsobject *obj = (node_php_jsobject *)
174-
zend_object_store_get_object(this_ptr TSRMLS_CC);
175175
JsReadPropertyMsg msg(obj->channel, obj->id, member, 0);
176176
obj->channel->Send(&msg);
177177
msg.WaitForResponse();
178-
// ok, result is in msg.retval_ or msg.exception_
179-
if (msg.HasException()) {
180-
zend_throw_exception_ex(
181-
zend_exception_get_default(TSRMLS_C), 0 TSRMLS_CC,
182-
"JS exception thrown during __get of \"%*s\"",
183-
Z_STRLEN_P(member), Z_STRVAL_P(member));
184-
return;
178+
THROW_IF_EXCEPTION("JS exception thrown during __get of \"%*s\"",
179+
Z_STRLEN_P(member), Z_STRVAL_P(member));
180+
msg.retval_.ToPhp(obj->channel, return_value, return_value_ptr TSRMLS_CC);
181+
}
182+
183+
class JsWritePropertyMsg : public MessageToJs {
184+
Value object_;
185+
Value member_;
186+
Value value_;
187+
public:
188+
JsWritePropertyMsg(ObjectMapper* m, objid_t objId, zval *member, zval *value)
189+
: MessageToJs(m), object_(), member_(m, member), value_(m, value) {
190+
object_.SetJsObject(objId);
191+
}
192+
protected:
193+
virtual void InJs(ObjectMapper *m) {
194+
v8::Local<v8::Object> jsObj = Nan::To<v8::Object>(object_.ToJs(m))
195+
.ToLocalChecked();
196+
v8::Local<v8::String> jsKey = Nan::To<v8::String>(member_.ToJs(m))
197+
.ToLocalChecked();
198+
v8::Local<v8::Value> jsVal = value_.ToJs(m);
199+
200+
if (Nan::Set(jsObj, jsKey, jsVal).FromMaybe(false)) {
201+
retval_.SetBool(true);
202+
}
203+
}
204+
};
205+
206+
PHP_METHOD(JsObject, __set) {
207+
zval *member; zval *value;
208+
PARSE_PARAMS(__set, "z/z", &member, &value);
209+
convert_to_string(member);
210+
JsWritePropertyMsg msg(obj->channel, obj->id, member, value);
211+
obj->channel->Send(&msg);
212+
msg.WaitForResponse();
213+
THROW_IF_EXCEPTION("JS exception thrown during __set of \"%*s\"",
214+
Z_STRLEN_P(member), Z_STRVAL_P(member));
215+
msg.retval_.ToPhp(obj->channel, return_value, return_value_ptr TSRMLS_CC);
216+
}
217+
218+
class JsDeletePropertyMsg : public MessageToJs {
219+
Value object_;
220+
Value member_;
221+
public:
222+
JsDeletePropertyMsg(ObjectMapper* m, objid_t objId, zval *member)
223+
: MessageToJs(m), object_(), member_(m, member) {
224+
object_.SetJsObject(objId);
225+
}
226+
protected:
227+
virtual void InJs(ObjectMapper *m) {
228+
v8::Local<v8::Object> jsObj = Nan::To<v8::Object>(object_.ToJs(m))
229+
.ToLocalChecked();
230+
v8::Local<v8::String> jsKey = Nan::To<v8::String>(member_.ToJs(m))
231+
.ToLocalChecked();
232+
233+
if (Nan::Delete(jsObj, jsKey).FromMaybe(false)) {
234+
retval_.SetBool(true);
235+
}
185236
}
237+
};
238+
239+
PHP_METHOD(JsObject, __unset) {
240+
zval *member;
241+
PARSE_PARAMS(__unset, "z/", &member);
242+
convert_to_string(member);
243+
JsDeletePropertyMsg msg(obj->channel, obj->id, member);
244+
obj->channel->Send(&msg);
245+
msg.WaitForResponse();
246+
THROW_IF_EXCEPTION("JS exception thrown during __unset of \"%*s\"",
247+
Z_STRLEN_P(member), Z_STRVAL_P(member));
186248
msg.retval_.ToPhp(obj->channel, return_value, return_value_ptr TSRMLS_CC);
187249
}
188250

251+
189252
/* Use (slightly thunked) versions of the has/read/write property handlers
190253
* for dimensions as well, so that $obj['foo'] acts like $obj->foo. */
254+
191255
static int node_php_jsobject_has_dimension(zval *obj, zval *idx, int chk_type TSRMLS_DC) {
192256
// thunk!
193257
if (chk_type == 0) { chk_type = 2; }
@@ -285,10 +349,19 @@ STUB_METHOD(__construct)
285349
STUB_METHOD(__sleep)
286350
STUB_METHOD(__wakeup)
287351

288-
ZEND_BEGIN_ARG_INFO_EX(node_php_jsobject_one_arg, 0, 0, 1)
352+
#if USE_MAGIC_ISSET
353+
ZEND_BEGIN_ARG_INFO_EX(node_php_jsobject_isset_args, 0, 0, 1)
354+
ZEND_ARG_INFO(0, member)
355+
ZEND_END_ARG_INFO()
356+
#endif
357+
ZEND_BEGIN_ARG_INFO_EX(node_php_jsobject_get_args, 0, 1/*return by ref*/, 1)
358+
ZEND_ARG_INFO(0, member)
359+
ZEND_END_ARG_INFO()
360+
ZEND_BEGIN_ARG_INFO_EX(node_php_jsobject_set_args, 0, 0, 2)
289361
ZEND_ARG_INFO(0, member)
362+
ZEND_ARG_INFO(0, value)
290363
ZEND_END_ARG_INFO()
291-
ZEND_BEGIN_ARG_INFO_EX(node_php_jsobject_one_arg_retref, 0, 1, 1)
364+
ZEND_BEGIN_ARG_INFO_EX(node_php_jsobject_unset_args, 0, 0, 1)
292365
ZEND_ARG_INFO(0, member)
293366
ZEND_END_ARG_INFO()
294367

@@ -297,9 +370,11 @@ static const zend_function_entry node_php_jsobject_methods[] = {
297370
PHP_ME(JsObject, __sleep, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
298371
PHP_ME(JsObject, __wakeup, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
299372
#if USE_MAGIC_ISSET
300-
PHP_ME(JsObject, __isset, node_php_jsobject_one_arg, ZEND_ACC_PUBLIC)
373+
PHP_ME(JsObject, __isset, node_php_jsobject_isset_args, ZEND_ACC_PUBLIC)
301374
#endif
302-
PHP_ME(JsObject, __get, node_php_jsobject_one_arg_retref, ZEND_ACC_PUBLIC)
375+
PHP_ME(JsObject, __get, node_php_jsobject_get_args, ZEND_ACC_PUBLIC)
376+
PHP_ME(JsObject, __set, node_php_jsobject_set_args, ZEND_ACC_PUBLIC)
377+
PHP_ME(JsObject, __unset, node_php_jsobject_unset_args, ZEND_ACC_PUBLIC)
303378
ZEND_FE_END
304379
};
305380

test/context.js

Lines changed: 122 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -66,49 +66,50 @@ describe('Passing context object from JS to PHP', function() {
6666
f: '1'
6767
}
6868
}).then(function(v) {
69-
out.toString().should.equal(
70-
'->a: int(0)\n' +
71-
'[\'a\']: int(0)\n' +
72-
'isset: bool(true)\n' +
73-
'empty: bool(true)\n' +
74-
'exists: bool(true)\n' +
75-
'\n' +
76-
'->b: int(42)\n' +
77-
'[\'b\']: int(42)\n' +
78-
'isset: bool(true)\n' +
79-
'empty: bool(false)\n' +
80-
'exists: bool(true)\n' +
81-
'\n' +
82-
'->c: NULL\n' +
83-
'[\'c\']: NULL\n' +
84-
'isset: bool(false)\n' +
85-
'empty: bool(true)\n' +
86-
'exists: bool(true)\n' +
87-
'\n' +
88-
'->d: NULL\n' +
89-
'[\'d\']: NULL\n' +
90-
'isset: bool(false)\n' +
91-
'empty: bool(true)\n' +
92-
'exists: bool(true)\n' +
93-
'\n' +
94-
'->e: string(1) "0"\n' +
95-
'[\'e\']: string(1) "0"\n' +
96-
'isset: bool(true)\n' +
97-
'empty: bool(true)\n' +
98-
'exists: bool(true)\n' +
99-
'\n' +
100-
'->f: string(1) "1"\n' +
101-
'[\'f\']: string(1) "1"\n' +
102-
'isset: bool(true)\n' +
103-
'empty: bool(false)\n' +
104-
'exists: bool(true)\n' +
105-
'\n' +
106-
'->g: NULL\n' +
107-
'[\'g\']: NULL\n' +
108-
'isset: bool(false)\n' +
109-
'empty: bool(true)\n' +
110-
'exists: bool(false)\n' +
111-
'\n'
69+
out.toString().should.equal([
70+
'->a: int(0)',
71+
'[\'a\']: int(0)',
72+
'isset: bool(true)',
73+
'empty: bool(true)',
74+
'exists: bool(true)',
75+
'',
76+
'->b: int(42)',
77+
'[\'b\']: int(42)',
78+
'isset: bool(true)',
79+
'empty: bool(false)',
80+
'exists: bool(true)',
81+
'',
82+
'->c: NULL',
83+
'[\'c\']: NULL',
84+
'isset: bool(false)',
85+
'empty: bool(true)',
86+
'exists: bool(true)',
87+
'',
88+
'->d: NULL',
89+
'[\'d\']: NULL',
90+
'isset: bool(false)',
91+
'empty: bool(true)',
92+
'exists: bool(true)',
93+
'',
94+
'->e: string(1) "0"',
95+
'[\'e\']: string(1) "0"',
96+
'isset: bool(true)',
97+
'empty: bool(true)',
98+
'exists: bool(true)',
99+
'',
100+
'->f: string(1) "1"',
101+
'[\'f\']: string(1) "1"',
102+
'isset: bool(true)',
103+
'empty: bool(false)',
104+
'exists: bool(true)',
105+
'',
106+
'->g: NULL',
107+
'[\'g\']: NULL',
108+
'isset: bool(false)',
109+
'empty: bool(true)',
110+
'exists: bool(false)',
111+
'',
112+
''].join('\n')
112113
);
113114
});
114115
});
@@ -119,7 +120,84 @@ describe('Passing context object from JS to PHP', function() {
119120
throw new Error('boo');
120121
} });
121122
return php.request({
122-
source: "call_user_func(function () { try { var_dump($_SERVER['CONTEXT']->a); } catch (Exception $e) { echo 'exception caught'; } })",
123+
source: [
124+
"call_user_func(function () {",
125+
" try {",
126+
" var_dump($_SERVER['CONTEXT']->a);",
127+
" } catch (Exception $e) {",
128+
" echo 'exception caught';",
129+
" }",
130+
"})"].join('\n'),
131+
stream: out,
132+
context: context
133+
}).then(function() {
134+
out.toString().should.equal('exception caught');
135+
});
136+
});
137+
it('should implement __set and __unset', function() {
138+
var out = new StringStream();
139+
var context = { a: 42 };
140+
Object.defineProperty(context, 'b', {
141+
get: function() { return 13; },
142+
set: function(v) { this.a = v; }
143+
});
144+
return php.request({
145+
source: [
146+
"call_user_func(function () {",
147+
" $c = $_SERVER['CONTEXT'];",
148+
" echo 'a is '; var_dump($c->a);",
149+
" echo 'b is '; var_dump($c->b);",
150+
" $c->a = 1;",
151+
" echo 'a is '; var_dump($c->a);",
152+
" echo 'b is '; var_dump($c->b);",
153+
" $c->b = 2;",
154+
" echo 'a is '; var_dump($c->a);",
155+
" echo 'b is '; var_dump($c->b);",
156+
" $c['b'] = 3;",
157+
" echo 'a is '; var_dump($c->a);",
158+
" echo 'b is '; var_dump($c->b);",
159+
" unset($c->a);",
160+
" echo 'a is '; var_dump($c->a);",
161+
" echo 'exists? '; var_dump(property_exists($c, 'a'));",
162+
" try {",
163+
" unset($c->b);",
164+
" } catch (Exception $e) {",
165+
" echo 'exception caught';",
166+
" }",
167+
"})"].join('\n'),
168+
stream: out,
169+
context: context
170+
}).then(function() {
171+
out.toString().should.equal([
172+
'a is int(42)',
173+
'b is int(13)',
174+
'a is int(1)',
175+
'b is int(13)',
176+
'a is int(2)',
177+
'b is int(13)',
178+
'a is int(3)',
179+
'b is int(13)',
180+
'a is NULL',
181+
'exists? bool(false)',
182+
'exception caught'
183+
].join('\n'));
184+
});
185+
});
186+
it('should handle exceptions in setters', function() {
187+
var out = new StringStream();
188+
var context = {};
189+
Object.defineProperty(context, 'a', { set: function() {
190+
throw new Error('boo');
191+
} });
192+
return php.request({
193+
source: [
194+
"call_user_func(function () {",
195+
" try {",
196+
" $_SERVER['CONTEXT']->a = 42;",
197+
" } catch (Exception $e) {",
198+
" echo 'exception caught';",
199+
" }",
200+
"})"].join('\n'),
123201
stream: out,
124202
context: context
125203
}).then(function() {

0 commit comments

Comments
 (0)