diff --git a/Zend/zend.c b/Zend/zend.c
index cbd5aef42b9fe..4916c6bb05994 100644
--- a/Zend/zend.c
+++ b/Zend/zend.c
@@ -1323,6 +1323,21 @@ static ZEND_COLD void zend_error_impl(
zend_observer_error_notify(type, error_filename, error_lineno, message);
+ /* Call all the observer end handlers for fatal errors */
+ if (ZEND_OBSERVER_ENABLED && (type & E_FATAL_ERRORS) && EG(current_execute_data)) {
+ zend_execute_data *ex = EG(current_execute_data);
+ do {
+ if (ex->func->type != ZEND_INTERNAL_FUNCTION) {
+ zend_observer_fcall_end(ex, NULL);
+ /* If an extension catches a fatal error and raises another one, the
+ * observer end handlers will fire more than once. This requires us
+ * to "unobserve" the functions after the end handlers have fired the
+ * first time. */
+ zend_observer_fcall_unobserve(ex->func);
+ }
+ } while ((ex = ex->prev_execute_data) != NULL);
+ }
+
/* if we don't have a user defined error handler */
if (Z_TYPE(EG(user_error_handler)) == IS_UNDEF
|| !(EG(user_error_handler_error_reporting) & type)
diff --git a/Zend/zend_observer.c b/Zend/zend_observer.c
index 9c2d1cdf51c4e..ac8653f931a9c 100644
--- a/Zend/zend_observer.c
+++ b/Zend/zend_observer.c
@@ -210,6 +210,15 @@ ZEND_API void ZEND_FASTCALL zend_observer_fcall_end(
}
}
+ZEND_API void zend_observer_fcall_unobserve(zend_function *func)
+{
+ if (ZEND_OBSERVER_ENABLED
+ && ZEND_OBSERVABLE_FN(func->common.fn_flags)
+ && ZEND_OBSERVER_DATA(&func->op_array)) {
+ ZEND_OBSERVER_DATA(&func->op_array) = ZEND_OBSERVER_NOT_OBSERVED;
+ }
+}
+
ZEND_API void zend_observer_error_register(zend_observer_error_cb cb)
{
zend_llist_add_element(&zend_observer_error_callbacks, &cb);
diff --git a/Zend/zend_observer.h b/Zend/zend_observer.h
index 1d20306a17018..997d8a25dd14c 100644
--- a/Zend/zend_observer.h
+++ b/Zend/zend_observer.h
@@ -70,6 +70,8 @@ ZEND_API void ZEND_FASTCALL zend_observer_fcall_end(
zend_execute_data *execute_data,
zval *return_value);
+ZEND_API void zend_observer_fcall_unobserve(zend_function *func);
+
typedef void (*zend_observer_error_cb)(int type, const char *error_filename, uint32_t error_lineno, zend_string *message);
ZEND_API void zend_observer_error_register(zend_observer_error_cb callback);
diff --git a/ext/zend_test/tests/observer_call_user_func_01.phpt b/ext/zend_test/tests/observer_call_user_func_01.phpt
new file mode 100644
index 0000000000000..0f12fa6835f00
--- /dev/null
+++ b/ext/zend_test/tests/observer_call_user_func_01.phpt
@@ -0,0 +1,40 @@
+--TEST--
+Observer: call_user_func() from root namespace
+--SKIPIF--
+
+--INI--
+zend_test.observer.enabled=1
+zend_test.observer.observe_all=1
+--FILE--
+
+--EXPECTF--
+
+
+
+
+MyClass::myMethod called
+
+
+
+my_function called
+
+
diff --git a/ext/zend_test/tests/observer_call_user_func_02.phpt b/ext/zend_test/tests/observer_call_user_func_02.phpt
new file mode 100644
index 0000000000000..28dd6a9825440
--- /dev/null
+++ b/ext/zend_test/tests/observer_call_user_func_02.phpt
@@ -0,0 +1,40 @@
+--TEST--
+Observer: call_user_func_array() from root namespace
+--SKIPIF--
+
+--INI--
+zend_test.observer.enabled=1
+zend_test.observer.observe_all=1
+--FILE--
+
+--EXPECTF--
+
+
+
+
+MyClass::myMethod called
+
+
+
+my_function called
+
+
diff --git a/ext/zend_test/tests/observer_call_user_func_03.phpt b/ext/zend_test/tests/observer_call_user_func_03.phpt
new file mode 100644
index 0000000000000..1ff841d4340d5
--- /dev/null
+++ b/ext/zend_test/tests/observer_call_user_func_03.phpt
@@ -0,0 +1,39 @@
+--TEST--
+Observer: call_user_func() from namespace
+--SKIPIF--
+
+--INI--
+zend_test.observer.enabled=1
+zend_test.observer.observe_all=1
+--FILE--
+
+--EXPECTF--
+
+
+
+
+MyClass::myMethod called
+
+
+
+my_function called
+
+
diff --git a/ext/zend_test/tests/observer_call_user_func_04.phpt b/ext/zend_test/tests/observer_call_user_func_04.phpt
new file mode 100644
index 0000000000000..9df131db4dd0d
--- /dev/null
+++ b/ext/zend_test/tests/observer_call_user_func_04.phpt
@@ -0,0 +1,39 @@
+--TEST--
+Observer: call_user_func_array() from namespace
+--SKIPIF--
+
+--INI--
+zend_test.observer.enabled=1
+zend_test.observer.observe_all=1
+--FILE--
+
+--EXPECTF--
+
+
+
+
+MyClass::myMethod called
+
+
+
+my_function called
+
+
diff --git a/ext/zend_test/tests/observer_error_01.phpt b/ext/zend_test/tests/observer_error_01.phpt
new file mode 100644
index 0000000000000..08de38f601156
--- /dev/null
+++ b/ext/zend_test/tests/observer_error_01.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Observer: End handlers fire after a fatal error
+--SKIPIF--
+
+--INI--
+zend_test.observer.enabled=1
+zend_test.observer.observe_all=1
+zend_test.observer.show_return_value=1
+memory_limit=1M
+--FILE--
+
+--EXPECTF--
+
+
+
+
+
+
+
+Fatal error: Allowed memory size of 2097152 bytes exhausted%s(tried to allocate %d bytes) in %s on line %d
diff --git a/ext/zend_test/tests/observer_error_02.phpt b/ext/zend_test/tests/observer_error_02.phpt
new file mode 100644
index 0000000000000..70e2a57a50f9c
--- /dev/null
+++ b/ext/zend_test/tests/observer_error_02.phpt
@@ -0,0 +1,28 @@
+--TEST--
+Observer: End handlers fire after a userland fatal error
+--SKIPIF--
+
+--INI--
+zend_test.observer.enabled=1
+zend_test.observer.observe_all=1
+zend_test.observer.show_return_value=1
+--FILE--
+
+--EXPECTF--
+
+
+
+
+
+
+
+Fatal error: Foo error in %s on line %d
diff --git a/ext/zend_test/tests/observer_error_03.phpt b/ext/zend_test/tests/observer_error_03.phpt
new file mode 100644
index 0000000000000..3d8150a440754
--- /dev/null
+++ b/ext/zend_test/tests/observer_error_03.phpt
@@ -0,0 +1,39 @@
+--TEST--
+Observer: non-fatal errors do not fire end handlers prematurely
+--SKIPIF--
+
+--INI--
+zend_test.observer.enabled=1
+zend_test.observer.observe_all=1
+zend_test.observer.show_return_value=1
+--FILE--
+
+--EXPECTF--
+
+
+
+
+
+
+
+Warning: Undefined variable $this_does_not_exit in %s on line %d
+
+After error.
+
+Done.
+
diff --git a/ext/zend_test/tests/observer_error_04.phpt b/ext/zend_test/tests/observer_error_04.phpt
new file mode 100644
index 0000000000000..8aea004187e80
--- /dev/null
+++ b/ext/zend_test/tests/observer_error_04.phpt
@@ -0,0 +1,44 @@
+--TEST--
+Observer: fatal errors caught with zend_try will fire end handlers once
+--SKIPIF--
+
+
+--INI--
+zend_test.observer.enabled=1
+zend_test.observer.observe_all=1
+zend_test.observer.show_return_value=1
+--FILE--
+getMessage() . PHP_EOL;
+}
+
+echo 'Done.' . PHP_EOL;
+?>
+--EXPECTF--
+
+
+
+
+
+
+
+
+
+SOAP-ERROR: Parsing WSDL: Couldn't load from 'foo' : failed to load external entity "foo"
+
+Done.
diff --git a/ext/zend_test/tests/observer_generator_05.phpt b/ext/zend_test/tests/observer_generator_05.phpt
new file mode 100644
index 0000000000000..d13f6fa39b198
--- /dev/null
+++ b/ext/zend_test/tests/observer_generator_05.phpt
@@ -0,0 +1,53 @@
+--TEST--
+Observer: Generator with uncaught exception
+--SKIPIF--
+
+--INI--
+zend_test.observer.enabled=1
+zend_test.observer.observe_all=1
+zend_test.observer.show_return_value=1
+--FILE--
+
+--EXPECTF--
+
+
+
+
+
+
+
+0
+
+
+1
+
+
+
+
+
+
+
+
+Fatal error: Uncaught RuntimeException: Oops! in %s/observer_generator_%d.php:%d
+Stack trace:
+#0 %s/observer_generator_%d.php(%d): fooResults()
+#1 %s/observer_generator_%d.php(%d): doSomething()
+#2 {main}
+ thrown in %s/observer_generator_%d.php on line %d