Source for file Route.php
Documentation is available at Route.php
6: * Copyright (c) 2004, 2009 David Grudl (http://davidgrudl.com)
8: * This source file is subject to the "Nette license" that is bundled
9: * with this package in the file license.txt.
11: * For more information please see https://nette.org
13: * @copyright Copyright (c) 2004, 2009 David Grudl
14: * @license https://nette.org/license Nette license
15: * @link https://nette.org
17: * @package Nette\Application
23: require_once dirname(__FILE__) .
'/../Object.php';
25: require_once dirname(__FILE__) .
'/../Application/IRouter.php';
30: * The bidirectional route is responsible for mapping
31: * HTTP request to a PresenterRoute object for dispatch and vice-versa.
33: * @author David Grudl
34: * @copyright Copyright (c) 2004, 2009 David Grudl
35: * @package Nette\Application
51: /**#@+ key used in {@link Route::$styles} */
58: /**#@+ @ignore internal fixity types - how to handle 'default' value? {@link Route::$metadata} */
60: const PATH_OPTIONAL =
1;
65: public static $defaultFlags =
0;
68: public static $styles =
array(
69: '#' =>
array( // default style for path parameters
74: '?#' =>
array( // default style for query parameters
81: 'presenter' =>
array(
93: '?presenter' =>
array(
105: /** @var string regular expression pattern */
108: /** @var array of [default & fixity, filterIn, filterOut] */
114: /** @var int HOST, PATH, RELATIVE */
123: * @param string URL mask, e.g. '<presenter>/<action>/<id \d{1,3}>'
124: * @param array default values
127: public function __construct($mask, array $defaults =
array(), $flags =
0)
129: $this->flags =
$flags |
self::$defaultFlags;
130: $this->setMask($mask, $defaults);
136: * Maps HTTP request to a PresenterRequest object.
137: * @param IHttpRequest
138: * @return PresenterRequest|NULL
140: public function match(IHttpRequest $httpRequest)
142: // combine with precedence: mask (params in URL-path), fixity, query, (post,) defaults
145: $uri =
$httpRequest->getUri();
148: $path =
'//' .
$uri->host .
$uri->path;
150: } elseif ($this->type ===
self::RELATIVE) {
151: $basePath =
$uri->basePath;
167: // stop, not matched
171: // deletes numeric keys, restore '-' chars
173: foreach ($matches as $k =>
$v) {
180: // 2) CONSTANT FIXITY
182: if (isset($params[$name])) {
183: //$params[$name] = $this->flags & self::CASE_SENSITIVE === 0 ? strtolower($params[$name]) : */$params[$name]; // strtolower damages UTF-8
185: } elseif (isset($meta['fixity']) &&
$meta['fixity'] !==
self::OPTIONAL) {
186: $params[$name] =
NULL; // cannot be overwriten in 3) and detected by isset() in 4)
195: $params +=
$httpRequest->getQuery();
199: // 4) APPLY FILTERS & FIXITY
201: if (isset($params[$name])) {
202: if (isset($meta[self::FILTER_TABLE][$params[$name]])) { // applyies filterTable only to path parameters
203: $params[$name] =
$meta[self::FILTER_TABLE][$params[$name]];
205: } elseif (isset($meta[self::FILTER_IN])) { // applyies filterIn only to path parameters
209: } elseif (isset($meta['fixity'])) {
210: $params[$name] =
$meta['default'];
215: // 5) BUILD PresenterRequest
216: if (!isset($params[self::PRESENTER_KEY])) {
220: if (!isset($params[self::MODULE_KEY])) {
223: $presenter =
$params[self::MODULE_KEY] .
':' .
$params[self::PRESENTER_KEY];
224: unset($params[self::MODULE_KEY], $params[self::PRESENTER_KEY]);
227: $presenter =
$params[self::PRESENTER_KEY];
228: unset($params[self::PRESENTER_KEY]);
233: $httpRequest->getMethod(),
235: $httpRequest->getPost(),
236: $httpRequest->getFiles(),
237: array('secured' =>
$httpRequest->isSecured())
244: * Constructs absolute URL from PresenterRequest object.
245: * @param IHttpRequest
246: * @param PresenterRequest
247: * @return string|NULL
249: public function constructUrl(PresenterRequest $appRequest, IHttpRequest $httpRequest)
255: $params =
$appRequest->getParams();
258: $presenter =
$appRequest->getPresenterName();
259: if (isset($metadata[self::MODULE_KEY])) {
260: if (isset($metadata[self::MODULE_KEY]['fixity'])) {
261: $a =
strlen($metadata[self::MODULE_KEY]['default']);
263: return NULL; // module not match
268: $params[self::MODULE_KEY] =
substr($presenter, 0, $a);
269: $params[self::PRESENTER_KEY] =
substr($presenter, $a +
1);
271: $params[self::PRESENTER_KEY] =
$presenter;
274: foreach ($metadata as $name =>
$meta) {
275: if (!isset($params[$name])) continue; // retains NULL values
277: if (isset($meta['fixity'])) {
278: if (strcasecmp($params[$name], $meta['default']) ===
0) { // intentionally ==
279: // remove default values; NULL values are retain
280: unset($params[$name]);
283: } elseif ($meta['fixity'] ===
self::CONSTANT) {
284: return NULL; // missing or wrong parameter '$name'
288: if (isset($meta['filterTable2'][$params[$name]])) {
289: $params[$name] =
$meta['filterTable2'][$params[$name]];
291: } elseif (isset($meta[self::FILTER_OUT])) {
295: if (isset($meta[self::PATTERN]) &&
!preg_match($meta[self::PATTERN], $params[$name])) {
296: return NULL; // pattern not match
301: $sequence =
$this->sequence;
306: $uri =
$sequence[$i] .
$uri;
307: if ($i ===
0) break;
310: $name =
$sequence[$i]; $i--
; // parameter name
312: if ($name[0] ===
'?') { // "foo" parameter
315: } elseif (isset($params[$name]) &&
$params[$name] !=
'') { // intentionally ==
317: $uri =
$params[$name] .
$uri;
318: unset($params[$name]);
320: } elseif (isset($metadata[$name]['fixity'])) { // has default value?
324: } elseif ($metadata[$name]['default'] ==
'') { // intentionally ==
325: if ($uri[0] ===
'/' &&
substr($sequence[$i], -
1) ===
'/') {
326: return NULL; // default value is empty but is required
330: $uri =
$metadata[$name]['defOut'] .
$uri;
334: return NULL; // missing parameter '$name'
339: // build query string
341: $params =
self::renameKeys($params, $this->xlat);
345: if ($query !=
'') $uri .=
'?' .
$query; // intentionally ==
348: if ($this->type ===
self::RELATIVE) {
349: $uri =
'//' .
$httpRequest->getUri()->authority .
$httpRequest->getUri()->basePath .
$uri;
351: } elseif ($this->type ===
self::PATH) {
352: $uri =
'//' .
$httpRequest->getUri()->authority .
$uri;
355: $uri =
($this->flags & self::SECURED ?
'https:' :
'http:') .
$uri;
363: * Parse mask and array of default values; initializes object.
368: private function setMask($mask, array $defaults)
370: $this->mask =
$mask;
372: // detect '//host/path' vs. '/abs. path' vs. 'relative path'
373: if (substr($mask, 0, 2) ===
'//') {
376: } elseif (substr($mask, 0, 1) ===
'/') {
383: $metadata =
array();
384: foreach ($defaults as $name =>
$def) {
385: if ($name ===
'view') $name =
'action'; // back compatibility
386: $metadata[$name] =
array(
388: 'fixity' =>
self::CONSTANT
393: // 1) PARSE QUERY PART OF MASK
396: if ($pos !==
FALSE) {
398: '/(?:([a-zA-Z0-9_.-]+)=)?<([^># ]+) *([^>#]*)(#?[^>]*)>/', // name=<parameter-name [pattern][#class]>
405: foreach ($matches as $match) {
406: list(, $param, $name, $pattern, $class) =
$match; // $pattern is unsed
407: if ($name ===
'view') $name =
'action'; // back compatibility
409: if ($class !==
'') {
410: if (!isset(self::$styles[$class])) {
411: throw new InvalidStateException("Parameter '$name' has '$class' flag, but Route::\$styles['$class'] is not set.");
413: $meta =
self::$styles[$class];
415: } elseif (isset(self::$styles['?' .
$name])) {
416: $meta =
self::$styles['?' .
$name];
419: $meta =
self::$styles['?#'];
422: if (isset($metadata[$name])) {
423: $meta =
$meta +
$metadata[$name];
426: if (array_key_exists('default', $meta)) {
427: $meta['fixity'] =
self::OPTIONAL;
430: unset($meta['pattern']);
431: $meta['filterTable2'] =
empty($meta[self::FILTER_TABLE]) ?
NULL :
array_flip($meta[self::FILTER_TABLE]);
433: $metadata[$name] =
$meta;
434: if ($param !==
'') {
441: // 2) PARSE URI-PATH PART OF MASK
443: '/<([^># ]+) *([^>#]*)(#?[^>]*)>/', // <parameter-name [pattern][#class]>
446: PREG_SPLIT_DELIM_CAPTURE
450: $sequence =
array();
456: if ($i ===
0) break;
459: $class =
$parts[$i]; $i--
; // validation class
460: $pattern =
$parts[$i]; $i--
; // validation condition (as regexp)
461: $name =
$parts[$i]; $i--
; // parameter name
462: if ($name ===
'view') $name =
'action'; // back compatibility
465: if ($name[0] ===
'?') { // "foo" parameter
466: $re =
'(?:' .
$pattern .
')' .
$re;
467: $sequence[1] =
substr($name, 1) .
$sequence[1];
471: // check name (limitation by regexp)
473: throw new InvalidArgumentException("Parameter name must be alphanumeric string due to limitations of PCRE, '$name' given.");
476: // pattern, condition & metadata
477: if ($class !==
'') {
478: if (!isset(self::$styles[$class])) {
479: throw new InvalidStateException("Parameter '$name' has '$class' flag, but Route::\$styles['$class'] is not set.");
481: $meta =
self::$styles[$class];
483: } elseif (isset(self::$styles[$name])) {
484: $meta =
self::$styles[$name];
487: $meta =
self::$styles['#'];
490: if (isset($metadata[$name])) {
491: $meta =
$meta +
$metadata[$name];
494: if ($pattern ==
'' &&
isset($meta[self::PATTERN])) {
495: $pattern =
$meta[self::PATTERN];
498: $meta['filterTable2'] =
empty($meta[self::FILTER_TABLE]) ?
NULL :
array_flip($meta[self::FILTER_TABLE]);
499: if (isset($meta['default'])) {
500: if (isset($meta['filterTable2'][$meta['default']])) {
501: $meta['defOut'] =
$meta['filterTable2'][$meta['default']];
503: } elseif (isset($meta[self::FILTER_OUT])) {
507: $meta['defOut'] =
$meta['default'];
510: $meta[self::PATTERN] =
"#(?:$pattern)$#A" .
($this->flags & self::CASE_SENSITIVE ?
'' :
'i');
511: $metadata[$name] =
$meta;
513: // include in expression
514: $tmp =
str_replace('-', '___', $name); // dirty trick to enable '-' in parameter name
515: if (isset($meta['fixity'])) { // has default value?
517: throw new InvalidArgumentException("Parameter '$name' must not be optional because parameters standing on the right side are not optional.");
519: $re =
'(?:(?P<' .
$tmp .
'>' .
$pattern .
')' .
$re .
')?';
520: $metadata[$name]['fixity'] =
self::PATH_OPTIONAL;
524: $re =
'(?P<' .
$tmp .
'>' .
$pattern .
')' .
$re;
528: $this->re =
'#' .
$re .
'/?$#A' .
($this->flags & self::CASE_SENSITIVE ?
'' :
'i');
530: $this->sequence =
$sequence;
547: * Returns default values.
552: $defaults =
array();
554: if (isset($meta['fixity'])) {
555: $defaults[$name] =
$meta['default'];
563: /********************* Utilities ****************d*g**/
568: * Proprietary cache aim.
569: * @return string|FALSE
580: if (isset($m[self::MODULE_KEY])) {
581: if (isset($m[self::MODULE_KEY]['fixity']) &&
$m[self::MODULE_KEY]['fixity'] ===
self::CONSTANT) {
582: $module =
$m[self::MODULE_KEY]['default'] .
':';
588: if (isset($m[self::PRESENTER_KEY]['fixity']) &&
$m[self::PRESENTER_KEY]['fixity'] ===
self::CONSTANT) {
589: return $module .
$m[self::PRESENTER_KEY]['default'];
597: * Rename keys in array.
602: private static function renameKeys($arr, $xlat)
604: if (empty($xlat)) return $arr;
608: foreach ($arr as $k =>
$v) {
609: if (isset($xlat[$k])) {
610: $res[$xlat[$k]] =
$v;
612: } elseif (!isset($occupied[$k])) {
621: /********************* Inflectors ****************d*g**/
626: * camelCaseAction name -> dash-separated.
630: private static function action2path($s)
641: * dash-separated -> camelCaseAction name.
645: private static function path2action($s)
650: //$s = lcfirst(ucwords($s));
658: * PascalCase:Presenter name -> dash-and-dot-separated.
662: private static function presenter2path($s)
674: * dash-and-dot-separated -> PascalCase:Presenter name.
678: private static function path2presenter($s)
690: /********************* Route::$styles manipulator ****************d*g**/
695: * Creates new style.
696: * @param string style name (#style, urlParameter, ?queryParameter)
697: * @param string optional parent style name
702: if (isset(self::$styles[$style])) {
703: throw new InvalidArgumentException("Style '$style' already exists.");
706: if ($parent !==
NULL) {
707: if (!isset(self::$styles[$parent])) {
708: throw new InvalidArgumentException("Parent style '$parent' doesn't exist.");
710: self::$styles[$style] =
self::$styles[$parent];
713: self::$styles[$style] =
array();
720: * Changes style property value.
721: * @param string style name (#style, urlParameter, ?queryParameter)
722: * @param string property name (Route::PATTERN, Route::FILTER_IN, Route::FILTER_OUT, Route::FILTER_TABLE)
723: * @param mixed property value
726: public static function setStyleProperty($style, $key, $value)
728: if (!isset(self::$styles[$style])) {
729: throw new InvalidArgumentException("Style '$style' doesn't exist.");
731: self::$styles[$style][$key] =
$value;
738: // back-compatibility
739: Route::$styles['view'] =
& Route::$styles['action'];
740: Route::$styles['?view'] =
& Route::$styles['?action'];