|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace Interop\Async\Promise; |
| 4 | + |
| 5 | +use Interop\Async\Promise; |
| 6 | + |
| 7 | +/** |
| 8 | + * Global error handler for promises. |
| 9 | + * |
| 10 | + * Callbacks passed to `Promise::when()` should never throw, but they might. Such errors have to be passed to this |
| 11 | + * global error handler to make them easily loggable. These can't be handled gracefully in any way, so we just enable |
| 12 | + * 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. |
| 16 | + */ |
| 17 | +final class ErrorHandler |
| 18 | +{ |
| 19 | + /** @var callable|null */ |
| 20 | + private static $callback = null; |
| 21 | + |
| 22 | + private function __construct() |
| 23 | + { |
| 24 | + // disable construction, only static helper |
| 25 | + } |
| 26 | + |
| 27 | + /** |
| 28 | + * Set a new handler that will be notified on uncaught errors during promise resolution callback invocations. |
| 29 | + * |
| 30 | + * This callback can attempt to log the error or exit the execution of the script if it sees need. It receives the |
| 31 | + * exception as first and only parameter. |
| 32 | + * |
| 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. |
| 36 | + * |
| 37 | + * @param callable|null $onError Callback to invoke on errors or `null` to reset. |
| 38 | + * |
| 39 | + * @return callable|null Previous callback. |
| 40 | + */ |
| 41 | + public static function set(callable $onError = null) |
| 42 | + { |
| 43 | + $previous = self::$callback; |
| 44 | + self::$callback = $onError; |
| 45 | + return $previous; |
| 46 | + } |
| 47 | + |
| 48 | + /** |
| 49 | + * Notifies the registered handler, that an exception occurred. |
| 50 | + * |
| 51 | + * This method MUST be called by every promise implementation if a callback passed to `Promise::when()` throws upon |
| 52 | + * invocation. It MUST NOT be called otherwise. |
| 53 | + */ |
| 54 | + public static function notify($error) |
| 55 | + { |
| 56 | + // No type declaration, because of PHP 5 + PHP 7 support. |
| 57 | + if (!$error instanceof \Exception && !$error instanceof \Throwable) { |
| 58 | + // We have this error handler specifically so we never throw from Promise::when, so it doesn't make sense to |
| 59 | + // throw here. We just forward a generic exception to the registered handlers. |
| 60 | + $error = new \Exception(sprintf( |
| 61 | + "Promise implementation called %s with an invalid argument of type '%s'", |
| 62 | + __METHOD__, |
| 63 | + is_object($error) ? get_class($error) : gettype($error) |
| 64 | + )); |
| 65 | + } |
| 66 | + |
| 67 | + if (self::$callback === null) { |
| 68 | + trigger_error( |
| 69 | + "An exception has been thrown from an Interop\\Async\\Promise::when handler, but no handler has been" |
| 70 | + . " registered via Interop\\Async\\Promise\\ErrorHandler::set. A handler has to be registered to" |
| 71 | + . " prevent exceptions from going unnoticed. Do NOT install an empty handler that just" |
| 72 | + . " does nothing. If the handler is called, there is ALWAYS something wrong.\n\n" . (string) $error, |
| 73 | + E_USER_ERROR |
| 74 | + ); |
| 75 | + |
| 76 | + return; |
| 77 | + } |
| 78 | + |
| 79 | + try { |
| 80 | + \call_user_func(self::$callback, $error); |
| 81 | + } catch (\Exception $e) { |
| 82 | + // We're already a last chance handler, throwing doesn't make sense, so use a real fatal |
| 83 | + trigger_error(sprintf( |
| 84 | + "An exception has been thrown from the promise error handler registered to %s::set().\n\n%s", |
| 85 | + __CLASS__, |
| 86 | + (string) $e |
| 87 | + ), E_USER_ERROR); |
| 88 | + } catch (\Throwable $e) { |
| 89 | + // We're already a last chance handler, throwing doesn't make sense, so use a real fatal |
| 90 | + trigger_error(sprintf( |
| 91 | + "An exception has been thrown from the promise error handler registered to %s::set().\n\n%s", |
| 92 | + __CLASS__, |
| 93 | + (string) $e |
| 94 | + ), E_USER_ERROR); |
| 95 | + } |
| 96 | + } |
| 97 | +} |
0 commit comments