a tiny mvc framework for php using php-activerecord
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?>