From 9f92227547f5259ae352e0e1f70e84aebea57dd8 Mon Sep 17 00:00:00 2001 From: Funky Waddle Date: Wed, 25 Feb 2026 00:33:09 -0600 Subject: [PATCH] Milestone 3: Middleware Pipeline (The Onion) with Telemetry --- src/MiddlewarePipeline.php | 53 ++++++++++ src/TelemetryMiddleware.php | 54 ++++++++++ tests/Integration/MiddlewarePipelineTest.php | 103 +++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 src/MiddlewarePipeline.php create mode 100644 src/TelemetryMiddleware.php create mode 100644 tests/Integration/MiddlewarePipelineTest.php diff --git a/src/MiddlewarePipeline.php b/src/MiddlewarePipeline.php new file mode 100644 index 0000000..6c2909d --- /dev/null +++ b/src/MiddlewarePipeline.php @@ -0,0 +1,53 @@ + The list of global middleware. + */ + private array $middlewares = []; + + /** + * Adds a middleware to the pipeline. + * + * @param DispatchMiddlewareInterface $middleware + * @return void + */ + public function add(DispatchMiddlewareInterface $middleware): void + { + $this->middlewares[] = $middleware; + } + + /** + * Executes the middleware pipeline, wrapping the final action. + * + * @param object $event The event object. + * @param callable $finalAction The final action (typically listener execution). + * @return object The potentially modified event object. + */ + public function execute(object $event, callable $finalAction): object + { + $pipeline = array_reverse($this->middlewares); + + $runner = array_reduce( + $pipeline, + fn(callable $next, DispatchMiddlewareInterface $middleware) => + fn(object $event) => $middleware->handle($event, $next), + $finalAction + ); + + return $runner($event); + } +} diff --git a/src/TelemetryMiddleware.php b/src/TelemetryMiddleware.php new file mode 100644 index 0000000..f212487 --- /dev/null +++ b/src/TelemetryMiddleware.php @@ -0,0 +1,54 @@ + Recorded telemetry data. + */ + private array $data = []; + + /** + * Handles the event and records telemetry. + * + * @param object $event + * @param callable $next + * @return object + */ + public function handle(object $event, callable $next): object + { + $start = microtime(true); + $mem = memory_get_usage(); + + $result = $next($event); + + $this->data[$event::class] = [ + 'time' => microtime(true) - $start, + 'memory' => memory_get_usage() - $mem, + ]; + + return $result; + } + + /** + * Returns the recorded telemetry data. + * + * @return array + */ + public function getData(): array + { + return $this->data; + } +} diff --git a/tests/Integration/MiddlewarePipelineTest.php b/tests/Integration/MiddlewarePipelineTest.php new file mode 100644 index 0000000..4cc96be --- /dev/null +++ b/tests/Integration/MiddlewarePipelineTest.php @@ -0,0 +1,103 @@ +order[] = 'm1_before'; + $result = $next($event); + $this->order[] = 'm1_after'; + return $result; + } + }; + + $middleware2 = new class($order) implements DispatchMiddlewareInterface { + public function __construct(private array &$order) {} + public function handle(object $event, callable $next): object { + $this->order[] = 'm2_before'; + $result = $next($event); + $this->order[] = 'm2_after'; + return $result; + } + }; + + $dispatcher->addMiddleware($middleware1); + $dispatcher->addMiddleware($middleware2); + + $provider->addListener('stdClass', function() use (&$order) { + $order[] = 'listener'; + }); + + $dispatcher->dispatch(new \stdClass()); + + $expected = [ + 'm1_before', + 'm2_before', + 'listener', + 'm2_after', + 'm1_after' + ]; + + $this->assertEquals($expected, $order); + } + + public function test_middleware_can_modify_event(): void + { + $provider = new ListenerProvider(); + $dispatcher = new EventDispatcher($provider); + + $middleware = new class implements DispatchMiddlewareInterface { + public function handle(object $event, callable $next): object { + $event->modified = true; + return $next($event); + } + }; + + $dispatcher->addMiddleware($middleware); + + $event = new \stdClass(); + $event->modified = false; + + $dispatcher->dispatch($event); + + $this->assertTrue($event->modified); + } + + public function test_telemetry_middleware_records_data(): void + { + $provider = new ListenerProvider(); + $dispatcher = new EventDispatcher($provider); + $telemetry = new TelemetryMiddleware(); + + $dispatcher->addMiddleware($telemetry); + + $provider->addListener('stdClass', function() { + usleep(1000); // 1ms + }); + + $event = new \stdClass(); + $dispatcher->dispatch($event); + + $data = $telemetry->getData(); + + $this->assertArrayHasKey('stdClass', $data); + $this->assertGreaterThan(0, $data['stdClass']['time']); + } +}