1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11:
12:
13:
14:
15: 16: 17: 18: 19: 20: 21:
22: class NRoute extends NObject implements IRouter
23: {
24: const PRESENTER_KEY = 'presenter';
25: const MODULE_KEY = 'module';
26:
27:
28: const CASE_SENSITIVE = 256;
29: const FULL_META = 128;
30:
31:
32: const HOST = 1;
33: const PATH = 2;
34: const RELATIVE = 3;
35:
36:
37:
38: const VALUE = 'value';
39: const PATTERN = 'pattern';
40: const FILTER_IN = 'filterIn';
41: const FILTER_OUT = 'filterOut';
42: const FILTER_TABLE = 'filterTable';
43:
44:
45:
46: const OPTIONAL = 0;
47: const PATH_OPTIONAL = 1;
48: const CONSTANT = 2;
49:
50:
51:
52: public static $defaultFlags = 0;
53:
54:
55: public static $styles = array(
56: '#' => array(
57: self::PATTERN => '[^/]+',
58: self::FILTER_IN => 'rawurldecode',
59: self::FILTER_OUT => 'rawurlencode',
60: ),
61: '?#' => array(
62: ),
63: 'module' => array(
64: self::PATTERN => '[a-z][a-z0-9.-]*',
65: self::FILTER_IN => array(__CLASS__, 'path2presenter'),
66: self::FILTER_OUT => array(__CLASS__, 'presenter2path'),
67: ),
68: 'presenter' => array(
69: self::PATTERN => '[a-z][a-z0-9.-]*',
70: self::FILTER_IN => array(__CLASS__, 'path2presenter'),
71: self::FILTER_OUT => array(__CLASS__, 'presenter2path'),
72: ),
73: 'action' => array(
74: self::PATTERN => '[a-z][a-z0-9-]*',
75: self::FILTER_IN => array(__CLASS__, 'path2action'),
76: self::FILTER_OUT => array(__CLASS__, 'action2path'),
77: ),
78: '?module' => array(
79: ),
80: '?presenter' => array(
81: ),
82: '?action' => array(
83: ),
84: );
85:
86:
87: private $mask;
88:
89:
90: private $sequence;
91:
92:
93: private $re;
94:
95:
96: private $metadata = array();
97:
98:
99: private $xlat;
100:
101:
102: private $type;
103:
104:
105: private $flags;
106:
107:
108:
109: 110: 111: 112: 113:
114: public function __construct($mask, array $metadata = array(), $flags = 0)
115: {
116: $this->flags = $flags | self::$defaultFlags;
117: if (!($this->flags & self::FULL_META)) {
118: foreach ($metadata as $name => $def) {
119: $metadata[$name] = is_array($def) ? $def : array(self::VALUE => $def);
120: }
121: }
122: $this->setMask($mask, $metadata);
123: }
124:
125:
126:
127: 128: 129: 130: 131:
132: public function match(IHttpRequest $httpRequest)
133: {
134:
135:
136:
137: $uri = $httpRequest->getUri();
138:
139: if ($this->type === self::HOST) {
140: $path = '//' . $uri->getHost() . $uri->getPath();
141:
142: } elseif ($this->type === self::RELATIVE) {
143: $basePath = $uri->getBasePath();
144: if (strncmp($uri->getPath(), $basePath, strlen($basePath)) !== 0) {
145: return NULL;
146: }
147: $path = (string) substr($uri->getPath(), strlen($basePath));
148:
149: } else {
150: $path = $uri->getPath();
151: }
152:
153: if ($path !== '') {
154: $path = rtrim($path, '/') . '/';
155: }
156:
157: if (!preg_match($this->re, $path, $matches)) {
158:
159: return NULL;
160: }
161:
162:
163: $params = array();
164: foreach ($matches as $k => $v) {
165: if (is_string($k) && $v !== '') {
166: $params[str_replace('___', '-', $k)] = $v;
167: }
168: }
169:
170:
171:
172: foreach ($this->metadata as $name => $meta) {
173: if (isset($params[$name])) {
174:
175:
176: } elseif (isset($meta['fixity']) && $meta['fixity'] !== self::OPTIONAL) {
177: $params[$name] = NULL;
178: }
179: }
180:
181:
182:
183: if ($this->xlat) {
184: $params += self::renameKeys($httpRequest->getQuery(), array_flip($this->xlat));
185: } else {
186: $params += $httpRequest->getQuery();
187: }
188:
189:
190:
191: foreach ($this->metadata as $name => $meta) {
192: if (isset($params[$name])) {
193: if (!is_scalar($params[$name])) {
194:
195: } elseif (isset($meta[self::FILTER_TABLE][$params[$name]])) {
196: $params[$name] = $meta[self::FILTER_TABLE][$params[$name]];
197:
198: } elseif (isset($meta[self::FILTER_IN])) {
199: $params[$name] = call_user_func($meta[self::FILTER_IN], (string) $params[$name]);
200: if ($params[$name] === NULL && !isset($meta['fixity'])) {
201: return NULL;
202: }
203: }
204:
205: } elseif (isset($meta['fixity'])) {
206: $params[$name] = $meta[self::VALUE];
207: }
208: }
209:
210:
211:
212: if (!isset($params[self::PRESENTER_KEY])) {
213: throw new InvalidStateException('Missing presenter in route definition.');
214: }
215: if (isset($this->metadata[self::MODULE_KEY])) {
216: if (!isset($params[self::MODULE_KEY])) {
217: throw new InvalidStateException('Missing module in route definition.');
218: }
219: $presenter = $params[self::MODULE_KEY] . ':' . $params[self::PRESENTER_KEY];
220: unset($params[self::MODULE_KEY], $params[self::PRESENTER_KEY]);
221:
222: } else {
223: $presenter = $params[self::PRESENTER_KEY];
224: unset($params[self::PRESENTER_KEY]);
225: }
226:
227: return new NPresenterRequest(
228: $presenter,
229: $httpRequest->getMethod(),
230: $params,
231: $httpRequest->getPost(),
232: $httpRequest->getFiles(),
233: array(NPresenterRequest::SECURED => $httpRequest->isSecured())
234: );
235: }
236:
237:
238:
239: 240: 241: 242: 243: 244:
245: public function constructUrl(NPresenterRequest $appRequest, IHttpRequest $httpRequest)
246: {
247: if ($this->flags & self::ONE_WAY) {
248: return NULL;
249: }
250:
251: $params = $appRequest->getParams();
252: $metadata = $this->metadata;
253:
254: $presenter = $appRequest->getPresenterName();
255: $params[self::PRESENTER_KEY] = $presenter;
256:
257: if (isset($metadata[self::MODULE_KEY])) {
258: $module = $metadata[self::MODULE_KEY];
259: if (isset($module['fixity']) && strncasecmp($presenter, $module[self::VALUE] . ':', strlen($module[self::VALUE]) + 1) === 0) {
260: $a = strlen($module[self::VALUE]);
261: } else {
262: $a = strrpos($presenter, ':');
263: }
264: if ($a === FALSE) {
265: $params[self::MODULE_KEY] = '';
266: } else {
267: $params[self::MODULE_KEY] = substr($presenter, 0, $a);
268: $params[self::PRESENTER_KEY] = substr($presenter, $a + 1);
269: }
270: }
271:
272: foreach ($metadata as $name => $meta) {
273: if (!isset($params[$name])) continue;
274:
275: if (isset($meta['fixity'])) {
276: if (is_scalar($params[$name]) && strcasecmp($params[$name], $meta[self::VALUE]) === 0) {
277:
278: unset($params[$name]);
279: continue;
280:
281: } elseif ($meta['fixity'] === self::CONSTANT) {
282: return NULL;
283: }
284: }
285:
286: if (!is_scalar($params[$name])) {
287:
288: } elseif (isset($meta['filterTable2'][$params[$name]])) {
289: $params[$name] = $meta['filterTable2'][$params[$name]];
290:
291: } elseif (isset($meta[self::FILTER_OUT])) {
292: $params[$name] = call_user_func($meta[self::FILTER_OUT], $params[$name]);
293: }
294:
295: if (isset($meta[self::PATTERN]) && !preg_match($meta[self::PATTERN], rawurldecode($params[$name]))) {
296: return NULL;
297: }
298: }
299:
300:
301: $sequence = $this->sequence;
302: $brackets = array();
303: $required = 0;
304: $uri = '';
305: $i = count($sequence) - 1;
306: do {
307: $uri = $sequence[$i] . $uri;
308: if ($i === 0) break;
309: $i--;
310:
311: $name = $sequence[$i]; $i--;
312:
313: if ($name === ']') {
314: $brackets[] = $uri;
315:
316: } elseif ($name[0] === '[') {
317: $tmp = array_pop($brackets);
318: if ($required < count($brackets) + 1) {
319: if ($name !== '[!') {
320: $uri = $tmp;
321: }
322: } else {
323: $required = count($brackets);
324: }
325:
326: } elseif ($name[0] === '?') {
327: continue;
328:
329: } elseif (isset($params[$name]) && $params[$name] != '') {
330: $required = count($brackets);
331: $uri = $params[$name] . $uri;
332: unset($params[$name]);
333:
334: } elseif (isset($metadata[$name]['fixity'])) {
335: $uri = $metadata[$name]['defOut'] . $uri;
336:
337: } else {
338: return NULL;
339: }
340: } while (TRUE);
341:
342:
343:
344: if ($this->xlat) {
345: $params = self::renameKeys($params, $this->xlat);
346: }
347:
348: $sep = ini_get('arg_separator.input');
349: $query = http_build_query($params, '', $sep ? $sep[0] : '&');
350: if ($query != '') $uri .= '?' . $query;
351:
352:
353: if ($this->type === self::RELATIVE) {
354: $uri = '//' . $httpRequest->getUri()->getAuthority() . $httpRequest->getUri()->getBasePath() . $uri;
355:
356: } elseif ($this->type === self::PATH) {
357: $uri = '//' . $httpRequest->getUri()->getAuthority() . $uri;
358: }
359:
360: if (strpos($uri, '//', 2) !== FALSE) {
361: return NULL;
362: }
363:
364: $uri = ($this->flags & self::SECURED ? 'https:' : 'http:') . $uri;
365:
366: return $uri;
367: }
368:
369:
370:
371: 372: 373: 374: 375: 376:
377: private function setMask($mask, array $metadata)
378: {
379: $this->mask = $mask;
380:
381:
382: if (substr($mask, 0, 2) === '//') {
383: $this->type = self::HOST;
384:
385: } elseif (substr($mask, 0, 1) === '/') {
386: $this->type = self::PATH;
387:
388: } else {
389: $this->type = self::RELATIVE;
390: }
391:
392: foreach ($metadata as $name => $meta) {
393: if (array_key_exists(self::VALUE, $meta)) {
394: $metadata[$name]['fixity'] = self::CONSTANT;
395: }
396: }
397:
398:
399: $parts = preg_split(
400: '/<([^># ]+) *([^>#]*)(#?[^>\[\]]*)>|(\[!?|\]|\s*\?.*)/',
401: $mask,
402: -1,
403: PREG_SPLIT_DELIM_CAPTURE
404: );
405:
406: $this->xlat = array();
407: $i = count($parts) - 1;
408:
409:
410: if (isset($parts[$i - 1]) && substr(ltrim($parts[$i - 1]), 0, 1) === '?') {
411: preg_match_all(
412: '/(?:([a-zA-Z0-9_.-]+)=)?<([^># ]+) *([^>#]*)(#?[^>]*)>/',
413: $parts[$i - 1],
414: $matches,
415: PREG_SET_ORDER
416: );
417: foreach ($matches as $match) {
418: list(, $param, $name, $pattern, $class) = $match;
419:
420: if ($class !== '') {
421: if (!isset(self::$styles[$class])) {
422: throw new InvalidStateException("Parameter '$name' has '$class' flag, but Route::\$styles['$class'] is not set.");
423: }
424: $meta = self::$styles[$class];
425:
426: } elseif (isset(self::$styles['?' . $name])) {
427: $meta = self::$styles['?' . $name];
428:
429: } else {
430: $meta = self::$styles['?#'];
431: }
432:
433: if (isset($metadata[$name])) {
434: $meta = $metadata[$name] + $meta;
435: }
436:
437: if (array_key_exists(self::VALUE, $meta)) {
438: $meta['fixity'] = self::OPTIONAL;
439: }
440:
441: unset($meta['pattern']);
442: $meta['filterTable2'] = empty($meta[self::FILTER_TABLE]) ? NULL : array_flip($meta[self::FILTER_TABLE]);
443:
444: $metadata[$name] = $meta;
445: if ($param !== '') {
446: $this->xlat[$name] = $param;
447: }
448: }
449: $i -= 5;
450: }
451:
452: $brackets = 0;
453: $re = '';
454: $sequence = array();
455: $autoOptional = array(0, 0);
456: do {
457: array_unshift($sequence, $parts[$i]);
458: if (strpos($parts[$i], '{') !== FALSE) {
459: throw new DeprecatedException('Optional parts delimited using {...} are deprecated; use [...] instead.');
460: }
461: $re = preg_quote($parts[$i], '#') . $re;
462: if ($i === 0) break;
463: $i--;
464:
465: $part = $parts[$i];
466: if ($part === '[' || $part === ']' || $part === '[!') {
467: $brackets += $part[0] === '[' ? -1 : 1;
468: if ($brackets < 0) {
469: throw new InvalidArgumentException("Unexpected '$part' in mask '$mask'.");
470: }
471: array_unshift($sequence, $part);
472: $re = ($part[0] === '[' ? '(?:' : ')?') . $re;
473: $i -= 4;
474: continue;
475: }
476:
477: $class = $parts[$i]; $i--;
478: $pattern = trim($parts[$i]); $i--;
479: $name = $parts[$i]; $i--;
480: array_unshift($sequence, $name);
481:
482: if ($name[0] === '?') {
483: $re = '(?:' . preg_quote(substr($name, 1), '#') . '|' . $pattern . ')' . $re;
484: $sequence[1] = substr($name, 1) . $sequence[1];
485: continue;
486: }
487:
488:
489: if (preg_match('#[^a-z0-9_-]#i', $name)) {
490: throw new InvalidArgumentException("Parameter name must be alphanumeric string due to limitations of PCRE, '$name' given.");
491: }
492:
493:
494: if ($class !== '') {
495: if (!isset(self::$styles[$class])) {
496: throw new InvalidStateException("Parameter '$name' has '$class' flag, but Route::\$styles['$class'] is not set.");
497: }
498: $meta = self::$styles[$class];
499:
500: } elseif (isset(self::$styles[$name])) {
501: $meta = self::$styles[$name];
502:
503: } else {
504: $meta = self::$styles['#'];
505: }
506:
507: if (isset($metadata[$name])) {
508: $meta = $metadata[$name] + $meta;
509: }
510:
511: if ($pattern == '' && isset($meta[self::PATTERN])) {
512: $pattern = $meta[self::PATTERN];
513: }
514:
515: $meta['filterTable2'] = empty($meta[self::FILTER_TABLE]) ? NULL : array_flip($meta[self::FILTER_TABLE]);
516: if (array_key_exists(self::VALUE, $meta)) {
517: if (isset($meta['filterTable2'][$meta[self::VALUE]])) {
518: $meta['defOut'] = $meta['filterTable2'][$meta[self::VALUE]];
519:
520: } elseif (isset($meta[self::FILTER_OUT])) {
521: $meta['defOut'] = call_user_func($meta[self::FILTER_OUT], $meta[self::VALUE]);
522:
523: } else {
524: $meta['defOut'] = $meta[self::VALUE];
525: }
526: }
527: $meta[self::PATTERN] = "#(?:$pattern)$#A" . ($this->flags & self::CASE_SENSITIVE ? '' : 'iu');
528:
529:
530: $re = '(?P<' . str_replace('-', '___', $name) . '>' . $pattern . ')' . $re;
531: if ($brackets) {
532: if (!isset($meta[self::VALUE])) {
533: $meta[self::VALUE] = $meta['defOut'] = NULL;
534: }
535: $meta['fixity'] = self::PATH_OPTIONAL;
536:
537: } elseif (isset($meta['fixity'])) {
538: $re = '(?:' . substr_replace($re, ')?', strlen($re) - $autoOptional[0], 0);
539: array_splice($sequence, count($sequence) - $autoOptional[1], 0, array(']', ''));
540: array_unshift($sequence, '[', '');
541: $meta['fixity'] = self::PATH_OPTIONAL;
542:
543: } else {
544: $autoOptional = array(strlen($re), count($sequence));
545: }
546:
547: $metadata[$name] = $meta;
548: } while (TRUE);
549:
550: if ($brackets) {
551: throw new InvalidArgumentException("Missing closing ']' in mask '$mask'.");
552: }
553:
554: $this->re = '#' . $re . '/?$#A' . ($this->flags & self::CASE_SENSITIVE ? '' : 'iu');
555: $this->metadata = $metadata;
556: $this->sequence = $sequence;
557: }
558:
559:
560:
561: 562: 563: 564:
565: public function getMask()
566: {
567: return $this->mask;
568: }
569:
570:
571:
572: 573: 574: 575:
576: public function getDefaults()
577: {
578: $defaults = array();
579: foreach ($this->metadata as $name => $meta) {
580: if (isset($meta['fixity'])) {
581: $defaults[$name] = $meta[self::VALUE];
582: }
583: }
584: return $defaults;
585: }
586:
587:
588:
589:
590:
591:
592:
593: 594: 595: 596:
597: public function getTargetPresenter()
598: {
599: if ($this->flags & self::ONE_WAY) {
600: return FALSE;
601: }
602:
603: $m = $this->metadata;
604: $module = '';
605:
606: if (isset($m[self::MODULE_KEY])) {
607: if (isset($m[self::MODULE_KEY]['fixity']) && $m[self::MODULE_KEY]['fixity'] === self::CONSTANT) {
608: $module = $m[self::MODULE_KEY][self::VALUE] . ':';
609: } else {
610: return NULL;
611: }
612: }
613:
614: if (isset($m[self::PRESENTER_KEY]['fixity']) && $m[self::PRESENTER_KEY]['fixity'] === self::CONSTANT) {
615: return $module . $m[self::PRESENTER_KEY][self::VALUE];
616: }
617: return NULL;
618: }
619:
620:
621:
622: 623: 624: 625: 626: 627:
628: private static function renameKeys($arr, $xlat)
629: {
630: if (empty($xlat)) return $arr;
631:
632: $res = array();
633: $occupied = array_flip($xlat);
634: foreach ($arr as $k => $v) {
635: if (isset($xlat[$k])) {
636: $res[$xlat[$k]] = $v;
637:
638: } elseif (!isset($occupied[$k])) {
639: $res[$k] = $v;
640: }
641: }
642: return $res;
643: }
644:
645:
646:
647:
648:
649:
650:
651: 652: 653: 654: 655:
656: private static function action2path($s)
657: {
658: $s = preg_replace('#(.)(?=[A-Z])#', '$1-', $s);
659: $s = strtolower($s);
660: $s = rawurlencode($s);
661: return $s;
662: }
663:
664:
665:
666: 667: 668: 669: 670:
671: private static function path2action($s)
672: {
673: $s = strtolower($s);
674: $s = preg_replace('#-(?=[a-z])#', ' ', $s);
675: $s = substr(ucwords('x' . $s), 1);
676:
677: $s = str_replace(' ', '', $s);
678: return $s;
679: }
680:
681:
682:
683: 684: 685: 686: 687:
688: private static function presenter2path($s)
689: {
690: $s = strtr($s, ':', '.');
691: $s = preg_replace('#([^.])(?=[A-Z])#', '$1-', $s);
692: $s = strtolower($s);
693: $s = rawurlencode($s);
694: return $s;
695: }
696:
697:
698:
699: 700: 701: 702: 703:
704: private static function path2presenter($s)
705: {
706: $s = strtolower($s);
707: $s = preg_replace('#([.-])(?=[a-z])#', '$1 ', $s);
708: $s = ucwords($s);
709: $s = str_replace('. ', ':', $s);
710: $s = str_replace('- ', '', $s);
711: return $s;
712: }
713:
714:
715:
716:
717:
718:
719:
720: 721: 722: 723: 724: 725:
726: public static function addStyle($style, $parent = '#')
727: {
728: if (isset(self::$styles[$style])) {
729: throw new InvalidArgumentException("Style '$style' already exists.");
730: }
731:
732: if ($parent !== NULL) {
733: if (!isset(self::$styles[$parent])) {
734: throw new InvalidArgumentException("Parent style '$parent' doesn't exist.");
735: }
736: self::$styles[$style] = self::$styles[$parent];
737:
738: } else {
739: self::$styles[$style] = array();
740: }
741: }
742:
743:
744:
745: 746: 747: 748: 749: 750: 751:
752: public static function setStyleProperty($style, $key, $value)
753: {
754: if (!isset(self::$styles[$style])) {
755: throw new InvalidArgumentException("Style '$style' doesn't exist.");
756: }
757: self::$styles[$style][$key] = $value;
758: }
759:
760: }
761: