added an event system

pull/632/head
n0nag0n 4 weeks ago
parent fe3e78a7d0
commit 4b7cd68e24

@ -8,6 +8,7 @@ use Closure;
use ErrorException;
use Exception;
use flight\core\Dispatcher;
use flight\core\EventDispatcher;
use flight\core\Loader;
use flight\net\Request;
use flight\net\Response;
@ -51,6 +52,10 @@ use flight\net\Route;
* @method void render(string $file, ?array<string,mixed> $data = null, ?string $key = null) Renders template
* @method View view() Gets current view
*
* # Events
* @method void onEvent(string $event, callable $callback) Registers a callback for an event.
* @method void triggerEvent(string $event, ...$args) Triggers an event.
*
* # Request-Response
* @method Request request() Gets current request
* @method Response response() Gets current response
@ -79,7 +84,8 @@ class Engine
private const MAPPABLE_METHODS = [
'start', 'stop', 'route', 'halt', 'error', 'notFound',
'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonHalt', 'jsonp',
'post', 'put', 'patch', 'delete', 'group', 'getUrl', 'download', 'resource'
'post', 'put', 'patch', 'delete', 'group', 'getUrl', 'download', 'resource',
'onEvent', 'triggerEvent'
];
/** @var array<string, mixed> Stored variables. */
@ -88,9 +94,12 @@ class Engine
/** Class loader. */
protected Loader $loader;
/** Event dispatcher. */
/** Method and class dispatcher. */
protected Dispatcher $dispatcher;
/** Event dispatcher. */
protected EventDispatcher $eventDispatcher;
/** If the framework has been initialized or not. */
protected bool $initialized = false;
@ -101,6 +110,7 @@ class Engine
{
$this->loader = new Loader();
$this->dispatcher = new Dispatcher();
$this->eventDispatcher = new EventDispatcher();
$this->init();
}
@ -493,6 +503,8 @@ class Engine
$this->router()->reset();
}
$request = $this->request();
$this->triggerEvent('flight.request.received', $request);
$response = $this->response();
$router = $this->router();
@ -515,6 +527,7 @@ class Engine
// Route the request
$failedMiddlewareCheck = false;
while ($route = $router->route($request)) {
$this->triggerEvent('flight.route.matched', $route);
$params = array_values($route->params);
// Add route info to the parameter list
@ -548,6 +561,7 @@ class Engine
$failedMiddlewareCheck = true;
break;
}
$this->triggerEvent('flight.route.middleware.before', $route);
}
$useV3OutputBuffering =
@ -563,6 +577,7 @@ class Engine
$route->callback,
$params
);
$this->triggerEvent('flight.route.executed', $route);
if ($useV3OutputBuffering === true) {
$response->write(ob_get_clean());
@ -577,6 +592,7 @@ class Engine
$failedMiddlewareCheck = true;
break;
}
$this->triggerEvent('flight.route.middleware.after', $route);
}
$dispatched = true;
@ -662,6 +678,8 @@ class Engine
}
$response->send();
$this->triggerEvent('flight.response.sent', $response);
}
}
@ -992,4 +1010,26 @@ class Engine
{
return $this->router()->getUrlByAlias($alias, $params);
}
/**
* Adds an event listener.
*
* @param string $eventName The name of the event to listen to
* @param callable $callback The callback to execute when the event is triggered
*/
public function _onEvent(string $eventName, callable $callback): void
{
$this->eventDispatcher->on($eventName, $callback);
}
/**
* Triggers an event.
*
* @param string $eventName The name of the event to trigger
* @param mixed ...$args The arguments to pass to the event listeners
*/
public function _triggerEvent(string $eventName, ...$args): void
{
$this->eventDispatcher->trigger($eventName, ...$args);
}
}

@ -46,14 +46,15 @@ require_once __DIR__ . '/autoload.php';
* Adds standardized RESTful routes for a controller.
* @method static Router router() Returns Router instance.
* @method static string getUrl(string $alias, array<string, mixed> $params = []) Gets a url from an alias
*
* @method static void map(string $name, callable $callback) Creates a custom framework method.
*
* # Filters
* @method static void before(string $name, Closure(array<int, mixed> &$params, string &$output): (void|false) $callback)
* Adds a filter before a framework method.
* @method static void after(string $name, Closure(array<int, mixed> &$params, string &$output): (void|false) $callback)
* Adds a filter after a framework method.
*
* # Variables
* @method static void set(string|iterable<string, mixed> $key, mixed $value) Sets a variable.
* @method static mixed get(?string $key) Gets a variable.
* @method static bool has(string $key) Checks if a variable is set.
@ -64,6 +65,10 @@ require_once __DIR__ . '/autoload.php';
* Renders a template file.
* @method static View view() Returns View instance.
*
* # Events
* @method void onEvent(string $event, callable $callback) Registers a callback for an event.
* @method void triggerEvent(string $event, ...$args) Triggers an event.
*
* # Request-Response
* @method static Request request() Returns Request instance.
* @method static Response response() Returns Response instance.

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace flight\core;
class EventDispatcher
{
/** @var array<string, array<int, callable>> */
protected array $listeners = [];
/**
* Register a callback for an event.
*
* @param string $event Event name
* @param callable $callback Callback function
*/
public function on(string $event, callable $callback): void
{
if (isset($this->listeners[$event]) === false) {
$this->listeners[$event] = [];
}
$this->listeners[$event][] = $callback;
}
/**
* Trigger an event with optional arguments.
*
* @param string $event Event name
* @param mixed ...$args Arguments to pass to the callbacks
*/
public function trigger(string $event, ...$args): void
{
if (isset($this->listeners[$event]) === true) {
foreach ($this->listeners[$event] as $callback) {
call_user_func_array($callback, $args);
}
}
}
}

@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
namespace flight\tests;
use Flight;
use PHPUnit\Framework\TestCase;
use flight\Engine;
use TypeError;
class EventSystemTest extends TestCase
{
protected function setUp(): void
{
// Reset the Flight engine before each test to ensure a clean state
Flight::setEngine(new Engine());
Flight::app()->init();
}
/**
* Test registering and triggering a single listener.
*/
public function testRegisterAndTriggerSingleListener()
{
$called = false;
Flight::onEvent('test.event', function () use (&$called) {
$called = true;
});
Flight::triggerEvent('test.event');
$this->assertTrue($called, 'Single listener should be called when event is triggered.');
}
/**
* Test registering multiple listeners for the same event.
*/
public function testRegisterMultipleListeners()
{
$counter = 0;
Flight::onEvent('test.event', function () use (&$counter) {
$counter++;
});
Flight::onEvent('test.event', function () use (&$counter) {
$counter++;
});
Flight::triggerEvent('test.event');
$this->assertEquals(2, $counter, 'All registered listeners should be called.');
}
/**
* Test triggering an event with no listeners registered.
*/
public function testTriggerWithNoListeners()
{
// Should not throw any errors
Flight::triggerEvent('non.existent.event');
$this->assertTrue(true, 'Triggering an event with no listeners should not throw an error.');
}
/**
* Test that a listener receives a single argument correctly.
*/
public function testListenerReceivesSingleArgument()
{
$received = null;
Flight::onEvent('test.event', function ($arg) use (&$received) {
$received = $arg;
});
Flight::triggerEvent('test.event', 'hello');
$this->assertEquals('hello', $received, 'Listener should receive the passed argument.');
}
/**
* Test that a listener receives multiple arguments correctly.
*/
public function testListenerReceivesMultipleArguments()
{
$received = [];
Flight::onEvent('test.event', function ($arg1, $arg2) use (&$received) {
$received = [$arg1, $arg2];
});
Flight::triggerEvent('test.event', 'first', 'second');
$this->assertEquals(['first', 'second'], $received, 'Listener should receive all passed arguments.');
}
/**
* Test that listeners are called in the order they were registered.
*/
public function testListenersCalledInOrder()
{
$order = [];
Flight::onEvent('test.event', function () use (&$order) {
$order[] = 1;
});
Flight::onEvent('test.event', function () use (&$order) {
$order[] = 2;
});
Flight::triggerEvent('test.event');
$this->assertEquals([1, 2], $order, 'Listeners should be called in registration order.');
}
/**
* Test that listeners are not called for unrelated events.
*/
public function testListenerNotCalledForOtherEvents()
{
$called = false;
Flight::onEvent('test.event1', function () use (&$called) {
$called = true;
});
Flight::triggerEvent('test.event2');
$this->assertFalse($called, 'Listeners should not be called for different events.');
}
/**
* Test overriding the onEvent method.
*/
public function testOverrideOnEvent()
{
$called = false;
Flight::map('onEvent', function ($event, $callback) use (&$called) {
$called = true;
});
Flight::onEvent('test.event', function () {
});
$this->assertTrue($called, 'Overridden onEvent method should be called.');
}
/**
* Test overriding the triggerEvent method.
*/
public function testOverrideTriggerEvent()
{
$called = false;
Flight::map('triggerEvent', function ($event, ...$args) use (&$called) {
$called = true;
});
Flight::triggerEvent('test.event');
$this->assertTrue($called, 'Overridden triggerEvent method should be called.');
}
/**
* Test that an overridden onEvent can still register listeners by calling the original method.
*/
public function testOverrideOnEventStillRegistersListener()
{
$overrideCalled = false;
Flight::map('onEvent', function ($event, $callback) use (&$overrideCalled) {
$overrideCalled = true;
// Call the original method
Flight::app()->_onEvent($event, $callback);
});
$listenerCalled = false;
Flight::onEvent('test.event', function () use (&$listenerCalled) {
$listenerCalled = true;
});
Flight::triggerEvent('test.event');
$this->assertTrue($overrideCalled, 'Overridden onEvent should be called.');
$this->assertTrue($listenerCalled, 'Listener should still be triggered after override.');
}
/**
* Test that an overridden triggerEvent can still trigger listeners by calling the original method.
*/
public function testOverrideTriggerEventStillTriggersListeners()
{
$overrideCalled = false;
Flight::map('triggerEvent', function ($event, ...$args) use (&$overrideCalled) {
$overrideCalled = true;
// Call the original method
Flight::app()->_triggerEvent($event, ...$args);
});
$listenerCalled = false;
Flight::onEvent('test.event', function () use (&$listenerCalled) {
$listenerCalled = true;
});
Flight::triggerEvent('test.event');
$this->assertTrue($overrideCalled, 'Overridden triggerEvent should be called.');
$this->assertTrue($listenerCalled, 'Listeners should still be triggered after override.');
}
/**
* Test that an invalid callable throws an exception (if applicable).
*/
public function testInvalidCallableThrowsException()
{
$this->expectException(TypeError::class);
// Assuming the event system validates callables
Flight::onEvent('test.event', 'not_a_callable');
}
}
Loading…
Cancel
Save