Router.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. <?php
  2. namespace KarmaFW\Routing;
  3. use \KarmaFW\App;
  4. use \KarmaFW\App\Tools;
  5. use \KarmaFW\WebApp;
  6. use \KarmaFW\App\Pipe;
  7. use \KarmaFW\Http\Request;
  8. use \KarmaFW\Http\Response;
  9. class Router
  10. {
  11. private static $routes = [];
  12. private static $routed_url = null;
  13. private static $config = [];
  14. private static $matched_route = null;
  15. public static function config($config)
  16. {
  17. self::$config = $config;
  18. }
  19. public static function setConfig($key, $value)
  20. {
  21. self::$config[$key] = $value;
  22. }
  23. public static function getConfig($key)
  24. {
  25. return self::$config[$key];
  26. }
  27. public static function group($config, $callable)
  28. {
  29. $old_config = self::$config;
  30. //$middlewares = isset($config['middlewares']) ? $config['middlewares'] : [];
  31. self::$config = $config;
  32. $callable();
  33. self::$config = $old_config;
  34. }
  35. // Register a route in the router
  36. public static function add($methods, $url_match, $callback=null, $type_match='exact', $regex_params=[])
  37. {
  38. $route = new Route();
  39. $default_get_prefix = null;
  40. $default_get_prefix = function () {
  41. return Router::getMatchedRoute()->getMatchedPrefix();
  42. };
  43. if (! empty(self::$config['prefix'])) {
  44. // ex: $prefix == "/fr"
  45. $route->setPrefix(self::$config['prefix'], 'exact', self::$config['prefix']);
  46. } else if (! empty(self::$config['prefix_regex'])) {
  47. // ex: $prefix == "/[a-zA-Z0-9-]+"
  48. $get_prefix = empty(self::$config['get_prefix']) ? $default_get_prefix : self::$config['get_prefix'];
  49. $route->setPrefix(self::$config['prefix_regex'], 'regex', $get_prefix);
  50. } else if (! empty(self::$config['prefix_array'])) {
  51. // ex: $prefix == ["/fr", "/us"]
  52. $get_prefix = empty(self::$config['get_prefix']) ? $default_get_prefix : self::$config['get_prefix'];
  53. $route->setPrefix(self::$config['prefix_array'], 'array', $get_prefix);
  54. }
  55. if (! empty(self::$config['before_callback'])) {
  56. $route->setBeforeCallback(self::$config['before_callback']);
  57. }
  58. if (! empty(self::$config['middlewares'])) {
  59. $route->setMiddlewares(self::$config['middlewares']);
  60. }
  61. if (isset(self::$config['continue'])) {
  62. $route->setContinue(self::$config['continue']);
  63. }
  64. $route->setMatchUrl($url_match);
  65. $route->setCallback($callback);
  66. $route->setMatchType($type_match);
  67. $route->setRegexParams($regex_params);
  68. if (! is_array($methods)) {
  69. $methods = [$methods];
  70. }
  71. foreach ($methods as $method) {
  72. $route->setMethod($method);
  73. }
  74. self::$routes[] = $route;
  75. return $route;
  76. }
  77. public static function error404($callback=null)
  78. {
  79. // todo: faire en sorte qu'elle soit executée en dernier
  80. return self::all('.*', $callback, 'regex');
  81. }
  82. // Allow whatever method (GET, POST, HEAD, OPTION, DELETE, PUT, ...)
  83. public static function all($url_match, $callback=null, $type_match='exact', $regex_params=[])
  84. {
  85. return self::Add(null, $url_match, $callback, $type_match, $regex_params);
  86. }
  87. // GET method
  88. public static function get($url_match, $callback=null, $type_match='exact', $regex_params=[])
  89. {
  90. return self::Add('GET', $url_match, $callback, $type_match, $regex_params);
  91. }
  92. // POST method
  93. public static function post($url_match, $callback=null, $type_match='exact', $regex_params=[])
  94. {
  95. return self::Add('POST', $url_match, $callback, $type_match, $regex_params);
  96. }
  97. // DELETE method
  98. public static function delete($url_match, $callback=null, $type_match='exact', $regex_params=[])
  99. {
  100. return self::Add('DELETE', $url_match, $callback, $type_match, $regex_params);
  101. }
  102. // PUT method
  103. public static function put($url_match, $callback=null, $type_match='exact', $regex_params=[])
  104. {
  105. return self::Add('PUT', $url_match, $callback, $type_match, $regex_params);
  106. }
  107. // HEAD method
  108. public static function head($url_match, $callback=null, $type_match='exact', $regex_params=[])
  109. {
  110. return self::Add('HEAD', $url_match, $callback, $type_match, $regex_params);
  111. }
  112. // PATCH method
  113. public static function patch($url_match, $callback=null, $type_match='exact', $regex_params=[])
  114. {
  115. return self::Add('PATCH', $url_match, $callback, $type_match, $regex_params);
  116. }
  117. // OPTIONS method
  118. public static function options($url_match, $callback=null, $type_match='exact', $regex_params=[])
  119. {
  120. return self::Add('OPTIONS', $url_match, $callback, $type_match, $regex_params);
  121. }
  122. // Lookup the first matching route then execute it
  123. public static function routeByUrl($request_method, $request_uri, $debug = false, $response = null)
  124. {
  125. foreach (self::$routes as $route) {
  126. if ($debug) {
  127. pre($route);
  128. }
  129. $route->setCalledMethod($request_method);
  130. $route->setCalledUrl($request_uri);
  131. $match = $route->match($request_method, $request_uri);
  132. if ($match) {
  133. if ($debug) {
  134. echo " => MATCH !<br />" . PHP_EOL;
  135. }
  136. self::$matched_route = $route;
  137. $before_callback = $route->getBeforeCallback();
  138. if (! empty($before_callback)) {
  139. $before_callback($route);
  140. }
  141. $callback = $route->getCallback();
  142. if (empty($callback)) {
  143. // Do nothing
  144. return 0;
  145. } else if (is_callable($callback)) {
  146. self::$routed_url = $route;
  147. self::routeRun($route, $callback, $request_method, $request_uri, $response);
  148. } else {
  149. // Error: callback not callable
  150. return null;
  151. }
  152. return $route;
  153. }
  154. }
  155. // No matching route
  156. return false;
  157. }
  158. public static function routeRun($route, $callback, $request_method, $request_uri, $response=null)
  159. {
  160. $matched_params = $route->getMatchedParams();
  161. $suffix = empty($route->getName()) ? '' : (' [' . $route->getName() . ']');
  162. $service_name = $route->getMethods() . ' ' . $route->getMatchUrl() . $suffix;
  163. $debugbar = App::getData('debugbar');
  164. if ($debugbar) {
  165. if (isset($debugbar['time'])) {
  166. $debugbar['time']->startMeasure($service_name, [], Tools::getCaller([__FILE__]));
  167. }
  168. }
  169. if (gettype($callback) == 'array') {
  170. //echo " => ARRAY !<br />" . PHP_EOL;
  171. //pre($callback, 1);
  172. $controller = new $callback[0]($request_uri, $request_method, $route, $response);
  173. WebApp::$controller = $controller;
  174. call_user_func([$controller, $callback[1]], $matched_params);
  175. } else {
  176. //echo " => FUNCTION !<br />" . PHP_EOL;
  177. //pre($callback, 1);
  178. $callback($request_uri, $request_method, $route, $matched_params, $response);
  179. }
  180. $debugbar = App::getData('debugbar');
  181. if ($debugbar) {
  182. if (isset($debugbar['time'])) {
  183. $debugbar['time']->stopMeasure($service_name, $service_name);
  184. }
  185. }
  186. return true;
  187. }
  188. public static function routeRequest(Request $request, Response $response)
  189. {
  190. $request_method = $request->getMethod();
  191. $request_uri = $request->getUrl();
  192. foreach (self::$routes as $route) {
  193. $route->setCalledMethod($request_method);
  194. $route->setCalledUrl($request_uri);
  195. $match = $route->match($request_method, $request_uri);
  196. if ($match) {
  197. self::$matched_route = $route;
  198. $request->setRoute($route);
  199. $before_callback = $route->getBeforeCallback();
  200. if (! empty($before_callback)) {
  201. $before_callback($route);
  202. }
  203. $callback = $route->getCallback();
  204. if (empty($callback)) {
  205. // route found but no callback defined
  206. //return 0;
  207. $continue = $request->getRoute()->getContinue();
  208. $response = self::requestRouteRun($route, null, $request, $response);
  209. //$middlewares_result = true; // TODO: $request->getRoute()->getMiddlewaresResult();
  210. if ($continue && $response->getStatus() == 200) {
  211. //$response = $response_old;
  212. continue;
  213. }
  214. if ($response->getStatus() !== 200) {
  215. return $response;
  216. }
  217. return $response->setHtml('<h1>Page not Found</h1><p>Warning: route found but no callback defined</p>', 404);
  218. } else if (is_callable($callback)) {
  219. // OK !
  220. self::$routed_url = $route;
  221. //$response_old = clone $response;
  222. $response = self::requestRouteRun($route, $callback, $request, $response);
  223. //pre($response, 1); exit;
  224. if ($request->getRoute()->getContinue() && $response->getStatus() == 200) {
  225. //$response = $response_old;
  226. continue;
  227. }
  228. return $response;
  229. } else {
  230. // route found but callback is not callable
  231. //return null;
  232. $response = self::requestRouteRun($route, null, $request, $response);
  233. if ($request->getRoute()->getContinue() && $response->getStatus() == 200) {
  234. //$response = $response_old;
  235. continue;
  236. }
  237. return $response->setHtml('<h1>Page not Found</h1><p>Warning: route callback is not callable</p>', 404);
  238. }
  239. }
  240. }
  241. // no matching route
  242. //return false;
  243. return $response->setHtml('<h1>Page not Found</h1><p>Warning: no matching route</p>', 404);
  244. }
  245. public static function requestRouteRun(Route $route, callable $callback=null, Request $request, Response $response)
  246. {
  247. $matched_params = $route->getMatchedParams();
  248. $middlewares_result = ['success' => false];
  249. $suffix = empty($route->getName()) ? '' : (' [' . $route->getName() . ']');
  250. $service_name = $route->getMethods() . ' ' . $route->getMatchUrl() . $suffix;
  251. $debugbar = App::getData('debugbar');
  252. if ($debugbar) {
  253. if (isset($debugbar['time'])) {
  254. $debugbar['time']->startMeasure($service_name, [], Tools::getCaller([__FILE__]));
  255. }
  256. }
  257. if (empty($callback)) {
  258. // do nothing
  259. $route->addMiddleware(function () use ($request, $response, &$middlewares_result) {
  260. $middlewares_result['success'] = true;
  261. return $response;
  262. });
  263. } else if (gettype($callback) == 'array') {
  264. //echo " => ARRAY !<br />" . PHP_EOL;
  265. //pre($callback, 1);
  266. $controller = new $callback[0]($request, $response);
  267. WebApp::$controller = $controller;
  268. //$route_response = call_user_func([$controller, $callback[1]], $matched_params);
  269. $route->addMiddleware(function () use ($controller, $callback, $matched_params, &$middlewares_result) {
  270. $middlewares_result['success'] = true;
  271. return call_user_func([$controller, $callback[1]], $matched_params);
  272. });
  273. } else {
  274. //echo " => FUNCTION !<br />" . PHP_EOL;
  275. //pre($callback, 1);
  276. //$route_response = $callback($request, $response, $matched_params);
  277. $route->addMiddleware(function () use ($callback, $request, $response, $matched_params, &$middlewares_result) {
  278. $middlewares_result['success'] = true;
  279. return $callback($request, $response, $matched_params);
  280. });
  281. }
  282. $pipe = new Pipe($route->getMiddlewares());
  283. $route_response = $pipe->next($request, $response);
  284. $debugbar = App::getData('debugbar');
  285. if ($debugbar) {
  286. if (isset($debugbar['time'])) {
  287. $debugbar['time']->stopMeasure($service_name, $service_name);
  288. }
  289. }
  290. //pre($middlewares_result);
  291. if (! $middlewares_result['success']) {
  292. // TODO
  293. $request->getRoute()->setContinue(false); // ?
  294. //$request->getRoute()->setMiddlewaresResult(true); // ?
  295. }
  296. if (is_object($route_response) && get_class($route_response) === Response::class) {
  297. $response = $route_response;
  298. } else if ($route_response) {
  299. return $response->setHtml('<html><body><h1>Server Error</h1><p>Error: $response is not a Response</p></body></html>', 404);
  300. } else {
  301. if (! $response->getContentLength()) {
  302. //return $response->setHtml('<html><body><h1>Server Error</h1><p>Error: $response is empty</p></body></html>', 404);
  303. }
  304. }
  305. return $response;
  306. }
  307. // Search a route by its name
  308. public static function findRouteByName($expected_route_name, $debug = false)
  309. {
  310. if (empty($expected_route_name)) {
  311. return null;
  312. }
  313. foreach (self::$routes as $route) {
  314. $route_name = $route->getName();
  315. if (! empty($route_name) && $route_name == $expected_route_name) {
  316. return $route;
  317. }
  318. }
  319. return null;
  320. }
  321. public static function getRouteUrl($route_name, $urls_args=[])
  322. {
  323. if (empty($urls_args)) {
  324. $urls_args = array();
  325. }
  326. if (! is_array($urls_args)) {
  327. $urls_args = array($urls_args);
  328. }
  329. $route = Router::findRouteByName($route_name);
  330. if (empty($route) || $route === true) {
  331. return null;
  332. }
  333. //pre($route, 1);
  334. $get_prefix = null;
  335. if (true) {
  336. //$get_prefix = self::$matched_route->getCallbackGetPrefix();
  337. $get_prefix = $route->getCallbackGetPrefix();
  338. //pre($get_prefix, 0, 'get_prefix: ');
  339. }
  340. $link = $route->getMatchUrl();
  341. if ($get_prefix) {
  342. $link = $get_prefix . $link;
  343. }
  344. //pre($link, 1, 'link: ');
  345. $link = rtrim($link, '$');
  346. $link = ltrim($link, '^');
  347. $link = str_replace('\\.', '.', $link);
  348. $link = str_replace('\\?', '?', $link);
  349. $link = str_replace('\\+', '+', $link);
  350. $link = str_replace('\\-', '-', $link);
  351. if (! empty($urls_args)) {
  352. foreach ($urls_args as $arg_value) {
  353. $pos1 = strpos($link, '(');
  354. if ($pos1 === false) {
  355. break;
  356. }
  357. $pos2 = strpos($link, ')', $pos1);
  358. if ($pos2 === false) {
  359. break;
  360. }
  361. $link = substr($link, 0, $pos1) . $arg_value . substr($link, $pos2+1);
  362. }
  363. }
  364. return $link;
  365. }
  366. public static function printRoutes()
  367. {
  368. dump(self::$routes);
  369. exit;
  370. }
  371. public static function getRoutedUrl()
  372. {
  373. return self::$routed_url;
  374. }
  375. public static function getMatchedRoute()
  376. {
  377. return self::$matched_route;
  378. }
  379. }