diff --git a/build/router/controller/api.php b/build/router/controller/api.php new file mode 100755 index 0000000..6778a66 --- /dev/null +++ b/build/router/controller/api.php @@ -0,0 +1,56 @@ + Calling URI + * + */ + public function __construct($matches){ + + /* (1) Rebuild request url */ + $uri = $matches['uri']; + + /* (2) Creates request */ + $this->request = Loader::remote($uri); + + } + + + /* CALL + * + */ + public function call(){ + + /* (1) Process response */ + $this->response = $this->request->dispatch(); + + /* (2) Manages result */ + if( $this->response instanceof Response ) + echo $this->response->serialize(); + + return true; + + } + + /* POST-CALL + * + */ + public function __destruct(){ + + } + + + + } diff --git a/build/router/controller/page.php b/build/router/controller/page.php new file mode 100644 index 0000000..f40fe03 --- /dev/null +++ b/build/router/controller/page.php @@ -0,0 +1,34 @@ + Calling URI + * + */ + public function __construct($url){ + } + + + /* CALL + * + */ + public function load(){ + require_once(__ROOT__.'/view/home.php'); + } + + /* POST-CALL + * + */ + public function __destruct(){ + + } + + + + } diff --git a/build/router/controller/redirect.php b/build/router/controller/redirect.php new file mode 100755 index 0000000..68a0e2e --- /dev/null +++ b/build/router/controller/redirect.php @@ -0,0 +1,34 @@ + Calling URI + * + */ + public function __construct($url){ + } + + + /* CALL + * + */ + public function root(){ + header('Location: /'); + } + + /* POST-CALL + * + */ + public function __destruct(){ + + } + + + + } diff --git a/build/router/core/ControllerFactory.php b/build/router/core/ControllerFactory.php new file mode 100755 index 0000000..679e927 --- /dev/null +++ b/build/router/core/ControllerFactory.php @@ -0,0 +1,61 @@ + Nom du controller + * + * @return exists Si oui ou non le controller existe + * + */ + public static function checkController($controller){ + /* (1) Check type + pattern */ + if( !is_string($controller) || !preg_match('/^[A-Za-z_]\w+$/', $controller) ) + return false; + + /* (2) On vérifie que la classe existe */ + if( !file_exists(__BUILD__."/router/controller/$controller.php") ) + return false; + + /* (3) Sinon il existe */ + return true; + } + + + + /* INSTANCIE UN CONTROLLER + * + * @controller Nom du controller + * @arguments [OPTIONNEL] Arguments à passer au constructeur + * + * @return instance Instance du controller en question + * + */ + public static function getController($controller, $arguments=[]){ + /* (1) On vérifie l'existance du controller */ + if( !self::checkController($controller) ) + return false; + + /* (2) On récupère la classe */ + $class_name = "\\router\\controller\\$controller"; + + /* (3) On retourne une instance */ + return new $class_name($arguments); + } + + } diff --git a/build/router/core/Route.php b/build/router/core/Route.php index 6753091..83bfd95 100755 --- a/build/router/core/Route.php +++ b/build/router/core/Route.php @@ -1,63 +1,100 @@ Pattern correspondant a la route - * @callback Fonction de callback de la route - * - * @return this Retour de l'instance courante - * - */ - public function __construct($pattern, $callback){ - // On enregistre la fonction de callback - $this->callback = $callback; + class Route{ - // On formatte le pattern en regexp - $this->pattern = '#^'.$pattern.'$#'; + /* [1] Attributs + =========================================================*/ + private $pattern; + private $controller; + private $method; + private $matches; - return $this; - } - /* Verifie si l'URL correspond a la route - * - * @url URL pour laquelle on veut verifier - * - * @return match TRUE si match sinon FAUX - * - */ - public function match($url){ - // Si ne match pas -> FALSE - if( !preg_match($this->pattern, $url, $matches) ) - return false; + /* [2] Instanciation de la route + * + * @pattern Pattern correspondant a la route + * @controller Controller de la route + * @method Methode du controller + * + * @return instance Retour de l'instance courante + * + =========================================================*/ + public function __construct($pattern=null, $controller=null, $method=null){ + // Note: all arguments must be verified by 'Router->add' method - // On supprime le premier match global - array_shift($matches); + /* (1) Pattern -> regex format */ + $this->pattern = "/^$pattern$/"; - $this->matches = $matches; + /* (2) Controller */ + $this->controller = $controller; - return true; + /* (3) Controller's method */ + $this->method = $method; + + /* (4) Initialize matches */ + $this->matches = []; + + } + + + + /* [3] Checks if route matches URL + * + * @url URL + * + * @return matches If matches URL + * + =========================================================*/ + public function match($url){ + + /* (1) If doesn't matches @url -> false */ + if( !preg_match($this->pattern, $url, $matches) ) + return false; + + /* (2) Return only named matches */ + foreach($matches as $name=>$match) + if( !is_numeric($name) ) + $this->matches[$name] = $match; + + /* (4) Add complete URL */ + $this->matches['__URL__'] = $url; + + /* (5) Return status */ + return true; + } + + + /* [4] Method call + * + * @return response Response + * + =========================================================*/ + public function call(){ + /* (1) Instanciate controller */ + $instance = ControllerFactory::getController($this->controller, $this->matches); + + /* (2) Launch method & catch response */ + $response = call_user_func([$instance, $this->method]); + + /* (3) Call controller's destructor */ + $instance = null; + + /* (4) Return response */ + return $response; + } } - - /* Amorcage de la fonction de callback - * - */ - public function call(){ - return call_user_func($this->callback, $this->matches); - } - -} - -?> diff --git a/build/router/core/Router.php b/build/router/core/Router.php index ee62743..8873dfa 100755 --- a/build/router/core/Router.php +++ b/build/router/core/Router.php @@ -1,91 +1,335 @@ l'URL de la page courante - * - * @return this Retour de l'instance courante - * - */ - public function __construct($url){ - $this->url = $url; + namespace router\core; - // On initialise les routes - $this->routes = [ - 'GET' => [], - 'POST' => [] - ]; - - return $this; - } - - /* Ajoute une route GET - * - * @pattern le format de l'URL associe - * @callback function a appeler si l'URL correspond - * - * @return this Retour de l'instance courante - * - */ - public function get($pattern, $callback){ - array_push( - $this->routes['GET'], - new Route($pattern, $callback) - ); - - return $this; - } + class Router{ - /* Ajoute une route POST - * - * @pattern le format de l'URL associe - * @callback function a appeler si l'URL correspond - * - * @return this Retour de l'instance courante - * - */ - public function post($pattern, $callback){ - array_push( - $this->routes['POST'], - new Route($pattern, $callback) - ); - return $this; - } + /* [1] Attributes + =========================================================*/ + private $url; // current URL + private $cnf; // parsed configuration + private $http_methods; // allowed HTTP methods + private $routes; // built routes - /* Demarre le routeur - * - * @return this Retour de l'instance courante - * - */ - public function run(){ - $httpMethod = $_SERVER['REQUEST_METHOD']; - // Si aucune route pour la methode courante -> false - if( count($this->routes[$httpMethod]) <= 0 ) - return false; + /* [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(); - // Pour chaque route - foreach($this->routes[$httpMethod] as $route){ - // Si la route match - if( $route->match($this->url) ) - return $route->call(); // On l'amorce } - // Retourne false si erreur - return false; - } -} -?> + /* [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; + } + + } diff --git a/config/routes.json b/config/routes.json new file mode 100755 index 0000000..746489f --- /dev/null +++ b/config/routes.json @@ -0,0 +1,32 @@ +{ + + "methods": [ "GET", "POST", "PUT", "DELETE", "VIEW" ], + + + "routes": { + + "/api/v/1.0/{uri}": { + "methods": ["GET", "POST", "PUT", "DELETE", "VIEW"], + "controller": "api:call", + "arguments": { + "uri": ".*" + } + }, + + "/": { + "methods": ["GET"], + "controller": "page:load", + "arguments": {} + }, + + "/{any}": { + "methods": ["GET"], + "controller": "redirect:root", + "arguments": { + "any": ".*" + } + } + + } + +}