kunststube / router
一个灵活的路由/反向路由器
Requires
- php: >=5.3.0
This package is not auto-updated.
Last update: 2024-09-14 13:28:29 UTC
README
简介
Kunststube\Router 是一个简单且灵活的路由和反向路由器。不多也不少。
路由是应用程序的第一步。它决定了如何从 URL 到可执行代码。在 Kunststube\Router 中,这个过程有三个部分
- URL
- 分发器信息
- 分发器
Kunststube\Router 只处理前两个部分。它允许您指定规则将 URL 转换为分发器信息,这称为“路由”。它还允许您执行反向操作,根据您设置的规则将分发器信息转换为 URL。这称为“反向路由”。最后,它允许您挂钩任何想要的分发器,但它本身不处理分发。
使用示例
use Kunststube\Router\Router, Kunststube\Router\Route; require_once 'Kunststube/Router/Router.php'; $r = new Router; $r->add('/', array('controller' => 'static', 'action' => 'index')); $r->add('/user/profile/:name', array('controller' => 'users', 'action' => 'profile')); $r->add('/foo/bar', array(), function (Route $route) { header('Location: /bar/baz'); exit; }); $r->add('/:controller/:action/*'); $r->defaultCallback(function (Route $route) { require_once 'MyDispatcher.php'; $dispatcher = new Dispatcher; $dispatcher->dispatch($route); }); $r->route($_GET['url']);
您可以使用 Router::add
将路由添加到路由器。此方法接受三个参数
- 一个模式
- 一个分发器信息数组
- 一个分发器回调
模式是必填的,分发器信息和分发器回调是可选的。下面将详细介绍模式和分发器信息如何配合使用。如果没有为特定路由提供分发器回调,则将调用使用 Router::defaultCallback
设置的默认回调。回调将传递一个匹配的 Route
对象。实际的路由过程是通过 Route::route
启动的,该函数传递要路由的 URL。
模式
路由的第一个和定义性参数是模式。模式与 URL 进行匹配。以下语法是支持的
- 文本:
/foo
- 命名参数:
/:bar
- 命名参数与正则表达式:
/\d+:baz
- 跟随通配符:
/*
如果模式中的参数包含一个 :
,则它是一个命名参数,将被捕获为分发器信息(见下文)。名称在 :
之后指定。在 :
之前可以是正则表达式。没有正则表达式的命名参数基本上只是具有匹配任何内容的参数的正则表达式的简写;即 /:foo
是 /[^/]+:foo
的简写。
最后一个参数可能是 *
。这将匹配任何内容,允许您仅指定 URL 的初始部分。如果没有通配符,则模式将仅匹配长度相同的 URL。例如
/foo/:bar
将 不 匹配 URL/foo/bar/baz
/foo/:bar/*
将 匹配 URL/foo/bar/baz
不支持其他语法。正则表达式应保持简单,并且不得包含一个 /
,否则您可能会看到奇怪的结果。以下是一些您可能想要使用的典型表达式
- 任意数字:
\d+
- 任意“单词”:
\w+
- 以大写字母开头的“单词”:
[A-Z]\w*
- 后跟数字的“单词”:
\w+\d+
不支持未命名的正则表达式参数;每个动态匹配的参数都必须捕获为分发器信息。否则,反向路由的可靠性相当困难。
分发器信息数组
路由的第二部分是调度器信息。这为调度器设置了默认值。它是一个完全任意的任意信息数组,稍后将被传递给调度器。调度器可以决定如何处理这些信息。调度器信息没有预定义的结构。它可以包含关联的键值和数字索引值。
规范调度器信息
模式中的命名参数被认为是调度器信息的一部分。当一个URL匹配时,所有命名参数都将添加到调度器信息中。例如
-
路由
$r->add('/foo/:action/\d+:id', array('controller' => 'foos'));
-
URL
/foo/view/42
-
调度器接收
'controller' => 'foos', 'action' => 'view', 'id' => 42
模式中的任何命名参数都传递给调度器(action
和 id
)。由路由定义的默认调度器信息(controller => foos
)与命名参数合并。它们共同构成了路由的规范调度器信息(action
、id
和 controller => foos
)。规范调度器信息应被视为应用程序中某个控制器/操作的“主id”。这允许灵活的反向路由。例如,假设有一个这样的调度器
function (Route $route) { $className = ucfirst($route->controller) . 'Controller'; require_once "MyControllers/$className.php"; $controller = new $className; $controller->{$route->action}($route->id); }
$route
是从路由器传递的匹配的路由对象。上述调度器加载文件 FoosController.php
,实例化一个新的 FoosController
类,然后在该实例上调用方法 ->view(42)
。这显示了当请求任何URL /foo/(action)/(id)
时加载和执行 FoosController
中任何方法的简单方法。
注意:您无法阻止您在模式和调度器数组中定义没有参数的路由,从而导致空调度器信息。虽然这在为某些路由硬编码特定回调时可能很有用,但对于更复杂的路由/调度场景应避免使用。
反向路由
上述示例显示了如何从URL路由到特定文件中的特定类方法。通常,您希望在应用程序的某个地方输出链接,这些链接将再次指向此文件/类/方法。您可以通过硬编码所有链接来做到这一点
<a href="/foo/view/42">See foo number 42</a>
但这会使您的URL结构变得非常不灵活。您最终可能会决定将这些URL缩短为 /foos/42
,因为这看起来更好。改变路由以适应这一点非常容易
$r->add('/foo/\d+:id', array('controller' => 'foos', 'action' => 'view'));
上述路由将URL /foo/42
路由到相同的 'controller' => 'foos', 'action' => 'view', 'id' => 42
。但是,您的页面上仍然到处都是硬编码的URL /foo/view/42
。为了解决这个问题并保持URL结构的灵活性,请使用反向路由,它将规范调度器信息转换回URL
$url = $r->reverseRoute(array('controller' => 'foos', 'action' => 'view', 'id' => 42)); printf('<a href="%s">See foo number 42</a>', $url);
Router::reverseRoute
方法接受一个规范调度器信息数组,并基于您定义的第一个匹配的路由输出一个URL
$r = new Router; $r->add('/foo', array('controller' => 'foos', 'action' => 'index')); $r->add('/foo/:action', array('controller' => 'foos')); echo $r->reverseRoute(array('controller' => 'foos', 'action' => 'index')); // /foo echo $r->reverseRoute(array('controller' => 'foos', 'action' => 'bar')); // /foo/bar
反向路由允许您灵活地将控制器和操作(或您喜欢的任何其他范式和组织结构)与URL以及反之亦然绑定。规范调度器信息是中间人,它唯一地代表了两边相同的东西(因此称为“规范”)。您的定义路由通过 路由 将URL转换为调度器信息,并通过 反向路由 将调度器信息转换为URL。
在反向路由时,模式中的正则表达式将评估传递的调度器信息
$r->add('/\d+:id', array('controller' => 'foo', 'action' => 'bar')); $r->add('/foo/\w+:id', array('controller' => 'foo', 'action' => 'bar')); echo $r->reverseRoute(array('controller' => 'foo', 'action' => 'bar', 'id' => 42)); // /42 echo $r->reverseRoute(array('controller' => 'foo', 'action' => 'bar', 'id' => 'baz')); // /foo/baz
在上面的示例中,第一个路由不反向匹配 array('controller' => 'foo', 'action' => 'bar', 'id' => 'baz')
,因为 id
被定义为 \d+
,它不匹配 'baz'
。第二个路由匹配。
通配符参数
如果路由模式定义了一个尾随的 *
,它允许通配符参数,如上所述。这些参数可以是命名的,也可以是无名的。例如
$r->addRoute('/foo/*', array('controller' => 'foos')); $r->route('/foo/bar/baz:42')
生成的调度信息将简单地是 'controller' => 'foos'
,因为在路由中没有指定其他参数。调度器接收的通配符参数将是 'bar', 'baz' => 42
,或者技术上 array(0 => 'bar', 'baz' => 42)
。换句话说,以 name:value
表示法传递的值被分开处理,并作为关联键值对。
应避免命名参数和通配符参数之间的命名冲突。通过 $route->name
访问值始终优先考虑调度信息;如果设置了同名通配符参数,则必须通过 $route->wildcardArg('name')
访问。
在反向路由时,如果调度信息包含通配符参数,则只有当路由允许通配符参数时,路由才会匹配。
在反向路由时,调度信息与通配符参数之间没有区别,它们都在一个数组中指定。换句话说,无法使用冲突的调度/通配符参数进行反向路由。避免为不同的目的使用相同的名称。
$r->add('/foo', array('controller' => 'foos', 'action' => 'index')); $r->add('/foo/bar/*', array('controller' => 'foos', 'action' => 'index')); $r->reverseRoute(array('controller' => 'foos', 'action' => 'index')); // /foo $r->reverseRoute(array('controller' => 'foos', 'action' => 'index', 'baz' => '42')); // /foo/bar/baz:42
上面两个路由都具有相同的调度信息 'controller' => 'foos', 'action' => 'index'
,但只有一个允许通配符参数。当使用 array('controller' => 'foos', 'action' => 'index')
进行反向路由时,第一个路由匹配,并返回 /foo
。当使用额外的参数 'baz' => 42
进行反向路由时,第一个路由不匹配,但第二个匹配。
调度
Kunststube\Router 不进行调度,这完全由您来添加。Kunststube\Router 允许您指定一个回调,该回调将在匹配的路由上执行。回调可以以 PHP 所支持的任何语法实现,包括对象方法和匿名函数。如果需要,回调可以直接进入控制器动作。它还可以加载一个单独的调度器类,该类实现一些逻辑,根据通过路由器接收到的信息加载其他类。它还可以用于实现重定向。
将静态路由直接连接到类方法
$r->add('/foo', array(), function () { FooController::execute(); });
这也可以写成这样
$r->add('/foo', array(), 'FooController::execute');
重定向易于实现
$r->add('/foo', array(), function () { header('Location: /bar'); exit; });
您可以使用预处理逻辑链式调用您的调度器
function dispatch(Route $route) { $controller = $route->controller; require "$controller.php"; $controller::{$route->action}(); } $r->add('/foo/:action', array(), function (Route $route) { $route->controller = 'bar'; dispatch($route); }); $r->add('/:controller/:action', array(), 'dispatch');
上面的内容基本上将 /bar/...
别名到 /foo/...
。这只是为了展示回调的灵活性;它也可以简单地写成这样
$r->add('/foo/:action', array('controller' => 'bar'), 'dispatch'); $r->add('/:controller/:action', array(), 'dispatch');
为了避免在您的每个路由中传递相同的回调,您可以指定一个默认回调,并像这样编写上述代码
$r->add('/foo/:action', array('controller' => 'bar')); $r->add('/:controller/:action'); $r->defaultCallback('dispatch');
RESTful 路由(按请求方法路由)
为了实现更细粒度的路由,路由器允许为每个路由指定特定的 HTTP 请求方法。这可以通过以下 API 方法完成
addGet(string $pattern [, array $dispatch [, callable $callback ] ])
addPost(string $pattern [, array $dispatch [, callable $callback ] ])
addPut(string $pattern [, array $dispatch [, callable $callback ] ])
addDelete(string $pattern [, array $dispatch [, callable $callback ] ])
addMethod(int $method, string $pattern [, array $dispatch [, callable $callback ] ])
addMethodRoute(int $method, Route $route [, callable $callback ])
Router::addMethodRoute
方法是添加所有路由的标准方法,其他方法仅是它的便利包装。$method
是以下常量的位掩码
Router::GET
Router::POST
Router::PUT
Router::DELETE
Router::HEAD
Router::TRACE
Router::OPTIONS
Router::CONNECT
前四种 HTTP 请求方法有自己的便利包装(如 addGet
等),不太常见的 HEAD
、TRACE
、OPTIONS
和 CONNECT
可以使用 addMethod
和 addMethodRoute
方法。可以通过按位或组合几个方法。使用 Router::add
和 Router::addRoute
方法默认添加匹配 GET
、POST
、PUT
和 DELETE
请求的路由。示例
$r = new Router; // matches GET, POST, PUT and DELETE requests $r->add('/foo'); // matches only GET requests $r->addGet('/bar'); // matches the same URL as above, but only for POST requests $r->addPost('/bar'); // matches PUT and POST requests $r->addMethod($r::PUT | $r::POST, '/baz'); // custom route matching only HEAD requests $r->addMethodRoute($r::HEAD, new CaseInsensitiveRoute('/42'));
要启动特定请求方法的路由,请使用 Router::routeMethod
或 Router::routeMethodFromString
。前一种方法需要一个常量作为参数传递,而后一种方法接受请求方法作为字符串。
$r->routeMethod($r::POST, $_GET['url']); $r->routeMethodFromString('POST', $_GET['url']); $r->routeMethodFromString($_SERVER['REQUEST_METHOD'], $_GET['url']);
正常的 Router::route
方法将请求处理为 GET、POST、PUT 或 DELETE,并无法区分它们。即它是围绕 Router::routeMethod(Router::GET | Router::POST | Router::PUT | Router::DELETE, $url)
的便利包装。路由器本身不会检测当前请求方法,必须显式传递给路由方法之一。
请求方法由 Router
类处理,而不是由 Route
类处理(见下文)。传递给回调的 Route
对象不包含有关请求方法的信息。您应该在调度器数组中包含适当的参数或将请求方法传递给调度器。
路由匹配
路由按定义顺序从第一个路由到最后一个路由进行匹配。第一个匹配的路由将调用相关的回调(或默认回调)并停止路由过程。正确定义路由顺序非常重要。例如,这里定义的第二个路由将永远不会匹配,因为第一个路由匹配一切。
$r->add('/*'); $r->add('/foo');
这是一个强大的行为,但也很复杂。通常,您应该定义特定的、狭窄的路由,然后再定义广泛的路由。
如果没有路由匹配给定的 URL,将抛出 NotFoundException
。或者,您可以将回调作为 Router::route
的第二个参数传递,如果没有 URL 匹配,则调用该回调。
$r->route($_GET['url'], function ($url) { die("404: $url not found"); });
在这种情况下不会抛出 NotFoundException
。
这为您提供了处理不匹配的几种不同策略。您可以捕获抛出的异常
try { $r->route($_GET['url']); } catch (Kunststube\Router\NotFoundException $e) { die($e->getMessage()); }
这不推荐,因为异常代价高昂,并且 404 事件并不是真正的异常事件,但它可能与您的现有错误处理策略很好地配合。
通常更好的方法是像上面那样将回调传递给 route()
。最后,您还可以定义一个作为最后一个路由的通配符路由来处理它。
// define regular routes here... $r->add('/*', array(), 'ErrorHandler::handle404');
通配符路由的优点是 URL 将被解析,您的回调将接收到一个常规的 Route
对象。这不是传递给 route()
的回调的情况,这些回调只会接收到不匹配的 URL 作为字符串。
URL 是什么以及如何设置路由
URL 简单地说是一个由几个部分组成的字符串。
http://example.com/foo?bar=baz
| | | |
| | | +- query
| | +- path
| +- host
+- scheme
URL 还可能包含身份验证信息、端口和片段,但在这里我们会尽量保持简单。Kunststube\Router 仅处理路径。方案和查询通常对路由没有影响,而主机通常由 Web 服务器处理。
假设使用 Apache Web 服务器的典型设置,Apache 通常会为您执行“路由”。它接收一个看起来像这样的 HTTP 请求
GET /foo/bar/baz?some=parameters HTTP/1.1
Web 服务器现在可以自由地以任何方式响应该请求。大多数 Web 服务器默认做的事情是将 URL 的路径映射到硬盘上的文件。Web 服务器首先确定适当的 DocumentRoot,即配置为“公共 Web 文件夹”的磁盘上的文件夹。假设 DocumentRoot 是 /var/www
。然后它将请求路径连接到该根,结果为 /var/www/foo/bar/baz
。然后它将尝试确定该文件是否存在于磁盘上,并将其作为响应提供。如果请求的文件以 .php
结尾或 Apache 配置为将文件视为 PHP 文件,它将首先通过 PHP 解释器运行该文件,然后返回其输出。
要使用我们自己的自定义路由和PHP路由器,我们需要拦截Apache查找磁盘上要服务的文件的过程。这可以在Apache配置文件中完成;但如果您有权访问这些文件,我假设您知道自己在做什么,因此不会深入讨论最佳设置的具体细节。相反,我将介绍一种典型情况,即您无法或不想编辑核心Apache配置文件,而转而使用.htaccess
文件。当Apache遍历磁盘上的目录结构以查找要服务的正确文件时,它会检查每个目录中是否放置了名为.htaccess
的文件。如果找到,它将执行并/或将其中定义的规则合并到文件查找过程中,然后继续在路径中的下一个更深层的目录中查找。
您想要实现的是让Apache“找到”并执行特定PHP文件以处理所有请求,并使原始URL可用于该PHP文件,以便它执行自己的路由。最简单、最直接的方法是使用简单的RewriteRule
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^(.*)$ index.php?url=/$1 [QSA,L]
</IfModule>
假设您将其放入/var/www/.htaccess
文件中。当Apache从该目录开始文件查找时,它将解析这些重写规则。此时Apache正在查找的路径“内部状态”是foo/bar/baz
。RewriteRule
的正则表达式^(.*)$
将匹配该路径(该表达式基本上表示“匹配任何内容”),并将路径重写为index.php?url=/foo/bar/baz
。然后,原始的some=parameters
将被再次附加到该路径/URL上(由于QSA
标志)。然后Apache将继续查找现在已重写的路径index.php
。因此,只需将您的代码放入/var/www/index.php
,Apache将启动该PHP文件的PHP解释器。PHP将传递URL查询部分?url=/foo/bar/baz&some=parameters
,在PHP中可以分别通过$_GET['url']
和$_GET['some']
访问。因此,完整的设置如下所示
基本设置
文件/文件夹结构
/var
/www
.htaccess
index.php
/Kunststube
/Router
Router.php
...
.htaccess
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^(.*)$ index.php?url=/$1 [QSA,L]
</IfModule>
index.php
<?php require_once 'Kunststube/Router/Router.php'; $r = new Kunststube\Router\Router; $r->add('/foo'); ... $r->route($_GET['url']);
就是这样。一条基本的重写规则,将所有请求重定向到同一个PHP文件,并将原始URL作为查询参数附加,然后用于调用路由过程。
注意事项和调整
上述设置的一个重要注意事项是,您的URL中不能包含名为url
的查询参数,因为原始查询参数会被添加回重写的URL。URL /foo/bar?url=baz
将被重写为
index.php?url=/foo/bar&url=baz
第二个url
参数将替换第一个。如果您需要在应用程序中使用查询参数url
,请为您的重写规则选择不同的参数名称。
其次,请注意,Kunststube\Router期望传递给route()
的URL以/
开头。您可以在上面的重写过程中添加该斜杠,或者在PHP中添加,但请确保它存在。
第三,您通常还有不希望通过PHP路由的文件,例如CSS和图像文件。您希望这些文件由Apache直接服务。这种配置非常好
/var
/Kunststube
/Router
...
/MyApp
MyScript.php
...
/www
.htaccess
index.php
/css
style.css
...
/img
kitten.jpg
...
将所有实际的PHP文件从公共Web根目录中移除,只留下公共资产文件和最小的index.php
文件。调整您的RewriteRule如下
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?url=/$1 [QSA,L]
</IfModule>
RewriteCond
确保只有当请求的文件实际上不存在时(!-f
),RewriteRule
才生效。这意味着对URL css/style.css
的请求将按原样通过,因为该文件确实存在,Apache可以直接提供服务。任何对不存在文件的请求(即“虚拟”文件)都将进入index.php
并可以在那里进行路由。在index.php
内部,确保使用到正确的路由器路径
require_once '../Kunststube/Router/Router.php';
实际上,更好的做法是在完全不同的地方定义路由,并使用自动加载器加载所需文件,但这超出了本文档的范围。
集成
由于 Kunststube\Router 只处理路径,它只是您更大应用中的一小部分。如果您需要基于方案、主机或查询参数的逻辑,您必须单独处理它们。所有这些信息都可以通过 PHP 中的 $_SERVER
超全局变量访问。您可以根据主机名等自定义路由
$r = new Kunststube\Router\Router; switch ($_SERVER['HTTP_HOST']) { case 'example.com' : $r->add('/foo'); ... break; case 'elpmaxe.moc' : $r->add('/bar'); ... break; } $r->route($_GET['url']);
对于组装反向路由的 URL,建议创建一个包装函数,该函数将组装完整的 URL,并仅使用 Kunststube\Router 生成路径组件,但在此周围添加查询参数以及可能的主机和方案。
要将路由实例传递出去以供以后反向路由使用,有几种方法可以做到这一点,但我建议使用闭包。
$r = new Kunststube\Router\Router; $r->add('/foo', array(), function (Route $route) use ($r) { $controller = new MyController; $controller->run($r); });
这巧妙地将路由实例注入到您的调用堆栈中。全局变量、注册表、抽象整个路由过程的包装对象等也是您可以考虑的其他选项。
扩展
您可以修改和扩展 Kunststube\Router 的行为。最有意思的可能是在构造函数中将自定义的 RouteFactory
传递给 Router
。这里是一个使用 CaseInsensitiveRoute
的例子。
require_once 'Kunststube/Router/CaseInsensitiveRouteFactory.php'; $r = new Router(new CaseInsensitiveRouteFactory);
大部分的路由逻辑都位于 Route
对象中。它们负责解析和匹配 URL。如果不另外指定,Router
在您调用 Router::add
时使用 RouteFactory
创建新的 Route
对象。默认的 Route
对象在匹配时是严格区分大小写的。一个名为 CaseInsensitiveRoute
的 Route
类扩展匹配 URL 和模式,即使它们的大小写不同。
如果您不希望所有路由都不区分大小写,但只想让其中一些这样做,您可以自己创建一个 CaseInsensitiveRoute
并将其添加到路由链中。
require_once 'Kunststube/Router/CaseInsensitiveRoute.php'; $r = new Router; $r->add('/regular/case/sensitive/route'); $caseInsensitiveRoute = new CaseInsensitiveRoute('/case/insensitive/route'); $r->addRoute($caseInsensitiveRoute, function () { echo 'This will match'; }); $r->route('/Case/INSENSITIVE/rOuTe');
Route
类
将传递一个 Route
实例给分发回调。这样做的主要目的是使其能够访问匹配和解析的值。它们可以直接作为对象的属性访问。
function (Route $route) { echo $route->controller; echo $route->action; }
Route
对象也可以被操作并用于根据设置的图案生成新的 URL。例如
$r = new Route('/foo/:id'); $r->id = 42; echo $r->url(); // /foo/42
这创建了一个新的 Route
对象(通常在您调用 Router::add
时在幕后完成),然后设置缺失的占位符 id
为值 42
,然后从设置的值和模式生成 URL。值将根据模式严格验证;以下将抛出 InvalidArgumentException
$r = new Route('/foo/\d+:id'); $r->id = 'bar'; // invalid value for pattern \d+
支持相同的通配符参数,但只有当路由支持通配符参数时。
这主要用作为类似路由高效生成 URL 的方法。使用 Router::reverseRoute
,必须评估所有路由以找到匹配的路由来生成正确的 URL。如果您已经知道 URL 的模式并且只需要更改一个或两个值来重新生成 URL,则在此正确的 Route
对象上这样做会更高效。
$r = new Router; $r->add('/item/\d+:id', array(), function (Route $route) { echo "Now visiting item {$route->id}. "; $route->id = $route->id + 1; echo "The next item is at " . $route->url(); }); $r->route('/item/42'); // Now visiting item 42. The next item is at /item/43
请小心使用此功能,因为明确地 没有 评估所有定义的路由,您可能会得到与使用反向路由时不同的结果。
PSR-0
该存储库按组织方式可以将其内容倒入到文件夹 Kunststube/Router/
中,并且命名遵守 PSR-0 规范。
信息
版本:0.2
作者:David Zentgraf
联系:router@kunststube.net
网站:http://kunststube.net,https://github.com/deceze/Kunststube-Router
版本历史
0.2.2
修复了在模式中将分组表达式与通配符一起的固定问题,例如:/foo/(bar|baz):id/*
0.2.1
在无匹配路由的情况下,将抛出的异常更改为特定的NotFoundException。感谢Kévin Gomez(K-Phoen)。
0.2
添加了按请求方法进行路由的API。
0.1
初始版本。
免责声明
代码按原样提供。请随意使用,关于任何内容的保证均无。
目前只是将其发布出来,将来可能适用适当的许可证。