no message

This commit is contained in:
BarboraFleg
2019-04-04 18:34:40 +02:00
commit ce48ca4a21
589 changed files with 82184 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
{
"name": "nette/routing",
"description": "Nette Routing: two-ways URL conversion",
"keywords": ["nette"],
"homepage": "https://nette.org",
"license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"require": {
"php": ">=7.1",
"nette/http": "^3.0",
"nette/utils": "^3.0"
},
"require-dev": {
"nette/tester": "^2.0",
"tracy/tracy": "^2.3"
},
"autoload": {
"classmap": ["src/"]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
}
}
+33
View File
@@ -0,0 +1,33 @@
How to contribute & use the issue tracker
=========================================
Nette welcomes your contributions. There are several ways to help out:
* Create an issue on GitHub, if you have found a bug
* Write test cases for open bug issues
* Write fixes for open bug/feature issues, preferably with test cases included
* Contribute to the [documentation](https://nette.org/en/writing)
Issues
------
Please **do not use the issue tracker to ask questions**. We will be happy to help you
on [Nette forum](https://forum.nette.org) or chat with us on [Gitter](https://gitter.im/nette/nette).
A good bug report shouldn't leave others needing to chase you up for more
information. Please try to be as detailed as possible in your report.
**Feature requests** are welcome. But take a moment to find out whether your idea
fits with the scope and aims of the project. It's up to *you* to make a strong
case to convince the project's developers of the merits of this feature.
Contributing
------------
If you'd like to contribute, please take a moment to read [the contributing guide](https://nette.org/en/contributing).
The best way to propose a feature is to discuss your ideas on [Nette forum](https://forum.nette.org) before implementing them.
Please do not fix whitespace, format code, or make a purely cosmetic patch.
Thanks! :heart:
+60
View File
@@ -0,0 +1,60 @@
Licenses
========
Good news! You may use Nette Framework under the terms of either
the New BSD License or the GNU General Public License (GPL) version 2 or 3.
The BSD License is recommended for most projects. It is easy to understand and it
places almost no restrictions on what you can do with the framework. If the GPL
fits better to your project, you can use the framework under this license.
You don't have to notify anyone which license you are using. You can freely
use Nette Framework in commercial projects as long as the copyright header
remains intact.
Please be advised that the name "Nette Framework" is a protected trademark and its
usage has some limitations. So please do not use word "Nette" in the name of your
project or top-level domain, and choose a name that stands on its own merits.
If your stuff is good, it will not take long to establish a reputation for yourselves.
New BSD License
---------------
Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com)
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of "Nette Framework" nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
This software is provided by the copyright holders and contributors "as is" and
any express or implied warranties, including, but not limited to, the implied
warranties of merchantability and fitness for a particular purpose are
disclaimed. In no event shall the copyright owner or contributors be liable for
any direct, indirect, incidental, special, exemplary, or consequential damages
(including, but not limited to, procurement of substitute goods or services;
loss of use, data, or profits; or business interruption) however caused and on
any theory of liability, whether in contract, strict liability, or tort
(including negligence or otherwise) arising in any way out of the use of this
software, even if advised of the possibility of such damage.
GNU General Public License
--------------------------
GPL licenses are very very long, so instead of including them here we offer
you URLs with full text:
- [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html)
- [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html)
+326
View File
@@ -0,0 +1,326 @@
Nette Routing: two-ways URL conversion
======================================
[![Downloads this Month](https://img.shields.io/packagist/dm/nette/routing.svg)](https://packagist.org/packages/nette/routing)
[![Build Status](https://travis-ci.org/nette/routing.svg?branch=master)](https://travis-ci.org/nette/routing)
[![Coverage Status](https://coveralls.io/repos/github/nette/routing/badge.svg?branch=master)](https://coveralls.io/github/nette/routing?branch=master)
Routing is a two-way conversion between URL and presenter action. *Two-way* means that we can both determine what presenter URL links to, but also vice versa: generate URL for given action.
SimpleRouter
------------
Desired URL format is set by a *router*. The most plain implementation of router is SimpleRouter. It can be used when there's no need for a specific URL format, or when `mod_rewrite` (or alternatives) is not available.
Addresses look like this:
```
http://example.com/index.php?presenter=Product&action=detail&id=123
```
The first parameter of `SimpleRouter` constructor is an array of default parameters.
```php
// defaults to controller 'Homepage' and action 'default'
$router = new Nette\Routing\SimpleRouter(['controller' => 'homepage', 'action' => 'default']);
```
The second constructor parameter is optional and is used to pass additional flags (only `Router::ONE_WAY` is supported).
Route: for prettier URLs
------------------------
Human-friendly URLs (also more cool & prettier) are easier to remember and do help SEO. Nette Framework keeps current trends in mind and fully meets developers' desires.
All requests must be handled by `index.php` file. This can be accomplished e.g. by using Apache module `mod_rewrite` or Nginx's try_files.
Class Route can create addresses in pretty much any format one can though of. Let's start with a simple example generating a pretty URL for action `Product:default` with `id = 123`:
```
http://example.com/product/detail/123
```
The following snippet creates a `Route` object, passing path mask as the first argument and specifying default action in the second argument.
```php
// action defaults to controller Homepage and action default
$route = new Route('<controller>/<action>[/<id>]', ['controller' => 'homepage', 'action' => 'default']);
```
This route is usable by all controllers and actions. Accepts paths such as `/article/edit/10` or `/catalog/list`, because the `id` part is wrapped in square brackets, which marks it as optional.
Because other parameters (`controller` and `action`) do have default values (`Homepage` and `default`), they are optional too. If their value is the same as the default one, they are skipped while URL is generated. Link to `Product:default` generates only `http://example.com/product` and link to `Homepage:default` generates only `http://example.com/`.
Path Mask
---------
The simplest path mask consists only of a **static URL** and a target action.
```php
$route = new Route('products', ...);
```
Most real masks however contain some **parameters**. Parameters are enclosed in angle brackets (e.g. `<year>`) and are passed to target presenter.
```php
$route = new Route('history/<year>', ...);
```
Mask can also contain traditional GET arguments (query after a question mark). Neither validation expressions nor more complex structures are supported in this part of path mask, but you can set what key belongs to which variable:
``` php
$route = new Route('<presenter>/<action> ? id=<productId> & cat=<categoryId>', ...);
```
The parameters before a question mark are called *path parameters* and the parameters after a question mark are called *query parameters*.
Mask can not only describe path relative to application document root (web root), but can as well contain path relative to server document root (starts with a single slash) or absolute path with domain (starts with a double slash).
```php
// relative to application document root (www directory)
$route = new Route('<presenter>/<action>', ...);
// relative to server document root
$route = new Route('/<presenter>/<action>', ...);
// absolute path including hostname
$route = new Route('//<subdomain>.example.com/<presenter>/<action>', ...);
```
Absolute path mask may utilize the following variables:
- `%tld%` = top level domain, e.g. `com` or `org`
- `%domain%` = second level domain, e.g. `example.com`
- `%basePath%`
```php
$route = new Route('//www.%domain%/%basePath%/<presenter>/<action>', ...);
```
Route Collection
----------------
Because we usually define more than one route, we wrap them into a RouteList.
```php
use Nette\Routing\RouteList;
use Nette\Routing\Route;
$router = new RouteList();
$router[] = new Route('rss.xml', ...);
$router[] = new Route('article/<id>', ...);
$router[] = new Route('<presenter>/<action>[/<id>]', ...);
```
Unlike other frameworks, Nette does not require routes to be named.
It's important in which order are the routes defined as they are tried from top to bottom. The rule of thumb here is that routes are declared from the **most specific at the top** to the **most vague at the bottom**. Keep in mind that huge amount of routes can negatively effect application speed, mostly when generating links. It's worth to keep routes as simple as possible.
Default Values
--------------
Each parameter may have defined a default value in the mask:
```php
$route = new Route('<presenter=Homepage>/<action=default>/<id=>');
```
Or utilizing an array:
```php
$route = new Route('<presenter>/<action>/<id>', [
'presenter' => 'Homepage',
'action' => 'default',
'id' => null,
]);
// equals to the following complex notation
$route = new Route('<presenter>/<action>/<id>', [
'presenter' => array(
Route::VALUE => 'Homepage',
),
'action' => array(
Route::VALUE => 'default',
),
'id' => array(
Route::VALUE => null,
),
]);
```
Validation Expressions
----------------------
Each parameter may have defined a **regular expression** which it needs to match. This regular expression is checked both when matching and generating URL. For example let's set `id` to be only numerical, using `\d+` regexp:
```php
// regexp can be defined directly in the path mask after parameter name
$route = new Route('<presenter>/<action>[/<id \d+>]', ...);
// equals to the following complex notation
$route = new Route('<presenter>/<action>[/<id>]', [
'presenter' => 'Homepage',
'action' => 'default',
'id' => [
Route::PATTERN => '\d+',
],
]);
```
Default validation expression for path parameters is `[^/]+`, meaning all characters but a slash. If a parameter is supposed to match a slash as well, we can set the regular expression to `.+`.
Regular expressions are *case-sensitive* by default.
Optional Sequences
------------------
Square brackets denote optional parts of mask. Any part of mask may be set as optional, including those containing parameters:
```php
$route = new Route('[<lang [a-z]{2}>/]<name>', ...);
// Accepted URLs: Parameters:
// /en/download lang => en, name => download
// /download lang => null, name => download
```
Obviously, if a parameter is inside an optional sequence, it's optional too and defaults to `null`. Sequence should define it's surroundings, in this case a slash which must follow a parameter, if set.
The technique may be used for example for optional language subdomains:
```php
$route = new Route('//[<lang=en>.]%domain%/<presenter>/<action>', ...);
```
Sequences may be freely nested and combined:
```php
$route = new Route(
'[<lang [a-z]{2}>[-<sublang>]/]<name>[/page-<page=0>]',
...
);
// Accepted URLs:
// /cs/hello
// /en-us/hello
// /hello
// /hello/page-12
```
URL generator tries to keep the URL as short as possible (while unique), so what can be omitted is not used. That's why `index[.html]` route generates `/index`. This behavior can be inverted by writing an exclamation mark after the leftmost square bracket that denotes the respective optional sequence:
```php
// accepts both /hello and /hello.html, generates /hello
$route = new Route('<name>[.html]');
// accepts both /hello and /hello.html, generates /hello.html
$route = new Route('<name>[!.html]');
```
Optional parameters (ie. parameters having default value) without square brackets do behave as if wrapped like this:
```php
$route = new Route('<presenter=Homepage>/<action=default>/<id=>');
// equals to:
$route = new Route('[<presenter=Homepage>/[<action=default>/[<id>]]]');
```
If we would like to change how the rightmost slashes are generated, that is instead of `/homepage/` get a `/homepage`, we can adjust the route:
```php
$route = new Route('[<presenter=Homepage>[/<action=default>[/<id>]]]');
```
Filters and Translation
-----------------------
It's a good practice to write source code in English, but what if you need your application to run in a different environment? Simple routes such as:
```php
$route = new Route('<presenter>/<action>/<id>', [
'presenter' => 'Homepage',
'action' => 'default',
'id' => null,
]);
```
will generate English URLs, such as `/product/detail/123`, `/cart` or `/catalog/view`. If we would like to translate those URLs, we can use a *dictionary* defined under `Route::FILTER_TABLE` key. We'd extend the route so:
```php
$route = new Route('<presenter>/<action>/<id>', [
'presenter' => [
Route::VALUE => 'Homepage', // default value
Route::FILTER_TABLE => [
// translated string in URL => presenter
'produkt' => 'Product',
'einkaufswagen' => 'Cart',
'katalog' => 'Catalog',
],
],
'action' => [
Route::VALUE => 'default',
Route::FILTER_TABLE => [
'sehen' => 'view',
],
],
'id' => null,
]);
```
Multiple keys under `Route::FILTER_TABLE` may have the same value. That's how aliases are created. The last value is the canonical one (used for generating links).
Dictionaries may be applied to any path parameter. If a translation is not found, the original (non-translated) value is used. The route by default accepts both translated (e.g. `/einkaufswagen`) and original (e.g. `/cart`) URLs. If you would like to accept only translated URLs, you need to add `Route::FILTER_STRICT => true` to the route definition.
Besides setting dictionaries as arrays, it's possible to set **input and output filters**. Input and output filter are callbacks defined under `Route::FILTER_IN` and `Route::FILTER_OUT` keys respectively.
```php
$route = new Route('<presenter=Homepage>/<action=default>', [
'action' => [
Route::FILTER_IN => function ($action) {
return strrev($action);
},
Route::FILTER_OUT => function ($action) {
return strrev($action);
},
],
]);
```
The **input filter** accepts value from URL and should return a value which will be passed to a presenter. The **output filter** accepts value from presenter and should return a value which will be used in URL. If any of those filters is unable to filter the value (usually because it is invalid), it should return `null`.
Besides filters for specific parameters, you can also define **global filters** which accepts an associative array with all parameters and returns an array with filtered parameters. Global filters are defined under `null` key.
```php
$route = new Route('<presenter>/<action>', [
'presenter' => 'Homepage',
'action' => 'default',
null => [
Route::FILTER_IN => function (array $params) {
// ...
return $params;
},
Route::FILTER_OUT => function (array $params) {
// ...
return $params;
},
],
]);
```
You can use *global filters* to filter certain parameter based on a value of another parameter, e.g. translate `<presenter>` and `<action>` based on `<lang>`.
+576
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+72
View File
@@ -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;
}
}