run(); } /* [3] Constructor * * @url Current URL * * @return instance Instance du routeur * =========================================================*/ public function __construct($url=null){ /* (1) Checks arguments ---------------------------------------------------------*/ /* (1) Default value if incorrect */ $this->url = is_string($url) ? $url : ''; /* (2) Add first '/' if missing */ if( !preg_match('/^\//', $url) ) $this->url = '/'.$this->url; /* (2) Loads configuration ---------------------------------------------------------*/ /* (1) Tries to load configuration */ $this->cnf = self::loadConfig(); /* (2) If error occurs, throw Exception */ if( is_null($this->cnf) ) throw new \Exception("[Router] Configuration file error found"); /* (3) Set allowed HTTP methods ---------------------------------------------------------*/ /* (1) If not defined */ if( !isset($this->cnf['methods']) ) throw new \Exception('[Router] Configuration file error, \'methods\' clause missing'); /* (2) Try to clean methods */ $this->http_methods = self::cleanMethods($this->cnf['methods']); /* (3) Manage error */ if( is_null($this->http_methods) ) throw new \Exception('[Router] Configuration file error. \'methods\' must be an array of HTTP methods ["GET", "POST", ...]'); /* (4) Initialize routes ---------------------------------------------------------*/ /* (1) Init routes */ $this->routes = []; foreach($this->http_methods as $method) $this->routes[$method] = []; /* (2) Default configuration if missing */ if( !isset($this->cnf['routes']) || !is_array($this->cnf['routes']) ) $this->cnf['routes'] = []; /* (5) Loads each route ---------------------------------------------------------*/ foreach($this->cnf['routes'] as $pattern=>$route){ /* (1) If missing (required) parameters */ if( !isset($route['controller']) ) continue; /* (2) Default value for 'methods' */ ( !isset($route['methods']) || !is_array($route['methods']) ) && ($route['methods'] = $this->http_methods); /* (3) Default value for 'arguments' */ ( !isset($route['arguments']) || !is_array($route['arguments']) ) && ($route['arguments'] = []); /* (4) Add route */ $added = $this->add($pattern, $route['controller'], $route['arguments']); // if error -> next if( $added === false ) continue; /* (5) Add route for each method */ foreach($route['methods'] as $method) if( in_array($method, $this->http_methods) ) $this->routes[$method][] = $added; } } /* [4] Adds a route * * @pattern URL pattern with {somevar} variables within * @controller Controller name + method "controllername:methodname" * @arguments List of pattern's arguments and their RegExp composition (default is alphanumeric) * * @return route New instance of Route || false on error * =========================================================*/ public function add($pattern=null, $controller=null, $arguments=[]){ /* (1) Format and check pattern ---------------------------------------------------------*/ /* (1) If not a string */ if( !is_string($pattern) ) return false; /* (2) Format pattern and check result */ // /*DEBUG*/ var_dump($pattern); $pattern = self::formatPattern($pattern, $arguments); // /*DEBUG*/ var_dump($pattern); if( $pattern === false ) return false; /* (2) Check controller ---------------------------------------------------------*/ /* (1) Check default type */ if( !is_string($controller) || !preg_match('/^([A-Za-z_]\w+):([A-Za-z_]\w+)$/', $controller, $c_matches) ) return false; /* (2) Check existence */ if( !ControllerFactory::checkController($c_matches[1]) ) return false; /* (3) Check method ---------------------------------------------------------*/ if( !method_exists('\\router\\controller\\'.$c_matches[1], $c_matches[2]) ) return false; /* (4) Return new route ---------------------------------------------------------*/ return new Route($pattern, $c_matches[1], $c_matches[2]); } /* [5] Router launch * =========================================================*/ public function run(){ /* (1) Manage HTTP method ---------------------------------------------------------*/ /* (1) Fetch HTTP method */ $httpMethod = $_SERVER['REQUEST_METHOD']; /* (2) If no route for this -> exit */ if( !isset($this->routes[$httpMethod]) || count($this->routes[$httpMethod]) <= 0 ) return false; /* (2) Manage routes (matching) ---------------------------------------------------------*/ /* (1) Check for each HTTP method's route */ foreach($this->routes[$httpMethod] as $route) /* (2) First route that matches -> call & return response */ if( $route->match($this->url) ) return $route->call(); /* (3) If no route found -> return false ---------------------------------------------------------*/ return false; } /* FORMATS A PATTERN * * @pattern Pattern to process on * @arguments List of used arguments, with regex if given * @vars [OPT] If variable replacement have to be done * * @return formatted Formatted pattern || false on error * */ public static function formatPattern($pattern, $arguments=[]){ /* (1) Check arguments ---------------------------------------------------------*/ /* (1) Check minimal length */ if( strlen($pattern) < 1 ) return false; /* (2) Arguments formatting */ $arguments = !is_array($arguments) ? [] : $arguments; /* (2) Replace special characters + replace vars ---------------------------------------------------------*/ /* (1) Check default URL format */ if( !preg_match('/^(\/[\w\.]*(?:\{[a-z_]+\})*[\w\.]*)*\/?$/i', $pattern) ) return false; /* (2) Escape special characters */ $pattern = str_replace('/', '\\/', $pattern); /* (3) Add optional ending '/' */ if( !preg_match('/\/$/', $pattern) ) $pattern .= '\\/?'; /* (4) Replace variable by tagged capturing groups */ $boundary = self::randBoundary(); $pattern = preg_replace('/\{([a-z_][a-z0-9_]*)\}/i', '(?P<$1>'.$boundary.'-$1-'.$boundary.')', $pattern); /* (3) Variable replacement ---------------------------------------------------------*/ /* (1) List variables */ $vars = []; $var_pattern = '/'.$boundary.'\-([A-Za-z_][A-Za-z0-9_]*)\-'.$boundary.'/'; preg_match_all($var_pattern, $pattern, $matches); /* (2) For each matching variable -> replace with associated regex */ if( is_array($matches) && isset($matches[1]) ){ foreach($matches[1] as $m=>$varname){ // {3.1.1} Not in @arguments -> default regex // if( !isset($arguments[$varname]) || !is_string($arguments[$varname]) ){ $pattern = str_replace($matches[0][$m], '[A-Za-z0-9_]+', $pattern); continue; } // {3.1.2} If variable in @arguments -> set regex without capturing-groups // // $without_capg = str_replace('(', '(?:', $arguments[$varname]); $pattern = str_replace($matches[0][$m], $arguments[$varname], $pattern); } } /* (4) Return formatted pattern ---------------------------------------------------------*/ return $pattern; } /* LOADS CONFIGURATION * * @return cnf Configuration content || NULL if error * */ private static function loadConfig(){ /* (1) Set configuration file's path */ $cnfpath = self::config_path(); /* (2) Checks file */ if( !file_exists($cnfpath) ) return null; // throw new \Exception("[Router] Configuration file not found"); /* (3) Checks format -> null if error */ return json_decode( file_get_contents($cnfpath), true ); } /* CHECKS METHODS AND CLEAN THE LIST IF CORRECT * * @wanted List of wanted methods * * @return cleaned Cleaned methods || null if error * */ private static function cleanMethods($wanted=[]){ /* (1) Checks @wanted */ if( !is_array($wanted) || count($wanted) < 1 ) return null; // throw new \Exception('[Router] Configuration file error, \'methods\' must be an array containing managed HTTP methods'); /* (2) Set methods (uppercase and unique) */ $cleaned = []; foreach($wanted as $method) if( !in_array(strtoupper($method), $cleaned) ) $cleaned[] = strtoupper($method); /* (3) Return cleaned method list */ return $cleaned; } }