Merge branch 'release/0.3.0'

This commit is contained in:
Marco Stoll 2019-06-21 11:52:59 +02:00
commit 273338d36c
12 changed files with 1044 additions and 2 deletions

View File

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

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