Skip to content

Commit ff075a0

Browse files
committed
Make ArrayAccess objects appear as arrays when accessed in JavaScript.
1 parent af804b5 commit ff075a0

File tree

3 files changed

+732
-545
lines changed

3 files changed

+732
-545
lines changed

src/node_php_phpobject_class.cc

Lines changed: 195 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,32 @@ NAN_INDEX_ENUMERATOR(PhpObject::IndexEnumerate) {
198198
TRACE("<");
199199
}
200200

201+
// Helper function, called from PHP only
202+
static bool IsArrayAccess(zval *z TSRMLS_DC) {
203+
if (Z_TYPE_P(z) != IS_OBJECT) {
204+
TRACE("false (not object)");
205+
return false;
206+
}
207+
zend_class_entry *ce = Z_OBJCE_P(z);
208+
bool has_array_access = false;
209+
bool has_countable = false;
210+
for (zend_uint i = 0; i < ce->num_interfaces; i++) {
211+
if (strcmp(ce->interfaces[i]->name, "ArrayAccess") == 0) {
212+
has_array_access = true;
213+
}
214+
if (strcmp(ce->interfaces[i]->name, "Countable") == 0) {
215+
has_countable = true;
216+
}
217+
if (has_array_access && has_countable) {
218+
// Bail early from loop, don't need to look further.
219+
TRACE("true");
220+
return true;
221+
}
222+
}
223+
TRACE("false");
224+
return false;
225+
}
226+
201227
class PhpObject::PhpEnumerateMsg : public MessageToPhp {
202228
public:
203229
PhpEnumerateMsg(ObjectMapper *m, Nan::Callback *callback, bool is_sync,
@@ -211,8 +237,10 @@ class PhpObject::PhpEnumerateMsg : public MessageToPhp {
211237

212238
obj_.ToPhp(m, obj TSRMLS_CC);
213239
assert(obj.IsObject() || obj.IsArray());
214-
if (obj.IsArray()) {
215-
return ArrayEnum(m, op_, obj, &retval_, &exception_ TSRMLS_CC);
240+
bool is_array_access = IsArrayAccess(obj.Ptr() TSRMLS_CC);
241+
if (obj.IsArray() || is_array_access) {
242+
return ArrayEnum(m, op_, obj, is_array_access, &retval_, &exception_
243+
TSRMLS_CC);
216244
}
217245
// XXX unimplemented
218246
retval_.SetArrayByValue(0, [](uint32_t idx, Value& v) { });
@@ -271,8 +299,9 @@ class PhpObject::PhpPropertyMsg : public MessageToPhp {
271299
}
272300
// Arrays are handled in a separate method (but the ZVals here will
273301
// handle the memory management for us).
274-
if (obj.IsArray()) {
275-
return ArrayInPhp(m, obj, zname, value TSRMLS_CC);
302+
bool is_array_access = IsArrayAccess(obj.Ptr() TSRMLS_CC);
303+
if (obj.IsArray() || is_array_access) {
304+
return ArrayInPhp(m, obj, is_array_access, zname, value TSRMLS_CC);
276305
}
277306
const char *cname = Z_STRVAL_P(*zname);
278307
uint cname_len = Z_STRLEN_P(*zname);
@@ -481,43 +510,21 @@ class PhpObject::PhpPropertyMsg : public MessageToPhp {
481510
}
482511
}
483512
}
484-
void ArrayInPhp(PhpObjectMapper *m, const ZVal &arr, const ZVal &name,
485-
const ZVal &value TSRMLS_DC) {
486-
assert(arr.IsArray());
487-
HashTable *arrht = Z_ARRVAL_P(arr.Ptr());
513+
void ArrayInPhp(PhpObjectMapper *m, const ZVal &arr, bool is_array_access,
514+
const ZVal &name, const ZVal &value TSRMLS_DC) {
515+
assert(is_array_access ? arr.IsObject() : arr.IsArray());
488516
const char *cname = Z_STRVAL_P(name.Ptr());
489517
uint cname_len = Z_STRLEN_P(name.Ptr());
490518
// Length is special
491519
if (cname_len == 6 && 0 == strcmp(cname, "length")) {
492520
if (op_ == PropertyOp::QUERY) {
493521
// Length is not enumerable and not configurable, but *is* writable.
494522
retval_.SetInt(v8::DontEnum|v8::DontDelete);
495-
} else if (op_ == PropertyOp::GETTER) {
523+
} else if (op_ == PropertyOp::GETTER || op_ == PropertyOp::SETTER) {
496524
// Length property is "maximum index in hash", not "number of items"
497-
retval_.SetInt(zend_hash_next_free_element(arrht));
498-
} else if (op_ == PropertyOp::SETTER) {
499-
if (!value.IsLong()) {
500-
// convert to int
501-
const_cast<ZVal&>(value).Separate();
502-
convert_to_long(value.Ptr());
503-
}
504-
if (value.IsLong() && Z_LVAL_P(value.Ptr()) >= 0) {
505-
ulong nlen = Z_LVAL_P(value.Ptr());
506-
ulong olen = zend_hash_next_free_element(arrht);
507-
if (nlen < olen) {
508-
// We have to iterate here, rather unfortunate.
509-
// XXX We could look at zend_hash_num_elements() and
510-
// iterate over the elements if num_elements < (olen - nlen)
511-
for (ulong i = nlen; i < olen; i++) {
512-
zend_hash_index_del(arrht, i);
513-
}
514-
}
515-
// This is quite dodgy, since we're going to write the
516-
// nNextFreeElement field directly. Perhaps not portable!
517-
arrht->nNextFreeElement = nlen;
518-
}
519-
retval_.Set(m, value TSRMLS_CC);
520-
retval_.TakeOwnership();
525+
return PhpObject::ArraySize(m, op_, EnumOp::ONLY_INDEX,
526+
arr, is_array_access, value,
527+
&retval_, &exception_ TSRMLS_CC);
521528
} else if (op_ == PropertyOp::DELETER) {
522529
// Can't delete the length property.
523530
retval_.SetBool(false); // "property found here, but not deletable"
@@ -528,7 +535,7 @@ class PhpObject::PhpPropertyMsg : public MessageToPhp {
528535
}
529536
// All-numeric keys are special
530537
if (is_index_) {
531-
return PhpObject::ArrayOp(m, op_, arr, name, value,
538+
return PhpObject::ArrayOp(m, op_, arr, is_array_access, name, value,
532539
&retval_, &exception_ TSRMLS_CC);
533540
}
534541
// Special Map-like methods
@@ -669,8 +676,10 @@ class PhpObject::PhpInvokeMsg : public MessageToPhp {
669676
assert(method.IsString());
670677
// Arrays are handled in a separate method (but the ZVals here will
671678
// handle the memory management for us).
672-
if (obj.IsArray()) {
673-
return ArrayInPhp(m, obj, method, args.size(), args.data() TSRMLS_CC);
679+
bool is_array_access = IsArrayAccess(obj.Ptr() TSRMLS_CC);
680+
if (obj.IsArray() || is_array_access) {
681+
return ArrayInPhp(m, obj, is_array_access,
682+
method, args.size(), args.data() TSRMLS_CC);
674683
}
675684
ZVal retval{ZEND_FILE_LINE_C};
676685
// If the method name is __call, then shift the new method name off
@@ -701,9 +710,10 @@ class PhpObject::PhpInvokeMsg : public MessageToPhp {
701710
retval_.TakeOwnership(); // This will outlive scope of `retval`
702711
}
703712
}
704-
void ArrayInPhp(PhpObjectMapper *m, const ZVal &arr, const ZVal &name,
705-
int argc, ZVal* argv TSRMLS_DC) {
706-
assert(arr.IsArray() && name.IsString());
713+
void ArrayInPhp(PhpObjectMapper *m, const ZVal &arr, bool is_array_access,
714+
const ZVal &name, int argc, ZVal* argv TSRMLS_DC) {
715+
assert(is_array_access ? arr.IsObject() : arr.IsArray());
716+
assert(name.IsString());
707717
#define THROW_IF_BAD_ARGS(meth, n) \
708718
if (argc < n) { \
709719
zend_throw_exception_ex( \
@@ -716,7 +726,6 @@ class PhpObject::PhpInvokeMsg : public MessageToPhp {
716726
convert_to_string(argv[0].Ptr()); \
717727
} \
718728
assert(argv[0].IsString())
719-
HashTable *arrht = Z_ARRVAL_P(arr.Ptr());
720729
const char *cname = Z_STRVAL_P(name.Ptr());
721730
uint cname_len = Z_STRLEN_P(name.Ptr());
722731
// Special Map-like methods
@@ -725,14 +734,15 @@ class PhpObject::PhpInvokeMsg : public MessageToPhp {
725734
if (0 == strcmp(cname, "get")) {
726735
THROW_IF_BAD_ARGS("get", 1);
727736
ZVal ignore{ZEND_FILE_LINE_C};
728-
return PhpObject::ArrayOp(m, PropertyOp::GETTER, arr, argv[0], ignore,
729-
&retval_, &exception_ TSRMLS_CC);
737+
return PhpObject::ArrayOp(m, PropertyOp::GETTER, arr, is_array_access,
738+
argv[0], ignore, &retval_, &exception_
739+
TSRMLS_CC);
730740
}
731741
if (0 == strcmp(cname, "has")) {
732742
THROW_IF_BAD_ARGS("has", 1);
733743
ZVal ignore{ZEND_FILE_LINE_C};
734-
PhpObject::ArrayOp(m, PropertyOp::QUERY, arr, argv[0], ignore,
735-
&retval_, &exception_ TSRMLS_CC);
744+
PhpObject::ArrayOp(m, PropertyOp::QUERY, arr, is_array_access,
745+
argv[0], ignore, &retval_, &exception_ TSRMLS_CC);
736746
// return true only if the property exists & is enumerable
737747
if (retval_.IsEmpty()) {
738748
retval_.SetBool(false);
@@ -749,28 +759,32 @@ class PhpObject::PhpInvokeMsg : public MessageToPhp {
749759
}
750760
if (0 == strcmp(cname, "set")) {
751761
THROW_IF_BAD_ARGS("set", 2);
752-
return PhpObject::ArrayOp(m, PropertyOp::SETTER, arr, argv[0], argv[1],
753-
&retval_, &exception_ TSRMLS_CC);
762+
return PhpObject::ArrayOp(m, PropertyOp::SETTER, arr, is_array_access,
763+
argv[0], argv[1], &retval_, &exception_
764+
TSRMLS_CC);
754765
}
755766
break;
756767
case 4:
757768
if (0 == strcmp(cname, "size")) {
758-
// This is "number of items" (including string keys), not "max index"
759-
retval_.SetInt(zend_hash_num_elements(arrht));
760-
return;
769+
// Size of all items, not just maximum index in hash.
770+
ZVal ignore{ZEND_FILE_LINE_C};
771+
return PhpObject::ArraySize(m, PropertyOp::GETTER, EnumOp::ALL,
772+
arr, is_array_access, ignore,
773+
&retval_, &exception_ TSRMLS_CC);
761774
}
762775
if (0 == strcmp(cname, "keys")) {
763776
// Map#keys() should actually return an Iterator, not an array.
764777
should_convert_array_to_iterator_ = true;
765-
return PhpObject::ArrayEnum(m, EnumOp::ALL, arr,
778+
return PhpObject::ArrayEnum(m, EnumOp::ALL, arr, is_array_access,
766779
&retval_, &exception_ TSRMLS_CC);
767780
}
768781
break;
769782
case 6:
770783
if (0 == strcmp(cname, "delete")) {
771784
THROW_IF_BAD_ARGS("delete", 1);
772785
ZVal ignore{ZEND_FILE_LINE_C};
773-
PhpObject::ArrayOp(m, PropertyOp::DELETER, arr, argv[0], ignore,
786+
PhpObject::ArrayOp(m, PropertyOp::DELETER, arr, is_array_access,
787+
argv[0], ignore,
774788
&retval_, &exception_ TSRMLS_CC);
775789
retval_.SetBool(!retval_.IsEmpty());
776790
return;
@@ -828,10 +842,67 @@ void PhpObject::MethodThunk_(v8::Local<v8::String> method,
828842
info.GetReturnValue().Set(msg.retval().ToJs(channel_));
829843
}
830844
}
845+
void PhpObject::ArrayAccessOp(PhpObjectMapper *m, PropertyOp op,
846+
const ZVal &arr, const ZVal &name, const ZVal &value,
847+
Value *retval, Value *exception TSRMLS_DC) {
848+
assert(arr.IsObject() && name.IsString());
849+
zval *rv = nullptr;
850+
851+
zval **objpp = const_cast<ZVal&>(arr).PtrPtr();
852+
// Make sure calling the interface method doesn't screw with `name`;
853+
// this is done in spl_array.c, presumably for good reason.
854+
ZVal offset(name.Ptr() ZEND_FILE_LINE_CC);
855+
offset.Separate();
856+
if (op == PropertyOp::QUERY) {
857+
zend_call_method_with_1_params(objpp, nullptr, nullptr, "offsetExists",
858+
&rv, offset.Ptr());
859+
if (rv && zend_is_true(rv)) {
860+
retval->SetInt(v8::None);
861+
} else {
862+
retval->SetEmpty();
863+
}
864+
if (rv) { zval_ptr_dtor(&rv); }
865+
} else if (op == PropertyOp::GETTER) {
866+
zval *rv2;
867+
// We need to call offsetExists to distinguish between "missing offset"
868+
// and "offset present, but with value NULL."
869+
zend_call_method_with_1_params(objpp, nullptr, nullptr, "offsetExists",
870+
&rv2, offset.Ptr());
871+
if (rv2 && zend_is_true(rv2)) {
872+
zend_call_method_with_1_params(objpp, nullptr, nullptr, "offsetGet",
873+
&rv, offset.Ptr());
874+
}
875+
if (rv) {
876+
retval->Set(m, rv TSRMLS_CC);
877+
retval->TakeOwnership();
878+
zval_ptr_dtor(&rv);
879+
} else {
880+
retval->SetEmpty();
881+
}
882+
if (rv2) { zval_ptr_dtor(&rv2); }
883+
} else if (op == PropertyOp::SETTER) {
884+
zend_call_method_with_2_params(objpp, nullptr, nullptr, "offsetSet",
885+
NULL, offset.Ptr(), value.Ptr());
886+
retval->Set(m, value TSRMLS_CC);
887+
retval->TakeOwnership();
888+
} else if (op == PropertyOp::DELETER) {
889+
zend_call_method_with_1_params(objpp, nullptr, nullptr, "offsetUnset",
890+
NULL, offset.Ptr());
891+
retval->SetBool(true);
892+
} else {
893+
assert(false);
894+
}
895+
}
831896

832897
void PhpObject::ArrayOp(PhpObjectMapper *m, PropertyOp op,
833-
const ZVal &arr, const ZVal &name, const ZVal &value,
898+
const ZVal &arr, bool is_array_access,
899+
const ZVal &name, const ZVal &value,
834900
Value *retval, Value *exception TSRMLS_DC) {
901+
if (is_array_access) {
902+
// Split this case into its own function to avoid cluttering this one
903+
// with two dissimilar cases.
904+
return ArrayAccessOp(m, op, arr, name, value, retval, exception TSRMLS_CC);
905+
}
835906
assert(arr.IsArray() && name.IsString());
836907
HashTable *arrht = Z_ARRVAL_P(arr.Ptr());
837908
const char *cname = Z_STRVAL_P(name.Ptr());
@@ -873,11 +944,82 @@ void PhpObject::ArrayOp(PhpObjectMapper *m, PropertyOp op,
873944
}
874945
}
875946

876-
void PhpObject::ArrayEnum(PhpObjectMapper *m, EnumOp op, const ZVal &arr,
947+
void PhpObject::ArrayEnum(PhpObjectMapper *m, EnumOp op,
948+
const ZVal &arr, bool is_array_access,
877949
Value *retval, Value *exception TSRMLS_DC) {
878950
// XXX unimplemented
879951
retval->SetArrayByValue(0, [](uint32_t idx, Value& v) { });
880952
}
881953

954+
void PhpObject::ArraySize(PhpObjectMapper *m, PropertyOp op, EnumOp which,
955+
const ZVal &arr, bool is_array_access,
956+
const ZVal &value,
957+
Value *retval, Value *exception TSRMLS_DC) {
958+
if (is_array_access) {
959+
assert(arr.IsObject());
960+
zval **objpp = const_cast<ZVal&>(arr).PtrPtr();
961+
zval *rv;
962+
if (op == PropertyOp::GETTER) {
963+
if (which == EnumOp::ALL) {
964+
zend_call_method_with_0_params(objpp, nullptr, nullptr, "count", &rv);
965+
ZVal r(rv ZEND_FILE_LINE_CC);
966+
if (rv) { zval_ptr_dtor(&rv); }
967+
r.Separate();
968+
convert_to_long(r.Ptr());
969+
retval->Set(m, r TSRMLS_CC);
970+
retval->TakeOwnership();
971+
} else if (which == EnumOp::ONLY_INDEX) {
972+
// XXX Not supported by standard ArrayAccess API.
973+
// XXX Define our own Js\Array interface, and try to call
974+
// a `getLength` method in it, iff the object implements
975+
// Js\Array?
976+
retval->SetInt(0);
977+
}
978+
} else if (op == PropertyOp::SETTER && which == EnumOp::ONLY_INDEX) {
979+
// XXX Not supported by standard ArrayAccess API.
980+
// XXX Define our own Js\Array interface, and try to call
981+
// a `setLength` method in it, iff the object implements
982+
// Js\Array?
983+
retval->Set(m, value TSRMLS_CC);
984+
retval->TakeOwnership();
985+
}
986+
} else {
987+
assert(arr.IsArray());
988+
HashTable *arrht = Z_ARRVAL_P(arr.Ptr());
989+
if (op == PropertyOp::GETTER) {
990+
if (which == EnumOp::ALL) {
991+
// This is "number of items" (including string keys), not "max index"
992+
retval->SetInt(zend_hash_num_elements(arrht));
993+
return;
994+
} else if (which == EnumOp::ONLY_INDEX) {
995+
retval->SetInt(zend_hash_next_free_element(arrht));
996+
}
997+
} else if (op == PropertyOp::SETTER && which == EnumOp::ONLY_INDEX) {
998+
if (!value.IsLong()) {
999+
// convert to int
1000+
const_cast<ZVal&>(value).Separate();
1001+
convert_to_long(value.Ptr());
1002+
}
1003+
if (value.IsLong() && Z_LVAL_P(value.Ptr()) >= 0) {
1004+
ulong nlen = Z_LVAL_P(value.Ptr());
1005+
ulong olen = zend_hash_next_free_element(arrht);
1006+
if (nlen < olen) {
1007+
// We have to iterate here, rather unfortunate.
1008+
// XXX We could look at zend_hash_num_elements() and
1009+
// iterate over the elements if num_elements < (olen - nlen)
1010+
for (ulong i = nlen; i < olen; i++) {
1011+
zend_hash_index_del(arrht, i);
1012+
}
1013+
}
1014+
// This is quite dodgy, since we're going to write the
1015+
// nNextFreeElement field directly. Perhaps not portable!
1016+
arrht->nNextFreeElement = nlen;
1017+
}
1018+
retval->Set(m, value TSRMLS_CC);
1019+
retval->TakeOwnership();
1020+
}
1021+
}
1022+
}
1023+
8821024

8831025
} // namespace node_php_embed

src/node_php_phpobject_class.h

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,19 @@ class PhpObject : public Nan::ObjectWrap {
6767

6868
// PHP-side array access
6969
static void ArrayOp(PhpObjectMapper *m, PropertyOp op,
70-
const ZVal &arr, const ZVal &name, const ZVal &value,
70+
const ZVal &arr, bool is_array_access,
71+
const ZVal &name, const ZVal &value,
7172
Value *retval, Value *exception TSRMLS_DC);
72-
static void ArrayEnum(PhpObjectMapper *m, EnumOp op, const ZVal &arr,
73+
static void ArrayAccessOp(PhpObjectMapper *m, PropertyOp op,
74+
const ZVal &arr,
75+
const ZVal &name, const ZVal &value,
76+
Value *retval, Value *exception TSRMLS_DC);
77+
static void ArrayEnum(PhpObjectMapper *m, EnumOp op,
78+
const ZVal &arr, bool is_array_access,
79+
Value *retval, Value *exception TSRMLS_DC);
80+
static void ArraySize(PhpObjectMapper *m, PropertyOp op, EnumOp which,
81+
const ZVal &arr, bool is_array_access,
82+
const ZVal &value,
7383
Value *retval, Value *exception TSRMLS_DC);
7484

7585
// Stash away the constructor's template for later use.

0 commit comments

Comments
 (0)