From 5cdcf7752ebc07526f9dcc9fd9072592fc4348db Mon Sep 17 00:00:00 2001
From: Marco Stoll <marco.stoll@rocketmail.com>
Date: Fri, 21 Jun 2019 11:43:14 +0200
Subject: [PATCH 1/2] [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 @@
+<?php
+/**
+ * Definition of Error
+ *
+ * @author Marco Stoll <marco@fast-forward-encoding.de>
+ * @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 @@
+<?php
+/**
+ * Definition of Exception
+ *
+ * @author Marco Stoll <marco@fast-forward-encoding.de>
+ * @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 @@
+<?php
+/**
+ * Definition of Shutdown
+ *
+ * @author Marco Stoll <marco@fast-forward-encoding.de>
+ * @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 @@
+<?php
+/**
+ * Definition of ErrorHandler
+ *
+ * @author Marco Stoll <marco@fast-forward-encoding.de>
+ * @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 @@
+<?php
+/**
+ * Definition of ExceptionHandler
+ *
+ * @author Marco Stoll <marco@fast-forward-encoding.de>
+ * @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 @@
+<?php
+/**
+ * Definition of RuntimeEventHandlerInterface
+ *
+ * @author Marco Stoll <marco@fast-forward-encoding.de>
+ * @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 @@
+<?php
+/**
+ * Definition of ShutdownHandler
+ *
+ * @author Marco Stoll <marco@fast-forward-encoding.de>
+ * @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 @@
+<?php
+/**
+ * Definition of ErrorHandlerTest
+ *
+ * @author Marco Stoll <marco@fast-forward-encoding.de>
+ * @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 @@
+<?php
+/**
+ * Definition of ExceptionHandlerTest
+ *
+ * @author Marco Stoll <marco@fast-forward-encoding.de>
+ * @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 @@
+<?php
+/**
+ * Definition of ShutdownHandlerTest
+ *
+ * @author Marco Stoll <marco@fast-forward-encoding.de>
+ * @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

From 86c3341f9bfb532f6435cf3caaac232ef770ce45 Mon Sep 17 00:00:00 2001
From: Marco Stoll <marco.stoll@rocketmail.com>
Date: Fri, 21 Jun 2019 11:51:11 +0200
Subject: [PATCH 2/2] [DOCS] update readme

---
 readme.md | 39 ++++++++++++++++++++++++++++++++++++++-
 1 file changed, 38 insertions(+), 1 deletion(-)

diff --git a/readme.md b/readme.md
index 2632a14..efeb096 100644
--- a/readme.md
+++ b/readme.md
@@ -18,6 +18,7 @@ to use.
 Currently **FF** is composed of the following features:
 1. Services and the Service Factory
 2. Events and the Event Broker
+3. Runtime event handlers
 
 More features will follow (see the Road Map section below).
 
@@ -268,9 +269,45 @@ thereof) to be found by the `EventsFactory`.
         }
     }
 
+# Runtime event handlers
+
+This feature introduces three different handler classes for registering as callbacks to one of the three runtime events 
+of the php engine (error, exception, shutdown). The handlers translate php's core event information to **FF\Events** 
+event instances using the `EventBroker`.
+
+## Registering runtime event handlers
+
+All handlers implement the `RuntimeEventHandlerInterface` which lets you `register()` them on demand.  
+The `ErrorHandler` as well as the `ExceptionHandler` each are aware of any previous handlers that might have been 
+registered to their runtime events and let you restore the previous state. When registering shutdown handlers no
+information regarding the previous state is provided by php.
+
+## Subscribing to runtime events
+
+The handlers fire their own events containing all available event data which makes it easy for you to handle them by
+subscribing to the `FF\Events\EventBroker`.
+
+Example:
+
+    use FF\Events\Runtime\Error;
+    use FF\Factories\SF;
+    use FF\Services\Events\EventBroker;
+    use FF\Services\Runtime\ErrorHandler;
+    
+    // register the ErrorHandler
+    (new ErrorHandler())->register();
+    
+    /** @var EventBroker $eventBroker */
+    $eventBroker = SF::i()->get('events\EventBroker');
+    
+    // subscribe to the Runtime\Error event
+    $eventBroker->subscribe('Runtime\Error', function (Error $event) {
+        // handle the event data
+        var_dump($event->getErroNo(), $event->getErrMsg());  
+    }};  
+
 # Road map
 
-- Runtime
 - Controllers
 - Sessions
 - Security