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