Skip to content

Commit 4b7cd68

Browse files
committed
added an event system
1 parent fe3e78a commit 4b7cd68

File tree

4 files changed

+285
-3
lines changed

4 files changed

+285
-3
lines changed

flight/Engine.php

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use ErrorException;
99
use Exception;
1010
use flight\core\Dispatcher;
11+
use flight\core\EventDispatcher;
1112
use flight\core\Loader;
1213
use flight\net\Request;
1314
use flight\net\Response;
@@ -51,6 +52,10 @@
5152
* @method void render(string $file, ?array<string,mixed> $data = null, ?string $key = null) Renders template
5253
* @method View view() Gets current view
5354
*
55+
* # Events
56+
* @method void onEvent(string $event, callable $callback) Registers a callback for an event.
57+
* @method void triggerEvent(string $event, ...$args) Triggers an event.
58+
*
5459
* # Request-Response
5560
* @method Request request() Gets current request
5661
* @method Response response() Gets current response
@@ -79,7 +84,8 @@ class Engine
7984
private const MAPPABLE_METHODS = [
8085
'start', 'stop', 'route', 'halt', 'error', 'notFound',
8186
'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonHalt', 'jsonp',
82-
'post', 'put', 'patch', 'delete', 'group', 'getUrl', 'download', 'resource'
87+
'post', 'put', 'patch', 'delete', 'group', 'getUrl', 'download', 'resource',
88+
'onEvent', 'triggerEvent'
8389
];
8490

8591
/** @var array<string, mixed> Stored variables. */
@@ -88,9 +94,12 @@ class Engine
8894
/** Class loader. */
8995
protected Loader $loader;
9096

91-
/** Event dispatcher. */
97+
/** Method and class dispatcher. */
9298
protected Dispatcher $dispatcher;
9399

100+
/** Event dispatcher. */
101+
protected EventDispatcher $eventDispatcher;
102+
94103
/** If the framework has been initialized or not. */
95104
protected bool $initialized = false;
96105

@@ -101,6 +110,7 @@ public function __construct()
101110
{
102111
$this->loader = new Loader();
103112
$this->dispatcher = new Dispatcher();
113+
$this->eventDispatcher = new EventDispatcher();
104114
$this->init();
105115
}
106116

@@ -493,6 +503,8 @@ public function _start(): void
493503
$this->router()->reset();
494504
}
495505
$request = $this->request();
506+
$this->triggerEvent('flight.request.received', $request);
507+
496508
$response = $this->response();
497509
$router = $this->router();
498510

@@ -515,6 +527,7 @@ public function _start(): void
515527
// Route the request
516528
$failedMiddlewareCheck = false;
517529
while ($route = $router->route($request)) {
530+
$this->triggerEvent('flight.route.matched', $route);
518531
$params = array_values($route->params);
519532

520533
// Add route info to the parameter list
@@ -548,6 +561,7 @@ public function _start(): void
548561
$failedMiddlewareCheck = true;
549562
break;
550563
}
564+
$this->triggerEvent('flight.route.middleware.before', $route);
551565
}
552566

553567
$useV3OutputBuffering =
@@ -563,6 +577,7 @@ public function _start(): void
563577
$route->callback,
564578
$params
565579
);
580+
$this->triggerEvent('flight.route.executed', $route);
566581

567582
if ($useV3OutputBuffering === true) {
568583
$response->write(ob_get_clean());
@@ -577,6 +592,7 @@ public function _start(): void
577592
$failedMiddlewareCheck = true;
578593
break;
579594
}
595+
$this->triggerEvent('flight.route.middleware.after', $route);
580596
}
581597

582598
$dispatched = true;
@@ -662,6 +678,8 @@ public function _stop(?int $code = null): void
662678
}
663679

664680
$response->send();
681+
682+
$this->triggerEvent('flight.response.sent', $response);
665683
}
666684
}
667685

@@ -992,4 +1010,26 @@ public function _getUrl(string $alias, array $params = []): string
9921010
{
9931011
return $this->router()->getUrlByAlias($alias, $params);
9941012
}
1013+
1014+
/**
1015+
* Adds an event listener.
1016+
*
1017+
* @param string $eventName The name of the event to listen to
1018+
* @param callable $callback The callback to execute when the event is triggered
1019+
*/
1020+
public function _onEvent(string $eventName, callable $callback): void
1021+
{
1022+
$this->eventDispatcher->on($eventName, $callback);
1023+
}
1024+
1025+
/**
1026+
* Triggers an event.
1027+
*
1028+
* @param string $eventName The name of the event to trigger
1029+
* @param mixed ...$args The arguments to pass to the event listeners
1030+
*/
1031+
public function _triggerEvent(string $eventName, ...$args): void
1032+
{
1033+
$this->eventDispatcher->trigger($eventName, ...$args);
1034+
}
9951035
}

flight/Flight.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,15 @@
4646
* Adds standardized RESTful routes for a controller.
4747
* @method static Router router() Returns Router instance.
4848
* @method static string getUrl(string $alias, array<string, mixed> $params = []) Gets a url from an alias
49-
*
5049
* @method static void map(string $name, callable $callback) Creates a custom framework method.
5150
*
51+
* # Filters
5252
* @method static void before(string $name, Closure(array<int, mixed> &$params, string &$output): (void|false) $callback)
5353
* Adds a filter before a framework method.
5454
* @method static void after(string $name, Closure(array<int, mixed> &$params, string &$output): (void|false) $callback)
5555
* Adds a filter after a framework method.
5656
*
57+
* # Variables
5758
* @method static void set(string|iterable<string, mixed> $key, mixed $value) Sets a variable.
5859
* @method static mixed get(?string $key) Gets a variable.
5960
* @method static bool has(string $key) Checks if a variable is set.
@@ -64,6 +65,10 @@
6465
* Renders a template file.
6566
* @method static View view() Returns View instance.
6667
*
68+
* # Events
69+
* @method void onEvent(string $event, callable $callback) Registers a callback for an event.
70+
* @method void triggerEvent(string $event, ...$args) Triggers an event.
71+
*
6772
* # Request-Response
6873
* @method static Request request() Returns Request instance.
6974
* @method static Response response() Returns Response instance.

flight/core/EventDispatcher.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace flight\core;
6+
7+
class EventDispatcher
8+
{
9+
/** @var array<string, array<int, callable>> */
10+
protected array $listeners = [];
11+
12+
/**
13+
* Register a callback for an event.
14+
*
15+
* @param string $event Event name
16+
* @param callable $callback Callback function
17+
*/
18+
public function on(string $event, callable $callback): void
19+
{
20+
if (isset($this->listeners[$event]) === false) {
21+
$this->listeners[$event] = [];
22+
}
23+
$this->listeners[$event][] = $callback;
24+
}
25+
26+
/**
27+
* Trigger an event with optional arguments.
28+
*
29+
* @param string $event Event name
30+
* @param mixed ...$args Arguments to pass to the callbacks
31+
*/
32+
public function trigger(string $event, ...$args): void
33+
{
34+
if (isset($this->listeners[$event]) === true) {
35+
foreach ($this->listeners[$event] as $callback) {
36+
call_user_func_array($callback, $args);
37+
}
38+
}
39+
}
40+
}

tests/EventSystemTest.php

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace flight\tests;
6+
7+
use Flight;
8+
use PHPUnit\Framework\TestCase;
9+
use flight\Engine;
10+
use TypeError;
11+
12+
class EventSystemTest extends TestCase
13+
{
14+
protected function setUp(): void
15+
{
16+
// Reset the Flight engine before each test to ensure a clean state
17+
Flight::setEngine(new Engine());
18+
Flight::app()->init();
19+
}
20+
21+
/**
22+
* Test registering and triggering a single listener.
23+
*/
24+
public function testRegisterAndTriggerSingleListener()
25+
{
26+
$called = false;
27+
Flight::onEvent('test.event', function () use (&$called) {
28+
$called = true;
29+
});
30+
Flight::triggerEvent('test.event');
31+
$this->assertTrue($called, 'Single listener should be called when event is triggered.');
32+
}
33+
34+
/**
35+
* Test registering multiple listeners for the same event.
36+
*/
37+
public function testRegisterMultipleListeners()
38+
{
39+
$counter = 0;
40+
Flight::onEvent('test.event', function () use (&$counter) {
41+
$counter++;
42+
});
43+
Flight::onEvent('test.event', function () use (&$counter) {
44+
$counter++;
45+
});
46+
Flight::triggerEvent('test.event');
47+
$this->assertEquals(2, $counter, 'All registered listeners should be called.');
48+
}
49+
50+
/**
51+
* Test triggering an event with no listeners registered.
52+
*/
53+
public function testTriggerWithNoListeners()
54+
{
55+
// Should not throw any errors
56+
Flight::triggerEvent('non.existent.event');
57+
$this->assertTrue(true, 'Triggering an event with no listeners should not throw an error.');
58+
}
59+
60+
/**
61+
* Test that a listener receives a single argument correctly.
62+
*/
63+
public function testListenerReceivesSingleArgument()
64+
{
65+
$received = null;
66+
Flight::onEvent('test.event', function ($arg) use (&$received) {
67+
$received = $arg;
68+
});
69+
Flight::triggerEvent('test.event', 'hello');
70+
$this->assertEquals('hello', $received, 'Listener should receive the passed argument.');
71+
}
72+
73+
/**
74+
* Test that a listener receives multiple arguments correctly.
75+
*/
76+
public function testListenerReceivesMultipleArguments()
77+
{
78+
$received = [];
79+
Flight::onEvent('test.event', function ($arg1, $arg2) use (&$received) {
80+
$received = [$arg1, $arg2];
81+
});
82+
Flight::triggerEvent('test.event', 'first', 'second');
83+
$this->assertEquals(['first', 'second'], $received, 'Listener should receive all passed arguments.');
84+
}
85+
86+
/**
87+
* Test that listeners are called in the order they were registered.
88+
*/
89+
public function testListenersCalledInOrder()
90+
{
91+
$order = [];
92+
Flight::onEvent('test.event', function () use (&$order) {
93+
$order[] = 1;
94+
});
95+
Flight::onEvent('test.event', function () use (&$order) {
96+
$order[] = 2;
97+
});
98+
Flight::triggerEvent('test.event');
99+
$this->assertEquals([1, 2], $order, 'Listeners should be called in registration order.');
100+
}
101+
102+
/**
103+
* Test that listeners are not called for unrelated events.
104+
*/
105+
public function testListenerNotCalledForOtherEvents()
106+
{
107+
$called = false;
108+
Flight::onEvent('test.event1', function () use (&$called) {
109+
$called = true;
110+
});
111+
Flight::triggerEvent('test.event2');
112+
$this->assertFalse($called, 'Listeners should not be called for different events.');
113+
}
114+
115+
/**
116+
* Test overriding the onEvent method.
117+
*/
118+
public function testOverrideOnEvent()
119+
{
120+
$called = false;
121+
Flight::map('onEvent', function ($event, $callback) use (&$called) {
122+
$called = true;
123+
});
124+
Flight::onEvent('test.event', function () {
125+
});
126+
$this->assertTrue($called, 'Overridden onEvent method should be called.');
127+
}
128+
129+
/**
130+
* Test overriding the triggerEvent method.
131+
*/
132+
public function testOverrideTriggerEvent()
133+
{
134+
$called = false;
135+
Flight::map('triggerEvent', function ($event, ...$args) use (&$called) {
136+
$called = true;
137+
});
138+
Flight::triggerEvent('test.event');
139+
$this->assertTrue($called, 'Overridden triggerEvent method should be called.');
140+
}
141+
142+
/**
143+
* Test that an overridden onEvent can still register listeners by calling the original method.
144+
*/
145+
public function testOverrideOnEventStillRegistersListener()
146+
{
147+
$overrideCalled = false;
148+
Flight::map('onEvent', function ($event, $callback) use (&$overrideCalled) {
149+
$overrideCalled = true;
150+
// Call the original method
151+
Flight::app()->_onEvent($event, $callback);
152+
});
153+
154+
$listenerCalled = false;
155+
Flight::onEvent('test.event', function () use (&$listenerCalled) {
156+
$listenerCalled = true;
157+
});
158+
159+
Flight::triggerEvent('test.event');
160+
161+
$this->assertTrue($overrideCalled, 'Overridden onEvent should be called.');
162+
$this->assertTrue($listenerCalled, 'Listener should still be triggered after override.');
163+
}
164+
165+
/**
166+
* Test that an overridden triggerEvent can still trigger listeners by calling the original method.
167+
*/
168+
public function testOverrideTriggerEventStillTriggersListeners()
169+
{
170+
$overrideCalled = false;
171+
Flight::map('triggerEvent', function ($event, ...$args) use (&$overrideCalled) {
172+
$overrideCalled = true;
173+
// Call the original method
174+
Flight::app()->_triggerEvent($event, ...$args);
175+
});
176+
177+
$listenerCalled = false;
178+
Flight::onEvent('test.event', function () use (&$listenerCalled) {
179+
$listenerCalled = true;
180+
});
181+
182+
Flight::triggerEvent('test.event');
183+
184+
$this->assertTrue($overrideCalled, 'Overridden triggerEvent should be called.');
185+
$this->assertTrue($listenerCalled, 'Listeners should still be triggered after override.');
186+
}
187+
188+
/**
189+
* Test that an invalid callable throws an exception (if applicable).
190+
*/
191+
public function testInvalidCallableThrowsException()
192+
{
193+
$this->expectException(TypeError::class);
194+
// Assuming the event system validates callables
195+
Flight::onEvent('test.event', 'not_a_callable');
196+
}
197+
}

0 commit comments

Comments
 (0)