From b98bd80a6c27e3011da08e7ee4b049e77547bdb8 Mon Sep 17 00:00:00 2001 From: Douglas Silva Date: Sat, 21 Mar 2020 14:35:02 -0300 Subject: [PATCH] Redesigned the Router component --- src/Fragments/Component/Request.php | 18 +- .../Component/Routing/Model/Route.php | 112 ++++++++++++ .../ParserInterface.php} | 78 +++------ .../Component/Routing/Parser/XMLParser.php | 51 ++++++ .../Component/Routing/RequestMatcher.php | 138 --------------- src/Fragments/Component/Routing/Route.php | 60 ------- src/Fragments/Component/Routing/Router.php | 163 ++++++++++-------- src/Fragments/Component/Routing/XMLParser.php | 54 ------ 8 files changed, 301 insertions(+), 373 deletions(-) create mode 100644 src/Fragments/Component/Routing/Model/Route.php rename src/Fragments/Component/Routing/{RequestContext.php => Parser/ParserInterface.php} (56%) create mode 100644 src/Fragments/Component/Routing/Parser/XMLParser.php delete mode 100644 src/Fragments/Component/Routing/RequestMatcher.php delete mode 100644 src/Fragments/Component/Routing/Route.php delete mode 100644 src/Fragments/Component/Routing/XMLParser.php diff --git a/src/Fragments/Component/Request.php b/src/Fragments/Component/Request.php index 9ae8dfe..22d8dd4 100644 --- a/src/Fragments/Component/Request.php +++ b/src/Fragments/Component/Request.php @@ -21,6 +21,8 @@ namespace Fragments\Component; +use Fragments\Component\Routing\Router; + /** * Server Request Utility * @@ -53,11 +55,23 @@ public function get(string $value) } /** - * Redirects the web browser to the specified URI. + * Redirects the client to the specified path. */ - public function redirect(string $path) + public function redirect(string $routeId) { header('Location: ' . $path, true, 301); exit; } + + /** + * Generates a path from a route ID and redirects to it. + */ + public function redirectToRoute(string $routeId) + { + $router = new Router; + $path = $router->generateUrl($routeId); + + header('Location: ' . $path, true, 301); + exit; + } } diff --git a/src/Fragments/Component/Routing/Model/Route.php b/src/Fragments/Component/Routing/Model/Route.php new file mode 100644 index 0000000..4ec3f73 --- /dev/null +++ b/src/Fragments/Component/Routing/Model/Route.php @@ -0,0 +1,112 @@ +. + */ + +namespace Fragments\Component\Routing\Model; + +class Route +{ + /** + * The route identifier. + */ + private $id; + + /** + * The associated URL. + */ + private $path; + + /** + * The fully qualified class name of the controller. + */ + private $controller; + + /** + * The class method to be executed. + */ + private $action; + + /** + * The request methods supported. + */ + private $methods; + + public function getId(): string + { + return $this->id; + } + + public function setId(string $id) + { + $this->id = $id; + + return $this; + } + + public function getPath(): string + { + return $this->path; + } + + public function setPath(string $path) + { + $this->path = $path; + + return $this; + } + + public function getController(): string + { + return $this->controller; + } + + public function setController(string $controller) + { + $this->controller = $controller; + + return $this; + } + + public function getAction(): string + { + return $this->action; + } + + public function setAction(string $action) + { + $this->action = $action; + + return $this; + } + + public function getMethods(): array + { + return $this->methods; + } + + public function setMethods(string $methods) + { + // Store them in an array + $methods = explode('|', $methods); + $this->methods = $methods; + + return $this; + } +} diff --git a/src/Fragments/Component/Routing/RequestContext.php b/src/Fragments/Component/Routing/Parser/ParserInterface.php similarity index 56% rename from src/Fragments/Component/Routing/RequestContext.php rename to src/Fragments/Component/Routing/Parser/ParserInterface.php index 63e7055..618c214 100644 --- a/src/Fragments/Component/Routing/RequestContext.php +++ b/src/Fragments/Component/Routing/Parser/ParserInterface.php @@ -1,51 +1,27 @@ -. - */ - -namespace Fragments\Component\Routing; - -use Fragments\Component\Request; - -/** - * Request context. - * - * Stores information about the HTTP request. - */ -class RequestContext -{ - public $uri; - - public $requestMethod; - - public function __construct() - { - $request = new Request; - - $uri = $request->getURI(); - - // Remove trailing slash from path, except for the root - if ($uri !== '/') { - $uri = rtrim($uri, '/'); - } - - $this->uri = $uri; - $this->requestMethod = $request->requestMethod(); - } -} +. + */ + +namespace Fragments\Component\Routing\Parser; + +interface ParserInterface +{ + public function getRoutes(): array; +} \ No newline at end of file diff --git a/src/Fragments/Component/Routing/Parser/XMLParser.php b/src/Fragments/Component/Routing/Parser/XMLParser.php new file mode 100644 index 0000000..389bc2e --- /dev/null +++ b/src/Fragments/Component/Routing/Parser/XMLParser.php @@ -0,0 +1,51 @@ +. + */ + +namespace Fragments\Component\Routing\Parser; + +use Fragments\Bundle\Exception\ServerErrorHttpException; +use Fragments\Component\Routing\Model\Route; + +class XMLParser implements ParserInterface +{ + public function getRoutes(): array + { + if (!file_exists('../config/routes.xml')) { + throw new ServerErrorHttpException('The route definition file is missing.'); + } + + $file = simplexml_load_file('../config/routes.xml'); + $routes = []; + + foreach ($file as $entry) { + $route = new Route; + $route->setId($entry->id); + $route->setPath($entry->path); + $route->setMethods($entry->methods); + $route->setController($entry->controller); + $route->setAction($entry->action); + + $routes[] = $route; + } + + return $routes; + } +} diff --git a/src/Fragments/Component/Routing/RequestMatcher.php b/src/Fragments/Component/Routing/RequestMatcher.php deleted file mode 100644 index cf1b080..0000000 --- a/src/Fragments/Component/Routing/RequestMatcher.php +++ /dev/null @@ -1,138 +0,0 @@ -. - */ - -namespace Fragments\Component\Routing; - -use Fragments\Bundle\Exception\MethodNotAllowedHttpException; - -/** - * Request matcher - * - * Combines route and request information to determine - * the path to the requested resource. - */ -class RequestMatcher -{ - /** - * Request context. - */ - private $context; - - /** - * Route collection. - * - * Route objects organized in an array. - */ - private $routes; - - /** - * When a matching route is found, its name is stored here. - */ - public $matchedRouteName; - - /** - * The parameter to be passed to the controller. - */ - public $parameter; - - public function __construct($routeCollection, RequestContext $context) - { - $this->context = $context; - $this->routes = $routeCollection; - } - - public function match() - { - foreach ($this->routes as $name => $route) { - if ($this->testWildcard($route) === true) { - $this->matchedRouteName = $name; - } - } - } - - private function testWildcard(Route $route): bool - { - if ($this->containsWildcard($route) === true) { - return $this->matchPathWithWildcard($route); - } - - return $this->matchPathWithoutWildcard($route); - } - - /** - * Test the URI and its wildcard against the registered route - * and retrieve the parameter. - */ - private function matchPathWithWildcard(Route $route): bool { - $path = $route->path; - $pattern = '/\/{alpha}/'; - $replacement = ''; - - $prefix = preg_replace($pattern, $replacement, $path); - - // Using ~ as the regex delimiter to prevent conflict - $prefix = '~^' . $prefix . '\/' . '(?[a-zA-Z0-9_]+)$~'; - - if (true == preg_match($prefix, $this->context->uri, $match)) { - if (!in_array($this->context->requestMethod, $route->methods)) { - throw new MethodNotAllowedHttpException; - } - - $this->parameter = $match['alpha']; - - return true; - } - - return false; - } - - /** - * Test the URI against the registered route. - */ - private function matchPathWithoutWildcard(Route $route): bool - { - if ($route->path !== $this->context->uri) { - return false; - } - - if (!in_array($this->context->requestMethod, $route->methods)) { - throw new MethodNotAllowedHttpException; - } - - return true; - } - - /** - * Returns true if the path contains a wildcard, - * such as {alpha}. - */ - private function containsWildcard(Route $route): bool - { - $path = $route->path; - $pattern = '/{alpha}/'; - - if (preg_match($pattern, $path) == true) { - return true; - } - - return false; - } -} diff --git a/src/Fragments/Component/Routing/Route.php b/src/Fragments/Component/Routing/Route.php deleted file mode 100644 index 9fed530..0000000 --- a/src/Fragments/Component/Routing/Route.php +++ /dev/null @@ -1,60 +0,0 @@ -. - */ - -namespace Fragments\Component\Routing; - -/** - * A route description - */ -class Route -{ - /** - * The requested URI. - * - * Example: /profile/username - */ - public $path; - - /** - * The fully qualified class name of the controller. - */ - public $controller; - - /** - * The configured action. A method name. - */ - public $action; - - /** - * The request methods supported. - * - * Example: POST, GET. - */ - public $methods; - - public function __construct($path, $controller, $action, $methods) - { - $this->path = $path; - $this->controller = $controller; - $this->action = $action; - $this->methods = explode('|', $methods); - } -} diff --git a/src/Fragments/Component/Routing/Router.php b/src/Fragments/Component/Routing/Router.php index 8d49a42..ac5557c 100644 --- a/src/Fragments/Component/Routing/Router.php +++ b/src/Fragments/Component/Routing/Router.php @@ -1,68 +1,95 @@ -. - */ - -namespace Fragments\Component\Routing; - -use Fragments\Bundle\Exception\NotFoundHttpException; - -/** - * The router controller. - * - * Controls the routing process. Influenced by Symfony's - * routing component. - */ -class Router -{ - private $parameter; - - public function start() - { - $routeLoader = new XMLParser; - $routeCollection = $routeLoader->getRouteCollection(); - - $context = new RequestContext; - - $matcher = new RequestMatcher($routeCollection, $context); - $matcher->match(); - - if (!$matcher->matchedRouteName) { - throw new NotFoundHttpException('The page you were looking for could not be found.'); - } - - if ($matcher->parameter) { - $this->parameter = $matcher->parameter; - } - - $routeName = $matcher->matchedRouteName; - $matchedRoute = $routeCollection[$routeName]; - - $this->loadRoute($matchedRoute); - } - - private function loadRoute(Route $matchedRoute) - { - $controller = $matchedRoute->controller; - $action = $matchedRoute->action; - - $controller = new $controller; - $controller->{$action}($this->parameter); - } -} +. + */ + +namespace Fragments\Component\Routing; + +use Fragments\Component\Routing\Model\Route; +use Fragments\Component\Routing\Parser\XMLParser; +use Fragments\Component\Request; +use Fragments\Bundle\Exception\NotFoundHttpException; +use Fragments\Bundle\Exception\MethodNotAllowedHttpException; + +class Router +{ + private $parser; + + private $request; + + public function __construct() + { + $this->parser = new XMLParser; + $this->request = new Request; + } + + public function start() + { + $routes = $this->parser->getRoutes(); + $route = $this->matchRoute($routes); + + $this->load($route); + } + + private function matchRoute(array $routes): Route + { + foreach ($routes as $route) { + if ($route->getPath() != $this->request->getURI()) { + continue; + } + + if (!in_array($this->request->requestMethod(), $route->getMethods())) { + throw new MethodNotAllowedHttpException; + } + + return $route; + } + + throw new NotFoundHttpException('Route not found.'); + } + + private function load(Route $route) + { + $controller = $route->getController(); + $action = $route->getAction(); + + $controller = new $controller; + $controller->{$action}(); + } + + private function getRouteById(string $routeId): Route + { + $routes = $this->parser->getRoutes(); + + foreach ($routes as $route) { + if ($route->getId() == $routeId) { + return $route; + } + } + + throw new NotFoundHttpException('Route not found.'); + } + + public function generateUrl(string $routeId): string + { + $route = $this->getRouteById($routeId); + + return $route->getPath(); + } +} diff --git a/src/Fragments/Component/Routing/XMLParser.php b/src/Fragments/Component/Routing/XMLParser.php deleted file mode 100644 index 1978675..0000000 --- a/src/Fragments/Component/Routing/XMLParser.php +++ /dev/null @@ -1,54 +0,0 @@ -. - */ - -namespace Fragments\Component\Routing; - -/** - * XML Loader - * - * Populates route objects using data from an XML file - * - * @author Douglas Silva <0x9fd287d56ec107ac> - */ -class XMLParser -{ - private $routes = []; - - public function __construct() - { - $routing = simplexml_load_file('../config/routes.xml'); - - foreach ($routing->route as $route) { - $id = (string)$route->id; - $path = (string)$route->path; - $methods = (string)$route->methods; - $controller = (string)$route->controller; - $action = (string)$route->action; - - $this->routes[$id] = new Route($path, $controller, $action, $methods); - } - } - - public function getRouteCollection() - { - return $this->routes; - } -}