From 4b7cd68e240c0caf5a46408d22dbae6eaad28a3a Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Mon, 3 Mar 2025 23:48:22 -0700 Subject: [PATCH 1/4] added an event system --- flight/Engine.php | 44 ++++++- flight/Flight.php | 7 +- flight/core/EventDispatcher.php | 40 +++++++ tests/EventSystemTest.php | 197 ++++++++++++++++++++++++++++++++ 4 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 flight/core/EventDispatcher.php create mode 100644 tests/EventSystemTest.php diff --git a/flight/Engine.php b/flight/Engine.php index b2900c4..f75f42a 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -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 $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 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); + } } diff --git a/flight/Flight.php b/flight/Flight.php index 59a9945..fb35427 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -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 $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 &$params, string &$output): (void|false) $callback) * Adds a filter before a framework method. * @method static void after(string $name, Closure(array &$params, string &$output): (void|false) $callback) * Adds a filter after a framework method. * + * # Variables * @method static void set(string|iterable $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. diff --git a/flight/core/EventDispatcher.php b/flight/core/EventDispatcher.php new file mode 100644 index 0000000..0553dc7 --- /dev/null +++ b/flight/core/EventDispatcher.php @@ -0,0 +1,40 @@ +> */ + 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); + } + } + } +} diff --git a/tests/EventSystemTest.php b/tests/EventSystemTest.php new file mode 100644 index 0000000..74ce27a --- /dev/null +++ b/tests/EventSystemTest.php @@ -0,0 +1,197 @@ +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'); + } +} From 79efffc33342856f6d0475c47ad5fdd904a9c76a Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Tue, 4 Mar 2025 07:14:22 -0700 Subject: [PATCH 2/4] added stopPropagation logic --- flight/core/EventDispatcher.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/flight/core/EventDispatcher.php b/flight/core/EventDispatcher.php index 0553dc7..828388c 100644 --- a/flight/core/EventDispatcher.php +++ b/flight/core/EventDispatcher.php @@ -33,7 +33,12 @@ class EventDispatcher { if (isset($this->listeners[$event]) === true) { foreach ($this->listeners[$event] as $callback) { - call_user_func_array($callback, $args); + $result = call_user_func_array($callback, $args); + + // If you return false, it will break the loop and stop the other event listeners. + if ($result === false) { + break; // Stop executing further listeners + } } } } From 2e68ccb0d0a71745d9340d6fa57b57b730cb8db1 Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Tue, 4 Mar 2025 07:19:13 -0700 Subject: [PATCH 3/4] and added new unit tests --- tests/EventSystemTest.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/EventSystemTest.php b/tests/EventSystemTest.php index 74ce27a..26d51e6 100644 --- a/tests/EventSystemTest.php +++ b/tests/EventSystemTest.php @@ -194,4 +194,34 @@ class EventSystemTest extends TestCase // Assuming the event system validates callables Flight::onEvent('test.event', 'not_a_callable'); } + + /** + * Test that event propagation stops if a listener returns false. + */ + public function testStopPropagation() + { + $firstCalled = false; + $secondCalled = false; + $thirdCalled = false; + + Flight::onEvent('test.event', function () use (&$firstCalled) { + $firstCalled = true; + return true; // Continue propagation + }); + + Flight::onEvent('test.event', function () use (&$secondCalled) { + $secondCalled = true; + return false; // Stop propagation + }); + + Flight::onEvent('test.event', function () use (&$thirdCalled) { + $thirdCalled = true; + }); + + Flight::triggerEvent('test.event'); + + $this->assertTrue($firstCalled, 'First listener should be called'); + $this->assertTrue($secondCalled, 'Second listener should be called'); + $this->assertFalse($thirdCalled, 'Third listener should not be called after propagation stopped'); + } } From 046a034455a7c8e761ba15995494e3d564175b9c Mon Sep 17 00:00:00 2001 From: n0nag0n Date: Tue, 4 Mar 2025 07:20:07 -0700 Subject: [PATCH 4/4] prettified --- flight/core/EventDispatcher.php | 2 +- tests/EventSystemTest.php | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/flight/core/EventDispatcher.php b/flight/core/EventDispatcher.php index 828388c..b80eb42 100644 --- a/flight/core/EventDispatcher.php +++ b/flight/core/EventDispatcher.php @@ -35,7 +35,7 @@ class EventDispatcher foreach ($this->listeners[$event] as $callback) { $result = call_user_func_array($callback, $args); - // If you return false, it will break the loop and stop the other event listeners. + // If you return false, it will break the loop and stop the other event listeners. if ($result === false) { break; // Stop executing further listeners } diff --git a/tests/EventSystemTest.php b/tests/EventSystemTest.php index 26d51e6..6dba2c7 100644 --- a/tests/EventSystemTest.php +++ b/tests/EventSystemTest.php @@ -195,7 +195,7 @@ class EventSystemTest extends TestCase Flight::onEvent('test.event', 'not_a_callable'); } - /** + /** * Test that event propagation stops if a listener returns false. */ public function testStopPropagation() @@ -203,23 +203,23 @@ class EventSystemTest extends TestCase $firstCalled = false; $secondCalled = false; $thirdCalled = false; - + Flight::onEvent('test.event', function () use (&$firstCalled) { $firstCalled = true; return true; // Continue propagation }); - + Flight::onEvent('test.event', function () use (&$secondCalled) { $secondCalled = true; return false; // Stop propagation }); - + Flight::onEvent('test.event', function () use (&$thirdCalled) { $thirdCalled = true; }); - + Flight::triggerEvent('test.event'); - + $this->assertTrue($firstCalled, 'First listener should be called'); $this->assertTrue($secondCalled, 'Second listener should be called'); $this->assertFalse($thirdCalled, 'Third listener should not be called after propagation stopped');