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