no message
This commit is contained in:
+576
@@ -0,0 +1,576 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Routing;
|
||||
|
||||
use Nette;
|
||||
use Nette\Utils\Strings;
|
||||
|
||||
|
||||
/**
|
||||
* The bidirectional route is responsible for mapping
|
||||
* HTTP request to an array for dispatch and vice-versa.
|
||||
*/
|
||||
class Route implements Router
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** key used in metadata {@link Route::__construct} */
|
||||
public const
|
||||
VALUE = 'value',
|
||||
PATTERN = 'pattern',
|
||||
FILTER_IN = 'filterIn',
|
||||
FILTER_OUT = 'filterOut',
|
||||
FILTER_TABLE = 'filterTable',
|
||||
FILTER_STRICT = 'filterStrict';
|
||||
|
||||
/** key used in metadata */
|
||||
private const
|
||||
DEFAULT = 'defOut',
|
||||
FIXITY = 'fixity',
|
||||
FILTER_TABLE_OUT = 'filterTO';
|
||||
|
||||
/** url type */
|
||||
private const
|
||||
HOST = 1,
|
||||
PATH = 2,
|
||||
RELATIVE = 3;
|
||||
|
||||
/** fixity types - how to handle default value? {@link Route::$metadata} */
|
||||
private const
|
||||
OPTIONAL = 0,
|
||||
PATH_OPTIONAL = 1,
|
||||
CONSTANT = 2;
|
||||
|
||||
/** @var array */
|
||||
protected $defaultMeta = [
|
||||
'#' => [ // default style for path parameters
|
||||
self::PATTERN => '[^/]+',
|
||||
self::FILTER_OUT => [__CLASS__, 'param2path'],
|
||||
],
|
||||
];
|
||||
|
||||
/** @var string */
|
||||
private $mask;
|
||||
|
||||
/** @var array */
|
||||
private $sequence;
|
||||
|
||||
/** @var string regular expression pattern */
|
||||
private $re;
|
||||
|
||||
/** @var string[] parameter aliases in regular expression */
|
||||
private $aliases;
|
||||
|
||||
/** @var array of [value & fixity, filterIn, filterOut] */
|
||||
private $metadata = [];
|
||||
|
||||
/** @var array */
|
||||
private $xlat;
|
||||
|
||||
/** @var int HOST, PATH, RELATIVE */
|
||||
private $type;
|
||||
|
||||
/** @var string http | https */
|
||||
private $scheme;
|
||||
|
||||
|
||||
/**
|
||||
* @param string $mask e.g. '<presenter>/<action>/<id \d{1,3}>'
|
||||
*/
|
||||
public function __construct(string $mask, array $metadata = [])
|
||||
{
|
||||
$this->setMask($mask, $metadata);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Maps HTTP request to an array.
|
||||
*/
|
||||
public function match(Nette\Http\IRequest $httpRequest): ?array
|
||||
{
|
||||
// combine with precedence: mask (params in URL-path), fixity, query, (post,) defaults
|
||||
|
||||
// 1) URL MASK
|
||||
$url = $httpRequest->getUrl();
|
||||
$re = $this->re;
|
||||
|
||||
if ($this->type === self::HOST) {
|
||||
$host = $url->getHost();
|
||||
$path = '//' . $host . $url->getPath();
|
||||
$parts = ip2long($host) ? [$host] : array_reverse(explode('.', $host));
|
||||
$re = strtr($re, [
|
||||
'/%basePath%/' => preg_quote($url->getBasePath(), '#'),
|
||||
'%tld%' => preg_quote($parts[0], '#'),
|
||||
'%domain%' => preg_quote(isset($parts[1]) ? "$parts[1].$parts[0]" : $parts[0], '#'),
|
||||
'%sld%' => preg_quote($parts[1] ?? '', '#'),
|
||||
'%host%' => preg_quote($host, '#'),
|
||||
]);
|
||||
|
||||
} elseif ($this->type === self::RELATIVE) {
|
||||
$basePath = $url->getBasePath();
|
||||
if (strncmp($url->getPath(), $basePath, strlen($basePath)) !== 0) {
|
||||
return null;
|
||||
}
|
||||
$path = substr($url->getPath(), strlen($basePath));
|
||||
|
||||
} else {
|
||||
$path = $url->getPath();
|
||||
}
|
||||
|
||||
if ($path !== '') {
|
||||
$path = rtrim(rawurldecode($path), '/') . '/';
|
||||
}
|
||||
|
||||
if (!$matches = Strings::match($path, $re)) {
|
||||
// stop, not matched
|
||||
return null;
|
||||
}
|
||||
|
||||
// assigns matched values to parameters
|
||||
$params = [];
|
||||
foreach ($matches as $k => $v) {
|
||||
if (is_string($k) && $v !== '') {
|
||||
$params[$this->aliases[$k]] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 2) CONSTANT FIXITY
|
||||
foreach ($this->metadata as $name => $meta) {
|
||||
if (!isset($params[$name]) && isset($meta[self::FIXITY]) && $meta[self::FIXITY] !== self::OPTIONAL) {
|
||||
$params[$name] = null; // cannot be overwriten in 3) and detected by isset() in 4)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 3) QUERY
|
||||
if ($this->xlat) {
|
||||
$params += self::renameKeys($httpRequest->getQuery(), array_flip($this->xlat));
|
||||
} else {
|
||||
$params += $httpRequest->getQuery();
|
||||
}
|
||||
|
||||
|
||||
// 4) APPLY FILTERS & FIXITY
|
||||
foreach ($this->metadata as $name => $meta) {
|
||||
if (isset($params[$name])) {
|
||||
if (!is_scalar($params[$name])) {
|
||||
// do nothing
|
||||
} elseif (isset($meta[self::FILTER_TABLE][$params[$name]])) { // applies filterTable only to scalar parameters
|
||||
$params[$name] = $meta[self::FILTER_TABLE][$params[$name]];
|
||||
|
||||
} elseif (isset($meta[self::FILTER_TABLE]) && !empty($meta[self::FILTER_STRICT])) {
|
||||
return null; // rejected by filterTable
|
||||
|
||||
} elseif (isset($meta[self::FILTER_IN])) { // applies filterIn only to scalar parameters
|
||||
$params[$name] = $meta[self::FILTER_IN]((string) $params[$name]);
|
||||
if ($params[$name] === null && !isset($meta[self::FIXITY])) {
|
||||
return null; // rejected by filter
|
||||
}
|
||||
}
|
||||
|
||||
} elseif (isset($meta[self::FIXITY])) {
|
||||
$params[$name] = $meta[self::VALUE];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->metadata[null][self::FILTER_IN])) {
|
||||
$params = $this->metadata[null][self::FILTER_IN]($params);
|
||||
if ($params === null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constructs absolute URL from array.
|
||||
*/
|
||||
public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string
|
||||
{
|
||||
$metadata = $this->metadata;
|
||||
|
||||
if (isset($metadata[null][self::FILTER_OUT])) {
|
||||
$params = $metadata[null][self::FILTER_OUT]($params);
|
||||
if ($params === null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($metadata as $name => $meta) {
|
||||
if (!isset($params[$name])) {
|
||||
continue; // retains null values
|
||||
}
|
||||
|
||||
if (is_scalar($params[$name])) {
|
||||
$params[$name] = $params[$name] === false ? '0' : (string) $params[$name];
|
||||
}
|
||||
|
||||
if (isset($meta[self::FIXITY])) {
|
||||
if ($params[$name] === $meta[self::VALUE]) { // remove default values; null values are retain
|
||||
unset($params[$name]);
|
||||
continue;
|
||||
|
||||
} elseif ($meta[self::FIXITY] === self::CONSTANT) {
|
||||
return null; // missing or wrong parameter '$name'
|
||||
}
|
||||
}
|
||||
|
||||
if (is_scalar($params[$name]) && isset($meta[self::FILTER_TABLE_OUT][$params[$name]])) {
|
||||
$params[$name] = $meta[self::FILTER_TABLE_OUT][$params[$name]];
|
||||
|
||||
} elseif (isset($meta[self::FILTER_TABLE_OUT]) && !empty($meta[self::FILTER_STRICT])) {
|
||||
return null;
|
||||
|
||||
} elseif (isset($meta[self::FILTER_OUT])) {
|
||||
$params[$name] = $meta[self::FILTER_OUT]($params[$name]);
|
||||
}
|
||||
|
||||
if (isset($meta[self::PATTERN]) && !preg_match("#(?:{$meta[self::PATTERN]})\\z#A", rawurldecode((string) $params[$name]))) {
|
||||
return null; // pattern not match
|
||||
}
|
||||
}
|
||||
|
||||
// compositing path
|
||||
$sequence = $this->sequence;
|
||||
$brackets = [];
|
||||
$required = null; // null for auto-optional
|
||||
$url = '';
|
||||
$i = count($sequence) - 1;
|
||||
do {
|
||||
$url = $sequence[$i] . $url;
|
||||
if ($i === 0) {
|
||||
break;
|
||||
}
|
||||
$i--;
|
||||
|
||||
$name = $sequence[$i--]; // parameter name
|
||||
|
||||
if ($name === ']') { // opening optional part
|
||||
$brackets[] = $url;
|
||||
|
||||
} elseif ($name[0] === '[') { // closing optional part
|
||||
$tmp = array_pop($brackets);
|
||||
if ($required < count($brackets) + 1) { // is this level optional?
|
||||
if ($name !== '[!') { // and not "required"-optional
|
||||
$url = $tmp;
|
||||
}
|
||||
} else {
|
||||
$required = count($brackets);
|
||||
}
|
||||
|
||||
} elseif ($name[0] === '?') { // "foo" parameter
|
||||
continue;
|
||||
|
||||
} elseif (isset($params[$name]) && $params[$name] != '') { // intentionally ==
|
||||
$required = count($brackets); // make this level required
|
||||
$url = $params[$name] . $url;
|
||||
unset($params[$name]);
|
||||
|
||||
} elseif (isset($metadata[$name][self::FIXITY])) { // has default value?
|
||||
if ($required === null && !$brackets) { // auto-optional
|
||||
$url = '';
|
||||
} else {
|
||||
$url = $metadata[$name][self::DEFAULT] . $url;
|
||||
}
|
||||
|
||||
} else {
|
||||
return null; // missing parameter '$name'
|
||||
}
|
||||
} while (true);
|
||||
|
||||
$scheme = $this->scheme ?: $refUrl->getScheme();
|
||||
|
||||
// absolutize path
|
||||
if ($this->type === self::RELATIVE) {
|
||||
$url = (($tmp = $refUrl->getAuthority()) ? "//$tmp" : '') . $refUrl->getBasePath() . $url;
|
||||
|
||||
} elseif ($this->type === self::PATH) {
|
||||
$url = (($tmp = $refUrl->getAuthority()) ? "//$tmp" : '') . $url;
|
||||
|
||||
} else {
|
||||
$host = $refUrl->getHost();
|
||||
$parts = ip2long($host) ? [$host] : array_reverse(explode('.', $host));
|
||||
$url = strtr($url, [
|
||||
'/%basePath%/' => $refUrl->getBasePath(),
|
||||
'%tld%' => $parts[0],
|
||||
'%domain%' => isset($parts[1]) ? "$parts[1].$parts[0]" : $parts[0],
|
||||
'%sld%' => $parts[1] ?? '',
|
||||
'%host%' => $host,
|
||||
]);
|
||||
}
|
||||
|
||||
$url = $scheme . ':' . $url;
|
||||
|
||||
// build query string
|
||||
if ($this->xlat) {
|
||||
$params = self::renameKeys($params, $this->xlat);
|
||||
}
|
||||
|
||||
$sep = ini_get('arg_separator.input');
|
||||
$query = http_build_query($params, '', $sep ? $sep[0] : '&');
|
||||
if ($query != '') { // intentionally ==
|
||||
$url .= '?' . $query;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse mask and array of default values; initializes object.
|
||||
*/
|
||||
private function setMask(string $mask, array $metadata): void
|
||||
{
|
||||
$this->mask = $mask;
|
||||
|
||||
// detect '//host/path' vs. '/abs. path' vs. 'relative path'
|
||||
if (preg_match('#(?:(https?):)?(//.*)#A', $mask, $m)) {
|
||||
$this->type = self::HOST;
|
||||
[, $this->scheme, $mask] = $m;
|
||||
|
||||
} elseif (substr($mask, 0, 1) === '/') {
|
||||
$this->type = self::PATH;
|
||||
|
||||
} else {
|
||||
$this->type = self::RELATIVE;
|
||||
}
|
||||
|
||||
foreach ($metadata as $name => $meta) {
|
||||
if (!is_array($meta)) {
|
||||
$metadata[$name] = $meta = [self::VALUE => $meta];
|
||||
}
|
||||
|
||||
if (array_key_exists(self::VALUE, $meta)) {
|
||||
if (is_scalar($meta[self::VALUE])) {
|
||||
$metadata[$name][self::VALUE] = $meta[self::VALUE] === false ? '0' : (string) $meta[self::VALUE];
|
||||
}
|
||||
$metadata[$name]['fixity'] = self::CONSTANT;
|
||||
}
|
||||
}
|
||||
|
||||
if (strpbrk($mask, '?<>[]') === false) {
|
||||
$this->re = '#' . preg_quote($mask, '#') . '/?\z#A';
|
||||
$this->sequence = [$mask];
|
||||
$this->metadata = $metadata;
|
||||
return;
|
||||
}
|
||||
|
||||
// PARSE MASK
|
||||
// <parameter-name[=default] [pattern]> or [ or ] or ?...
|
||||
$parts = Strings::split($mask, '/<([^<>= ]+)(=[^<> ]*)? *([^<>]*)>|(\[!?|\]|\s*\?.*)/');
|
||||
|
||||
$this->xlat = [];
|
||||
$i = count($parts) - 1;
|
||||
|
||||
// PARSE QUERY PART OF MASK
|
||||
if (isset($parts[$i - 1]) && substr(ltrim($parts[$i - 1]), 0, 1) === '?') {
|
||||
// name=<parameter-name [pattern]>
|
||||
$matches = Strings::matchAll($parts[$i - 1], '/(?:([a-zA-Z0-9_.-]+)=)?<([^> ]+) *([^>]*)>/');
|
||||
|
||||
foreach ($matches as [, $param, $name, $pattern]) { // $pattern is not used
|
||||
$meta = ($metadata[$name] ?? []) + ($this->defaultMeta['?' . $name] ?? []);
|
||||
|
||||
if (array_key_exists(self::VALUE, $meta)) {
|
||||
$meta[self::FIXITY] = self::OPTIONAL;
|
||||
}
|
||||
|
||||
unset($meta[self::PATTERN]);
|
||||
$meta[self::FILTER_TABLE_OUT] = empty($meta[self::FILTER_TABLE]) ? null : array_flip($meta[self::FILTER_TABLE]);
|
||||
|
||||
$metadata[$name] = $meta;
|
||||
if ($param !== '') {
|
||||
$this->xlat[$name] = $param;
|
||||
}
|
||||
}
|
||||
$i -= 5;
|
||||
}
|
||||
|
||||
// PARSE PATH PART OF MASK
|
||||
$brackets = 0; // optional level
|
||||
$re = '';
|
||||
$sequence = [];
|
||||
$autoOptional = true;
|
||||
$aliases = [];
|
||||
do {
|
||||
$part = $parts[$i]; // part of path
|
||||
if (strpbrk($part, '<>') !== false) {
|
||||
throw new Nette\InvalidArgumentException("Unexpected '$part' in mask '$mask'.");
|
||||
}
|
||||
array_unshift($sequence, $part);
|
||||
$re = preg_quote($part, '#') . $re;
|
||||
if ($i === 0) {
|
||||
break;
|
||||
}
|
||||
$i--;
|
||||
|
||||
$part = $parts[$i]; // [ or ]
|
||||
if ($part === '[' || $part === ']' || $part === '[!') {
|
||||
$brackets += $part[0] === '[' ? -1 : 1;
|
||||
if ($brackets < 0) {
|
||||
throw new Nette\InvalidArgumentException("Unexpected '$part' in mask '$mask'.");
|
||||
}
|
||||
array_unshift($sequence, $part);
|
||||
$re = ($part[0] === '[' ? '(?:' : ')?') . $re;
|
||||
$i -= 4;
|
||||
continue;
|
||||
}
|
||||
|
||||
$pattern = trim($parts[$i--]); // validation condition (as regexp)
|
||||
$default = $parts[$i--]; // default value
|
||||
$name = $parts[$i--]; // parameter name
|
||||
array_unshift($sequence, $name);
|
||||
|
||||
if ($name[0] === '?') { // "foo" parameter
|
||||
$name = substr($name, 1);
|
||||
$re = $pattern ? '(?:' . preg_quote($name, '#') . "|$pattern)$re" : preg_quote($name, '#') . $re;
|
||||
$sequence[1] = $name . $sequence[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
// pattern, condition & metadata
|
||||
$meta = ($metadata[$name] ?? []) + ($this->defaultMeta[$name] ?? $this->defaultMeta['#']);
|
||||
|
||||
if ($pattern == '' && isset($meta[self::PATTERN])) {
|
||||
$pattern = $meta[self::PATTERN];
|
||||
}
|
||||
|
||||
if ($default !== '') {
|
||||
$meta[self::VALUE] = substr($default, 1);
|
||||
$meta[self::FIXITY] = self::PATH_OPTIONAL;
|
||||
}
|
||||
|
||||
$meta[self::FILTER_TABLE_OUT] = empty($meta[self::FILTER_TABLE]) ? null : array_flip($meta[self::FILTER_TABLE]);
|
||||
if (array_key_exists(self::VALUE, $meta)) {
|
||||
if (isset($meta[self::FILTER_TABLE_OUT][$meta[self::VALUE]])) {
|
||||
$meta[self::DEFAULT] = $meta[self::FILTER_TABLE_OUT][$meta[self::VALUE]];
|
||||
|
||||
} elseif (isset($meta[self::VALUE], $meta[self::FILTER_OUT])) {
|
||||
$meta[self::DEFAULT] = $meta[self::FILTER_OUT]($meta[self::VALUE]);
|
||||
|
||||
} else {
|
||||
$meta[self::DEFAULT] = $meta[self::VALUE];
|
||||
}
|
||||
}
|
||||
$meta[self::PATTERN] = $pattern;
|
||||
|
||||
// include in expression
|
||||
$aliases['p' . $i] = $name;
|
||||
$re = '(?P<p' . $i . '>(?U)' . $pattern . ')' . $re;
|
||||
if ($brackets) { // is in brackets?
|
||||
if (!isset($meta[self::VALUE])) {
|
||||
$meta[self::VALUE] = $meta[self::DEFAULT] = null;
|
||||
}
|
||||
$meta[self::FIXITY] = self::PATH_OPTIONAL;
|
||||
|
||||
} elseif (isset($meta[self::FIXITY])) {
|
||||
if ($autoOptional) {
|
||||
$re = '(?:' . $re . ')?';
|
||||
}
|
||||
$meta[self::FIXITY] = self::PATH_OPTIONAL;
|
||||
|
||||
} else {
|
||||
$autoOptional = false;
|
||||
}
|
||||
|
||||
$metadata[$name] = $meta;
|
||||
} while (true);
|
||||
|
||||
if ($brackets) {
|
||||
throw new Nette\InvalidArgumentException("Missing '[' in mask '$mask'.");
|
||||
}
|
||||
|
||||
$this->aliases = $aliases;
|
||||
$this->re = '#' . $re . '/?\z#A';
|
||||
$this->metadata = $metadata;
|
||||
$this->sequence = $sequence;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns mask.
|
||||
*/
|
||||
public function getMask(): string
|
||||
{
|
||||
return $this->mask;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns default values.
|
||||
*/
|
||||
public function getDefaults(): array
|
||||
{
|
||||
$defaults = [];
|
||||
foreach ($this->metadata as $name => $meta) {
|
||||
if (isset($meta[self::FIXITY])) {
|
||||
$defaults[$name] = $meta[self::VALUE];
|
||||
}
|
||||
}
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
protected function getMetadata(): array
|
||||
{
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
public function getConstantParameters(): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($this->metadata as $name => $meta) {
|
||||
if (isset($meta[self::FIXITY]) && $meta[self::FIXITY] === self::CONSTANT) {
|
||||
$res[$name] = $meta[self::VALUE];
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/********************* Utilities ****************d*g**/
|
||||
|
||||
|
||||
/**
|
||||
* Rename keys in array.
|
||||
*/
|
||||
private static function renameKeys(array $arr, array $xlat): array
|
||||
{
|
||||
if (empty($xlat)) {
|
||||
return $arr;
|
||||
}
|
||||
|
||||
$res = [];
|
||||
$occupied = array_flip($xlat);
|
||||
foreach ($arr as $k => $v) {
|
||||
if (isset($xlat[$k])) {
|
||||
$res[$xlat[$k]] = $v;
|
||||
|
||||
} elseif (!isset($occupied[$k])) {
|
||||
$res[$k] = $v;
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Url encode.
|
||||
*/
|
||||
public static function param2path(string $s): string
|
||||
{
|
||||
return str_replace('%2F', '/', rawurlencode($s));
|
||||
}
|
||||
}
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Routing;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* The router broker.
|
||||
*/
|
||||
class RouteList implements Router
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var self|null */
|
||||
protected $parent;
|
||||
|
||||
/** @var array of [Router, flags] */
|
||||
private $list = [];
|
||||
|
||||
/** @var Router[][]|null */
|
||||
private $ranks;
|
||||
|
||||
/** @var string */
|
||||
private $cacheKey;
|
||||
|
||||
/** @var string|null */
|
||||
private $domain;
|
||||
|
||||
/** @var string|null */
|
||||
private $path;
|
||||
|
||||
/** @var \SplObjectStorage|null */
|
||||
private $refUrlCache;
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Maps HTTP request to an array.
|
||||
*/
|
||||
public function match(Nette\Http\IRequest $httpRequest): ?array
|
||||
{
|
||||
if ($this->domain) {
|
||||
$host = $httpRequest->getUrl()->getHost();
|
||||
if ($host !== $this->expandDomain($host)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->path) {
|
||||
$url = $httpRequest->getUrl();
|
||||
if (strncmp($url->getRelativePath(), $this->path, strlen($this->path))) {
|
||||
return null;
|
||||
}
|
||||
$url = $url->withPath($url->getPath(), $url->getBasePath() . $this->path);
|
||||
$httpRequest = $httpRequest->withUrl($url);
|
||||
}
|
||||
|
||||
foreach ($this->list as [$router]) {
|
||||
$params = $router->match($httpRequest);
|
||||
if ($params !== null) {
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constructs absolute URL from array.
|
||||
*/
|
||||
public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string
|
||||
{
|
||||
if ($this->domain) {
|
||||
if (!isset($this->refUrlCache[$refUrl])) {
|
||||
$this->refUrlCache[$refUrl] = $refUrl->withHost(
|
||||
$this->expandDomain($refUrl->getHost())
|
||||
);
|
||||
}
|
||||
$refUrl = $this->refUrlCache[$refUrl];
|
||||
}
|
||||
|
||||
if ($this->path) {
|
||||
if (!isset($this->refUrlCache[$refUrl])) {
|
||||
$this->refUrlCache[$refUrl] = $refUrl->withPath($refUrl->getBasePath() . $this->path);
|
||||
}
|
||||
$refUrl = $this->refUrlCache[$refUrl];
|
||||
}
|
||||
|
||||
if ($this->ranks === null) {
|
||||
$this->warmupCache();
|
||||
}
|
||||
|
||||
$key = $params[$this->cacheKey] ?? null;
|
||||
if (!is_scalar($key) || !isset($this->ranks[$key])) {
|
||||
$key = '*';
|
||||
}
|
||||
|
||||
foreach ($this->ranks[$key] as $router) {
|
||||
$url = $router->constructUrl($params, $refUrl);
|
||||
if ($url !== null) {
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public function warmupCache(): void
|
||||
{
|
||||
// find best key
|
||||
$candidates = [];
|
||||
$routers = [];
|
||||
foreach ($this->list as [$router, $flags]) {
|
||||
if ($flags & self::ONE_WAY) {
|
||||
continue;
|
||||
} elseif ($router instanceof self) {
|
||||
$router->warmupCache();
|
||||
}
|
||||
$params = $router instanceof Route
|
||||
? $router->getConstantParameters()
|
||||
: [];
|
||||
|
||||
foreach (array_filter($params, 'is_scalar') as $name => $value) {
|
||||
$candidates[$name][$value] = true;
|
||||
}
|
||||
$routers[] = [$router, $params];
|
||||
}
|
||||
|
||||
$this->cacheKey = $count = null;
|
||||
foreach ($candidates as $name => $items) {
|
||||
if (count($items) > $count) {
|
||||
$count = count($items);
|
||||
$this->cacheKey = $name;
|
||||
}
|
||||
}
|
||||
|
||||
// classify routers
|
||||
$ranks = ['*' => []];
|
||||
|
||||
foreach ($routers as [$router, $params]) {
|
||||
$value = $params[$this->cacheKey] ?? null;
|
||||
$values = $value === null
|
||||
? array_keys($ranks)
|
||||
: [is_scalar($value) ? $value : '*'];
|
||||
|
||||
foreach ($values as $value) {
|
||||
if (!isset($ranks[$value])) {
|
||||
$ranks[$value] = $ranks['*'];
|
||||
}
|
||||
$ranks[$value][] = $router;
|
||||
}
|
||||
}
|
||||
|
||||
$this->ranks = $ranks;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds a router.
|
||||
* @return static
|
||||
*/
|
||||
public function add(Router $router, int $flags = 0)
|
||||
{
|
||||
$this->list[] = [$router, $flags];
|
||||
$this->ranks = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Prepends a router.
|
||||
*/
|
||||
public function prepend(Router $router, int $flags = 0): void
|
||||
{
|
||||
array_splice($this->list, 0, 0, [[$router, $flags]]);
|
||||
$this->ranks = null;
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
protected function modify(int $index, ?Router $router): void
|
||||
{
|
||||
if (!isset($this->list[$index])) {
|
||||
throw new Nette\OutOfRangeException('Offset invalid or out of range');
|
||||
} elseif ($router) {
|
||||
$this->list[$index] = [$router, 0];
|
||||
} else {
|
||||
array_splice($this->list, $index, 1);
|
||||
}
|
||||
$this->ranks = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $mask e.g. '<presenter>/<action>/<id \d{1,3}>'
|
||||
* @param array $metadata default values or metadata
|
||||
* @return static
|
||||
*/
|
||||
public function addRoute(string $mask, $metadata = [], int $flags = 0)
|
||||
{
|
||||
$this->add(new Route($mask, $metadata), $flags);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an iterator over all routers.
|
||||
* @return static
|
||||
*/
|
||||
public function withDomain(string $domain)
|
||||
{
|
||||
$router = new static;
|
||||
$router->domain = $domain;
|
||||
$router->refUrlCache = new \SplObjectStorage;
|
||||
$router->parent = $this;
|
||||
$this->add($router);
|
||||
return $router;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public function withPath(string $path)
|
||||
{
|
||||
$router = new static;
|
||||
$router->path = rtrim($path, '/') . '/';
|
||||
$router->refUrlCache = new \SplObjectStorage;
|
||||
$router->parent = $this;
|
||||
$this->add($router);
|
||||
return $router;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public function end()
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Router[]
|
||||
*/
|
||||
public function getRouters(): array
|
||||
{
|
||||
return array_column($this->list, 0);
|
||||
}
|
||||
|
||||
|
||||
private function expandDomain(string $host): string
|
||||
{
|
||||
$parts = ip2long($host) ? [$host] : array_reverse(explode('.', $host));
|
||||
return strtr($this->domain, [
|
||||
'%tld%' => $parts[0],
|
||||
'%domain%' => isset($parts[1]) ? "$parts[1].$parts[0]" : $parts[0],
|
||||
'%sld%' => $parts[1] ?? '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Routing;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* The bi-directional router.
|
||||
*/
|
||||
interface Router
|
||||
{
|
||||
/** for back compatibility */
|
||||
public const ONE_WAY = 0b0001;
|
||||
|
||||
/**
|
||||
* Maps HTTP request to an array.
|
||||
*/
|
||||
function match(Nette\Http\IRequest $httpRequest): ?array;
|
||||
|
||||
/**
|
||||
* Constructs absolute URL from array.
|
||||
*/
|
||||
function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Routing;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* The bidirectional route for trivial routing via query parameters.
|
||||
*/
|
||||
class SimpleRouter implements Router
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var array */
|
||||
private $defaults;
|
||||
|
||||
|
||||
public function __construct(array $defaults = [])
|
||||
{
|
||||
$this->defaults = $defaults;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Maps HTTP request to an array.
|
||||
*/
|
||||
public function match(Nette\Http\IRequest $httpRequest): ?array
|
||||
{
|
||||
return $httpRequest->getUrl()->getPathInfo() === ''
|
||||
? $httpRequest->getQuery() + $this->defaults
|
||||
: null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constructs absolute URL from array.
|
||||
*/
|
||||
public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string
|
||||
{
|
||||
// remove default values; null values are retain
|
||||
foreach ($this->defaults as $key => $value) {
|
||||
if (isset($params[$key]) && $params[$key] == $value) { // intentionally ==
|
||||
unset($params[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$url = $refUrl->getHostUrl() . $refUrl->getPath();
|
||||
$sep = ini_get('arg_separator.input');
|
||||
$query = http_build_query($params, '', $sep ? $sep[0] : '&');
|
||||
if ($query != '') { // intentionally ==
|
||||
$url .= '?' . $query;
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns default values.
|
||||
*/
|
||||
public function getDefaults(): array
|
||||
{
|
||||
return $this->defaults;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user