From 96352bb9b7a6574da98311da299536951926680d Mon Sep 17 00:00:00 2001 From: ido alit Date: Wed, 5 Jul 2017 15:29:06 +0700 Subject: [PATCH] Add: REST router for REST Full API --- api/v1/controllers/BiblioController.php | 31 +++ api/v1/controllers/Controller.php | 21 ++ api/v1/controllers/HomeController.php | 25 +++ api/v1/routes.php | 29 +++ index.php | 4 +- lib/AltoRouter.php | 276 ++++++++++++++++++++++++ lib/router.inc.php | 130 +++++++++++ sysconfig.inc.php | 3 + 8 files changed, 518 insertions(+), 1 deletion(-) create mode 100644 api/v1/controllers/BiblioController.php create mode 100644 api/v1/controllers/Controller.php create mode 100644 api/v1/controllers/HomeController.php create mode 100644 api/v1/routes.php create mode 100644 lib/AltoRouter.php create mode 100644 lib/router.inc.php diff --git a/api/v1/controllers/BiblioController.php b/api/v1/controllers/BiblioController.php new file mode 100644 index 00000000..913c348b --- /dev/null +++ b/api/v1/controllers/BiblioController.php @@ -0,0 +1,31 @@ +sysconf = $sysconf; + $this->db = $obj_db; + } + + public function detail($id, $token) + { + require_once __DIR__ . './../../../lib/api.inc.php'; + $biblio = api::biblio_load($this->db, $id); + parent::withJson($biblio[0]); + } +} \ No newline at end of file diff --git a/api/v1/controllers/Controller.php b/api/v1/controllers/Controller.php new file mode 100644 index 00000000..9ca686cf --- /dev/null +++ b/api/v1/controllers/Controller.php @@ -0,0 +1,21 @@ + false, + 'message' => 'Sugeng rawuh is selamat datang.' + ); + parent::withJson($response); + } +} \ No newline at end of file diff --git a/api/v1/routes.php b/api/v1/routes.php new file mode 100644 index 00000000..c92b7c91 --- /dev/null +++ b/api/v1/routes.php @@ -0,0 +1,29 @@ +setBasePath('api'); + +/*---------- Create routes ----------*/ +$router->map('GET', '/', 'HomeController:index'); +$router->map('GET', '/biblio/[i:id]/[a:token]', 'BiblioController:detail'); + +/*---------- Run matching route ----------*/ +$router->run(); + +// doesn't need template +exit(); \ No newline at end of file diff --git a/index.php b/index.php index e9d908d3..06f13260 100755 --- a/index.php +++ b/index.php @@ -81,7 +81,9 @@ echo $content_data['Content']; unset($content_data); } else { - header ("location:index.php"); + // header ("location:index.php"); + // check in api router + require 'api/v'.$sysconf['api']['version'].'/routes.php'; } } } else { diff --git a/lib/AltoRouter.php b/lib/AltoRouter.php new file mode 100644 index 00000000..153eaa2e --- /dev/null +++ b/lib/AltoRouter.php @@ -0,0 +1,276 @@ + '[0-9]++', + 'a' => '[0-9A-Za-z]++', + 'h' => '[0-9A-Fa-f]++', + '*' => '.+?', + '**' => '.++', + '' => '[^/\.]++' + ); + + /** + * Create router in one call from config. + * + * @param array $routes + * @param string $basePath + * @param array $matchTypes + */ + public function __construct( $routes = array(), $basePath = '', $matchTypes = array() ) { + $this->addRoutes($routes); + $this->setBasePath($basePath); + $this->addMatchTypes($matchTypes); + } + + /** + * Retrieves all routes. + * Useful if you want to process or display routes. + * @return array All routes. + */ + public function getRoutes() { + return $this->routes; + } + + /** + * Add multiple routes at once from array in the following format: + * + * $routes = array( + * array($method, $route, $target, $name) + * ); + * + * @param array $routes + * @return void + * @author Koen Punt + * @throws Exception + */ + public function addRoutes($routes){ + if(!is_array($routes) && !$routes instanceof Traversable) { + throw new \Exception('Routes should be an array or an instance of Traversable'); + } + foreach($routes as $route) { + call_user_func_array(array($this, 'map'), $route); + } + } + + /** + * Set the base path. + * Useful if you are running your application from a subdirectory. + */ + public function setBasePath($basePath) { + $this->basePath = $basePath; + } + + /** + * Add named match types. It uses array_merge so keys can be overwritten. + * + * @param array $matchTypes The key is the name and the value is the regex. + */ + public function addMatchTypes($matchTypes) { + $this->matchTypes = array_merge($this->matchTypes, $matchTypes); + } + + /** + * Map a route to a target + * + * @param string $method One of 5 HTTP Methods, or a pipe-separated list of multiple HTTP Methods (GET|POST|PATCH|PUT|DELETE) + * @param string $route The route regex, custom regex must start with an @. You can use multiple pre-set regex filters, like [i:id] + * @param mixed $target The target where this route should point to. Can be anything. + * @param string $name Optional name of this route. Supply if you want to reverse route this url in your application. + * @throws Exception + */ + public function map($method, $route, $target, $name = null) { + + $this->routes[] = array($method, $route, $target, $name); + + if($name) { + if(isset($this->namedRoutes[$name])) { + throw new \Exception("Can not redeclare route '{$name}'"); + } else { + $this->namedRoutes[$name] = $route; + } + + } + + return; + } + + /** + * Reversed routing + * + * Generate the URL for a named route. Replace regexes with supplied parameters + * + * @param string $routeName The name of the route. + * @param array @params Associative array of parameters to replace placeholders with. + * @return string The URL of the route with named parameters in place. + * @throws Exception + */ + public function generate($routeName, array $params = array()) { + + // Check if named route exists + if(!isset($this->namedRoutes[$routeName])) { + throw new \Exception("Route '{$routeName}' does not exist."); + } + + // Replace named parameters + $route = $this->namedRoutes[$routeName]; + + // prepend base path to route url again + $url = $this->basePath . $route; + + if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) { + + foreach($matches as $index => $match) { + list($block, $pre, $type, $param, $optional) = $match; + + if ($pre) { + $block = substr($block, 1); + } + + if(isset($params[$param])) { + // Part is found, replace for param value + $url = str_replace($block, $params[$param], $url); + } elseif ($optional && $index !== 0) { + // Only strip preceeding slash if it's not at the base + $url = str_replace($pre . $block, '', $url); + } else { + // Strip match block + $url = str_replace($block, '', $url); + } + } + + } + + return $url; + } + + /** + * Match a given Request Url against stored routes + * @param string $requestUrl + * @param string $requestMethod + * @return array|boolean Array with route information on success, false on failure (no match). + */ + public function match($requestUrl = null, $requestMethod = null) { + + $params = array(); + $match = false; + + // set Request Url if it isn't passed as parameter + if($requestUrl === null) { + $requestUrl = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; + } + + // strip base path from request url + $requestUrl = substr($requestUrl, strlen($this->basePath)); + + // Strip query string (?a=b) from Request Url + if (($strpos = strpos($requestUrl, '?')) !== false) { + $requestUrl = substr($requestUrl, 0, $strpos); + } + + // set Request Method if it isn't passed as a parameter + if($requestMethod === null) { + $requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; + } + + foreach($this->routes as $handler) { + list($methods, $route, $target, $name) = $handler; + + $method_match = (stripos($methods, $requestMethod) !== false); + + // Method did not match, continue to next route. + if (!$method_match) continue; + + if ($route === '*') { + // * wildcard (matches all) + $match = true; + } elseif (isset($route[0]) && $route[0] === '@') { + // @ regex delimiter + $pattern = '`' . substr($route, 1) . '`u'; + $match = preg_match($pattern, $requestUrl, $params) === 1; + } elseif (($position = strpos($route, '[')) === false) { + // No params in url, do string comparison + $match = strcmp($requestUrl, $route) === 0; + } else { + // Compare longest non-param string with url + if (strncmp($requestUrl, $route, $position) !== 0) { + continue; + } + $regex = $this->compileRoute($route); + $match = preg_match($regex, $requestUrl, $params) === 1; + } + + if ($match) { + + if ($params) { + foreach($params as $key => $value) { + if(is_numeric($key)) unset($params[$key]); + } + } + + return array( + 'target' => $target, + 'params' => $params, + 'name' => $name + ); + } + } + return false; + } + + /** + * Compile the regex for a given route (EXPENSIVE) + */ + protected function compileRoute($route) { + if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) { + + $matchTypes = $this->matchTypes; + foreach($matches as $match) { + list($block, $pre, $type, $param, $optional) = $match; + + if (isset($matchTypes[$type])) { + $type = $matchTypes[$type]; + } + if ($pre === '.') { + $pre = '\.'; + } + + $optional = $optional !== '' ? '?' : null; + + //Older versions of PCRE require the 'P' in (?P) + $pattern = '(?:' + . ($pre !== '' ? $pre : null) + . '(' + . ($param !== '' ? "?P<$param>" : null) + . $type + . ')' + . $optional + . ')' + . $optional; + + $route = str_replace($block, $pattern, $route); + } + + } + return "`^$route$`u"; + } +} diff --git a/lib/router.inc.php b/lib/router.inc.php new file mode 100644 index 00000000..60580fa1 --- /dev/null +++ b/lib/router.inc.php @@ -0,0 +1,130 @@ +sysconf = $sysconf; + $this->db = $obj_db; + } + + public function match($requestUrl = null, $requestMethod = null) + { + $params = array(); + $match = false; + + // set Request Url if it isn't passed as parameter + if($requestUrl === null) { + $path = explode('/', $_GET['p']); + if ($path[0] == $this->basePath) { + $requestUrl = $_GET['p']; + } else { + $requestUrl = '/'; + } + } + + // strip base path from request url + $requestUrl = substr($requestUrl, strlen($this->basePath)); + + // Strip query string (?a=b) from Request Url + if (($strpos = strpos($requestUrl, '?')) !== false) { + $requestUrl = substr($requestUrl, 0, $strpos); + } + + // set Request Method if it isn't passed as a parameter + if($requestMethod === null) { + $requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; + } + + foreach($this->routes as $handler) { + list($methods, $route, $target, $name) = $handler; + + $method_match = (stripos($methods, $requestMethod) !== false); + + // Method did not match, continue to next route. + if (!$method_match) continue; + + if ($route === '*') { + // * wildcard (matches all) + $match = true; + } elseif (isset($route[0]) && $route[0] === '@') { + // @ regex delimiter + $pattern = '`' . substr($route, 1) . '`u'; + $match = preg_match($pattern, $requestUrl, $params) === 1; + } elseif (($position = strpos($route, '[')) === false) { + // No params in url, do string comparison + $match = strcmp($requestUrl, $route) === 0; + } else { + // Compare longest non-param string with url + if (strncmp($requestUrl, $route, $position) !== 0) { + continue; + } + $regex = $this->compileRoute($route); + $match = preg_match($regex, $requestUrl, $params) === 1; + } + + if ($match) { + + if ($params) { + foreach($params as $key => $value) { + if(is_numeric($key)) unset($params[$key]); + } + } + + return array( + 'target' => $target, + 'params' => $params, + 'name' => $name + ); + } + } + return false; + } + + public function makeCallable($string) + { + $method = explode(':', $string); + if (isset($method[1]) && class_exists($method[0])) { + $instance = new $method[0]($this->sysconf, $this->db); + if (method_exists($instance, $method[1])) { + return array($instance, $method[1]); + } + } + return false; + } + + public function run() + { + // match current request url + $match = $this->match(); + // call closure or throw 404 status + if( $match && is_callable( $match['target'] ) ) { + call_user_func_array( $match['target'], $match['params'] ); + } else { + if ($callable = $this->makeCallable($match['target'])) { + call_user_func_array($callable, $match['params']); + } else { + // no route was matched + // header( $_SERVER["SERVER_PROTOCOL"] . ' 404 Not Found'); + // include $this->sysconf['template']['dir'].'/'.$this->sysconf['template']['theme'].'/404.php'; + header ("location:index.php"); + } + } + } +} \ No newline at end of file diff --git a/sysconfig.inc.php b/sysconfig.inc.php index cbffc5e6..912f37b7 100755 --- a/sysconfig.inc.php +++ b/sysconfig.inc.php @@ -652,3 +652,6 @@ function stripslashes_deep($value) /* new log system */ $sysconf['log']['biblio'] = TRUE; + +// REST Api +$sysconf['api']['version'] = 1; \ No newline at end of file