@@ -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+
201227class 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
832897void 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
0 commit comments