1 <?php
2
3 4 5 6 7 8 9 10
11
12 namespace ICanBoogie;
13
14 use ICanBoogie\HTTP\RedirectResponse;
15 use ICanBoogie\HTTP\Request;
16 use ICanBoogie\HTTP\Response;
17 use ICanBoogie\Prototype\MethodNotDefined;
18 use ICanBoogie\Routing\Pattern;
19
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
41 class Routes implements \IteratorAggregate, \ArrayAccess
42 {
43 static protected $instance;
44
45 46 47 48 49
50 static public function get()
51 {
52 if (!self::$instance)
53 {
54 self::$instance = new static();
55 }
56
57
58
59 if (func_num_args() > 1)
60 {
61 return self::$instance->__call('get', func_get_args());
62 }
63
64 return self::$instance;
65 }
66
67 protected $routes = array();
68
69 protected $instances = array();
70
71 protected $default_route_class = 'ICanBoogie\Route';
72
73 74 75
76 protected function __construct()
77 {
78 $this->routes = $this->collect();
79 }
80
81 public function __call($method, array $arguments)
82 {
83 $method = strtoupper($method);
84
85 if ($method === Request::METHOD_ANY || in_array($method, Request::$methods))
86 {
87 list($pattern, $controller) = $arguments;
88
89 $definition = array
90 (
91 'pattern' => $pattern,
92 'via' => $method,
93 'controller' => $controller
94 );
95
96 $id = $method . ' ' . $pattern;
97 $this[$id] = $definition;
98
99 if ($method === Request::METHOD_GET)
100 {
101 $this[Request::METHOD_HEAD . ' ' . $pattern] = array_merge($definition, array('via' => Request::METHOD_HEAD));
102 }
103
104 return $this[$id];
105 }
106
107 throw new MethodNotDefined(array($method, $this));
108 }
109
110 public function getIterator()
111 {
112 return new \ArrayIterator($this->routes);
113 }
114
115 public function offsetExists($offset)
116 {
117 return isset($this->routes[$offset]);
118 }
119
120 public function offsetGet($id)
121 {
122 if (isset($this->instances[$id]))
123 {
124 return $this->instances[$id];
125 }
126
127 if (!$this->offsetExists($id))
128 {
129 throw new RouteNotDefined($id);
130 }
131
132 $properties = $this->routes[$id];
133
134 $class = $this->default_route_class;
135
136 if (isset($properties['class']))
137 {
138 $class = $properties['class'];
139 }
140
141 return $this->instances[$id] = new $class($properties['pattern'], $properties);
142 }
143
144 145 146 147 148 149 150 151 152 153
154 public function offsetSet($id, $route)
155 {
156 if (empty($route['pattern']))
157 {
158 throw new \LogicException(format
159 (
160 "Route %id has no pattern. !route", array
161 (
162 'id' => $id,
163 'route' => $route
164 )
165 ));
166 }
167
168 $this->routes[$id] = $route + array
169 (
170 'id' => $id,
171 'via' => Request::METHOD_ANY
172 );
173 }
174
175 static public function add($id, $definition)
176 {
177 $routes = static::get();
178 $routes[$id] = $definition;
179 }
180
181 182 183 184 185 186 187
188 public function offsetUnset($offset)
189 {
190 unset($this->routes[$offset]);
191 }
192
193 194 195 196 197 198 199 200 201 202 203 204 205
206 protected function collect()
207 {
208 global $core;
209
210
211
212 if (!isset($core))
213 {
214 return array();
215 }
216
217 $collection = $this;
218
219 return $core->configs->synthesize
220 (
221 'routes', function($fragments) use($collection)
222 {
223 global $core;
224
225 $module_roots = array();
226
227 foreach ($core->modules->descriptors as $module_id => $descriptor)
228 {
229 $module_roots[$descriptor[Module::T_PATH]] = $module_id;
230 }
231
232 foreach ($fragments as $module_root => &$fragment)
233 {
234 $module_root = dirname(dirname($module_root)) . DIRECTORY_SEPARATOR;
235 $module_id = isset($module_roots[$module_root]) ? $module_roots[$module_root] : null;
236
237 foreach ($fragment as $route_id => &$route)
238 {
239 $route += array
240 (
241 'via' => Request::METHOD_ANY,
242 'module' => $module_id
243 );
244 }
245 }
246
247 unset($fragment);
248 unset($route);
249
250 new Routes\BeforeCollectEvent($collection, array('fragments' => &$fragments));
251
252 $routes = array();
253
254 foreach ($fragments as $fragment)
255 {
256 foreach ($fragment as $id => $route)
257 {
258 $routes[$id] = $route + array
259 (
260 'pattern' => null
261 );
262 }
263 }
264
265 new Routes\CollectEvent($collection, array('routes' => &$routes));
266
267 return $routes;
268 }
269 );
270 }
271
272 273 274 275 276 277 278 279 280 281 282 283
284 public function find($uri, &$captured=null, $method=Request::METHOD_ANY, $namespace=null)
285 {
286 $captured = array();
287
288 if ($namespace)
289 {
290 $namespace = '/' . $namespace . '/';
291 }
292
293 $found = null;
294 $pattern = null;
295
296 $qs = null;
297 $qs_pos = strpos($uri, '?');
298
299 if ($qs_pos !== false)
300 {
301 $qs = substr($uri, $qs_pos + 1);
302 $uri = substr($uri, 0, $qs_pos);
303 }
304
305 foreach ($this->routes as $id => $route)
306 {
307 $pattern = $route['pattern'];
308
309 if ($namespace && strpos($pattern, $namespace) !== 0)
310 {
311 continue;
312 }
313
314 $pattern = Pattern::from($pattern);
315
316 if (!$pattern->match($uri, $captured))
317 {
318 continue;
319 }
320
321 if ($method == Request::METHOD_ANY)
322 {
323 $found = true;
324 break;
325 }
326
327 $route_method = $route['via'];
328
329 if (is_array($route_method))
330 {
331 if (in_array($method, $route_method))
332 {
333 $found = true;
334 break;
335 }
336 }
337 else
338 {
339 if ($route_method === Request::METHOD_ANY || $route_method === $method)
340 {
341 $found = true;
342 break;
343 }
344 }
345 }
346
347 if (!$found)
348 {
349 return;
350 }
351
352 if ($qs)
353 {
354 parse_str($qs, $parsed_query_string);
355
356 $captured['__query__'] = $parsed_query_string;
357 }
358
359 return new Route
360 (
361 $pattern, $route + array
362 (
363 'id' => $id
364 )
365 );
366 }
367 }
368
369 370 371
372
373 374 375 376 377
378 class RouteNotDefined extends \Exception
379 {
380 private $id;
381
382 383 384 385 386
387 public function __construct($id, $code=404, \Exception $previous=null)
388 {
389 $this->id = $id;
390
391 parent::__construct("The route <q>$id</q> is not defined.", $code, $previous);
392 }
393
394 public function __get($property)
395 {
396 if ($property == 'id')
397 {
398 return $this->id;
399 }
400
401 throw new PropertyNotDefined(array($property, $this));
402 }
403 }
404
405 406 407
408
409 namespace ICanBoogie\Routes;
410
411 412 413 414 415 416
417 class BeforeCollectEvent extends \ICanBoogie\Event
418 {
419 420 421 422 423
424 public $fragments;
425
426 427 428 429 430 431
432 public function __construct(\ICanBoogie\Routes $target, array $payload)
433 {
434 parent::__construct($target, 'collect:before', $payload);
435 }
436 }
437
438 439 440 441 442
443 class CollectEvent extends \ICanBoogie\Event
444 {
445 446 447 448 449
450 public $routes;
451
452 453 454 455 456 457
458 public function __construct(\ICanboogie\Routes $target, array $payload)
459 {
460 parent::__construct($target, 'collect', $payload);
461 }
462 }