Browse Source

Merge pull request #62 from cscott/issue-25

Fix property and method calls on PHP native objects.
Patrick Reilly 11 năm trước cách đây
mục cha
commit
e73a9434db
11 tập tin đã thay đổi với 910 bổ sung247 xóa
  1. 42 0
      README.md
  2. 1 0
      TODO
  3. 40 0
      php_v8js_macros.h
  4. 1 0
      samples/test_call.php
  5. 332 0
      tests/magic_func.phpt
  6. 3 3
      tests/object.phpt
  7. 43 0
      tests/object_dom.phpt
  8. 2 2
      tests/object_prototype.phpt
  9. 5 5
      tests/var_dump.phpt
  10. 14 4
      v8js.cc
  11. 427 233
      v8js_convert.cc

+ 42 - 0
README.md

@@ -154,3 +154,45 @@ Javascript API
     // This makes use of the PHP module loader provided via V8Js::setModuleLoader (see PHP API above).
     require("path/to/module");
 
+The JavaScript `in` operator, when applied to a wrapped PHP object,
+works the same as the PHP `isset()` function.  Similarly, when applied
+to a wrapped PHP object, JavaScript `delete` works like PHP `unset`.
+
+```php
+<?php
+class Foo {
+  var $bar = null;
+}
+$v8 = new V8Js();
+$v8->foo = new Foo;
+// This prints "no"
+$v8->executeString('print( "bar" in PHP.foo ? "yes" : "no" );');
+?>
+```
+
+PHP has separate namespaces for properties and methods, while JavaScript
+has just one.  Usually this isn't an issue, but if you need to you can use
+a leading `$` to specify a property, or `__call` to specifically invoke a
+method.
+
+```php
+<?php
+class Foo {
+	var $bar = "bar";
+	function bar($what) { echo "I'm a ", $what, "!\n"; }
+}
+
+$foo = new Foo;
+// This prints 'bar'
+echo $foo->bar, "\n";
+// This prints "I'm a function!"
+$foo->bar("function");
+
+$v8 = new V8Js();
+$v8->foo = new Foo;
+// This prints 'bar'
+$v8->executeString('print(PHP.foo.$bar, "\n");');
+// This prints "I'm a function!"
+$v8->executeString('PHP.foo.__call("bar", ["function"]);');
+?>
+```

+ 1 - 0
TODO

@@ -1,5 +1,6 @@
 - Feature: Extension registering from php.ini
 - Feature: Thread safety
 - Missing: Indexed property handlers
+- Missing: static properties of PHP objects (on instance.constructor?)
 - Bug: exception propagation fails when property getter is set
 - Bug: method_exists() leaks when used with V8 objects

+ 40 - 0
php_v8js_macros.h

@@ -49,6 +49,35 @@ extern "C" {
 #define V8JS_THROW(type, message, message_len)	v8::ThrowException(v8::Exception::type(V8JS_STRL(message, message_len)))
 #define V8JS_GLOBAL			v8::Context::GetCurrent()->Global()
 
+#if PHP_V8_API_VERSION < 3022000
+/* CopyablePersistentTraits is only part of V8 from 3.22.0 on,
+   to be compatible with lower versions add our own (compatible) version. */
+namespace v8 {
+	template<class T>
+	struct CopyablePersistentTraits {
+		typedef Persistent<T, CopyablePersistentTraits<T> > CopyablePersistent;
+		static const bool kResetInDestructor = true;
+		template<class S, class M>
+#if PHP_V8_API_VERSION >= 3021015
+		static V8_INLINE void Copy(const Persistent<S, M>& source,
+								   CopyablePersistent* dest)
+#else
+		V8_INLINE(static void Copy(const Persistent<S, M>& source,
+								   CopyablePersistent* dest))
+#endif
+		{
+			// do nothing, just allow copy
+		}
+	};
+}
+#endif
+
+/* Abbreviate long type names */
+typedef v8::Persistent<v8::FunctionTemplate, v8::CopyablePersistentTraits<v8::FunctionTemplate> > v8js_tmpl_t;
+
+/* Hidden field name used to link JS wrappers with underlying PHP object */
+#define PHPJS_OBJECT_KEY "phpjs::object"
+
 /* Helper macros */
 #if PHP_V8_API_VERSION < 2005009
 # define V8JS_GET_CLASS_NAME(var, obj) \
@@ -63,9 +92,19 @@ extern "C" {
 #if ZEND_MODULE_API_NO >= 20100409
 # define ZEND_HASH_KEY_DC , const zend_literal *key
 # define ZEND_HASH_KEY_CC , key
+# define ZEND_HASH_KEY_NULL , NULL
 #else
 # define ZEND_HASH_KEY_DC
 # define ZEND_HASH_KEY_CC
+# define ZEND_HASH_KEY_NULL
+#endif
+
+/* method signatures of zend_update_property and zend_read_property were
+ * declared as 'char *' instead of 'const char *' before PHP 5.4 */
+#if ZEND_MODULE_API_NO >= 20100525
+# define V8JS_CONST
+#else
+# define V8JS_CONST (char *)
 #endif
 
 /* Global flags */
@@ -121,6 +160,7 @@ struct php_v8js_ctx {
   zval *module_loader;
   std::vector<char *> modules_stack;
   std::vector<char *> modules_base;
+  std::map<const char *,v8js_tmpl_t> template_cache;
 };
 /* }}} */
 

+ 1 - 0
samples/test_call.php

@@ -8,6 +8,7 @@ class Foo {
 	function __Get($name) {
 		echo "Called __get(): ";
 		var_dump($name);
+		return "xyz";
 	}
 	function __Set($name, $args) {
 		echo "Called __set(): ";

+ 332 - 0
tests/magic_func.phpt

@@ -0,0 +1,332 @@
+--TEST--
+Test V8::executeString() : Object with magic functions
+--SKIPIF--
+<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
+--FILE--
+<?php
+
+class Foo {
+	var $bar = 'foobar';
+	var $nullprop = null;
+
+	function Foo() {
+		echo "called constructor: ";
+		var_dump(func_get_args());
+	}
+
+	function MyOwnFunc() {
+		echo "called MyOwnFunc\n";
+	}
+
+	function __Get($name) {
+		echo "Called __get(): ";
+		var_dump($name);
+		return null;
+	}
+	function __Set($name, $args) {
+		echo "Called __set(): ";
+		var_dump($name, $args);
+	}
+	function __Isset($name) {
+		echo "Called __isset(): ";
+		var_dump($name);
+		return true;
+	}
+	function __unSet($name) {
+		echo "Called __unset(): ";
+		var_dump($name);
+	}
+	function __call($name, $args) {
+		echo "Called __call(): ";
+		var_dump($name, $args);
+		return "call";
+	}
+	function __Invoke($name, $arg1, $arg2) {
+		echo "Called __invoke(): ";
+		var_dump(func_get_args());
+		return 'foobar';
+	}
+	function __toString() {
+		echo "Called __tostring: ";
+		return $this->bar;
+	}
+}
+
+class Bar {
+	function foo($arg1, $arg2, $arg3) {
+		echo "Called foo(): ";
+		var_dump(func_get_args());
+		return "test";
+	}
+}
+
+
+$blaa = new V8Js();
+$blaa->obj = $obj = new Foo;
+
+try {
+  echo "__invoke() [PHP]\n";
+  var_dump($obj('arg1','arg2','arg3'));
+  echo "__invoke() [JS]\n";
+  $blaa->executeString("var_dump(PHP.obj('arg1','arg2','arg3'));", "invoke_test1 #1.js");
+  echo "------------\n";
+
+  echo " __invoke() with new [PHP]\n";
+  $myobj = new $obj('arg1','arg2','arg3'); $myobj->myownfunc();
+  echo " __invoke() with new [JS]\n";
+  $blaa->executeString("myobj = new PHP.obj('arg1','arg2','arg3'); myobj.myownfunc();", "invoke_test2 #2.js");
+  echo "------------\n";
+
+  echo " __tostring() [PHP]\n";
+  echo $obj; echo "\n";
+  echo " __tostring() [JS]\n";
+  $blaa->executeString('print(PHP.obj + "\n");', "tostring_test #3.js");
+  echo "------------\n";
+
+  echo " __isset() not called with existing property [PHP]\n";
+  if (isset($obj->bar)) { echo "bar exists\n"; }
+  echo " __isset() not called with existing property [JS]\n";
+  $blaa->executeString('if ("bar" in PHP.obj) print("bar exists\n");', "isset_test1 #4.js");
+  echo "------------\n";
+
+  echo " __isset() with non-existing property [PHP]\n";
+  if (!isset($obj->foobar)) { echo "foobar does not exist\n"; } else { echo "We called __isset and it said yes!\n"; }
+  echo " __isset() with non-existing property [JS]\n";
+  $blaa->executeString('if (!("foobar" in PHP.obj)) print("foobar does not exist\n"); else print("We called __isset and it said yes!\n");', "isset_test2 #5.js");
+  echo "------------\n";
+
+  echo " in works like isset [PHP]\n";
+  echo "nullprop is ", (isset($obj->nullprop) ? "" : "not "), "set\n";
+  echo " in works like isset [JS]\n";
+  $blaa->executeString('print("nullprop is ", ("nullprop" in PHP.obj) ? "" : "not ", "set\n");', "isset_test3 #6.js");
+  echo "------------\n";
+
+  echo " __get() not called with existing property [PHP]\n";
+  var_dump($obj->bar);
+  echo " __get() not called with existing property [JS]\n";
+  $blaa->executeString('var_dump(PHP.obj.bar);', "get_test1 #7.js");
+  echo "------------\n";
+
+  echo " __get() with non-existing property [PHP]\n";
+  var_dump($obj->fooish);
+  echo " __get() with non-existing property [JS]\n";
+  $blaa->executeString('var_dump(PHP.obj.fooish);', "get_test2 #8.js");
+  echo "------------\n";
+
+  echo " __unset() with non-existing property [PHP]\n";
+  unset($obj->foobar);
+  echo " __unset() with non-existing property [JS]\n";
+  $blaa->executeString('delete PHP.obj.foobar;', "unset_test1 #9.js");
+  echo "------------\n";
+
+  echo " __unset() with existing property [PHP]\n";
+  $obj2 = new Foo; unset($obj2->bar);
+  echo " __unset() with existing property [JS]\n";
+  $blaa->obj2 = new Foo;
+  $blaa->executeString('delete PHP.obj2.bar;', "unset_test2 #10.js");
+  echo " fetching the unset property [PHP]\n";
+  var_dump($obj2->bar);
+  echo " fetching the unset property [JS]\n";
+  $blaa->executeString('var_dump(PHP.obj2.bar);', "unset_test3 #11.js");
+  echo "------------\n";
+
+  echo " __call() [PHP]\n";
+  var_dump($obj->fooish(1,2,3));
+  echo " __call() [JS]\n";
+  # note that 'PHP.obj.fooish(1,2,3)' won't work in JS, we need to use the
+  # '__call' pseudo-method.
+  $blaa->executeString('var_dump(PHP.obj.__call("fooish", [1,2,3]));', "call_test1 #12.js");
+  echo "------------\n";
+
+  # the __call pseudo-method should work in JS even if the PHP class doesn't
+  # define an explicit __call magic function.  This makes it always safe to
+  # use __call() if you want to be sure that any __call() handlers are invoked
+  # (bypassing __get handlers, as is it done in PHP)
+  $blaa->obj3 = $obj3 = new Bar;
+  echo " __call() w/o handler [PHP]\n";
+  var_dump($obj3->foo(1,2,3));
+  echo " __call() w/o handler [JS]\n";
+  $blaa->executeString('var_dump(PHP.obj3.__call("foo", [1,2,3]));', "call_test2 #13.js");
+  echo "------------\n";
+
+  # The Bar object should inherit toString() and hasOwnProperty() methods
+  # from Object
+  echo " __toString in Bar [PHP]\n";
+  var_dump(method_exists( $obj3, '__toString' ));
+  echo " toString in Bar [PHP]\n";
+  var_dump(method_exists( $obj3, 'toString' ));
+  echo " hasOwnProperty in Bar [PHP]\n";
+  var_dump(method_exists( $obj3, 'hasOwnProperty' ));
+  echo " __toString in Bar [JS]\n";
+  $blaa->executeString('var_dump("__toString" in PHP.obj3 && typeof PHP.obj3.__toString == "function");', "inherit_test1 #14.js");
+  # use '$toString' if you actually wanted to check for a PHP property
+  # named 'toString' in Bar (instead of the inherited JavaScript property)
+  echo " toString in Bar [JS]\n";
+  $blaa->executeString('var_dump("toString" in PHP.obj3 && typeof PHP.obj3.toString == "function");', "inherit_test1 #15.js");
+  # use '$hasOwnProperty' if you actually wanted to check for a PHP property
+  # named 'hasOwnProperty' in Bar (instead of the inherited JavaScript property)
+  echo " hasOwnProperty in Bar [JS]\n";
+  $blaa->executeString('var_dump("hasOwnProperty" in PHP.obj3 && typeof PHP.obj3.hasOwnProperty == "function");', "inherit_test1 #16.js");
+  echo "------------\n";
+
+} catch (V8JsScriptException $e) {
+  echo $e->getMessage(), "\n";
+}
+?>
+===EOF===
+--EXPECT--
+called constructor: array(0) {
+}
+__invoke() [PHP]
+Called __invoke(): array(3) {
+  [0]=>
+  string(4) "arg1"
+  [1]=>
+  string(4) "arg2"
+  [2]=>
+  string(4) "arg3"
+}
+string(6) "foobar"
+__invoke() [JS]
+Called __invoke(): array(3) {
+  [0]=>
+  string(4) "arg1"
+  [1]=>
+  string(4) "arg2"
+  [2]=>
+  string(4) "arg3"
+}
+string(6) "foobar"
+------------
+ __invoke() with new [PHP]
+called constructor: array(3) {
+  [0]=>
+  string(4) "arg1"
+  [1]=>
+  string(4) "arg2"
+  [2]=>
+  string(4) "arg3"
+}
+called MyOwnFunc
+ __invoke() with new [JS]
+called constructor: array(3) {
+  [0]=>
+  string(4) "arg1"
+  [1]=>
+  string(4) "arg2"
+  [2]=>
+  string(4) "arg3"
+}
+called MyOwnFunc
+------------
+ __tostring() [PHP]
+Called __tostring: foobar
+ __tostring() [JS]
+Called __get(): string(7) "valueOf"
+Called __tostring: foobar
+------------
+ __isset() not called with existing property [PHP]
+bar exists
+ __isset() not called with existing property [JS]
+bar exists
+------------
+ __isset() with non-existing property [PHP]
+Called __isset(): string(6) "foobar"
+We called __isset and it said yes!
+ __isset() with non-existing property [JS]
+Called __isset(): string(6) "foobar"
+We called __isset and it said yes!
+------------
+ in works like isset [PHP]
+nullprop is not set
+ in works like isset [JS]
+nullprop is not set
+------------
+ __get() not called with existing property [PHP]
+string(6) "foobar"
+ __get() not called with existing property [JS]
+string(6) "foobar"
+------------
+ __get() with non-existing property [PHP]
+Called __get(): string(6) "fooish"
+NULL
+ __get() with non-existing property [JS]
+Called __get(): string(6) "fooish"
+NULL
+------------
+ __unset() with non-existing property [PHP]
+Called __unset(): string(6) "foobar"
+ __unset() with non-existing property [JS]
+Called __unset(): string(6) "foobar"
+------------
+ __unset() with existing property [PHP]
+called constructor: array(0) {
+}
+ __unset() with existing property [JS]
+called constructor: array(0) {
+}
+ fetching the unset property [PHP]
+Called __get(): string(3) "bar"
+NULL
+ fetching the unset property [JS]
+Called __get(): string(3) "bar"
+NULL
+------------
+ __call() [PHP]
+Called __call(): string(6) "fooish"
+array(3) {
+  [0]=>
+  int(1)
+  [1]=>
+  int(2)
+  [2]=>
+  int(3)
+}
+string(4) "call"
+ __call() [JS]
+Called __call(): string(6) "fooish"
+array(3) {
+  [0]=>
+  int(1)
+  [1]=>
+  int(2)
+  [2]=>
+  int(3)
+}
+string(4) "call"
+------------
+ __call() w/o handler [PHP]
+Called foo(): array(3) {
+  [0]=>
+  int(1)
+  [1]=>
+  int(2)
+  [2]=>
+  int(3)
+}
+string(4) "test"
+ __call() w/o handler [JS]
+Called foo(): array(3) {
+  [0]=>
+  int(1)
+  [1]=>
+  int(2)
+  [2]=>
+  int(3)
+}
+string(4) "test"
+------------
+ __toString in Bar [PHP]
+bool(false)
+ toString in Bar [PHP]
+bool(false)
+ hasOwnProperty in Bar [PHP]
+bool(false)
+ __toString in Bar [JS]
+bool(false)
+ toString in Bar [JS]
+bool(true)
+ hasOwnProperty in Bar [JS]
+bool(true)
+------------
+===EOF===

+ 3 - 3
tests/object.phpt

@@ -43,8 +43,8 @@ var_dump($a->myobj->foo);
 ===EOF===
 --EXPECT--
 mytest => function () { [native code] }
-foo => ORIGINAL
+$foo => ORIGINAL
 Here be monsters..
-ORIGINAL
-string(8) "ORIGINAL"
+CHANGED
+string(7) "CHANGED"
 ===EOF===

+ 43 - 0
tests/object_dom.phpt

@@ -0,0 +1,43 @@
+--TEST--
+Test V8::executeString() : DOM object passed from PHP
+--SKIPIF--
+<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
+--FILE--
+<?php
+
+$JS = <<< EOT
+print('js1: ', PHP.test.length, "\\n");
+var elements = PHP.dom.getElementsByTagName('node');
+print('js2: ', elements.length, "\\n");
+var node = elements.item(0);
+print("hasChildNodes: "); var_dump(node.hasChildNodes());
+print("hasAttribute('class'): "); var_dump(node.hasAttribute('class'));
+//var_dump(node);
+EOT;
+
+$dom = new DomDocument();
+$dom->loadXML('<node class="test"/>');
+
+$elements = $dom->getElementsByTagName('node');
+echo 'php: ', $elements->length, "\n";
+$node = $elements->item(0);
+echo "hasChildNodes: "; var_dump($node->hasChildNodes());
+echo "hasAttribute('class'): "; var_dump($node->hasAttribute('class'));
+//var_dump($node);
+
+$a = new V8Js();
+$a->dom = $dom;
+$a->test = array( 'length' => 1 );
+$a->executeString($JS, "test.js");
+
+?>
+===EOF===
+--EXPECT--
+php: 1
+hasChildNodes: bool(false)
+hasAttribute('class'): bool(true)
+js1: 1
+js2: 1
+hasChildNodes: bool(false)
+hasAttribute('class'): bool(true)
+===EOF===

+ 2 - 2
tests/object_prototype.phpt

@@ -7,9 +7,9 @@ Test V8::executeString() : Prototype with PHP callbacks
 $js = <<<'EOT'
 
 String.prototype.test = function(){ return PHP.test(this.toString(), arguments); };
-String.prototype.test_two = function(){ return PHP.test_two.func(this.toString(), arguments); };
+String.prototype.test_two = function(){ return PHP.test_two.__call('func', [this.toString(), arguments]); };
 Array.prototype.test = function(){ return PHP.test(this.toString(), arguments); };
-Array.prototype.test_two = function(){ return PHP.test_two.func(this.toString(), arguments); };
+Array.prototype.test_two = function(){ return PHP.test_two.__call('func', [this.toString(), arguments]); };
 
 "Foobar".test("foo", "bar");
 "Foobar".test_two("foo", "bar");

+ 5 - 5
tests/var_dump.phpt

@@ -198,11 +198,11 @@ array (11) {
     object(Closure)#%d {
         function () { [native code] }
     }
-    ["date"] =>
+    ["$date"] =>
     string(19) "1976-09-27 09:00:00"
-    ["timezone_type"] =>
+    ["$timezone_type"] =>
     int(3)
-    ["timezone"] =>
+    ["$timezone"] =>
     string(3) "UTC"
   }
   ["array"] =>
@@ -224,7 +224,7 @@ array (11) {
   }
   ["phpobject"] =>
   object(Foo)#%d (1) {
-    ["field"] =>
+    ["$field"] =>
     string(3) "php"
   }
 }
@@ -266,7 +266,7 @@ object(Object)#%d (12) {
   }
   ["phpobject"] =>
   object(Foo)#%d (1) {
-    ["field"] =>
+    ["$field"] =>
     string(3) "php"
   }
 }

+ 14 - 4
v8js.cc

@@ -285,11 +285,13 @@ int php_v8js_v8_get_properties_hash(v8::Handle<v8::Value> jsValue, HashTable *re
 			const char *key = ToCString(cstr);
 			zval *value = NULL;
 
-			if(jsVal->IsObject()
-			   && !jsVal->IsFunction()
-			   && jsVal->ToObject()->InternalFieldCount() == 2) {
+			v8::Local<v8::Value> php_object;
+			if (jsVal->IsObject()) {
+				php_object = v8::Local<v8::Object>::Cast(jsVal)->GetHiddenValue(V8JS_SYM(PHPJS_OBJECT_KEY));
+			}
+			if (!php_object.IsEmpty()) {
 				/* This is a PHP object, passed to JS and back. */
-				value = reinterpret_cast<zval *>(jsVal->ToObject()->GetAlignedPointerFromInternalField(0));
+				value = reinterpret_cast<zval *>(v8::External::Cast(*php_object)->Value());
 				Z_ADDREF_P(value);
 			}
 			else {
@@ -530,6 +532,13 @@ static void php_v8js_free_storage(void *object TSRMLS_DC) /* {{{ */
 	c->global_template.Reset();
 	c->global_template.~Persistent();
 
+	/* Clear persistent handles in template cache */
+	for (std::map<const char *,v8js_tmpl_t>::iterator it = c->template_cache.begin();
+		 it != c->template_cache.end(); ++it) {
+		it->second.Reset();
+	}
+	c->template_cache.~map();
+
 	/* Clear global object, dispose context */
 	if (!c->context.IsEmpty()) {
 		c->context.Reset();
@@ -569,6 +578,7 @@ static zend_object_value php_v8js_new(zend_class_entry *ce TSRMLS_DC) /* {{{ */
 
 	new(&c->modules_stack) std::vector<char*>();
 	new(&c->modules_base) std::vector<char*>();
+	new(&c->template_cache) std::map<const char *,v8js_tmpl_t>();
 
 	retval.handle = zend_objects_store_put(c, NULL, (zend_objects_free_object_storage_t) php_v8js_free_storage, NULL TSRMLS_CC);
 	retval.handlers = &v8js_object_handlers;

+ 427 - 233
v8js_convert.cc

@@ -20,6 +20,7 @@
 extern "C" {
 #include "php.h"
 #include "ext/date/php_date.h"
+#include "ext/standard/php_string.h"
 #include "zend_interfaces.h"
 #include "zend_closures.h"
 }
@@ -28,35 +29,6 @@ extern "C" {
 #include <v8.h>
 #include <stdexcept>
 
-#define PHPJS_OBJECT_KEY "phpjs::object"
-
-#if PHP_V8_API_VERSION < 3022000
-/* CopyablePersistentTraits is only part of V8 from 3.22.0 on,
-   to be compatible with lower versions add our own (compatible) version. */
-namespace v8 {
-	template<class T>
-	struct CopyablePersistentTraits {
-		typedef Persistent<T, CopyablePersistentTraits<T> > CopyablePersistent;
-		static const bool kResetInDestructor = true;
-		template<class S, class M>
-#if PHP_V8_API_VERSION >= 3021015
-		static V8_INLINE void Copy(const Persistent<S, M>& source,
-								   CopyablePersistent* dest)
-#else
-		V8_INLINE(static void Copy(const Persistent<S, M>& source,
-								   CopyablePersistent* dest))
-#endif
-		{
-			// do nothing, just allow copy
-		}
-	};
-}
-#endif
-
-typedef std::pair<struct php_v8js_ctx *, const char *> TemplateCacheKey;
-typedef v8::Persistent<v8::FunctionTemplate, v8::CopyablePersistentTraits<v8::FunctionTemplate> > TemplateCacheEntry;
-typedef std::map<TemplateCacheKey, TemplateCacheEntry> TemplateCache;
-
 /* Callback for PHP methods and functions */
 static void php_v8js_call_php_func(zval *value, zend_class_entry *ce, zend_function *method_ptr, v8::Isolate *isolate, const v8::FunctionCallbackInfo<v8::Value>& info TSRMLS_DC) /* {{{ */
 {
@@ -112,11 +84,13 @@ static void php_v8js_call_php_func(zval *value, zend_class_entry *ce, zend_funct
 		fci.params = (zval ***) safe_emalloc(argc, sizeof(zval **), 0);
 		argv = (zval **) safe_emalloc(argc, sizeof(zval *), 0);
 		for (i = 0; i < argc; i++) {
-			if(info[i]->IsObject()
-			   && !info[i]->IsFunction()
-			   && info[i]->ToObject()->InternalFieldCount() == 2) {
+			v8::Local<v8::Value> php_object;
+			if (info[i]->IsObject()) {
+				php_object = v8::Local<v8::Object>::Cast(info[i])->GetHiddenValue(V8JS_SYM(PHPJS_OBJECT_KEY));
+			}
+			if (!php_object.IsEmpty()) {
 				/* This is a PHP object, passed to JS and back. */
-				argv[i] = reinterpret_cast<zval *>(info[i]->ToObject()->GetAlignedPointerFromInternalField(0));
+				argv[i] = reinterpret_cast<zval *>(v8::External::Cast(*php_object)->Value());
 				Z_ADDREF_P(argv[i]);
 			} else {
 				MAKE_STD_ZVAL(argv[i]);
@@ -177,8 +151,9 @@ failure:
 /* Callback for PHP methods and functions */
 static void php_v8js_php_callback(const v8::FunctionCallbackInfo<v8::Value>& info) /* {{{ */
 {
-	zval *value = reinterpret_cast<zval *>(info.Holder()->GetAlignedPointerFromInternalField(0));
-	v8::Isolate *isolate = reinterpret_cast<v8::Isolate *>(info.Holder()->GetAlignedPointerFromInternalField(1));
+	v8::Isolate *isolate = info.GetIsolate();
+	v8::Local<v8::Object> self = info.Holder();
+	zval *value = reinterpret_cast<zval *>(v8::External::Cast(*self->GetHiddenValue(V8JS_SYM(PHPJS_OBJECT_KEY)))->Value());
 	zend_function *method_ptr;
 	TSRMLS_FETCH();
 	zend_class_entry *ce = Z_OBJCE_P(value);
@@ -201,19 +176,23 @@ static void php_v8js_construct_callback(const v8::FunctionCallbackInfo<v8::Value
 
 	// @todo assert constructor call
 	v8::Handle<v8::Object> newobj = info.This();
-	zval *value;
+	v8::Local<v8::External> php_object;
 	TSRMLS_FETCH();
 
 	if (!info.IsConstructCall()) {
 		return;
 	}
 
+	v8::Local<v8::Array> cons_data = v8::Local<v8::Array>::Cast(info.Data());
+	v8::Local<v8::External> ext_tmpl = v8::Local<v8::External>::Cast(cons_data->Get(0));
+	v8::Local<v8::External> ext_ce =  v8::Local<v8::External>::Cast(cons_data->Get(1));
+
 	if (info[0]->IsExternal()) {
 		// Object created by v8js in php_v8js_hash_to_jsobj, PHP object passed as v8::External.
-		value = static_cast<zval *>(v8::External::Cast(*info[0])->Value());
+		php_object = v8::Local<v8::External>::Cast(info[0]);
 	} else {
 		// Object created from JavaScript context.  Need to create PHP object first.
-		zend_class_entry *ce = static_cast<zend_class_entry *>(v8::External::Cast(*info.Data())->Value());
+		zend_class_entry *ce = static_cast<zend_class_entry *>(ext_ce->Value());
 		zend_function *ctor_ptr = ce->constructor;
 
 		// Check access on __construct function, if any
@@ -222,6 +201,7 @@ static void php_v8js_construct_callback(const v8::FunctionCallbackInfo<v8::Value
 			return;
 		}
 
+		zval *value;
 		MAKE_STD_ZVAL(value);
 		object_init_ex(value, ce);
 
@@ -229,11 +209,11 @@ static void php_v8js_construct_callback(const v8::FunctionCallbackInfo<v8::Value
 		if (ctor_ptr != NULL) {
 			php_v8js_call_php_func(value, ce, ctor_ptr, isolate, info TSRMLS_CC);
 		}
+		php_object = v8::External::New(value);
 	}
 
-	newobj->SetAlignedPointerInInternalField(0, value);
-	newobj->SetAlignedPointerInInternalField(1, (void *) isolate);
-	newobj->SetHiddenValue(V8JS_SYM(PHPJS_OBJECT_KEY), V8JS_BOOL(true));
+	newobj->SetAlignedPointerInInternalField(0, ext_tmpl->Value());
+	newobj->SetHiddenValue(V8JS_SYM(PHPJS_OBJECT_KEY), php_object);
 }
 /* }}} */
 
@@ -259,140 +239,412 @@ static int _php_v8js_is_assoc_array(HashTable *myht TSRMLS_DC) /* {{{ */
 }
 /* }}} */
 
-static void php_v8js_property_caller(const v8::FunctionCallbackInfo<v8::Value>& info) /* {{{ */
+static void php_v8js_weak_object_callback(const v8::WeakCallbackData<v8::Object, zval> &data) {
+	TSRMLS_FETCH();
+	zval *value = data.GetParameter();
+	if (READY_TO_DESTROY(value)) {
+		zval_dtor(value);
+		FREE_ZVAL(value);
+	} else {
+		Z_DELREF_P(value);
+	}
+
+	v8::V8::AdjustAmountOfExternalAllocatedMemory(-1024);
+}
+
+static void php_v8js_weak_closure_callback(const v8::WeakCallbackData<v8::Object, v8js_tmpl_t> &data) {
+	v8js_tmpl_t *persist_tpl_ = data.GetParameter();
+	persist_tpl_->Reset();
+	delete persist_tpl_;
+};
+
+/* These are not defined by Zend */
+#define ZEND_WAKEUP_FUNC_NAME    "__wakeup"
+#define ZEND_SLEEP_FUNC_NAME     "__sleep"
+#define ZEND_SET_STATE_FUNC_NAME "__set_state"
+
+#define IS_MAGIC_FUNC(mname) \
+	((key_len == sizeof(mname)) && \
+	!strncasecmp(key, mname, key_len - 1))
+
+#define PHP_V8JS_CALLBACK(mptr, tmpl)										\
+	v8::FunctionTemplate::New(php_v8js_php_callback, v8::External::New(mptr), v8::Signature::New(tmpl))->GetFunction()
+
+
+static void php_v8js_named_property_enumerator(const v8::PropertyCallbackInfo<v8::Array> &info) /* {{{ */
 {
+	// note: 'special' properties like 'constructor' are not enumerated.
+	v8::Isolate *isolate = info.GetIsolate();
 	v8::Local<v8::Object> self = info.Holder();
-	v8::Isolate *isolate = reinterpret_cast<v8::Isolate *>(self->GetAlignedPointerFromInternalField(1));
-	v8::Local<v8::String> cname = info.Callee()->GetName()->ToString();
-	v8::Local<v8::Value> value;
-	v8::Local<v8::String> cb_func = v8::Local<v8::String>::Cast(info.Data());
+	v8::Local<v8::Array> result = v8::Array::New(0);
+	uint32_t result_len = 0;
 
-	value = self->GetHiddenValue(cb_func);
+	zend_class_entry *ce;
+	zend_function *method_ptr;
+	HashTable *proptable;
+	HashPosition pos;
+	char *key = NULL;
+	uint key_len;
+	ulong index;
 
-	if (!value.IsEmpty() && value->IsFunction())
-	{
-		int argc = info.Length(), i = 0;
-		v8::Local<v8::Value> argv[argc];
-		v8::Local<v8::Function> cb = v8::Local<v8::Function>::Cast(value);
+	zval *object = reinterpret_cast<zval *>(v8::External::Cast(*self->GetHiddenValue(V8JS_SYM(PHPJS_OBJECT_KEY)))->Value());
+	ce = Z_OBJCE_P(object);
 
-		if (cb_func->Equals(V8JS_SYM(ZEND_INVOKE_FUNC_NAME))) {
-			for (; i < argc; ++i) {
-				argv[i] = info[i];
-			}
-			value = cb->Call(self, argc, argv);
+	/* enumerate all methods */
+	zend_hash_internal_pointer_reset_ex(&ce->function_table, &pos);
+	for (;; zend_hash_move_forward_ex(&ce->function_table, &pos)) {
+		if (zend_hash_get_current_key_ex(&ce->function_table, &key, &key_len, &index, 0, &pos) != HASH_KEY_IS_STRING  ||
+			zend_hash_get_current_data_ex(&ce->function_table, (void **) &method_ptr, &pos) == FAILURE
+			) {
+			break;
+		}
+
+		if ((method_ptr->common.fn_flags & ZEND_ACC_PUBLIC) == 0) {
+			/* Allow only public methods */
+			continue;
+		}
+		if ((method_ptr->common.fn_flags & (ZEND_ACC_CTOR|ZEND_ACC_DTOR|ZEND_ACC_CLONE)) != 0) {
+			/* no __construct, __destruct(), or __clone() functions */
+			continue;
+		}
+		// hide (do not enumerate) other PHP magic functions
+		if (IS_MAGIC_FUNC(ZEND_CALLSTATIC_FUNC_NAME) ||
+			IS_MAGIC_FUNC(ZEND_SLEEP_FUNC_NAME) ||
+			IS_MAGIC_FUNC(ZEND_WAKEUP_FUNC_NAME) ||
+			IS_MAGIC_FUNC(ZEND_SET_STATE_FUNC_NAME) ||
+			IS_MAGIC_FUNC(ZEND_GET_FUNC_NAME) ||
+			IS_MAGIC_FUNC(ZEND_SET_FUNC_NAME) ||
+			IS_MAGIC_FUNC(ZEND_UNSET_FUNC_NAME) ||
+			IS_MAGIC_FUNC(ZEND_CALL_FUNC_NAME) ||
+			IS_MAGIC_FUNC(ZEND_INVOKE_FUNC_NAME) ||
+			IS_MAGIC_FUNC(ZEND_ISSET_FUNC_NAME)) {
+			continue;
+		}
+		v8::Local<v8::String> method_name = V8JS_STR(method_ptr->common.function_name);
+		// rename PHP special method names to JS equivalents.
+		if (IS_MAGIC_FUNC(ZEND_TOSTRING_FUNC_NAME)) {
+			method_name = V8JS_SYM("toString");
 		}
-		else /* __call() */
-		{
-			v8::Local<v8::Array> argsarr = v8::Array::New(argc);
-			for (; i < argc; ++i) {
-				argsarr->Set(i, info[i]);
+		result->Set(result_len++, method_name);
+	}
+	/* enumerate all properties */
+	/* Z_OBJPROP uses the get_properties handler */
+	proptable = Z_OBJPROP_P(object);
+	zend_hash_internal_pointer_reset_ex(proptable, &pos);
+	for (;; zend_hash_move_forward_ex(proptable, &pos)) {
+		int i = zend_hash_get_current_key_ex(proptable, &key, &key_len, &index, 0, &pos);
+		if (i == HASH_KEY_NON_EXISTANT)
+			break;
+
+		// for consistency with the 'in' operator, skip properties whose
+		// value IS_NULL (like isset does)
+		zval **data;
+		if (zend_hash_get_current_data_ex(proptable, (void **) &data, &pos) == SUCCESS &&
+			ZVAL_IS_NULL(*data))
+			continue;
+
+		if (i == HASH_KEY_IS_STRING) {
+			/* skip protected and private members */
+			if (key[0] == '\0') {
+				continue;
 			}
-			v8::Local<v8::Value> argsv[2] = { cname, argsarr };
-			value = cb->Call(self, 2, argsv);
+			// prefix enumerated property names with '$' so they can be
+			// dereferenced unambiguously (ie, don't conflict with method
+			// names)
+			char prefixed[key_len + 1];
+			prefixed[0] = '$';
+			strncpy(prefixed + 1, key, key_len);
+			result->Set(result_len++, V8JS_STRL(prefixed, key_len));
+		} else {
+			// even numeric indices are enumerated as strings in JavaScript
+			result->Set(result_len++, V8JS_FLOAT((double) index)->ToString());
 		}
 	}
 
-	if (info.IsConstructCall()) {
-		if (!value.IsEmpty() && !value->IsNull()) {
-			info.GetReturnValue().Set(value);
-			return;
-		}
+	/* done */
+	info.GetReturnValue().Set(result);
+}
+/* }}} */
 
-		info.GetReturnValue().Set(self);
-		return;
+static void php_v8js_invoke_callback(const v8::FunctionCallbackInfo<v8::Value>& info) /* {{{ */
+{
+	v8::Local<v8::Object> self = info.Holder();
+	v8::Local<v8::Function> cb = v8::Local<v8::Function>::Cast(info.Data());
+	int argc = info.Length(), i;
+	v8::Local<v8::Value> argv[argc];
+	v8::Local<v8::Value> result;
+
+	for (i=0; i<argc; i++) {
+		argv[i] = info[i];
+	}
+	if (info.IsConstructCall() && self->GetConstructor()->IsFunction()) {
+		// this is a 'new obj(...)' invocation.  Handle this like PHP does;
+		// that is, treat it as synonymous with 'new obj.constructor(...)'
+		cb = v8::Local<v8::Function>::Cast(self->GetConstructor());
+		result = cb->NewInstance(argc, argv);
+	} else {
+		result = cb->Call(self, argc, argv);
 	}
-
-	info.GetReturnValue().Set(value);
+	info.GetReturnValue().Set(result);
 }
 /* }}} */
 
-static void php_v8js_property_getter(v8::Local<v8::String> property, const v8::PropertyCallbackInfo<v8::Value> &info) /* {{{ */
+// this is a magic '__call' implementation for PHP classes which don't actually
+// have a '__call' magic function.  This way we can always force a method
+// call (as opposed to a property get) from JavaScript using __call.
+static void php_v8js_fake_call_impl(const v8::FunctionCallbackInfo<v8::Value>& info) /* {{{ */
 {
+	v8::Isolate *isolate = info.GetIsolate();
 	v8::Local<v8::Object> self = info.Holder();
-	v8::Local<v8::Value> value;
-	v8::Local<v8::Function> cb;
+	v8::Handle<v8::Value> return_value;
+
+	char *error;
+	int error_len;
 
-	/* Check first if JS object has the named property */
-	value = self->GetRealNamedProperty(property);
+	zend_class_entry *ce;
+	zval *object = reinterpret_cast<zval *>(v8::External::Cast(*self->GetHiddenValue(V8JS_SYM(PHPJS_OBJECT_KEY)))->Value());
+	ce = Z_OBJCE_P(object);
 
-	if (!value.IsEmpty()) {
-		info.GetReturnValue().Set(value);
+	// first arg is method name, second arg is array of args.
+	if (info.Length() < 2) {
+		error_len = spprintf(&error, 0,
+			"%s::__call expects 2 parameters, %d given",
+			ce->name, (int) info.Length());
+		return_value = V8JS_THROW(TypeError, error, error_len);
+		efree(error);
+		info.GetReturnValue().Set(return_value);
 		return;
 	}
-
-	v8::Isolate *isolate = reinterpret_cast<v8::Isolate *>(self->GetAlignedPointerFromInternalField(1));
-
-	/* If __get() is set for PHP object, call it */
-	value = self->GetHiddenValue(V8JS_SYM(ZEND_GET_FUNC_NAME));
-	if (!value.IsEmpty() && value->IsFunction()) {
-		cb = v8::Local<v8::Function>::Cast(value);
-		v8::Local<v8::Value> argv[1] = {property};
-		value = cb->Call(self, 1, argv);
+	if (!info[1]->IsArray()) {
+		error_len = spprintf(&error, 0,
+			"%s::__call expects 2nd parameter to be an array",
+			ce->name);
+		return_value = V8JS_THROW(TypeError, error, error_len);
+		efree(error);
+		info.GetReturnValue().Set(return_value);
+		return;
 	}
-
-	/* If __get() does not exist or returns NULL, create new function with callback for __call() */
-	if ((value.IsEmpty() || value->IsNull()) && info.Data()->IsTrue()) {
-		v8::Local<v8::FunctionTemplate> cb_t = v8::FunctionTemplate::New(php_v8js_property_caller, V8JS_SYM(ZEND_CALL_FUNC_NAME));
-		cb = cb_t->GetFunction();
-		cb->SetName(property);
-		info.GetReturnValue().Set(cb);
+	v8::String::Utf8Value str(info[0]->ToString());
+	const char *method_name = ToCString(str);
+	uint method_name_len = strlen(method_name);
+	v8::Local<v8::Array> args = v8::Local<v8::Array>::Cast(info[1]);
+	if (args->Length() > 1000000) {
+		// prevent overflow, since args->Length() is a uint32_t and args
+		// in the Function->Call method below is a (signed) int.
+		error_len = spprintf(&error, 0,
+			"%s::__call expects fewer than a million arguments",
+			ce->name);
+		return_value = V8JS_THROW(TypeError, error, error_len);
+		efree(error);
+		info.GetReturnValue().Set(return_value);
+		return;
+	}
+	// okay, look up the method name and manually invoke it.
+	const zend_object_handlers *h = Z_OBJ_HT_P(object);
+	zend_function *method_ptr =
+		h->get_method(&object, (char*)method_name, method_name_len
+			ZEND_HASH_KEY_NULL TSRMLS_DC);
+	if (method_ptr == NULL ||
+		(method_ptr->common.fn_flags & ZEND_ACC_PUBLIC) == 0 ||
+		(method_ptr->common.fn_flags & (ZEND_ACC_CTOR|ZEND_ACC_DTOR|ZEND_ACC_CLONE)) != 0) {
+		error_len = spprintf(&error, 0,
+			"%s::__call to %s method %s", ce->name,
+			(method_ptr == NULL) ? "undefined" : "non-public", method_name);
+		return_value = V8JS_THROW(TypeError, error, error_len);
+		efree(error);
+		info.GetReturnValue().Set(return_value);
 		return;
 	}
 
-	info.GetReturnValue().Set(value);
+	v8::Local<v8::FunctionTemplate> tmpl =
+		v8::Local<v8::FunctionTemplate>::New
+			(isolate, *reinterpret_cast<v8js_tmpl_t *>(self->GetAlignedPointerFromInternalField(0)));
+	// use php_v8js_php_callback to actually execute the method
+	v8::Local<v8::Function> cb = PHP_V8JS_CALLBACK(method_ptr, tmpl);
+	uint32_t i, argc = args->Length();
+	v8::Local<v8::Value> argv[argc];
+	for (i=0; i<argc; i++) {
+		argv[i] = args->Get(i);
+	}
+	return_value = cb->Call(info.This(), (int) argc, argv);
+	info.GetReturnValue().Set(return_value);
 }
 /* }}} */
 
-static void php_v8js_property_query(v8::Local<v8::String> property, const v8::PropertyCallbackInfo<v8::Integer> &info) /* {{{ */
+typedef enum {
+	V8JS_PROP_GETTER,
+	V8JS_PROP_SETTER,
+	V8JS_PROP_QUERY,
+	V8JS_PROP_DELETER
+} property_op_t;
+
+/* This method handles named property and method get/set/query/delete. */
+template<typename T>
+static inline v8::Local<v8::Value> php_v8js_named_property_callback(v8::Local<v8::String> property, const v8::PropertyCallbackInfo<T> &info, property_op_t callback_type, v8::Local<v8::Value> set_value = v8::Local<v8::Value>()) /* {{{ */
 {
-	v8::Local<v8::Object> self = info.Holder();
-	v8::Isolate *isolate = reinterpret_cast<v8::Isolate *>(self->GetAlignedPointerFromInternalField(1));
-	v8::Local<v8::Value> value;
+	v8::Isolate *isolate = info.GetIsolate();
+	v8::String::Utf8Value cstr(property);
+	const char *name = ToCString(cstr);
+	uint name_len = strlen(name);
+	char *lower = estrndup(name, name_len);
+	const char *method_name;
+	uint method_name_len;
 
-	/* Return early if property is set in JS object */
-	if (self->HasRealNamedProperty(property)) {
-		info.GetReturnValue().Set(V8JS_INT(v8::ReadOnly));
-		return;
-	}
+	v8::Local<v8::Object> self = info.Holder();
+	v8::Local<v8::Value> ret_value;
+	v8::Local<v8::Function> cb;
 
-	value = self->GetHiddenValue(V8JS_SYM(ZEND_ISSET_FUNC_NAME));
-	if (!value.IsEmpty() && value->IsFunction()) {
-		v8::Local<v8::Function> cb = v8::Local<v8::Function>::Cast(value);
-		v8::Local<v8::Value> argv[1] = {property};
-		value = cb->Call(self, 1, argv);
+	zend_class_entry *scope = NULL; /* XXX? */
+	zend_class_entry *ce;
+	zend_function *method_ptr = NULL;
+	zval *php_value;
+
+	zval *object = reinterpret_cast<zval *>(v8::External::Cast(*self->GetHiddenValue(V8JS_SYM(PHPJS_OBJECT_KEY)))->Value());
+	v8::Local<v8::FunctionTemplate> tmpl =
+		v8::Local<v8::FunctionTemplate>::New
+		(isolate, *reinterpret_cast<v8js_tmpl_t *>(self->GetAlignedPointerFromInternalField(0)));
+	ce = Z_OBJCE_P(object);
+
+	/* First, check the (case-insensitive) method table */
+	php_strtolower(lower, name_len);
+	method_name = lower;
+	method_name_len = name_len;
+	// toString() -> __tostring()
+	if (name_len == 8 && strcmp(name, "toString") == 0) {
+		method_name = ZEND_TOSTRING_FUNC_NAME;
+		method_name_len = sizeof(ZEND_TOSTRING_FUNC_NAME) - 1;
+	}
+	bool is_constructor = (name_len == 11 && strcmp(name, "constructor") == 0);
+	bool is_magic_call = (method_name_len == 6 && strcmp(method_name, "__call") == 0);
+	if (is_constructor ||
+		(name[0] != '$' /* leading '$' means property, not method */ &&
+		 zend_hash_find(&ce->function_table, method_name, method_name_len + 1, (void**)&method_ptr) == SUCCESS &&
+		 ((method_ptr->common.fn_flags & ZEND_ACC_PUBLIC) != 0) && /* Allow only public methods */
+		 ((method_ptr->common.fn_flags & (ZEND_ACC_CTOR|ZEND_ACC_DTOR|ZEND_ACC_CLONE)) == 0) /* no __construct, __destruct(), or __clone() functions */
+		 ) || (method_ptr=NULL, is_magic_call)
+	) {
+		if (callback_type == V8JS_PROP_GETTER) {
+			if (is_constructor) {
+				ret_value = self->GetConstructor();
+			} else {
+				if (is_magic_call && method_ptr==NULL) {
+					// Fake __call implementation
+					// (only use this if method_ptr==NULL, which means
+					//  there is no actual PHP __call() implementation)
+					v8::Local<v8::Function> cb =
+						v8::FunctionTemplate::New(
+							php_v8js_fake_call_impl, V8JS_NULL,
+							v8::Signature::New(tmpl))->GetFunction();
+					cb->SetName(property);
+					ret_value = cb;
+				} else {
+					ret_value = PHP_V8JS_CALLBACK(method_ptr, tmpl);
+				}
+			}
+		} else if (callback_type == V8JS_PROP_QUERY) {
+			// methods are not enumerable
+			ret_value = v8::Integer::NewFromUnsigned(v8::ReadOnly|v8::DontEnum|v8::DontDelete, isolate);
+		} else if (callback_type == V8JS_PROP_SETTER) {
+			ret_value = set_value; // lie.  this field is read-only.
+		} else if (callback_type == V8JS_PROP_DELETER) {
+			ret_value = V8JS_BOOL(false);
+		} else {
+			/* shouldn't reach here! but bail safely */
+			ret_value = v8::Handle<v8::Value>();
+		}
+	} else {
+		if (name[0]=='$') {
+			// this is a property (not a method)
+			name++; name_len--;
+		}
+		if (callback_type == V8JS_PROP_GETTER) {
+			/* Nope, not a method -- must be a (case-sensitive) property */
+			php_value = zend_read_property(scope, object, V8JS_CONST name, name_len, true);
+			// special case 'NULL' and return an empty value (indicating that
+			// we don't intercept this property) if the property doesn't
+			// exist.
+			if (ZVAL_IS_NULL(php_value)) {
+				const zend_object_handlers *h = Z_OBJ_HT_P(object);
+				zval *prop;
+				MAKE_STD_ZVAL(prop);
+				ZVAL_STRINGL(prop, name, name_len, 1);
+				if (!h->has_property(object, prop, 2 ZEND_HASH_KEY_NULL TSRMLS_CC))
+					ret_value = v8::Handle<v8::Value>();
+				else {
+					ret_value = V8JS_NULL;
+				}
+				zval_ptr_dtor(&prop);
+			} else {
+				// wrap it
+				ret_value = zval_to_v8js(php_value, isolate TSRMLS_CC);
+			}
+			/* php_value is the value in the property table; we don't own a
+			 * reference to it (and so don't have to deref) */
+		} else if (callback_type == V8JS_PROP_SETTER) {
+			MAKE_STD_ZVAL(php_value);
+			if (v8js_to_zval(set_value, php_value, 0, isolate) == SUCCESS) {
+				zend_update_property(scope, object, V8JS_CONST name, name_len, php_value);
+				ret_value = set_value;
+			} else {
+				ret_value = v8::Handle<v8::Value>();
+			}
+		} else if (callback_type == V8JS_PROP_QUERY ||
+				   callback_type == V8JS_PROP_DELETER) {
+			const zend_object_handlers *h = Z_OBJ_HT_P(object);
+			zval *prop;
+			MAKE_STD_ZVAL(prop);
+			ZVAL_STRINGL(prop, name, name_len, 1);
+			if (callback_type == V8JS_PROP_QUERY) {
+				if (h->has_property(object, prop, 0 ZEND_HASH_KEY_NULL TSRMLS_CC)) {
+					ret_value = v8::Integer::NewFromUnsigned(v8::None);
+				} else {
+					ret_value = v8::Handle<v8::Value>(); // empty handle
+				}
+			} else {
+				h->unset_property(object, prop ZEND_HASH_KEY_NULL TSRMLS_CC);
+				ret_value = V8JS_BOOL(true);
+			}
+			zval_ptr_dtor(&prop);
+		} else {
+			/* shouldn't reach here! but bail safely */
+			ret_value = v8::Handle<v8::Value>();
+		}
 	}
 
-	info.GetReturnValue().Set((!value.IsEmpty() && value->IsTrue()) ? V8JS_INT(v8::ReadOnly) : v8::Local<v8::Integer>());
+	efree(lower);
+	return ret_value;
 }
 /* }}} */
 
-/* These are not defined by Zend */
-#define ZEND_WAKEUP_FUNC_NAME    "__wakeup"
-#define ZEND_SLEEP_FUNC_NAME     "__sleep"
-#define ZEND_SET_STATE_FUNC_NAME "__set_state"
-
-#define IS_MAGIC_FUNC(mname) \
-	((key_len == sizeof(mname)) && \
-	!strncasecmp(key, mname, key_len - 1))
-
-#define PHP_V8JS_CALLBACK(mptr, tmpl)										\
-	v8::FunctionTemplate::New(php_v8js_php_callback, v8::External::New(mptr), v8::Signature::New(tmpl))->GetFunction()
-
+static void php_v8js_named_property_getter(v8::Local<v8::String> property, const v8::PropertyCallbackInfo<v8::Value> &info) /* {{{ */
+{
+	info.GetReturnValue().Set(php_v8js_named_property_callback(property, info, V8JS_PROP_GETTER));
+}
+/* }}} */
 
-static void php_v8js_weak_object_callback(v8::Isolate *isolate, v8::Persistent<v8::Object> *object, zval *value)
+static void php_v8js_named_property_setter(v8::Local<v8::String> property, v8::Local<v8::Value> value, const v8::PropertyCallbackInfo<v8::Value> &info) /* {{{ */
 {
-	TSRMLS_FETCH();
+	info.GetReturnValue().Set(php_v8js_named_property_callback(property, info, V8JS_PROP_SETTER, value));
+}
+/* }}} */
 
-	if (READY_TO_DESTROY(value)) {
-		zval_dtor(value);
-		FREE_ZVAL(value);
-	} else {
-		Z_DELREF_P(value);
+static void php_v8js_named_property_query(v8::Local<v8::String> property, const v8::PropertyCallbackInfo<v8::Integer> &info) /* {{{ */
+{
+	v8::Local<v8::Value> r = php_v8js_named_property_callback(property, info, V8JS_PROP_QUERY);
+	if (!r.IsEmpty()) {
+		info.GetReturnValue().Set(r->ToInteger());
 	}
+}
+/* }}} */
 
-	v8::V8::AdjustAmountOfExternalAllocatedMemory(-1024);
-	object->Dispose();
+static void php_v8js_named_property_deleter(v8::Local<v8::String> property, const v8::PropertyCallbackInfo<v8::Boolean> &info) /* {{{ */
+{
+	v8::Local<v8::Value> r = php_v8js_named_property_callback(property, info, V8JS_PROP_DELETER);
+	if (!r.IsEmpty()) {
+		info.GetReturnValue().Set(r->ToBoolean());
+	}
 }
+/* }}} */
 
 static v8::Handle<v8::Value> php_v8js_hash_to_jsobj(zval *value, v8::Isolate *isolate TSRMLS_DC) /* {{{ */
 {
@@ -404,7 +656,6 @@ static v8::Handle<v8::Value> php_v8js_hash_to_jsobj(zval *value, v8::Isolate *is
 	HashTable *myht;
 	HashPosition pos;
 	zend_class_entry *ce = NULL;
-	zend_function *method_ptr, *call_ptr = NULL, *get_ptr = NULL, *invoke_ptr = NULL, *isset_ptr = NULL;
 
 	if (Z_TYPE_P(value) == IS_ARRAY) {
 		myht = HASH_OF(value);
@@ -427,100 +678,57 @@ static v8::Handle<v8::Value> php_v8js_hash_to_jsobj(zval *value, v8::Isolate *is
 	} else if (ce) {
 		php_v8js_ctx *ctx = (php_v8js_ctx *) isolate->GetData();
 		v8::Local<v8::FunctionTemplate> new_tpl;
-		bool cached_tpl = true;
-		static TemplateCache tpl_map;
+		v8js_tmpl_t *persist_tpl_;
 
 		try {
 			new_tpl = v8::Local<v8::FunctionTemplate>::New
-				(isolate, tpl_map.at(std::make_pair(ctx, ce->name)));
+				(isolate, ctx->template_cache.at(ce->name));
 		}
 		catch (const std::out_of_range &) {
-			cached_tpl = false;
-
 			/* No cached v8::FunctionTemplate available as of yet, create one. */
 			new_tpl = v8::FunctionTemplate::New();
 
 			new_tpl->SetClassName(V8JS_STRL(ce->name, ce->name_length));
-			new_tpl->InstanceTemplate()->SetInternalFieldCount(2);
-
-			v8::Handle<v8::Value> wrapped_ce = v8::External::New(ce);
-			new_tpl->SetCallHandler(php_v8js_construct_callback, wrapped_ce);
+			new_tpl->InstanceTemplate()->SetInternalFieldCount(1);
 
 			if (ce == zend_ce_closure) {
 				/* Got a closure, mustn't cache ... */
+				persist_tpl_ = new v8js_tmpl_t(isolate, new_tpl);
+				/* We'll free persist_tpl_ via php_v8js_weak_closure_callback, below */
 				new_tpl->InstanceTemplate()->SetCallAsFunctionHandler(php_v8js_php_callback);
 			} else {
 				/* Add new v8::FunctionTemplate to tpl_map, as long as it is not a closure. */
-				TemplateCacheEntry tce(isolate, new_tpl);
-				tpl_map[std::make_pair(ctx, ce->name)] = tce;
-			}
-		}
-
-		if (ce != zend_ce_closure) {
-			/* Attach object methods to the instance template. */
-			zend_hash_internal_pointer_reset_ex(&ce->function_table, &pos);
-			for (;; zend_hash_move_forward_ex(&ce->function_table, &pos)) {
-				if (zend_hash_get_current_key_ex(&ce->function_table, &key, &key_len, &index, 0, &pos) != HASH_KEY_IS_STRING  ||
-					zend_hash_get_current_data_ex(&ce->function_table, (void **) &method_ptr, &pos) == FAILURE
-				) {
-					break;
-				}
-
-				if ((method_ptr->common.fn_flags & ZEND_ACC_PUBLIC)     && /* Allow only public methods */
-					(method_ptr->common.fn_flags & ZEND_ACC_CTOR) == 0  && /* ..and no __construct() */
-					(method_ptr->common.fn_flags & ZEND_ACC_DTOR) == 0  && /* ..or __destruct() */
-					(method_ptr->common.fn_flags & ZEND_ACC_CLONE) == 0 /* ..or __clone() functions */
-				) {
-					/* Override native toString() with __tostring() if it is set in passed object */
-					if (!cached_tpl && IS_MAGIC_FUNC(ZEND_TOSTRING_FUNC_NAME)) {
-						new_tpl->InstanceTemplate()->Set(V8JS_SYM("toString"), PHP_V8JS_CALLBACK(method_ptr, new_tpl));
-					/* TODO: __set(), __unset() disabled as JS is not allowed to modify the passed PHP object yet.
-					 *  __sleep(), __wakeup(), __set_state() are always ignored */
-					} else if (
-						IS_MAGIC_FUNC(ZEND_CALLSTATIC_FUNC_NAME)|| /* TODO */
-						IS_MAGIC_FUNC(ZEND_SLEEP_FUNC_NAME)     ||
-						IS_MAGIC_FUNC(ZEND_WAKEUP_FUNC_NAME)    ||
-						IS_MAGIC_FUNC(ZEND_SET_STATE_FUNC_NAME) ||
-						IS_MAGIC_FUNC(ZEND_SET_FUNC_NAME)       ||
-						IS_MAGIC_FUNC(ZEND_UNSET_FUNC_NAME)
-					) {
-					/* Register all magic function as hidden with lowercase name */
-					} else if (IS_MAGIC_FUNC(ZEND_GET_FUNC_NAME)) {
-						get_ptr = method_ptr;
-					} else if (IS_MAGIC_FUNC(ZEND_CALL_FUNC_NAME)) {
-						call_ptr = method_ptr;
-					} else if (IS_MAGIC_FUNC(ZEND_INVOKE_FUNC_NAME)) {
-						invoke_ptr = method_ptr;
-					} else if (IS_MAGIC_FUNC(ZEND_ISSET_FUNC_NAME)) {
-						isset_ptr = method_ptr;
-					} else if (!cached_tpl) {
-						new_tpl->InstanceTemplate()->Set(V8JS_STR(method_ptr->common.function_name), PHP_V8JS_CALLBACK(method_ptr, new_tpl), v8::ReadOnly);
-					}
-				}
-			}
-
-			if (!cached_tpl) {
-				/* Only register getter, etc. when they're set in PHP side */
-				if (call_ptr || get_ptr || isset_ptr)
-				{
-					/* Set __get() handler which acts also as __call() proxy */
-					new_tpl->InstanceTemplate()->SetNamedPropertyHandler(
-						php_v8js_property_getter,					/* getter */
-						0,											/* setter */
-						isset_ptr ? php_v8js_property_query : 0,	/* query */
-						0,											/* deleter */
-						0,											/* enumerator */
-						V8JS_BOOL(call_ptr ? true : false)
-					);
-				}
-
-				/* __invoke() handler */
-				if (invoke_ptr) {
-					new_tpl->InstanceTemplate()->SetCallAsFunctionHandler(php_v8js_property_caller, V8JS_SYM(ZEND_INVOKE_FUNC_NAME));
+				persist_tpl_ = &ctx->template_cache[ce->name];
+				persist_tpl_->Reset(isolate, new_tpl);
+				/* We'll free persist_tpl_ when template_cache is destroyed */
+				// Finish setup of new_tpl
+				new_tpl->InstanceTemplate()->SetNamedPropertyHandler
+					(php_v8js_named_property_getter, /* getter */
+					 php_v8js_named_property_setter, /* setter */
+					 php_v8js_named_property_query, /* query */
+					 php_v8js_named_property_deleter, /* deleter */
+					 php_v8js_named_property_enumerator, /* enumerator */
+					 V8JS_NULL /* data */
+					 );
+				// add __invoke() handler
+				zend_function *invoke_method_ptr;
+				if (zend_hash_find(&ce->function_table, ZEND_INVOKE_FUNC_NAME,
+								   sizeof(ZEND_INVOKE_FUNC_NAME),
+								   (void**)&invoke_method_ptr) == SUCCESS &&
+					invoke_method_ptr->common.fn_flags & ZEND_ACC_PUBLIC) {
+					new_tpl->InstanceTemplate()->SetCallAsFunctionHandler(php_v8js_invoke_callback, PHP_V8JS_CALLBACK(invoke_method_ptr, new_tpl));
 				}
 			}
+			v8::Local<v8::Array> call_handler_data = v8::Array::New(2);
+			call_handler_data->Set(0, v8::External::New(persist_tpl_));
+			call_handler_data->Set(1, v8::External::New(ce));
+			new_tpl->SetCallHandler(php_v8js_construct_callback, call_handler_data);
 		}
 
+		// Create v8 wrapper object
+		v8::Handle<v8::Value> external = v8::External::New(value);
+		newobj = new_tpl->GetFunction()->NewInstance(1, &external);
+
 		// Increase the reference count of this value because we're storing it internally for use later
 		// See https://github.com/preillyme/v8js/issues/6
 		Z_ADDREF_P(value);
@@ -528,33 +736,18 @@ static v8::Handle<v8::Value> php_v8js_hash_to_jsobj(zval *value, v8::Isolate *is
 		// Since we got to decrease the reference count again, in case v8 garbage collector
 		// decides to dispose the JS object, we add a weak persistent handle and register
 		// a callback function that removes the reference.
-		v8::Handle<v8::Value> external = v8::External::New(value);
-		v8::Persistent<v8::Object> persist_newobj(isolate, new_tpl->GetFunction()->NewInstance(1, &external));
-		persist_newobj.MakeWeak(value, php_v8js_weak_object_callback);
+		v8::Persistent<v8::Object> persist_newobj(isolate, newobj);
+		persist_newobj.SetWeak(value, php_v8js_weak_object_callback);
 
 		// Just tell v8 that we're allocating some external memory
 		// (for the moment we just always tell 1k instead of trying to find out actual values)
 		v8::V8::AdjustAmountOfExternalAllocatedMemory(1024);
 
-		newobj = v8::Local<v8::Object>::New(isolate, persist_newobj);
-
-		if (ce != zend_ce_closure) {
-			// These unfortunately cannot be attached to the template, hence we have to put them
-			// on each and every object instance manually.
-			if (call_ptr) {
-				newobj->SetHiddenValue(V8JS_SYM(ZEND_CALL_FUNC_NAME), PHP_V8JS_CALLBACK(call_ptr, new_tpl));
-			}
-			if (get_ptr) {
-				newobj->SetHiddenValue(V8JS_SYM(ZEND_GET_FUNC_NAME), PHP_V8JS_CALLBACK(get_ptr, new_tpl));
-			}
-			if (invoke_ptr) {
-				newobj->SetHiddenValue(V8JS_SYM(ZEND_INVOKE_FUNC_NAME), PHP_V8JS_CALLBACK(invoke_ptr, new_tpl));
-			}
-			if (isset_ptr) {
-				newobj->SetHiddenValue(V8JS_SYM(ZEND_ISSET_FUNC_NAME), PHP_V8JS_CALLBACK(isset_ptr, new_tpl));
-			}
+		if (ce == zend_ce_closure) {
+			// free uncached function template when object is freed
+			v8::Persistent<v8::Object> persist_newobj2(isolate, newobj);
+			persist_newobj2.SetWeak(persist_tpl_, php_v8js_weak_closure_callback);
 		}
-
 	} else {
 		v8::Local<v8::FunctionTemplate> new_tpl = v8::FunctionTemplate::New();	// @todo re-use template likewise
 
@@ -565,7 +758,7 @@ static v8::Handle<v8::Value> php_v8js_hash_to_jsobj(zval *value, v8::Isolate *is
 	/* Object properties */
 	i = myht ? zend_hash_num_elements(myht) : 0;
 
-	if (i > 0)
+	if (i > 0 && !ce)
 	{
 		zval **data;
 		HashTable *tmp_ht;
@@ -743,8 +936,9 @@ int v8js_to_zval(v8::Handle<v8::Value> jsValue, zval *return_value, int flags, v
 	{
 		v8::Handle<v8::Object> self = v8::Handle<v8::Object>::Cast(jsValue);
 		// if this is a wrapped PHP object, then just unwrap it.
-		if (!self->GetHiddenValue(V8JS_SYM(PHPJS_OBJECT_KEY)).IsEmpty()) {
-			zval *object = reinterpret_cast<zval *>(self->GetAlignedPointerFromInternalField(0));
+		v8::Local<v8::Value> php_object = self->GetHiddenValue(V8JS_SYM(PHPJS_OBJECT_KEY));
+		if (!php_object.IsEmpty()) {
+			zval *object = reinterpret_cast<zval *>(v8::External::Cast(*php_object)->Value());
 			RETVAL_ZVAL(object, 1, 0);
 			return SUCCESS;
 		}