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 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 Application
51: /**#@+ key used in {@link NRoute::$styles} or metadata {@link NRoute::__construct} */
59: /**#@+ @ignore internal fixity types - how to handle default value? {@link NRoute::$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] */
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 NPresenterRequest object.
143: * @param IHttpRequest
144: * @return NPresenterRequest|NULL
146: public function match(IHttpRequest $httpRequest)
148: // combine with precedence: mask (params in URL-path), fixity, query, (post,) defaults
151: $uri =
$httpRequest->getUri();
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
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)
200: $params +=
$httpRequest->getQuery();
204: // 4) APPLY FILTERS & FIXITY
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 NPresenterRequest
226: if (!isset($params[self::PRESENTER_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 NPresenterRequest object.
255: * @param IHttpRequest
256: * @param NPresenterRequest
257: * @return string|NULL
259: public function constructUrl(NPresenterRequest $appRequest, IHttpRequest $httpRequest)
265: $params =
$appRequest->getParams();
268: $presenter =
$appRequest->getPresenterName();
269: if (isset($metadata[self::MODULE_KEY])) {
270: if (isset($metadata[self::MODULE_KEY]['fixity'])) {
271: $a =
strlen($metadata[self::MODULE_KEY][self::VALUE]);
273: return NULL; // module not match
278: $params[self::MODULE_KEY] =
substr($presenter, 0, $a);
279: $params[self::PRESENTER_KEY] =
substr($presenter, $a +
1);
281: $params[self::PRESENTER_KEY] =
$presenter;
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;
318: $uri =
$sequence[$i] .
$uri;
319: if ($i ===
0) break;
322: $name =
$sequence[$i]; $i--
; // parameter name
324: if ($name[0] ===
'?') { // "foo" parameter
327: } elseif (isset($params[$name]) &&
$params[$name] !=
'') { // intentionally ==
329: $uri =
$params[$name] .
$uri;
330: unset($params[$name]);
332: } elseif (isset($metadata[$name]['fixity'])) { // has default value?
336: } elseif ($metadata[$name][self::VALUE] ==
'') { // intentionally ==
337: if ($uri[0] ===
'/' &&
substr($sequence[$i], -
1) ===
'/') {
338: return NULL; // default value is empty but is required
342: $uri =
$metadata[$name]['defOut'] .
$uri;
346: return NULL; // missing parameter '$name'
351: // build query string
353: $params =
self::renameKeys($params, $this->xlat);
358: if ($query !=
'') $uri .=
'?' .
$query; // intentionally ==
361: if ($this->type ===
self::RELATIVE) {
362: $uri =
'//' .
$httpRequest->getUri()->getAuthority() .
$httpRequest->getUri()->getBasePath() .
$uri;
364: } elseif ($this->type ===
self::PATH) {
365: $uri =
'//' .
$httpRequest->getUri()->getAuthority() .
$uri;
368: $uri =
($this->flags & self::SECURED ?
'https:' :
'http:') .
$uri;
376: * Parse mask and array of default values; initializes object.
381: private function setMask($mask, array $metadata)
383: $this->mask =
$mask;
385: // detect '//host/path' vs. '/abs. path' vs. 'relative path'
386: if (substr($mask, 0, 2) ===
'//') {
389: } elseif (substr($mask, 0, 1) ===
'/') {
396: foreach ($metadata as $name =>
$meta) {
398: $metadata[$name]['fixity'] =
self::CONSTANT;
403: // 1) PARSE QUERY PART OF MASK
406: if ($pos !==
FALSE) {
408: '/(?:([a-zA-Z0-9_.-]+)=)?<([^># ]+) *([^>#]*)(#?[^>]*)>/', // name=<parameter-name [pattern][#class]>
415: foreach ($matches as $match) {
416: list(, $param, $name, $pattern, $class) =
$match; // $pattern is unsed
418: if ($class !==
'') {
419: if (!isset(self::$styles[$class])) {
420: throw new InvalidStateException("Parameter '$name' has '$class' flag, but NRoute::\$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 !==
'') {
450: // 2) PARSE URI-PATH PART OF MASK
452: '/<([^># ]+) *([^>#]*)(#?[^>]*)>/', // <parameter-name [pattern] [#class]>
455: PREG_SPLIT_DELIM_CAPTURE
459: $sequence =
array();
465: if ($i ===
0) break;
468: $class =
$parts[$i]; $i--
; // validation class
469: $pattern =
trim($parts[$i]); $i--
; // validation condition (as regexp)
470: $name =
$parts[$i]; $i--
; // parameter name
473: if ($name[0] ===
'?') { // "foo" parameter
475: $sequence[1] =
substr($name, 1) .
$sequence[1];
479: // check name (limitation by regexp)
481: throw new InvalidArgumentException("Parameter name must be alphanumeric string due to limitations of PCRE, '$name' given.");
484: // pattern, condition & metadata
485: if ($class !==
'') {
486: if (!isset(self::$styles[$class])) {
487: throw new InvalidStateException("Parameter '$name' has '$class' flag, but NRoute::\$styles['$class'] is not set.");
489: $meta =
self::$styles[$class];
491: } elseif (isset(self::$styles[$name])) {
492: $meta =
self::$styles[$name];
495: $meta =
self::$styles['#'];
498: if (isset($metadata[$name])) {
499: $meta =
$metadata[$name] +
$meta;
502: if ($pattern ==
'' &&
isset($meta[self::PATTERN])) {
503: $pattern =
$meta[self::PATTERN];
506: $meta['filterTable2'] =
empty($meta[self::FILTER_TABLE]) ?
NULL :
array_flip($meta[self::FILTER_TABLE]);
507: if (isset($meta[self::VALUE])) {
508: if (isset($meta['filterTable2'][$meta[self::VALUE]])) {
509: $meta['defOut'] =
$meta['filterTable2'][$meta[self::VALUE]];
511: } elseif (isset($meta[self::FILTER_OUT])) {
515: $meta['defOut'] =
$meta[self::VALUE];
518: $meta[self::PATTERN] =
"#(?:$pattern)$#A" .
($this->flags & self::CASE_SENSITIVE ?
'' :
'i');
519: $metadata[$name] =
$meta;
521: // include in expression
522: $tmp =
str_replace('-', '___', $name); // dirty trick to enable '-' in parameter name
523: if (isset($meta['fixity'])) { // has default value?
525: throw new InvalidArgumentException("Parameter '$name' must not be optional because parameters standing on the right side are not optional.");
527: $re =
'(?:(?P<' .
$tmp .
'>' .
$pattern .
')' .
$re .
')?';
528: $metadata[$name]['fixity'] =
self::PATH_OPTIONAL;
532: $re =
'(?P<' .
$tmp .
'>' .
$pattern .
')' .
$re;
536: $this->re =
'#' .
$re .
'/?$#A' .
($this->flags & self::CASE_SENSITIVE ?
'' :
'i');
538: $this->sequence =
$sequence;
555: * Returns default values.
560: $defaults =
array();
562: if (isset($meta['fixity'])) {
563: $defaults[$name] =
$meta[self::VALUE];
571: /********************* Utilities ****************d*g**/
576: * Proprietary cache aim.
577: * @return string|FALSE
588: if (isset($m[self::MODULE_KEY])) {
589: if (isset($m[self::MODULE_KEY]['fixity']) &&
$m[self::MODULE_KEY]['fixity'] ===
self::CONSTANT) {
590: $module =
$m[self::MODULE_KEY][self::VALUE] .
':';
596: if (isset($m[self::PRESENTER_KEY]['fixity']) &&
$m[self::PRESENTER_KEY]['fixity'] ===
self::CONSTANT) {
597: return $module .
$m[self::PRESENTER_KEY][self::VALUE];
605: * Rename keys in array.
610: private static function renameKeys($arr, $xlat)
612: if (empty($xlat)) return $arr;
616: foreach ($arr as $k =>
$v) {
617: if (isset($xlat[$k])) {
618: $res[$xlat[$k]] =
$v;
620: } elseif (!isset($occupied[$k])) {
629: /********************* Inflectors ****************d*g**/
634: * camelCaseAction name -> dash-separated.
638: private static function action2path($s)
649: * dash-separated -> camelCaseAction name.
653: private static function path2action($s)
658: //$s = lcfirst(ucwords($s));
666: * PascalCase:NPresenter name -> dash-and-dot-separated.
670: private static function presenter2path($s)
682: * dash-and-dot-separated -> PascalCase:NPresenter name.
686: private static function path2presenter($s)
698: /********************* NRoute::$styles manipulator ****************d*g**/
703: * Creates new style.
704: * @param string style name (#style, urlParameter, ?queryParameter)
705: * @param string optional parent style name
710: if (isset(self::$styles[$style])) {
711: throw new InvalidArgumentException("Style '$style' already exists.");
714: if ($parent !==
NULL) {
715: if (!isset(self::$styles[$parent])) {
716: throw new InvalidArgumentException("Parent style '$parent' doesn't exist.");
718: self::$styles[$style] =
self::$styles[$parent];
721: self::$styles[$style] =
array();
728: * Changes style property value.
729: * @param string style name (#style, urlParameter, ?queryParameter)
730: * @param string property name (NRoute::PATTERN, NRoute::FILTER_IN, NRoute::FILTER_OUT, NRoute::FILTER_TABLE)
731: * @param mixed property value
734: public static function setStyleProperty($style, $key, $value)
736: if (!isset(self::$styles[$style])) {
737: throw new InvalidArgumentException("Style '$style' doesn't exist.");
739: self::$styles[$style][$key] =
$value;