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