From 5cdcf7752ebc07526f9dcc9fd9072592fc4348db Mon Sep 17 00:00:00 2001 From: Marco Stoll Date: Fri, 21 Jun 2019 11:43:14 +0200 Subject: [PATCH] [FEATURE] add the runtime event handlers --- src/Events/Runtime/Error.php | 107 ++++++++++ src/Events/Runtime/Exception.php | 42 ++++ src/Events/Runtime/Shutdown.php | 23 +++ src/Factories/ServicesFactory.php | 2 +- src/Services/Runtime/ErrorHandler.php | 153 ++++++++++++++ src/Services/Runtime/ExceptionHandler.php | 85 ++++++++ .../Runtime/RuntimeEventHandlerInterface.php | 26 +++ src/Services/Runtime/ShutdownHandler.php | 113 +++++++++++ tests/Services/Runtime/ErrorHandlerTest.php | 192 ++++++++++++++++++ .../Services/Runtime/ExceptionHandlerTest.php | 159 +++++++++++++++ .../Services/Runtime/ShutdownHandlerTest.php | 105 ++++++++++ 11 files changed, 1006 insertions(+), 1 deletion(-) create mode 100644 src/Events/Runtime/Error.php create mode 100644 src/Events/Runtime/Exception.php create mode 100644 src/Events/Runtime/Shutdown.php create mode 100644 src/Services/Runtime/ErrorHandler.php create mode 100644 src/Services/Runtime/ExceptionHandler.php create mode 100644 src/Services/Runtime/RuntimeEventHandlerInterface.php create mode 100644 src/Services/Runtime/ShutdownHandler.php create mode 100644 tests/Services/Runtime/ErrorHandlerTest.php create mode 100644 tests/Services/Runtime/ExceptionHandlerTest.php create mode 100644 tests/Services/Runtime/ShutdownHandlerTest.php diff --git a/src/Events/Runtime/Error.php b/src/Events/Runtime/Error.php new file mode 100644 index 0000000..9190022 --- /dev/null +++ b/src/Events/Runtime/Error.php @@ -0,0 +1,107 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Events\Runtime; + +use FF\Events\AbstractEvent; + +/** + * Class Error + * + * @package FF\Events\Runtime + */ +class Error extends AbstractEvent +{ + /** + * @var int + */ + protected $errNo; + + /** + * @var string + */ + protected $errMsg; + + /** + * @var string + */ + protected $errFile; + + /** + * @var int + */ + protected $errLine; + + /** + * @var array + */ + protected $errContext; + + /** + * @param int $errNo + * @param string $errMsg + * @param string $errFile + * @param int $errLine + * @param array $errContext + */ + public function __construct( + int $errNo, + string $errMsg, + string $errFile = '', + int $errLine = null, + array $errContext = [] + ) { + $this->errNo = $errNo; + $this->errMsg = $errMsg; + $this->errFile = $errFile; + $this->errLine = $errLine; + $this->errContext = $errContext; + } + + /** + * @return int + */ + public function getErrNo(): int + { + return $this->errNo; + } + + /** + * @return string + */ + public function getErrMsg(): string + { + return $this->errMsg; + } + + /** + * @return string + */ + public function getErrFile(): string + { + return $this->errFile; + } + + /** + * @return int + */ + public function getErrLine(): int + { + return $this->errLine; + } + + /** + * @return array + */ + public function getErrContext(): array + { + return $this->errContext; + } +} \ No newline at end of file diff --git a/src/Events/Runtime/Exception.php b/src/Events/Runtime/Exception.php new file mode 100644 index 0000000..9fdf0c8 --- /dev/null +++ b/src/Events/Runtime/Exception.php @@ -0,0 +1,42 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Events\Runtime; + +use FF\Events\AbstractEvent; + +/** + * Class Exception + * + * @package FF\Events\Runtime + */ +class Exception extends AbstractEvent +{ + /** + * @var \Throwable + */ + protected $exception; + + /** + * @param \Throwable $exception + */ + public function __construct(\Throwable $exception) + { + $this->exception = $exception; + } + + /** + * @return \Throwable + */ + public function getException(): \Throwable + { + return $this->exception; + } +} \ No newline at end of file diff --git a/src/Events/Runtime/Shutdown.php b/src/Events/Runtime/Shutdown.php new file mode 100644 index 0000000..c25278f --- /dev/null +++ b/src/Events/Runtime/Shutdown.php @@ -0,0 +1,23 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Events\Runtime; + +use FF\Events\AbstractEvent; + +/** + * Class Shutdown + * + * @package FF\Events\Runtime + */ +class Shutdown extends AbstractEvent +{ + +} \ No newline at end of file diff --git a/src/Factories/ServicesFactory.php b/src/Factories/ServicesFactory.php index 6f9dab0..d79d3df 100644 --- a/src/Factories/ServicesFactory.php +++ b/src/Factories/ServicesFactory.php @@ -88,7 +88,7 @@ class ServicesFactory extends AbstractSingletonFactory * Returns an array of service instances instead if two or more class identifiers were passed. The returned list * will ordered in the same way as the class identifier arguments have been passed. * - * @param string ...$classIdentifiers + * @param string[] $classIdentifiers * @return AbstractService|AbstractService[] * @throws ClassNotFoundException * @throws ConfigurationException diff --git a/src/Services/Runtime/ErrorHandler.php b/src/Services/Runtime/ErrorHandler.php new file mode 100644 index 0000000..3a42146 --- /dev/null +++ b/src/Services/Runtime/ErrorHandler.php @@ -0,0 +1,153 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Services\Runtime; + +use FF\Services\AbstractService; + +/** + * Class ErrorHandler + * + * Options: + * + * - error-types : int (default: E_ALL) - error type bit mask to trigger this handler + * - bypass-php-error-handling . bool (default: false) - whether to suppress php's built-in error handling + * + * @package FF\Services\Runtime + */ +class ErrorHandler extends AbstractService implements RuntimeEventHandlerInterface +{ + /** + * @var callable|null + */ + protected $previousHandler; + + /** + * @var int + */ + protected $errorTypes; + + /** + * @var bool + */ + protected $bypassPhpErrorHandling = false; + + /** + * {@inheritdoc} + * + * Replaces the currently registered error handler callback (if any). + * Stores any previously registered error handler callback. + * + * @see http://php.net/set_error_handler + */ + public function register() + { + $this->previousHandler = set_error_handler([$this, 'onError'], $this->errorTypes); + return $this; + } + + /** + * Retrieves the previous error handler callback before registration (if any) + * + * If register() wasn't called yet or no previous error handler callback was registered, null will be returned. + * + * @return callable|null + */ + public function getPreviousHandler(): ?callable + { + return $this->previousHandler; + } + + /** + * Restores the previous error handler (if any) + * + * @return $this + */ + public function restorePreviousHandler() + { + if (!is_callable($this->previousHandler)) return $this; + + set_error_handler($this->previousHandler); + $this->previousHandler = null; + + return $this; + } + + /** + * @return int + */ + public function getErrorTypes(): int + { + return $this->errorTypes; + } + + /** + * @param int $errorTypes + * @return $this + */ + public function setErrorTypes(int $errorTypes) + { + $this->errorTypes = $errorTypes; + return $this; + } + + /** + * @return bool + */ + public function getBypassPhpErrorHandling(): bool + { + return $this->bypassPhpErrorHandling; + } + + /** + * @param bool $bypassPhpErrorHandling + * @return $this + */ + public function setBypassPhpErrorHandling(bool $bypassPhpErrorHandling) + { + $this->bypassPhpErrorHandling = $bypassPhpErrorHandling; + return $this; + } + + /** + * Generic error handler callback + * + * @param int $errNo + * @param string $errMsg + * @param string $errFile + * @param int $errLine + * @param array $errContext + * @return boolean + * @fires Runtime\Error + * @see http://php.net/set_error_handler + */ + public function onError( + int $errNo, + string $errMsg, + string $errFile = '', + int $errLine = null, + array $errContext = [] + ): bool { + $this->fire('Runtime\Error', $errNo, $errMsg, $errFile, $errLine, $errContext); + + return $this->bypassPhpErrorHandling; + } + + /** + * {@inheritDoc} + */ + protected function initialize(array $options) + { + parent::initialize($options); + + $this->errorTypes = $this->getOption('error-types', E_ALL); + $this->bypassPhpErrorHandling = $this->getOption('bypass-php-error-handling', false); + } +} \ No newline at end of file diff --git a/src/Services/Runtime/ExceptionHandler.php b/src/Services/Runtime/ExceptionHandler.php new file mode 100644 index 0000000..9755d51 --- /dev/null +++ b/src/Services/Runtime/ExceptionHandler.php @@ -0,0 +1,85 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Services\Runtime; + +use FF\Services\AbstractService; + +/** + * Class ErrorHandler + * + * @package FF\Runtime + */ +class ExceptionHandler extends AbstractService implements RuntimeEventHandlerInterface +{ + /** + * @var callable|null + */ + protected $previousHandler; + + /** + * {@inheritdoc} + * + * @return $this + * @see http://php.net/set_exception_handler + */ + public function register() + { + $this->previousHandler = set_exception_handler([$this, 'onException']); + return $this; + } + + /** + * Retrieves the previous exception handler callback before registration (if any) + * + * If register() wasn't called yet or no previous exception handler callback was registered, null will be returned. + * + * @return callable|null + */ + public function getPreviousHandler(): ?callable + { + return $this->previousHandler; + } + + /** + * Restores the previous exception handler (if any) + * + * @return $this + */ + public function restorePreviousHandler() + { + if (!is_callable($this->previousHandler)) return $this; + + set_exception_handler($this->previousHandler); + $this->previousHandler = null; + + return $this; + } + + /** + * Generic exception handler callback + * + * To prevent potential loops any unhandled exception thrown while processing the Exception event + * is caught and discarded. + * + * @param \Throwable $e + * @fires Runtime\Exception + * @see http://php.net/set_exception_handler + */ + public function onException(\Throwable $e) + { + try { + $this->fire('Runtime\Exception', $e); + } catch (\Exception $e) { + // do not handle exceptions thrown while + // processing the Exception event + } + } +} \ No newline at end of file diff --git a/src/Services/Runtime/RuntimeEventHandlerInterface.php b/src/Services/Runtime/RuntimeEventHandlerInterface.php new file mode 100644 index 0000000..a53147e --- /dev/null +++ b/src/Services/Runtime/RuntimeEventHandlerInterface.php @@ -0,0 +1,26 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Services\Runtime; + +/** + * Interface RuntimeEventHandlerInterface + * + * @package FF\Services\Runtime + */ +interface RuntimeEventHandlerInterface +{ + /** + * Registers the handler to some runtime event + * + * @return $this + */ + public function register(); +} \ No newline at end of file diff --git a/src/Services/Runtime/ShutdownHandler.php b/src/Services/Runtime/ShutdownHandler.php new file mode 100644 index 0000000..29a6312 --- /dev/null +++ b/src/Services/Runtime/ShutdownHandler.php @@ -0,0 +1,113 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Services\Runtime; + +use FF\Services\AbstractService; + +/** + * Class ErrorHandler + * + * Options: + * + * - force-exit : bool (default: false) - invoke exit() after firing Shutdown event + * + * @package FF\Services\Runtime + */ +class ShutdownHandler extends AbstractService implements RuntimeEventHandlerInterface +{ + /** + * List of codes indicating fatal errors + */ + const FATAL_ERRORS = [ + E_ERROR, + E_PARSE, + E_CORE_ERROR, + E_COMPILE_ERROR, + E_USER_ERROR, + E_RECOVERABLE_ERROR + ]; + + /** + * @var bool + */ + protected $forceExit; + + /** + * {@inheritdoc} + * + * @return $this + * @see http://php.net/register_shutdown_function + */ + public function register() + { + register_shutdown_function([$this, 'onShutdown']); + return $this; + } + + /** + * @return bool + */ + public function getForceExit(): bool + { + return $this->forceExit; + } + + /** + * @param bool $forceExit + * @return $this + */ + public function setForceExit(bool $forceExit) + { + $this->forceExit = $forceExit; + return $this; + } + + /** + * Generic shutdown handler callback + * + * Terminates further execution via exit() if configured to do so, + * thus stopping any additional shutdown handlers from being called. + * + * Fires an additional OnError event, in case a fatal error is detected + * + * @fires Runtime\Shutdown + * @fires Runtime\Error + * @see http://php.net/register_shutdown_function + * @see http://php.net/error_get_last + */ + public function onShutdown() + { + $error = error_get_last(); + if (!is_null($error) && in_array($error['type'], self::FATAL_ERRORS)) { + $this->fire( + 'Runtime\Error', + $error['type'], + $error['message'], + $error['file'], + $error['line'] + ); + } + + $this->fire('Runtime\Shutdown'); + + if ($this->forceExit) exit(); + } + + /** + * {@inheritDoc} + */ + protected function initialize(array $options) + { + parent::initialize($options); + + $this->forceExit = $this->getOption('force-exit', false); + } +} \ No newline at end of file diff --git a/tests/Services/Runtime/ErrorHandlerTest.php b/tests/Services/Runtime/ErrorHandlerTest.php new file mode 100644 index 0000000..7ecfb30 --- /dev/null +++ b/tests/Services/Runtime/ErrorHandlerTest.php @@ -0,0 +1,192 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Tests\Services\Runtime; + +use FF\Events\Runtime\Error; +use FF\Factories\ServicesFactory; +use FF\Factories\SF; +use FF\Services\Events\EventBroker; +use FF\Services\Runtime\ErrorHandler; +use PHPUnit\Framework\TestCase; + +/** + * Test ErrorHandlerTest + * + * @package FF\Tests + */ +class ErrorHandlerTest extends TestCase +{ + /** + * @var mixed + */ + protected static $currentHandler; + + /** + * @var ErrorHandler + */ + protected $uut; + + /** + * @var Error + */ + protected static $lastEvent; + + /** + * {@inheritdoc} + */ + public static function setUpBeforeClass(): void + { + // store current handler + self::$currentHandler = set_error_handler(null); + + SF::setInstance(new ServicesFactory()); + + /** @var EventBroker $eventBroker */ + $eventBroker = SF::i()->get('Events\EventBroker'); + + // register test listener + $eventBroker->subscribe([__CLASS__, 'listener'], 'Runtime\Error'); + } + + /** + * {@inheritdoc} + */ + protected function setUp(): void + { + // unregister all error handlers + while (true) { + if (is_null(set_error_handler(null))) break; + } + + $this->uut = new ErrorHandler(); + self::$lastEvent = null; + } + + /** + * {@inheritdoc} + */ + public static function tearDownAfterClass(): void + { + // unregister all error handlers + while (true) { + if (is_null(set_error_handler(null))) break; + } + + // re-register original error handler (if any) + set_error_handler(self::$currentHandler); + } + + /** + * @param Error $event + */ + public static function listener(Error $event) + { + self::$lastEvent = $event; + } + + /** + * Dummy callback + * + * @param int $errNo + * @param string $errMsg + * @param string $errFile + * @param int $errLine + * @param array $errContext + * @return boolean + */ + public function dummyHandler( + int $errNo, + string $errMsg, + string $errFile = '', + int $errLine = null, + array $errContext = [] + ): bool { + $this->fail('dummy handler should never be called [' . serialize(func_get_args()) . ']'); + return false; + } + + /** + * Tests the namesake method/feature + */ + public function testSetGetErrorTypes() + { + $value = E_ERROR; + $same = $this->uut->setErrorTypes($value); + $this->assertSame($this->uut, $same); + $this->assertEquals($value, $this->uut->getErrorTypes()); + } + + /** + * Tests the namesake method/feature + */ + public function testSetGetBypassPhpErrorHandling() + { + $same = $this->uut->setBypassPhpErrorHandling(false); + $this->assertSame($this->uut, $same); + $this->assertFalse($this->uut->getBypassPhpErrorHandling()); + } + + /** + * Tests the namesake method/feature + */ + public function testRegister() + { + $same = $this->uut->register(); + $this->assertSame($this->uut, $same); + + // register another error handler on top + // uut error handler should be found as the previous one + $uutHandler = set_error_handler([$this, 'dummyHandler']); + $this->assertSame([$this->uut, 'onError'], $uutHandler); + } + + /** + * Tests the namesake method/feature + */ + public function testRegisterWithPrevious() + { + $callback = [$this, 'dummyHandler']; + set_error_handler($callback); + + $this->uut->register(); + $this->assertSame($callback, $this->uut->getPreviousHandler()); + } + + /** + * Tests the namesake method/feature + */ + public function testRestorePreviousHandler() + { + $callback = [$this, 'dummyHandler']; + set_error_handler($callback); + + $same = $this->uut->register()->restorePreviousHandler(); + $this->assertSame($same, $this->uut); + $this->assertNull($this->uut->getPreviousHandler()); + + $previous = set_error_handler(null); + $this->assertSame($callback, $previous); + } + + /** + * Tests the namesake method/feature + */ + public function testTriggerErrorHandling() + { + $msg = 'testing ErrorHandler'; + $this->uut->register() + ->onError(E_NOTICE, $msg); + + $this->assertNotNull(self::$lastEvent); + $this->assertEquals(E_NOTICE, self::$lastEvent->getErrNo()); + $this->assertEquals($msg, self::$lastEvent->getErrMsg()); + } +} \ No newline at end of file diff --git a/tests/Services/Runtime/ExceptionHandlerTest.php b/tests/Services/Runtime/ExceptionHandlerTest.php new file mode 100644 index 0000000..3e0a500 --- /dev/null +++ b/tests/Services/Runtime/ExceptionHandlerTest.php @@ -0,0 +1,159 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Tests\Runtime; + +use FF\Events\Runtime\Exception; +use FF\Factories\ServicesFactory; +use FF\Factories\SF; +use FF\Services\Events\EventBroker; +use FF\Services\Runtime\ExceptionHandler; +use PHPUnit\Framework\TestCase; + +/** + * Test ExceptionHandlerTest + * + * @package FF\Tests + */ +class ExceptionHandlerTest extends TestCase +{ + /** + * @var mixed + */ + protected static $currentHandler; + + /** + * @var ExceptionHandler + */ + protected $uut; + + /** + * @var Exception + */ + protected static $lastEvent; + + /** + * {@inheritdoc} + */ + public static function setUpBeforeClass(): void + { + // store current handler + self::$currentHandler = set_exception_handler(null); + + SF::setInstance(new ServicesFactory()); + + /** @var EventBroker $eventBroker */ + $eventBroker = SF::i()->get('Events\EventBroker'); + + // register test listener + $eventBroker->subscribe([__CLASS__, 'listener'], 'Runtime\Exception'); + } + + /** + * {@inheritdoc} + */ + protected function setUp(): void + { + // unregister all error handlers + while (true) { + if (is_null(set_exception_handler(null))) break; + } + + $this->uut = new ExceptionHandler(); + self::$lastEvent = null; + } + + /** + * {@inheritdoc} + */ + public static function tearDownAfterClass(): void + { + // unregister all error handlers + while (true) { + if (is_null(set_exception_handler(null))) break; + } + + // re-register original error handler (if any) + set_exception_handler(self::$currentHandler); + } + + /** + * @param Exception $event + */ + public static function listener(Exception $event) + { + self::$lastEvent = $event; + } + + /** + * Dummy handler callback + * + * @param \Throwable $e + */ + public function dummyHandler(\Throwable $e) + { + $this->fail('dummy handler should never be called [' . serialize(func_get_args()) . ']'); + } + + /** + * Tests the namesake method/feature + */ + public function testRegister() + { + $same = $this->uut->register(); + $this->assertSame($this->uut, $same); + + // register another error handler on top + // uut error handler should be found as the previous one + $uutHandler = set_exception_handler([$this, 'dummyHandler']); + $this->assertSame([$this->uut, 'onException'], $uutHandler); + } + + /** + * Tests the namesake method/feature + */ + public function testRegisterWithPrevious() + { + $callback = [$this, 'dummyHandler']; + set_exception_handler($callback); + + $this->uut->register(); + $this->assertSame($callback, $this->uut->getPreviousHandler()); + } + + /** + * Tests the namesake method/feature + */ + public function testTriggerExceptionHandling() + { + $e = new \Exception('testing ExceptionHandler'); + $this->uut->register() + ->onException($e); + + $this->assertNotNull(self::$lastEvent); + $this->assertSame($e, self::$lastEvent->getException()); + } + + /** + * Tests the namesake method/feature + */ + public function testRestorePreviousHandler() + { + $callback = [$this, 'dummyHandler']; + set_exception_handler($callback); + + $same = $this->uut->register()->restorePreviousHandler(); + $this->assertSame($same, $this->uut); + $this->assertNull($this->uut->getPreviousHandler()); + + $previous = set_exception_handler(null); + $this->assertSame($callback, $previous); + } +} \ No newline at end of file diff --git a/tests/Services/Runtime/ShutdownHandlerTest.php b/tests/Services/Runtime/ShutdownHandlerTest.php new file mode 100644 index 0000000..ba4d6ab --- /dev/null +++ b/tests/Services/Runtime/ShutdownHandlerTest.php @@ -0,0 +1,105 @@ + + * @copyright 2019-forever Marco Stoll + * @filesource + */ +declare(strict_types=1); + +namespace FF\Tests\Services\Runtime; + +use FF\Events\Runtime\Shutdown; +use FF\Factories\ServicesFactory; +use FF\Factories\SF; +use FF\Services\Events\EventBroker; +use FF\Services\Runtime\ShutdownHandler; +use PHPUnit\Framework\TestCase; + +/** + * Test ShutdownHandlerTest + * + * @package FF\Tests + */ +class ShutdownHandlerTest extends TestCase +{ + /** + * @var ShutdownHandler + */ + protected $uut; + + /** + * @var Shutdown + */ + protected static $lastEvent; + + /** + * {@inheritdoc} + */ + public static function setUpBeforeClass(): void + { + SF::setInstance(new ServicesFactory()); + + /** @var EventBroker $eventBroker */ + $eventBroker = SF::i()->get('Events\EventBroker'); + + // register test listener + $eventBroker->subscribe([__CLASS__, 'listener'], 'Runtime\Shutdown'); + } + + /** + * {@inheritdoc} + */ + protected function setUp(): void + { + $this->uut = new ShutdownHandler(); + self::$lastEvent = null; + } + + /** + * @param Shutdown $event + */ + public static function listener(Shutdown $event) + { + self::$lastEvent = $event; + } + + /** + * Dummy callback + */ + public function dummyHandler() + { + $this->fail('dummy handler should never be called'); + } + + /** + * Tests the namesake method/feature + */ + public function testSetGetForceExit() + { + $same = $this->uut->setForceExit(false); + $this->assertSame($this->uut, $same); + $this->assertFalse($this->uut->getForceExit()); + } + + /** + * Tests the namesake method/feature + */ + public function testRegister() + { + $same = $this->uut->register(); + $this->assertSame($this->uut, $same); + } + + /** + * Tests the namesake method/feature + */ + public function testTriggerShutdownHandling() + { + $this->uut->register() + ->onShutdown(); + + $this->assertNotNull(self::$lastEvent); + } +} \ No newline at end of file