diff --git a/VERSION b/VERSION index fe1507c..6cefb80 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2403.282326 +0.2403.301624 diff --git a/src/Http/HttpFx.php b/src/Http/HttpFx.php deleted file mode 100644 index 23d4f2d..0000000 --- a/src/Http/HttpFx.php +++ /dev/null @@ -1,290 +0,0 @@ -router = $router ?? new Router; - - $this->addObjectHandler( - fn(object $object) => $object instanceof Stream, - function(HttpResponseBuilder $responseBuilder, object $object) { - if(!$responseBuilder->hasContentType()) - $responseBuilder->setTypeStream(); - $responseBuilder->setContent(new StreamContent($object)); - } - ); - - $this->addObjectHandler( - fn(object $object) => $object instanceof JsonSerializable || $object instanceof stdClass, - function(HttpResponseBuilder $responseBuilder, object $object) { - if(!$responseBuilder->hasContentType()) - $responseBuilder->setTypeJson(); - $responseBuilder->setContent(new JsonContent($object)); - } - ); - - $this->addObjectHandler( - fn(object $object) => $object instanceof IBencodeSerialisable, - function(HttpResponseBuilder $responseBuilder, object $object) { - if(!$responseBuilder->hasContentType()) - $responseBuilder->setTypePlain(); - $responseBuilder->setContent(new BencodedContent($object)); - } - ); - } - - public function getRouter(): Router { - return $this->router; - } - - public function addObjectHandler(callable $match, callable $handler): void { - $this->objectHandlers[] = [$match, $handler]; - } - - public function addErrorHandler(int $code, callable $handler): void { - $this->errorHandlers[$code] = $handler; - } - - public function setDefaultErrorHandler(callable $handler): void { - $this->defaultErrorHandler = $handler; - } - - public function restoreDefaultErrorHandler(): void { - $this->defaultErrorHandler = [self::class, 'defaultErrorHandler']; - } - - public function dispatch(?HttpRequest $request = null, array $args = []): void { - $request ??= HttpRequest::fromRequest(); - $responseBuilder = new HttpResponseBuilder; - $handlers = null; - - try { - $handlers = $this->router->resolve($request->getMethod(), $request->getPath(), array_merge([ - $responseBuilder, $request, - ], $args)); - } catch(RoutePathNotFoundException $ex) { - $statusCode = 404; - } catch(RouteMethodNotSupportedException $ex) { - $statusCode = 405; - } catch(Exception $ex) { - if(Environment::isDebug()) - throw $ex; - } - - if($handlers === null) { - $this->errorPage($responseBuilder, $request, $statusCode ?? 500); - } else { - $result = $handlers->run(); - - if(is_int($result)) { - if(!$responseBuilder->hasStatusCode() && $result >= 100 && $result < 600) { - $this->errorPage($responseBuilder, $request, $result); - } elseif(!$responseBuilder->hasContent()) { - $responseBuilder->setContent(new StringContent((string)$result)); - } - } elseif(!$responseBuilder->hasContent()) { - if(is_array($result)) { - if(!$responseBuilder->hasContentType()) - $responseBuilder->setTypeJson(); - $responseBuilder->setContent(new JsonContent($result)); - } elseif(is_object($result)) { - foreach($this->objectHandlers as $info) - if($info[0]($result)) { - $info[1]($responseBuilder, $result); - break; - } - } - - if(!$responseBuilder->hasContent() && $result !== null) { - $result = (string)$result; - $responseBuilder->setContent(new StringContent($result)); - - if(!$responseBuilder->hasContentType()) { - if(strtolower(substr($result, 0, 14)) === 'setTypeHTML('utf-8'); - else { - $charset = strtolower(mb_preferred_mime_name(mb_detect_encoding($result))); - - if(strtolower(substr($result, 0, 5)) === 'setTypeXML($charset); - else - $responseBuilder->setTypePlain($charset); - } - } - } - } - } - - self::output($responseBuilder->toResponse()); - } - - public static function defaultErrorHandler( - HttpResponseBuilder $responseBuilder, - HttpRequest $request, - int $code, - string $message - ): void { - $responseBuilder->setTypeHTML(); - $responseBuilder->setContent(new StringContent(sprintf( - '%1$03d %2$s

%1$03d %2$s


Index
', - $code, - $message, - strtolower(mb_preferred_mime_name(mb_internal_encoding())) - ))); - } - - public function errorPage( - HttpResponseBuilder $responseBuilder, - HttpRequest $request, - int $statusCode - ): void { - $responseBuilder->setStatusCode($statusCode); - $responseBuilder->clearStatusText(); - if(!$responseBuilder->hasContent()) - ($this->errorHandlers[$statusCode] ?? $this->defaultErrorHandler)( - $responseBuilder, - $request, - $responseBuilder->getStatusCode(), - $responseBuilder->getStatusText() - ); - } - - public static function output(HttpResponse $response): void { - $version = $response->getHttpVersion(); - header(sprintf( - 'HTTP/%d.%d %03d %s', - $version->getMajor(), - $version->getMinor(), - $response->getStatusCode(), - $response->getStatusText() - )); - - $headers = $response->getHeaders(); - foreach($headers as $header) { - $name = (string)$header->getName(); - $lines = $header->getLines(); - - foreach($lines as $line) - header(sprintf('%s: %s', $name, (string)$line)); - } - - if($response->hasContent()) - echo (string)$response->getContent(); - } - - /** - * Apply middleware functions to a path. - * - * @param string $path Path to apply the middleware to. - * @param callable $handler Middleware function. - */ - public function use(string $path, callable $handler): void { - $this->router->use($path, $handler); - } - - /** - * Adds a new route. - * - * @param string $method Request method. - * @param string $path Request path. - * @param callable $handler Request handler. - */ - public function add(string $method, string $path, callable $handler): void { - $this->router->add($method, $path, $handler); - } - - /** - * Adds a new GET route. - * - * @param string $path Request path. - * @param callable $handler Request handler. - */ - public function get(string $path, callable $handler): void { - $this->router->add('get', $path, $handler); - } - - /** - * Adds a new POST route. - * - * @param string $path Request path. - * @param callable $handler Request handler. - */ - public function post(string $path, callable $handler): void { - $this->router->add('post', $path, $handler); - } - - /** - * Adds a new DELETE route. - * - * @param string $path Request path. - * @param callable $handler Request handler. - */ - public function delete(string $path, callable $handler): void { - $this->router->add('delete', $path, $handler); - } - - /** - * Adds a new PATCH route. - * - * @param string $path Request path. - * @param callable $handler Request handler. - */ - public function patch(string $path, callable $handler): void { - $this->router->add('patch', $path, $handler); - } - - /** - * Adds a new PUT route. - * - * @param string $path Request path. - * @param callable $handler Request handler. - */ - public function put(string $path, callable $handler): void { - $this->router->add('put', $path, $handler); - } - - /** - * Adds a new OPTIONS route. - * - * @param string $path Request path. - * @param callable $handler Request handler. - */ - public function options(string $path, callable $handler): void { - $this->router->add('options', $path, $handler); - } - - /** - * Registers routes in an IRouteHandler implementation. - * - * @param IRouteHandler $handler Routes handler. - */ - public function register(IRouteHandler $handler): void { - $handler->registerRoutes($this); - } -} diff --git a/src/Routing/IRouteHandler.php b/src/Routing/IRouteHandler.php deleted file mode 100644 index adadb91..0000000 --- a/src/Routing/IRouteHandler.php +++ /dev/null @@ -1,18 +0,0 @@ -method = null; - $this->path = $pathOrMethod; - } else { - $this->method = $pathOrMethod; - $this->path = $path; - } - } - - /** - * Returns the target method name. - * - * @return ?string - */ - public function getMethod(): ?string { - return $this->method; - } - - /** - * Whether this route should be used as middleware. - * - * @return bool - */ - public function isMiddleware(): bool { - return $this->method === null; - } - - /** - * Returns the target path. - * - * @return string - */ - public function getPath(): string { - return $this->path; - } - - /** - * Reads attributes from methods in a IRouteHandler instance and registers them to a given IRouter instance. - * - * @param IRouter $router Router instance. - * @param IRouteHandler $handler Handler instance. - */ - public static function handleAttributes(IRouter $router, IRouteHandler $handler): void { - $objectInfo = new ReflectionObject($handler); - $methodInfos = $objectInfo->getMethods(); - - foreach($methodInfos as $methodInfo) { - $attrInfos = $methodInfo->getAttributes(Route::class); - - foreach($attrInfos as $attrInfo) { - $routeInfo = $attrInfo->newInstance(); - $closure = $methodInfo->getClosure($methodInfo->isStatic() ? null : $handler); - - if($routeInfo->isMiddleware()) - $router->use($routeInfo->getPath(), $closure); - else - $router->add($routeInfo->getMethod(), $routeInfo->getPath(), $closure); - } - } - } -} diff --git a/src/Routing/RouteCallable.php b/src/Routing/RouteCallable.php deleted file mode 100644 index dcbe073..0000000 --- a/src/Routing/RouteCallable.php +++ /dev/null @@ -1,72 +0,0 @@ -callables = $callables; - $this->args = $args; - } - - /** - * Callables in order that they should be executed. - * - * @return array Sequential list of callables. - */ - public function getCallables(): array { - return $this->callables; - } - - /** - * Arguments to be sent to each callable. - * - * @return array Sequential argument list for the callables. - */ - public function getArguments(): array { - return $this->args; - } - - /** - * Runs all callables and returns their returns as an array. - * - * @return array Results from the callables. - */ - public function runAll(): array { - $results = []; - - foreach($this->callables as $callable) { - $result = $callable(...$this->args); - if($result !== null) - $results[] = $result; - } - - return $results; - } - - /** - * Runs all callables unless one returns something. - * - * @return mixed Result from the returning callable. - */ - public function run(): mixed { - foreach($this->callables as $callable) { - $result = $callable(...$this->args); - if($result !== null) - return $result; - } - - return null; - } -} diff --git a/src/Routing/RouteHandler.php b/src/Routing/RouteHandler.php deleted file mode 100644 index 5b6ed52..0000000 --- a/src/Routing/RouteHandler.php +++ /dev/null @@ -1,14 +0,0 @@ -dynamicChild ?? ($this->dynamicChild = new RouteInfo); - else - $child = $this->children[$name] ?? ($this->children[$name] = new RouteInfo); - - $next = $parts[1] ?? ''; - - return $child; - } - - /** - * @internal - */ - public function addMiddleware(string $path, callable $handler): void { - if($path === '') { - $this->middlewares[] = $handler; - return; - } - - $this->getChild($path, $next)->addMiddleware($next, $handler); - } - - /** - * @internal - */ - public function addMethod(string $method, string $path, callable $handler): void { - if($path === '') { - $this->methods[strtolower($method)] = $handler; - return; - } - - $this->getChild($path, $next)->addMethod($method, $next, $handler); - } - - /** - * @internal - */ - public function resolve(string $method, string $path, array $args = [], array $callables = []): RouteCallable { - if($path === '') { - $method = strtolower($method); - $handlers = []; - - if(isset($this->methods[$method])) { - $handlers[] = $this->methods[$method]; - } else { - if($method !== 'options') { - if(empty($this->methods)) - throw new RoutePathNotFoundException; - throw new RouteMethodNotSupportedException; - } - } - - return new RouteCallable( - array_merge($callables, $this->middlewares, $handlers), - $args - ); - } - - $parts = explode('/', $path, 2); - $name = $parts[0]; - $mName = '_' . $name; - - foreach($this->children as $cName => $cObj) - if($cName === $mName) { - $child = $cObj; - break; - } - - if(!isset($child)) { - $args[] = $name; - $child = $this->dynamicChild; - } - - if($child === null) - throw new RoutePathNotFoundException; - - return $child->resolve( - $method, - $parts[1] ?? '', - $args, - array_merge($callables, $this->middlewares) - ); - } -} diff --git a/src/Routing/RouteMethodNotSupportedException.php b/src/Routing/RouteMethodNotSupportedException.php deleted file mode 100644 index c9a6c61..0000000 --- a/src/Routing/RouteMethodNotSupportedException.php +++ /dev/null @@ -1,13 +0,0 @@ -route = new RouteInfo; - } - - /** - * Resolves a request method and uri. - * - * @param string $method Request method. - * @param string $path Request path. - * @param array $args Arguments to be passed on to the callables. - * @return RouteCallable A collection of callables representing the route. - */ - public function resolve(string $method, string $path, array $args = []): RouteCallable { - $method = strtolower($method); - if($method === 'head') - $method = 'get'; - - return $this->route->resolve($method, $path, $args); - } - - /** - * Apply middleware functions to a path. - * - * @param string $path Path to apply the middleware to. - * @param callable $handler Middleware function. - */ - public function use(string $path, callable $handler): void { - $this->route->addMiddleware($path, $handler); - } - - /** - * Adds a new route. - * - * @param string $method Request method. - * @param string $path Request path. - * @param callable $handler Request handler. - */ - public function add(string $method, string $path, callable $handler): void { - if(empty($method)) - throw new InvalidArgumentException('$method may not be empty.'); - - $this->route->addMethod($method, $path, $handler); - } - - /** - * Adds a new GET route. - * - * @param string $path Request path. - * @param callable $handler Request handler. - */ - public function get(string $path, callable $handler): void { - $this->add('get', $path, $handler); - } - - /** - * Adds a new POST route. - * - * @param string $path Request path. - * @param callable $handler Request handler. - */ - public function post(string $path, callable $handler): void { - $this->add('post', $path, $handler); - } - - /** - * Adds a new DELETE route. - * - * @param string $path Request path. - * @param callable $handler Request handler. - */ - public function delete(string $path, callable $handler): void { - $this->add('delete', $path, $handler); - } - - /** - * Adds a new PATCH route. - * - * @param string $path Request path. - * @param callable $handler Request handler. - */ - public function patch(string $path, callable $handler): void { - $this->add('patch', $path, $handler); - } - - /** - * Adds a new PUT route. - * - * @param string $path Request path. - * @param callable $handler Request handler. - */ - public function put(string $path, callable $handler): void { - $this->add('put', $path, $handler); - } - - /** - * Adds a new OPTIONS route. - * - * @param string $path Request path. - * @param callable $handler Request handler. - */ - public function options(string $path, callable $handler): void { - $this->add('options', $path, $handler); - } - - /** - * Registers routes in an IRouteHandler implementation. - * - * @param IRouteHandler $handler Routes handler. - */ - public function register(IRouteHandler $handler): void { - $handler->registerRoutes($this); - } -} diff --git a/tests/RouterTest.php b/tests/RouterTest.php index f88f3fb..d367efc 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -1,144 +1,115 @@ get('/', function() { - return 'get'; - }); - $router1->post('/', function() { - return 'post'; - }); - $router1->delete('/', function() { - return 'delete'; - }); - $router1->patch('/', function() { - return 'patch'; - }); - $router1->put('/', function() { - return 'put'; - }); - $router1->add('custom', '/', function() { - return 'wacky'; - }); + $router1->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', '/')->runAll()); - $this->assertEquals(['wacky'], $router1->resolve('CUSTOM', '/')->runAll()); + $this->assertEquals('get', $router1->resolve('GET', '/')->dispatch([])); + $this->assertEquals('wacky', $router1->resolve('CUSTOM', '/')->dispatch([])); - $router1->use('/', function() { - return 'warioware'; - }); - $router1->use('/deep', function() { - return 'deep'; - }); + $router1->use('/', function() { /* this one intentionally does nothing */ }); - $this->assertEquals(['warioware', 'post'], $router1->resolve('POST', '/')->runAll()); + // registration order should matter + $router1->use('/deep', fn() => 'deep'); - $router1->use('/user/:user/below', function(string $user) { - return 'warioware below ' . $user; - }); + $postRoot = $router1->resolve('POST', '/'); + $this->assertNull($postRoot->runMiddleware([])); + $this->assertEquals('post', $postRoot->dispatch([])); - $router1->get('/user/static', function() { - return 'the static one'; - }); - $router1->get('/user/static/below', function() { - return 'below the static one'; - }); - $router1->get('/user/:user', function(string $user) { - return $user; - }); - $router1->get('/user/:user/below', function(string $user) { - return 'below ' . $user; - }); + $this->assertEquals('deep', $router1->resolve('GET', '/deep/nothing')->runMiddleware([])); - $this->assertEquals( - ['warioware', 'below the static one'], - $router1->resolve('GET', '/user/static/below')->runAll() - ); - $this->assertEquals( - ['warioware', 'warioware below flashwave', 'below flashwave'], - $router1->resolve('GET', '/user/flashwave/below')->runAll() - ); + $router1->use('/user/([A-Za-z0-9]+)/below', fn(string $user) => 'warioware below ' . $user); - $router2 = new Router; - $router2->use('/', function() { - return 'meow'; - }); - $router2->get('/rules', function() { - return 'rules page'; - }); - $router2->get('/contact', function() { - return 'contact page'; - }); - $router2->get('/25252', function() { - return 'numeric test'; - }); + $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(['meow', 'rules page'], $router2->resolve('GET', '/rules')->runAll()); - $this->assertEquals(['meow', 'numeric test'], $router2->resolve('GET', '/25252')->runAll()); + $this->assertEquals('below the static one', $router1->resolve('GET', '/user/static/below')->dispatch([])); - $router3 = new Router; - $router3->get('/static', function() { - return 'wrong'; - }); - $router3->get('/static/0', function() { - return 'correct'; - }); - $router3->get('/variable', function() { - return 'wrong'; - }); - $router3->get('/variable/:num', function(string $num) { - return $num === '0' ? 'correct' : 'VERY wrong'; - }); + $getWariowareBelowFlashwave = $router1->resolve('GET', '/user/flashwave/below'); + $this->assertEquals('warioware below flashwave', $getWariowareBelowFlashwave->runMiddleware([])); + $this->assertEquals('below flashwave', $getWariowareBelowFlashwave->dispatch([])); - $this->assertEquals('correct', $router3->resolve('GET', '/static/0')->run()); - $this->assertEquals('correct', $router3->resolve('GET', '/variable/0')->run()); + $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 Router; + $router = new HttpRouter; $handler = new class extends RouteHandler { - #[Route('GET', '/')] + #[HttpGet('/')] public function getIndex() { return 'index'; } - #[Route('POST', '/avatar')] + #[HttpPost('/avatar')] public function postAvatar() { return 'avatar'; } - #[Route('PUT', '/static')] + #[HttpPut('/static')] public static function putStatic() { return 'static'; } - #[Route('GET', '/meow')] - #[Route('POST', '/meow')] + #[HttpGet('/meow')] + #[HttpPost('/meow')] public function multiple() { return 'meow'; } - #[Route('/mw')] + #[HttpMiddleware('/mw')] public function useMw() { return 'this intercepts'; } - #[Route('GET', '/mw')] + #[HttpGet('/mw')] public function getMw() { return 'this is intercepted'; } @@ -149,11 +120,31 @@ final class RouterTest extends TestCase { }; $router->register($handler); - $this->assertEquals('index', $router->resolve('GET', '/')->run()); - $this->assertEquals('avatar', $router->resolve('POST', '/avatar')->run()); - $this->assertEquals('static', $router->resolve('PUT', '/static')->run()); - $this->assertEquals('meow', $router->resolve('GET', '/meow')->run()); - $this->assertEquals('meow', $router->resolve('POST', '/meow')->run()); - $this->assertEquals('this intercepts', $router->resolve('GET', '/mw')->run()); + $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([])); } }