Source for file Route.php
Documentation is available at Route.php
6: * @copyright Copyright (c) 2004, 2010 David Grudl
7: * @license http://nettephp.com/license Nette license
8: * @link http://nettephp.com
10: * @package Nette\Application
16: * The bidirectional route is responsible for mapping
17: * HTTP request to a PresenterRoute object for dispatch and vice-versa.
19: * @copyright Copyright (c) 2004, 2010 David Grudl
20: * @package Nette\Application
31: /**#@+ @ignore internal uri type */
37: /**#@+ key used in {@link Route::$styles} or metadata {@link Route::__construct} */
45: /**#@+ @ignore internal fixity types - how to handle default value? {@link Route::$metadata} */
47: const PATH_OPTIONAL =
1;
52: public static $defaultFlags =
0;
55: public static $styles =
array(
56: '#' =>
array( // default style for path parameters
61: '?#' =>
array( // default style for query parameters
68: 'presenter' =>
array(
80: '?presenter' =>
array(
92: /** @var string regular expression pattern */
95: /** @var array of [value & fixity, filterIn, filterOut] */
96: private $metadata =
array();
101: /** @var int HOST, PATH, RELATIVE */
110: * @param string URL mask, e.g. '<presenter>/<action>/<id \d{1,3}>'
111: * @param array default values or metadata
114: public function __construct($mask, array $metadata =
array(), $flags =
0)
116: $this->flags =
$flags |
self::$defaultFlags;
117: if (!($this->flags & self::FULL_META)) {
118: foreach ($metadata as $name =>
$def) {
119: $metadata[$name] =
array(self::VALUE =>
$def);
122: $this->setMask($mask, $metadata);
128: * Maps HTTP request to a PresenterRequest object.
129: * @param IHttpRequest
130: * @return PresenterRequest|NULL
132: public function match(IHttpRequest $httpRequest)
134: // combine with precedence: mask (params in URL-path), fixity, query, (post,) defaults
137: $uri =
$httpRequest->getUri();
139: if ($this->type ===
self::HOST) {
140: $path =
'//' .
$uri->getHost() .
$uri->getPath();
142: } elseif ($this->type ===
self::RELATIVE) {
143: $basePath =
$uri->getBasePath();
150: $path =
$uri->getPath();
158: // stop, not matched
162: // deletes numeric keys, restore '-' chars
164: foreach ($matches as $k =>
$v) {
171: // 2) CONSTANT FIXITY
172: foreach ($this->metadata as $name =>
$meta) {
173: if (isset($params[$name])) {
174: //$params[$name] = $this->flags & self::CASE_SENSITIVE === 0 ? strtolower($params[$name]) : */$params[$name]; // strtolower damages UTF-8
176: } elseif (isset($meta['fixity']) &&
$meta['fixity'] !==
self::OPTIONAL) {
177: $params[$name] =
NULL; // cannot be overwriten in 3) and detected by isset() in 4)
184: $params +=
self::renameKeys($httpRequest->getQuery(), array_flip($this->xlat));
186: $params +=
$httpRequest->getQuery();
190: // 4) APPLY FILTERS & FIXITY
191: foreach ($this->metadata as $name =>
$meta) {
192: if (isset($params[$name])) {
195: } elseif (isset($meta[self::FILTER_TABLE][$params[$name]])) { // applyies filterTable only to scalar parameters
196: $params[$name] =
$meta[self::FILTER_TABLE][$params[$name]];
198: } elseif (isset($meta[self::FILTER_IN])) { // applyies filterIn only to scalar parameters
200: if ($params[$name] ===
NULL &&
!isset($meta['fixity'])) {
201: return NULL; // rejected by filter
205: } elseif (isset($meta['fixity'])) {
206: $params[$name] =
$meta[self::VALUE];
211: // 5) BUILD PresenterRequest
212: if (!isset($params[self::PRESENTER_KEY])) {
215: if (isset($this->metadata[self::MODULE_KEY])) {
216: if (!isset($params[self::MODULE_KEY])) {
219: $presenter =
$params[self::MODULE_KEY] .
':' .
$params[self::PRESENTER_KEY];
220: unset($params[self::MODULE_KEY], $params[self::PRESENTER_KEY]);
223: $presenter =
$params[self::PRESENTER_KEY];
224: unset($params[self::PRESENTER_KEY]);
229: $httpRequest->getMethod(),
231: $httpRequest->getPost(),
232: $httpRequest->getFiles(),
240: * Constructs absolute URL from PresenterRequest object.
241: * @param IHttpRequest
242: * @param PresenterRequest
243: * @return string|NULL
245: public function constructUrl(PresenterRequest $appRequest, IHttpRequest $httpRequest)
247: if ($this->flags & self::ONE_WAY) {
251: $params =
$appRequest->getParams();
252: $metadata =
$this->metadata;
254: $presenter =
$appRequest->getPresenterName();
255: $params[self::PRESENTER_KEY] =
$presenter;
257: if (isset($metadata[self::MODULE_KEY])) { // try split into module and [submodule:]presenter parts
258: $module =
$metadata[self::MODULE_KEY];
259: if (isset($module['fixity']) &&
strncasecmp($presenter, $module[self::VALUE] .
':', strlen($module[self::VALUE]) +
1) ===
0) {
265: $params[self::MODULE_KEY] =
substr($presenter, 0, $a);
266: $params[self::PRESENTER_KEY] =
substr($presenter, $a +
1);
270: foreach ($metadata as $name =>
$meta) {
271: if (!isset($params[$name])) continue; // retains NULL values
273: if (isset($meta['fixity'])) {
275: // remove default values; NULL values are retain
276: unset($params[$name]);
279: } elseif ($meta['fixity'] ===
self::CONSTANT) {
280: return NULL; // missing or wrong parameter '$name'
286: } elseif (isset($meta['filterTable2'][$params[$name]])) {
287: $params[$name] =
$meta['filterTable2'][$params[$name]];
289: } elseif (isset($meta[self::FILTER_OUT])) {
294: return NULL; // pattern not match
299: $sequence =
$this->sequence;
300: $brackets =
array();
305: $uri =
$sequence[$i] .
$uri;
306: if ($i ===
0) break;
309: $name =
$sequence[$i]; $i--
; // parameter name
311: if ($name ===
']') { // opening optional part
314: } elseif ($name[0] ===
'[') { // closing optional part
316: if ($required <
count($brackets) +
1) { // is this level optional?
317: if ($name !==
'[!') { // and not "required"-optional
324: } elseif ($name[0] ===
'?') { // "foo" parameter
327: } elseif (isset($params[$name]) &&
$params[$name] !=
'') { // intentionally ==
328: $required =
count($brackets); // make this level required
329: $uri =
$params[$name] .
$uri;
330: unset($params[$name]);
332: } elseif (isset($metadata[$name]['fixity'])) { // has default value?
333: $uri =
$metadata[$name]['defOut'] .
$uri;
336: return NULL; // missing parameter '$name'
341: // build query string
343: $params =
self::renameKeys($params, $this->xlat);
348: if ($query !=
'') $uri .=
'?' .
$query; // intentionally ==
351: if ($this->type ===
self::RELATIVE) {
352: $uri =
'//' .
$httpRequest->getUri()->getAuthority() .
$httpRequest->getUri()->getBasePath() .
$uri;
354: } elseif ($this->type ===
self::PATH) {
355: $uri =
'//' .
$httpRequest->getUri()->getAuthority() .
$uri;
359: return NULL; // TODO: implement counterpart in match() ?
362: $uri =
($this->flags & self::SECURED ?
'https:' :
'http:') .
$uri;
370: * Parse mask and array of default values; initializes object.
375: private function setMask($mask, array $metadata)
377: $this->mask =
$mask;
379: // detect '//host/path' vs. '/abs. path' vs. 'relative path'
380: if (substr($mask, 0, 2) ===
'//') {
381: $this->type =
self::HOST;
383: } elseif (substr($mask, 0, 1) ===
'/') {
384: $this->type =
self::PATH;
387: $this->type =
self::RELATIVE;
390: foreach ($metadata as $name =>
$meta) {
392: $metadata[$name]['fixity'] =
self::CONSTANT;
398: '/<([^># ]+) *([^>#]*)(#?[^>\[\]]*)>|(\[!?|\]|\s*\?.*)/', // <parameter-name [pattern] [#class]> or [ or ] or ?...
401: PREG_SPLIT_DELIM_CAPTURE
404: $this->xlat =
array();
407: // PARSE QUERY PART OF MASK
408: if (isset($parts[$i -
1]) &&
substr(ltrim($parts[$i -
1]), 0, 1) ===
'?') {
410: '/(?:([a-zA-Z0-9_.-]+)=)?<([^># ]+) *([^>#]*)(#?[^>]*)>/', // name=<parameter-name [pattern][#class]>
415: foreach ($matches as $match) {
416: list(, $param, $name, $pattern, $class) =
$match; // $pattern is not used
418: if ($class !==
'') {
419: if (!isset(self::$styles[$class])) {
420: throw new InvalidStateException("Parameter '$name' has '$class' flag, but Route::\$styles['$class'] is not set.");
422: $meta =
self::$styles[$class];
424: } elseif (isset(self::$styles['?' .
$name])) {
425: $meta =
self::$styles['?' .
$name];
428: $meta =
self::$styles['?#'];
431: if (isset($metadata[$name])) {
432: $meta =
$metadata[$name] +
$meta;
435: if (array_key_exists(self::VALUE, $meta)) {
436: $meta['fixity'] =
self::OPTIONAL;
439: unset($meta['pattern']);
440: $meta['filterTable2'] =
empty($meta[self::FILTER_TABLE]) ?
NULL :
array_flip($meta[self::FILTER_TABLE]);
442: $metadata[$name] =
$meta;
443: if ($param !==
'') {
444: $this->xlat[$name] =
$param;
450: $brackets =
0; // optional level
452: $sequence =
array();
453: $autoOptional =
array(0, 0); // strlen($re), count($sequence)
460: if ($i ===
0) break;
463: $part =
$parts[$i]; // [ or ]
464: if ($part ===
'[' ||
$part ===
']' ||
$part ===
'[!') {
465: $brackets +=
$part[0] ===
'[' ? -
1 :
1;
466: if ($brackets <
0) {
467: throw new InvalidArgumentException("Unexpected '$part' in mask '$mask'.");
470: $re =
($part[0] ===
'[' ?
'(?:' :
')?') .
$re;
475: $class =
$parts[$i]; $i--
; // validation class
476: $pattern =
trim($parts[$i]); $i--
; // validation condition (as regexp)
477: $name =
$parts[$i]; $i--
; // parameter name
480: if ($name[0] ===
'?') { // "foo" parameter
482: $sequence[1] =
substr($name, 1) .
$sequence[1];
486: // check name (limitation by regexp)
488: throw new InvalidArgumentException("Parameter name must be alphanumeric string due to limitations of PCRE, '$name' given.");
491: // pattern, condition & metadata
492: if ($class !==
'') {
493: if (!isset(self::$styles[$class])) {
494: throw new InvalidStateException("Parameter '$name' has '$class' flag, but Route::\$styles['$class'] is not set.");
496: $meta =
self::$styles[$class];
498: } elseif (isset(self::$styles[$name])) {
499: $meta =
self::$styles[$name];
502: $meta =
self::$styles['#'];
505: if (isset($metadata[$name])) {
506: $meta =
$metadata[$name] +
$meta;
509: if ($pattern ==
'' &&
isset($meta[self::PATTERN])) {
510: $pattern =
$meta[self::PATTERN];
513: $meta['filterTable2'] =
empty($meta[self::FILTER_TABLE]) ?
NULL :
array_flip($meta[self::FILTER_TABLE]);
515: if (isset($meta['filterTable2'][$meta[self::VALUE]])) {
516: $meta['defOut'] =
$meta['filterTable2'][$meta[self::VALUE]];
518: } elseif (isset($meta[self::FILTER_OUT])) {
522: $meta['defOut'] =
$meta[self::VALUE];
525: $meta[self::PATTERN] =
"#(?:$pattern)$#A" .
($this->flags & self::CASE_SENSITIVE ?
'' :
'iu');
527: // include in expression
528: $re =
'(?P<' .
str_replace('-', '___', $name) .
'>' .
$pattern .
')' .
$re; // str_replace is dirty trick to enable '-' in parameter name
529: if ($brackets) { // is in brackets?
530: if (!isset($meta[self::VALUE])) {
531: $meta[self::VALUE] =
$meta['defOut'] =
NULL;
533: $meta['fixity'] =
self::PATH_OPTIONAL;
535: } elseif (isset($meta['fixity'])) { // auto-optional
539: $meta['fixity'] =
self::PATH_OPTIONAL;
545: $metadata[$name] =
$meta;
549: throw new InvalidArgumentException("Missing closing ']' in mask '$mask'.");
552: $this->re =
'#' .
$re .
'/?$#A' .
($this->flags & self::CASE_SENSITIVE ?
'' :
'iu');
553: $this->metadata =
$metadata;
554: $this->sequence =
$sequence;
571: * Returns default values.
576: $defaults =
array();
577: foreach ($this->metadata as $name =>
$meta) {
578: if (isset($meta['fixity'])) {
579: $defaults[$name] =
$meta[self::VALUE];
587: /********************* Utilities ****************d*g**/
592: * Proprietary cache aim.
593: * @return string|FALSE
597: if ($this->flags & self::ONE_WAY) {
601: $m =
$this->metadata;
604: if (isset($m[self::MODULE_KEY])) {
605: if (isset($m[self::MODULE_KEY]['fixity']) &&
$m[self::MODULE_KEY]['fixity'] ===
self::CONSTANT) {
606: $module =
$m[self::MODULE_KEY][self::VALUE] .
':';
612: if (isset($m[self::PRESENTER_KEY]['fixity']) &&
$m[self::PRESENTER_KEY]['fixity'] ===
self::CONSTANT) {
613: return $module .
$m[self::PRESENTER_KEY][self::VALUE];
621: * Rename keys in array.
626: private static function renameKeys($arr, $xlat)
628: if (empty($xlat)) return $arr;
632: foreach ($arr as $k =>
$v) {
633: if (isset($xlat[$k])) {
634: $res[$xlat[$k]] =
$v;
636: } elseif (!isset($occupied[$k])) {
645: /********************* Inflectors ****************d*g**/
650: * camelCaseAction name -> dash-separated.
654: private static function action2path($s)
665: * dash-separated -> camelCaseAction name.
669: private static function path2action($s)
674: //$s = lcfirst(ucwords($s));
682: * PascalCase:Presenter name -> dash-and-dot-separated.
686: private static function presenter2path($s)
698: * dash-and-dot-separated -> PascalCase:Presenter name.
702: private static function path2presenter($s)
714: /********************* Route::$styles manipulator ****************d*g**/
719: * Creates new style.
720: * @param string style name (#style, urlParameter, ?queryParameter)
721: * @param string optional parent style name
726: if (isset(self::$styles[$style])) {
727: throw new InvalidArgumentException("Style '$style' already exists.");
730: if ($parent !==
NULL) {
731: if (!isset(self::$styles[$parent])) {
732: throw new InvalidArgumentException("Parent style '$parent' doesn't exist.");
734: self::$styles[$style] =
self::$styles[$parent];
737: self::$styles[$style] =
array();
744: * Changes style property value.
745: * @param string style name (#style, urlParameter, ?queryParameter)
746: * @param string property name (Route::PATTERN, Route::FILTER_IN, Route::FILTER_OUT, Route::FILTER_TABLE)
747: * @param mixed property value
750: public static function setStyleProperty($style, $key, $value)
752: if (!isset(self::$styles[$style])) {
753: throw new InvalidArgumentException("Style '$style' doesn't exist.");
755: self::$styles[$style][$key] =
$value;