[FEATURE] add the runtime event handlers

This commit is contained in:
Marco Stoll 2019-06-21 11:43:14 +02:00
parent 558620dbe7
commit 5cdcf7752e
11 changed files with 1006 additions and 1 deletions

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
{
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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
}
}
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}