diff --git a/src/main/php/web/Application.class.php b/src/main/php/web/Application.class.php index fb404239..5e3dba29 100755 --- a/src/main/php/web/Application.class.php +++ b/src/main/php/web/Application.class.php @@ -32,7 +32,7 @@ public function environment() { return $this->environment; } */ public final function routing() { if (null === $this->routing) { - $routing= Routing::cast($this->routes(), true); + $routing= Routes::cast($this->routes(), true); $this->routing= $this->filters ? new Filters($this->filters, $routing) : $routing; } return $this->routing; diff --git a/src/main/php/web/Routes.class.php b/src/main/php/web/Routes.class.php new file mode 100755 index 00000000..17e35f27 --- /dev/null +++ b/src/main/php/web/Routes.class.php @@ -0,0 +1,128 @@ + handlers, which are passed to `route()` + * - A handler, which becomes the argument to `default()`. + * + * @param web.Handler|web.Application|function(web.Request, web.Response): var|[:var] $routes + * @param bool $top Whether this is the top-level routing + * @return self + */ + public static function cast($routes, $top= false) { + if ($routes instanceof self) { + $r= $routes; + } else if ($routes instanceof Application) { + $r= $routes->routing(); + } else if (is_array($routes)) { + $r= new self(); + foreach ($routes as $definition => $target) { + $r->route($definition, $target); + } + } else { + $r= (new self())->default($routes); + } + + $r->top= $top; + return $r; + } + + /** @return [:web.Handler] */ + public function routes() { return $this->routes; } + + /** + * Matches a given definition, routing it to the specified target. + * + * - `GET` matches GET requests + * - `GET /` matches GET requests to any path + * - `GET /test` matches GET requests inside /test + * - `GET|POST` matches GET and POST requests + * - `/` matches any request to any path + * - `/test` matches any request inside /test + * + * @param string $definition + * @param web.Handler|function(web.Request, web.Response): var $target + * @return self + */ + public function route($definition, $target) { + static $quote= ['#' => '\\#', '.' => '\\.']; + + $handler= $target instanceof Handler ? $target : new Call($target); + if ('/' === $definition[0]) { + $this->routes['#^[A-Z]+ '.strtr(rtrim($definition, '/'), $quote).'/#']= $handler; + } else { + sscanf($definition, "%[A-Z|] %[^\r]", $methods, $path); + $this->routes['#^'.$methods.' '.(null === $path ? '' : strtr(rtrim($path, '/'), $quote)).'/#']= $handler; + } + return $this; + } + + /** + * Maps all requests not otherwise mapped to a given target. + * + * @param web.Handler|function(web.Request, web.Response): var $target + * @return self + */ + public function default($target) { + $this->default= $target instanceof Handler ? $target : new Call($target); + return $this; + } + + /** + * Routes a request to the handler specified by this routing instance's + * routes. Throws a `CannotRoute` error if not route is matched and no + * default route exists. + * + * @param web.Request $request + * @return web.Handler + * @throws web.CannotRoute + */ + public function target($request) { + $match= $request->method().' '.rtrim($request->uri()->path(), '/').'/'; + foreach ($this->routes as $pattern => $handler) { + if (preg_match($pattern, $match)) return $handler; + } + if ($this->default) return $this->default; + + throw new CannotRoute($request); + } + + /** + * Handle a request + * + * @param web.Request $request + * @param web.Response $response + * @return var + */ + public function handle($request, $response) { + $seen= []; + + dispatch: $result= $this->target($request)->handle($request, $response); + if ($this->top && $result instanceof Dispatch) { + $seen[$request->uri()->hashCode()]= true; + $request->rewrite($result->uri()); + if (isset($seen[$request->uri()->hashCode()])) { + throw new Error(508, 'Internal redirect loop caused by dispatch to '.$result->uri()); + } + goto dispatch; + } + + return $result; + } +} \ No newline at end of file diff --git a/src/main/php/web/Routing.class.php b/src/main/php/web/Routing.class.php index 58d5dcd8..f597c728 100755 --- a/src/main/php/web/Routing.class.php +++ b/src/main/php/web/Routing.class.php @@ -7,6 +7,7 @@ * Routing takes care of directing the request to the correct target * by using one or more routes given to it. * + * @deprecated Use web.Routes instead! * @test web.unittest.RoutingTest */ class Routing implements Handler { @@ -29,7 +30,7 @@ public static function cast($routes, $top= false) { if ($routes instanceof self) { $r= $routes; } else if ($routes instanceof Application) { - $r= $routes->routing(); + $r= self::cast($routes->routes(), true); } else if (is_array($routes)) { $r= new self(); foreach ($routes as $definition => $target) { diff --git a/src/test/php/web/unittest/ApplicationTest.class.php b/src/test/php/web/unittest/ApplicationTest.class.php index 89d8beac..fe49dafc 100755 --- a/src/test/php/web/unittest/ApplicationTest.class.php +++ b/src/test/php/web/unittest/ApplicationTest.class.php @@ -4,7 +4,7 @@ use test\{Assert, Expect, Test, Values}; use util\Objects; use web\io\{TestInput, TestOutput}; -use web\{Application, Environment, Error, Filter, Filters, Handler, Request, Response, Routing}; +use web\{Application, Environment, Error, Filter, Filters, Handler, Request, Response, Routes}; class ApplicationTest { private $environment; @@ -69,7 +69,7 @@ public function routes() { /* Implementation irrelevant for this test */ } #[Test] public function routing() { - $routing= new Routing(); + $routing= new Routes(); $app= newinstance(Application::class, [$this->environment], [ 'routes' => function() use($routing) { return $routing; } ]); @@ -78,7 +78,7 @@ public function routing() { #[Test] public function routes_only_called_once() { - $routing= new Routing(); + $routing= new Routes(); $called= 0; $app= newinstance(Application::class, [$this->environment], [ 'routes' => function() use($routing, &$called) { @@ -95,7 +95,7 @@ public function routes_only_called_once() { #[Test] public function with_routing() { $this->assertHandled($handled, function() use(&$handled) { - return (new Routing())->fallbacks(function($request, $response) use(&$handled) { + return (new Routes())->default(function($request, $response) use(&$handled) { $handled[]= [$request, $response]; }); }); diff --git a/src/test/php/web/unittest/RoutingTest.class.php b/src/test/php/web/unittest/RoutingTest.class.php index 419f3667..01d4da7e 100755 --- a/src/test/php/web/unittest/RoutingTest.class.php +++ b/src/test/php/web/unittest/RoutingTest.class.php @@ -5,6 +5,7 @@ use web\routing\{CannotRoute, Target}; use web\{Application, Environment, Filters, Handler, Request, Response, Route, Routing}; +/** @deprecated */ class RoutingTest { private $handlers;