get('/', fn() => 'get'); $router1->post('/', fn() => 'post'); $router1->delete('/', fn() => 'delete'); $router1->patch('/', fn() => 'patch'); $router1->put('/', fn() => 'put'); $router1->add('custom', '/', fn() => 'wacky'); $this->assertEquals('get', $router1->resolve('GET', '/')->dispatch([])); $this->assertEquals('wacky', $router1->resolve('CUSTOM', '/')->dispatch([])); $router1->use('/', function() { /* this one intentionally does nothing */ }); // registration order should matter $router1->use('/deep', fn() => 'deep'); $postRoot = $router1->resolve('POST', '/'); $this->assertNull($postRoot->runMiddleware([])); $this->assertEquals('post', $postRoot->dispatch([])); $this->assertEquals('deep', $router1->resolve('GET', '/deep/nothing')->runMiddleware([])); $router1->use('/user/([A-Za-z0-9]+)/below', fn(string $user) => 'warioware below ' . $user); $router1->get('/user/static', fn() => 'the static one'); $router1->get('/user/static/below', fn() => 'below the static one'); $router1->get('/user/([A-Za-z0-9]+)', fn(string $user) => $user); $router1->get('/user/([A-Za-z0-9]+)/below', fn(string $user) => 'below ' . $user); $this->assertEquals('below the static one', $router1->resolve('GET', '/user/static/below')->dispatch([])); $getWariowareBelowFlashwave = $router1->resolve('GET', '/user/flashwave/below'); $this->assertEquals('warioware below flashwave', $getWariowareBelowFlashwave->runMiddleware([])); $this->assertEquals('below flashwave', $getWariowareBelowFlashwave->dispatch([])); $router2 = new HttpRouter; $router2->use('/', fn() => 'meow'); $router2->get('/rules', fn() => 'rules page'); $router2->get('/contact', fn() => 'contact page'); $router2->get('/25252', fn() => 'numeric test'); $getRules = $router2->resolve('GET', '/rules'); $this->assertEquals('meow', $getRules->runMiddleware([])); $this->assertEquals('rules page', $getRules->dispatch([])); $get25252 = $router2->resolve('GET', '/25252'); $this->assertEquals('meow', $get25252->runMiddleware([])); $this->assertEquals('numeric test', $get25252->dispatch([])); $router3 = $router1->scopeTo('/scoped'); $router3->get('/static', fn() => 'wrong'); $router1->get('/scoped/static/0', fn() => 'correct'); $router3->get('/variable', fn() => 'wrong'); $router3->get('/variable/([0-9]+)', fn(string $num) => $num === '0' ? 'correct' : 'VERY wrong'); $router3->get('/variable/([a-z]+)', fn(string $char) => $char === 'a' ? 'correct' : 'VERY wrong'); $this->assertEquals('correct', $router3->resolve('GET', '/static/0')->dispatch([])); $this->assertEquals('correct', $router1->resolve('GET', '/scoped/variable/0')->dispatch([])); $this->assertEquals('correct', $router3->resolve('GET', '/variable/a')->dispatch([])); } public function testAttribute(): void { $router = new HttpRouter; $handler = new class implements RouteHandler { use RouteHandlerTrait; #[HttpGet('/')] public function getIndex(): string { return 'index'; } #[HttpPost('/avatar')] public function postAvatar(): string { return 'avatar'; } #[HttpPut('/static')] public static function putStatic(): string { return 'static'; } #[HttpGet('/meow')] #[HttpPost('/meow')] public function multiple(): string { return 'meow'; } #[HttpMiddleware('/mw')] public function useMw(): string { return 'this intercepts'; } #[HttpGet('/mw')] public function getMw(): string { return 'this is intercepted'; } public function hasNoAttr(): string { return 'not a route'; } }; $router->register($handler); $this->assertFalse($router->resolve('GET', '/soap')->hasHandler()); $patchAvatar = $router->resolve('PATCH', '/avatar'); $this->assertFalse($patchAvatar->hasHandler()); $this->assertTrue($patchAvatar->hasOtherMethods()); $this->assertEquals(['POST'], $patchAvatar->getSupportedMethods()); $this->assertEquals('index', $router->resolve('GET', '/')->dispatch([])); $this->assertEquals('avatar', $router->resolve('POST', '/avatar')->dispatch([])); $this->assertEquals('static', $router->resolve('PUT', '/static')->dispatch([])); $this->assertEquals('meow', $router->resolve('GET', '/meow')->dispatch([])); $this->assertEquals('meow', $router->resolve('POST', '/meow')->dispatch([])); // stopping on middleware is the dispatcher's job $getMw = $router->resolve('GET', '/mw'); $this->assertEquals('this intercepts', $getMw->runMiddleware([])); $this->assertEquals('this is intercepted', $getMw->dispatch([])); $scoped = $router->scopeTo('/scoped'); $scoped->register($handler); $this->assertEquals('index', $scoped->resolve('GET', '/')->dispatch([])); $this->assertEquals('avatar', $router->resolve('POST', '/scoped/avatar')->dispatch([])); $this->assertEquals('static', $scoped->resolve('PUT', '/static')->dispatch([])); $this->assertEquals('meow', $router->resolve('GET', '/scoped/meow')->dispatch([])); $this->assertEquals('meow', $scoped->resolve('POST', '/meow')->dispatch([])); } public function testEEPROMSituation(): void { $router = new HttpRouter; $router->options('/uploads/([A-Za-z0-9\-_]+)(?:\.(t|json))?', function() {}); $router->get('/uploads/([A-Za-z0-9\-_]+)(?:\.(t|json))?', function() {}); $router->delete('/uploads/([A-Za-z0-9\-_]+)', function() {}); $resolved = $router->resolve('DELETE', '/uploads/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'); $this->assertEquals(['OPTIONS', 'GET', 'DELETE'], $resolved->getSupportedMethods()); } public function testMiddlewareInterceptionOnRoot(): void { $router = new HttpRouter; $router->use('/', fn() => 'expected'); $router->get('/', fn() => 'unexpected'); $router->get('/test', fn() => 'also unexpected'); ob_start(); $router->dispatch(new HttpRequest('::1', true, 'NL', '1.1', 'GET', '/', [], [], new HttpHeaders([]), null)); $this->assertEquals('expected', ob_get_clean()); ob_start(); $router->dispatch(new HttpRequest('::1', true, 'US', '1.1', 'GET', '/test', [], [], new HttpHeaders([]), null)); $this->assertEquals('expected', ob_get_clean()); ob_start(); $router->dispatch(new HttpRequest('::1', true, 'GB', '1.1', 'GET', '/error', [], [], new HttpHeaders([]), null)); $this->assertEquals('expected', ob_get_clean()); } }