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 http://nettephp.com
13: * @copyright Copyright (c) 2004, 2009 David Grudl
14: * @license http://nettephp.com/license Nette license
15: * @link http://nettephp.com
17: * @package Nette\Application
22: require_once dirname(__FILE__) .
'/../Object.php';
24: require_once dirname(__FILE__) .
'/../Application/IRouter.php';
29: * The bidirectional route is responsible for mapping
30: * HTTP request to a PresenterRoute object for dispatch and vice-versa.
32: * @author David Grudl
33: * @copyright Copyright (c) 2004, 2009 David Grudl
34: * @package Nette\Application
50: /**#@+ key used in {@link Route::$styles} */
57: /**#@+ @ignore internal fixity types - how to handle 'default' value? {@link Route::$metadata} */
59: const PATH_OPTIONAL =
1;
64: public static $defaultFlags =
0;
67: public static $styles =
array(
68: '#' =>
array( // default style for path parameters
73: '?#' =>
array( // default style for query parameters
80: 'presenter' =>
array(
92: '?presenter' =>
array(
104: /** @var string regular expression pattern */
107: /** @var array of [default & fixity, filterIn, filterOut] */
113: /** @var int HOST, PATH, RELATIVE */
122: * @param string URL mask, e.g. '<presenter>/<action>/<id \d{1,3}>'
123: * @param array default values
126: public function __construct($mask, array $defaults =
array(), $flags =
0)
128: $this->flags =
$flags |
self::$defaultFlags;
129: $this->setMask($mask, $defaults);
135: * Maps HTTP request to a PresenterRequest object.
136: * @param IHttpRequest
137: * @return PresenterRequest|NULL
139: public function match(IHttpRequest $httpRequest)
141: // combine with precedence: mask (params in URL-path), fixity, query, (post,) defaults
144: $uri =
$httpRequest->getUri();
147: $path =
'//' .
$uri->getHost() .
$uri->getPath();
149: } elseif ($this->type ===
self::RELATIVE) {
150: $basePath =
$uri->getBasePath();
157: $path =
$uri->getPath();
165: // stop, not matched
169: // deletes numeric keys, restore '-' chars
171: foreach ($matches as $k =>
$v) {
178: // 2) CONSTANT FIXITY
180: if (isset($params[$name])) {
181: //$params[$name] = $this->flags & self::CASE_SENSITIVE === 0 ? strtolower($params[$name]) : */$params[$name]; // strtolower damages UTF-8
183: } elseif (isset($meta['fixity']) &&
$meta['fixity'] !==
self::OPTIONAL) {
184: $params[$name] =
NULL; // cannot be overwriten in 3) and detected by isset() in 4)
193: $params +=
$httpRequest->getQuery();
197: // 4) APPLY FILTERS & FIXITY
199: if (isset($params[$name])) {
202: } elseif (isset($meta[self::FILTER_TABLE][$params[$name]])) { // applyies filterTable only to scalar parameters
203: $params[$name] =
$meta[self::FILTER_TABLE][$params[$name]];
205: } elseif (isset($meta[self::FILTER_IN])) { // applyies filterIn only to scalar parameters
207: if ($params[$name] ===
NULL &&
!isset($meta['fixity'])) {
208: return NULL; // rejected by filter
212: } elseif (isset($meta['fixity'])) {
213: $params[$name] =
$meta['default'];
218: // 5) BUILD PresenterRequest
219: if (!isset($params[self::PRESENTER_KEY])) {
223: if (!isset($params[self::MODULE_KEY])) {
226: $presenter =
$params[self::MODULE_KEY] .
':' .
$params[self::PRESENTER_KEY];
227: unset($params[self::MODULE_KEY], $params[self::PRESENTER_KEY]);
230: $presenter =
$params[self::PRESENTER_KEY];
231: unset($params[self::PRESENTER_KEY]);
236: $httpRequest->getMethod(),
238: $httpRequest->getPost(),
239: $httpRequest->getFiles(),
247: * Constructs absolute URL from PresenterRequest object.
248: * @param IHttpRequest
249: * @param PresenterRequest
250: * @return string|NULL
252: public function constructUrl(PresenterRequest $appRequest, IHttpRequest $httpRequest)
258: $params =
$appRequest->getParams();
261: $presenter =
$appRequest->getPresenterName();
262: if (isset($metadata[self::MODULE_KEY])) {
263: if (isset($metadata[self::MODULE_KEY]['fixity'])) {
264: $a =
strlen($metadata[self::MODULE_KEY]['default']);
266: return NULL; // module not match
271: $params[self::MODULE_KEY] =
substr($presenter, 0, $a);
272: $params[self::PRESENTER_KEY] =
substr($presenter, $a +
1);
274: $params[self::PRESENTER_KEY] =
$presenter;
277: foreach ($metadata as $name =>
$meta) {
278: if (!isset($params[$name])) continue; // retains NULL values
280: if (isset($meta['fixity'])) {
282: // remove default values; NULL values are retain
283: unset($params[$name]);
286: } elseif ($meta['fixity'] ===
self::CONSTANT) {
287: return NULL; // missing or wrong parameter '$name'
293: } elseif (isset($meta['filterTable2'][$params[$name]])) {
294: $params[$name] =
$meta['filterTable2'][$params[$name]];
296: } elseif (isset($meta[self::FILTER_OUT])) {
300: if (isset($meta[self::PATTERN]) &&
!preg_match($meta[self::PATTERN], $params[$name])) {
301: return NULL; // pattern not match
306: $sequence =
$this->sequence;
311: $uri =
$sequence[$i] .
$uri;
312: if ($i ===
0) break;
315: $name =
$sequence[$i]; $i--
; // parameter name
317: if ($name[0] ===
'?') { // "foo" parameter
320: } elseif (isset($params[$name]) &&
$params[$name] !=
'') { // intentionally ==
322: $uri =
$params[$name] .
$uri;
323: unset($params[$name]);
325: } elseif (isset($metadata[$name]['fixity'])) { // has default value?
329: } elseif ($metadata[$name]['default'] ==
'') { // intentionally ==
330: if ($uri[0] ===
'/' &&
substr($sequence[$i], -
1) ===
'/') {
331: return NULL; // default value is empty but is required
335: $uri =
$metadata[$name]['defOut'] .
$uri;
339: return NULL; // missing parameter '$name'
344: // build query string
346: $params =
self::renameKeys($params, $this->xlat);
351: if ($query !=
'') $uri .=
'?' .
$query; // intentionally ==
354: if ($this->type ===
self::RELATIVE) {
355: $uri =
'//' .
$httpRequest->getUri()->getAuthority() .
$httpRequest->getUri()->getBasePath() .
$uri;
357: } elseif ($this->type ===
self::PATH) {
358: $uri =
'//' .
$httpRequest->getUri()->getAuthority() .
$uri;
361: $uri =
($this->flags & self::SECURED ?
'https:' :
'http:') .
$uri;
369: * Parse mask and array of default values; initializes object.
374: private function setMask($mask, array $defaults)
376: $this->mask =
$mask;
378: // detect '//host/path' vs. '/abs. path' vs. 'relative path'
379: if (substr($mask, 0, 2) ===
'//') {
382: } elseif (substr($mask, 0, 1) ===
'/') {
389: $metadata =
array();
390: foreach ($defaults as $name =>
$def) {
391: $metadata[$name] =
array(
393: 'fixity' =>
self::CONSTANT
398: // 1) PARSE QUERY PART OF MASK
401: if ($pos !==
FALSE) {
403: '/(?:([a-zA-Z0-9_.-]+)=)?<([^># ]+) *([^>#]*)(#?[^>]*)>/', // name=<parameter-name [pattern][#class]>
410: foreach ($matches as $match) {
411: list(, $param, $name, $pattern, $class) =
$match; // $pattern is unsed
413: if ($class !==
'') {
414: if (!isset(self::$styles[$class])) {
415: throw new InvalidStateException("Parameter '$name' has '$class' flag, but Route::\$styles['$class'] is not set.");
417: $meta =
self::$styles[$class];
419: } elseif (isset(self::$styles['?' .
$name])) {
420: $meta =
self::$styles['?' .
$name];
423: $meta =
self::$styles['?#'];
426: if (isset($metadata[$name])) {
427: $meta =
$meta +
$metadata[$name];
430: if (array_key_exists('default', $meta)) {
431: $meta['fixity'] =
self::OPTIONAL;
434: unset($meta['pattern']);
435: $meta['filterTable2'] =
empty($meta[self::FILTER_TABLE]) ?
NULL :
array_flip($meta[self::FILTER_TABLE]);
437: $metadata[$name] =
$meta;
438: if ($param !==
'') {
445: // 2) PARSE URI-PATH PART OF MASK
447: '/<([^># ]+) *([^>#]*)(#?[^>]*)>/', // <parameter-name [pattern][#class]>
450: PREG_SPLIT_DELIM_CAPTURE
454: $sequence =
array();
460: if ($i ===
0) break;
463: $class =
$parts[$i]; $i--
; // validation class
464: $pattern =
$parts[$i]; $i--
; // validation condition (as regexp)
465: $name =
$parts[$i]; $i--
; // parameter name
468: if ($name[0] ===
'?') { // "foo" parameter
470: $sequence[1] =
substr($name, 1) .
$sequence[1];
474: // check name (limitation by regexp)
476: throw new InvalidArgumentException("Parameter name must be alphanumeric string due to limitations of PCRE, '$name' given.");
479: // pattern, condition & metadata
480: if ($class !==
'') {
481: if (!isset(self::$styles[$class])) {
482: throw new InvalidStateException("Parameter '$name' has '$class' flag, but Route::\$styles['$class'] is not set.");
484: $meta =
self::$styles[$class];
486: } elseif (isset(self::$styles[$name])) {
487: $meta =
self::$styles[$name];
490: $meta =
self::$styles['#'];
493: if (isset($metadata[$name])) {
494: $meta =
$meta +
$metadata[$name];
497: if ($pattern ==
'' &&
isset($meta[self::PATTERN])) {
498: $pattern =
$meta[self::PATTERN];
501: $meta['filterTable2'] =
empty($meta[self::FILTER_TABLE]) ?
NULL :
array_flip($meta[self::FILTER_TABLE]);
502: if (isset($meta['default'])) {
503: if (isset($meta['filterTable2'][$meta['default']])) {
504: $meta['defOut'] =
$meta['filterTable2'][$meta['default']];
506: } elseif (isset($meta[self::FILTER_OUT])) {
510: $meta['defOut'] =
$meta['default'];
513: $meta[self::PATTERN] =
"#(?:$pattern)$#A" .
($this->flags & self::CASE_SENSITIVE ?
'' :
'i');
514: $metadata[$name] =
$meta;
516: // include in expression
517: $tmp =
str_replace('-', '___', $name); // dirty trick to enable '-' in parameter name
518: if (isset($meta['fixity'])) { // has default value?
520: throw new InvalidArgumentException("Parameter '$name' must not be optional because parameters standing on the right side are not optional.");
522: $re =
'(?:(?P<' .
$tmp .
'>' .
$pattern .
')' .
$re .
')?';
523: $metadata[$name]['fixity'] =
self::PATH_OPTIONAL;
527: $re =
'(?P<' .
$tmp .
'>' .
$pattern .
')' .
$re;
531: $this->re =
'#' .
$re .
'/?$#A' .
($this->flags & self::CASE_SENSITIVE ?
'' :
'i');
533: $this->sequence =
$sequence;
550: * Returns default values.
555: $defaults =
array();
557: if (isset($meta['fixity'])) {
558: $defaults[$name] =
$meta['default'];
566: /********************* Utilities ****************d*g**/
571: * Proprietary cache aim.
572: * @return string|FALSE
583: if (isset($m[self::MODULE_KEY])) {
584: if (isset($m[self::MODULE_KEY]['fixity']) &&
$m[self::MODULE_KEY]['fixity'] ===
self::CONSTANT) {
585: $module =
$m[self::MODULE_KEY]['default'] .
':';
591: if (isset($m[self::PRESENTER_KEY]['fixity']) &&
$m[self::PRESENTER_KEY]['fixity'] ===
self::CONSTANT) {
592: return $module .
$m[self::PRESENTER_KEY]['default'];
600: * Rename keys in array.
605: private static function renameKeys($arr, $xlat)
607: if (empty($xlat)) return $arr;
611: foreach ($arr as $k =>
$v) {
612: if (isset($xlat[$k])) {
613: $res[$xlat[$k]] =
$v;
615: } elseif (!isset($occupied[$k])) {
624: /********************* Inflectors ****************d*g**/
629: * camelCaseAction name -> dash-separated.
633: private static function action2path($s)
644: * dash-separated -> camelCaseAction name.
648: private static function path2action($s)
653: //$s = lcfirst(ucwords($s));
661: * PascalCase:Presenter name -> dash-and-dot-separated.
665: private static function presenter2path($s)
677: * dash-and-dot-separated -> PascalCase:Presenter name.
681: private static function path2presenter($s)
693: /********************* Route::$styles manipulator ****************d*g**/
698: * Creates new style.
699: * @param string style name (#style, urlParameter, ?queryParameter)
700: * @param string optional parent style name
705: if (isset(self::$styles[$style])) {
706: throw new InvalidArgumentException("Style '$style' already exists.");
709: if ($parent !==
NULL) {
710: if (!isset(self::$styles[$parent])) {
711: throw new InvalidArgumentException("Parent style '$parent' doesn't exist.");
713: self::$styles[$style] =
self::$styles[$parent];
716: self::$styles[$style] =
array();
723: * Changes style property value.
724: * @param string style name (#style, urlParameter, ?queryParameter)
725: * @param string property name (Route::PATTERN, Route::FILTER_IN, Route::FILTER_OUT, Route::FILTER_TABLE)
726: * @param mixed property value
729: public static function setStyleProperty($style, $key, $value)
731: if (!isset(self::$styles[$style])) {
732: throw new InvalidArgumentException("Style '$style' doesn't exist.");
734: self::$styles[$style][$key] =
$value;