Router.php 13 KB

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