Skip to content
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class V8Js

const FLAG_NONE = 1;
const FLAG_FORCE_ARRAY = 2;
const FLAG_PROPAGATE_PHP_EXCEPTIONS = 4;

const DEBUG_AUTO_BREAK_NEVER = 1;
const DEBUG_AUTO_BREAK_ONCE = 2;
Expand Down Expand Up @@ -297,3 +298,28 @@ PHP Objects implementing ArrayAccess, Countable
The above rule that PHP objects are generally converted to JavaScript objects also applies to PHP objects of `ArrayObject` type or other classes, that implement both the `ArrayAccess` and the `Countable` interface -- even so they behave like PHP arrays.

This behaviour can be changed by enabling the php.ini flag `v8js.use_array_access`. If set, objects of PHP classes that implement the aforementioned interfaces are converted to JavaScript Array-like objects. This is by-index access of this object results in immediate calls to the `offsetGet` or `offsetSet` PHP methods (effectively this is live-binding of JavaScript against the PHP object). Such an Array-esque object also supports calling every attached public method of the PHP object + methods of JavaScript's native Array.prototype methods (as long as they are not overloaded by PHP methods).

Exceptions
==========

If the JavaScript code throws (without catching), causes errors or doesn't
compile, `V8JsScriptException` exceptions are thrown unless the `V8Js` object
is constructed with `report_uncaught_exceptions` set `FALSE`.

PHP exceptions that occur due to calls from JavaScript code by default are
*not* re-thrown into JavaScript context but cause the JavaScript execution to
be stopped immediately and then are reported at the location calling the JS code.

This behaviour can be changed by setting the `FLAG_PROPAGATE_PHP_EXCEPTIONS`
flag. If it is set, PHP exception (objects) are converted to JavaScript
objects obeying the above rules and re-thrown in JavaScript context. If they
are not caught by JavaScript code the execution stops and a
`V8JsScriptException` is thrown, which has the original PHP exception accessible
via `getPrevious` method.

V8Js versions 0.2.4 and before did not stop JS code execution on PHP exceptions,
but silently ignored them (even so succeeding PHP calls from within the same piece
of JS code were not executed by the PHP engine). This behaviour is considered as
a bug and hence was fixed with 0.2.5 release. Nevertheless there is a
compatibility php.ini switch (`v8js.compat_php_exceptions`) which turns previous
behaviour back on.
6 changes: 2 additions & 4 deletions php_v8js_macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,10 @@ extern "C" {
# define V8JS_CONST (char *)
#endif

/* Global flags */
#define V8JS_GLOBAL_SET_FLAGS(isolate,flags) V8JS_GLOBAL(isolate)->SetHiddenValue(V8JS_SYM("__php_flags__"), V8JS_INT(flags))
#define V8JS_GLOBAL_GET_FLAGS(isolate) V8JS_GLOBAL(isolate)->GetHiddenValue(V8JS_SYM("__php_flags__"))->IntegerValue();

/* Options */
#define V8JS_FLAG_NONE (1<<0)
#define V8JS_FLAG_FORCE_ARRAY (1<<1)
#define V8JS_FLAG_PROPAGATE_PHP_EXCEPTIONS (1<<2)

#define V8JS_DEBUG_AUTO_BREAK_NEVER 0
#define V8JS_DEBUG_AUTO_BREAK_ONCE 1
Expand Down Expand Up @@ -124,6 +121,7 @@ ZEND_BEGIN_MODULE_GLOBALS(v8js)
char *v8_flags; /* V8 command line flags */
bool use_date; /* Generate JS Date objects instead of PHP DateTime */
bool use_array_access; /* Convert ArrayAccess, Countable objects to array-like objects */
bool compat_php_exceptions; /* Don't stop JS execution on PHP exception */

// Timer thread globals
std::deque<v8js_timer_ctx *> timer_stack;
Expand Down
8 changes: 6 additions & 2 deletions tests/exception_propagation_2.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ class Foo {
$this->v8->foo = $this;
$this->v8->executeString('fooobar', 'throw_0');
var_dump($this->v8->getPendingException());
// the exception is not cleared before the next executeString call,
// hence the next *exiting* executeString will throw.
// In this case this is the executeString call in bar() function.
$this->v8->executeString('try { PHP.foo.bar(); } catch (e) { print(e + " caught!\n"); }', 'trycatch1');
$this->v8->executeString('try { PHP.foo.bar(); } catch (e) { print(e + " caught!\n"); }', 'trycatch2');
}

public function bar()
{
echo "To Bar!\n";
// This executeString call throws a PHP exception, not propagated
// to JS, hence immediately triggering the top-level catch handler.
$this->v8->executeString('throw new Error();', 'throw_1');
}
}
Expand Down Expand Up @@ -71,7 +76,7 @@ object(V8JsScriptException)#%d (13) {
["file"]=>
string(%d) "%s"
["line"]=>
int(24)
int(29)
["function"]=>
string(11) "__construct"
["class"]=>
Expand Down Expand Up @@ -100,6 +105,5 @@ object(V8JsScriptException)#%d (13) {
at throw_0:1:1"
}
To Bar!
Error caught!
PHP Exception: throw_0:1: ReferenceError: fooobar is not defined
===EOF===
33 changes: 33 additions & 0 deletions tests/issue_156_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
--TEST--
Test V8::executeString() : Backwards compatibility for issue #156
--SKIPIF--
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
--INI--
v8js.compat_php_exceptions = 1
--FILE--
<?php

$v8 = new V8Js();

$v8->throwPHPException = function () {
echo "throwing PHP exception now ...\n";
throw new \Exception('foo');
};

$JS = <<< EOT
PHP.throwPHPException();
print("... old behaviour was to not stop JS execution on PHP exceptions\\n");
EOT;

try {
$v8->executeString($JS, 'issue_156_001.js');
} catch(Exception $e) {
var_dump($e->getMessage());
}
?>
===EOF===
--EXPECT--
throwing PHP exception now ...
... old behaviour was to not stop JS execution on PHP exceptions
string(3) "foo"
===EOF===
50 changes: 50 additions & 0 deletions tests/php_exceptions_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
--TEST--
Test V8::executeString() : PHP Exception handling (repeated)
--SKIPIF--
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
--FILE--
<?php

class Foo {
function throwException() {
throw new \Exception("Test-Exception");
}
}

$v8 = new V8Js();
$v8->foo = new \Foo();

$JS = <<< EOT
try {
PHP.foo.throwException();
// the exception should abort further execution,
// hence the print must not pop up
print("after throwException\\n");
} catch(e) {
// JS should not catch in default mode
print("JS caught exception");
}
EOT;

for($i = 0; $i < 5; $i ++) {
var_dump($i);
try {
$v8->executeString($JS);
} catch (Exception $e) {
var_dump($e->getMessage());
}
}
?>
===EOF===
--EXPECTF--
int(0)
string(14) "Test-Exception"
int(1)
string(14) "Test-Exception"
int(2)
string(14) "Test-Exception"
int(3)
string(14) "Test-Exception"
int(4)
string(14) "Test-Exception"
===EOF===
67 changes: 67 additions & 0 deletions tests/php_exceptions_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
--TEST--
Test V8::executeString() : PHP Exception handling (multi-level)
--SKIPIF--
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
--FILE--
<?php

class Foo {
function throwException() {
throw new \Exception("Test-Exception");
}

function recurse($i) {
echo "recurse[$i] ...\n";
global $work;
$work($i);
}
}

$v8 = new V8Js();
$v8->foo = new \Foo();

$work = $v8->executeString(<<<EOT
var work = function(level) {
if(level--) {
PHP.foo.recurse(level);
}
else {
PHP.foo.throwException();
}
};
work;
EOT
);

for($i = 0; $i < 5; $i ++) {
var_dump($i);
try {
$work($i);
} catch (Exception $e) {
var_dump($e->getMessage());
}
}
?>
===EOF===
--EXPECT--
int(0)
string(14) "Test-Exception"
int(1)
recurse[0] ...
string(14) "Test-Exception"
int(2)
recurse[1] ...
recurse[0] ...
string(14) "Test-Exception"
int(3)
recurse[2] ...
recurse[1] ...
recurse[0] ...
string(14) "Test-Exception"
int(4)
recurse[3] ...
recurse[2] ...
recurse[1] ...
recurse[0] ...
string(14) "Test-Exception"
===EOF===
36 changes: 36 additions & 0 deletions tests/php_exceptions_003.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
--TEST--
Test V8::executeString() : PHP Exception handling (basic JS propagation)
--SKIPIF--
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
--FILE--
<?php

class Foo {
function throwException() {
throw new \Exception("Test-Exception");
}
}

$v8 = new V8Js();
$v8->foo = new \Foo();

$JS = <<< EOT
try {
PHP.foo.throwException();
// the exception should abort further execution,
// hence the print must not pop up
print("after throwException\\n");
} catch(e) {
print("JS caught exception!\\n");
var_dump(e.getMessage());
}
EOT;

$v8->executeString($JS, 'php_exceptions_003', V8Js::FLAG_PROPAGATE_PHP_EXCEPTIONS);

?>
===EOF===
--EXPECTF--
JS caught exception!
string(14) "Test-Exception"
===EOF===
36 changes: 36 additions & 0 deletions tests/php_exceptions_004.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
--TEST--
Test V8::executeString() : PHP Exception handling (PHP->JS->PHP back propagation)
--SKIPIF--
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
--FILE--
<?php

class Foo {
function throwException() {
throw new \Exception("Test-Exception");
}
}

$v8 = new V8Js();
$v8->foo = new \Foo();

$JS = <<< EOT
PHP.foo.throwException();
// the exception should abort further execution,
// hence the print must not pop up
print("after throwException\\n");
EOT;

try {
$v8->executeString($JS, 'php_exceptions_004', V8Js::FLAG_PROPAGATE_PHP_EXCEPTIONS);
}
catch(V8JsScriptException $e) {
echo "Got V8JsScriptException\n";
var_dump($e->getPrevious()->getMessage());
}
?>
===EOF===
--EXPECTF--
Got V8JsScriptException
string(14) "Test-Exception"
===EOF===
43 changes: 43 additions & 0 deletions tests/php_exceptions_005.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
--TEST--
Test V8::executeString() : PHP Exception handling (JS throw PHP-exception)
--SKIPIF--
<?php require_once(dirname(__FILE__) . '/skipif.inc'); ?>
--FILE--
<?php

class Foo {
function getException() {
return new \Exception("Test-Exception");
}
}

$v8 = new V8Js();
$v8->foo = new \Foo();

$JS = <<< EOT
var ex = PHP.foo.getException();
print("after getException\\n");
throw ex;
print("after throw\\n");
EOT;

try {
$v8->executeString($JS, 'php_exceptions_005');
}
catch(V8JsScriptException $e) {
echo "Got V8JsScriptException\n";
var_dump($e->getMessage());
var_dump($e->getPrevious()->getMessage());
}
?>
===EOF===
--EXPECTF--
after getException
Got V8JsScriptException
string(%d) "php_exceptions_005:3: exception 'Exception' with message 'Test-Exception' in %s
Stack trace:
#0 [internal function]: Foo->getException()
#1 %s: V8Js->executeString('var ex = PHP.fo...', 'php_exceptions_...')
#2 {main}"
string(14) "Test-Exception"
===EOF===
Loading