BIG UPDATE: upgraded router version + configuration

This commit is contained in:
xdrm-brackets 2017-12-10 20:35:23 +01:00
parent f1da57666d
commit 6379837deb
7 changed files with 625 additions and 127 deletions

56
build/router/controller/api.php Executable file
View File

@ -0,0 +1,56 @@
<?php
namespace router\controller;
use \api\core\Request;
use \api\core\Response;
use \api\core\Loader;
class api{
private $request;
private $response;
/* PRE-CALL
*
* @url<String> 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(){
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace router\controller;
class page{
/* PRE-CALL
*
* @url<String> Calling URI
*
*/
public function __construct($url){
}
/* CALL
*
*/
public function load(){
require_once(__ROOT__.'/view/home.php');
}
/* POST-CALL
*
*/
public function __destruct(){
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace router\controller;
class redirect{
/* PRE-CALL
*
* @url<String> Calling URI
*
*/
public function __construct($url){
}
/* CALL
*
*/
public function root(){
header('Location: /');
}
/* POST-CALL
*
*/
public function __destruct(){
}
}

View File

@ -0,0 +1,61 @@
<?php
/**************************
* Route *
* 08-12-2016 *
***************************
* Designed & Developed by *
* xdrm-brackets *
***************************
* https://xdrm.io/ *
**************************/
namespace router\core;
class ControllerFactory{
/* VERIFIE UN CONTROLLER
*
* @controller<String> Nom du controller
*
* @return exists<boolean> 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<String> Nom du controller
* @arguments<Array> [OPTIONNEL] Arguments à passer au constructeur
*
* @return instance<Module> 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);
}
}

View File

@ -1,63 +1,100 @@
<?php
/**************************************/
/* Classe de gestion de route (match) */
/**************************************/
namespace router\core;
/**************************
* Route *
* 08-12-2016 *
***************************
* Designed & Developed by *
* xdrm-brackets *
***************************
* https://xdrm.io/ *
**************************/
class Route{
use router\core\ControllerFactory;
// ATTRIBUTS
private $pattern;
private $callback;
private $matches = [];
namespace router\core;
/* Initialise la route
*
* @pattern<String> Pattern correspondant a la route
* @callback<Function> Fonction de callback de la route
*
* @return this<Route> 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<String> URL pour laquelle on veut verifier
*
* @return match<Boolean> 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<String> Pattern correspondant a la route
* @controller<String> Controller de la route
* @method<String> Methode du controller
*
* @return instance<Route> 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<String> URL
*
* @return matches<Boolean> 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<String> 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);
}
}
?>

View File

@ -1,91 +1,335 @@
<?php
/*************************************************/
/* Classe de gestion des routes (URL/ressources) */
/*************************************************/
namespace router\core;
/**************************
* Router *
* 08-12-2016 *
***************************
* Designed & Developed by *
* xdrm-brackets *
***************************
* https://xdrm.io/ *
**************************/
class Router{
// ATTRIBUTS
private $url;
private $routes;
use \router\core\ControllerFactory;
/* Initialise le routeur
*
* @url<String> l'URL de la page courante
*
* @return this<Router> 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<String> le format de l'URL associe
* @callback<Function> function a appeler si l'URL correspond
*
* @return this<Router> 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<String> le format de l'URL associe
* @callback<Function> function a appeler si l'URL correspond
*
* @return this<Router> 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<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 */
// /*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<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\.]*(?:\{[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<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;
}
}

32
config/routes.json Executable file
View File

@ -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": ".*"
}
}
}
}