332 lines
9.5 KiB
PHP
332 lines
9.5 KiB
PHP
<?php
|
|
|
|
/**************************
|
|
* Router *
|
|
* 08-12-2016 *
|
|
***************************
|
|
* Designed & Developed by *
|
|
* xdrm-brackets *
|
|
***************************
|
|
* https://xdrm.io/ *
|
|
**************************/
|
|
|
|
namespace router\core;
|
|
|
|
class Router{
|
|
|
|
|
|
|
|
/* [1] Attributes
|
|
=========================================================*/
|
|
private $url; // current URL
|
|
private $cnf; // parsed configuration
|
|
private $http_methods; // allowed HTTP methods
|
|
private $routes; // built routes
|
|
|
|
|
|
/* [2] Configuration file
|
|
=========================================================*/
|
|
private static function config_path(){ return __CONFIG__.'/routes.json'; }
|
|
|
|
// Get random token
|
|
public static function randBoundary(){ return dechex( random_int((int) 1e10, (int) 1e20) ); }
|
|
|
|
|
|
// Instance getter
|
|
public static function launch($url=null){
|
|
/* (1) Instanciate the router (propagation) */
|
|
$instance = new Router($url);
|
|
|
|
|
|
/* (2) Launches the router */
|
|
$instance->run();
|
|
|
|
}
|
|
|
|
|
|
/* [3] Constructor
|
|
*
|
|
* @url<String> Current URL
|
|
*
|
|
* @return instance<Router> 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<String> URL pattern with {somevar} variables within
|
|
* @controller<String> Controller name + method "controllername:methodname"
|
|
* @arguments<Array> List of pattern's arguments and their RegExp composition (default is alphanumeric)
|
|
*
|
|
* @return route<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 */
|
|
$pattern = self::formatPattern($pattern, $arguments);
|
|
|
|
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<String> Pattern to process on
|
|
* @arguments<Array> List of used arguments, with regex if given
|
|
* @vars<Boolean> [OPT] If variable replacement have to be done
|
|
*
|
|
* @return formatted<String> 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\{\}\.-]*)*\/?$/', $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<Array> 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<Array> List of wanted methods
|
|
*
|
|
* @return cleaned<Array> 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;
|
|
}
|
|
|
|
}
|