<?php declare(strict_types=1); namespace PhpSlides\Router; use PhpSlides\Core\Controller\Controller; use PhpSlides\Core\Foundation\Application; use PhpSlides\Router\Interface\MapInterface; /** * Class MapRoute * * This class is responsible for mapping and matching routes against the current request URI and HTTP method. * It extends the Controller class and implements the MapInterface interface. * * @author dconco <info@dconco.dev> * @version 1.4.0 * @package PhpSlides */ class MapRoute extends Controller implements MapInterface { use \PhpSlides\Core\Utils\Validate; use \PhpSlides\Core\Utils\Routes\StrictTypes; /** * @var string|array $route The route(s) to be mapped. */ private static string|array $route; /** * @var string $request_uri The URI of the current request. */ private static string $request_uri; /** * @var array $method An array to store HTTP methods for routing. */ private static array $method; /** * Matches the given HTTP method and route against the current request URI. * * @param string $method The HTTP method(s) to match, separated by '|'. * @param string|array $route The route pattern(s) to match. * @return bool|array Returns false if no match is found, or an array with the matched method, route, and parameters if a match is found. * * The function performs the following steps: * - Sets the HTTP method(s) to match. * - Normalizes the request URI by removing leading and trailing slashes and converting to lowercase. * - Normalizes the route pattern(s) by removing leading and trailing slashes and converting to lowercase. * - Checks if the route contains a pattern and resolves it if necessary. * - Extracts parameter names from the route pattern. * - Matches the request URI against the route pattern and extracts parameter values. * - Constructs a regex pattern to match the route. * - Checks if the request method is allowed for the matched route. * - Returns an array with the matched method, route, and parameters if a match is found. * - Returns false if no match is found. */ public function match(string $method, string|array $route): bool|array { self::$method = explode('|', $method); /** * ---------------------------------------------- * | Replacing first and last forward slashes * | $_REQUEST['uri'] will be empty if req uri is / * ---------------------------------------------- */ self::$request_uri = preg_replace("/(^\/)|(\/$)/", '', Application::$request_uri); self::$request_uri = empty(self::$request_uri) ? '/' : self::$request_uri; self::$route = is_array($route) ? $route : preg_replace("/(^\/)|(\/$)/", '', $route); // Firstly, resolve route with pattern if (is_array(self::$route)) { foreach (self::$route as $value) { if (str_starts_with($value, 'pattern:')) { if ($p = $this->pattern()) { return $p; } } } } elseif (str_starts_with(self::$route, 'pattern:')) { return $this->pattern(); } // will store all the parameters value in this array $req = []; $unvalidate_req = []; $req_value = []; // will store all the parameters names in this array $paramKey = []; // finding if there is any {?} parameter in $route if (is_string(self::$route)) { preg_match_all('/(?<={).+?(?=})/', self::$route, $paramMatches); } // if the route does not contain any param call routing(); if ( empty($paramMatches) || empty($paramMatches[0] ?? []) || is_array(self::$route) ) { /** * ------------------------------------------------------ * | Check if $callback is a callable function * | or array of controller, and if not, * | it's a string of text or html document * ------------------------------------------------------ */ return $this->match_routing(); } // setting parameters names foreach ($paramMatches[0] as $key) { $paramKey[] = $key; } // exploding route address $uri = explode('/', self::$route); // will store index number where {?} parameter is required in the $route $indexNum = []; // storing index number, where {?} parameter is required with the help of regex foreach ($uri as $index => $param) { if (preg_match('/{.*}/', $param)) { $indexNum[] = $index; } } /** * ---------------------------------------------------------------------------------- * | Exploding request uri string to array to get the exact index number value of parameter from $_REQUEST['uri'] * ---------------------------------------------------------------------------------- */ $reqUri = explode('/', self::$request_uri); /** * ---------------------------------------------------------------------------------- * | Running for each loop to set the exact index number with reg expression this will help in matching route * ---------------------------------------------------------------------------------- */ foreach ($indexNum as $key => $index) { /** * -------------------------------------------------------------------------------- * | In case if req uri with param index is empty then return because URL is not valid for this route * -------------------------------------------------------------------------------- */ if (empty($reqUri[$index])) { return false; } if (str_contains($paramKey[$key], ':')) { $unvalidate_req[] = [ $paramKey[$key], $reqUri[$index] ]; } // setting params with params names $key = trim((string) explode(':', $paramKey[$key], 2)[0]); $req[$key] = $reqUri[$index]; $req_value[] = $reqUri[$index]; // this is to create a regex for comparing route address $reqUri[$index] = '{.*}'; } // converting array to string $reqUri = implode('/', $reqUri); /** * ----------------------------------- * | replace all / with \/ for reg expression * | regex to match route is ready! * ----------------------------------- */ $reqUri = str_replace('/', '\\/', $reqUri); $route = self::$route; if (Application::$caseInSensitive === true) { $reqUri = strtolower($reqUri); $route = strtolower($route); } // now matching route with regex if (preg_match("/$reqUri/", $route . '$')) { // checks if the requested method is of the given route if ( !in_array($_SERVER['REQUEST_METHOD'], self::$method) && !in_array('*', self::$method) ) { http_response_code(405); exit('Method Not Allowed'); } if (!empty($unvalidate_req)) { foreach ($unvalidate_req as $value) { $param_name = trim((string) explode(':', $value[0], 2)[0]); $param_types = trim((string) explode(':', $value[0], 2)[1]); $param_types = preg_split('/\|(?![^<]*>)/', $param_types); $param_value = $value[1]; $parsed_value = static::matchStrictType( $param_value, $param_types, ); $req[$param_name] = $parsed_value; } } return [ 'method' => $_SERVER['REQUEST_METHOD'], 'route' => self::$route, 'params_value' => $req_value, 'params' => $req, ]; } return false; } /** * Matches the current request URI and method against the defined routes. * * This method checks if the current request URI matches any of the defined routes * and if the request method is allowed for the matched route. If a match is found, * it returns an array containing the request method and the matched route. If no * match is found, it returns false. * * @return bool|array Returns an array with 'method' and 'route' keys if a match is found, otherwise false. */ private function match_routing (): bool|array { $uri = []; $str_route = ''; $request_uri = self::$request_uri; if (is_array(self::$route)) { for ($i = 0; $i < count(self::$route); $i++) { $each_route = preg_replace("/(^\/)|(\/$)/", '', self::$route[$i]); empty($each_route) ? array_push($uri, '/') : array_push($uri, Application::$caseInSensitive === true ? strtolower($each_route) : $each_route); } } else { $str_route = empty(self::$route) ? '/' : self::$route; } if (Application::$caseInSensitive === true) { $request_uri = strtolower($request_uri); $str_route = strtolower($str_route); } if ( in_array(self::$request_uri, $uri) || self::$request_uri === $str_route ) { if ( !in_array($_SERVER['REQUEST_METHOD'], self::$method) && !in_array('*', self::$method) ) { http_response_code(405); exit('Method Not Allowed'); } return [ 'method' => $_SERVER['REQUEST_METHOD'], 'route' => self::$route, ]; } else { return false; } } /** * Validates and matches a route pattern. * * This method checks if the route is an array and iterates through each value to find a pattern match. * If a pattern match is found, it validates the pattern and returns the matched result. * If no match is found in the array, it returns false. * If the route is not an array, it directly validates the pattern and returns the result. * * @return array|bool The matched pattern as an array if found, or false if no match is found. */ private function pattern (): array|bool { if (is_array(self::$route)) { foreach (self::$route as $value) { if (str_starts_with('pattern:', $value)) { $matched = $this->validatePattern($value); if ($matched) { return $matched; } } } return false; } return $this->validatePattern(self::$route); } /** * Validates the given pattern against the request URI and checks the request method. * * @param string $pattern The pattern to validate. * @return array|bool Returns an array with the request method and route if the pattern matches, otherwise false. */ private function validatePattern (string $pattern): array|bool { $request_uri = self::$request_uri; $pattern = preg_replace("/(^\/)|(\/$)/", '', trim(substr($pattern, 8))); if (Application::$caseInSensitive === true) { $request_uri = strtolower($request_uri); $pattern = strtolower($pattern); } if (fnmatch($pattern, $request_uri)) { if ( !in_array($_SERVER['REQUEST_METHOD'], self::$method) && !in_array('*', self::$method) ) { http_response_code(405); exit('Method Not Allowed'); } return [ 'method' => $_SERVER['REQUEST_METHOD'], 'route' => self::$route, ]; } return false; } }