diff --git a/WebFiori/Framework/Router/RouteBuilder.php b/WebFiori/Framework/Router/RouteBuilder.php new file mode 100644 index 000000000..f7129869d --- /dev/null +++ b/WebFiori/Framework/Router/RouteBuilder.php @@ -0,0 +1,368 @@ +router = $router; + } + + /** + * Adds new route to the router. + * + * @param array $options An associative array of route options. + * + * @return bool If the route is added, the method will return true. + */ + public function addRoute(array $options): bool { + if (isset($options[RouteOption::SUB_ROUTES])) { + $routesArr = $this->addRoutesGroupHelper($options); + $added = true; + + foreach ($routesArr as $route) { + $added = $added && $this->addRoute($route); + } + + return $added; + } + + if (!isset($options[RouteOption::TO])) { + return false; + } else { + $options = $this->checkOptionsArr($options); + $routeType = $options[RouteOption::TYPE]; + } + + if (strlen($this->router->getBase()) != 0 && ($routeType == Router::API_ROUTE || + $routeType == Router::VIEW_ROUTE || + $routeType == Router::CUSTOMIZED || + $routeType == Router::CLOSURE_ROUTE)) { + return $this->buildAndRegister($options); + } + + return false; + } + + /** + * Adds middleware group name to options array. + */ + public static function addToMiddlewareGroup(array &$options, string $groupName): void { + if (isset($options[RouteOption::MIDDLEWARE])) { + if (gettype($options[RouteOption::MIDDLEWARE]) == 'array') { + $options[RouteOption::MIDDLEWARE][] = $groupName; + } else { + $options[RouteOption::MIDDLEWARE] = [$options[RouteOption::MIDDLEWARE], $groupName]; + } + } else { + $options[RouteOption::MIDDLEWARE] = $groupName; + } + } + + /** + * Removes any extra forward slash in the beginning or the end. + * + * @param string $path Any string that represents the path part of a URI. + * + * @return string A string in the format '/nice/work/boy'. + */ + public function fixUriPath(string $path): string { + if (strlen($path) != 0 && $path != '/') { + if ($path[strlen($path) - 1] == '/' || $path[0] == '/') { + while (strlen($path) > 0 && ($path[0] == '/' || $path[strlen($path) - 1] == '/')) { + $path = trim($path, '/'); + } + $path = '/'.$path; + } + + if ($path[0] != '/') { + $path = '/'.$path; + } + } else { + $path = '/'; + } + + return $path; + } + + private function addRoutesGroupHelper(array $options, array &$routesToAddArr = []): array { + $subRoutes = isset($options[RouteOption::SUB_ROUTES]) && gettype($options[RouteOption::SUB_ROUTES]) == 'array' ? $options[RouteOption::SUB_ROUTES] : []; + + foreach ($subRoutes as $subRoute) { + if (isset($subRoute[RouteOption::PATH])) { + $this->copyOptionsToSub($options, $subRoute); + $subRoute[RouteOption::PATH] = $options[RouteOption::PATH].'/'.$subRoute[RouteOption::PATH]; + + if (isset($subRoute[RouteOption::SUB_ROUTES]) && gettype($subRoute[RouteOption::SUB_ROUTES]) == 'array') { + $this->addRoutesGroupHelper($subRoute, $routesToAddArr); + } else { + $routesToAddArr[] = $subRoute; + } + } + } + + if (isset($options[RouteOption::TO])) { + $sub = [ + RouteOption::PATH => $options[RouteOption::PATH], + RouteOption::TO => $options[RouteOption::TO] + ]; + $this->copyOptionsToSub($options, $sub); + $routesToAddArr[] = $sub; + } + + return $routesToAddArr; + } + + private function buildAndRegister(array $options): bool { + $routeTo = $options[RouteOption::TO]; + $caseSensitive = $options[RouteOption::CASE_SENSITIVE]; + $routeType = $options[RouteOption::TYPE]; + $incInSiteMap = $options[RouteOption::SITEMAP]; + $asApi = $options[RouteOption::API]; + $closureParams = $options[RouteOption::CLOSURE_PARAMS]; + $path = $options[RouteOption::PATH]; + $cache = $options[RouteOption::CACHE_DURATION]; + + if ($routeType == Router::CLOSURE_ROUTE && !is_callable($routeTo)) { + return false; + } + $routeUri = new RouterUri($this->router->getBase().$path, $routeTo, $caseSensitive, $closureParams); + $routeUri->setAction($options[RouteOption::ACTION]); + $routeUri->setCacheDuration($cache); + + if (!$this->router->getMatcher()->hasRoute($routeUri)) { + if ($asApi === true) { + $routeUri->setType(Router::API_ROUTE); + } else { + $routeUri->setType($routeType); + } + $routeUri->setIsInSiteMap($incInSiteMap); + $routeUri->setRequestMethods($options[RouteOption::REQUEST_METHODS]); + + foreach ($options[RouteOption::LANGS] as $langCode) { + $routeUri->addLanguage($langCode); + } + + foreach ($options[RouteOption::VALUES] as $varName => $varValues) { + $routeUri->addAllowedParameterValues($varName, $varValues); + } + $path = $routeUri->isCaseSensitive() ? $routeUri->getPath() : strtolower($routeUri->getPath()); + + foreach ($options[RouteOption::MIDDLEWARE] as $mwName) { + $routeUri->addMiddleware($mwName); + } + + $routes = &$this->router->getRoutesRef(); + + if ($routeUri->hasParameters()) { + $routes['variable'][$path] = $routeUri; + } else { + $routes['static'][$path] = $routeUri; + } + + return true; + } + + return false; + } + + /** + * Checks for provided options and set defaults for the ones which are + * not provided. + */ + private function checkOptionsArr(array $options): array { + $routeTo = $options[RouteOption::TO]; + + if (isset($options[RouteOption::CASE_SENSITIVE])) { + $caseSensitive = $options[RouteOption::CASE_SENSITIVE] === true; + } else { + $caseSensitive = true; + } + + if (isset($options[RouteOption::CACHE_DURATION])) { + $cacheDuration = $options[RouteOption::CACHE_DURATION]; + } else { + $cacheDuration = 0; + } + + $routeType = $options[RouteOption::TYPE] ?? Router::CUSTOMIZED; + $incInSiteMap = $options[RouteOption::SITEMAP] ?? false; + + if (isset($options[RouteOption::MIDDLEWARE])) { + $raw = $options[RouteOption::MIDDLEWARE]; + + if (is_array($raw)) { + $mdArr = $raw; + } else if (is_string($raw) || $raw instanceof AbstractMiddleware) { + $mdArr = [$raw]; + } else { + $mdArr = []; + } + } else { + $mdArr = []; + } + + if (isset($options[RouteOption::API])) { + $asApi = $options[RouteOption::API] === true; + } else { + $asApi = false; + } + $closureParams = isset($options[RouteOption::CLOSURE_PARAMS]) && gettype($options[RouteOption::CLOSURE_PARAMS]) == 'array' ? + $options[RouteOption::CLOSURE_PARAMS] : []; + $path = isset($options[RouteOption::PATH]) ? $this->fixUriPath($options[RouteOption::PATH]) : ''; + $languages = isset($options[RouteOption::LANGS]) && gettype($options[RouteOption::LANGS]) == 'array' ? $options[RouteOption::LANGS] : []; + $varValues = isset($options[RouteOption::VALUES]) && gettype($options[RouteOption::VALUES]) == 'array' ? $options[RouteOption::VALUES] : []; + + $action = ''; + + if (isset($options[RouteOption::ACTION])) { + $trimmed = trim($options[RouteOption::ACTION]); + + if (strlen($trimmed) > 0) { + $action = $trimmed; + } + } + + return [ + RouteOption::CASE_SENSITIVE => $caseSensitive, + RouteOption::TYPE => $routeType, + RouteOption::SITEMAP => $incInSiteMap, + RouteOption::API => $asApi, + RouteOption::PATH => $path, + RouteOption::TO => $routeTo, + RouteOption::CLOSURE_PARAMS => $closureParams, + RouteOption::LANGS => $languages, + RouteOption::VALUES => $varValues, + RouteOption::MIDDLEWARE => $mdArr, + RouteOption::REQUEST_METHODS => $this->getRequestMethodsHelper($options), + RouteOption::ACTION => $action, + RouteOption::CACHE_DURATION => $cacheDuration + ]; + } + + private function copyOptionsToSub(array $options, array &$subRoute): void { + if (!isset($subRoute[RouteOption::CASE_SENSITIVE])) { + if (isset($options[RouteOption::CASE_SENSITIVE])) { + $caseSensitive = $options[RouteOption::CASE_SENSITIVE] === true; + } else { + $caseSensitive = true; + } + $subRoute[RouteOption::CASE_SENSITIVE] = $caseSensitive; + } + + $subRoute[RouteOption::TYPE] = $options[RouteOption::TYPE] ?? Router::CUSTOMIZED; + + if (!isset($subRoute[RouteOption::SITEMAP])) { + $incInSiteMap = $options[RouteOption::SITEMAP] ?? false; + $subRoute[RouteOption::SITEMAP] = $incInSiteMap; + } + + if (isset($options[RouteOption::MIDDLEWARE])) { + if (gettype($options[RouteOption::MIDDLEWARE]) == 'array') { + $mdArr = $options[RouteOption::MIDDLEWARE]; + } else { + if (gettype($options[RouteOption::MIDDLEWARE]) == 'string') { + $mdArr = [$options[RouteOption::MIDDLEWARE]]; + } else { + $mdArr = []; + } + } + } else { + $mdArr = []; + } + + if (!isset($subRoute[RouteOption::MIDDLEWARE])) { + $subRoute[RouteOption::MIDDLEWARE] = $mdArr; + } else { + if (gettype($subRoute[RouteOption::MIDDLEWARE]) == 'array') { + foreach ($mdArr as $md) { + $subRoute[RouteOption::MIDDLEWARE][] = $md; + } + } else { + if (gettype($subRoute[RouteOption::MIDDLEWARE]) == 'string') { + $newMd = [$subRoute[RouteOption::MIDDLEWARE]]; + + foreach ($mdArr as $md) { + $newMd[] = $md; + } + $subRoute[RouteOption::MIDDLEWARE] = $newMd; + } + } + } + $languages = isset($options[RouteOption::LANGS]) && gettype($options[RouteOption::LANGS]) == 'array' ? $options[RouteOption::LANGS] : []; + + if (isset($subRoute[RouteOption::LANGS]) && gettype($subRoute[RouteOption::LANGS]) == 'array') { + foreach ($languages as $langCode) { + if (!in_array($langCode, $subRoute[RouteOption::LANGS])) { + $subRoute[RouteOption::LANGS][] = $langCode; + } + } + } else { + $subRoute[RouteOption::LANGS] = $languages; + } + + $reqMethArr = $this->getRequestMethodsHelper($options); + + if (isset($subRoute[RouteOption::REQUEST_METHODS])) { + if (gettype($subRoute[RouteOption::REQUEST_METHODS]) != 'array') { + $reqMethArr[] = $subRoute[RouteOption::REQUEST_METHODS]; + $subRoute[RouteOption::REQUEST_METHODS] = $reqMethArr; + } else { + foreach ($reqMethArr as $meth) { + $subRoute[RouteOption::REQUEST_METHODS][] = $meth; + } + } + } else { + $subRoute[RouteOption::REQUEST_METHODS] = $reqMethArr; + } + } + + private function getRequestMethodsHelper(array $options): array { + $requestMethodsArr = []; + + if (isset($options[RouteOption::REQUEST_METHODS])) { + $methTypes = gettype($options[RouteOption::REQUEST_METHODS]); + + if ($methTypes == 'array') { + foreach ($options[RouteOption::REQUEST_METHODS] as $reqMethod) { + $upper = strtoupper(trim($reqMethod)); + + if (in_array($upper, RequestMethod::getAll())) { + $requestMethodsArr[] = $upper; + } + } + } else { + if ($methTypes == 'string') { + $upper = strtoupper(trim($options[RouteOption::REQUEST_METHODS])); + + if (in_array($upper, RequestMethod::getAll())) { + $requestMethodsArr[] = $upper; + } + } + } + } + + return $requestMethodsArr; + } +} diff --git a/WebFiori/Framework/Router/RouteDispatcher.php b/WebFiori/Framework/Router/RouteDispatcher.php new file mode 100644 index 000000000..0d902d66f --- /dev/null +++ b/WebFiori/Framework/Router/RouteDispatcher.php @@ -0,0 +1,228 @@ +router = $router; + } + + /** + * Dispatches a matched route. + * + * @param RouterUri $route The matched route. + * @param bool $loadResource Whether to load the resource. + */ + public function dispatch(RouterUri $route, bool $loadResource): void { + if ($route->isRequestMethodAllowed((App::getRequest()->getMethod()))) { + $this->router->setRouteUri($route); + + foreach ($route->getMiddleware() as $mw) { + $mw->before(App::getRequest(), App::getResponse()); + + if (App::getResponse()->getCode() >= 400) { + App::getResponse()->send(); + + return; + } + } + + if ($route->getType() == Router::API_ROUTE && !defined('API_CALL')) { + define('API_CALL', true); + } + + if (is_callable($route->getRouteTo())) { + if ($loadResource === true) { + call_user_func_array($route->getRouteTo(), $route->getClosureParams()); + } + } else { + $file = $route->getRouteTo(); + $xFile = '\\'.str_replace("/", "\\", $file); + + if (class_exists($xFile) && $loadResource) { + $class = new $xFile(); + + if ($class instanceof WebServicesManager) { + $class->process(); + } else if ($class instanceof WebPage) { + $class->render(); + } else if ($route->getAction() !== null) { + $toCall = $route->getAction(); + $class->$toCall(); + } + } else { + $routeType = $route->getType(); + + if ($routeType == Router::VIEW_ROUTE || $routeType == Router::CUSTOMIZED || $routeType == Router::API_ROUTE) { + $file = ROOT_PATH.$routeType.$this->fixFilePath($file); + } else { + $file = ROOT_PATH.$this->fixFilePath($file); + } + + if (gettype($file) == 'string' && file_exists($file)) { + if ($loadResource === true) { + $route->setRoute($file); + $this->loadResource($route); + } + } else { + if ($loadResource === true) { + $message = 'The resource "'.App::getRequest()->getRequestedURI().'" was available. ' + .'but its route is not configured correctly. ' + .'The resource which the route is pointing to was not found.'; + + if (defined('WF_VERBOSE') && WF_VERBOSE) { + $message = 'The resource "'.App::getRequest()->getRequestedURI().'" was available. ' + .'but its route is not configured correctly. ' + .'The resource which the route is pointing to was not found ('.$file.').'; + } + throw new RoutingException($message); + } + } + } + } + } else { + App::getResponse()->setCode(405); + + if (!defined('API_CALL')) { + $notFoundView = new HTTPCodeView(405); + $notFoundView->render(); + } else { + $json = new Json([ + 'message' => 'Request method not allowed.', + 'type' => 'error' + ]); + App::getResponse()->write($json); + } + } + } + + /** + * Send http 301 response code and redirect the request to non-www URI. + */ + public function redirectToNonWWW(RouterUri $uriObj): void { + App::getResponse()->setCode(301); + $path = ''; + + $host = substr($uriObj->getHost(), strpos($uriObj->getHost(), '.')); + + for ($x = 1 ; $x < count($uriObj->getPathArray()) ; $x++) { + $path .= '/'.$uriObj->getPathArray()[$x]; + } + $queryString = ''; + + if (strlen($uriObj->getQueryString()) > 0) { + $queryString = '?'.$uriObj->getQueryString(); + } + $fragment = ''; + + if (strlen($uriObj->getFragment()) > 0) { + $fragment = '#'.$uriObj->getFragment(); + } + $port = ''; + + if (strlen($uriObj->getPort()) > 0) { + $port = ':'.$uriObj->getPort(); + } + App::getResponse()->addHeader('location', $uriObj->getScheme().'://'.$host.$port.$path.$queryString.$fragment); + App::getResponse()->send(); + } + + private function fixFilePath(string $path): string { + if (strlen($path) != 0 && $path != '/') { + $path00 = str_replace('/', DS, $path); + $path01 = str_replace('\\', DS, $path00); + + if ($path01[strlen($path01) - 1] == DS || $path01[0] == DS) { + while ($path01[0] == DS || $path01[strlen($path01) - 1] == DS) { + $path01 = trim($path01, DS); + } + $path01 = DS.$path01; + } + + if ($path01[0] != DS) { + $path01 = DS.$path01; + } + $path = $path01; + } else { + $path = DS; + } + + return $path; + } + + private function getFileDirAndName(string $absDir): array { + $explode = explode(DS, $absDir); + $fileName = $explode[count($explode) - 1]; + $dir = substr($absDir, 0, strlen($absDir) - strlen($fileName)); + + return [ + 'name' => $fileName, + 'dir' => $dir + ]; + } + + private function loadResource(RouterUri $route): void { + $file = $route->getRouteTo(); + $info = $this->getFileDirAndName($file); + $fileObj = new File($info['name'], $info['dir']); + $fileObj->read(); + + if ($fileObj->getMIME() === 'text/plain') { + $classNamespace = require_once $file; + + if (gettype($classNamespace) == 'string') { + if (strlen($classNamespace) == 0) { + $constructor = '\\'.$route->getClassName(); + } else { + $constructor = '\\'.$classNamespace.'\\'.$route->getClassName(); + } + + if (class_exists($constructor)) { + $instance = new $constructor(); + + if ($instance instanceof WebServicesManager) { + if (!defined('API_CALL')) { + define('API_CALL', true); + } + $instance->process(); + } else if ($instance instanceof WebPage) { + $instance->render(); + } else if ($route->getAction() !== null) { + $toCall = $route->getAction(); + $instance->$toCall(); + } + } + } + } else { + $fileObj->view(); + } + } +} diff --git a/WebFiori/Framework/Router/RouteMatcher.php b/WebFiori/Framework/Router/RouteMatcher.php new file mode 100644 index 000000000..d007c0c6f --- /dev/null +++ b/WebFiori/Framework/Router/RouteMatcher.php @@ -0,0 +1,207 @@ +router = $router; + } + + /** + * Returns an object of type 'RouterUri' that represents route URI. + * + * @param string $path The path part of the URI. + * + * @return RouterUri|null If a route was found, an object of type 'RouterUri' + * is returned. If no route is found, null is returned. + */ + public function getUriObj(string $path): ?RouterUri { + $routes = $this->router->getRoutesRef(); + + if (isset($routes['static'][$path])) { + return $routes['static'][$path]; + } else if (isset($routes['variable'][$path])) { + return $routes['variable'][$path]; + } + + return null; + } + + /** + * Checks if a given RouterUri object has a registered route. + * + * @param RouterUri $uriObj The URI object to check. + * + * @return bool True if the route exists. + */ + public function hasRoute(RouterUri $uriObj): bool { + $path = $uriObj->getPath(); + + if (!$uriObj->isCaseSensitive()) { + $path = strtolower($path); + } + + $routes = $this->router->getRoutesRef(); + + if ($uriObj->hasParameters()) { + return isset($routes['variable'][$path]); + } else { + return isset($routes['static'][$path]); + } + } + + /** + * Route a given URI to its specified resource. + * + * @param string $uri A URI such as 'http://www.example.com/hello/ibrahim' + * @param bool $loadResource If set to true, the resource will be loaded. + */ + public function resolveUrl(string $uri, bool $loadResource = true): void { + $this->router->setRouteUri(null); + + if (Router::routesCount() != 0) { + $routeUri = new RouterUri($uri, ''); + + if ($routeUri->hasWWW() && defined('NO_WWW') && NO_WWW === true) { + $this->router->getDispatcher()->redirectToNonWWW($routeUri); + } + + //first, search for the URI without checking parameters + if ($this->searchRoute($routeUri, $uri, $loadResource)) { + return; + } + + //if no route found, try to replace parameters with values + if ($this->searchRoute($routeUri, $uri, $loadResource, true)) { + return; + } + + //if we reach this part, this means the route was not found + if ($loadResource) { + call_user_func($this->router->getOnNotFound()); + } + } else { + if ($loadResource === true) { + $page = new StarterPage(); + $page->render(); + } + } + } + + /** + * Checks if a directory name is a parameter or not. + */ + private function isDirectoryAVar(string $dir): bool { + return $dir[0] == '{' && $dir[strlen($dir) - 1] == '}'; + } + + /** + * Searches for a matching route. + * + * @param RouterUri $routeUri The parsed request URI. + * @param string $uri The raw URI string. + * @param bool $loadResource Whether to load the resource. + * @param bool $withVars Whether to check variable routes. + * + * @return bool True if a route was found. + */ + private function searchRoute(RouterUri $routeUri, string $uri, bool $loadResource, bool $withVars = false): bool { + $pathArray = $routeUri->getPathArray(); + $requestMethod = App::getRequest()->getMethod(); + $indexToSearch = 'static'; + + if ($withVars) { + $indexToSearch = 'variable'; + } + + $routes = $this->router->getRoutesRef(); + + if ($indexToSearch == 'static') { + $route = isset($routes[$indexToSearch][$routeUri->getPath()]) ? + $routes[$indexToSearch][$routeUri->getPath()] : null; + + if ($route instanceof RouterUri) { + if (!$route->isCaseSensitive()) { + $isEqual = strtolower($route->getUri()) == + strtolower($routeUri->getUri()); + } else { + $isEqual = $route->getUri() == $routeUri->getUri(); + } + + if ($isEqual) { + $route->setRequestedUri($uri); + $this->router->getDispatcher()->dispatch($route, $loadResource); + + return true; + } + } + } else { + foreach ($routes['variable'] as $route) { + $this->setUriVarsHelper($route, $pathArray, $requestMethod); + + if ($route->isAllParametersSet() && $route->setRequestedUri($uri)) { + $this->router->getDispatcher()->dispatch($route, $loadResource); + + return true; + } + } + } + + return false; + } + + /** + * Sets URI parameter values from the requested path. + */ + private function setUriVarsHelper(RouterUri $uriRouteObj, array $requestedPathArr, string $requestMethod): void { + $routePathArray = $uriRouteObj->getPathArray(); + $pathVarsCount = count($routePathArray); + $requestedPathPartsCount = count($requestedPathArr); + + for ($x = 0 ; $x < $pathVarsCount ; $x++) { + if ($x == $requestedPathPartsCount) { + break; + } + + if ($this->isDirectoryAVar($routePathArray[$x])) { + $varName = trim($routePathArray[$x], '{}'); + + if ($varName[strlen($varName) - 1] == '?') { + $varName = trim($varName, '?'); + } + $uriRouteObj->setParameterValue($varName, $requestedPathArr[$x]); + + if ($requestMethod == 'POST' || $requestMethod == 'PUT') { + $_POST[$varName] = filter_var(urldecode($requestedPathArr[$x])); + } else if ($requestMethod == 'GET' || $requestMethod == 'DELETE' || Runner::isCLI()) { + $_GET[$varName] = filter_var(urldecode($requestedPathArr[$x])); + } + } else if ((!$uriRouteObj->isCaseSensitive() && (strtolower($routePathArray[$x]) != strtolower($requestedPathArr[$x]))) || $routePathArray[$x] != $requestedPathArr[$x]) { + break; + } + } + } +} diff --git a/WebFiori/Framework/Router/Router.php b/WebFiori/Framework/Router/Router.php index 38812dd41..28c54f95e 100644 --- a/WebFiori/Framework/Router/Router.php +++ b/WebFiori/Framework/Router/Router.php @@ -1,4 +1,5 @@ baseUrl = trim(Uri::getBaseURL(), '/'); + $this->builder = new RouteBuilder($this); + $this->matcher = new RouteMatcher($this); + $this->dispatcher = new RouteDispatcher($this); } + + /** + * Returns a reference to the routes array. + * + * @return array + */ + public function &getRoutesRef(): array { + return $this->routes; + } + /** * Adds new route to a file inside the root folder. * * @param array $options An associative array of options. - * The class 'RouteOption' can be used to access options. Available options - * are: - * * * @return bool The method will return true if the route was created. * If a route for the given path was already created, the method will return @@ -231,7 +206,7 @@ private function __construct() { public static function addRoute(array $options) : bool { $options[RouteOption::TYPE] = Router::CUSTOMIZED; - return Router::getInstance()->addRouteHelper1($options); + return Router::getInstance()->getBuilder()->addRoute($options); } /** * Adds new route to a web services set. @@ -239,42 +214,7 @@ public static function addRoute(array $options) : bool { * Note that the route which created using this method will be added to * 'global' and 'api' middleware groups. * - * @param array $options An associative array that contains route - * options. Available options are: - * + * @param array $options An associative array that contains route options. * * @return bool The method will return true if the route was created. * If a route for the given path was already created, the method will return @@ -284,16 +224,14 @@ public static function addRoute(array $options) : bool { */ public static function api(array $options) : bool { $options[RouteOption::TYPE] = Router::API_ROUTE; - self::addToMiddlewareGroup($options, 'api'); + RouteBuilder::addToMiddlewareGroup($options, 'api'); - return Router::getInstance()->addRouteHelper1($options); + return Router::getInstance()->getBuilder()->addRoute($options); } /** * Returns the base URI which is used to create routes. * - * @return string The base URL which is used to create routes. The returned - * value is based on one of two values. Either the value that is returned - * by the method 'Util::getBaseURL()' or the method 'SiteConfig::getBaseURL()'. + * @return string The base URL which is used to create routes. * * @since 1.3.1 */ @@ -307,49 +245,7 @@ public static function base() : string { * Note that the route which created using this method will be added to * 'global' and 'closure' middleware groups. * - * @param array $options An associative array that contains route - * options. Available options are: - * - * + * @param array $options An associative array that contains route options. * * @return bool The method will return true if the route was created. * If a route for the given path was already created, the method will return @@ -359,15 +255,13 @@ public static function base() : string { */ public static function closure(array $options) : bool { $options[RouteOption::TYPE] = Router::CLOSURE_ROUTE; - self::addToMiddlewareGroup($options, 'closure'); + RouteBuilder::addToMiddlewareGroup($options, 'closure'); - return Router::getInstance()->addRouteHelper1($options); + return Router::getInstance()->getBuilder()->addRoute($options); } /** * Returns the value of the base URI which is appended to the path. * - * This method is similar to calling the method Router::base(). - * * @return string * * @since 1.0 @@ -375,12 +269,63 @@ public static function closure(array $options) : bool { public static function getBase() : string { return self::getInstance()->baseUrl; } + + /** + * Returns the RouteBuilder instance. + * + * @return RouteBuilder + */ + public function getBuilder(): RouteBuilder { + return $this->builder; + } + + /** + * Returns the RouteDispatcher instance. + * + * @return RouteDispatcher + */ + public function getDispatcher(): RouteDispatcher { + return $this->dispatcher; + } + + /** + * Creates and Returns a single instance of the router. + * + * @return Router + * + * @since 1.0 + */ + public static function getInstance(): Router { + if (self::$router != null) { + return self::$router; + } + self::$router = new Router(); + + return self::$router; + } + + /** + * Returns the RouteMatcher instance. + * + * @return RouteMatcher + */ + public function getMatcher(): RouteMatcher { + return $this->matcher; + } + + /** + * Returns the on-not-found callback. + * + * @return callable + */ + public function getOnNotFound(): callable { + return $this->onNotFound; + } /** * Returns the value of a parameter which exist in the path part of the * URI. * - * @param string $varName The name of the parameter. Note that it must - * not include braces. + * @param string $varName The name of the parameter. * * @return string|null The method will return the value of the * parameter if it was set. If it is not set or routing is still not yet @@ -400,9 +345,6 @@ public static function getParameterValue(string $varName) { /** * Returns an object of type 'RouterUri' which contains route information. * - * When the method Router::route() is called and a route is found, an - * object of type 'RouterUri' is created which has route information. - * * @return RouterUri|null An object which has route information. If the * method 'Router::route()' is not yet called or no route was found, * the method will return null. @@ -424,23 +366,21 @@ public static function getRouteUri() { * @since 1.3.3 */ public static function getUriObj(string $path) { - return self::getInstance()->getUriObjHelper($path); + return self::getInstance()->getMatcher()->getUriObj($path); } /** * Returns an object of type 'RouterUri' which contains URL route information. * - * @param string $url A string that represents a URL (such as 'https://example.com/my-resource'). + * @param string $url A string that represents a URL. * * @return RouterUri|null If a resource was found which has the given route, an - * object of type RouterUri is returned. Other than that, null is returned. Note - * that if the URI is invalid, the method will return null. Also, if the library - * 'http' is not loaded, the method will return null. + * object of type RouterUri is returned. Other than that, null is returned. * * @since 1.3.6 */ public static function getUriObjByURL(string $url) { try { - self::getInstance()->resolveUrlHelper($url, false); + self::getInstance()->getMatcher()->resolveUrl($url, false); return self::getRouteUri(); } catch (Error $ex) { @@ -461,19 +401,13 @@ public static function getUriObjByURL(string $url) { */ public static function hasRoute(string $path): bool { $routesArr = self::getInstance()->routes; - $trimmed = self::getInstance()->fixUriPath($path); + $trimmed = self::getInstance()->getBuilder()->fixUriPath($path); return isset($routesArr['static'][$trimmed]) || isset($routesArr['variable'][$trimmed]); } /** * Adds a route to a basic xml site map. * - * If this method is called, a route in the form 'http://example.com/sitemam.xml' - * and in the form 'http://example.com/sitemam' will be created. - * The method will check all created routes objects and check if they - * should be included in the site map. Note that if a URI has parameters, it - * will be not included unless possible values are given for the parameter. - * * @since 1.3.2 */ public static function incSiteMapRoute() { @@ -483,7 +417,7 @@ public static function incSiteMapRoute() { $urlSet->setIsQuotedAttribute(true); $urlSet->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9') ->setAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml'); - $routes = Router::getInstance()->getRoutesHelper(); + $routes = Router::getInstance()->getRoutesRef(); foreach ($routes['static'] as $route) { if ($route->isInSiteMap()) { @@ -513,13 +447,13 @@ public static function incSiteMapRoute() { RouteOption::PATH => '/sitemap.xml', RouteOption::TO => $sitemapFunc, RouteOption::SITEMAP => true, - RouteOption::CACHE_DURATION => 86400//1 day + RouteOption::CACHE_DURATION => 86400 ]); self::closure([ RouteOption::PATH => '/sitemap', RouteOption::TO => $sitemapFunc, RouteOption::SITEMAP => true, - RouteOption::CACHE_DURATION => 86400//1 day + RouteOption::CACHE_DURATION => 86400 ]); } /** @@ -534,47 +468,9 @@ public static function notFound() { * Adds new route to a web page. * * Note that the route which created using this method will be added to - * 'global' and 'web' middleware groups. Additionally, the routes will - * be cached for one hour. - * - * @param array $options An associative array that contains route - * options. Available options are: - * + * 'global' and 'web' middleware groups. + * + * @param array $options An associative array that contains route options. * * @return bool The method will return true if the route was created. * If a route for the given path was already created, the method will return @@ -589,11 +485,8 @@ public static function page(array $options) : bool { * Adds a redirect route. * * @param string $path The path at which when the user visits will be redirected. - * * @param string $to A path or a URL at which the user will be sent to. - * - * @param int $code HTTP redirect code. Can have one of the following values: - * 301, 302, 303, 307 and 308. Default is 301 (Permanent redirect). + * @param int $code HTTP redirect code. Default is 301. * * @since 1.3.11 */ @@ -609,6 +502,7 @@ public static function redirect(string $path, string $to, int $code = 301) { $httpCode = 301; } $requestedUri = Router::getRouteUri()->getRequestedUri(); + if ($requestedUri !== null && strlen($requestedUri->getQueryString()) > 0) { $to .= '?'.$requestedUri->getQueryString(); } @@ -647,7 +541,7 @@ public static function removeAll() { * @since 1.3.7 */ public static function removeRoute(string $path) : bool { - $pathFix = self::getInstance()->fixUriPath($path); + $pathFix = self::getInstance()->getBuilder()->fixUriPath($path); $retVal = false; $routes = &self::getInstance()->routes; @@ -663,6 +557,12 @@ public static function removeRoute(string $path) : bool { return $retVal; } + /** + * Destroys the default Router instance. Next call creates a fresh one. + */ + public static function resetInstance(): void { + self::$router = null; + } /** * Redirect a URI to its route. * @@ -671,24 +571,23 @@ public static function removeRoute(string $path) : bool { * @since 1.2 */ public static function route(string $uri) { - Router::getInstance()->resolveUrlHelper($uri); + Router::getInstance()->getMatcher()->resolveUrl($uri); } /** * Returns an associative array of all available routes. * - * @return array An associative array of all available routes. The - * keys will be requested URIs and the values are the routes. + * @return array An associative array of all available routes. * * @since 1.2 */ public static function routes() : array { $routesArr = []; - foreach (Router::getInstance()->getRoutesHelper()['static'] as $routeUri) { + foreach (Router::getInstance()->routes['static'] as $routeUri) { $routesArr[$routeUri->getUri()] = $routeUri->getRouteTo(); } - foreach (Router::getInstance()->getRoutesHelper()['variable'] as $routeUri) { + foreach (Router::getInstance()->routes['variable'] as $routeUri) { $routesArr[$routeUri->getUri()] = $routeUri->getRouteTo(); } @@ -697,12 +596,6 @@ public static function routes() : array { /** * Returns an associative array that contains all routes. * - * The returned array will have two indices, 'static' and 'variable'. The 'static' - * index will contain routes to resources at which they don't contain parameters in - * their path part. Each index of the two will have another sub associative array. - * The indices of each sub array ill be URLs that represents the route and - * the value at each index will be an object of type 'RouterUri'. - * * @return array An associative array that contains all routes. * * @since 1.3.7 @@ -722,6 +615,14 @@ public static function routesCount() : int { return count($routesArr['variable']) + count($routesArr['static']); } + /** + * Replaces the default Router instance. + * + * @param Router $router The router instance to use. + */ + public static function setInstance(Router $router): void { + self::$router = $router; + } /** * Sets a callback to call in case a given rout is not found. * @@ -731,7 +632,16 @@ public static function routesCount() : int { * @since 1.3.8 */ public static function setOnNotFound(callable $func) { - self::getInstance()->setOnNotFoundHelper($func); + self::getInstance()->onNotFound = $func; + } + + /** + * Sets the current route URI object. + * + * @param RouterUri|null $uri + */ + public function setRouteUri(?RouterUri $uri): void { + $this->uriObj = $uri; } /** * Adds an object of type 'RouterUri' as new route. @@ -739,13 +649,12 @@ public static function setOnNotFound(callable $func) { * @param RouterUri $routerUri An object of type 'RouterUri'. * * @return bool If the object is added as new route, the method will - * return true. If the given parameter is not an instance of 'RouterUri' - * or a route is already added, The method will return false. + * return true. If a route is already added, The method will return false. * * @since 1.3.2 */ public static function uriObj(RouterUri $routerUri) : bool { - if (!self::getInstance()->hasRouteHelper($routerUri->getPath())) { + if (!self::getInstance()->getMatcher()->hasRoute($routerUri)) { if ($routerUri->hasVars()) { self::getInstance()->routes['variable'] = $routerUri; } else { @@ -757,885 +666,13 @@ public static function uriObj(RouterUri $routerUri) : bool { return false; } - private function addRouteHelper0($options): bool { - $routeTo = $options[RouteOption::TO]; - $caseSensitive = $options[RouteOption::CASE_SENSITIVE]; - $routeType = $options[RouteOption::TYPE]; - $incInSiteMap = $options[RouteOption::SITEMAP]; - $asApi = $options[RouteOption::API]; - $closureParams = $options[RouteOption::CLOSURE_PARAMS] ; - $path = $options[RouteOption::PATH]; - $cache = $options[RouteOption::CACHE_DURATION]; - - if ($routeType == self::CLOSURE_ROUTE && !is_callable($routeTo)) { - return false; - } - $routeUri = new RouterUri($this->getBase().$path, $routeTo,$caseSensitive, $closureParams); - $routeUri->setAction($options[RouteOption::ACTION]); - $routeUri->setCacheDuration($cache); - if (!$this->hasRouteHelper($routeUri)) { - if ($asApi === true) { - $routeUri->setType(self::API_ROUTE); - } else { - $routeUri->setType($routeType); - } - $routeUri->setIsInSiteMap($incInSiteMap); - - $routeUri->setRequestMethods($options[RouteOption::REQUEST_METHODS]); - - foreach ($options[RouteOption::LANGS] as $langCode) { - $routeUri->addLanguage($langCode); - } - - foreach ($options[RouteOption::VALUES] as $varName => $varValues) { - $routeUri->addAllowedParameterValues($varName, $varValues); - } - $path = $routeUri->isCaseSensitive() ? $routeUri->getPath() : strtolower($routeUri->getPath()); - - foreach ($options[RouteOption::MIDDLEWARE] as $mwName) { - $routeUri->addMiddleware($mwName); - } - - if ($routeUri->hasParameters()) { - $this->routes['variable'][$path] = $routeUri; - } else { - $this->routes['static'][$path] = $routeUri; - } - - return true; - } - - return false; - } - /** - * Adds new route to the router. - * - * @param array $options An associative array of route options. The - * array can have the following indices: - * - * - * @return bool If the route is added, the method will return true. - * The method one return false only in two cases, either the route type - * is not correct or a similar route was already added. - * - * @since 1.0 - */ - private function addRouteHelper1(array $options): bool { - if (isset($options[RouteOption::SUB_ROUTES])) { - $routesArr = $this->addRoutesGroupHelper($options); - $added = true; - - foreach ($routesArr as $route) { - $added = $added && $this->addRouteHelper1($route); - } - - return $added; - } - - if (!isset($options[RouteOption::TO])) { - return false; - } else { - $options = $this->checkOptionsArr($options); - $routeType = $options[RouteOption::TYPE]; - } - - if (strlen($this->getBase()) != 0 && ($routeType == self::API_ROUTE || - $routeType == self::VIEW_ROUTE || - $routeType == self::CUSTOMIZED || - $routeType == self::CLOSURE_ROUTE)) { - return $this->addRouteHelper0($options); - } - - return false; - } - private function addRoutesGroupHelper($options, &$routesToAddArr = []) { - $subRoutes = isset($options[RouteOption::SUB_ROUTES]) && gettype($options[RouteOption::SUB_ROUTES]) == 'array' ? $options[RouteOption::SUB_ROUTES] : []; - - foreach ($subRoutes as $subRoute) { - if (isset($subRoute[RouteOption::PATH])) { - $this->copyOptionsToSub($options, $subRoute); - $subRoute[RouteOption::PATH] = $options[RouteOption::PATH].'/'.$subRoute[RouteOption::PATH]; - - if (isset($subRoute[RouteOption::SUB_ROUTES]) && gettype($subRoute[RouteOption::SUB_ROUTES]) == 'array') { - $this->addRoutesGroupHelper($subRoute, $routesToAddArr); - } else { - $routesToAddArr[] = $subRoute; - } - } - } - - if (isset($options[RouteOption::TO])) { - $sub = [ - RouteOption::PATH => $options[RouteOption::PATH], - RouteOption::TO => $options[RouteOption::TO] - ]; - $this->copyOptionsToSub($options, $sub); - $routesToAddArr[] = $sub; - } - return $routesToAddArr; - } - private static function addToMiddlewareGroup(&$options, $groupName) { - if (isset($options[RouteOption::MIDDLEWARE])) { - if (gettype($options[RouteOption::MIDDLEWARE]) == 'array') { - $options[RouteOption::MIDDLEWARE][] = $groupName; - } else { - $options[RouteOption::MIDDLEWARE] = [$options[RouteOption::MIDDLEWARE], $groupName]; - } - } else { - $options[RouteOption::MIDDLEWARE] = $groupName; - } - } - /** - * Checks for provided options and set defaults for the ones which are - * not provided. - * - * @param array $options - * - * @return array - */ - private function checkOptionsArr(array $options): array { - $routeTo = $options[RouteOption::TO]; - - if (isset($options[RouteOption::CASE_SENSITIVE])) { - $caseSensitive = $options[RouteOption::CASE_SENSITIVE] === true; - } else { - $caseSensitive = true; - } - - if (isset($options[RouteOption::CACHE_DURATION])) { - $cacheDuration = $options[RouteOption::CACHE_DURATION]; - } else { - $cacheDuration = 0; - } - - $routeType = $options[RouteOption::TYPE] ?? Router::CUSTOMIZED; - - $incInSiteMap = $options[RouteOption::SITEMAP] ?? false; - - if (isset($options[RouteOption::MIDDLEWARE])) { - $raw = $options[RouteOption::MIDDLEWARE]; - - if (is_array($raw)) { - $mdArr = $raw; - } else if (is_string($raw) || $raw instanceof AbstractMiddleware) { - $mdArr = [$raw]; - } else { - $mdArr = []; - } - } else { - $mdArr = []; - } - - if (isset($options[RouteOption::API])) { - $asApi = $options[RouteOption::API] === true; - } else { - $asApi = false; - } - $closureParams = isset($options[RouteOption::CLOSURE_PARAMS]) && gettype($options[RouteOption::CLOSURE_PARAMS]) == 'array' ? - $options[RouteOption::CLOSURE_PARAMS] : []; - $path = isset($options[RouteOption::PATH]) ? $this->fixUriPath($options[RouteOption::PATH]) : ''; - $languages = isset($options[RouteOption::LANGS]) && gettype($options[RouteOption::LANGS]) == 'array' ? $options[RouteOption::LANGS] : []; - $varValues = isset($options[RouteOption::VALUES]) && gettype($options[RouteOption::VALUES]) == 'array' ? $options[RouteOption::VALUES] : []; - - $action = ''; - - if (isset($options[RouteOption::ACTION])) { - $trimmed = trim($options[RouteOption::ACTION]); - - if (strlen($trimmed) > 0) { - $action = $trimmed; - } - } - - return [ - RouteOption::CASE_SENSITIVE => $caseSensitive, - RouteOption::TYPE => $routeType, - RouteOption::SITEMAP => $incInSiteMap, - RouteOption::API => $asApi, - RouteOption::PATH => $path, - RouteOption::TO => $routeTo, - RouteOption::CLOSURE_PARAMS => $closureParams, - RouteOption::LANGS => $languages, - RouteOption::VALUES => $varValues, - RouteOption::MIDDLEWARE => $mdArr, - RouteOption::REQUEST_METHODS => $this->getRequestMethodsHelper($options), - RouteOption::ACTION => $action, - RouteOption::CACHE_DURATION => $cacheDuration - ]; - } - private function copyOptionsToSub($options, &$subRoute) { - if (!isset($subRoute[RouteOption::CASE_SENSITIVE])) { - if (isset($options[RouteOption::CASE_SENSITIVE])) { - $caseSensitive = $options[RouteOption::CASE_SENSITIVE] === true; - } else { - $caseSensitive = true; - } - $subRoute[RouteOption::CASE_SENSITIVE] = $caseSensitive; - } - - $subRoute[RouteOption::TYPE] = $options[RouteOption::TYPE] ?? Router::CUSTOMIZED; - - if (!isset($subRoute[RouteOption::SITEMAP])) { - $incInSiteMap = $options[RouteOption::SITEMAP] ?? false; - $subRoute[RouteOption::SITEMAP] = $incInSiteMap; - } - - if (isset($options[RouteOption::MIDDLEWARE])) { - if (gettype($options[RouteOption::MIDDLEWARE]) == 'array') { - $mdArr = $options[RouteOption::MIDDLEWARE]; - } else { - if (gettype($options[RouteOption::MIDDLEWARE]) == 'string') { - $mdArr = [$options[RouteOption::MIDDLEWARE]]; - } else { - $mdArr = []; - } - } - } else { - $mdArr = []; - } - - if (!isset($subRoute[RouteOption::MIDDLEWARE])) { - $subRoute[RouteOption::MIDDLEWARE] = $mdArr; - } else { - if (gettype($subRoute[RouteOption::MIDDLEWARE]) == 'array') { - foreach ($mdArr as $md) { - $subRoute[RouteOption::MIDDLEWARE][] = $md; - } - } else { - if (gettype($subRoute[RouteOption::MIDDLEWARE]) == 'string') { - $newMd = [$subRoute[RouteOption::MIDDLEWARE]]; - - foreach ($mdArr as $md) { - $newMd[] = $md; - } - $subRoute[RouteOption::MIDDLEWARE] = $newMd; - } - } - } - $languages = isset($options[RouteOption::LANGS]) && gettype($options[RouteOption::LANGS]) == 'array' ? $options[RouteOption::LANGS] : []; - - if (isset($subRoute[RouteOption::LANGS]) && gettype($subRoute[RouteOption::LANGS]) == 'array') { - foreach ($languages as $langCode) { - if (!in_array($langCode, $subRoute[RouteOption::LANGS])) { - $subRoute[RouteOption::LANGS][] = $langCode; - } - } - } else { - $subRoute[RouteOption::LANGS] = $languages; - } - - $reqMethArr = $this->getRequestMethodsHelper($options); - - if (isset($subRoute[RouteOption::REQUEST_METHODS])) { - if (gettype($subRoute[RouteOption::REQUEST_METHODS]) != 'array') { - $reqMethArr[] = $subRoute[RouteOption::REQUEST_METHODS]; - $subRoute[RouteOption::REQUEST_METHODS] = $reqMethArr; - } else { - foreach ($reqMethArr as $meth) { - $subRoute[RouteOption::REQUEST_METHODS][] = $meth; - } - } - } else { - $subRoute[RouteOption::REQUEST_METHODS] = $reqMethArr; - } - } - private function fixFilePath($path) { - if (strlen($path) != 0 && $path != '/') { - $path00 = str_replace('/', DS, $path); - $path01 = str_replace('\\', DS, $path00); - - if ($path01[strlen($path01) - 1] == DS || $path01[0] == DS) { - while ($path01[0] == DS || $path01[strlen($path01) - 1] == DS) { - $path01 = trim($path01, DS); - } - $path01 = DS.$path01; - } - - if ($path01[0] != DS) { - $path01 = DS.$path01; - } - $path = $path01; - } else { - $path = DS; - } - - return $path; - } - /** - * Removes any extra forward slash in the beginning or the end. - * - * @param string $path Any string that represents the path part of a URI. - * - * @return string A string in the format '/nice/work/boy'. - * - * @since 1.1 - */ - private function fixUriPath(string $path): string { - if (strlen($path) != 0 && $path != '/') { - if ($path[strlen($path) - 1] == '/' || $path[0] == '/') { - while (strlen($path) > 0 && ($path[0] == '/' || $path[strlen($path) - 1] == '/')) { - $path = trim($path, '/'); - } - $path = '/'.$path; - } - - if ($path[0] != '/') { - $path = '/'.$path; - } - } else { - $path = '/'; - } - - return $path; - } - private function getFileDirAndName($absDir): array { - $explode = explode(DS, $absDir); - $fileName = $explode[count($explode) - 1]; - $dir = substr($absDir, 0, strlen($absDir) - strlen($fileName)); - - return [ - 'name' => $fileName, - 'dir' => $dir - ]; - } - /** - * Creates and Returns a single instance of the router. - * - * @return Router - * - * @since 1.0 - */ - /** - * Returns the default Router instance. - * - * @return Router - */ - public static function getInstance(): Router { - if (self::$router != null) { - return self::$router; - } - self::$router = new Router(); - - return self::$router; - } - /** - * Replaces the default Router instance. - * - * @param Router $router The router instance to use. - */ - public static function setInstance(Router $router): void { - self::$router = $router; - } - /** - * Destroys the default Router instance. Next call creates a fresh one. - */ - public static function resetInstance(): void { - self::$router = null; - } - /** - * Returns an array that holds allowed request methods for fetching the - * specified resource. - * - * @param array $options The array which used to hold route options. - * - * @return array If the route has no specific request methods, the - * array will be empty. Other than that, the array will have request - * methods as strings. - */ - private function getRequestMethodsHelper(array $options): array { - $requestMethodsArr = []; - - if (isset($options[RouteOption::REQUEST_METHODS])) { - $methTypes = gettype($options[RouteOption::REQUEST_METHODS]); - - if ($methTypes == 'array') { - foreach ($options[RouteOption::REQUEST_METHODS] as $reqMethod) { - $upper = strtoupper(trim($reqMethod)); - - if (in_array($upper, RequestMethod::getAll())) { - $requestMethodsArr[] = $upper; - } - } - } else { - if ($methTypes == 'string') { - $upper = strtoupper(trim($options[RouteOption::REQUEST_METHODS])); - - if (in_array($upper, RequestMethod::getAll())) { - $requestMethodsArr[] = $upper; - } - } - } - } - - return $requestMethodsArr; - } - /** - * Returns an array which contains all routes as RouteURI object. - * - * @return array An array which contains all routes as RouteURI object. - * - * @since 1.2 - */ - private function getRoutesHelper() : array { - return $this->routes; - } - /** - * Returns an object of type 'RouterUri' that represents route URI. - * - * @param string $path The path part of the URI. - * - * @return RouterUri|null If a route was found which has the given path, - * an object of type 'RouterUri' is returned. If no route is found, null - * is returned. - * - * @since 1.3.3 - */ - private function getUriObjHelper(string $path) { - if (isset($this->routes['static'][$path])) { - return $this->routes['static'][$path]; - } else { - if (isset($this->routes['variable'][$path])) { - return $this->routes['variable'][$path]; - } - } - - return null; - } - /** - * Checks if a given path has a route or not. - * - * @param RouterUri $path The path which will be checked (such as '/path1/path2') - * - * @return bool The method will return true if the given path - * has a route. - * - * @since 1.1 - */ - private function hasRouteHelper($uriObj): bool { - $path = $uriObj->getPath(); - - if (!$uriObj->isCaseSensitive()) { - $path = strtolower($path); - } - - if ($uriObj->hasParameters()) { - return isset($this->routes['variable'][$path]); - } else { - return isset($this->routes['static'][$path]); - } - } - /** - * Checks if a directory name is a parameter or not. - * - * @param string $dir - * - * @return bool - * - * @since 1.1 - */ - private function isDirectoryAVar(string $dir): bool { - return $dir[0] == '{' && $dir[strlen($dir) - 1] == '}'; - } - /** - * - * @param RouterUri $route - * @throws FileException - */ - private function loadResourceHelper(RouterUri $route) { - $file = $route->getRouteTo(); - $info = $this->getFileDirAndName($file); - $fileObj = new File($info['name'], $info['dir']); - $fileObj->read(); - - if ($fileObj->getMIME() === 'text/plain') { - $classNamespace = require_once $file; - - if (gettype($classNamespace) == 'string') { - if (strlen($classNamespace) == 0) { - $constructor = '\\'.$route->getClassName(); - } else { - $constructor = '\\'.$classNamespace.'\\'.$route->getClassName(); - } - - if (class_exists($constructor)) { - $instance = new $constructor(); - - if ($instance instanceof WebServicesManager) { - if (!defined('API_CALL')) { - define('API_CALL', true); - } - $instance->process(); - } else if ($instance instanceof WebPage) { - $instance->render(); - } else if ($route->getAction() !== null) { - $toCall = $route->getAction(); - $instance->$toCall(); - } - } - } - } else { - $fileObj->view(); - } - } - /** - * Send http 301 response code and redirect the request to non-www URI. - * - * @param RouterUri $uriObj - */ - private function redirectToNonWWW(RouterUri $uriObj) { - App::getResponse()->setCode(301); - $path = ''; - - $host = substr($uriObj->getHost(), strpos($uriObj->getHost(), '.')); - - for ($x = 1 ; $x < count($uriObj->getPathArray()) ; $x++) { - $path .= '/'.$uriObj->getPathArray()[$x]; - } - $queryString = ''; - - if (strlen($uriObj->getQueryString()) > 0) { - $queryString = '?'.$uriObj->getQueryString(); - } - $fragment = ''; - - if (strlen($uriObj->getFragment()) > 0) { - $fragment = '#'.$uriObj->getFragment(); - } - $port = ''; - - if (strlen($uriObj->getPort()) > 0) { - $port = ':'.$uriObj->getPort(); - } - App::getResponse()->addHeader('location', $uriObj->getScheme().'://'.$host.$port.$path.$queryString.$fragment); - App::getResponse()->send(); - } - /** - * Route a given URI to its specified resource. - * - * If the router has no routes, the router will send back a '418 - I'm A - * Teapot' response. If the route is available but the file that the - * router is routing to do not exist, a '500 - Server Error' Response - * with the message 'The resource 'a_resource' was available but its route is not configured correctly.' is - * sent back. If the route is not found, The router will call the function - * that was set by the user in case a route is not found. - * - * @param string $uri A URI such as 'http://www.example.com/hello/ibrahim' - * - * @param bool $loadResource If set to true, the resource that represents the - * route will be loaded. If false, the route will be only resolved. Default - * is true. - * - * @since 1.0 - */ - private function resolveUrlHelper(string $uri, bool $loadResource = true) { - $this->uriObj = null; - - if (self::routesCount() != 0) { - $routeUri = new RouterUri($uri, ''); - - if ($routeUri->hasWWW() && defined('NO_WWW') && NO_WWW === true) { - $this->redirectToNonWWW($routeUri); - } - //first, search for the URI without checking parameters - if ($this->searchRoute($routeUri, $uri, $loadResource)) { - return; - } - //if no route found, try to replace parameters with values - //note that query string vars are optional. - if ($this->searchRoute($routeUri, $uri, $loadResource, true)) { - return; - } - - //if we reach this part, this means the route was not found - if ($loadResource) { - call_user_func($this->onNotFound); - } - } else { - if ($loadResource === true) { - $page = new StarterPage(); - - $page->render(); - } - } - } - - /** - * - * @param RouterUri $route - * @param bool $loadResource - * @throws RoutingException - */ - private function routeFound(RouterUri $route, bool $loadResource) { - - if ($route->isRequestMethodAllowed((App::getRequest()->getMethod()))) { - $this->uriObj = $route; - - foreach ($route->getMiddleware() as $mw) { - $mw->before(App::getRequest(), App::getResponse()); - - if (App::getResponse()->getCode() >= 400) { - App::getResponse()->send(); - - return; - } - } - - if ($route->getType() == self::API_ROUTE && !defined('API_CALL')) { - define('API_CALL', true); - } - if (is_callable($route->getRouteTo())) { - if ($loadResource === true) { - call_user_func_array($route->getRouteTo(),$route->getClosureParams()); - } - } else { - $file = $route->getRouteTo(); - // A route created using the syntax Class::class - $xFile = '\\'.str_replace("/", "\\", $file); - - if (class_exists($xFile) && $loadResource) { - $class = new $xFile(); - - if ($class instanceof WebServicesManager) { - $class->process(); - } else if ($class instanceof WebPage) { - $class->render(); - } else if ($route->getAction() !== null) { - $toCall = $route->getAction(); - $class->$toCall(); - } - } else { - $routeType = $route->getType(); - - if ($routeType == self::VIEW_ROUTE || $routeType == self::CUSTOMIZED || $routeType == self::API_ROUTE) { - $file = ROOT_PATH.$routeType.$this->fixFilePath($file); - } else { - $file = ROOT_PATH.$this->fixFilePath($file); - } - - if (gettype($file) == 'string' && file_exists($file)) { - if ($loadResource === true) { - $route->setRoute($file); - $this->loadResourceHelper($route); - } - } else { - if ($loadResource === true) { - $message = 'The resource "'.App::getRequest()->getRequestedURI().'" was available. ' - .'but its route is not configured correctly. ' - .'The resource which the route is pointing to was not found.'; - - if (defined('WF_VERBOSE') && WF_VERBOSE) { - $message = 'The resource "'.App::getRequest()->getRequestedURI().'" was available. ' - .'but its route is not configured correctly. ' - .'The resource which the route is pointing to was not found ('.$file.').'; - } - throw new RoutingException($message); - } - } - } - } - } else { - App::getResponse()->setCode(405); - - if (!defined('API_CALL')) { - $notFoundView = new HTTPCodeView(405); - $notFoundView->render(); - } else { - $json = new Json([ - 'message' => 'Request method not allowed.', - 'type' => 'error' - ]); - App::getResponse()->write($json); - } - } - } - - /** - * - * @param RouterUri $routeUri - * @param string $uri - * @param bool $loadResource - * @param bool $withVars - * @return bool - * @throws RoutingException - */ - private function searchRoute(RouterUri $routeUri, string $uri, bool $loadResource, bool $withVars = false): bool { - - $pathArray = $routeUri->getPathArray(); - $requestMethod = App::getRequest()->getMethod(); - $indexToSearch = 'static'; - - if ($withVars) { - $indexToSearch = 'variable'; - } - - if ($indexToSearch == 'static') { - $route = isset($this->routes[$indexToSearch][$routeUri->getPath()]) ? - $this->routes[$indexToSearch][$routeUri->getPath()] : null; - - if ($route instanceof RouterUri) { - if (!$route->isCaseSensitive()) { - $isEqual = strtolower($route->getUri()) == - strtolower($routeUri->getUri()); - } else { - $isEqual = $route->getUri() == $routeUri->getUri(); - } - - if ($isEqual) { - $route->setRequestedUri($uri); - $this->routeFound($route, $loadResource); - - return true; - } - } - } else { - foreach ($this->routes['variable'] as $route) { - $this->setUriVarsHelper($route, $pathArray, $requestMethod); - - if ($route->isAllParametersSet() && $route->setRequestedUri($uri)) { - $this->routeFound($route, $loadResource); - - return true; - } - } - } - - return false; - } - /** - * Sets a callback to call in case a given rout is not found. - * - * @param callable $function The function which will be called if - * the rout is not found. - * - * @since 1.0 - */ - private function setOnNotFoundHelper(callable $function) { - $this->onNotFound = $function; - } - /** - * - * @param RouterUri $uriRouteObj One URI object taken from stored routes. - * - * @param array $requestedPathArr An array that contains requested URI path - * part. - * - * @param string $requestMethod - */ - private function setUriVarsHelper(RouterUri $uriRouteObj, array $requestedPathArr, string $requestMethod) { - $routePathArray = $uriRouteObj->getPathArray(); - $pathVarsCount = count($routePathArray); - $requestedPathPartsCount = count($requestedPathArr); - - for ($x = 0 ; $x < $pathVarsCount ; $x++) { - if ($x == $requestedPathPartsCount) { - break; - } - - if ($this->isDirectoryAVar($routePathArray[$x])) { - $varName = trim($routePathArray[$x], '{}'); - - if ($varName[strlen($varName) - 1] == '?') { - $varName = trim($varName, '?'); - } - $uriRouteObj->setParameterValue($varName, $requestedPathArr[$x]); - - if ($requestMethod == 'POST' || $requestMethod == 'PUT') { - $_POST[$varName] = filter_var(urldecode($requestedPathArr[$x])); - } else if ($requestMethod == 'GET' || $requestMethod == 'DELETE' || Runner::isCLI()) { - //usually, in CLI there is no request method. - //but we store result in $_GET. - $_GET[$varName] = filter_var(urldecode($requestedPathArr[$x])); - } - } else if ((!$uriRouteObj->isCaseSensitive() && (strtolower($routePathArray[$x]) != strtolower($requestedPathArr[$x]))) || $routePathArray[$x] != $requestedPathArr[$x]) { - break; - } - } - } /** * Adds new route to a page. * - * A page can be any file that is added inside the folder '/pages'. - * - * @param array $options An associative array that contains route - * options. Available options are: - * + * @param array $options An associative array that contains route options. * * @return bool The method will return true if the route was created. - * If a route for the given path was already created, the method will return - * false. * * @since 1.2 * @@ -1644,12 +681,13 @@ private function setUriVarsHelper(RouterUri $uriRouteObj, array $requestedPathAr private static function view(array $options): bool { if (gettype($options) == 'array') { $options[RouteOption::TYPE] = Router::VIEW_ROUTE; - self::addToMiddlewareGroup($options, 'web'); + RouteBuilder::addToMiddlewareGroup($options, 'web'); + if (!isset($options[RouteOption::CACHE_DURATION])) { - //Cache pages for 1 hour by default $options[RouteOption::CACHE_DURATION] = 3600; } - return Router::getInstance()->addRouteHelper1($options); + + return Router::getInstance()->getBuilder()->addRoute($options); } return false;