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] =
is_array($def) ?
$def :
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] =
'';
267: $params[self::MODULE_KEY] =
substr($presenter, 0, $a);
268: $params[self::PRESENTER_KEY] =
substr($presenter, $a +
1);
272: foreach ($metadata as $name =>
$meta) {
273: if (!isset($params[$name])) continue; // retains NULL values
275: if (isset($meta['fixity'])) {
277: // remove default values; NULL values are retain
278: unset($params[$name]);
281: } elseif ($meta['fixity'] ===
self::CONSTANT) {
282: return NULL; // missing or wrong parameter '$name'
288: } elseif (isset($meta['filterTable2'][$params[$name]])) {
289: $params[$name] =
$meta['filterTable2'][$params[$name]];
291: } elseif (isset($meta[self::FILTER_OUT])) {
296: return NULL; // pattern not match
301: $sequence =
$this->sequence;
302: $brackets =
array();
307: $uri =
$sequence[$i] .
$uri;
308: if ($i ===
0) break;
311: $name =
$sequence[$i]; $i--
; // parameter name
313: if ($name ===
']') { // opening optional part
316: } elseif ($name[0] ===
'[') { // closing optional part
318: if ($required <
count($brackets) +
1) { // is this level optional?
319: if ($name !==
'[!') { // and not "required"-optional
326: } elseif ($name[0] ===
'?') { // "foo" parameter
329: } elseif (isset($params[$name]) &&
$params[$name] !=
'') { // intentionally ==
330: $required =
count($brackets); // make this level required
331: $uri =
$params[$name] .
$uri;
332: unset($params[$name]);
334: } elseif (isset($metadata[$name]['fixity'])) { // has default value?
335: $uri =
$metadata[$name]['defOut'] .
$uri;
338: return NULL; // missing parameter '$name'
343: // build query string
345: $params =
self::renameKeys($params, $this->xlat);
350: if ($query !=
'') $uri .=
'?' .
$query; // intentionally ==
353: if ($this->type ===
self::RELATIVE) {
354: $uri =
'//' .
$httpRequest->getUri()->getAuthority() .
$httpRequest->getUri()->getBasePath() .
$uri;
356: } elseif ($this->type ===
self::PATH) {
357: $uri =
'//' .
$httpRequest->getUri()->getAuthority() .
$uri;
361: return NULL; // TODO: implement counterpart in match() ?
364: $uri =
($this->flags & self::SECURED ?
'https:' :
'http:') .
$uri;
372: * Parse mask and array of default values; initializes object.
377: private function setMask($mask, array $metadata)
379: $this->mask =
$mask;
381: // detect '//host/path' vs. '/abs. path' vs. 'relative path'
382: if (substr($mask, 0, 2) ===
'//') {
383: $this->type =
self::HOST;
385: } elseif (substr($mask, 0, 1) ===
'/') {
386: $this->type =
self::PATH;
389: $this->type =
self::RELATIVE;
392: foreach ($metadata as $name =>
$meta) {
394: $metadata[$name]['fixity'] =
self::CONSTANT;
400: '/<([^># ]+) *([^>#]*)(#?[^>\[\]]*)>|(\[!?|\]|\s*\?.*)/', // <parameter-name [pattern] [#class]> or [ or ] or ?...
403: PREG_SPLIT_DELIM_CAPTURE
406: $this->xlat =
array();
409: // PARSE QUERY PART OF MASK
410: if (isset($parts[$i -
1]) &&
substr(ltrim($parts[$i -
1]), 0, 1) ===
'?') {
412: '/(?:([a-zA-Z0-9_.-]+)=)?<([^># ]+) *([^>#]*)(#?[^>]*)>/', // name=<parameter-name [pattern][#class]>
417: foreach ($matches as $match) {
418: list(, $param, $name, $pattern, $class) =
$match; // $pattern is not used
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.");
424: $meta =
self::$styles[$class];
426: } elseif (isset(self::$styles['?' .
$name])) {
427: $meta =
self::$styles['?' .
$name];
430: $meta =
self::$styles['?#'];
433: if (isset($metadata[$name])) {
434: $meta =
$metadata[$name] +
$meta;
437: if (array_key_exists(self::VALUE, $meta)) {
438: $meta['fixity'] =
self::OPTIONAL;
441: unset($meta['pattern']);
442: $meta['filterTable2'] =
empty($meta[self::FILTER_TABLE]) ?
NULL :
array_flip($meta[self::FILTER_TABLE]);
444: $metadata[$name] =
$meta;
445: if ($param !==
'') {
446: $this->xlat[$name] =
$param;
452: $brackets =
0; // optional level
454: $sequence =
array();
455: $autoOptional =
array(0, 0); // strlen($re), count($sequence)
462: if ($i ===
0) break;
465: $part =
$parts[$i]; // [ or ]
466: if ($part ===
'[' ||
$part ===
']' ||
$part ===
'[!') {
467: $brackets +=
$part[0] ===
'[' ? -
1 :
1;
468: if ($brackets <
0) {
469: throw new InvalidArgumentException("Unexpected '$part' in mask '$mask'.");
472: $re =
($part[0] ===
'[' ?
'(?:' :
')?') .
$re;
477: $class =
$parts[$i]; $i--
; // validation class
478: $pattern =
trim($parts[$i]); $i--
; // validation condition (as regexp)
479: $name =
$parts[$i]; $i--
; // parameter name
482: if ($name[0] ===
'?') { // "foo" parameter
484: $sequence[1] =
substr($name, 1) .
$sequence[1];
488: // check name (limitation by regexp)
490: throw new InvalidArgumentException("Parameter name must be alphanumeric string due to limitations of PCRE, '$name' given.");
493: // pattern, condition & metadata
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.");
498: $meta =
self::$styles[$class];
500: } elseif (isset(self::$styles[$name])) {
501: $meta =
self::$styles[$name];
504: $meta =
self::$styles['#'];
507: if (isset($metadata[$name])) {
508: $meta =
$metadata[$name] +
$meta;
511: if ($pattern ==
'' &&
isset($meta[self::PATTERN])) {
512: $pattern =
$meta[self::PATTERN];
515: $meta['filterTable2'] =
empty($meta[self::FILTER_TABLE]) ?
NULL :
array_flip($meta[self::FILTER_TABLE]);
517: if (isset($meta['filterTable2'][$meta[self::VALUE]])) {
518: $meta['defOut'] =
$meta['filterTable2'][$meta[self::VALUE]];
520: } elseif (isset($meta[self::FILTER_OUT])) {
524: $meta['defOut'] =
$meta[self::VALUE];
527: $meta[self::PATTERN] =
"#(?:$pattern)$#A" .
($this->flags & self::CASE_SENSITIVE ?
'' :
'iu');
529: // include in expression
530: $re =
'(?P<' .
str_replace('-', '___', $name) .
'>' .
$pattern .
')' .
$re; // str_replace is dirty trick to enable '-' in parameter name
531: if ($brackets) { // is in brackets?
532: if (!isset($meta[self::VALUE])) {
533: $meta[self::VALUE] =
$meta['defOut'] =
NULL;
535: $meta['fixity'] =
self::PATH_OPTIONAL;
537: } elseif (isset($meta['fixity'])) { // auto-optional
541: $meta['fixity'] =
self::PATH_OPTIONAL;
547: $metadata[$name] =
$meta;
551: throw new InvalidArgumentException("Missing closing ']' in mask '$mask'.");
554: $this->re =
'#' .
$re .
'/?$#A' .
($this->flags & self::CASE_SENSITIVE ?
'' :
'iu');
555: $this->metadata =
$metadata;
556: $this->sequence =
$sequence;
573: * Returns default values.
578: $defaults =
array();
579: foreach ($this->metadata as $name =>
$meta) {
580: if (isset($meta['fixity'])) {
581: $defaults[$name] =
$meta[self::VALUE];
589: /********************* Utilities ****************d*g**/
594: * Proprietary cache aim.
595: * @return string|FALSE
599: if ($this->flags & self::ONE_WAY) {
603: $m =
$this->metadata;
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] .
':';
614: if (isset($m[self::PRESENTER_KEY]['fixity']) &&
$m[self::PRESENTER_KEY]['fixity'] ===
self::CONSTANT) {
615: return $module .
$m[self::PRESENTER_KEY][self::VALUE];
623: * Rename keys in array.
628: private static function renameKeys($arr, $xlat)
630: if (empty($xlat)) return $arr;
634: foreach ($arr as $k =>
$v) {
635: if (isset($xlat[$k])) {
636: $res[$xlat[$k]] =
$v;
638: } elseif (!isset($occupied[$k])) {
647: /********************* Inflectors ****************d*g**/
652: * camelCaseAction name -> dash-separated.
656: private static function action2path($s)
667: * dash-separated -> camelCaseAction name.
671: private static function path2action($s)
676: //$s = lcfirst(ucwords($s));
684: * PascalCase:Presenter name -> dash-and-dot-separated.
688: private static function presenter2path($s)
700: * dash-and-dot-separated -> PascalCase:Presenter name.
704: private static function path2presenter($s)
716: /********************* Route::$styles manipulator ****************d*g**/
721: * Creates new style.
722: * @param string style name (#style, urlParameter, ?queryParameter)
723: * @param string optional parent style name
728: if (isset(self::$styles[$style])) {
729: throw new InvalidArgumentException("Style '$style' already exists.");
732: if ($parent !==
NULL) {
733: if (!isset(self::$styles[$parent])) {
734: throw new InvalidArgumentException("Parent style '$parent' doesn't exist.");
736: self::$styles[$style] =
self::$styles[$parent];
739: self::$styles[$style] =
array();
746: * Changes style property value.
747: * @param string style name (#style, urlParameter, ?queryParameter)
748: * @param string property name (Route::PATTERN, Route::FILTER_IN, Route::FILTER_OUT, Route::FILTER_TABLE)
749: * @param mixed property value
752: public static function setStyleProperty($style, $key, $value)
754: if (!isset(self::$styles[$style])) {
755: throw new InvalidArgumentException("Style '$style' doesn't exist.");
757: self::$styles[$style][$key] =
$value;