nette/routing

Nette Routing:双向URL转换

v3.1.0 2024-01-21 21:13 UTC

This package is auto-updated.

Last update: 2024-09-18 22:15:13 UTC


README

Nette Routing:双向URL转换

Downloads this Month Tests Coverage Status

简介

路由器负责所有与URL相关的事务,因此您不再需要考虑它们。我们将展示

  • 如何设置路由器,使URL看起来像您希望的那样
  • 关于SEO重定向的一些注意事项
  • 以及如何编写自己的路由器

需要PHP版本8.1,并支持PHP 8.4。

文档可以在网站上找到

支持我

您喜欢Nette Routing吗?您期待新功能吗?

Buy me a coffee

谢谢!

基础

更人性化的URL(或酷或美观的URL)更易用、更易记,并对SEO产生积极影响。Nette 框架牢记这一点,完全满足了开发者的需求。

让我们从技术上开始。路由器是一个实现了Nette\Routing\Router接口的对象,它可以分解URL为一个参数数组(方法match)和反过来,从一个参数数组构建一个URL(方法constructUrl)。因此,它也被说成是双向的。Nette 提供了一种非常优雅的方式来定义应用程序的URL看起来像什么。

您可以在完全不同的场景中使用它,例如 REST API、不使用控制器的应用程序等。

因此,路由是应用程序的一个独立而复杂的层,这使得在应用程序准备就绪时可以轻松设计或更改URL地址的外观,因为它可以在不修改代码或模板的情况下完成。这为开发者提供了巨大的自由度。

路由集合

在应用程序中定义URL地址最令人愉悦的方式是通过Nette\Routing\RouteList类。最大的优点是整个路由器在一个地方定义,而不是以注解的形式分散在所有控制器中。

定义由所谓的路由组成,即URL掩码及其关联的控制器和操作,使用简单的API。我们不必命名路由。

$router = new Nette\Routing\RouteList;
$router->addRoute('rss.xml', [
	'controller' => 'RssFeedController',
]);
$router->addRoute('article/<id \d+>', [
	'controller' => 'ArticleController',
]);
...

路由的顺序很重要,因为它们是按顺序从第一个到最后的顺序尝试的。基本规则是从最具体到最一般地声明路由

现在我们必须让路由器开始工作

$params = $router->match($httpRequest);
if ($params === null) {
	// no matching route found, we will send a 404 error
	exit;
}

// we process the received parameters
$controller = $params['controller'];
...

相反,我们将使用路由器来创建链接

$params = ['controller' => 'ArticleController', 'id' => 123];
$url = $router->constructUrl($params, $httpRequest->getUrl());

掩码和参数

掩码描述了基于网站根目录的相对路径。最简单的掩码是静态URL

$router->addRoute('products', ...);

掩码通常包含所谓的参数。它们被括号包围(例如<year>)。

$router->addRoute('chronicle/<year>', ...);

我们可以在掩码中直接指定参数的默认值,从而使它成为可选的

$router->addRoute('chronicle/<year=2020>', ...);

现在路由将接受URL https://any-domain.com/chronicle/,它将再次显示参数year: 2020

掩码不仅可以描述基于网站根目录的相对路径,还可以描述以斜杠开始的绝对路径,甚至以两个斜杠开始的整个绝对URL

// relative path to application document root
$router->addRoute('<controller>/<action>', ...);

// absolute path, relative to server hostname
$router->addRoute('/<controller>/<action>', ...);

// absolute URL including hostname (but scheme-relative)
$router->addRoute('//<lang>.example.com/<controller>/<action>', ...);

// absolute URL including schema
$router->addRoute('https://<lang>.example.com/<controller>/<action>', ...);

验证表达式

可以使用正则表达式为每个参数指定验证条件。例如,让我们将id设置为仅数值,使用\d+正则表达式

$router->addRoute('<controller>/<action>[/<id \d+>]', ...);

所有参数的默认正则表达式是 [^/]+,即除了斜杠之外的所有内容。如果参数应该匹配斜杠,我们将正则表达式设置为 .+

// accepts https://example.com/a/b/c, path is 'a/b/c'
$router->addRoute('<path .+>', ...);

可选序列

方括号表示掩码的可选部分。掩码的任何部分都可以设置为可选,包括包含参数的部分

$router->addRoute('[<lang [a-z]{2}>/]<name>', ...);

// Accepted URLs:      Parameters:
//   /en/download        lang => en, name => download
//   /download           lang => null, name => download

当然,当参数是可选序列的一部分时,它也变为可选。如果没有默认值,它将是 null。

可选部分也可以在域中

$router->addRoute('//[<lang=en>.]example.com/<controller>/<action>', ...);

序列可以自由嵌套和组合

$router->addRoute(
	'[<lang [a-z]{2}>[-<sublang>]/]<name>[/page-<page=0>]',
	...
);

// Accepted URLs:
//   /cs/hello
//   /en-us/hello
//   /hello
//   /hello/page-12

URL生成器尝试使URL尽可能短,因此可以省略的内容将被省略。因此,例如,路由 index[.html] 生成路径 /index。您可以通过在左方括号后写一个感叹号来反转此行为

// accepts both /hello and /hello.html, generates /hello
$router->addRoute('<name>[.html]', ...);

// accepts both /hello and /hello.html, generates /hello.html
$router->addRoute('<name>[!.html]', ...);

没有方括号的可选参数(即具有默认值的参数)的行为类似于这样包装

$router->addRoute('<controller=Homepage>/<action=default>/<id=>', ...);

// equals to:
$router->addRoute('[<controller=Homepage>/[<action=default>/[<id>]]]', ...);

要更改最右侧斜杠的生成方式,即从 /homepage/ 获取 /homepage,请按这种方式调整路由

$router->addRoute('[<controller=Homepage>[/<action=default>[/<id>]]]', ...);

通配符

在绝对路径掩码中,我们可以使用以下通配符来避免,例如,写入掩码中可能在不同开发和生产环境中不同的域

  • %tld% =顶级域名,例如 comorg
  • %sld% =二级域名,例如 example
  • %domain% =不带子域的域名,例如 example.com
  • %host% =整个主机,例如 www.example.com
  • %basePath% =根目录的路径
$router->addRoute('//www.%domain%/%basePath%/<controller>/<action>', ...);
$router->addRoute('//www.%sld%.%tld%/%basePath%/<controller>/<action', ...);

第二个参数

路由的第二个参数是各个参数默认值的数组

$router->addRoute('<controller>/<action>[/<id \d+>]', [
	'controller' => 'Homepage',
	'action' => 'default',
]);

或者我们可以使用这种形式,注意验证正则表达式的重写

use Nette\Routing\Route;

$router->addRoute('<controller>/<action>[/<id>]', [
	'controller' => [
		Route::Value => 'Homepage',
	],
	'action' => [
		Route::Value => 'default',
	],
	'id' => [
		Route::Pattern => '\d+',
	],
]);

这些更详细的形式对于添加其他元数据很有用。

过滤器与翻译

编写源代码时使用英语是一种好习惯,但如果您需要网站具有不同语言的翻译URL怎么办?简单的路由如下

$router->addRoute('<controller>/<action>', [...]);

将生成英语URL,如 /product/123/cart。如果我们希望URL中的控制器和操作翻译成德语(例如,/produkt/123/einkaufswagen),我们可以使用翻译字典。要添加它,我们需要第二个参数的“更详细”的变体

use Nette\Routing\Route;

$router->addRoute('<controller>/<action>', [
	'controller' => [
		Route::Value => 'Homepage',
		Route::FilterTable => [
			// string in URL => controller
			'produkt' => 'Product',
			'einkaufswagen' => 'Cart',
			'katalog' => 'Catalog',
		],
	],
	'action' => [
		Route::Value => 'default',
		Route::FilterTable => [
			'liste' => 'list',
		],
	],
]);

可以使用多个字典键为同一个控制器。它们将为其创建各种别名。最后一个键被认为是规范变体(即将包含在生成的URL中的那个)。

可以通过这种方式将翻译表应用于此处的任何参数。但是,如果不存在翻译,则取原始值。我们可以通过添加 Router::FILTER_STRICT => true 来更改此行为,然后路由将拒绝不包含在字典中的值。

除了数组形式的翻译字典外,还可以设置自己的翻译函数

use Nette\Routing\Route;

$router->addRoute('<controller>/<action>/<id>', [
	'controller' => [
		Route::Value => 'Homepage',
		Route::FilterIn => function (string $s): string { ... },
		Route::FilterOut => function (string $s): string { ... },
	],
	'action' => 'default',
	'id' => null,
]);

函数 Route::FILTER_IN 在URL中的参数和传递给控制器的字符串之间进行转换,然后函数 FILTER_OUT 确保反向转换。

全局过滤器

除了特定参数的过滤器之外,您还可以定义全局过滤器,它接收可以以任何方式修改的所有参数的关联数组,然后返回。全局过滤器在 null 键下定义。

use Nette\Routing\Route;

$router->addRoute('<controller>/<action>', [
	'controller' => 'Homepage',
	'action' => 'default',
	null => [
		Route::FilterIn => function (array $params): array { ... },
		Route::FilterOut => function (array $params): array { ... },
	],
]);

全局过滤器使您能够以任何方式调整路由的行为。例如,我们可以使用它们根据其他参数修改参数。例如,基于参数 <lang> 的当前值的翻译 <controller><action>

如果参数同时定义了自定义过滤器和一个全局过滤器,则先执行自定义的FILTER_IN,然后是全局的,反之亦然,全局的FILTER_OUT先于自定义执行。因此,在全局过滤器中,参数的值以PascalCase resp. camelCase风格写入controller resp. action

单向标志

单向路由用于保留应用程序不再生成但仍接受的旧URL的功能。我们用oneWay标记它们。

// old URL /product-info?id=123
$router->addRoute('product-info', [...], oneWay: true);
// new URL /product/123
$router->addRoute('product/<id>', [...]);

访问旧URL时,控制器会自动重定向到新URL,这样搜索引擎就不会对这些页面进行两次索引(见 [#SEO 和规范化])。

子域名

路由集合可以按子域名分组

$router = new RouteList;
$router->withDomain('example.com')
	->addRoute('rss', [...])
	->addRoute('<controller>/<action>');

您还可以在域名中使用 [#通配符]

$router = new RouteList;
$router->withDomain('example.%tld%')
	...

路径前缀

路由集合可以按URL中的路径分组

$router = new RouteList;
$router->withPath('/eshop')
	->addRoute('rss', [...]) // matches URL /eshop/rss
	->addRoute('<controller>/<action>'); // matches URL /eshop/<controller>/<action>

组合

上述用法可以组合使用

$router = (new RouteList)
	->withDomain('admin.example.com')
		->addRoute(...)
		->addRoute(...)
	->end()
	->withDomain('example.com')
		->withPath('export')
			->addRoute(...)
			...

查询参数

掩码还可以包含查询参数(URL中问号后面的参数)。它们不能定义验证表达式,但可以更改它们传递给控制器的名称

// use query parameter 'cat' as a 'categoryId' in application
$router->addRoute('product ? id=<productId> & cat=<categoryId>', ...);

Foo 参数

现在我们将更深入地探讨。Foo参数基本上是无名的参数,允许匹配正则表达式。以下路由匹配/index/index.html/index.htm/index.php

$router->addRoute('index<? \.html?|\.php|>', ...);

也可以显式定义一个用于URL生成的字符串。该字符串必须直接放置在问号之后。以下路由与上一个类似,但生成/index.html而不是/index,因为字符串.html被设置为“生成值”。

$router->addRoute('index<?.html \.html?|\.php|>', ...);

SimpleRouter

与路由集合相比,SimpleRouter要简单得多。SimpleRouter可以在不需要特定URL格式、没有mod_rewrite(或替代方案)或我们尚未打算使用用户友好的URL时使用。

生成地址的大致形式

http://example.com/?controller=Product&action=detail&id=123

SimpleRouter构造函数的参数是默认控制器 & 动作,即当我们打开例如http://example.com/而不带额外参数时将执行的动作。

$router = new Nette\Application\Routers\SimpleRouter();

SEO 和规范化

该框架通过防止在不同URL上重复内容来提高SEO(搜索引擎优化)。如果有多个地址链接到相同的目的地,例如/index/index.html,则框架确定第一个为主要的(规范化的),并使用HTTP代码301将其余的重定向到它。因此,搜索引擎不会对页面进行两次索引,也不会破坏它们的页面排名。

这个过程称为规范化。规范化的URL是路由器生成的URL,即集合中第一个匹配的路由,没有OneWay标志。因此,在集合中,我们首先列出主要路由

规范化由控制器执行,更多内容请参阅规范化章节。

HTTPS

为了使用HTTPS协议,需要在托管服务上激活它并配置服务器。

整个站点重定向到HTTPS必须在服务器级别执行,例如使用应用程序根目录中的.htaccess文件,并使用HTTP代码301。设置可能因托管服务而异,看起来像这样

<IfModule mod_rewrite.c>
	RewriteEngine On
	...
	RewriteCond %{HTTPS} off
	RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
	...
</IfModule>

路由器生成的URL与页面加载时使用的协议相同,因此不需要设置其他内容。

但是,如果我们例外地需要不同路由在不同的协议下运行,我们将它在路由掩码中设置

// Will generate an HTTP address
$router->addRoute('http://%host%/<controller>/<action>', ...);

// Will generate an HTTPS address
$router->addRoute('https://%host%/<controller>/<action>', ...);

路由调试器

我们不会隐瞒,一开始路由可能看起来有点神奇,在你深入之前,路由调试器将是一个很好的助手。这是一个在Tracy栏中显示的面板,它提供了一个清晰的路线列表以及路由器从URL中获得的参数。

带有✓符号的绿色栏表示与当前URL匹配的路线,带有≈符号的蓝色栏表示如果绿色没有超过它们,也会匹配URL的路线。我们还可以看到当前的控制器 & 操作。

自定义路由器

以下代码是为非常高级的用户准备的。你可以创建自己的路由器,并将其自然地添加到你的路线集合中。路由器是Router接口的实现,包含两个方法

use Nette\Http\IRequest as HttpRequest;
use Nette\Http\UrlScript;

class MyRouter implements Nette\Routing\Router
{
	public function match(HttpRequest $httpRequest): ?array
	{
		// ...
	}

	public function constructUrl(array $params, UrlScript $refUrl): ?string
	{
		// ...
	}
}

方法match处理当前请求(在$httpRequest参数中,[http-request-response#HTTP request](http-request-response#HTTP request)中提供了不仅仅是URL)到包含控制器名称及其参数的数组中。如果它无法处理请求,则返回null。

另一方面,方法constructUrl从参数数组生成一个绝对URL。它可以使用来自参数$refUrl的信息,即当前URL。

要将自定义路由器添加到路线集合中,请使用add()

$router = new Nette\Application\Routers\RouteList;
$router->add(new MyRouter);
$router->addRoute(...);
...