router.php 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. <?php
  2. /**
  3. * Functions for URL routing: given an URL, call the corresponding handler
  4. * function (the 'route' to the corresponding output).
  5. *
  6. * @author Taddeus Kroes
  7. * @date 14-07-2012
  8. */
  9. namespace webbasics;
  10. require_once 'base.php';
  11. /**
  12. * A Router is used to call a handler with corresponding to an URL.
  13. *
  14. * Simple example: a website with the pages 'home' and 'contact'.
  15. * <code>
  16. * function home() {
  17. * return 'This is the home page.';
  18. * }
  19. *
  20. * function contact() {
  21. * return 'This is the contact page.';
  22. * }
  23. *
  24. * $router = new Router(array(
  25. * '/home' => 'home',
  26. * '/contact' => 'contact'
  27. * ));
  28. * $response = $router->callHandler('/home'); // 'This is the home page.'
  29. * $response = $router->callHandler('/contact'); // 'This is the contact page.'
  30. * </code>
  31. *
  32. * You can use regular expression patterns to specify an URL. Any matches are
  33. * passed to the handler function in a single parameter:
  34. * <code>
  35. * function page(array $data) {
  36. * $pagename = $data[0];
  37. * return "This is the $pagename page.";
  38. * }
  39. *
  40. * $router = new Router(array(
  41. * '/(home|contact)' => 'page'
  42. * ));
  43. * $response = $router->callHandler('/home'); // 'This is the home page.'
  44. * $response = $router->callHandler('/contact'); // 'This is the contact page.'
  45. * </code>
  46. *
  47. * Instead of functions, you can implement the RouteHandler interface in a
  48. * handler class. The router will create an instance of the handler class and call *handleRequest*
  49. * <code>
  50. * class MyHandler implements RouteHandler {
  51. * function handleRequest(array $data) {
  52. * $pagename = $data[0];
  53. * return "This is the $pagename page.";
  54. * }
  55. * }
  56. *
  57. * $router = new Router(array(
  58. * '/(home|contact)' => 'MyHandler'
  59. * ));
  60. * $response = $router->callHandler('/home'); // 'This is the home page.'
  61. * $response = $router->callHandler('/contact'); // 'This is the contact page.'
  62. * </code>
  63. *
  64. * The WebBasics library provides a set of base handler classes implementing
  65. * the RouteHandler interface. These are sufficient for most usage cases.
  66. *
  67. * @package WebBasics
  68. */
  69. class Router extends Base {
  70. /**
  71. * The regex delimiter that is added to the begin and end of patterns.
  72. *
  73. * @var string
  74. */
  75. const DELIMITER = '%';
  76. /**
  77. * An associative array of regex patterns pointing to handler functions.
  78. *
  79. * @var array
  80. */
  81. private $routes = array();
  82. /**
  83. * Create a new Router instance.
  84. *
  85. * @param array $routes An initial list of routes to set.
  86. */
  87. function __construct(array $routes=array()) {
  88. foreach ($routes as $pattern => $handler)
  89. $this->addRoute($pattern, $handler);
  90. }
  91. /**
  92. * Add a route as a (pattern, handler) pair.
  93. *
  94. * The pattern is regular expression pattern without delimiters. The
  95. * function adds '%^' at the begin and '$%' at the end of the pattern as
  96. * delimiters.
  97. *
  98. * Any matches for groups in the pattern (which are contained in
  99. * parentheses), the matches for these are passed to the handler function.
  100. *
  101. * @param string $pattern A regex pattern to mach URL's against.
  102. * @param RouteHandler|callable $handler The handler function to call when $pattern is matched.
  103. * @throws \InvalidArgumentException If $handler is not callable.
  104. */
  105. function addRoute($pattern, $handler) {
  106. if (is_callable($handler)) {
  107. $handler = array('callable', $handler);
  108. } else if (!is_string($handler)) {
  109. throw new \InvalidArgumentException('Handler should be callable or class name.');
  110. } else if (!class_exists($handler)) {
  111. throw new \InvalidArgumentException(sprintf(
  112. 'Handler class "%s" does not exist.', $handler
  113. ));
  114. } else if (!in_array(__NAMESPACE__ . '\RouteHandler', class_implements($handler))) {
  115. throw new \InvalidArgumentException(sprintf(
  116. 'Handler class "%s" should implement the RouteHandler interface.', $handler
  117. ));
  118. } else {
  119. $handler = array('class', $handler);
  120. }
  121. $this->routes[self::DELIMITER . '^' . $pattern . '$' . self::DELIMITER] = $handler;
  122. }
  123. /**
  124. * Call the handler function corresponding to the specified url.
  125. *
  126. * If any groups are in the matched regex pattern, a list of matches is
  127. * passed to the handler function. If the handler function returns FALSE,
  128. * the url has not been 'handled' and the next pattern will be checked for
  129. * a match. Otherwise, the return value of the handler function is
  130. * returned as the result.
  131. *
  132. * @param string $url An url to match the saved patterns against.
  133. * @return mixed FALSE if no pattern was matched, the return value of the
  134. * corresponding handler function otherwise.
  135. */
  136. function callHandler($url) {
  137. foreach ($this->routes as $pattern => $tuple) {
  138. if (preg_match($pattern, $url, $matches)) {
  139. list($type, $handler) = $tuple;
  140. array_shift($matches);
  141. switch ($type) {
  142. case 'callable':
  143. $result = count($matches) ? $handler($matches) : $handler();
  144. break;
  145. case 'class':
  146. $instance = new $handler;
  147. $result = $instance->handleRequest($matches);
  148. }
  149. if ($result !== false)
  150. return $result;
  151. }
  152. }
  153. return false;
  154. }
  155. }
  156. /**
  157. * Interface for handler classes which can be bound to a router pattern.
  158. *
  159. * @package WebBasics
  160. */
  161. interface RouteHandler {
  162. /**
  163. * Handle an HTTP request.
  164. *
  165. * @param string[] $data A list of matched pattern groups, without the zero-group.
  166. * @return mixed FALSE if the handler function was not able to handle the
  167. * request, else the result of the reqeust.
  168. */
  169. function handleRequest(array $data);
  170. }
  171. ?>