Milestone 3: Middleware Pipeline (The Onion) with Telemetry

This commit is contained in:
Funky Waddle 2026-02-25 00:33:09 -06:00
parent 9aa63f2744
commit 9f92227547
3 changed files with 210 additions and 0 deletions

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Phred\Beacon;
use Phred\BeaconContracts\DispatchMiddlewareInterface;
/**
* MiddlewarePipeline
*
* Coordinates the execution of global middleware before listeners are called.
* Uses a recursive "Onion" style closure chain.
*/
class MiddlewarePipeline
{
/**
* @var array<DispatchMiddlewareInterface> 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);
}
}

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Phred\Beacon;
use Phred\BeaconContracts\DispatchMiddlewareInterface;
/**
* TelemetryMiddleware
*
* A middleware that records execution time and memory usage for a dispatch.
* In a real-world scenario, this would push data to a Telemetry system.
* For now, it provides a basic structure for performance tracking.
*/
class TelemetryMiddleware implements DispatchMiddlewareInterface
{
/**
* @var array<string, array{time: float, memory: int}> 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<string, array{time: float, memory: int}>
*/
public function getData(): array
{
return $this->data;
}
}

View file

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Phred\Beacon\Tests\Integration;
use PHPUnit\Framework\TestCase;
use Phred\Beacon\EventDispatcher;
use Phred\Beacon\ListenerProvider;
use Phred\Beacon\TelemetryMiddleware;
use Phred\BeaconContracts\DispatchMiddlewareInterface;
class MiddlewarePipelineTest extends TestCase
{
public function test_it_executes_middleware_in_order(): void
{
$provider = new ListenerProvider();
$dispatcher = new EventDispatcher($provider);
$order = [];
$middleware1 = new class($order) implements DispatchMiddlewareInterface {
public function __construct(private array &$order) {}
public function handle(object $event, callable $next): object {
$this->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']);
}
}