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
45: /**#@+ @ignore internal uri type */
51: /**#@+ key used in {@link Route::$styles} or metadata {@link Route::__construct} */
59: /**#@+ @ignore internal fixity types - how to handle default value? {@link Route::$metadata} */
61: const PATH_OPTIONAL =
1;
66: public static $defaultFlags =
0;
69: public static $styles =
array(
70: '#' =>
array( // default style for path parameters
75: '?#' =>
array( // default style for query parameters
82: 'presenter' =>
array(
94: '?presenter' =>
array(
106: /** @var string regular expression pattern */
109: /** @var array of [value & fixity, filterIn, filterOut] */
110: private $metadata =
array();
115: /** @var int HOST, PATH, RELATIVE */
124: * @param string URL mask, e.g. '<presenter>/<action>/<id \d{1,3}>'
125: * @param array default values or metadata
128: public function __construct($mask, array $metadata =
array(), $flags =
0)
130: $this->flags =
$flags |
self::$defaultFlags;
131: if (!($this->flags & self::FULL_META)) {
132: foreach ($metadata as $name =>
$def) {
133: $metadata[$name] =
array(self::VALUE =>
$def);
136: $this->setMask($mask, $metadata);
142: * Maps HTTP request to a PresenterRequest object.
143: * @param IHttpRequest
144: * @return PresenterRequest|NULL
146: public function match(IHttpRequest $httpRequest)
148: // combine with precedence: mask (params in URL-path), fixity, query, (post,) defaults
151: $uri =
$httpRequest->getUri();
153: if ($this->type ===
self::HOST) {
154: $path =
'//' .
$uri->getHost() .
$uri->getPath();
156: } elseif ($this->type ===
self::RELATIVE) {
157: $basePath =
$uri->getBasePath();
164: $path =
$uri->getPath();
172: // stop, not matched
176: // deletes numeric keys, restore '-' chars
178: foreach ($matches as $k =>
$v) {
185: // 2) CONSTANT FIXITY
186: foreach ($this->metadata as $name =>
$meta) {
187: if (isset($params[$name])) {
188: //$params[$name] = $this->flags & self::CASE_SENSITIVE === 0 ? strtolower($params[$name]) : */$params[$name]; // strtolower damages UTF-8
190: } elseif (isset($meta['fixity']) &&
$meta['fixity'] !==
self::OPTIONAL) {
191: $params[$name] =
NULL; // cannot be overwriten in 3) and detected by isset() in 4)
198: $params +=
self::renameKeys($httpRequest->getQuery(), array_flip($this->xlat));
200: $params +=
$httpRequest->getQuery();
204: // 4) APPLY FILTERS & FIXITY
205: foreach ($this->metadata as $name =>
$meta) {
206: if (isset($params[$name])) {
209: } elseif (isset($meta[self::FILTER_TABLE][$params[$name]])) { // applyies filterTable only to scalar parameters
210: $params[$name] =
$meta[self::FILTER_TABLE][$params[$name]];
212: } elseif (isset($meta[self::FILTER_IN])) { // applyies filterIn only to scalar parameters
214: if ($params[$name] ===
NULL &&
!isset($meta['fixity'])) {
215: return NULL; // rejected by filter
219: } elseif (isset($meta['fixity'])) {
220: $params[$name] =
$meta[self::VALUE];
225: // 5) BUILD PresenterRequest
226: if (!isset($params[self::PRESENTER_KEY])) {
229: if (isset($this->metadata[self::MODULE_KEY])) {
230: if (!isset($params[self::MODULE_KEY])) {
233: $presenter =
$params[self::MODULE_KEY] .
':' .
$params[self::PRESENTER_KEY];
234: unset($params[self::MODULE_KEY], $params[self::PRESENTER_KEY]);
237: $presenter =
$params[self::PRESENTER_KEY];
238: unset($params[self::PRESENTER_KEY]);
243: $httpRequest->getMethod(),
245: $httpRequest->getPost(),
246: $httpRequest->getFiles(),
254: * Constructs absolute URL from PresenterRequest object.
255: * @param IHttpRequest
256: * @param PresenterRequest
257: * @return string|NULL
259: public function constructUrl(PresenterRequest $appRequest, IHttpRequest $httpRequest)
261: if ($this->flags & self::ONE_WAY) {
265: $params =
$appRequest->getParams();
266: $metadata =
$this->metadata;
268: $presenter =
$appRequest->getPresenterName();
269: $params[self::PRESENTER_KEY] =
$presenter;
271: if (isset($metadata[self::MODULE_KEY])) { // try split into module and [submodule:]presenter parts
272: $module =
$metadata[self::MODULE_KEY];
273: if (isset($module['fixity']) &&
strncasecmp($presenter, $module[self::VALUE] .
':', strlen($module[self::VALUE]) +
1) ===
0) {
279: $params[self::MODULE_KEY] =
substr($presenter, 0, $a);
280: $params[self::PRESENTER_KEY] =
substr($presenter, $a +
1);
284: foreach ($metadata as $name =>
$meta) {
285: if (!isset($params[$name])) continue; // retains NULL values
287: if (isset($meta['fixity'])) {
289: // remove default values; NULL values are retain
290: unset($params[$name]);
293: } elseif ($meta['fixity'] ===
self::CONSTANT) {
294: return NULL; // missing or wrong parameter '$name'
300: } elseif (isset($meta['filterTable2'][$params[$name]])) {
301: $params[$name] =
$meta['filterTable2'][$params[$name]];
303: } elseif (isset($meta[self::FILTER_OUT])) {
307: if (isset($meta[self::PATTERN]) &&
!preg_match($meta[self::PATTERN], $params[$name])) {
308: return NULL; // pattern not match
313: $sequence =
$this->sequence;
314: $brackets =
array();
319: $uri =
$sequence[$i] .
$uri;
320: if ($i ===
0) break;
323: $name =
$sequence[$i]; $i--
; // parameter name
325: if ($name ===
']') { // opening optional part
328: } elseif ($name[0] ===
'[') { // closing optional part
330: if ($required <
count($brackets) +
1) { // is this level optional?
331: if ($name !==
'[!') { // and not "required"-optional
338: } elseif ($name[0] ===
'?') { // "foo" parameter
341: } elseif (isset($params[$name]) &&
$params[$name] !=
'') { // intentionally ==
342: $required =
count($brackets); // make this level required
343: $uri =
$params[$name] .
$uri;
344: unset($params[$name]);
346: } elseif (isset($metadata[$name]['fixity'])) { // has default value?
347: $uri =
$metadata[$name]['defOut'] .
$uri;
350: return NULL; // missing parameter '$name'
355: // build query string
357: $params =
self::renameKeys($params, $this->xlat);
362: if ($query !=
'') $uri .=
'?' .
$query; // intentionally ==
365: if ($this->type ===
self::RELATIVE) {
366: $uri =
'//' .
$httpRequest->getUri()->getAuthority() .
$httpRequest->getUri()->getBasePath() .
$uri;
368: } elseif ($this->type ===
self::PATH) {
369: $uri =
'//' .
$httpRequest->getUri()->getAuthority() .
$uri;
373: return NULL; // TODO: implement counterpart in match() ?
376: $uri =
($this->flags & self::SECURED ?
'https:' :
'http:') .
$uri;
384: * Parse mask and array of default values; initializes object.
389: private function setMask($mask, array $metadata)
391: $this->mask =
$mask;
393: // detect '//host/path' vs. '/abs. path' vs. 'relative path'
394: if (substr($mask, 0, 2) ===
'//') {
395: $this->type =
self::HOST;
397: } elseif (substr($mask, 0, 1) ===
'/') {
398: $this->type =
self::PATH;
401: $this->type =
self::RELATIVE;
404: foreach ($metadata as $name =>
$meta) {
406: $metadata[$name]['fixity'] =
self::CONSTANT;
412: '/<([^># ]+) *([^>#]*)(#?[^>\[\]]*)>|(\[!?|\]|\s*\?.*)/', // <parameter-name [pattern] [#class]> or [ or ] or ?...
415: PREG_SPLIT_DELIM_CAPTURE
418: $this->xlat =
array();
421: // PARSE QUERY PART OF MASK
422: if (isset($parts[$i -
1]) &&
substr(ltrim($parts[$i -
1]), 0, 1) ===
'?') {
424: '/(?:([a-zA-Z0-9_.-]+)=)?<([^># ]+) *([^>#]*)(#?[^>]*)>/', // name=<parameter-name [pattern][#class]>
429: foreach ($matches as $match) {
430: list(, $param, $name, $pattern, $class) =
$match; // $pattern is not used
432: if ($class !==
'') {
433: if (!isset(self::$styles[$class])) {
434: throw new InvalidStateException("Parameter '$name' has '$class' flag, but Route::\$styles['$class'] is not set.");
436: $meta =
self::$styles[$class];
438: } elseif (isset(self::$styles['?' .
$name])) {
439: $meta =
self::$styles['?' .
$name];
442: $meta =
self::$styles['?#'];
445: if (isset($metadata[$name])) {
446: $meta =
$metadata[$name] +
$meta;
449: if (array_key_exists(self::VALUE, $meta)) {
450: $meta['fixity'] =
self::OPTIONAL;
453: unset($meta['pattern']);
454: $meta['filterTable2'] =
empty($meta[self::FILTER_TABLE]) ?
NULL :
array_flip($meta[self::FILTER_TABLE]);
456: $metadata[$name] =
$meta;
457: if ($param !==
'') {
458: $this->xlat[$name] =
$param;
464: $brackets =
0; // optional level
466: $sequence =
array();
467: $autoOptional =
array(0, 0); // strlen($re), count($sequence)
471: if ($i ===
0) break;
474: $part =
$parts[$i]; // [ or ]
475: if ($part ===
'[' ||
$part ===
']' ||
$part ===
'[!') {
476: $brackets +=
$part[0] ===
'[' ? -
1 :
1;
477: if ($brackets <
0) {
478: throw new InvalidArgumentException("Unexpected '$part' in mask '$mask'.");
481: $re =
($part[0] ===
'[' ?
'(?:' :
')?') .
$re;
486: $class =
$parts[$i]; $i--
; // validation class
487: $pattern =
trim($parts[$i]); $i--
; // validation condition (as regexp)
488: $name =
$parts[$i]; $i--
; // parameter name
491: if ($name[0] ===
'?') { // "foo" parameter
493: $sequence[1] =
substr($name, 1) .
$sequence[1];
497: // check name (limitation by regexp)
499: throw new InvalidArgumentException("Parameter name must be alphanumeric string due to limitations of PCRE, '$name' given.");
502: // pattern, condition & metadata
503: if ($class !==
'') {
504: if (!isset(self::$styles[$class])) {
505: throw new InvalidStateException("Parameter '$name' has '$class' flag, but Route::\$styles['$class'] is not set.");
507: $meta =
self::$styles[$class];
509: } elseif (isset(self::$styles[$name])) {
510: $meta =
self::$styles[$name];
513: $meta =
self::$styles['#'];
516: if (isset($metadata[$name])) {
517: $meta =
$metadata[$name] +
$meta;
520: if ($pattern ==
'' &&
isset($meta[self::PATTERN])) {
521: $pattern =
$meta[self::PATTERN];
524: $meta['filterTable2'] =
empty($meta[self::FILTER_TABLE]) ?
NULL :
array_flip($meta[self::FILTER_TABLE]);
526: if (isset($meta['filterTable2'][$meta[self::VALUE]])) {
527: $meta['defOut'] =
$meta['filterTable2'][$meta[self::VALUE]];
529: } elseif (isset($meta[self::FILTER_OUT])) {
533: $meta['defOut'] =
$meta[self::VALUE];
536: $meta[self::PATTERN] =
"#(?:$pattern)$#A" .
($this->flags & self::CASE_SENSITIVE ?
'' :
'i');
538: // include in expression
539: $re =
'(?P<' .
str_replace('-', '___', $name) .
'>' .
$pattern .
')' .
$re; // str_replace is dirty trick to enable '-' in parameter name
540: if ($brackets) { // is in brackets?
541: if (!isset($meta[self::VALUE])) {
542: $meta[self::VALUE] =
$meta['defOut'] =
NULL;
544: $meta['fixity'] =
self::PATH_OPTIONAL;
546: } elseif (isset($meta['fixity'])) { // auto-optional
550: $meta['fixity'] =
self::PATH_OPTIONAL;
556: $metadata[$name] =
$meta;
560: throw new InvalidArgumentException("Missing closing ']' in mask '$mask'.");
563: $this->re =
'#' .
$re .
'/?$#A' .
($this->flags & self::CASE_SENSITIVE ?
'' :
'i');
564: $this->metadata =
$metadata;
565: $this->sequence =
$sequence;
582: * Returns default values.
587: $defaults =
array();
588: foreach ($this->metadata as $name =>
$meta) {
589: if (isset($meta['fixity'])) {
590: $defaults[$name] =
$meta[self::VALUE];
598: /********************* Utilities ****************d*g**/
603: * Proprietary cache aim.
604: * @return string|FALSE
608: if ($this->flags & self::ONE_WAY) {
612: $m =
$this->metadata;
615: if (isset($m[self::MODULE_KEY])) {
616: if (isset($m[self::MODULE_KEY]['fixity']) &&
$m[self::MODULE_KEY]['fixity'] ===
self::CONSTANT) {
617: $module =
$m[self::MODULE_KEY][self::VALUE] .
':';
623: if (isset($m[self::PRESENTER_KEY]['fixity']) &&
$m[self::PRESENTER_KEY]['fixity'] ===
self::CONSTANT) {
624: return $module .
$m[self::PRESENTER_KEY][self::VALUE];
632: * Rename keys in array.
637: private static function renameKeys($arr, $xlat)
639: if (empty($xlat)) return $arr;
643: foreach ($arr as $k =>
$v) {
644: if (isset($xlat[$k])) {
645: $res[$xlat[$k]] =
$v;
647: } elseif (!isset($occupied[$k])) {
656: /********************* Inflectors ****************d*g**/
661: * camelCaseAction name -> dash-separated.
665: private static function action2path($s)
676: * dash-separated -> camelCaseAction name.
680: private static function path2action($s)
685: //$s = lcfirst(ucwords($s));
693: * PascalCase:Presenter name -> dash-and-dot-separated.
697: private static function presenter2path($s)
709: * dash-and-dot-separated -> PascalCase:Presenter name.
713: private static function path2presenter($s)
725: /********************* Route::$styles manipulator ****************d*g**/
730: * Creates new style.
731: * @param string style name (#style, urlParameter, ?queryParameter)
732: * @param string optional parent style name
737: if (isset(self::$styles[$style])) {
738: throw new InvalidArgumentException("Style '$style' already exists.");
741: if ($parent !==
NULL) {
742: if (!isset(self::$styles[$parent])) {
743: throw new InvalidArgumentException("Parent style '$parent' doesn't exist.");
745: self::$styles[$style] =
self::$styles[$parent];
748: self::$styles[$style] =
array();
755: * Changes style property value.
756: * @param string style name (#style, urlParameter, ?queryParameter)
757: * @param string property name (Route::PATTERN, Route::FILTER_IN, Route::FILTER_OUT, Route::FILTER_TABLE)
758: * @param mixed property value
761: public static function setStyleProperty($style, $key, $value)
763: if (!isset(self::$styles[$style])) {
764: throw new InvalidArgumentException("Style '$style' doesn't exist.");
766: self::$styles[$style][$key] =
$value;