log = new ArrayObject(); $this->resolver = new FakeResolver(); $this->resolver->bind(RealisticPublicController::class, new RealisticPublicController()); $this->resolver->bind(RealisticAdminController::class, new RealisticAdminController()); $this->resolver->bind(AdminLoggingMiddleware::class, new AdminLoggingMiddleware($this->log)); $this->resolver->bind(CsrfLoggingMiddleware::class, new CsrfLoggingMiddleware($this->log)); $this->router = new Router($this->resolver); $this->router->group('/admin', [AdminLoggingMiddleware::class]); $this->router->group('/admin', [CsrfLoggingMiddleware::class], ['POST']); $this->router->registerController(RealisticPublicController::class); $this->router->registerController(RealisticAdminController::class); } public function testRouteTableCompilesToTwentyTwoRoutes(): void { $loaded = count((new RouteLoader())->load(RealisticPublicController::class)) + count((new RouteLoader())->load(RealisticAdminController::class)); $this->assertSame(22, $loaded); } public function testLiteralPreviewBeatsParameterizedId(): void { $this->assertSame( 'entries-preview-literal', $this->dispatch('POST', '/admin/entries/preview')->body(), ); } public function testInjectsSlugAndIdParameters(): void { $this->assertSame('entry-show:hello-world', $this->dispatch('GET', '/entries/hello-world')->body()); $this->assertSame('note-show:42', $this->dispatch('GET', '/notes/42')->body()); $this->assertSame('entries-edit:7', $this->dispatch('GET', '/admin/entries/7/edit')->body()); $this->assertSame('notes-edit:9', $this->dispatch('GET', '/admin/notes/9/edit')->body()); } public function testLiteralSegmentsWithDotsMatch(): void { $this->assertSame('feed', $this->dispatch('GET', '/feed.xml')->body()); $this->assertSame('robots', $this->dispatch('GET', '/robots.txt')->body()); $this->assertSame('sitemap', $this->dispatch('GET', '/sitemap.xml')->body()); } public function testAdminGroupThenCsrfGroupOrderOnPost(): void { $this->dispatch('POST', '/admin/entries/preview'); $this->assertSame( ['admin:before', 'csrf:before', 'csrf:after', 'admin:after'], $this->log->getArrayCopy(), ); } public function testCsrfGroupSkippedForGet(): void { $this->dispatch('GET', '/admin/notes'); $this->assertSame(['admin:before', 'admin:after'], $this->log->getArrayCopy()); } public function testNotFoundStillThrows(): void { $this->expectException(RouteNotFoundException::class); $this->dispatch('GET', '/missing'); } public function testFiveLayerOnionOrder(): void { $order = new ArrayObject(); $mk = static function (string $name) use ($order): MiddlewareInterface { return new class ($name, $order) implements MiddlewareInterface { /** @param ArrayObject $order */ public function __construct( private readonly string $name, private readonly ArrayObject $order, ) { } public function handle(RequestInterface $request, callable $next): ResponseInterface { $this->order->append($this->name . ':before'); $response = $next($request); $this->order->append($this->name . ':after'); return $response; } }; }; $core = static function (RequestInterface $request) use ($order): ResponseInterface { $order->append('core'); return new TestResponse(200, 'ok'); }; $pipeline = MiddlewarePipeline::compose([$mk('A'), $mk('B'), $mk('C'), $mk('D'), $mk('E')], $core); $pipeline(new TestRequest('GET', '/')); $this->assertSame( ['A:before', 'B:before', 'C:before', 'D:before', 'E:before', 'core', 'E:after', 'D:after', 'C:after', 'B:after', 'A:after'], $order->getArrayCopy(), ); } private function dispatch(string $method, string $path): ResponseInterface { return $this->router->dispatch(new TestRequest($method, $path)); } }