new PassMiddleware(), FirewallMiddleware::class => new PassMiddleware(), AuthenticationMiddleware::class => new PassMiddleware(), RouterMiddleware::class => new StreamMiddleware( static function () use (&$kernel): bool { return $kernel->active; }, ), ]), new TenantContext($this->createStub(TenantService::class)), new IdentityContext(), ); ob_start(); try { (new HttpRuntime($kernel, false))->run(Request::create('/stream')); $content = ob_get_contents(); } finally { ob_end_clean(); } self::assertSame('active', $content); self::assertFalse($kernel->active); self::assertSame(1, $kernel->terminations); } #[Test] #[TestDox('Successful HTTP handling terminates exactly once')] public function succeeds(): void { $kernel = $this->kernel(new ResponseMiddleware(new Response('ok', 200))); $response = (new HttpRuntime($kernel, false))->handle(Request::create('/')); self::assertSame(200, $response->getStatusCode()); self::assertSame('ok', $response->getContent()); self::assertSame(1, $kernel->terminations); self::assertTrue($kernel->outcome?->successful); } #[Test] #[TestDox('HTTP exceptions render a response and still terminate')] public function fails(): void { $kernel = $this->kernel(new ThrowingMiddleware()); $previousLog = ini_get('error_log'); ini_set('error_log', '/dev/null'); try { $response = (new HttpRuntime($kernel, false))->handle(Request::create('/')); } finally { ini_set('error_log', (string) $previousLog); } self::assertSame(500, $response->getStatusCode()); self::assertSame(1, $kernel->terminations); self::assertFalse($kernel->outcome?->successful); self::assertInstanceOf(\RuntimeException::class, $kernel->outcome?->error); } #[Test] #[TestDox('Deferred termination failures do not replace the HTTP response')] public function preservesResponses(): void { $kernel = $this->kernel(new ResponseMiddleware(new Response('accepted', 202))); $kernel->terminationReport = new TerminationReport( failures: [new \RuntimeException('Deferred listener failed.')], ); $response = (new HttpRuntime($kernel, false))->handle(Request::create('/')); self::assertSame(202, $response->getStatusCode()); self::assertSame('accepted', $response->getContent()); } private function kernel(MiddlewareInterface $terminal): RuntimeKernel { return new RuntimeKernel( new RuntimeContainer([ TenantMiddleware::class => new PassMiddleware(), FirewallMiddleware::class => new PassMiddleware(), AuthenticationMiddleware::class => new PassMiddleware(), RouterMiddleware::class => $terminal, ]), new TenantContext($this->createStub(TenantService::class)), new IdentityContext(), ); } } final class RuntimeKernel implements KernelInterface { public bool $active = false; public int $terminations = 0; public ?ExecutionOutcome $outcome = null; public TerminationReport $terminationReport; public function __construct( private readonly ContainerInterface $services, private readonly TenantContext $tenantContext, private readonly IdentityContext $identityContext, ) { $this->terminationReport = new TerminationReport(); } public function boot(): void { } public function beginExecution(ExecutionDescriptor $descriptor): ExecutionScope { $this->active = true; return new ExecutionScope( $descriptor, ExecutionContext::fromDescriptor($descriptor), $this->tenantContext, $this->identityContext, ); } public function terminateExecution( ExecutionScope $scope, ExecutionOutcome $outcome, ): TerminationReport { $this->active = false; $this->terminations++; $this->outcome = $outcome; $scope->dispose(); $scope->markTerminated(); return $this->terminationReport; } public function shutdown(): void { } public function container(): ContainerInterface { return $this->services; } public function environment(): string { return 'test'; } public function debug(): bool { return false; } } final class RuntimeContainer implements ContainerInterface { public function __construct( private readonly array $services, ) { } public function get(string $id): mixed { return $this->services[$id]; } public function has(string $id): bool { return isset($this->services[$id]); } } class PassMiddleware implements MiddlewareInterface { public function process(Request $request, RequestHandlerInterface $handler): Response { return $handler->handle($request); } } final class StreamMiddleware extends PassMiddleware { public function __construct( private readonly \Closure $active, ) { } public function process(Request $request, RequestHandlerInterface $handler): Response { return new StreamedResponse(function (): void { echo ($this->active)() ? 'active' : 'terminated'; }); } } final class ResponseMiddleware extends PassMiddleware { public function __construct( private readonly Response $response, ) { } public function process(Request $request, RequestHandlerInterface $handler): Response { return $this->response; } } final class ThrowingMiddleware extends PassMiddleware { public function process(Request $request, RequestHandlerInterface $handler): Response { throw new \RuntimeException('HTTP failed.'); } }