a tiny mvc framework for php using php-activerecord
at v1 223 lines 6.7 kB view raw
1<?php 2/* 3 URL router 4*/ 5 6namespace HalfMoon; 7 8use Closure; 9 10class Router extends Singleton { 11 private $routes = array(); 12 private $rootRoutes = array(); 13 14 static $DEFAULT_ACTION = "index"; 15 16 public static function initialize(Closure $initializer) { 17 $initializer(parent::instance()); 18 } 19 20 public static function addRoute($route) { 21 if (!is_array($route)) 22 throw new HalfMoonException("invalid route of " 23 . var_export($route)); 24 25 array_push(parent::instance()->routes, $route); 26 } 27 28 public static function addRootRoute($route) { 29 if (!is_array($route)) 30 throw new HalfMoonException("invalid root route of " 31 . var_export($route, true)); 32 33 /* only one root route can match a particular condition */ 34 foreach (parent::instance()->rootRoutes as $rr) 35 if (!array_diff_assoc((array)$rr, (array)$route["conditions"])) 36 throw new HalfMoonException("cannot add second root route " 37 . "with no conditions: " . var_export($route, true)); 38 39 array_push(parent::instance()->rootRoutes, $route); 40 } 41 42 public static function clearRoutes() { 43 parent::instance()->routes = array(); 44 parent::instance()->rootRoutes = array(); 45 } 46 47 public static function getRoutes() { 48 return parent::instance()->routes; 49 } 50 51 public static function getRootRoutes() { 52 return parent::instance()->rootRoutes; 53 } 54 55 public static function routeRequest($request) { 56 $path_pieces = array_map(function($piece) { 57 return urldecode($piece); 58 }, explode("/", $request->path)); 59 60 $chosen_route = null; 61 62 /* find and take the first matching route, storing route components in 63 * $params */ 64 if ($request->path == "") { 65 if (empty(parent::instance()->rootRoutes)) 66 throw new RoutingException("no root route defined"); 67 68 foreach (parent::instance()->rootRoutes as $route) { 69 /* verify virtual host matches if there's a condition on it */ 70 if (isset($route["conditions"]["hostname"])) 71 if (!Utils::strcasecmp_or_preg_match( 72 $route["conditions"]["hostname"], $request->host)) 73 continue; 74 75 $chosen_route = $route; 76 break; 77 } 78 } else { 79 foreach (parent::instance()->routes as $route) { 80 /* verify virtual host matches if there's a condition on it */ 81 if (isset($route["conditions"]["hostname"])) 82 if (!Utils::strcasecmp_or_preg_match( 83 $route["conditions"]["hostname"], $request->host)) 84 continue; 85 86 /* trim slashes from route definition and bust it up into 87 * components */ 88 $route_pieces = explode("/", trim(preg_replace("/^\/*/", "", 89 preg_replace("/\/$/", "", trim($route["url"]))))); 90 91 $match = true; 92 for ($x = 0; $x < count($route_pieces); $x++) { 93 /* look for a condition */ 94 if (preg_match("/^:(.+)$/", $route_pieces[$x], $m)) { 95 $regex_or_string = isset($route["conditions"]) ? 96 @Utils::A((array)$route["conditions"], $m[1]) : 97 NULL; 98 99 /* if the corresponding path piece isn't there and 100 * there is either no condition, or a condition that 101 * matches against a blank string, it's ok. this lets 102 * controller/:action/:id match when the route is just 103 * "controller", assigning :action and :id to nothing */ 104 105 if ($regex_or_string == NULL || 106 Utils::strcasecmp_or_preg_match($regex_or_string, 107 Utils::is_blank($path_pieces[$x]) ? "" : 108 $path_pieces[$x])) { 109 if (isset($route[$m[1]]) && 110 preg_match("/^(.*):(.+)$/", $route[$m[1]], $n)) 111 /* route has a set parameter, but it wants to 112 * include the matching piece from the path in 113 * its parameter */ 114 $route[$m[1]] = $n[1] . 115 (isset($path_pieces[$x]) ? 116 $path_pieces[$x] : 117 static::$DEFAULT_ACTION); 118 else 119 /* store this named parameter (e.g. "/:blah" 120 * route on a path of "/hi" defines 121 * $route["blah"] to be "hi") */ 122 $route[$m[1]] = @$path_pieces[$x]; 123 } else 124 $match = false; 125 } 126 127 /* look for a glob condition */ 128 elseif (preg_match("/^\*(.+)$/", $route_pieces[$x], $m)) { 129 $regex_or_string = isset($route["conditions"]) ? 130 @Utils::A((array)$route["conditions"], $m[1]) : 131 NULL; 132 133 /* concatenate the rest of the path as this one param */ 134 $u = ""; 135 for ($j = $x; $j < count($path_pieces); $j++) 136 $u .= ($u == "" ? "" : "/") . $path_pieces[$j]; 137 138 if ($regex_or_string == NULL || 139 Utils::strcasecmp_or_preg_match($regex_or_string, $u)) 140 $route[$m[1]] = $u; 141 else 142 $match = false; 143 144 break; 145 } 146 147 /* else it must match exactly (case-insensitively) */ 148 elseif (@strcasecmp($route_pieces[$x], $path_pieces[$x]) 149 != 0) 150 $match = false; 151 152 if (!$match) 153 break; 154 } 155 156 if ($match) { 157 /* we need at least a valid controller */ 158 if ($route["controller"] == "") 159 continue; 160 161 /* note that we pass the action to the controller even if it 162 * doesn't exist, that way at least the backtrace will show 163 * what controller we resolved it to */ 164 165 $chosen_route = $route; 166 break; 167 } 168 } 169 } 170 171 if (!$chosen_route) 172 throw new RoutingException("no route for url \"" . $request->path 173 . "\""); 174 175 /* we need at least a controller */ 176 if ($chosen_route["controller"] == "") 177 throw new RoutingException("no controller specified"); 178 179 /* and a valid controller name */ 180 if (!preg_match("/^[a-zA-Z_][a-zA-Z0-9_]*$/", 181 $chosen_route["controller"])) 182 throw new RoutingException("invalid controller matched"); 183 184 /* but we can deal with no action by calling the index action */ 185 if (!isset($chosen_route["action"]) || $chosen_route["action"] == "") 186 $chosen_route["action"] = static::$DEFAULT_ACTION; 187 188 return $chosen_route; 189 } 190 191 public static function takeRouteForRequest($request) { 192 $route = parent::instance()->routeRequest($request); 193 return static::takeRoute($route, $request); 194 } 195 196 public static function takeRoute($route, $request) { 197 /* store the parameters named in the route with data from the url, 198 * overriding anything passed by the user as get/post */ 199 foreach ($route as $k => $v) 200 $request->params[$k] = $v; 201 202 /* camel_case -> CamelCaseController */ 203 $c = str_replace(" ", "", 204 ucwords(str_replace("_", " ", $route["controller"]))) 205 . "Controller"; 206 207 if (!class_exists($c)) 208 throw new RoutingException("controller " . $c . " does not exist"); 209 210 /* log some basic information */ 211 if (Config::log_level_at_least("full")) 212 Log::info("Processing " . $c . "::" . $route["action"] . " (for " 213 . $request->remote_ip() . ") [" . $request->request_method() 214 . "]"); 215 216 $request->start_times["app"] = microtime(true); 217 218 $controller = new $c($request); 219 $controller->render_action($route["action"]); 220 } 221} 222 223?>