1: <?php
2: 3: 4: 5: 6: 7:
8:
9: namespace Wei;
10:
11: 12: 13: 14: 15: 16: 17:
18: class Router extends Base
19: {
20: 21: 22: 23: 24:
25: protected $routes = array();
26:
27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39:
40: protected $routeOptions = array(
41: 'pattern' => null,
42: 'rules' => array(),
43: 'defaults' => array(),
44: 'method' => null,
45: 'regex' => null,
46: );
47:
48: 49: 50: 51: 52:
53: protected $namespaces = array('admin', 'api');
54:
55: 56: 57: 58: 59:
60: protected $scopes = array('user');
61:
62: 63: 64: 65: 66:
67: protected $combinedResources = array();
68:
69: 70: 71:
72: protected $defaultController = 'index';
73:
74: 75: 76:
77: protected $defaultAction = 'index';
78:
79: 80: 81: 82: 83:
84: protected $methodToAction = array(
85: 'GET-collection' => 'index',
86: 'GET' => 'show',
87: 'POST' => 'create',
88: 'PATCH' => 'update',
89: 'PUT' => 'update',
90: 'DELETE' => 'destroy'
91: );
92:
93: 94: 95: 96: 97:
98: protected $singularRules = array(
99: '/(o|x|ch|ss|sh)es$/i' => '\1',
100: '/([^aeiouy]|qu)ies$/i' => '\1y',
101: '/s$/i' => '',
102: );
103:
104: 105: 106: 107: 108:
109: protected $singulars = array(
110: 'aliases' => 'alias',
111: 'analyses' => 'analysis',
112: 'buses' => 'bus',
113: 'children' => 'child',
114: 'cookies' => 'cookie',
115: 'criteria' => 'criterion',
116: 'data' => 'datum',
117: 'lives' => 'life',
118: 'matrices' => 'matrix',
119: 'men' => 'man',
120: 'menus' => 'menu',
121: 'monies' => 'money',
122: 'news' => 'news',
123: 'people' => 'person',
124: 'quizzes' => 'quiz',
125: );
126:
127: 128: 129: 130: 131: 132:
133: public function setRoutes($routes)
134: {
135: foreach ($routes as $route) {
136: $this->set($route);
137: }
138: return $this;
139: }
140:
141: 142: 143: 144: 145:
146: public function getRoutes()
147: {
148: return $this->routes;
149: }
150:
151: 152: 153: 154: 155: 156: 157:
158: public function set($route)
159: {
160: if (is_string($route)) {
161: $this->routes[] = array(
162: 'pattern' => $route
163: ) + $this->routeOptions;
164: } elseif (is_array($route)) {
165: $this->routes[] = $route + $this->routeOptions;
166: } else {
167: throw new \InvalidArgumentException(sprintf(
168: 'Expected argument of type string or array, "%s" given',
169: is_object($route) ? get_class($route) : gettype($route)
170: ));
171: }
172:
173: return $this;
174: }
175:
176: 177: 178: 179: 180: 181:
182: public function getRoute($id)
183: {
184: return isset($this->routes[$id]) ? $this->routes[$id] : false;
185: }
186:
187: 188: 189: 190: 191: 192:
193: public function remove($id)
194: {
195: unset($this->routes[$id]);
196: return $this;
197: }
198:
199: 200: 201: 202: 203: 204:
205: protected function compile(&$route)
206: {
207: if ($route['regex']) {
208: return $route;
209: }
210:
211: $regex = preg_replace('#[.\+*?[^\]${}=!|:-]#', '\\\\$0', $route['pattern']);
212:
213: $regex = str_replace(array('(', ')'), array('(?:', ')?'), $regex);
214:
215: $regex = str_replace(array('<', '>'), array('(?P<', '>.+?)'), $regex);
216:
217: if ($route['rules']) {
218: $search = $replace = array();
219: foreach ($route['rules'] as $key => $rule) {
220: $search[] = '<' . $key . '>.+?';
221: $replace[] = '<' . $key . '>' . $rule;
222: }
223: $regex = str_replace($search, $replace, $regex);
224: }
225:
226: $route['regex'] = '#^' . $regex . '$#uUD';
227:
228: return $route;
229: }
230:
231: 232: 233: 234: 235: 236: 237: 238:
239: public function match($pathInfo, $method = null)
240: {
241: $pathInfo = rtrim($pathInfo, '/');
242: !$pathInfo && $pathInfo = '/';
243: foreach ($this->routes as $id => $route) {
244: if (false !== ($parameters = $this->matchRoute($pathInfo, $method, $id))) {
245: return $parameters;
246: }
247: }
248: return false;
249: }
250:
251: 252: 253: 254: 255: 256: 257:
258: public function matchParamSet($pathInfo, $method = 'GET')
259: {
260: $params = $this->match($pathInfo, $method);
261: $restParams = $this->matchRestParamSet($pathInfo, $method);
262: return $params ? array_merge(array($params), $restParams) : $restParams;
263: }
264:
265: 266: 267: 268: 269: 270: 271: 272: 273:
274: protected function matchRoute($pathInfo, $method, $id)
275: {
276: $route = $this->compile($this->routes[$id]);
277:
278:
279: if ($method && $route['method'] && !preg_match('#' . $route['method'] . '#i', $method)) {
280: return false;
281: }
282:
283:
284: if (!preg_match($route['regex'], $pathInfo, $matches)) {
285: return false;
286: }
287:
288:
289: $parameters = array();
290: foreach ($matches as $key => $parameter) {
291: if (is_int($key)) {
292: continue;
293: }
294: $parameters[$key] = $parameter;
295: }
296:
297: $parameters += $route['defaults'];
298:
299: preg_match_all('#<([a-zA-Z0-9_]++)>#', $route['pattern'], $matches);
300: foreach ($matches[1] as $key) {
301: if (!array_key_exists($key, $parameters)) {
302: $parameters[$key] = null;
303: }
304: }
305:
306: return array('_id' => $id) + $parameters;
307: }
308:
309: 310: 311: 312: 313: 314: 315:
316: public function matchRestParamSet($path, $method = 'GET')
317: {
318: $rootCtrl = '';
319: $params = array();
320: $routes = array();
321:
322:
323: if (strpos($path, '.') !== false) {
324: $params['_format'] = pathinfo($path, PATHINFO_EXTENSION);
325: $path = substr($path, 0, - strlen($params['_format']) - 1);
326: }
327:
328: $parts = $this->parsePath($path);
329:
330:
331: if (count($parts) > 1 && in_array($parts[0], $this->namespaces)) {
332: $rootCtrl .= array_shift($parts) . '/';
333: }
334:
335:
336: if (count($parts) > 1 && in_array($parts[0], $this->scopes)) {
337: $rootCtrl .= array_shift($parts) . '/';
338: }
339:
340: $baseCtrl = $rootCtrl;
341:
342:
343:
344:
345: $lastParts = array_pad(array_splice($parts, count($parts) % 2 == 0 ? -2 : -3), 3, null);
346: list($ctrl, $actOrId, $ctrlOrAct) = $lastParts;
347:
348:
349: $count = count($parts);
350: for ($i = 0; $i < $count; $i += 2) {
351: $baseCtrl .= $parts[$i] . '/';
352: $params[$this->singularize($parts[$i]) . 'Id'] = $parts[$i + 1];
353: }
354:
355: if (is_null($actOrId)) {
356:
357: $routes[] = array(
358: 'controller' => $baseCtrl . ($ctrl ?: $this->defaultController),
359: 'action' => $this->methodToAction($method, true),
360: );
361: } elseif (is_null($ctrlOrAct)) {
362:
363: if ($this->isAction($actOrId)) {
364: $routes[] = array(
365: 'controller' => $baseCtrl . $ctrl,
366: 'action' => $actOrId,
367: );
368: }
369:
370:
371: $routes[] = array(
372: 'controller' => $baseCtrl . $ctrl,
373: 'action' => $this->methodToAction($method),
374: 'id' => $actOrId
375: );
376:
377:
378: if ($count >= 2 && $this->isAction($actOrId)) {
379: $routes[] = array(
380: 'controller' => $rootCtrl . $ctrl,
381: 'action' => $actOrId,
382: );
383: }
384: } else {
385:
386: $routes[] = array(
387: 'controller' => $baseCtrl . $ctrl,
388: 'action' => $ctrlOrAct,
389: 'id' => $actOrId
390: );
391:
392:
393: $routes[] = array(
394: 'controller' => $baseCtrl . $ctrl . '/' . $ctrlOrAct,
395: 'action' => $this->methodToAction($method, true),
396: $this->singularize($ctrl) . 'Id' => $actOrId,
397: );
398:
399:
400: $routes[] = array('controller' => $ctrlOrAct) + $routes[1];
401: }
402:
403: foreach ($routes as &$route) {
404: $route['controller'] = $this->camelize($route['controller']);
405: $route['action'] = $this->camelize($route['action']);
406: $route += $params;
407: }
408: return $routes;
409: }
410:
411: 412: 413: 414: 415: 416:
417: protected function parsePath($path)
418: {
419: $path = ltrim($path, '/');
420: foreach ($this->combinedResources as $resource) {
421: if (strpos($path, $resource) !== false) {
422: list($part1, $part2) = explode($resource, $path, 2);
423: $parts = array_merge(explode('/', $part1), array($resource), explode('/', $part2));
424: return array_values(array_filter($parts));
425: }
426: }
427: return explode('/', $path);
428: }
429:
430: 431: 432: 433: 434: 435:
436: public function setCombinedResources(array $combinedResources)
437: {
438: $this->combinedResources += $combinedResources;
439: return $this;
440: }
441:
442: 443: 444: 445: 446: 447:
448: public function setSingulars(array $singulars)
449: {
450: $this->singulars += $this->singulars;
451: return $this;
452: }
453:
454: 455: 456: 457: 458: 459: 460: 461: 462:
463: protected function singularize($word)
464: {
465: if (isset($this->singulars[$word])) {
466: return $this->singulars[$word];
467: }
468:
469: foreach ($this->singularRules as $rule => $replacement) {
470: if (preg_match($rule, $word)) {
471: $this->singulars[$word] = preg_replace($rule, $replacement, $word);
472: return $this->singulars[$word];
473: }
474: }
475:
476: $this->singulars[$word] = $word;
477: return $word;
478: }
479:
480: 481: 482: 483: 484: 485: 486:
487: protected function camelize($word)
488: {
489: return lcfirst(str_replace(' ', '', ucwords(strtr($word, '_-', ' '))));
490: }
491:
492: 493: 494: 495: 496: 497: 498:
499: protected function methodToAction($method, $collection = false)
500: {
501: if ($method == 'GET' && $collection) {
502: $method = $method . '-collection';
503: }
504: return isset($this->methodToAction[$method]) ? $this->methodToAction[$method] : $this->defaultAction;
505: }
506:
507: 508: 509: 510:
511: protected function isAction($action)
512: {
513: return $action && !is_numeric($action[0]);
514: }
515: }