Skip to content

Commit 341ac65

Browse files
committed
Change error handler to a single callback
1 parent be876a0 commit 341ac65

File tree

8 files changed

+60
-86
lines changed

8 files changed

+60
-86
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ Any implementation MUST at least provide these two parameters. The implementatio
7272

7373
> **NOTE:** The signature doesn't specify a type for `$error`. This is due to the new `Throwable` interface introduced in PHP 7. As this specification is PHP 5 compatible, we can use neither `Throwable` nor `Exception`.
7474
75-
All registered callbacks MUST be executed in the order they were registered. If one of the callbacks throws an `Exception` or `Throwable`, it MUST be forwarded to `Async\Interop\PromiseErrorHandler::notify`. The `Promise` implementation MUST then continue to call the remaining callbacks with the original parameters.
75+
All registered callbacks MUST be executed in the order they were registered. If one of the callbacks throws an `Exception` or `Throwable`, it MUST be forwarded to `Async\Interop\Promise\ErrorHandler::notify`. The `Promise` implementation MUST then continue to call the remaining callbacks with the original parameters.
7676

7777
If a `Promise` is resolved with another `Promise`, the `Promise` MUST keep in pending state until the passed `Promise` is resolved. Thus, the value of a `Promise` can never be a `Promise`.
7878

src/Promise/ErrorHandler.php

Lines changed: 39 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -10,70 +10,44 @@
1010
* Callbacks passed to `Promise::when()` should never throw, but they might. Such errors have to be passed to this
1111
* global error handler to make them easily loggable. These can't be handled gracefully in any way, so we just enable
1212
* logging with this handler and ignore them otherwise.
13+
*
14+
* If handler is set or that handler rethrows, it will fail hard by triggering an E_USER_ERROR leading to script
15+
* abortion.
1316
*/
1417
final class ErrorHandler
1518
{
16-
/** @var callable[] */
17-
private static $callbacks = [];
19+
/** @var callable|null */
20+
private static $callback = null;
1821

1922
private function __construct()
2023
{
2124
// disable construction, only static helper
2225
}
2326

2427
/**
25-
* Adds a new handler that will be notified on uncaught promise resolution callback errors.
28+
* Set a new handler that will be notified on uncaught errors during promise resolution callback invocations.
2629
*
2730
* This callback can attempt to log the error or exit the execution of the script if it sees need. It receives the
2831
* exception as first and only parameter.
2932
*
30-
* This callable MUST NOT throw in any case, as it's already a last chance handler. Thus it's suggested to always
31-
* wrap the body of your callback in a generic `try` / `catch` block.
32-
*
33-
* @return int Returns an integer identifier which allows to remove the handler again.
34-
*/
35-
public static function add(callable $onError)
36-
{
37-
self::$callbacks[] = $onError;
38-
end(self::$callbacks);
39-
return key(self::$callbacks);
40-
}
41-
42-
/**
43-
* Removes the handler with the specified identifier.
33+
* As it's already a last chance handler, the script will be aborted using E_USER_ERROR if the handler throws. Thus
34+
* it's suggested to always wrap the body of your callback in a generic `try` / `catch` block, if you want to avoid
35+
* that.
4436
*
45-
* @return bool Returns `true` if the handler existed, `false` otherwise.
46-
*/
47-
public static function remove($id)
48-
{
49-
if (!is_int($id)) {
50-
throw new \InvalidArgumentException(sprintf(
51-
"The provided identifier isn't an integer, %s provided.",
52-
is_object($id) ? get_class($id) : gettype($id)
53-
));
54-
}
55-
56-
$exists = array_key_exists($id, self::$callbacks);
57-
unset(self::$callbacks[$id]);
58-
return $exists;
59-
}
60-
61-
/**
62-
* Removes all handlers.
37+
* @param callable|null $onError Callback to invoke on errors or `null` to reset.
6338
*
64-
* This method should usually not be used, but it may be helpful in unit tests to start from a clean state if
65-
* certain handlers do not clean up after themselves.
39+
* @return void
6640
*/
67-
public static function reset()
41+
public static function set(callable $onError = null)
6842
{
69-
self::$callbacks = [];
43+
self::$callback = $onError;
7044
}
7145

7246
/**
73-
* Notifies all registered handlers, that an exception occurred.
47+
* Notifies the registered handler, that an exception occurred.
7448
*
7549
* This method MUST be called by every promise implementation if a callback passed to `Promise::when()` throws upon
76-
* invocation.
50+
* invocation. It MUST NOT be called otherwise.
7751
*/
7852
public static function notify($error)
7953
{
@@ -88,34 +62,35 @@ public static function notify($error)
8862
));
8963
}
9064

91-
foreach (self::$callbacks as $callback) {
92-
try {
93-
$callback($error);
94-
} catch (\Exception $e) {
95-
// We're already a last chance handler, throwing doesn't make sense, so use a real fatal
96-
trigger_error(sprintf(
97-
"An exception has been thrown in a handler registered to %s:\n%s",
98-
__CLASS__,
99-
(string) $e
100-
), E_USER_ERROR);
101-
} catch (\Throwable $e) {
102-
// We're already a last chance handler, throwing doesn't make sense, so use a real fatal
103-
trigger_error(sprintf(
104-
"An exception has been thrown in a handler registered to %s:\n%s",
105-
__CLASS__,
106-
(string) $e
107-
), E_USER_ERROR);
108-
}
109-
}
110-
111-
if (empty(self::$callbacks)) {
65+
if (self::$callback === null) {
11266
trigger_error(
11367
"An exception has been thrown from an Interop\\Async\\Promise::when handler, but no handler has been"
114-
. " registered via Interop\\Async\\Promise\\ErrorHandler::add. At least one handler has to be"
115-
. " registered to prevent exceptions from going unnoticed. Do NOT install an empty handler that just"
68+
. " registered via Interop\\Async\\Promise\\ErrorHandler::set. A handler has to be registered to"
69+
. " prevent exceptions from going unnoticed. Do NOT install an empty handler that just"
11670
. " does nothing. If the handler is called, there is ALWAYS something wrong.\n\n" . (string) $error,
11771
E_USER_ERROR
11872
);
73+
74+
return;
75+
}
76+
77+
try {
78+
$callback = self::$callback;
79+
$callback($error);
80+
} catch (\Exception $e) {
81+
// We're already a last chance handler, throwing doesn't make sense, so use a real fatal
82+
trigger_error(sprintf(
83+
"An exception has been thrown from the promise error handler registered to %s::set().\n\n%s",
84+
__CLASS__,
85+
(string) $e
86+
), E_USER_ERROR);
87+
} catch (\Throwable $e) {
88+
// We're already a last chance handler, throwing doesn't make sense, so use a real fatal
89+
trigger_error(sprintf(
90+
"An exception has been thrown from the promise error handler registered to %s::set().\n\n%s",
91+
__CLASS__,
92+
(string) $e
93+
), E_USER_ERROR);
11994
}
12095
}
12196
}

test/phpt/error_handler_001.phpt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ Interop\Async\Promise\ErrorHandler::notify(new Exception);
99

1010
?>
1111
--EXPECTF--
12-
Fatal error: An exception has been thrown from an Interop\Async\Promise::when handler, but no handler has been registered via Interop\Async\Promise\ErrorHandler::add. At least one handler has to be registered to prevent exceptions from going unnoticed. Do NOT install an empty handler that just does nothing. If the handler is called, there is ALWAYS something wrong.
12+
Fatal error: An exception has been thrown from an Interop\Async\Promise::when handler, but no handler has been registered via Interop\Async\Promise\ErrorHandler::set. A handler has to be registered to prevent exceptions from going unnoticed. Do NOT install an empty handler that just does nothing. If the handler is called, there is ALWAYS something wrong.
1313

1414
%s in %s:%d
1515
Stack trace:
16-
#0 {main} in %s on line %d
16+
#0 {main} in %s on line %d

test/phpt/error_handler_002.phpt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ ErrorHandler::notify() does not fatal with a handler
55

66
require __DIR__ . "/../../vendor/autoload.php";
77

8-
Interop\Async\Promise\ErrorHandler::add(function () { print "1"; });
8+
Interop\Async\Promise\ErrorHandler::set(function () { print "1"; });
99
Interop\Async\Promise\ErrorHandler::notify(new Exception);
1010

1111
?>
1212
--EXPECT--
13-
1
13+
1

test/phpt/error_handler_003.phpt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
--TEST--
2-
ErrorHandler::notify() fatals after handlers have been removed with ErrorHandler::reset()
2+
ErrorHandler::notify() fatals after handlers have been removed with ErrorHandler::set(null)
33
--FILE--
44
<?php
55

66
require __DIR__ . "/../../vendor/autoload.php";
77

8-
Interop\Async\Promise\ErrorHandler::add(function () { print "1"; });
9-
Interop\Async\Promise\ErrorHandler::reset();
8+
Interop\Async\Promise\ErrorHandler::set(function () { print "1"; });
9+
Interop\Async\Promise\ErrorHandler::set(null);
1010
Interop\Async\Promise\ErrorHandler::notify(new Exception);
1111

1212
?>
1313
--EXPECTF--
14-
Fatal error: An exception has been thrown from an Interop\Async\Promise::when handler, but no handler has been registered via Interop\Async\Promise\ErrorHandler::add. At least one handler has to be registered to prevent exceptions from going unnoticed. Do NOT install an empty handler that just does nothing. If the handler is called, there is ALWAYS something wrong.
14+
Fatal error: An exception has been thrown from an Interop\Async\Promise::when handler, but no handler has been registered via Interop\Async\Promise\ErrorHandler::set. A handler has to be registered to prevent exceptions from going unnoticed. Do NOT install an empty handler that just does nothing. If the handler is called, there is ALWAYS something wrong.
1515

1616
%s in %s:%d
1717
Stack trace:
18-
#0 {main} in %s on line %d
18+
#0 {main} in %s on line %d

test/phpt/error_handler_004.phpt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ Interop\Async\Promise\ErrorHandler::notify(42);
99

1010
?>
1111
--EXPECTF--
12-
Fatal error: An exception has been thrown from an Interop\Async\Promise::when handler, but no handler has been registered via Interop\Async\Promise\ErrorHandler::add. At least one handler has to be registered to prevent exceptions from going unnoticed. Do NOT install an empty handler that just does nothing. If the handler is called, there is ALWAYS something wrong.
12+
Fatal error: An exception has been thrown from an Interop\Async\Promise::when handler, but no handler has been registered via Interop\Async\Promise\ErrorHandler::set. A handler has to be registered to prevent exceptions from going unnoticed. Do NOT install an empty handler that just does nothing. If the handler is called, there is ALWAYS something wrong.
1313

1414
%SException%SPromise implementation called Interop\Async\Promise\ErrorHandler::notify with an invalid argument of type 'integer'%S in %s:%d
1515
Stack trace:
1616
#0 %s(%d): Interop\Async\Promise\ErrorHandler::notify(%S)
17-
#1 {main} in %s on line %d
17+
#1 {main} in %s on line %d

test/phpt/error_handler_005.phpt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
--TEST--
2-
ErrorHandler::notify() does call handlers in order
2+
ErrorHandler::set() replaces the current handler
33
--FILE--
44
<?php
55

66
require __DIR__ . "/../../vendor/autoload.php";
77

8-
Interop\Async\Promise\ErrorHandler::add(function () { print "1"; });
9-
Interop\Async\Promise\ErrorHandler::add(function () { print "2"; });
10-
Interop\Async\Promise\ErrorHandler::add(function () { print "3"; });
8+
Interop\Async\Promise\ErrorHandler::set(function () { print "1"; });
9+
Interop\Async\Promise\ErrorHandler::set(function () { print "2"; });
10+
Interop\Async\Promise\ErrorHandler::set(function () { print "3"; });
1111
Interop\Async\Promise\ErrorHandler::notify(new Exception);
1212

1313
?>
1414
--EXPECT--
15-
123
15+
3

test/phpt/error_handler_006.phpt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
--TEST--
2-
ErrorHandler::notify() fatals after handlers have been removed with ErrorHandler::remove()
2+
ErrorHandler::notify() fatals if handler throws
33
--FILE--
44
<?php
55

66
require __DIR__ . "/../../vendor/autoload.php";
77

8-
$id = Interop\Async\Promise\ErrorHandler::add(function () { print "1"; });
9-
Interop\Async\Promise\ErrorHandler::remove($id);
8+
Interop\Async\Promise\ErrorHandler::set(function ($e) { throw $e; });
109
Interop\Async\Promise\ErrorHandler::notify(new Exception);
1110

1211
?>
1312
--EXPECTF--
14-
Fatal error: An exception has been thrown from an Interop\Async\Promise::when handler, but no handler has been registered via Interop\Async\Promise\ErrorHandler::add. At least one handler has to be registered to prevent exceptions from going unnoticed. Do NOT install an empty handler that just does nothing. If the handler is called, there is ALWAYS something wrong.
13+
Fatal error: An exception has been thrown from the promise error handler registered to Interop\Async\Promise\ErrorHandler::set().
1514

1615
%s in %s:%d
1716
Stack trace:
18-
#0 {main} in %s on line %d
17+
#0 {main} in %s on line %d

0 commit comments

Comments
 (0)