monolyth/reroute

Monolyth无框架的极灵活路由器

5.3.2 2023-02-01 14:13 UTC

README

灵活的PHP5 HTTP路由器,支持各种类型的URL匹配、URL参数、自定义状态处理和URL生成。Reroute旨在在任何类型的项目中使用。

安装

Composer(推荐)

$ composer require monolyth/reroute

手动安装

  1. 获取代码;
    1. 从GitHub等克隆仓库;
    2. 下载ZIP文件(例如从GitHub)并解压。
  2. 让项目识别Reroute
    1. 在PSR-4自动加载器中注册/path/to/reroute/src为命名空间Monolyth\\Reroute\\(推荐);
    2. 或者,手动include所需的文件。

基本用法

响应请求的URL

由于Reroute路由器响应HTTP请求,我们使用when方法来定义一个有效的URL

<?php

use Monolyth\Reroute\Router;

$router = new Router('http://example.com');
$state = $router->when('/some/url/', 'some-state');

when返回一个State新对象作为指定URL的响应,可选地通过给定的名称标识。然后必须定义该状态将响应哪些HTTP动词以及如何响应

<?php

$state->get(new Zend\Diactoros\Response\HtmlResponse('Hello world!'));
$state->post(new Zend\Diactoros\Response\EmptyResponse(500));

目前支持的HTTP动词方法有getpostputdeleteheadoptions。还有一个特殊的any方法,它用一个单独的响应涵盖了它们所有。

响应必须是Psr\Http\Message\ResponseInterface的实例。然而,为了方便起见,你也可以定义其他作为响应的东西

  1. 如果它是一个可调用的,它会使用从URL中提取的参数被调用,直到它不再可调用。
  2. 如果它是一个字符串并且存在一个具有该名称的类,它将是该类的实例。
  3. 如果该类是可调用的,它将被调用,并且结果被使用。
  4. 否则,该类本身必须实现Psr\Http\Message\ResponseInterface

因此,以下形式是等效的

<?php

use Zend\Diactoros\Response\HtmlResponse;

$router->when('/some/url/')->get(function () {
    return new HtmlsReponse('Hello world!');
});
$router->when('/some/url/')->get(new HtmlResponse('Hello world!'));

class Foo
{
    public static function getInstance()
    {
        return new Foo;
    }

    public function __invoke()
    {
        return new HtmlResponse('Hello world!');
    }
}

$router->when('/some/url/')->get(new Foo);
$router->when('/some/url/')->get(Foo::class);
$router->when('/some/url/')->get(['Foo', 'getInstance']);

命名状态

when的第二个参数是(希望是!)状态的名称。这可以在稍后生成路由时使用(见下文)。如果不需要名称,它可以是null(也是默认值)。您还可以使用Router::get在稍后通过名称检索特定的状态(例如,也许您的路由非常复杂且分散在多个文件中)。

<?php

$router->when('/the/url/', 'myname')->get('handler');
$state = $router->get('myname'); // Ok!
$state instanceof Monolyth\Reroute\State; // true

解析请求

定义路由后,在您的入口控制器中的某个地方,您可能希望实际解析请求。然后可以使用例如Zend Diactoros发出ResponseInterface对象。

<?php

use Zend\Diactoros\ServerRequestFactory;
use Zend\Diactoros\Response\SapiEmitter;

if ($response = $router(ServerRequestFactory::fromGlobals())) {
    $emitted = new SapiEmitter;
    $emitter->emit($response);
} else {
    // 404!
}

路由器必须用当前请求作为参数调用。注意,在调用之前,将无法生成路由。如果您需要这样做(例如,在CRON作业中发送电子邮件),您可以手动调用路由器。

调用路由器开始一个管道。通过调用路由器的pipe方法,您可以向堆栈添加中间件。

如果找到了当前URL的有效状态,则管道返回其返回值。否则,它将解析为null

要模拟与实际请求类型不同的请求类型,只需更改$_SERVER['REQUEST_METHOD']

传递参数

您的URL实际上是正则表达式,因此您可以匹配变量并将它们传递到回调中

<?php

$router->when("/(?'name'\w+)/")->get(function (string $name) {
    return "Hi there, $name!";
});

变量可以是命名的(在这种情况下,您将它们传递到回调的顺序无关紧要 - Reroute会在可调用中进行反射以确定最佳匹配)或匿名的(在这种情况下,它们将按顺序传递)。

简写占位符

对于更简单的URL,您还可以使用一些简写占位符。以下三个语句是相同的

<?php

$router->when("/(?'param'.*?)/");
$router->when('/:param/');
$router->when('/{param}/');

当使用占位符时,请注意,对参数类型的控制较少。使用正则表达式更强大,因为您可以强制例如 "/(?'id'\d+)/" 匹配整数,甚至可以在可调用对象中类型提示。

检查当前请求

通过将参数类型提示为 Psr\Http\Message\RequestInterface 的实例,您可以注入原始请求对象并检查使用的方法(或当然任何其他内容)

<?php

use Psr\Http\Message\RequestInterface;
use Zend\Diactoros\Response\HtmlResponse;

$router->when('/some/url/')->any(function (RequestInterface $request) {
    switch ($request->getMethod()) {
        case 'POST':
            // Perform some action
        case 'GET':
            return new HtmlResponse('ok');
        default:
            return new HtmlResponse($request->getMethod()." method not allowed.", 405);
    }
});

引用其他回调

类型提示为 callable 且匹配已定义操作(大写)的参数可用于“链式”到另一个操作。因此,以下模式对于需要对例如 POST 进行特殊处理的 URL 是常见的

<?php

$router->when('/some/url/')->then('my-state', function() {
    return 'This is a normal page';
})->post(function (callable $GET) {
    // Perform some action...
    return $GET;
});

注意不需要重新传递任何 URL 参数给可调用对象;它们会自动注入。因此,对 getpost 等的调用可能会接受/识别不同顺序的不同参数。

自定义动词回调不会“冒泡”到路由链。因此,在 /foo/ 上特别禁用 POST 不会影响 /foo/bar/ 的默认行为。

如果注入的操作在此状态下不可用,则返回 405 错误。

分组

when 的可选第三个参数是一个可调用对象,它期望一个参数:一个新的(子)路由器。使用 when 在子路由器上定义的所有路由都将继承父路由器的 URL

<?php

$router->when('/foo/', null, function ($router) {
    $router->when('/bar/')->get('I match /foo/bar/!');
})->get('I match /foo/!);

分组 when 调用的结果是本身也是一个状态,它可以被管道和/或解析。为了方便,此状态也可以使用 ->when('/', ...) 在回调内部定义。因此,而不是这个(这本身是完全有效的)

<?php

$router->when('/', 'home', function ($router) {
    // Looooong list of subroutes under /...
})->pipe($somePipe)->get('Home!');

...您还可以编写(更易读的)

<?php

$router->when('/', null, function ($router) {
    $router->when('/', 'home')->get('Home!');
    // Looooong list of subroutes under /...
})->pipe($somePipe);

管道中间件

由于状态是管道化的,您可以在任何位置添加一个或多个对 pipe 方法的调用以添加中间件

<?php

$router->when('/restricted/')
    ->pipe(function ($payload) {
        if (!user_is_authenticated()) {
            return new RedirectResponse('/login/');
        }
        return $payload;
    })
    ->get(new Zend\Diactoros\Response\HtmlResponse('For authenticated eyes only!'));

您可以根据需要多次调用 pipe。如果管道在任何地方短路,则子路由器将不会执行。

当使用命名参数时,管道化的可调用对象可以可选地指定它还想要使用哪些参数

<?php

$router->when("/(?'foo':\d+)/")
    ->pipe(function ($payload, int $foo) {
        if ($foo != 42) {
            // return error response or something...
        }
        return $payload;
    });

这与状态解析的可调用对象类似,但有一个 始终 的第一个参数 $payload,并且无法注入 $request

此功能的一个常见用途是为一组路由中的第一个 $language 参数定义管道,并将某些环境变量设置为它的值,以便用于所有底层路由。

$payload 是,根据定义,是 Psr\Http\Message\RequestInterface 的一个实例。一旦任何管道返回 Psr\Http\Message\ResponseInterface 的一个实例,一切都会停止,它被指定为当前状态下此路由的选定响应。此功能的一个常见用途是在用户试图访问页面 A 但需要先在页面 B 上执行某些操作(例如登录)时重定向用户。

生成 URL

要为定义的命名状态生成 URL,请使用 generate 方法

<?php

$router->when('/:some/:params/', 'myname')->get('handler');
echo $router->generate('myname', ['some' => 'foo', 'params' => 'bar']);
// outputs: /foo/bar/

generate 的可选第三个参数是一个布尔值,告诉 generate 如果用户已经在当前主机上,是否应该优先选择没有方案/主机名的路由。默认值为 true。如果第三个参数为 false,上述示例可能输出 https:///foo/bar/。这在生成的路由需要在应用程序外部使用时很有用,例如在发送的电子邮件中。

生成仅适用于命名状态,因为匿名状态显然只能通过其实际 URL(在这种情况下,您可以直接将其硬编码)检索。如果您的 URL 很可能随着时间的推移而改变,请使用命名状态!

级联参数

在子路由器中生成路由时,所有在父路由器中设置的命名参数都会自动注入到传递的参数中。

示例

<?php

use Zend\Diactoros\Response\RedirectResponse;

$router->when("/(?'language'[a-z]{2})/", null, function ($router) {
    $router->when('/', 'home')->get(function () { /* some page */ });
    $router->when('/home/')->get(function () use ($router) {
        return new RedirectResponse($router->generate('home'));
    });
});

// Now, assuming we navigate to `/en/home/` we get redirected to `/en/`!

当然,您也可以将 string $langauge 作为参数注入到子路由中,但这会变得繁琐。

注意,在调用 generate 方法的第二个参数中传入的参数优先级高于之前匹配的参数。这意味着您可以通过以下方式在上述示例中将 /en/home/ 显式重定向到 /nl/$router->generate('home', ['language' => 'nl']);

处理404和其他错误

$router() 的结果如果没有找到匹配项将是 null,这意味着您需要显示404错误页面。最佳实践是将此调用包装在 try/catch 块中,并在出错时抛出异常。这样,您就可以在 catch 块中显示一个通用的500错误页面(并可能进行一些日志记录)。

具有默认参数的路由

在某些情况下,您可以在回调中指定一个“默认参数”很有用。例如,当调用 /user/ 应显示当前登录用户的个人资料,而当调用 /user/:id/ 时显示指定用户的个人资料。

在Reroute中,通过在URL中将参数设为可选并在回调中提供默认值,可以实现这一点。对于上述示例,例如可以这样操作:

<?php

$router->when("/user/(?'id'\d+/)?")->then(function ($id = null) {
    if (!isset($id)) {
        $id = $GLOBALS['user']->id;
    }
    // ...return profile for $id...
    return "<h1>User profile for $id</h1>";
});

可以通过在占位符后加上问号使简写URL匹配样式成为可选的

<?php

$router->when('/user/:id?/');
$router->when('/user/{id}?/');

请注意,任何以斜杠结尾的参数都会被删除,因为斜杠通常保留用于参数分隔。此外,连续的斜杠将被自动合并。

支持的HTTP方法

Reroute支持GET、POST、PUT、DELETE和OPTIONS HTTP方法。每个方法都有一个相应命名的 State 对象上的方法,可以用来定义响应。如果没有定义响应,Reroute将返回一个包含405状态码的 Laminas\Diactoros\Response\EmptyResponse 实例。

然而,有两种特殊情况。如果已定义了GET响应,但没有显式定义POST响应,则GET响应将被用来响应POST操作。通常您应该通过实际执行一些操作并将页面重定向到某处来响应POST,以防止重复提交,但这样至少您的网站不会崩溃。

另一个例外是HTTP HEAD方法。它通过获取GET响应,删除主体,添加内容长度标头,并返回一个状态为200的 Laminas\Diactoros\Response\EmptyResponse 来自动支持。这显然意味着定义了一个GET响应;否则将返回405空响应。